Add support for multiple mods remapping the same item/block/tile entity.#18
Add support for multiple mods remapping the same item/block/tile entity.#18
Conversation
Connor-Colenso
left a comment
There was a problem hiding this comment.
I'm still not entirely sure why this would be needed, it seems to complicate it a bit more than I would like. Couldn't you just register a general handler for a TileEntity or such, then have your own logic inside that to split out different cases, rather than register a bunch of transformers? I feel then the burden of handling transforms is on you rather than whatever order the Collection implementation decides to iterate over (it is not guaranteed to be insertion order, to my knowledge).
This PR would also introduce concerns about weighting of transformers, which does make me feel like if a transformer is present in the transformer map already, we should explicitly kill the client, rather than let you silently override.
Feel free to DM me on discord if easier, I don't often see GitHub notifications.
|
I have created #19 in response to some issues this PR raised. Though most of it may be irrelevant depending on the outcome of this. |
For the public record, yes, there are situations where you do need multiple handlers to target one id: when a meta-item type is removed or depreciated, and some metas have to be replaced with different things from different mods. This issue has already arisen thrice, CropsNH (the IC2 crop replacement mod):
For point 3 consider this:
|
|
Since I'm still working on "phase 3" of this upgrade, I'll mark this PR as a draft for now so it can be reviewed in full once it's done. |
|
I'm still updating the read me docs for this PR to better tutorialize postea usage, but most of the code should be locked in at this point. |
|
Not included in this PR is an example file that contains functional versions of all the examples linked in the readme. Here's a copy for posterity: package com.gtnewhorizons.postea;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.oredict.OreDictionary;
import com.gtnewhorizons.postea.api.BlockAccessCompat;
import com.gtnewhorizons.postea.api.BlockReplacementManager;
import com.gtnewhorizons.postea.api.IDExtenderCompat;
import com.gtnewhorizons.postea.api.ItemStackReplacementManager;
import com.gtnewhorizons.postea.api.TileEntityReplacementManager;
import com.gtnewhorizons.postea.utility.BlockConversionInfo;
import com.gtnewhorizons.postea.utility.BlockInfo;
import cpw.mods.fml.common.registry.GameRegistry;
abstract class ExampleMigrators {
static void postLoad() {
TEToBlockExample.postLoad();
TEModifyExample.postLoad();
TEFullReplacementExample.postLoad();
SimpleTransformationExample.postLoad();
ComplexBlockTransformer.postLoad();
ComplexItemTransformer.postLoad();
FMLReMappingExample.postLoad();
}
public static abstract class TEToBlockExample {
public static void postLoad() {
// it's with great shame that we do admit that gregtech ores used to be tile entities.
TileEntityReplacementManager.tileEntityTransformer("GT_TileEntity_Ores", (tag, world, chunk) -> {
if (world.provider.dimensionId == -1) {
// nether gets orange wool
return new BlockInfo(Blocks.wool, 1);
}
if (world.provider.dimensionId == 1) {
// the end gets magenta wool
return new BlockInfo(Blocks.wool, 2);
}
// all other dimensions get light blue wool
return new BlockInfo(Blocks.wool, 3);
});
}
}
public static abstract class TEModifyExample {
private static int reedsId = -1;
private static int diamondId = -1;
private static int stoneId = -1;
private static int record11Id = -1;
public static void postLoad() {
// pre caching ids like this will lead to faster transformers
ItemStackReplacementManager.registerIDResolver("minecraft:reeds", i -> reedsId = i);
ItemStackReplacementManager.registerIDResolver("minecraft:diamond", i -> diamondId = i);
ItemStackReplacementManager.registerIDResolver("minecraft:stone", i -> stoneId = i);
TileEntityReplacementManager.tileEntityTransformer("Chest", (tag, world, chunk) -> {
// This transformer replaces all sugar cane stacks inside a chest with
// stone, unless it's in the 13th slot at which point it's replaced with
// diamonds. All transformations are done without affecting the stack size.
NBTTagList slots = tag.getTagList("Items", 10);
for (int i = 0; i < slots.tagCount(); i++) {
NBTTagCompound slot = slots.getCompoundTagAt(i);
// only target the removed item
if (IDExtenderCompat.getItemStackID(slot) != reedsId) continue;
if (slot.getByte("Slot") == 13) {
// replace it with diamonds if it's in slot 13
IDExtenderCompat.setItemStackID(slot, diamondId);
} else {
// else replace it with stone.
IDExtenderCompat.setItemStackID(slot, stoneId);
}
}
return null;
});
// this tile entity transformer also executes because the other transformer returned null
ItemStackReplacementManager.registerIDResolver("minecraft:record_11", i -> record11Id = i);
byte targetSLot = (byte) 12;
TileEntityReplacementManager.tileEntityTransformer("Chest", (tag, world, chunk) -> {
// This transformer looks for the 12th slot of the chest and replaces
// or sets it's content to a new stack of record 11 with a stack size of 1.
NBTTagList slots = tag.getTagList("Items", 10);
NBTTagCompound toEdit = null;
for (int i = 0; i < slots.tagCount(); i++) {
NBTTagCompound slot = slots.getCompoundTagAt(i);
if (slot.getByte("Slot") != targetSLot) continue;
toEdit = slot;
break;
}
if (toEdit == null) {
toEdit = new NBTTagCompound();
toEdit.setByte("Slot", targetSLot);
slots.appendTag(toEdit);
}
toEdit.setByte("Count", (byte) 1);
toEdit.setShort("Damage", (short) 0);
IDExtenderCompat.setItemStackID(toEdit, record11Id);
return null;
});
}
}
public static abstract class TEFullReplacementExample {
public static int litFurnaceId = -1;
public static int unlitFurnaceId = -1;
public static int fireChargeId = -1;
public static void postLoad() {
BlockReplacementManager.registerIDResolver("minecraft:lit_furnace", i -> litFurnaceId = i);
BlockReplacementManager.registerIDResolver("minecraft:furnace", i -> unlitFurnaceId = i);
ItemStackReplacementManager.registerIDResolver("minecraft:fire_charge", i -> fireChargeId = i);
TileEntityReplacementManager.tileEntityTransformer("Furnace", TEFullReplacementExample::transformFurnace);
}
/**
* This transformer transforms a furnace into a chest, and adds a fire charge in the 4th slot of the chest if
* the block where the tile entity once stood was a lit furnace. If it's not either a lit or unlit furnace,
* the block is simply transformed into air.
*
* @param tag The NBT data of the tile entity being transformed.
* @param world The world in which the tile entity is located.
* @param chunk The chunk in which the tile entity is located.
* @return Null if we didn't update the block, else a BlockInfo with the transformation data you wish to
* execute.
*/
private static BlockInfo transformFurnace(NBTTagCompound tag, World world, Chunk chunk) {
int[] blockIdAndMeta = BlockAccessCompat.getBlockIDAndMetaAtTE(tag, chunk);
int blockId = blockIdAndMeta[0];
int meta = blockIdAndMeta[1];
// we return a value this time because we are actually replacing the block and tile entity.
// we copy the meta value since that preserves the orientation for these blocks.
return new BlockInfo(Blocks.chest, meta, oldTag -> transformFurnaceNBT(oldTag, blockId));
}
/**
* Moves the content of the furnace to the chest and adds a fire charge in the 4th slot if the furnace was lit.
*
* @param oldTag The current NBT data ofthe tile entity being transformed.
* @return null if we want the tile entity to be erased.
*/
private static NBTTagCompound transformFurnaceNBT(NBTTagCompound oldTag, int blockId) {
// if the block wasn't a furnace, clear the current tile entity and let the chest spawn a new one.
if (blockId != litFurnaceId && blockId != unlitFurnaceId) {
return null;
}
// use this function to create a new tile entity tag with the same positional data as the old one.
NBTTagCompound newTag = TileEntityReplacementManager.createTETagAtSamePosition("Chest", oldTag);
NBTTagList newSlots = (NBTTagList) oldTag.getTagList("Items", 10)
.copy();
// if the block used to be a lit furnace add a fire charge in the 4th slot.
if (blockId == litFurnaceId) {
NBTTagCompound fireChargeStack = new NBTTagCompound();
IDExtenderCompat.setItemStackID(fireChargeStack, fireChargeId);
fireChargeStack.setByte("Slot", (byte) 4);
fireChargeStack.setByte("Count", (byte) 1);
fireChargeStack.setShort("Damage", (short) 0);
newSlots.appendTag(fireChargeStack);
}
newTag.setTag("Items", newSlots);
return newTag;
}
}
public static abstract class SimpleTransformationExample {
public static void postLoad() {
// item/block -> item/block, preserving the Meta/Damage value.
//
// remap all block and items of colored stone blocks with wool of the same color
BlockReplacementManager.addSimpleReplacement("ExtraUtilities:colorStoneBrick", Blocks.wool);
// Item/Block with meta -> Item/Block, meta preserved
//
// angel ring:3 to disk 11
ItemStackReplacementManager
.addSimpleReplacement("ExtraUtilities:angelRing", 3, new ItemStack(Items.record_11));
ItemStackReplacementManager
.addSimpleReplacement("ExtraUtilities:angelRing", 4, new ItemStack(Items.record_13));
// Item/Block with meta -> Item/Block with specific meta
//
// remap all compressed cobble to oak planks
//
// When using a stack as a target the passed stack size is ignored, only the contained item and meta value
// are kept.
ItemStackReplacementManager
.addSimpleReplacement("ExtraUtilities:cobblestone_compressed", 0, new ItemStack(Blocks.planks, 0, 0));
// block only with meta -> block with specific meta value.
//
// remap blocks of double compressed cobble to cobble.
BlockReplacementManager
.addSimpleReplacement("ExtraUtilities:cobblestone_compressed", 1, Blocks.cobblestone, 0, true);
// item only with damage -> item only with specific damage value.
//
// remap stacks of double compressed cobble to oak saplings.
ItemStackReplacementManager.addSimpleReplacement(
"ExtraUtilities:cobblestone_compressed",
1,
new ItemStack(Blocks.sapling, 1, 0),
true);
// Block/Item with any Meta/Damage -> Block/Item with specific Meta/Damage value.
//
// Using OreDictionary.WILDCARD_VALUE will tell postea to use this mapping as a fallback when there is no
// transformer targeting the specific meta/damage value of the block/item.
//
// remap all other instances of compressed blocks to endstone
BlockReplacementManager.addSimpleReplacement(
"ExtraUtilities:cobblestone_compressed",
OreDictionary.WILDCARD_VALUE,
Blocks.end_stone,
0);
// Block/Item with any Meta/Damage -> Block/Item, preserving the Meta/Damage value.
//
// When passing OreDictionary.WILDCARD_VALUE as the target Meta/Damage value, Postea will preserve any
// existing
// Meta/Damage value. This is also the default value when you don't specify a target meta value.
//
// remap all other instances of compressed blocks to endstone
BlockReplacementManager.addSimpleReplacement(
"ExtraUtilities:cobblestone_compressed",
OreDictionary.WILDCARD_VALUE,
Blocks.end_stone,
OreDictionary.WILDCARD_VALUE);
}
}
public static abstract class ComplexBlockTransformer {
private static int woolId = -1;
public static void postLoad() {
// This transformer replaces all grass blocks with even x-coordinates with orange wool.
BlockReplacementManager.registerIDResolver("minecraft:wool", i -> woolId = i);
BlockReplacementManager
.addTransformationHandler("minecraft:grass", ComplexBlockTransformer::transformGrass);
}
private static boolean transformGrass(BlockConversionInfo info) {
// only affect blocks that are on even x coordinates
if (info.x % 2 != 0) return false;
// transform all these blocks into orange wool.
info.blockID = woolId;
info.metadata = 1;
return true;
}
}
public static abstract class ComplexItemTransformer {
private static int writableBookId = -1;
private static int woolId = -1;
private static void postLoad() {
ItemStackReplacementManager.registerIDResolver("minecraft:writable_book", i -> writableBookId = i);
ItemStackReplacementManager.registerIDResolver("minecraft:wool", i -> woolId = i);
ItemStackReplacementManager
.addTransformationHandler("minecraft:written_book", ComplexItemTransformer::unsignBook);
}
/**
* This transformer transforms a written book into either an unsigned book or orange wool depending on it's
* title.
*
* @param name The name of the item being transformed, even if that item doesn't exist anymore.
* @param nbt The NBT data of the stack item being transformed.
* @return True if we've actually transformed the item.
*/
private static boolean unsignBook(String name, NBTTagCompound nbt) {
// check if the book has a title.
if (!nbt.hasKey("tag", 10)) return false;
NBTTagCompound tag = nbt.getCompoundTag("tag");
if (!tag.hasKey("title", 8)) return false;
String title = tag.getString("title");
if (title.equalsIgnoreCase("unsign me")) {
// unsign the book
IDExtenderCompat.setItemStackID(nbt, writableBookId);
nbt.setShort("Damage", (short) 0);
nbt.setByte("Count", (byte) 1);
// clear the signed book nbt
tag.removeTag("title");
tag.removeTag("author");
return true;
} else if (title.equalsIgnoreCase("orange wool")) {
// Transform it into orange wool.
IDExtenderCompat.setItemStackID(nbt, woolId);
nbt.setShort("Damage", (short) 1);
nbt.setByte("Count", (byte) 1);
// removes the NBT tag from the stack.
nbt.removeTag("tag");
return true;
}
return false;
}
}
public static abstract class FMLReMappingExample {
// You have to make your own block class since the block constructor is protected.
public static class MyBlock extends Block {
protected MyBlock() {
super(Material.ground);
}
}
public static void postLoad() {
// this only works when the thing you're replacing with has never been registered to a world's
// id map. If you try to run this with a block that already exists (eg good old dirt), FML error out and
// you that your world is corrupted beyond repair, and advise you to undo what ever you just did.
//
// Postea Simple Transformers don't have that issue and are more lag friendly in the long term. Consider
// using them when ever possible, instead of FML mappings.
Item item = new Item();
GameRegistry.registerItem(item, "my_item");
ItemStackReplacementManager.replaceMissingMapping("IC2:itemFertilizer", item);
Block block = new MyBlock();
GameRegistry.registerBlock(block, "my_block");
BlockReplacementManager.replaceMissingMapping("IC2:blockCrop", block);
}
}
public static abstract class FMLIgnoreMissingMappingExample {
public static void postLoad() {
// It doesn't really matter which one you use, they both do the same thing under the hood.
ItemStackReplacementManager.ignoreMissingMapping("IC2:itemFertilizer");
BlockReplacementManager.ignoreMissingMapping("IC2:blockCrop");
}
}
} |
will publish a pre-tag and create integration PRs shortly
|
Created sister PRs in GTNH Core Mod, GT5u and Witching Gadgets; they are linked in the top post for easier tracking. |
|
For anyone who wants to do some perf testing, here is a script that adds roughly 4.2k handlers, it transforms all items and blocks from GT (and gt-related mods) to dirt, and replaces any I successfully loaded up my Gate World (along with all 3 sister PRs) into Daily 351, and it ran smoothly. package com.gtnewhorizons.postea;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
import net.minecraft.item.Item;
import com.gtnewhorizons.postea.api.BlockReplacementManager;
import com.gtnewhorizons.postea.api.ItemStackReplacementManager;
import com.gtnewhorizons.postea.api.TileEntityReplacementManager;
import com.gtnewhorizons.postea.utility.BlockInfo;
import cpw.mods.fml.common.registry.GameRegistry;
abstract class PackTest {
static void postLoad() {
Item dirtItem = Item.getItemFromBlock(Blocks.dirt);
final BlockInfo cobble = new BlockInfo(Blocks.cobblestone, 0);
TileEntityReplacementManager.tileEntityTransformer("BaseMetaTileEntity", (nbt, world, chunk) -> cobble);
Set<String> gtModIds = new HashSet<>();
gtModIds.add("bartworks");
gtModIds.add("berriespp");
gtModIds.add("dreamcraft");
gtModIds.add("ggfab");
gtModIds.add("GoodGenerator");
gtModIds.add("gregtech");
gtModIds.add("gtneioreplugin");
gtModIds.add("gtnhintergalactic");
gtModIds.add("gtnhlanth");
gtModIds.add("kekztech");
gtModIds.add("kubatech");
gtModIds.add("matter-manipulator");
gtModIds.add("miscutils");
gtModIds.add("tectech");
gtModIds.add("ThaumicHorizons");
gtModIds.add("ToxicEverglades");
gtModIds.add("GalaxySpace");
gtModIds.add("GraviSuite");
gtModIds.add("hydroenergy");
for (Object b : Block.blockRegistry) {
if (!(b instanceof Block block)) continue;
GameRegistry.UniqueIdentifier uuid = GameRegistry.findUniqueIdentifierFor(block);
if (uuid == null || !gtModIds.contains(uuid.modId)) continue;
BlockReplacementManager.addSimpleReplacement(uuid.toString(), Blocks.dirt, 0, true);
}
for (Object i : Item.itemRegistry) {
if (!(i instanceof Item item)) continue;
GameRegistry.UniqueIdentifier uuid = GameRegistry.findUniqueIdentifierFor(item);
if (uuid == null || !gtModIds.contains(uuid.modId)) continue;
ItemStackReplacementManager.addSimpleReplacement(uuid.toString(), dirtItem, 0, true);
}
}
} |
While working on the migrations for CropsNH, I found out that Postea can't register more than one handler per item/block/tile entity type.
This is already an issue for CropsNH, as it is supposed to depreciate Crops++, and that mod has an item that will need to be split between CropsNH and the GTNHCoreMod.
The current revision of this PR is intended to maximize compatibility and, therefore, makes a few assumptions about the existing handlers:
Some handlers will need to be updated to properly follow null = failed match behaviour:
UPDATE:
This PR has ended up adding a couple more features and optimizing a lot of the existing postea code:
The read-me has been updated with examples of all use cases for postea (and therefore all the systems I added), so feel free to give it a look.
Sister PRs :
GTNH Core Mod: GTNewHorizons/NewHorizonsCoreMod#1591
GT5u: GTNewHorizons/GT5-Unofficial#5806
Witching Gadgets: GTNewHorizons/WitchingGadgets#115