
VillageAI
Intelligent village defense system. Villages centered on bells automatically detect nearby villagers. Friendly players gain reputation by trading. Hostile players lose reputation by attacking villagers. When hostile players are nearby reputation drops belo
3.3.1
Compatibility
Changes
[3.3.1] - 2026-05-16 โ Critical Bug Fixes
๐ Bug Fixes
๐ด Critical โ Thread Safety Violations
-
Autosave called
buildVillagesConfig()on async thread โstartAutosaveTask()usedrunTaskTimerAsynchronously, which meantbuildVillagesConfig()(and theVillageSnapshotconstructor it invokes) accessedVillage.getCenter().getWorld()โ a Bukkit API call โ off the main thread. This could cause race conditions, corrupted snapshots, orNullPointerExceptionunder load. Fixed: the scheduler is now changed torunTaskTimerso the config snapshot is always built on the main thread; the file write still runs asynchronously viasaveVillagesAsync(). -
loadVillages()restored villages on async thread โ ThethenAccept()callback ofloadVillagesAsync()ran on the common ForkJoinPool thread. Inside it,snap.restore(getServer())calledserver.getWorld(UUID)โ a Bukkit API that is not thread-safe off the main thread. Fixed: the restore logic is now scheduled back onto the main thread viaBukkit.getScheduler().runTask(), while the file read remains async.
๐ก Medium โ Incorrect Logic
-
Raid reward silently capped at 64 emeralds โ
RaidManager.onRaidComplete()passedMath.min(totalEmeralds, 64)toInventory.addItem(), so players completing a 5-wave raid (320 emeralds) received only 64 while the chat message correctly announced the full amount. Fixed: the full reward is now distributed across multiple 64-stack inventory adds, with any overflow dropped naturally at the player's feet. -
decayReputation(boolean passiveGain)ignored its parameter โ ThepassiveGainargument was accepted but the method body never read it. Whenpassive_gain: falsein config, positive reputation was supposed to decay toward 0 symmetrically with negative decay, but instead stayed frozen forever. Fixed: whenpassiveGainisfalse, positive reputation now decrements by 1 per decay cycle (matching the existing negative-decay behaviour). -
VillageEconomy.analyzeVillageNeeds()read file from disk on every economy update โupdateEconomy()calledanalyzeVillageNeeds(), which calledYamlConfiguration.loadConfiguration(new File(...))โ synchronous disk I/O on the main tick thread โ once every 24 hours. While rare, this blocked the server thread for the duration of the file read. Fixed:economy_config.ymlis now loaded once into acachedEcoConfigfield at construction and can be refreshed via the newreloadEconomyConfig()method (called automatically by/villageai reload). No disk I/O occurs during tick.
๐ข Low โ Defensive Code Quality
openGuisinEconomyCommandused a plainHashMapโ Event handlers (GuiListener) and command handlers both access this map; usingConcurrentHashMapis consistent with every other map in the plugin and eliminates any theoretical ordering hazard. Changed fromHashMaptoConcurrentHashMap.
๐ง Technical Details
Thread Safety
VillageAIPlugin.startAutosaveTask():runTaskTimerAsynchronouslyโrunTaskTimer; file write remains asyncVillageAIPlugin.loadVillages(): restore logic wrapped inBukkit.getScheduler().runTask()to guarantee main-thread executionEconomyCommand.openGuis:HashMapโConcurrentHashMap
Economy Config Caching
VillageEconomynow holds acachedEcoConfigfield populated once in the constructor- New public method
reloadEconomyConfig()allows hot-reload without restarting the server analyzeVillageNeeds()readscachedEcoConfigin memory โ zero disk I/O during tick
Reputation System
decayReputation(false): positive reputation now correctly decays by 1 per decay intervaldecayReputation(true)(default): behaviour unchanged โ positive reputation is stable
๐ Performance Impact
- Autosave: eliminates risk of main-thread lag from
VillageSnapshotconstruction racing with async writes - Economy tick: removes all disk I/O from the tick path โ measurable improvement on servers with slow storage
- No performance regression on any other path
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:QkbM4VXn:1MqR0qqT"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:QkbM4VXn:1MqR0qqT"
}
