A Cuberite plugin that adds "sort of working" villager trading. Cuberite's Lua API does not expose the real villager profession, so by default the plugin gives every villager its own virtual profession, a persistent identifier and a trade list that is refreshed over time.
Note: the engine's internal profession (
cVillager::m_Type) can now be read safely throughtolua.casttype reinterpretation - see villager_profession.lua and the research notes (verified on both x86-64 LP64 and 32-bit armv6l ILP32 servers, matched field-by-field against the chunk NBTProfession). The plugin keeps its own assigned profession as the source of truth and now writes it back to the engine ([Features] AlignRealProfession), so the visible appearance (1.8-1.12 clients render by profession), farmer AI and zombie-villager conversion match the trades. Villager names also became readable English<Profession> <Name>([Features] ReadableNames), with automatic migration.
- Right-click a villager to open the trade window (the vanilla villager window: two input slots, one result slot, plus your inventory).
- Sneak + right-click a villager to list its trades in chat instead of opening the window.
- Per-villager identity: each villager gets a
vt-<profession>-<random>id stored in its CustomName (persisted in the world) and its own XP / last-refresh age invillager_data.txt. - Virtual professions
vtFarmer…vtGeneric(seetrades.txt); a villager only offers trades of its own profession. - XP levels: trading grants the villager XP and drops experience orbs for the player; levels 1–3 at 100 / 300 / 600 XP unlock more trades.
- Trade refresh: a villager's trade list is regenerated after 5 minutes of world time.
- Name-tag protection: name tags cannot rename a plugin-managed villager.
- Villager spawn eggs: craft 1 emerald + 1 egg, or buy one from a
vtGenericvillager (the crafting recipe has a switch insettings.ini). - Optional iron golem guard (off by default): lets existing iron golems attack nearby hostiles, and can spawn village guards on a periodic condition check — see "Optional iron golem guard" below.
Copy the VillagerTrade folder into <server>/Plugins/ and restart the server (or reload
plugins). Developed and tested against Cuberite's 1.12.2 protocol; the trade window layout
matches the vanilla villager window.
[Features]
EnableVillagerSpawnEggCrafting=1 ; 1 emerald + 1 egg = 1 villager spawn egg
[VillageLife] ; village chunk scanning (off by default)
ChunksPerTick=2
SaveIntervalSeconds=30
[IronGolem] ; iron golems (off by default)
EnableGuard=0 ; let existing golems guard (target injection)
EnableVillageGolemSpawning=0 ; spawn village guards on a periodic condition check
MinDoorsInChunk=2
MinVillagers=3
MaxGolemsPerVillage=2
VillageGolemRadius=64
VillageCheckSeconds=30-
[IronGolem] EnableGuard: lets existing iron golems attack nearby hostiles. Cuberite'scIronGolemcombat AI is complete, but as a neutral mob it only acquires a target when damaged, so the plugin only does two thin things: fake one attack to inject the target, and issue the chase path. Chasing, attacking, cooldowns, knockback and difficulty scaling stay engine-side; the simulated damage is healed back immediately (net loss 0). -
[IronGolem] EnableVillageGolemSpawning: spawns guards on a periodic condition check — nothing is spawned while scanning a chunk. EveryVillageCheckSecondsit re-checks the scanned door-bearing village chunks and requires «doors ≥ MinDoorsInChunk» and «villagers within VillageGolemRadius ≥ MinVillagers» and «golems within the radius < MaxGolemsPerVillage» before spawning one guard (VillageGolemRadiusmust cover the village span, otherwise adjacent chunks can bypass the cap). -
Village chunks are recorded by the
[VillageLife]scan and persisted invillage_scanned.txt(one line:<world> <cx> <cz> <doors> [door coords]; delete the file to re-scan everything once). -
Console:
villagelife(scan status),villagelife flush,villagelife scan <cx> <cz>,golemguard. -
Note: villagers are no longer spawned by this plugin (the former "villagers in newly generated villages" feature was removed and is planned to move to a separate plugin), so the
MinVillagerscondition must be satisfied by villagers from another source. -
Write keys without spaces around
=. Cuberite counts those spaces as part of the key name, the lookup fails, and the option silently falls back to its default. -
Reload the plugin after editing (
/reloador the consolereload).
| Action | Effect |
|---|---|
| Put items into slot 0 / 1 | Trade inputs (slot 1 is only used by two-input trades) |
| Left-click the result slot (2) | Perform one trade |
| Shift + left-click the result slot | Perform as many trades as fit; the output is placed into your inventory (existing stacks are filled first, then empty slots) |
| Right-click the result slot | Cycle through every trade that currently matches the inputs |
| Shift + right-click on slots 0–2 | Disabled |
| Close the window | Unused inputs are dropped at your feet; the result preview is discarded |
The result slot is owned by the plugin: it never accepts items, and a preview that was not paid for disappears as soon as the inputs stop matching — it cannot be looted.
Several entries in trades.txt can match the same inputs (for example 1 emerald matches every
emerald -> … entry whose (min,max) range allows one). The result slot always shows the
currently selected entry, and right-clicking the result slot cycles to the next match.
While more than one entry matches, the plugin reports the selection in chat:
[VillagerTrade] 交易 2/4:2x Emerald -> 1x White Wool
Item names are rendered by the client in the player's own language (the plugin sends
translate text components instead of internal English names): a Chinese client shows
绿宝石 / 白色羊毛, an English one shows Emerald / White Wool. See item_l10n.lua.
The selection resets to the first match whenever the set of matching trades changes (for example after you add or remove an input), so you always start from a predictable state. If two entries look identical, cycling is still meaningful for randomly generated outputs: each entry rolls its own counts and enchantments when the list is generated.
<Input1>, (<min>,<max>) [; <Input2>, (<min>,<max>)] = <Output>, (<min>,<max>) | <Weight> | <Profession> | <UnlockLevel> | <TradeXp>
- Item names use Cuberite's spelling (
lapislazuli,chainmain_leggings— the official typo) or a numeric item id. ^damagesets the item damage (e.g.spawn_egg^120);-"<Enchantments>"sets enchantments ("id=lvl;…"or"ByXpLevels(min,max)").Weightis the probability (0–1) that the entry is included when a trade list is generated.Profession: 0 = vtFarmer, 1 = vtLibrarian, 2 = vtPriest, 3 = vtBlacksmith, 4 = vtButcher, 5 = vtGeneric.UnlockLevelis 0–3.- Unknown item names are logged to the console instead of silently producing an empty trade.
villager_data.txt, one line per villager:
<villagerID> = <profession> | <xp1> | … | <xp6> | <lastRefreshWorldAge>
Written when a player leaves, every 5 minutes, and when the plugin unloads or the server stops.
-1 in the last field means "never refreshed".
- Shift + right-click has no behaviour (blocked on purpose).
- Cuberite's default click handling can leave zero-count "ghost" items behind after a pick-up; the plugin treats such slots as empty.
- Villager professions are virtual — the Lua API cannot read the real villager profession.
- Trade lists live in memory and are regenerated on demand; only the refresh age is persisted.
player_trades.txt/player_trade_experience.txtare v1 files kept only for migration.- Randomness: the plugin uses its own 32-bit LCG instead of
math.random, so it neither depends on nor disturbs the process-global RNG that the engine and other plugins share.