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.