Add Trial Spawner support and improve config handling

Introduces mixins and accessors for Trial Spawner logic, enabling Silk Touch mining and correct data transfer for Trial Spawners. Refactors config file handling to use FabricLoader and Path APIs for better compatibility. Updates mod metadata, adds a logo, and improves dropped item naming for both spawner types.
This commit is contained in:
2026-01-06 02:02:25 +01:00
parent 1b2c1c862b
commit 259926b6af
8 changed files with 200 additions and 55 deletions

View File

@@ -1,8 +1,14 @@
package com.kasetoatz.silkspawners.accessors;
import net.minecraft.block.spawner.TrialSpawnerData;
import net.minecraft.block.spawner.TrialSpawnerLogic;
import net.minecraft.text.Text;
import net.minecraft.world.World;
public interface ITrialSpawnerLogic
{
TrialSpawnerLogic.FullConfig silkspawners$getFullConfig();
void silkspawners$setFullConfig(World world, TrialSpawnerLogic.FullConfig fullConfig);
void silkspawners$setData(TrialSpawnerData data);
Text silkspawners$getEntityName(World world);
}

View File

@@ -3,14 +3,13 @@ package com.kasetoatz.silkspawners.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import net.minecraft.client.MinecraftClient;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Config
{
@@ -19,34 +18,34 @@ public class Config
public static boolean REQUIRE_PICKAXE = true;
public static boolean REQUIRE_SILK_TOUCH = true;
private static final File config = new File(MinecraftClient.getInstance().runDirectory, "config/silkspawners.json");
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private static final Path FILE = FabricLoader.getInstance().getConfigDir().resolve("silkspawners.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static void load()
{
if (!config.exists())
if (!Files.exists(FILE))
{
save();
return;
}
try (FileReader reader = new FileReader(config))
try
{
JsonObject json = gson.fromJson(reader, JsonObject.class);
if (json.has("enable-spawner"))
JsonObject data = GSON.fromJson(Files.readString(FILE), JsonObject.class);
if (data.has("enable-spawner"))
{
ENABLE_SPAWNER = json.get("enable-spawner").getAsBoolean();
ENABLE_SPAWNER = data.get("enable-spawner").getAsBoolean();
}
if (json.has("enable-trial-spawner"))
if (data.has("enable-trial-spawner"))
{
ENABLE_TRIAL_SPAWNER = json.get("enable-trial-spawner").getAsBoolean();
ENABLE_TRIAL_SPAWNER = data.get("enable-trial-spawner").getAsBoolean();
}
if (json.has("require-pickaxe"))
if (data.has("require-pickaxe"))
{
REQUIRE_PICKAXE = json.get("require-pickaxe").getAsBoolean();
REQUIRE_PICKAXE = data.get("require-pickaxe").getAsBoolean();
}
if (json.has("require-silk-touch"))
if (data.has("require-silk-touch"))
{
REQUIRE_SILK_TOUCH = json.get("require-silk-touch").getAsBoolean();
REQUIRE_SILK_TOUCH = data.get("require-silk-touch").getAsBoolean();
}
save();
}
@@ -58,14 +57,14 @@ public class Config
public static void save()
{
JsonObject json = new JsonObject();
json.addProperty("enable-spawner", ENABLE_SPAWNER);
json.addProperty("enable-trial-spawner", ENABLE_TRIAL_SPAWNER);
json.addProperty("require-pickaxe", REQUIRE_PICKAXE);
json.addProperty("require-silk-touch", REQUIRE_SILK_TOUCH);
try (FileWriter writer = new FileWriter(config))
try
{
gson.toJson(json, writer);
JsonObject data = new JsonObject();
data.addProperty("enable-spawner", ENABLE_SPAWNER);
data.addProperty("enable-trial-spawner", ENABLE_TRIAL_SPAWNER);
data.addProperty("require-pickaxe", REQUIRE_PICKAXE);
data.addProperty("require-silk-touch", REQUIRE_SILK_TOUCH);
Files.writeString(FILE, GSON.toJson(data));
}
catch (IOException exc)
{

View File

@@ -0,0 +1,59 @@
package com.kasetoatz.silkspawners.mixin;
import com.kasetoatz.silkspawners.accessors.ITrialSpawnerLogic;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.MobSpawnerBlockEntity;
import net.minecraft.block.entity.TrialSpawnerBlockEntity;
import net.minecraft.block.enums.TrialSpawnerState;
import net.minecraft.block.spawner.TrialSpawnerData;
import net.minecraft.block.spawner.TrialSpawnerLogic;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.NbtComponent;
import net.minecraft.entity.EntityType;
import net.minecraft.item.*;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
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.CallbackInfo;
import java.util.Objects;
import java.util.Optional;
@Mixin(BlockItem.class)
public abstract class BlockItemMixin
{
@Inject(method="copyComponentsToBlockEntity", at=@At(value="INVOKE", target="Lnet/minecraft/block/entity/BlockEntity;markDirty()V"))
private static void copyComponentsToBlockEntity(World world, BlockPos pos, ItemStack stack, CallbackInfo ci, @Local BlockEntity entity)
{
if (entity instanceof MobSpawnerBlockEntity spawner)
{
Optional<EntityType<?>> entityType = Objects.requireNonNullElse(stack.get(DataComponentTypes.CUSTOM_DATA), NbtComponent.DEFAULT).copyNbt().get("entityType", EntityType.CODEC);
if (entityType.isEmpty())
{
return;
}
spawner.setEntityType(entityType.get(), world.getRandom());
}
else if (entity instanceof TrialSpawnerBlockEntity trialSpawner)
{
NbtCompound nbt = Objects.requireNonNullElse(stack.get(DataComponentTypes.CUSTOM_DATA), NbtComponent.DEFAULT).copyNbt();
Optional<TrialSpawnerLogic.FullConfig> fullConfig = nbt.get("fullConfig", TrialSpawnerLogic.FullConfig.CODEC.codec());
Optional<TrialSpawnerData.Packed> data = nbt.get("data", TrialSpawnerData.Packed.CODEC.codec());
Optional<String> state = nbt.getString("state");
ITrialSpawnerLogic logic = ((ITrialSpawnerLogic)(Object)trialSpawner.getSpawner());
if (fullConfig.isEmpty() || data.isEmpty() || state.isEmpty() || logic == null)
{
return;
}
logic.silkspawners$setFullConfig(world, fullConfig.get());
TrialSpawnerData newData = new TrialSpawnerData();
newData.unpack(data.get());
logic.silkspawners$setData(newData);
trialSpawner.setSpawnerState(world, TrialSpawnerState.valueOf(state.get()));
}
}
}

View File

@@ -2,27 +2,37 @@ package com.kasetoatz.silkspawners.mixin;
import com.kasetoatz.silkspawners.accessors.IMobSpawnerLogic;
import com.kasetoatz.silkspawners.accessors.ITrialSpawnerLogic;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.block.*;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.MobSpawnerBlockEntity;
import net.minecraft.block.entity.Spawner;
import net.minecraft.block.entity.TrialSpawnerBlockEntity;
import net.minecraft.block.spawner.TrialSpawnerData;
import net.minecraft.block.spawner.TrialSpawnerLogic;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.NbtComponent;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.ItemTags;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.Rarity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.jspecify.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
import java.util.Objects;
@@ -32,46 +42,62 @@ import static com.kasetoatz.silkspawners.config.Config.*;
@Mixin(Block.class)
public class BlockMixin
{
@Redirect(method="dropStacks(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/entity/BlockEntity;Lnet/minecraft/entity/Entity;Lnet/minecraft/item/ItemStack;)V", at=@At(value="INVOKE", target="Lnet/minecraft/block/Block;getDroppedStacks(Lnet/minecraft/block/BlockState;Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/entity/BlockEntity;Lnet/minecraft/entity/Entity;Lnet/minecraft/item/ItemStack;)Ljava/util/List;"))
private static List<ItemStack> getDroppedStacks(BlockState state, ServerWorld world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack stack)
{
System.out.println(stack);
if (!(blockEntity instanceof Spawner))
{
return getDroppedStacks(state, world, pos, blockEntity, entity, stack);
@Unique
private static boolean shouldNotDrop(BlockEntity entity, Block block, ItemStack tool, Registry<Enchantment> enchantments) {
return !(entity instanceof Spawner) ||
(!ENABLE_SPAWNER && block instanceof SpawnerBlock) ||
(!ENABLE_TRIAL_SPAWNER && block instanceof TrialSpawnerBlock) ||
(REQUIRE_PICKAXE && !tool.isIn(ItemTags.PICKAXES)) ||
(REQUIRE_SILK_TOUCH && EnchantmentHelper.getLevel(enchantments.getOrThrow(Enchantments.SILK_TOUCH), tool) < 1);
}
if (!ENABLE_SPAWNER && state.getBlock() instanceof SpawnerBlock)
@Inject(method="getDroppedStacks(Lnet/minecraft/block/BlockState;Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/entity/BlockEntity;Lnet/minecraft/entity/Entity;Lnet/minecraft/item/ItemStack;)Ljava/util/List;", at=@At("HEAD"), cancellable=true)
private static void getDroppedStacks(BlockState state, ServerWorld world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, CallbackInfoReturnable<List<ItemStack>> cir)
{
return List.of();
}
if (!ENABLE_TRIAL_SPAWNER && state.getBlock() instanceof TrialSpawnerBlock)
if (shouldNotDrop(blockEntity, state.getBlock(), tool, world.getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT)))
{
return List.of();
}
if (REQUIRE_PICKAXE && !stack.isIn(ItemTags.PICKAXES))
{
return List.of();
}
if (REQUIRE_SILK_TOUCH && EnchantmentHelper.getLevel(world.getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH), stack) < 1)
{
return List.of();
return;
}
ItemStack item = state.getBlock().asItem().getDefaultStack();
NbtComponent component = Objects.requireNonNullElse(item.get(DataComponentTypes.CUSTOM_DATA), NbtComponent.DEFAULT);
if (blockEntity instanceof MobSpawnerBlockEntity spawner)
{
EntityType<?> type = ((IMobSpawnerLogic)spawner.getLogic()).silkspawners$getEntityType(world, world.getRandom(), pos);
Text name = Text.translatable("block.minecraft.spawner").formatted(Rarity.RARE.getFormatting());
if (type != null)
{
component.apply(nbtCompound -> nbtCompound.put("entityType", EntityType.CODEC, type));
component = component.apply(nbtCompound -> nbtCompound.put("entityType", EntityType.CODEC, type));
name = Text.empty().append(type.getName()).append(" ").append(Text.translatable("block.minecraft.spawner")).formatted(Rarity.RARE.getFormatting());
}
item.set(DataComponentTypes.ITEM_NAME, name);
}
else if (blockEntity instanceof TrialSpawnerBlockEntity trialSpawner)
{
component.apply(nbtCompound -> nbtCompound.put("fullConfig", TrialSpawnerLogic.FullConfig.CODEC.codec(), ((ITrialSpawnerLogic)(Object)trialSpawner.getSpawner()).silkspawners$getFullConfig()));
ITrialSpawnerLogic logic = (ITrialSpawnerLogic)(Object)trialSpawner.getSpawner();
if (logic == null)
{
return;
}
component = component.apply(nbtCompound -> {
nbtCompound.put("fullConfig", TrialSpawnerLogic.FullConfig.CODEC.codec(), logic.silkspawners$getFullConfig());
nbtCompound.put("data", TrialSpawnerData.Packed.CODEC.codec(), trialSpawner.getSpawner().getData().pack());
nbtCompound.putString("state", trialSpawner.getSpawnerState().name());
});
Text entityName = logic.silkspawners$getEntityName(world);
Text name = Text.translatable("block.minecraft.trial_spawner").formatted(Rarity.RARE.getFormatting());
if (entityName != null)
{
name = Text.empty().append(logic.silkspawners$getEntityName(world)).append(" ").append(Text.translatable("block.minecraft.trial_spawner")).formatted(Rarity.RARE.getFormatting());
}
item.set(DataComponentTypes.ITEM_NAME, name);
}
item.set(DataComponentTypes.CUSTOM_DATA, component);
return List.of(item);
cir.setReturnValue(List.of(item));
}
@ModifyArg(method="dropStacks(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/entity/BlockEntity;Lnet/minecraft/entity/Entity;Lnet/minecraft/item/ItemStack;)V", at=@At(value="INVOKE", target="Lnet/minecraft/block/BlockState;onStacksDropped(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/item/ItemStack;Z)V"), index=3)
private static boolean dropStacks(boolean original, @Local(argsOnly=true) BlockState state, @Local(argsOnly=true) World world, @Local(argsOnly=true) BlockEntity blockEntity, @Local(argsOnly=true) ItemStack tool)
{
return shouldNotDrop(blockEntity, state.getBlock(), tool, world.getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT));
}
}

View File

@@ -1,18 +1,67 @@
package com.kasetoatz.silkspawners.mixin;
import com.kasetoatz.silkspawners.accessors.ITrialSpawnerLogic;
import net.minecraft.block.spawner.MobSpawnerEntry;
import net.minecraft.block.spawner.TrialSpawnerConfig;
import net.minecraft.block.spawner.TrialSpawnerData;
import net.minecraft.block.spawner.TrialSpawnerLogic;
import net.minecraft.entity.EntityType;
import net.minecraft.storage.NbtReadView;
import net.minecraft.storage.ReadView;
import net.minecraft.text.Text;
import net.minecraft.util.ErrorReporter;
import net.minecraft.world.World;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Optional;
@Mixin(TrialSpawnerLogic.class)
public class TrialSpawnerLogicMixin implements ITrialSpawnerLogic
public abstract class TrialSpawnerLogicMixin implements ITrialSpawnerLogic
{
@Shadow private TrialSpawnerLogic.FullConfig fullConfig;
@Mutable @Shadow @Final private TrialSpawnerData data;
@Shadow @Final private static Logger LOGGER;
@Shadow public abstract TrialSpawnerConfig getConfig();
@Override
public TrialSpawnerLogic.FullConfig silkspawners$getFullConfig()
{
return fullConfig;
}
@Override
public void silkspawners$setFullConfig(World world, TrialSpawnerLogic.FullConfig fullConfig)
{
this.fullConfig = fullConfig;
}
@Override
public void silkspawners$setData(TrialSpawnerData data)
{
this.data = data;
}
@Override
public Text silkspawners$getEntityName(World world)
{
try (ErrorReporter.Logging logging = new ErrorReporter.Logging(LOGGER))
{
Optional<MobSpawnerEntry> entry = getConfig().spawnPotentials().getOrEmpty(world.getRandom());
if (entry.isEmpty())
{
return null;
}
ReadView readView = NbtReadView.create(logging, world.getRegistryManager(), entry.get().entity());
Optional<EntityType<?>> entity = EntityType.fromData(readView);
return entity.map(EntityType::getName).orElse(null);
}
catch (Exception ignored)
{
return null;
}
}
}

View File

@@ -3,11 +3,16 @@
"id": "silkspawners",
"version": "${version}",
"name": "SilkSpawners",
"description": "",
"authors": [],
"contact": {},
"description": "Mod that allows mining Spawners & Trial Spawners with Silk Touch.",
"authors": ["KaseToatz"],
"contact": {
"email": "kasetoatz@kasetoatz.com",
"homepage": "https://modrinth.com/mod/silkspawners",
"issues": "https://git.kasetoatz.com/KaseToatz/SilkSpawners/issues",
"sources": "https://git.kasetoatz.com/KaseToatz/SilkSpawners"
},
"license": "MIT",
"icon": "assets/silkspawners/icon.png",
"icon": "logo.png",
"environment": "*",
"entrypoints": {
"main": [

BIN
src/main/resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -4,6 +4,7 @@
"package": "com.kasetoatz.silkspawners.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"BlockItemMixin",
"BlockMixin",
"MobSpawnerLogicMixin",
"TrialSpawnerLogicMixin"