Warcraft III Neutral Shops And Mercenary Camps
Contract
OpenRealm's neutral-shop path is data-driven from unit/item tables rather than shop-specific rawcodes. A live unit with a non-empty
UnitProfile.sellItems (Sellitems / usei) or UnitProfile.makeItems (Makeitems / umki) is an item shop; a live unit with a non-empty UnitProfile.sellUnits (Sellunits /
useu) is a unit shop. Neutral Passive shops are public; owned/racial shops require ordinary G_UnitCanControl() authority. Enemy
shops are not made usable merely because they carry merchandise, and allied Aall sharing remains explicit future policy. A Neutral
Passive shop remains a neutral world selection: this does not grant the selecting player ordinary G_UnitCanControl() authority
over the building.
The supported purchase flows are:
selected neutral item shop
-> resolve nearby inventory patron for local player
-> build item buttons from Sellitems
-> validate stock/resources/inventory
-> SP_SpawnAtLocation(item)
-> G_PickupItem(patron, item)
selected neutral unit shop
-> resolve nearby Aneu-eligible patron for local player
-> build non-Hero unit buttons from Sellunits
-> validate stock/resources/food
-> SP_SpawnAtLocationNoBirth(unit)
-> find deterministic legal building exit
-> activate food for the purchasing player
Sellunits support in this path intentionally excludes Heroes. Taverns share the same object-data field but require separate Hero
availability, tier/limit, and neutral-Hero purchase rules; treating those as ordinary mercenaries would be incorrect.
Pawn/sell-back reuses inventory drag target mode. Dropping a pawnable carried item on an item shop while within the authored
GiveItemRange removes the item and refunds the local player using Misc.PawnItemRate.
Data Flow
| Behavior | Source |
|---|---|
| Item merchandise list | UnitProfile.sellItems (Sellitems / usei) and UnitProfile.makeItems (Makeitems / umki) |
| Unit merchandise list | UnitProfile.sellUnits (Sellunits / useu), non-Hero units only |
| Shop item-slot capacity | SetAllItemTypeSlots / SetItemTypeSlots, stored in edict.stock.item_slots |
| Shop unit-slot capacity | SetAllUnitTypeSlots / SetUnitTypeSlots, stored in edict.stock.unit_slots |
| Item gold/lumber price | ItemData.goldcost, ItemData.lumbercost |
| Unit gold/lumber/food price | UnitBalance.goldCost, lumberCost, foodUsed |
| Item maximum / replenish / start | ItemData.stockMax, stockRegen, stockStart |
| Unit maximum / replenish / start | UnitBalance.stockMax, stockRegen, stockStart |
| Pawn permission | Apit marker on the shop plus ItemData.pawnable |
| Pawn refund factor | Misc.PawnItemRate |
| Item handoff range | Misc.GiveItemRange |
| Neutral-shop activation range | Aneu/Aall DataA; 450 world-unit fallback for minimal/custom data |
A zero stockStart begins at stockMax. A positive start delay begins at zero stock; the first copy becomes available when the delay
expires, and further copies replenish every stockRegen until stockMax. Item and unit runtime stock are separate arrays on the shop
edict and are shared between players, so one player's purchase changes what every player sees.
Unit merchandise balance values resolve through the map-local war3map.w3u UnitBalance overlay before stock is initialized. This
includes usma (Stock Maximum), usrg (Stock Replenish Interval), usst (Stock Start Delay), and ordinary balance fields such as
unit cost/food when the map modifies them. Original-unit edits are also the inheritance source for custom unit IDs.
Item merchandise values use the matching map-local war3map.w3t ItemData overlay. isto (Stock Maximum), istr (Stock
Replenish Interval), isst (Stock Start Delay), price/model and other registered ItemData fields therefore override the base SLK row
before merchant stock is initialized. Original-item edits are the inheritance source for custom item IDs.
Stock updates are lazy: command-card queries and purchase attempts advance expired timers. No per-frame shop thinker is needed. The
stock fields are inline numeric edict_t state and are represented by stock_fields in g_save.c; save format 29 preserves item and
unit counts, each entry's effective maximum, plus absolute restock deadlines, while level.stock preserves the global item/unit slot
defaults. No F_EDICT pointer fixups are required.
Patron Selection
Warsmash's CAbilityNeutralBuilding retains one selected interaction unit per player and periodically reacquires it. OpenRealm's
current implementation resolves the patron deterministically when UI/purchase state is requested. It reads Aneu/Aall DataA for
activation radius and DataB for the authored interaction type (inventory unit, non-building, or any unit), then chooses the nearest
eligible living player-owned unit with entity number as a stable tie-break. Item shops without Aneu/Aall preserve their older
inventory-carrier rule; Sellunits has no safe interaction fallback and therefore requires valid Aneu/Aall DataB data.
Item purchase still requires inventory capacity even when Aneu is more permissive. Unit purchase does not incorrectly require an inventory. The selected item shop's inventory panel borrows the resolved inventory patron's display while the shop remains the selected world entity. OpenRealm still resolves patrons on demand rather than persisting Warsmash's explicit per-player selected patron.
Command Card And Purchase
Get_Commands_f() has a narrow neutral-shop exception to its normal control gate. G_GetShopButtons() combines authored Sellitems
and Makeitems plus supported Sellunits entries into the 12-slot command card. Item art/text/cost formatting continues to use ItemFunc/ItemStrings
and ItemData; unit art/text/cost formatting reuses the normal unit command-button path and UnitBalance.
A shop button click is handled before the ordinary G_UnitCanControl() check. The authoritative path first verifies that the selected
shop actually carries the raw object ID, then dispatches to item or unit purchase. Item purchase revalidates patron, stock, inventory
space, gold and lumber. Unit purchase revalidates patron, stock, gold, lumber and food before spawning anything.
Merchandise uses the unit profile's Sellitems and Makeitems lists, including bot shopping. Each item's ItemFunc.txt Requires list is checked when building its button, when selecting bot purchases, and again at purchase. A requirement category with DependencyOr is satisfied when any listed unit type is present; for example, retail phea requires TWN2, whose ItemFunc.txt DependencyOr lists hkee, ostr, etoa, and unp1. Moonstone and Lesser Clarity Potion have no such requirement and remain available without a Tree of Ages. Malformed requirement entries are logged and fail closed. These checks are independent from stockStart/stockRegen: unlocking a tier does not recreate or reset the shop's stock state.
Map-created item stock IDs inherit command-button art lookup from the item ID they were based on. The button command retains the map-created ID so JASS sale triggers can still distinguish the offered choice.
A successful mercenary hire is immediate rather than a training queue. The new unit is created directly for the purchasing player without a Birth presentation, moved to the same deterministic legal producer-exit search used by trained units, then has food activated. Gold/lumber and shared stock are deducted only after spawn/placement succeeds. Failures leave resources and stock unchanged.
JASS Stock Overrides
Warcraft campaign scripts can override neutral item and unit stock at runtime. OpenRealm registers AddItemToStock,
AddItemToAllStock, RemoveItemFromStock, RemoveItemFromAllStock, and the corresponding four unit-stock natives. Add natives use
their explicit currentStock and stockMax immediately, intentionally bypassing an authored stockStart delay for that runtime
entry. If current stock is below the supplied maximum, the item's or unit's authored stockRegen controls later replenishment.
Per-shop item forms require Warcraft Asid (Sell Items Dynamic); unit forms require Asud (Sell Units Dynamic). The all-shop forms
apply to currently live sellers carrying the corresponding ability. Runtime-added item/unit types participate in the same shared
command-card, purchase, save/load, and replenishment paths as authored merchandise. Removing stock removes only the merchandise entry;
it does not affect items already purchased or mercenaries already hired. SetAllItemTypeSlots / SetItemTypeSlots and their unit
counterparts still bound how many runtime entries each shop can carry.
Pawn / Sell Back
ItemDrag already owns right-click inventory drag target mode. Its entity callback now recognizes neutral item shops. A sale succeeds
only when:
- the dragged item is still carried by a local-player-controlled inventory unit;
- the target shop carries the
Apitpurchase-item marker ability; ItemData.pawnableis true;- carrier and shop are within
Misc.GiveItemRangeplus collision radii.
The refund is ceil(authored cost * PawnItemRate) independently for gold and lumber, matching the Warsmash behavior. The removed item
is not inserted into the shop's authored merchandise or stock.
Known Gaps
Aallshop-sharing/allied-building policy is not implemented.- OpenRealm does not yet retain/select a patron with Aneu's explicit
neutralinteractbutton or persistent selection indicator; it reacquires deterministically from range instead. - Walking an out-of-range Hero to the shop before pawning is not implemented; pawn targeting succeeds only when already in range.
- Neutral Hero/Tavern sales are not implemented; Hero entries in
Sellunitsare deliberately excluded from the mercenary path. - Unit-sale trigger events (
EVENT_UNIT_SELL/ player-unit sale context) are not published yet. - Power-up/auto-use-on-acquire item semantics remain part of the broader item lifecycle and are not special-cased by the shop.
- Item
Requires/DependencyOrprocessing currently uses completed unit-type counts; other requirement categories and amount semantics need separate evidence before expanding the contract.
Verification
games/warcraft-3/game/tests/t_items.c covers:
- nearby inventory-unit item-shop patron resolution;
- item purchase cost deduction and authoritative inventory handoff;
- Ancient of Wonders
Makeitemsmerchandise, ItemFuncRequires/DependencyOrbutton gating, and authoritative locked-purchase rejection; - malformed requirement rejection;
- out-of-range item purchase rejection with unchanged resources;
- shared item stock exhaustion and
stockRegenreplenishment; - runtime
AddItemToStockcurrent/max overrides, replenishment, removal, and all-shop native registration; - map-local
war3map.w3tItemData stock overrides and custom-item inheritance; - non-inventory mercenary patron resolution;
- immediate mercenary ownership, resource deduction, and food activation;
- unit
stockStart/stockRegen/stockMaxcommand availability; - runtime
AddUnitToStockcurrent/max overrides, replenishment, removal, and all-shop native registration; - map-local
war3map.w3uUnitBalance stock overrides and custom-unit inheritance; - failed mercenary food validation preserving stock/resources;
- pawnable item removal and
PawnItemRaterefund.
games/warcraft-3/game/tests/t_bot.c also verifies that Hero item policy buys unlocked merchandise from Makeitems and skips a
higher-priority item whose Requires gate is unsatisfied. games/warcraft-3/game/tests/t_game.c additionally round-trips both item and
unit shop stock/timer state through the save schema. The
test fixture's spro, moon, plcl, phea, oven, and nmer rows carry explicit price/food/stock metadata; its phea ItemFunc row requires TWN2, with the retail four-race TWN2.DependencyOr list in ItemFunc. The focused gameplay suite is
+dedicated 1 +test 'wc3_items.*'; save changes must also run the save tests and the full suite required by CONTRIBUTING.md.