载入中...
搜索中...
未找到
InventoryControl.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
4
5#include <algorithm>
6#include <limits>
7#include <string>
8#include <utility>
9
10namespace eve::inventory {
11namespace {
12
13template <typename T>
14Result<T> failure(DiagnosticCode code, std::string message, std::string path) {
16}
17
18LogicalId id(std::string_view text) { return LogicalId::parse(text).value(); }
19
20Result<const Value::Object*> object(const Value& value) {
21 const auto* result = value.getIf<Value::Object>();
22 if (!result)
23 return failure<const Value::Object*>(DiagnosticCode::InvalidArgument, "inventory parameters must be an object",
24 "parameters");
26}
27
28Result<std::string> text(const Value::Object& value, std::string_view name, std::string fallback = {}) {
29 const auto found = value.find(std::string(name));
30 if (found == value.end()) return Result<std::string>::success(std::move(fallback));
31 if (!found->second.isString())
32 return failure<std::string>(DiagnosticCode::InvalidArgument, "inventory parameter must be a string",
33 "parameters." + std::string(name));
34 return Result<std::string>::success(found->second.asString());
35}
36
37Result<int> integer(const Value::Object& value, std::string_view name, int fallback) {
38 const auto found = value.find(std::string(name));
39 if (found == value.end()) return Result<int>::success(fallback);
40 if (!found->second.isInt64() || found->second.asInt() < std::numeric_limits<int>::min() ||
41 found->second.asInt() > std::numeric_limits<int>::max())
42 return failure<int>(DiagnosticCode::InvalidArgument, "inventory parameter must be an integer",
43 "parameters." + std::string(name));
44 return Result<int>::success(static_cast<int>(found->second.asInt()));
45}
46
47const std::string kAddItem{"inventory:add-item"};
48const std::string kRemoveItem{"inventory:remove-item"};
49const std::string kMoveSlot{"inventory:move-slot"};
50const std::string kEquip{"inventory:equip"};
51const std::string kUnequip{"inventory:unequip"};
52
53} // namespace
54
56 cap::addListener<IGameplayControlProvider>(this);
57 cap::addListener<IGameplayInstanceCatalog>(this);
58}
59
61 cap::removeListener<IGameplayInstanceCatalog>(this);
62 cap::removeListener<IGameplayControlProvider>(this);
63}
64
66 if (!instance.isValid() || !owner.isValid())
68 DiagnosticCode::InvalidArgument, "inventory publication needs valid instance and owner ids", "instance"));
69 if (find(instance) != nullptr)
71 DiagnosticCode::Conflict, "a gameplay control already owns that inventory instance", "instance"));
72 Entry entry;
73 entry.instance = instance;
74 entry.owner = owner;
75 entry.bag = &bag;
76 entry.equipment = equipment;
77 entries_.push_back(std::move(entry));
78 return Result<void>::success();
79}
80
82 const auto found = std::find_if(entries_.begin(), entries_.end(),
83 [instance](const Entry& entry) { return entry.instance == instance; });
84 if (found == entries_.end())
86 Diagnostic::error(DiagnosticCode::NotFound, "that inventory instance is not published", "instance"));
87 entries_.erase(found);
88 return Result<void>::success();
89}
90
91void InventoryControl::clear() { entries_.clear(); }
92
93int InventoryControl::count() const { return static_cast<int>(entries_.size()); }
94
95std::vector<SubjectRef> InventoryControl::instances() const {
96 std::vector<SubjectRef> result;
97 result.reserve(entries_.size());
98 for (const auto& entry : entries_) result.push_back(entry.instance);
99 return result;
100}
101
102InventoryControl::Entry* InventoryControl::find(SubjectRef instance) {
103 const auto found = std::find_if(entries_.begin(), entries_.end(),
104 [instance](const Entry& entry) { return entry.instance == instance; });
105 return found == entries_.end() ? nullptr : &*found;
106}
107
108const InventoryControl::Entry* InventoryControl::find(SubjectRef instance) const {
109 const auto found = std::find_if(entries_.begin(), entries_.end(),
110 [instance](const Entry& entry) { return entry.instance == instance; });
111 return found == entries_.end() ? nullptr : &*found;
112}
113
114std::string_view InventoryControl::gameplayDomain() const noexcept { return "inventory"; }
115
116std::vector<SubjectRef> InventoryControl::gameplayInstances() const { return instances(); }
117
118bool InventoryControl::controls(const GameplaySession& session, const Entry& entry) const {
119 return session.access != GameplayAccess::PlayerEquivalent ||
120 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), entry.owner) !=
121 session.controlledSubjects.end();
122}
123
124Value InventoryControl::bagState(const Entry& entry) const {
125 Bag* bag = entry.bag;
127 if (bag != nullptr) {
128 const int slotCount = bag->getSlotCount();
129 for (int slot = 0; slot < slotCount; ++slot) {
130 const std::string itemId = bag->getSlotItemId(slot);
131 if (itemId.empty()) continue;
132 slots.emplace_back(
133 Value::Object{{"slot", Value(static_cast<std::int64_t>(slot))},
134 {"itemId", Value(itemId)},
135 {"quantity", Value(static_cast<std::int64_t>(bag->getSlotQuantity(slot)))},
136 {"instanceId", Value(static_cast<std::int64_t>(bag->getSlotInstanceId(slot)))}});
137 }
138 }
139 Value::Object state{{"bagId", Value(bag != nullptr ? bag->getId() : std::string{})},
140 {"kind", Value(bag != nullptr ? bag->getKind() : std::string{})},
141 {"slots", Value(std::move(slots))}};
142 if (bag != nullptr) {
143 state.emplace("slotCount", Value(static_cast<std::int64_t>(bag->getSlotCount())));
144 state.emplace("usedWeight", Value(static_cast<double>(bag->getUsedWeight())));
145 state.emplace("maxWeight", Value(static_cast<double>(bag->getMaxWeight())));
146 state.emplace("usedVolume", Value(static_cast<double>(bag->getUsedVolume())));
147 state.emplace("maxVolume", Value(static_cast<double>(bag->getMaxVolume())));
148 state.emplace("capacityPolicy", Value(bag->getCapacityPolicy()));
149 }
150 state.emplace("equipment", equipmentState(entry));
151 return Value(std::move(state));
152}
153
154Value InventoryControl::equipmentState(const Entry& entry) const {
155 EquipmentSet* equipment = entry.equipment;
157 if (equipment != nullptr) {
158 const int slotCount = equipment->getSlotCount();
159 for (int index = 0; index < slotCount; ++index) {
160 const std::string slotName = equipment->getSlotName(index);
161 if (slotName.empty()) continue;
162 Value::Object slot{{"slot", Value(slotName)}};
163 const std::string itemId = equipment->getSlotItemId(slotName);
164 if (!itemId.empty()) {
165 slot.emplace("itemId", Value(itemId));
166 slot.emplace("quantity", Value(static_cast<std::int64_t>(equipment->getSlotQuantity(slotName))));
167 slot.emplace("instanceId", Value(static_cast<std::int64_t>(equipment->getSlotInstanceId(slotName))));
168 }
169 slots.emplace_back(std::move(slot));
170 }
171 }
172 return Value(std::move(slots));
173}
174
176 SubjectRef instance) const {
177 const Entry* entry = find(instance);
178 if (entry == nullptr)
179 return failure<GameplayObservation>(DiagnosticCode::NotFound, "inventory gameplay instance was not found",
180 "instance");
181 if (!controls(session, *entry))
182 return failure<GameplayObservation>(DiagnosticCode::PreconditionViolation,
183 "session does not control the inventory owner", "instance");
184 GameplayObservation observation;
185 observation.domain = id("gameplay:inventory");
186 observation.instance = entry->instance;
187 observation.tick = entry->tick;
188 observation.revision = entry->revision;
189 observation.state = bagState(*entry);
190 return Result<GameplayObservation>::success(std::move(observation));
191}
192
194 SubjectRef instance,
195 SubjectRef subject) const {
196 auto observed = observeGameplay(session, instance);
197 if (!observed) return Result<std::vector<GameplayActionDescriptor>>::failure(observed.status());
198 const Entry* entry = find(instance);
199 if (entry == nullptr)
200 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::NotFound,
201 "inventory gameplay instance was not found", "instance");
202 if (subject != entry->owner)
203 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::PreconditionViolation,
204 "inventory action subject must be its owner", "subject");
205
206 const Value stringType(Value::Object{{"type", Value("string")}});
207 const Value integerType(Value::Object{{"type", Value("integer")}});
208 std::vector<GameplayActionDescriptor> actions{
209 {id(kRemoveItem), Value(Value::Object{{"itemId", stringType}, {"quantity", integerType}})},
210 {id(kMoveSlot),
211 Value(Value::Object{{"fromSlot", integerType}, {"toSlot", integerType}, {"quantity", integerType}})},
212 };
213 // A set without slots carries no equipment authority, so discovery stays
214 // truthful: everything advertised here can actually succeed.
215 if (entry->equipment != nullptr && entry->equipment->getSlotCount() > 0) {
216 actions.push_back({id(kEquip), Value(Value::Object{{"bagSlot", integerType}, {"equipSlot", stringType}})});
217 actions.push_back({id(kUnequip), Value(Value::Object{{"equipSlot", stringType}})});
218 }
219 // Granting items is not a player action; it is advertized only to the
220 // profiles that are allowed to use it, so discovery never offers an action
221 // the same session would be refused.
223 actions.push_back({id(kAddItem), Value(Value::Object{{"itemId", stringType}, {"quantity", integerType}})});
224 }
225 return Result<std::vector<GameplayActionDescriptor>>::success(std::move(actions));
226}
227
229 const GameplayCommand& command) {
230 Entry* entry = find(instance);
231 if (entry == nullptr)
232 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "inventory gameplay instance was not found",
233 "instance");
234 if (!controls(session, *entry) || command.subject != entry->owner)
235 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
236 "session does not control the inventory owner", "command.subject");
237 if (command.id.empty())
238 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument, "command id must not be empty",
239 "command.id");
240 if (command.observedTick != entry->tick || command.expectedRevision != entry->revision)
241 return failure<GameplayCommandReceipt>(
242 DiagnosticCode::Conflict, "inventory command was based on a stale observation", "command.expectedRevision");
243 auto values = object(command.parameters);
245 const Value::Object& parameters = *values.value();
246
247 Bag* bag = entry->bag;
248 EquipmentSet* equipment = entry->equipment;
249 if (bag == nullptr)
250 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "inventory instance lost its container",
251 "instance");
252
253 const std::string action = command.action.format();
254 std::string detail;
255 std::string eventType;
256 // How much the authority actually moved. `Bag` removal is "take up to N",
257 // so an agent that asks for more than the container holds gets a success
258 // with the applied amount disclosed here rather than a silent full grant.
259 std::int64_t applied = 0;
260 if (action == kAddItem) {
262 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
263 "adding items requires the test-driver or developer-cheat profile",
264 "session.access");
265 auto itemId = text(parameters, "itemId");
266 auto quantity = integer(parameters, "quantity", 1);
268 if (!quantity) return Result<GameplayCommandReceipt>::failure(quantity.status());
269 if (itemId.value().empty() || quantity.value() <= 0)
270 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
271 "add-item needs a non-empty itemId and a positive quantity",
272 "command.parameters");
273 if (!bag->canAddItem(itemId.value(), quantity.value()))
274 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
275 bag->canAddItemReason(itemId.value(), quantity.value()),
276 "command.parameters.itemId");
277 const int added = bag->addItem(itemId.value(), quantity.value());
278 if (added <= 0)
279 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "bag rejected the granted items",
280 "command.parameters");
281 detail = "added " + std::to_string(added) + " " + itemId.value();
282 eventType = "inventory.item-added";
283 applied = added;
284 } else if (action == kRemoveItem) {
285 auto itemId = text(parameters, "itemId");
286 auto quantity = integer(parameters, "quantity", 1);
288 if (!quantity) return Result<GameplayCommandReceipt>::failure(quantity.status());
289 if (itemId.value().empty() || quantity.value() <= 0)
290 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
291 "remove-item needs a non-empty itemId and a positive quantity",
292 "command.parameters");
293 const int removed = bag->removeItem(itemId.value(), quantity.value());
294 if (removed <= 0)
295 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation, "bag does not hold that item",
296 "command.parameters.itemId");
297 detail = "removed " + std::to_string(removed) + " " + itemId.value();
298 eventType = "inventory.item-removed";
299 applied = removed;
300 } else if (action == kMoveSlot) {
301 auto fromSlot = integer(parameters, "fromSlot", -1);
302 auto toSlot = integer(parameters, "toSlot", -1);
303 auto quantity = integer(parameters, "quantity", 1);
304 if (!fromSlot) return Result<GameplayCommandReceipt>::failure(fromSlot.status());
305 if (!toSlot) return Result<GameplayCommandReceipt>::failure(toSlot.status());
306 if (!quantity) return Result<GameplayCommandReceipt>::failure(quantity.status());
307 if (fromSlot.value() < 0 || toSlot.value() < 0 || quantity.value() <= 0)
308 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
309 "move-slot needs non-negative slots and a positive quantity",
310 "command.parameters");
311 if (!bag->splitStack(fromSlot.value(), quantity.value(), toSlot.value()))
312 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
313 "bag could not move that stack", "command.parameters.fromSlot");
314 detail = "moved " + std::to_string(quantity.value()) + " from slot " + std::to_string(fromSlot.value());
315 eventType = "inventory.slot-moved";
316 applied = quantity.value();
317 } else if (action == kEquip) {
318 if (equipment == nullptr)
319 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported,
320 "this inventory instance has no equipment set", "command.action");
321 auto bagSlot = integer(parameters, "bagSlot", -1);
322 auto equipSlot = text(parameters, "equipSlot");
323 if (!bagSlot) return Result<GameplayCommandReceipt>::failure(bagSlot.status());
324 if (!equipSlot) return Result<GameplayCommandReceipt>::failure(equipSlot.status());
325 std::string reason;
326 if (bagSlot.value() < 0 || equipSlot.value().empty() ||
327 !equipment->canEquipFromBag(bag, bagSlot.value(), equipSlot.value(), &reason))
328 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
329 reason.empty() ? "item cannot be equipped into that slot" : reason,
330 "command.parameters.equipSlot");
331 if (!equipment->equipFromBag(equipSlot.value(), bag, bagSlot.value()))
332 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "equip was rejected by the equipment set",
333 "command.parameters.equipSlot");
334 detail = "equipped into " + equipSlot.value();
335 eventType = "inventory.equipped";
336 applied = equipment->getSlotQuantity(equipSlot.value());
337 } else if (action == kUnequip) {
338 if (equipment == nullptr)
339 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported,
340 "this inventory instance has no equipment set", "command.action");
341 auto equipSlot = text(parameters, "equipSlot");
342 if (!equipSlot) return Result<GameplayCommandReceipt>::failure(equipSlot.status());
343 if (equipSlot.value().empty() || equipment->isSlotEmpty(equipSlot.value()))
344 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation, "equipment slot is empty",
345 "command.parameters.equipSlot");
346 const int carried = equipment->getSlotQuantity(equipSlot.value());
347 if (!equipment->unequipToBag(equipSlot.value(), bag))
348 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "unequip was rejected by the equipment set",
349 "command.parameters.equipSlot");
350 detail = "unequipped " + equipSlot.value();
351 eventType = "inventory.unequipped";
352 applied = carried;
353 } else {
354 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported, "unsupported inventory action",
355 "command.action");
356 }
357
358 ++entry->revision;
359 GameplayEvent event;
360 event.sequence = entry->nextEventSequence++;
361 event.tick = entry->tick;
362 event.type = eventType;
363 event.subject = entry->owner;
364 event.causationCommandId = command.id;
365 event.correlationId = command.id;
366 event.payload = Value(Value::Object{
367 {"instance", Value(entry->instance.format())}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
368 entry->events.push_back(std::move(event));
369
371 receipt.commandId = command.id;
372 receipt.executionId = command.id + ".executed";
373 receipt.acceptedTick = entry->tick;
374 receipt.resultingRevision = entry->revision;
375 receipt.details =
376 Value(Value::Object{{"action", Value(action)}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
378}
379
381 const SimulationStep& step) {
382 Entry* entry = find(instance);
383 if (entry == nullptr)
384 return failure<GameplayObservation>(DiagnosticCode::NotFound, "inventory gameplay instance was not found",
385 "instance");
386 if (step.tick <= entry->tick)
387 return failure<GameplayObservation>(DiagnosticCode::Conflict, "inventory simulation tick must increase",
388 "step.tick");
389 auto observed = observeGameplay(session, instance);
390 if (!observed) return Result<GameplayObservation>::failure(observed.status());
391 entry->tick = step.tick;
392 return observeGameplay(session, instance);
393}
394
396 std::uint64_t afterSequence) const {
397 auto observed = observeGameplay(session, instance);
398 if (!observed) return Result<std::vector<GameplayEvent>>::failure(observed.status());
399 const Entry* entry = find(instance);
400 if (entry == nullptr)
401 return failure<std::vector<GameplayEvent>>(DiagnosticCode::NotFound,
402 "inventory gameplay instance was not found", "instance");
403 std::vector<GameplayEvent> result;
404 for (const auto& event : entry->events)
405 if (event.sequence > afterSequence) result.push_back(event);
406 const bool empty = result.empty();
407 return Result<std::vector<GameplayEvent>>::success(std::move(result),
409}
410
411} // namespace eve::inventory
double value
LogicalId itemId
int subject
Definition AnimSmr.cpp:163
std::map< std::string, Var > values
std::string message
DiagnosticCode code
std::string text
std::string name
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
int detail
bool found
int removed
std::string string
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
std::optional< InteractionSession > session
Definition Tactics.cpp:39
Json object
float step
Definition TreeMesh.cpp:314
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
bool isValid() const noexcept
Return whether this reference is valid and non-nil.
Definition SubjectRef.h:38
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
格子型物品容器(脚本可直接操作)。
Definition Bag.h:21
std::string canAddItemReason(const std::string &itemId, int quantity)
Can add item reason.
Definition Bag.cpp:61
bool canAddItem(const std::string &itemId, int quantity)
便捷操作(转发 InventorySystem):增删 / 移动 / 查询。
Definition Bag.cpp:57
int addItem(const std::string &itemId, int quantity)
Adds item.
Definition Bag.cpp:67
bool splitStack(int slot, int quantity, int toSlot)
Split stack.
Definition Bag.cpp:83
int removeItem(const std::string &itemId, int quantity)
Removes item.
Definition Bag.cpp:71
EVENGINE_API_FOUNDATION public API.
Definition Equipment.h:19
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
Result< std::vector< GameplayEvent > > gameplayEvents(const GameplaySession &session, SubjectRef instance, std::uint64_t afterSequence) const override
Return events strictly newer than an instance-local sequence cursor.
Result< GameplayObservation > observeGameplay(const GameplaySession &session, SubjectRef instance) const override
Observe one provider-owned gameplay instance without mutation.
~InventoryControl() override
Unpublish every instance without destroying borrowed authorities.
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
InventoryControl()
Create an unpublished adapter and register it as the inventory domain.
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
int count() const
Number of published instances.
void clear()
Drop every publication without destroying borrowed containers.
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
std::vector< SubjectRef > instances() const
Published instance identities, in publication order.
Result< void > publish(SubjectRef instance, SubjectRef owner, Bag &bag, EquipmentSet *equipment)
Publish one borrowed inventory instance under a stable identity.
Result< void > unpublish(SubjectRef instance)
Drop one publication; NotFound when that instance is not published.
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
运行时容器:固定格数的背包 / 箱子 / 商店栏等。 行为由 InventorySystem 提供;本类暴露便于脚本绑定的薄封装方法。
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Owning evidence that a gameplay authority accepted one command.
One player-semantic command submitted to a domain authority.
std::uint64_t expectedRevision
SimulationTick observedTick
Owning event projection safe across frames and process boundaries.
std::uint64_t sequence
Immutable owning observation of one authoritative gameplay instance.
Owning authorization context shared by player, script and automation adapters.
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158