diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/AxeItemMixin.java b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/AxeItemMixin.java index 76cc3cb..777ff3b 100644 --- a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/AxeItemMixin.java +++ b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/AxeItemMixin.java @@ -2,21 +2,16 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import dev.spiritstudios.specter.api.block.BlockMetatags; import dev.spiritstudios.specter.impl.block.SpecterBlock; import net.minecraft.block.Block; import net.minecraft.block.BlockState; -import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.AxeItem; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Map; import java.util.Optional; @@ -25,19 +20,25 @@ public class AxeItemMixin { @Shadow @Final - public static Map STRIPPED_BLOCKS; + protected static Map STRIPPED_BLOCKS; @WrapOperation(method = "tryStrip", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/AxeItem;getStrippedState(Lnet/minecraft/block/BlockState;)Ljava/util/Optional;")) private Optional getStrippedState(AxeItem instance, BlockState state, Operation> original) { - Optional strippedBlock = BlockMetatags.STRIPPABLE.get(state.getBlock()); - if (strippedBlock.isEmpty()) strippedBlock = Optional.ofNullable(STRIPPED_BLOCKS.get(state.getBlock())); - - return strippedBlock.map(block -> block.getStateWithProperties(state)); + return Optional.ofNullable(STRIPPED_BLOCKS.get(state.getBlock())) + .or(() -> BlockMetatags.STRIPPABLE.get(state.getBlock())) + .map(block -> block.getStateWithProperties(state)) + .or(() -> original.call(instance, state)); } - @Inject(method = "tryStrip", at = @At(value = "INVOKE", target = "Ljava/util/Optional;ofNullable(Ljava/lang/Object;)Ljava/util/Optional;"), cancellable = true) - private void tryStrip(World world, BlockPos pos, @Nullable PlayerEntity player, BlockState state, CallbackInfoReturnable> cir) { - Optional unwaxedBlockState = Optional.ofNullable(SpecterBlock.WAXED_TO_UNWAXED_BLOCKS.get(state.getBlock())).map(unwaxed -> unwaxed.getStateWithProperties(state)); - if (unwaxedBlockState.isPresent()) cir.setReturnValue(unwaxedBlockState); + @WrapOperation(method = "tryStrip", at = @At(value = "INVOKE", target = "Ljava/util/Optional;ofNullable(Ljava/lang/Object;)Ljava/util/Optional;")) + private Optional tryStrip( + Object value, + Operation> original, + @Local(argsOnly = true) BlockState state + ) { + Optional unwaxedBlock = Optional.ofNullable(SpecterBlock.WAXED_TO_UNWAXED_BLOCKS.get(state.getBlock())) + .map(block -> block.getStateWithProperties(state)); + + return unwaxedBlock.or(() -> original.call(value)); } } diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/FireBlockMixin.java b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/FireBlockMixin.java index 2d3fa04..ead0b1b 100644 --- a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/FireBlockMixin.java +++ b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/FireBlockMixin.java @@ -1,27 +1,40 @@ package dev.spiritstudios.specter.mixin.block; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import dev.spiritstudios.specter.api.block.BlockMetatags; import dev.spiritstudios.specter.api.block.FlammableBlockData; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.FireBlock; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.Optional; @Mixin(FireBlock.class) public class FireBlockMixin { - @Inject(method = "getBurnChance(Lnet/minecraft/block/BlockState;)I", at = @At("HEAD"), cancellable = true) - private void getBurnChanceFromMetatag(BlockState state, CallbackInfoReturnable cir) { - Optional data = BlockMetatags.FLAMMABLE.get(state.getBlock()); - data.ifPresent(flammableBlockData -> cir.setReturnValue(flammableBlockData.burn())); + @WrapOperation(method = "getBurnChance(Lnet/minecraft/block/BlockState;)I", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/objects/Object2IntMap;getInt(Ljava/lang/Object;)I", remap = false)) + private int getBurnChanceFromMetatag( + Object2IntMap instance, + Object value, + Operation original, + @Local(argsOnly = true) BlockState state + ) { + return BlockMetatags.FLAMMABLE.get((state).getBlock()) + .map(FlammableBlockData::burn) + .orElse(original.call(instance, value)); } - @Inject(method = "getSpreadChance(Lnet/minecraft/block/BlockState;)I", at = @At("HEAD"), cancellable = true) - private void getSpreadChanceFromMetatag(BlockState state, CallbackInfoReturnable cir) { - Optional data = BlockMetatags.FLAMMABLE.get(state.getBlock()); - data.ifPresent(flammableBlockData -> cir.setReturnValue(flammableBlockData.spread())); + @WrapOperation(method = "getSpreadChance(Lnet/minecraft/block/BlockState;)I", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/objects/Object2IntMap;getInt(Ljava/lang/Object;)I", remap = false)) + private int getSpreadChanceFromMetatag( + Object2IntMap instance, + Object value, + Operation original, + @Local(argsOnly = true) BlockState state + ) { + return BlockMetatags.FLAMMABLE.get((state).getBlock()) + .map(FlammableBlockData::spread) + .orElse(original.call(instance, value)); } } diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/HoneycombItemMixin.java b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/HoneycombItemMixin.java index f681a12..788cf12 100644 --- a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/HoneycombItemMixin.java +++ b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/HoneycombItemMixin.java @@ -1,20 +1,26 @@ package dev.spiritstudios.specter.mixin.block; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import dev.spiritstudios.specter.impl.block.SpecterBlock; import net.minecraft.block.BlockState; import net.minecraft.item.HoneycombItem; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Optional; @Mixin(HoneycombItem.class) public class HoneycombItemMixin { - @Inject(method = "getWaxedState", at = @At("HEAD"), cancellable = true) - private static void getWaxedState(BlockState state, CallbackInfoReturnable> cir) { - Optional waxedBlockState = Optional.ofNullable(SpecterBlock.UNWAXED_TO_WAXED_BLOCKS.get(state.getBlock())).map(block -> block.getStateWithProperties(state)); - if (waxedBlockState.isPresent()) cir.setReturnValue(waxedBlockState); + @WrapOperation(method = "getWaxedState", at = @At(value = "INVOKE", target = "Ljava/util/Optional;ofNullable(Ljava/lang/Object;)Ljava/util/Optional;")) + private static Optional getWaxedState( + Object value, + Operation> original, + @Local(argsOnly = true) BlockState state + ) { + Optional waxedBlockState = Optional.ofNullable(SpecterBlock.UNWAXED_TO_WAXED_BLOCKS.get(state.getBlock())); + + return waxedBlockState.or(() -> original.call(value)); } } diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/OxidizableMixin.java b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/OxidizableMixin.java index 165de11..c652e2d 100644 --- a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/OxidizableMixin.java +++ b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/OxidizableMixin.java @@ -3,36 +3,41 @@ import com.google.common.collect.BiMap; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import dev.spiritstudios.specter.impl.block.SpecterBlock; import net.minecraft.block.Block; import net.minecraft.block.Oxidizable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Optional; @Mixin(Oxidizable.class) public interface OxidizableMixin { - @Inject(method = "getIncreasedOxidationBlock", at = @At("HEAD"), cancellable = true) - private static void getIncreasedOxidationBlock(Block block, CallbackInfoReturnable> cir) { - Optional increasedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_INCREASES.get(block)); - if (increasedOxidationBlock.isPresent()) cir.setReturnValue(increasedOxidationBlock); + @WrapOperation(method = "getIncreasedOxidationBlock", at = @At(value = "INVOKE", target = "Ljava/util/Optional;ofNullable(Ljava/lang/Object;)Ljava/util/Optional;")) + private static Optional getIncreasedOxidationBlock( + Object value, + Operation> original, + @Local(argsOnly = true) Block block + ) { + Optional increasedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_INCREASES.get(block)); + return increasedOxidationBlock.or(() -> original.call(value)); } - @Inject(method = "getDecreasedOxidationBlock", at = @At("HEAD"), cancellable = true) - private static void getDecreasedOxidationBlock(Block block, CallbackInfoReturnable> cir) { - Optional decreasedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_DECREASES.get(block)); - if (decreasedOxidationBlock.isPresent()) cir.setReturnValue(decreasedOxidationBlock); + @WrapOperation(method = "getDecreasedOxidationBlock", at = @At(value = "INVOKE", target = "Ljava/util/Optional;ofNullable(Ljava/lang/Object;)Ljava/util/Optional;")) + private static Optional getDecreasedOxidationBlock( + Object value, + Operation> original, + @Local(argsOnly = true) Block block + ) { + Optional decreasedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_DECREASES.get(block)); + return decreasedOxidationBlock.or(() -> original.call(value)); } - @SuppressWarnings("rawtypes") @WrapOperation(method = "getUnaffectedOxidationBlock", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/BiMap;get(Ljava/lang/Object;)Ljava/lang/Object;", remap = false)) - private static Object getUnaffectedOxidationBlock(BiMap instance, Object o, Operation original) { - Block block = (Block) o; - Optional unaffectedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_DECREASES.get(block)); - return unaffectedOxidationBlock.orElseGet(() -> (Block) original.call(instance, o)); + private static Object getUnaffectedOxidationBlock(BiMap instance, Object value, Operation original) { + Optional unaffectedOxidationBlock = Optional.ofNullable(SpecterBlock.OXIDATION_LEVEL_DECREASES.get((Block) value)); + return unaffectedOxidationBlock.orElseGet(() -> original.call(instance, value)); } } diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/ShovelItemMixin.java b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/ShovelItemMixin.java index 1c570b2..8b5551c 100644 --- a/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/ShovelItemMixin.java +++ b/specter-block/src/main/java/dev/spiritstudios/specter/mixin/block/ShovelItemMixin.java @@ -4,13 +4,11 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import dev.spiritstudios.specter.api.block.BlockMetatags; import net.minecraft.block.Block; -import net.minecraft.block.BlockState; import net.minecraft.item.ShovelItem; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import java.util.Map; -import java.util.Optional; @Mixin(ShovelItem.class) public class ShovelItemMixin { @@ -19,7 +17,8 @@ public class ShovelItemMixin { private V get(Map instance, Object o, Operation original) { if (!(o instanceof Block block)) return original.call(instance, o); - Optional flattenedBlock = BlockMetatags.FLATTENABLE.get(block); - return flattenedBlock.map(blockState -> (V) blockState).orElseGet(() -> original.call(instance, o)); + return BlockMetatags.FLATTENABLE.get(block) + .map(blockState -> (V) blockState) + .orElseGet(() -> original.call(instance, o)); } } diff --git a/specter-block/src/testmod/java/dev/spiritstudios/testmod/SpecterBlockGameTest.java b/specter-block/src/testmod/java/dev/spiritstudios/testmod/SpecterBlockGameTest.java index 03cec61..8ca1668 100644 --- a/specter-block/src/testmod/java/dev/spiritstudios/testmod/SpecterBlockGameTest.java +++ b/specter-block/src/testmod/java/dev/spiritstudios/testmod/SpecterBlockGameTest.java @@ -74,5 +74,4 @@ public void testOxidizableMetatag(TestContext context) { context.complete(); } - } diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java index 508232e..2b8b599 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java @@ -34,6 +34,7 @@ public void testTomlConfig(TestContext context) throws IOException { context.assertTrue(TestConfig.TOML_HOLDER.load(), "Config file failed to load"); context.assertTrue(Files.exists(path), "Config file does not exist"); context.assertTrue(TestConfig.TOML_HOLDER.get().testString.get().equals("test2"), "String is not equal to test2, Make sure you haven't modified the config"); + Files.deleteIfExists(path); context.complete(); } @@ -59,6 +60,7 @@ public void testJsonCConfig(TestContext context) throws IOException { context.assertTrue(TestConfig.JSON_HOLDER.load(), "Config file failed to load"); context.assertTrue(Files.exists(path), "Config file does not exist"); context.assertTrue(TestConfig.JSON_HOLDER.get().testString.get().equals("test2"), "String is not equal to test2, Make sure you haven't modified the config"); + Files.deleteIfExists(path); context.complete(); } diff --git a/specter-core/src/client/java/dev/spiritstudios/specter/mixin/core/client/RunArgsNetworkMixin.java b/specter-core/src/client/java/dev/spiritstudios/specter/mixin/core/client/RunArgsNetworkMixin.java index 491aeed..b4678da 100644 --- a/specter-core/src/client/java/dev/spiritstudios/specter/mixin/core/client/RunArgsNetworkMixin.java +++ b/specter-core/src/client/java/dev/spiritstudios/specter/mixin/core/client/RunArgsNetworkMixin.java @@ -23,7 +23,7 @@ public class RunArgsNetworkMixin { @Mutable public Session session; - @Inject(method = "", at = @At("TAIL")) + @Inject(method = "", at = @At("RETURN")) private void init(Session session, PropertyMap userProperties, PropertyMap profileProperties, Proxy proxy, CallbackInfo ci) { if (!SpecterGlobals.DEBUG) return; @@ -35,7 +35,7 @@ private void init(Session session, PropertyMap userProperties, PropertyMap profi String username = System.getProperty("specter.development.username"); UUID uuid = UndashedUuid.fromString(System.getProperty("specter.development.uuid").replace("-", "")); - SpecterGlobals.LOGGER.info(String.format("Using development account %s (%s)", username, uuid)); + SpecterGlobals.LOGGER.info("Using development account {} ({})", username, uuid); this.session = new Session( username, uuid, diff --git a/specter-debug/build.gradle.kts b/specter-debug/build.gradle.kts index f9648e2..51d64ae 100644 --- a/specter-debug/build.gradle.kts +++ b/specter-debug/build.gradle.kts @@ -1 +1,9 @@ -moduleDependencies(project, "specter-core", "specter-registry", "specter-item", "specter-block") +moduleDependencies( + project, + "specter-core", + "specter-registry", + "specter-item", + "specter-block", + "specter-serialization", + "specter-render" +) diff --git a/specter-debug/src/client/java/dev/spiritstudios/specter/impl/debug/SpecterDebugClient.java b/specter-debug/src/client/java/dev/spiritstudios/specter/impl/debug/SpecterDebugClient.java index b916768..bc9cb4f 100644 --- a/specter-debug/src/client/java/dev/spiritstudios/specter/impl/debug/SpecterDebugClient.java +++ b/specter-debug/src/client/java/dev/spiritstudios/specter/impl/debug/SpecterDebugClient.java @@ -2,27 +2,19 @@ import dev.spiritstudios.specter.api.block.BlockMetatags; import dev.spiritstudios.specter.api.item.ItemMetatags; +import dev.spiritstudios.specter.api.render.RenderMetatags; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; import net.minecraft.block.Block; import net.minecraft.item.tooltip.TooltipType; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; import java.util.List; public class SpecterDebugClient implements ClientModInitializer { - private static Text getMetatagHeader(String key) { - return Text.translatable("tooltip.metatag." + key).formatted(Formatting.DARK_PURPLE); - } - - private static Text getMetatagText(String key, Object... values) { - return Text.translatable("tooltip.metatag." + key + ".text", values).formatted(Formatting.BLUE); - } - private static void addMetatagLine(List lines, String key, Object... values) { - lines.add(getMetatagHeader(key)); - lines.add(getMetatagText(key, values)); + lines.add(Text.translatable("tooltip.metatag." + key)); + lines.add(Text.translatable("tooltip.metatag." + key + ".text", values)); } @Override @@ -40,6 +32,8 @@ public void onInitializeClient() { BlockMetatags.OXIDIZABLE.get(block).ifPresent(entry -> addMetatagLine(lines, "oxidizable", entry.getName())); BlockMetatags.STRIPPABLE.get(block).ifPresent(entry -> addMetatagLine(lines, "strippable", entry.getName())); BlockMetatags.WAXABLE.get(block).ifPresent(entry -> addMetatagLine(lines, "waxable", entry.getName())); + + RenderMetatags.RENDER_LAYER.get(block).ifPresent(entry -> addMetatagLine(lines, "render_layer", entry.asString())); }); } } diff --git a/specter-debug/src/main/resources/assets/specter-debug/lang/en_us.json b/specter-debug/src/main/resources/assets/specter-debug/lang/en_us.json index b42be49..dd285b3 100644 --- a/specter-debug/src/main/resources/assets/specter-debug/lang/en_us.json +++ b/specter-debug/src/main/resources/assets/specter-debug/lang/en_us.json @@ -7,18 +7,94 @@ "commands.heal.not_living": "Cannot heal non-living entity", "commands.heal.success": "Healed %s for %s health", "commands.components.no_components": "No components found", - "tooltip.metatag.flammable": "Flammable:", - "tooltip.metatag.flammable.text": "%s burn chance, %s spread chance", - "tooltip.metatag.flattenable": "Flattens to:", - "tooltip.metatag.flattenable.text": "%s", - "tooltip.metatag.oxidizable": "Oxidizes to:", - "tooltip.metatag.oxidizable.text": "%s", - "tooltip.metatag.strippable": "Strips to:", - "tooltip.metatag.strippable.text": "%s", - "tooltip.metatag.waxable": "Waxes to:", - "tooltip.metatag.waxable.text": "%s", - "tooltip.metatag.fuel": "Fuel burns for:", - "tooltip.metatag.fuel.text": "%s ticks", - "tooltip.metatag.composting_chance": "Composting chance:", - "tooltip.metatag.composting_chance.text": "%s%%" + "tooltip.metatag.flammable": { + "text": "Flammable:", + "color": "dark_purple" + }, + "tooltip.metatag.flammable.text": [ + { + "index": 0, + "color": "blue" + }, + { + "text": " burn chance, ", + "color": "blue" + }, + { + "index": 1, + "color": "blue" + }, + { + "text": " spread chance", + "color": "blue" + } + ], + "tooltip.metatag.flattenable": { + "text": "Flattens to:", + "color": "dark_purple" + }, + "tooltip.metatag.flattenable.text": { + "index": 0, + "color": "blue" + }, + "tooltip.metatag.oxidizable": { + "text": "Oxidizes to:", + "color": "dark_purple" + }, + "tooltip.metatag.oxidizable.text": { + "index": 0, + "color": "blue" + }, + "tooltip.metatag.strippable": { + "text": "Strips to:", + "color": "dark_purple" + }, + "tooltip.metatag.strippable.text": { + "index": 0, + "color": "blue" + }, + "tooltip.metatag.waxable": { + "text": "Waxes to:", + "color": "dark_purple" + }, + "tooltip.metatag.waxable.text": { + "index": 0, + "color": "blue" + }, + "tooltip.metatag.fuel": { + "text": "Fuel burns for:", + "color": "dark_purple" + }, + "tooltip.metatag.fuel.text": [ + { + "index": 0, + "color": "blue" + }, + { + "text": " ticks", + "color": "blue" + } + ], + "tooltip.metatag.composting_chance": { + "text": "Composting chance:", + "color": "dark_purple" + }, + "tooltip.metatag.composting_chance.text": [ + { + "index": 0, + "color": "blue" + }, + { + "text": "%", + "color": "blue" + } + ], + "tooltip.metatag.render_layer": { + "text": "Render Layer:", + "color": "dark_purple" + }, + "tooltip.metatag.render_layer.text": { + "index": 0, + "color": "blue" + } } diff --git a/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java b/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java index 38f5275..1f060c1 100644 --- a/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java +++ b/specter-entity/src/main/java/dev/spiritstudios/specter/mixin/entity/DefaultAttributeRegistryMixin.java @@ -1,10 +1,13 @@ package dev.spiritstudios.specter.mixin.entity; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import dev.spiritstudios.specter.api.entity.EntityMetatags; import dev.spiritstudios.specter.impl.entity.DataDefaultAttributeBuilder; import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; import net.minecraft.entity.attribute.DefaultAttributeContainer; import net.minecraft.entity.attribute.DefaultAttributeRegistry; import org.spongepowered.asm.mixin.Mixin; @@ -16,25 +19,21 @@ @Mixin(DefaultAttributeRegistry.class) public class DefaultAttributeRegistryMixin { - @SuppressWarnings("unchecked") - @WrapOperation(method = "get", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;")) - private static V get(Map instance, Object o, Operation original) { - if (!(o instanceof EntityType entityType)) return original.call(instance, o); - - Optional attributeBuilder = EntityMetatags.DEFAULT_ATTRIBUTES.get(entityType); - if (attributeBuilder.isEmpty()) return original.call(instance, o); - - DefaultAttributeContainer originalAttributes = (DefaultAttributeContainer) original.call(instance, o); - if (originalAttributes == null) return (V) attributeBuilder.get().build(); - - return (V) DataDefaultAttributeBuilder.with(attributeBuilder.get(), originalAttributes).build(); + @ModifyReturnValue(method = "get", at = @At("RETURN")) + private static DefaultAttributeContainer get(DefaultAttributeContainer original, @Local(argsOnly = true) EntityType type) { + Optional originalAttributes = Optional.ofNullable(original); + return EntityMetatags.DEFAULT_ATTRIBUTES.get(type) + .map(builder -> + originalAttributes + .map(attributes -> DataDefaultAttributeBuilder.with(builder, attributes)) + .orElse(builder) + .build() + ).orElse(original); } @WrapOperation(method = "hasDefinitionFor", at = @At(value = "INVOKE", target = "Ljava/util/Map;containsKey(Ljava/lang/Object;)Z")) private static boolean containsKey(Map instance, Object o, Operation original) { if (!(o instanceof EntityType entityType)) return original.call(instance, o); - - boolean hasDefinition = Objects.nonNull(EntityMetatags.DEFAULT_ATTRIBUTES.get(entityType)); - return hasDefinition || original.call(instance, o); + return Objects.nonNull(EntityMetatags.DEFAULT_ATTRIBUTES.get(entityType)) || original.call(instance, o); } } diff --git a/specter-entity/src/testmod/java/dev/spiritstudios/testmod/SpecterEntityGameTest.java b/specter-entity/src/testmod/java/dev/spiritstudios/testmod/SpecterEntityGameTest.java new file mode 100644 index 0000000..796eeb9 --- /dev/null +++ b/specter-entity/src/testmod/java/dev/spiritstudios/testmod/SpecterEntityGameTest.java @@ -0,0 +1,18 @@ +package dev.spiritstudios.testmod; + +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.WardenEntity; +import net.minecraft.test.GameTest; +import net.minecraft.test.TestContext; + +public class SpecterEntityGameTest { + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testDefaultAttributes(TestContext context) { + WardenEntity warden = context.spawnEntity(EntityType.WARDEN, 0, 0, 0); + warden.damage(context.getWorld().getDamageSources().generic(), 1); + + context.assertTrue(warden.isDead(), "Warden should be dead after being attacked by player"); + context.complete(); + } +} diff --git a/specter-entity/src/testmod/resources/fabric.mod.json b/specter-entity/src/testmod/resources/fabric.mod.json index aefa9e4..35a6d90 100644 --- a/specter-entity/src/testmod/resources/fabric.mod.json +++ b/specter-entity/src/testmod/resources/fabric.mod.json @@ -12,6 +12,9 @@ "license": "MPL-2.0", "environment": "*", "entrypoints": { + "fabric-gametest": [ + "dev.spiritstudios.testmod.SpecterEntityGameTest" + ] }, "depends": { "fabricloader": ">=${loader_version}", diff --git a/specter-item/src/client/java/dev/spiritstudios/specter/mixin/item/client/CreativeInventoryScreenMixin.java b/specter-item/src/client/java/dev/spiritstudios/specter/mixin/item/client/CreativeInventoryScreenMixin.java index fcbab05..6824758 100644 --- a/specter-item/src/client/java/dev/spiritstudios/specter/mixin/item/client/CreativeInventoryScreenMixin.java +++ b/specter-item/src/client/java/dev/spiritstudios/specter/mixin/item/client/CreativeInventoryScreenMixin.java @@ -13,6 +13,7 @@ public class CreativeInventoryScreenMixin { @Inject(method = "init", at = @At("RETURN")) private void init(CallbackInfo ci) { if (!ItemGroupReloader.RELOADED) return; + ((FabricCreativeInventoryScreen) this).switchToPage(0); ItemGroupReloader.RELOADED = false; } diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/AbstractFurnaceBlockEntityMixin.java b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/AbstractFurnaceBlockEntityMixin.java index 1c9d5ca..436fafa 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/AbstractFurnaceBlockEntityMixin.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/AbstractFurnaceBlockEntityMixin.java @@ -7,21 +7,9 @@ import net.minecraft.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.Optional; @Mixin(AbstractFurnaceBlockEntity.class) public class AbstractFurnaceBlockEntityMixin { - @Inject(method = "getFuelTime", at = @At("HEAD"), cancellable = true) - private void getFuelTime(ItemStack fuel, CallbackInfoReturnable cir) { - if (fuel.isEmpty()) return; - - Optional fuelTime = ItemMetatags.FUEL.get(fuel.getItem()); - fuelTime.ifPresent(cir::setReturnValue); - } - @ModifyReturnValue(method = "canUseAsFuel", at = @At("RETURN")) private static boolean canUseAsFuel(boolean original, @Local(argsOnly = true) ItemStack stack) { if (stack.isEmpty()) return original; @@ -29,4 +17,10 @@ private static boolean canUseAsFuel(boolean original, @Local(argsOnly = true) It boolean hasFuelMetatag = ItemMetatags.FUEL.get(stack.getItem()).isPresent(); return original || hasFuelMetatag; } + + @ModifyReturnValue(method = "getFuelTime", at = @At("RETURN")) + private int getFuelTime(int original, @Local(argsOnly = true) ItemStack fuel) { + if (fuel.isEmpty()) return original; + return ItemMetatags.FUEL.get(fuel.getItem()).orElse(original); + } } diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ComposterBlock$ComposterInventoryMixin.java b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ComposterBlock$ComposterInventoryMixin.java index d4ede3f..861e8bb 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ComposterBlock$ComposterInventoryMixin.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ComposterBlock$ComposterInventoryMixin.java @@ -12,9 +12,6 @@ public class ComposterBlock$ComposterInventoryMixin { @WrapOperation(method = "canInsert", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/objects/Object2FloatMap;containsKey(Ljava/lang/Object;)Z", remap = false)) private boolean canInsert(Object2FloatMap instance, Object o, Operation original) { - ItemConvertible itemConvertible = (ItemConvertible) o; - if (SpecterItem.ITEM_TO_LEVEL_INCREASE_CHANCE.containsKey(itemConvertible)) return true; - - return original.call(instance, o); + return SpecterItem.ITEM_TO_LEVEL_INCREASE_CHANCE.containsKey((ItemConvertible) o) || original.call(instance, o); } } diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupMixin.java b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupMixin.java index 4e688ef..85d6bed 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupMixin.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupMixin.java @@ -20,15 +20,14 @@ public abstract class ItemGroupMixin { @ModifyReturnValue(method = {"getDisplayStacks", "getSearchTabStacks"}, at = @At("RETURN")) private Collection getDisplayStacks(Collection original) { - if (this.getType() == ItemGroup.Type.SEARCH) { - List stacks = new ArrayList<>(original); - for (DataItemGroup group : ItemGroupReloader.ITEM_GROUPS) { - if (group.getSearchTabStacks().isEmpty()) continue; - stacks.addAll(group.getSearchTabStacks()); - } - return stacks; - } + if (this.getType() != ItemGroup.Type.SEARCH) return original; - return original; + List stacks = new ArrayList<>(original); + ItemGroupReloader.ITEM_GROUPS.stream() + .map(DataItemGroup::getSearchTabStacks) + .filter(searchTabStacks -> !searchTabStacks.isEmpty()) + .forEach(stacks::addAll); + + return stacks; } } diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupsMixin.java b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupsMixin.java index 52dc07d..34c1e2a 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupsMixin.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/mixin/item/ItemGroupsMixin.java @@ -32,6 +32,7 @@ private static List getGroups(List original) { int offset = 0; for (DataItemGroup group : ItemGroupReloader.ITEM_GROUPS) { if (groups.contains(group)) continue; + group.setup(filtered, offset); groups.add(group); offset++; diff --git a/specter-serialization/src/client/java/dev/spiritstudios/specter/mixin/serialization/client/TranslationStorageMixin.java b/specter-serialization/src/client/java/dev/spiritstudios/specter/mixin/serialization/client/TranslationStorageMixin.java new file mode 100644 index 0000000..e013e11 --- /dev/null +++ b/specter-serialization/src/client/java/dev/spiritstudios/specter/mixin/serialization/client/TranslationStorageMixin.java @@ -0,0 +1,44 @@ +package dev.spiritstudios.specter.mixin.serialization.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import dev.spiritstudios.specter.impl.serialization.SpecterSerialization; +import dev.spiritstudios.specter.impl.serialization.text.TextTranslationSupplier; +import net.minecraft.client.resource.language.TranslationStorage; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Map; + +@Mixin(TranslationStorage.class) +public class TranslationStorageMixin implements TextTranslationSupplier { + @Unique + private Map textTranslations; + + @ModifyReturnValue(method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resource/language/TranslationStorage;", at = @At("RETURN")) + private static TranslationStorage load(TranslationStorage original) { + ((TranslationStorageMixin) (Object) original).textTranslations = SpecterSerialization.TEXT_TRANSLATIONS_BUILDER.get().build(); + + SpecterSerialization.TEXT_TRANSLATIONS_BUILDER.remove(); + return original; + } + + @ModifyReturnValue(method = "hasTranslation", at = @At("RETURN")) + private boolean hasTranslation(boolean original, @Local(argsOnly = true) String key) { + if (textTranslations == null) return original; + return original || textTranslations.containsKey(key); + } + + @ModifyReturnValue(method = "get", at = @At("RETURN")) + private String get(String original) { + Text text = textTranslations.get(original); + return text != null ? text.getString() : original; + } + + @Override + public Text specter_serialization$getText(String key) { + return textTranslations.get(key); + } +} diff --git a/specter-serialization/src/client/resources/specter-serialization.client.mixins.json b/specter-serialization/src/client/resources/specter-serialization.client.mixins.json new file mode 100644 index 0000000..13e6803 --- /dev/null +++ b/specter-serialization/src/client/resources/specter-serialization.client.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.spiritstudios.specter.mixin.serialization.client", + "compatibilityLevel": "JAVA_21", + "client": [ + "TranslationStorageMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/format/DynamicFormat.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/format/DynamicFormat.java index 4e1066c..41f48da 100644 --- a/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/format/DynamicFormat.java +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/format/DynamicFormat.java @@ -1,7 +1,7 @@ package dev.spiritstudios.specter.api.serialization.format; import com.mojang.serialization.DynamicOps; -import dev.spiritstudios.specter.impl.serialization.WrappedDynamicFormat; +import dev.spiritstudios.specter.impl.serialization.format.WrappedDynamicFormat; import java.io.IOException; import java.io.Reader; @@ -17,6 +17,10 @@ * @param The type this format serializes and deserializes. */ public interface DynamicFormat extends DynamicOps { + static DynamicFormat of(DynamicOps ops, BiConsumer write, Function read, String name) { + return new WrappedDynamicFormat<>(ops, write, read, name); + } + void write(Writer writer, T value) throws IOException; T read(Reader reader) throws IOException; @@ -26,8 +30,4 @@ public interface DynamicFormat extends DynamicOps { default T read(String string) throws IOException { return read(new StringReader(string)); } - - static DynamicFormat of(DynamicOps ops, BiConsumer write, Function read, String name) { - return new WrappedDynamicFormat<>(ops, write, read, name); - } } diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/DynamicTextContent.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/DynamicTextContent.java new file mode 100644 index 0000000..6f65140 --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/DynamicTextContent.java @@ -0,0 +1,52 @@ +package dev.spiritstudios.specter.api.serialization.text; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.spiritstudios.specter.impl.serialization.SpecterSerialization; +import net.minecraft.text.*; + +import java.util.Optional; + +/** + * A text content that is resolved at runtime. + * + * @param index The index of the argument to resolve this content to. + */ +public record DynamicTextContent(int index) implements TextContent { + public static MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group(Codec.INT.fieldOf("index").forGetter(DynamicTextContent::index)).apply(instance, DynamicTextContent::new) + ); + + public static final TextContent.Type TYPE = new Type<>( + CODEC, + "dynamic" + ); + + @Override + public Type getType() { + return TYPE; + } + + @Override + public Optional visit(StringVisitable.Visitor visitor) { + TranslatableTextContent parent = SpecterSerialization.CURRENT_TRANSLATABLE.get().peek(); + if (parent == null || parent.getArgs().length <= index) + return visitor.accept("{" + index + "}"); + + Object arg = parent.getArgs()[index]; + if (arg instanceof Text text) return text.visit(visitor); + return visitor.accept(arg.toString()); + } + + @Override + public Optional visit(StringVisitable.StyledVisitor visitor, Style style) { + TranslatableTextContent parent = SpecterSerialization.CURRENT_TRANSLATABLE.get().peek(); + if (parent == null || parent.getArgs().length <= index) + return visitor.accept(style, "{" + index + "}"); + + Object arg = parent.getArgs()[index]; + if (arg instanceof Text text) return text.visit(visitor, style); + return visitor.accept(style, arg.toString()); + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/TextContentRegistry.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/TextContentRegistry.java new file mode 100644 index 0000000..e3baa8e --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/api/serialization/text/TextContentRegistry.java @@ -0,0 +1,21 @@ +package dev.spiritstudios.specter.api.serialization.text; + +import dev.spiritstudios.specter.impl.serialization.text.TextContentRegistryImpl; +import net.minecraft.text.TextCodecs; +import net.minecraft.text.TextContent; + +/** + * Allows adding your own {@link TextContent.Type} to be encoded and decoded by {@link TextCodecs}. + */ +public final class TextContentRegistry { + /** + * Registers a new {@link TextContent.Type}. + * This type will be used when either the type field is set to the id of your type, or it contains a field with the name you provided. + * + * @param field The field name + * @param type The type + */ + public static void register(String field, TextContent.Type type) { + TextContentRegistryImpl.register(field, type); + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/SpecterSerialization.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/SpecterSerialization.java new file mode 100644 index 0000000..eb7dc0a --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/SpecterSerialization.java @@ -0,0 +1,22 @@ +package dev.spiritstudios.specter.impl.serialization; + +import com.google.common.collect.ImmutableMap; +import dev.spiritstudios.specter.api.serialization.text.DynamicTextContent; +import dev.spiritstudios.specter.api.serialization.text.TextContentRegistry; +import net.fabricmc.api.ModInitializer; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableTextContent; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class SpecterSerialization implements ModInitializer { + public static final ThreadLocal> TEXT_TRANSLATIONS_BUILDER = ThreadLocal.withInitial(ImmutableMap.Builder::new); + + public static final ThreadLocal> CURRENT_TRANSLATABLE = ThreadLocal.withInitial(ArrayDeque::new); + + @Override + public void onInitialize() { + TextContentRegistry.register("index", DynamicTextContent.TYPE); + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/WrappedDynamicFormat.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/format/WrappedDynamicFormat.java similarity index 99% rename from specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/WrappedDynamicFormat.java rename to specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/format/WrappedDynamicFormat.java index f4d01a1..943a4c8 100644 --- a/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/WrappedDynamicFormat.java +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/format/WrappedDynamicFormat.java @@ -1,4 +1,4 @@ -package dev.spiritstudios.specter.impl.serialization; +package dev.spiritstudios.specter.impl.serialization.format; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.*; diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextContentRegistryImpl.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextContentRegistryImpl.java new file mode 100644 index 0000000..9a0244e --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextContentRegistryImpl.java @@ -0,0 +1,22 @@ +package dev.spiritstudios.specter.impl.serialization.text; + +import com.google.common.collect.ImmutableMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.text.TextContent; + +import java.util.Map; + +public final class TextContentRegistryImpl { + private static final Map> types = new Object2ObjectOpenHashMap<>(); + + public static Map> getTypes() { + return ImmutableMap.copyOf(types); + } + + public static void register(String field, TextContent.Type type) { + types.put(type.id(), new Entry<>(field, type)); + } + + public record Entry(String field, TextContent.Type type) { + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextTranslationSupplier.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextTranslationSupplier.java new file mode 100644 index 0000000..9cb4b66 --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/impl/serialization/text/TextTranslationSupplier.java @@ -0,0 +1,7 @@ +package dev.spiritstudios.specter.impl.serialization.text; + +import net.minecraft.text.Text; + +public interface TextTranslationSupplier { + Text specter_serialization$getText(String key); +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/LanguageMixin.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/LanguageMixin.java new file mode 100644 index 0000000..d913c52 --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/LanguageMixin.java @@ -0,0 +1,41 @@ +package dev.spiritstudios.specter.mixin.serialization; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import com.mojang.serialization.JsonOps; +import dev.spiritstudios.specter.impl.serialization.SpecterSerialization; +import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; +import net.minecraft.util.Language; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.BiConsumer; + +@Mixin(Language.class) +public class LanguageMixin { + @WrapOperation(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/JsonHelper;asString(Lcom/google/gson/JsonElement;Ljava/lang/String;)Ljava/lang/String;")) + private static String load(JsonElement element, String name, Operation original, @Local(argsOnly = true) BiConsumer entryConsumer, @Share("skip") LocalBooleanRef skip) { + if (element.isJsonPrimitive()) { + skip.set(false); + return original.call(element, name); + } + + Text text = TextCodecs.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow(JsonParseException::new); + SpecterSerialization.TEXT_TRANSLATIONS_BUILDER.get().put(name, text); + + skip.set(true); + return ""; + } + + @WrapWithCondition(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Ljava/util/function/BiConsumer;accept(Ljava/lang/Object;Ljava/lang/Object;)V")) + private static boolean skip(BiConsumer instance, T t, U u, @Share("skip") LocalBooleanRef skip) { + return !skip.get(); + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TextCodecsMixin.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TextCodecsMixin.java new file mode 100644 index 0000000..ba75ff6 --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TextCodecsMixin.java @@ -0,0 +1,62 @@ +package dev.spiritstudios.specter.mixin.serialization; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.serialization.*; +import dev.spiritstudios.specter.impl.serialization.text.TextContentRegistryImpl; +import net.minecraft.text.TextCodecs; +import net.minecraft.text.TextContent; +import net.minecraft.util.StringIdentifiable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +@Mixin(TextCodecs.class) +public class TextCodecsMixin { + @ModifyVariable(method = "dispatchingCodec", at = @At("STORE"), ordinal = 0) + private static MapCodec dispatchingCodec(MapCodec original, T[] types) { + if (!types.getClass().getComponentType().isAssignableFrom(TextContent.Type.class)) return original; + + return new MapCodec<>() { + @Override + public RecordBuilder encode(E input, DynamicOps ops, RecordBuilder prefix) { + return original.encode(input, ops, prefix); + } + + @SuppressWarnings("unchecked") + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + DataResult originalResult = original.decode(ops, input); + return originalResult.result().isPresent() ? originalResult : TextContentRegistryImpl.getTypes().values().stream() + .filter(entry -> input.get(entry.field()) != null) + .findFirst() + .map(entry -> (DataResult) entry.type().codec().decode(ops, input)) + .orElse(originalResult); + } + + @Override + public Stream keys(DynamicOps ops) { + return Stream.concat( + original.keys(ops), + TextContentRegistryImpl.getTypes().values().stream() + .flatMap(entry -> entry.type().codec().keys(ops)) + ); + } + }; + } + + @SuppressWarnings("unchecked") + @WrapOperation(method = "dispatchingCodec", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/StringIdentifiable;createBasicCodec(Ljava/util/function/Supplier;)Lcom/mojang/serialization/Codec;")) + private static Codec dispatchingCodec(Supplier values, Operation> original) { + Codec originalCodec = original.call(values); + if (!values.get().getClass().getComponentType().isAssignableFrom(TextContent.Type.class)) return originalCodec; + + return Codec.withAlternative( + originalCodec, + Codec.stringResolver(StringIdentifiable::asString, id -> (T) TextContentRegistryImpl.getTypes().get(id).type()) + ); + } +} diff --git a/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TranslatableTextContentMixin.java b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TranslatableTextContentMixin.java new file mode 100644 index 0000000..cf5577e --- /dev/null +++ b/specter-serialization/src/main/java/dev/spiritstudios/specter/mixin/serialization/TranslatableTextContentMixin.java @@ -0,0 +1,79 @@ +package dev.spiritstudios.specter.mixin.serialization; + +import com.google.common.collect.ImmutableList; +import com.llamalad7.mixinextras.sugar.Local; +import dev.spiritstudios.specter.impl.serialization.SpecterSerialization; +import dev.spiritstudios.specter.impl.serialization.text.TextTranslationSupplier; +import net.minecraft.text.StringVisitable; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableTextContent; +import net.minecraft.util.Language; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.Optional; + +@Mixin(TranslatableTextContent.class) +public abstract class TranslatableTextContentMixin { + @Shadow + @Final + private String key; + + @Shadow + private List translations; + + @Inject( + method = { + "visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", + "visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;" + }, + at = @At("HEAD") + ) + private void push(CallbackInfoReturnable> cir) { + if (SpecterSerialization.CURRENT_TRANSLATABLE.get().contains((TranslatableTextContent) (Object) this)) + throw new IllegalStateException("Detected recursive translation: " + key); + + SpecterSerialization.CURRENT_TRANSLATABLE.get().push((TranslatableTextContent) (Object) this); + } + + + @Inject( + method = { + "visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", + "visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;" + }, + at = @At("RETURN") + ) + private void pop(CallbackInfoReturnable> cir) { + SpecterSerialization.CURRENT_TRANSLATABLE.get().pop(); + + if (SpecterSerialization.CURRENT_TRANSLATABLE.get().isEmpty()) + SpecterSerialization.CURRENT_TRANSLATABLE.remove(); + } + + + @Inject( + method = "updateTranslations", + at = { + @At(value = "INVOKE", target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;)Ljava/lang/String;"), + @At(value = "INVOKE", target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;") + }, + cancellable = true + ) + private void updateTranslations(CallbackInfo ci, @Local Language language) { + if (!(language instanceof TextTranslationSupplier supplier)) + return; + + Text text = supplier.specter_serialization$getText(key); + if (text == null) return; + + translations = ImmutableList.of(text); + ci.cancel(); + } +} diff --git a/specter-serialization/src/main/resources/fabric.mod.json b/specter-serialization/src/main/resources/fabric.mod.json index b8d37dc..92572ba 100644 --- a/specter-serialization/src/main/resources/fabric.mod.json +++ b/specter-serialization/src/main/resources/fabric.mod.json @@ -12,9 +12,16 @@ "license": "MPL-2.0", "environment": "*", "entrypoints": { + "main": [ + "dev.spiritstudios.specter.impl.serialization.SpecterSerialization" + ] }, "mixins": [ - "specter-serialization.mixins.json" + "specter-serialization.mixins.json", + { + "config": "specter-serialization.client.mixins.json", + "environment": "client" + } ], "depends": { "fabricloader": ">=${loader_version}", diff --git a/specter-serialization/src/main/resources/specter-serialization.mixins.json b/specter-serialization/src/main/resources/specter-serialization.mixins.json index e803ce9..dfb5c9b 100644 --- a/specter-serialization/src/main/resources/specter-serialization.mixins.json +++ b/specter-serialization/src/main/resources/specter-serialization.mixins.json @@ -4,6 +4,9 @@ "package": "dev.spiritstudios.specter.mixin.serialization", "compatibilityLevel": "JAVA_21", "mixins": [ + "LanguageMixin", + "TextCodecsMixin", + "TranslatableTextContentMixin" ], "injectors": { "defaultRequire": 1