diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fc365f..399f5b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: run: ./gradlew build - name: capture build artifacts if: ${{ runner.os == 'Linux' && matrix.java == '17' }} # Only upload artifacts built from latest java on one OS - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Artifacts path: build/libs/ diff --git a/README.md b/README.md index 2da4a2c..e8eeaa4 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,5 @@ disableVillages: prevents village structures from spawning in the world curedZombieLoot: if this option is set to null, curing zombie villagers is impossible. By supplying lootTable data (for example generated on https://misode.github.io/loot-table/ ), zombie villagers die when they are completely healed and drop the configured loot. +tradeCycling: if this option is set to true, villagers trade will be set in stone when the villagers are spawned. + diff --git a/src/main/java/com/stratecide/disable/villagers/DisableVillagersMod.java b/src/main/java/com/stratecide/disable/villagers/DisableVillagersMod.java index 66135a3..0866b78 100644 --- a/src/main/java/com/stratecide/disable/villagers/DisableVillagersMod.java +++ b/src/main/java/com/stratecide/disable/villagers/DisableVillagersMod.java @@ -10,107 +10,107 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileWriter; import java.io.IOException; -import java.util.Scanner; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; public class DisableVillagersMod implements ModInitializer { public static final Logger LOGGER = LoggerFactory.getLogger("disable.villagers"); private static final String CONFIG_FOLDER = "config/"; private static final String CONFIG_FILE = CONFIG_FOLDER + "disable-villagers.json"; - private static final String DEFAULT_CONFIG = """ -{ - "killVillagers": true, - "disableWanderingTrader": true, - "blockTrading": true, - "spareExperiencedVillagers": true, - "breeding": false, - "disableVillages": true, - "disableZombies": false, - "curableZombies": true, - "curedZombieLoot": { - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:emerald_block" - } - ] - } - ] - } -}"""; + private static final String DEFAULT_CONFIG_PATH = "data/disable-villagers/default_config.json"; private static final Gson GSON = LootGsons.getTableGsonBuilder().create(); - private static boolean isInitialized = false; - - public static boolean killVillagers = false; - public static boolean disableWanderingTrader = false; - public static boolean blockTrading = false; - public static boolean spareExperiencedVillagers = false; - public static boolean breeding = false; - private static boolean disableZombies = true; - public static boolean getDisabledZombies() { - loadConfig(); - return disableZombies; - } - public static boolean curableZombies = true; - private static JsonElement curedZombieLootJson = null; - public static LootTable curedZombieLoot = null; - private static boolean disableVillages = false; - public static boolean getDisabledVillages() { - loadConfig(); - return disableVillages; - } + + private static ModConfig config; @Override public void onInitialize() { loadConfig(); - if (curedZombieLootJson != null) - curedZombieLoot = GSON.fromJson(curedZombieLootJson, LootTable.class); + LOGGER.info("DisableVillagers mod initialized with configuration: {}", config); } + /** + * Loads the mod configuration from file or creates default if not present + */ private static void loadConfig() { - if (isInitialized) - return; - isInitialized = true; - File file = new File(CONFIG_FILE); - String data; - if (!file.exists()) { - file.getParentFile().mkdirs(); - data = DEFAULT_CONFIG; - try (FileWriter writer = new FileWriter(CONFIG_FILE)) { - writer.write(data); + try { + Path configPath = Path.of(CONFIG_FILE); + String configData; + + if (!Files.exists(configPath)) { + LOGGER.info("Creating default configuration file"); + Files.createDirectories(configPath.getParent()); + configData = loadDefaultConfig(); + Files.writeString(configPath, configData); + } else { + configData = Files.readString(configPath); } - catch (IOException e) { - e.printStackTrace(); + + JsonObject jsonConfig = JsonParser.parseString(configData).getAsJsonObject(); + config = new ModConfig(jsonConfig); + LOGGER.info("Configuration loaded successfully"); + } catch (IOException e) { + LOGGER.error("Failed to load configuration", e); + // Fallback to default config + try { + String defaultConfig = loadDefaultConfig(); + config = new ModConfig(JsonParser.parseString(defaultConfig).getAsJsonObject()); + } catch (IOException ex) { + LOGGER.error("Failed to load default configuration", ex); + throw new RuntimeException("Failed to load configuration", ex); } } - else { - try (Scanner scanner = new Scanner(file)) { - StringBuilder builder = new StringBuilder(); - while (scanner.hasNextLine()) - builder.append(scanner.nextLine()); - data = builder.toString(); - } - catch (FileNotFoundException e) { - e.printStackTrace(); - data = DEFAULT_CONFIG; + } + + private static String loadDefaultConfig() throws IOException { + try (InputStream is = DisableVillagersMod.class.getClassLoader().getResourceAsStream(DEFAULT_CONFIG_PATH)) { + if (is == null) { + throw new IOException("Default configuration file not found in resources"); } + return new String(is.readAllBytes()); } - JsonObject config = JsonParser.parseString(data).getAsJsonObject(); - killVillagers = config.get("killVillagers").getAsBoolean(); - disableWanderingTrader = config.has("disableWanderingTrader") && config.get("disableWanderingTrader").getAsBoolean(); - blockTrading = config.get("blockTrading").getAsBoolean(); - spareExperiencedVillagers = config.get("spareExperiencedVillagers").getAsBoolean(); - breeding = config.get("breeding").getAsBoolean(); - disableZombies = config.has("disableZombies") && config.get("disableZombies").getAsBoolean(); - curableZombies = config.get("curableZombies").getAsBoolean(); - disableVillages = config.get("disableVillages").getAsBoolean(); - curedZombieLootJson = config.get("curedZombieLoot"); + } + + // Configuration getters + public static boolean isKillVillagers() { + return config.killVillagers; + } + + public static boolean isDisableWanderingTrader() { + return config.disableWanderingTrader; + } + + public static boolean isBlockTrading() { + return config.blockTrading; + } + + public static boolean isSpareExperiencedVillagers() { + return config.spareExperiencedVillagers; + } + + public static boolean isBreeding() { + return config.breeding; + } + + public static boolean isDisableZombies() { + return config.disableZombies; + } + + public static boolean isCurableZombies() { + return config.curableZombies; + } + + public static boolean isDisableVillages() { + return config.disableVillages; + } + + public static LootTable getCuredZombieLoot() { + return config.curedZombieLoot; + } + + public static boolean isTradeCycling() { + return config.tradeCycling; } } diff --git a/src/main/java/com/stratecide/disable/villagers/ModConfig.java b/src/main/java/com/stratecide/disable/villagers/ModConfig.java new file mode 100644 index 0000000..c64757b --- /dev/null +++ b/src/main/java/com/stratecide/disable/villagers/ModConfig.java @@ -0,0 +1,40 @@ +package com.stratecide.disable.villagers; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.loot.LootGsons; +import net.minecraft.loot.LootTable; + +/** + * Configuration class to hold all mod settings + */ +public class ModConfig { + private static final Gson GSON = LootGsons.getTableGsonBuilder().create(); + + public final boolean killVillagers; + public final boolean disableWanderingTrader; + public final boolean blockTrading; + public final boolean spareExperiencedVillagers; + public final boolean breeding; + public final boolean disableZombies; + public final boolean curableZombies; + public final boolean disableVillages; + public final LootTable curedZombieLoot; + public final boolean tradeCycling; + + public ModConfig(JsonObject json) { + this.killVillagers = json.get("killVillagers").getAsBoolean(); + this.disableWanderingTrader = json.has("disableWanderingTrader") && json.get("disableWanderingTrader").getAsBoolean(); + this.blockTrading = json.get("blockTrading").getAsBoolean(); + this.spareExperiencedVillagers = json.get("spareExperiencedVillagers").getAsBoolean(); + this.breeding = json.get("breeding").getAsBoolean(); + this.disableZombies = json.has("disableZombies") && json.get("disableZombies").getAsBoolean(); + this.curableZombies = json.get("curableZombies").getAsBoolean(); + this.disableVillages = json.get("disableVillages").getAsBoolean(); + this.tradeCycling = json.get("tradeCycling").getAsBoolean(); + + JsonElement lootJson = json.get("curedZombieLoot"); + this.curedZombieLoot = lootJson != null ? GSON.fromJson(lootJson, LootTable.class) : null; + } +} \ No newline at end of file diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/DefaultBiomeFeaturesMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/DefaultBiomeFeaturesMixin.java index 1c7fd7c..305c38d 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/DefaultBiomeFeaturesMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/DefaultBiomeFeaturesMixin.java @@ -16,8 +16,9 @@ public class DefaultBiomeFeaturesMixin { */ @ModifyVariable(method = "addMonsters", at = @At("HEAD"), ordinal = 0) private static int fixZombieChance(int weight, SpawnSettings.Builder builder, int zombieWeight, int zombieVillagerWeight, int skeletonWeight) { - if (DisableVillagersMod.getDisabledZombies()) + if (DisableVillagersMod.isDisableZombies()) { return weight + zombieVillagerWeight; + } return weight; } @@ -26,8 +27,9 @@ private static int fixZombieChance(int weight, SpawnSettings.Builder builder, in */ @Redirect(method = "addMonsters", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/biome/SpawnSettings$Builder;spawn(Lnet/minecraft/entity/SpawnGroup;Lnet/minecraft/world/biome/SpawnSettings$SpawnEntry;)Lnet/minecraft/world/biome/SpawnSettings$Builder;", ordinal = 2)) private static SpawnSettings.Builder removeZombieVillagers(SpawnSettings.Builder builder, SpawnGroup spawnGroup, SpawnSettings.SpawnEntry spawnEntry) { - if (!DisableVillagersMod.getDisabledZombies()) + if (!DisableVillagersMod.isDisableZombies()) { return builder.spawn(spawnGroup, spawnEntry); + } return builder; } } diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/MerchantEntityMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/MerchantEntityMixin.java index c5b80b2..0bb1c6e 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/MerchantEntityMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/MerchantEntityMixin.java @@ -14,9 +14,10 @@ public class MerchantEntityMixin { @Inject(method = "getOffers", at = @At("HEAD"), cancellable = true) - private void injectGetOffers(CallbackInfoReturnable cir) { - if (DisableVillagersMod.blockTrading && ((Object) this) instanceof VillagerEntity - || DisableVillagersMod.disableWanderingTrader && ((Object) this) instanceof WanderingTraderEntity) { + private void onGetOffersInject(CallbackInfoReturnable cir) { + Object self = this; + if ((self instanceof VillagerEntity && DisableVillagersMod.isBlockTrading()) || + (self instanceof WanderingTraderEntity && DisableVillagersMod.isDisableWanderingTrader())) { cir.setReturnValue(new TradeOfferList()); } } diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/StructurePlacementCalculatorMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/StructurePlacementCalculatorMixin.java index 8143425..3ad0ea6 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/StructurePlacementCalculatorMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/StructurePlacementCalculatorMixin.java @@ -15,19 +15,17 @@ @Mixin(StructurePlacementCalculator.class) public class StructurePlacementCalculatorMixin { + @ModifyVariable(method = "create(Lnet/minecraft/world/gen/noise/NoiseConfig;JLnet/minecraft/world/biome/source/BiomeSource;Ljava/util/stream/Stream;)Lnet/minecraft/world/gen/chunk/placement/StructurePlacementCalculator;", at = @At("HEAD"), argsOnly = true) - private static Stream> removeVillages1(Stream> structureSets) { - if (DisableVillagersMod.getDisabledVillages()) { - return structureSets.filter(entry -> - !entry.matchesKey(StructureSetKeys.VILLAGES) - ); - } else { - return structureSets; + private static Stream> filterVillageStructures(Stream> structureSets) { + if (DisableVillagersMod.isDisableVillages()) { + return structureSets.filter(entry -> !entry.matchesKey(StructureSetKeys.VILLAGES)); } + return structureSets; } @Redirect(method = "create(Lnet/minecraft/world/gen/noise/NoiseConfig;JLnet/minecraft/world/biome/source/BiomeSource;Lnet/minecraft/registry/RegistryWrapper;)Lnet/minecraft/world/gen/chunk/placement/StructurePlacementCalculator;", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;collect(Ljava/util/stream/Collector;)Ljava/lang/Object;")) - private static Object removeVillages2(Stream> stream, Collector collector) { - return removeVillages1(stream).collect(collector); + private static Object filterAndCollectStructures(Stream> stream, Collector collector) { + return filterVillageStructures(stream).collect(collector); } } diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/VillagerMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/VillagerMixin.java index 5e93a08..818c06b 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/VillagerMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/VillagerMixin.java @@ -4,11 +4,14 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityInteraction; import net.minecraft.entity.EntityType; +import net.minecraft.village.VillagerData; +import net.minecraft.village.VillagerType; +import net.minecraft.village.VillagerProfession; +import net.minecraft.entity.ai.brain.MemoryModuleType; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.passive.MerchantEntity; import net.minecraft.entity.passive.VillagerEntity; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.loot.context.LootContext; import net.minecraft.loot.context.LootContextParameterSet; import net.minecraft.loot.context.LootContextParameters; import net.minecraft.loot.context.LootContextTypes; @@ -30,31 +33,63 @@ public VillagerMixin(EntityType entityType, World worl } @Inject(method = "tick", at = @At("HEAD"), cancellable = true) - private void tickInject(CallbackInfo ci) { - if (DisableVillagersMod.killVillagers && (!DisableVillagersMod.spareExperiencedVillagers || experience == 0) && !this.isDead()) { + private void onVillagerTickInject(CallbackInfo ci) { + if (DisableVillagersMod.isKillVillagers() && (!DisableVillagersMod.isSpareExperiencedVillagers() || experience == 0) && !this.isDead()) { this.kill(); ci.cancel(); } } @Inject(method = "isReadyToBreed", at = @At("HEAD"), cancellable = true) - private void isReadyToBreedInject(CallbackInfoReturnable cir) { - if (!DisableVillagersMod.breeding) { + private void onIsReadyToBreedInject(CallbackInfoReturnable cir) { + if (!DisableVillagersMod.isBreeding()) { cir.setReturnValue(false); } } - + + @Inject(method = "fillRecipes", at = @At("RETURN")) + private void onFillRecipesInject(CallbackInfo ci) { + if (!DisableVillagersMod.isTradeCycling()) { + VillagerEntity villager = (VillagerEntity) (Object) this; + + // Lock current profession and level + VillagerData data = villager.getVillagerData(); + villager.setVillagerData(new VillagerData( + data.getType(), + data.getProfession(), + data.getLevel() + )); + + // Prevent further changes by removing job site memory + villager.getBrain().forget(MemoryModuleType.JOB_SITE); + } + } + @Inject(method = "onInteractionWith", at=@At("HEAD"), cancellable = true) - private void finishConversionInject(EntityInteraction interaction, Entity player, CallbackInfo ci) { - if (DisableVillagersMod.curedZombieLoot != null && interaction == EntityInteraction.ZOMBIE_VILLAGER_CURED) { - DamageSource source = getWorld().getDamageSources().generic(); - LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder((ServerWorld) getWorld()).add(LootContextParameters.THIS_ENTITY, this).add(LootContextParameters.ORIGIN, this.getPos()).add(LootContextParameters.DAMAGE_SOURCE, source).addOptional(LootContextParameters.KILLER_ENTITY, source.getSource()).addOptional(LootContextParameters.DIRECT_KILLER_ENTITY, source.getSource()); - if (player instanceof PlayerEntity) { - builder.add(LootContextParameters.LAST_DAMAGE_PLAYER, (PlayerEntity) player).luck(((PlayerEntity) player).getLuck()); - } - DisableVillagersMod.curedZombieLoot.generateLoot(builder.build(LootContextTypes.ENTITY), this::dropStack); - this.kill(); - ci.cancel(); + private void onInteractionWithInject(EntityInteraction interaction, Entity player, CallbackInfo ci) { + if (DisableVillagersMod.getCuredZombieLoot() == null || interaction != EntityInteraction.ZOMBIE_VILLAGER_CURED) { + return; } + + ServerWorld world = (ServerWorld) getWorld(); + DamageSource source = world.getDamageSources().generic(); + + // When a zombie villager is cured, generate loot from the configured loot table + // and kill the villager instead of converting it to a normal villager + LootContextParameterSet.Builder builder = new LootContextParameterSet.Builder(world) + .add(LootContextParameters.THIS_ENTITY, this) + .add(LootContextParameters.ORIGIN, this.getPos()) + .add(LootContextParameters.DAMAGE_SOURCE, source) + .addOptional(LootContextParameters.KILLER_ENTITY, source.getSource()) + .addOptional(LootContextParameters.DIRECT_KILLER_ENTITY, source.getSource()); + + if (player instanceof PlayerEntity playerEntity) { + builder.add(LootContextParameters.LAST_DAMAGE_PLAYER, playerEntity).luck(playerEntity.getLuck()); + } + + DisableVillagersMod.getCuredZombieLoot().generateLoot(builder.build(LootContextTypes.ENTITY), this::dropStack); + + this.kill(); + ci.cancel(); } } diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/WanderingTraderManagerMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/WanderingTraderManagerMixin.java index dd058d9..ac46389 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/WanderingTraderManagerMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/WanderingTraderManagerMixin.java @@ -10,9 +10,10 @@ @Mixin(WanderingTraderManager.class) public class WanderingTraderManagerMixin { + @Inject(method = "spawn", at = @At("HEAD"), cancellable = true) - void blockWanderingTraders(ServerWorld world, boolean spawnMonsters, boolean spawnAnimals, CallbackInfoReturnable cir) { - if (DisableVillagersMod.disableWanderingTrader) { + private void onSpawnBlockWanderingTraders(ServerWorld world, boolean spawnMonsters,boolean spawnAnimals, CallbackInfoReturnable cir) { + if (DisableVillagersMod.isDisableWanderingTrader()) { cir.setReturnValue(0); } } diff --git a/src/main/java/com/stratecide/disable/villagers/mixin/ZombieVillagerMixin.java b/src/main/java/com/stratecide/disable/villagers/mixin/ZombieVillagerMixin.java index d576624..6d0ae21 100644 --- a/src/main/java/com/stratecide/disable/villagers/mixin/ZombieVillagerMixin.java +++ b/src/main/java/com/stratecide/disable/villagers/mixin/ZombieVillagerMixin.java @@ -28,15 +28,15 @@ public abstract class ZombieVillagerMixin extends ZombieEntity { @Shadow private UUID converter; - public ZombieVillagerMixin(World world) { + protected ZombieVillagerMixin(World world) { super(world); } @Inject(method = "interactMob", at=@At("HEAD"), cancellable = true) - private void interactMobInject(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) { - if (!DisableVillagersMod.curableZombies) { + private void onInteractMobInject(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) { + if (!DisableVillagersMod.isCurableZombies()) { ItemStack itemStack = player.getStackInHand(hand); - if (itemStack.getItem() == Items.GOLDEN_APPLE) { + if (itemStack.isOf(Items.GOLDEN_APPLE)) { cir.setReturnValue(ActionResult.PASS); } } diff --git a/src/main/resources/data/disable-villagers/default_config.json b/src/main/resources/data/disable-villagers/default_config.json new file mode 100644 index 0000000..cb92301 --- /dev/null +++ b/src/main/resources/data/disable-villagers/default_config.json @@ -0,0 +1,24 @@ +{ + "killVillagers": true, + "disableWanderingTrader": true, + "blockTrading": true, + "spareExperiencedVillagers": true, + "breeding": false, + "disableVillages": true, + "disableZombies": false, + "curableZombies": true, + "tradeCycling": true, + "curedZombieLoot": { + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:emerald_block" + } + ] + } + ] + } +} \ No newline at end of file