Skip to content

Generated reference. This page mirrors Code/Vendors/README.md, this module's source-of-truth documentation (Standards/02_DOCUMENTATION_STANDARDS.md ยง1). Edit it there, not here -- this file is regenerated on every docs build and any local edit is silently overwritten.

Vendors

Purpose. A static, ownerless shop entity -- ROADMAP.md Milestone 14 is explicit: "nothing about a shop actually requires a person behind the counter." Reuses WorldContainer's placeable/registerable shape (a plain id generated once at spawn, a persisted record loaded idempotently, an in-memory Find lookup), not its ownability -- a vendor is never bought, never has an owner, and never gates access. Ships well before Milestone 18's NPC framework on purpose; that milestone may upgrade Vendors to NPC-driven later as a pure enhancement, not a rebuild. See 27_VENDORS.md for the full design.

Responsibilities. - Vendor -- Guid Id, DisplayName, CatalogueId. Deliberately not an OwnableId/ Ownership consumer -- see that type's own header comment for why. - VendorListing / VendorCatalogue -- plain [Property]-bearing content types, the same shape as NeedDefinition/NeedsConfiguration.Definitions. A listing is an ItemDefinition.AssetId and a Price; a catalogue is a named, keyed list of listings. - VendorConfiguration -- every named catalogue content authors maintain, plus the shared InteractionRange and PurchaseDuration. One asset per server, loaded once at VendorModule.Configure, following DoorConfiguration.cs's precedent. - IVendorService / VendorStore -- registers a placed vendor's persisted record (RegisterAsync, load-or-create, called from VendorComponent.OnAwake), and PurchaseAsync: resolve the vendor's catalogue and the requested item's listing, refuse if not sold here or unaffordable, then grant the item to the buyer's inventory and debit the price -- in that order. See that method's own doc comment for the full ordering rationale and why the concurrency guard is keyed by buyer, not by vendor (both deliberate deviations from PropertyStore.PurchaseAsync's shape, explained in full there). - VendorComponent -- the placed-in-map entity: [Property] VendorId/DisplayName/ CatalogueId, and [Rpc.Host] RequestBuyItem(string itemAssetId). Unlike DoorComponent.RequestPurchaseDoor/VehicleComponent.RequestPurchaseVehicle's instant bare RPC, buying is a channelled Interaction verb ("buy_item") -- see this file's header comment. - VendorModule -- registers IVendorService and the VendorDocument schema.

Not responsible for. - Moving money -- Economy's job; VendorStore calls IEconomyService.CanAfford/ DebitAsync, it never touches a balance directly. - Granting the item into an inventory slot -- Inventory's job; VendorStore calls IInventoryService/CharacterInventoryHolder, the same collaborators InventoryModule.GiveAsync itself uses. - Finite/depletable stock. Every listing is unlimited this pass -- see "Future improvements." VendorListing carries no stock count. - A client-facing catalogue/browsing UI. RequestBuyItem requires the caller to already know an ItemAssetId to ask for -- no panel lists a vendor's prices yet. The same "real backend, no reachable UI yet" gap InventoryPanel's own disabled TODO and Crime's fully-tested-but-unreachable ICrimeService already established elsewhere in this codebase. See "Future improvements." - AI, pathing, or any person behind the counter. Explicitly out of scope for this build item -- ROADMAP.md Milestone 18 is where that belongs, not here. - Licence-gated listings. VendorItemPurchasing is published specifically so a future Licences consumer (this same milestone's sibling build item) can veto a purchase without Vendors depending on Licences at all -- nothing subscribes to it yet.

Dependencies. Core (IServiceRegistry, IServiceResolver, IPersistenceBackend, ISchemaRegistry, IEventBus, IGameLog), Character (Character, CharacterId, ConnectionExtensions.GetCharacter()), Economy (IEconomyService), Items (IItemRegistry, ItemDefinition), Inventories (IInventoryService, InventoryConfiguration, CharacterInventoryHolder), Movement (IPawnLocator, for the distance check), Interaction (InteractionRequest, InteractionComponent, StillInRange), the S&box engine (Component, GameResource, Rpc, Sync). Deliberately not dependent on Ownerships -- see Vendor.cs.

Public API. - IVendorService -- RegisterAsync(Guid, string displayName, string catalogueId), Find(Guid), PurchaseAsync(Character buyer, Vendor vendor, string itemAssetId). - VendorComponent -- RequestBuyItem(string itemAssetId). - Vendor, VendorListing, VendorCatalogue, VendorConfiguration. - VendorItemPurchasing, VendorItemPurchased.

Events published. - VendorItemPurchasing -- cancellable, published once eligibility (sold here, affordable) is confirmed but before the item is granted or money moves. Mirrors Applejack.World.DoorPurchasing exactly. - VendorItemPurchased(Vendor, Character, ItemDefinition, int Count) -- published once the item has been granted. Mirrors Applejack.World.DoorPurchased, extended with which item and how many since (unlike a door) the entity itself doesn't change hands.

Persistence. VendorDocument { Id, DisplayName, CatalogueId } under vendors/{id}, schema version 1. The granted ItemInstance lives in the buyer's own inventories/{characterId} document (Inventories' persistence, unchanged by this module) -- no new persistence mechanism. No ownership/{entityId} document is ever created for a vendor.

Not unit-tested. VendorConfiguration.PostLoad's validation (empty/duplicate catalogue id, empty/duplicate item asset id within a catalogue, non-positive price/InteractionRange) has no test. Checked first: no GameResource.PostLoad override anywhere in this codebase has one either (ItemDefinition, PersistenceConfiguration, DoorConfiguration, NeedsConfiguration) -- PostLoad is engine-invoked only, and nothing calls it manually from a test. See Code/Needs/README.md's identical note.

Future improvements. - Finite, depletable per-listing stock, with restocking. Needs mutable per-vendor persisted state this pass deliberately omits -- see VendorListing's own doc comment. - A client-facing shop panel listing a vendor's catalogue and prices, so RequestBuyItem doesn't require the caller to already know an ItemAssetId. - A Licences subscriber to VendorItemPurchasing, gating a weapon-category listing behind a held licence -- the concrete, named use case this event exists for. - An NPC-driven upgrade once Milestone 18's NPC framework lands (ROADMAP.md Milestone 18's own sequencing note names this explicitly) -- a pure enhancement over the existing IVendorService/VendorStore, not a rebuild. - Buying more than one unit per RequestBuyItem call -- this pass always buys exactly 1.