载入中...
搜索中...
未找到
ProductControl.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
4#include "inventory/Bag.h"
5#include "rpg/GameState.h"
6#include "rpg/QuestReward.h"
8#include "rpg/Tracker.h"
10
11#include <algorithm>
12#include <limits>
13#include <optional>
14
15namespace eve::rpg {
16namespace {
17
18LogicalId gameplayId(std::string_view value) { return LogicalId::parse(value).value(); }
19
20Result<const Value::Object*> parameters(const Value& value) {
21 const auto* object = value.getIf<Value::Object>();
22 if (!object)
24 DiagnosticCode::InvalidArgument, "RPG product parameters must be an object", "parameters"));
26}
27
28Result<std::string> stringParameter(const Value& value, std::string_view name,
29 bool optional = false) {
30 auto object = parameters(value);
31 if (!object) return Result<std::string>::failure(object.status());
32 const auto found = object.value()->find(std::string(name));
33 if (found == object.value()->end() && optional) return Result<std::string>::success({});
34 if (found == object.value()->end() || !found->second.isString())
36 "RPG product parameter must be a string",
37 "parameters." + std::string(name)));
38 return Result<std::string>::success(found->second.asString());
39}
40
41Result<int> integerParameter(const Value& value, std::string_view name, int fallback = 0,
42 bool optional = false) {
43 auto object = parameters(value);
44 if (!object) return Result<int>::failure(object.status());
45 const auto found = object.value()->find(std::string(name));
46 if (found == object.value()->end() && optional) return Result<int>::success(fallback);
47 if (found == object.value()->end() || !found->second.isInt64())
49 "RPG product parameter must be an integer",
50 "parameters." + std::string(name)));
51 const auto number = found->second.asInt();
52 if (number < std::numeric_limits<int>::min() || number > std::numeric_limits<int>::max())
54 "RPG product integer parameter is out of range",
55 "parameters." + std::string(name)));
56 return Result<int>::success(static_cast<int>(number));
57}
58
59Result<double> numericParameter(const Value& value, std::string_view name,
60 double fallback = 0.0, bool optional = false) {
61 auto object = parameters(value);
62 if (!object) return Result<double>::failure(object.status());
63 const auto found = object.value()->find(std::string(name));
64 if (found == object.value()->end() && optional) return Result<double>::success(fallback);
65 if (found == object.value()->end() || !found->second.isNumeric())
67 "RPG product parameter must be numeric",
68 "parameters." + std::string(name)));
69 return Result<double>::success(found->second.isInt64()
70 ? static_cast<double>(found->second.asInt())
71 : found->second.asDouble());
72}
73
74Value schema(std::initializer_list<std::pair<const std::string, Value>> fields) {
75 return Value(Value::Object(fields));
76}
77
78} // namespace
79
81 inventory::Bag& bag)
82 : instance_(instance), gameState_(&gameState), tracker_(&tracker), bag_(&bag) {
83 cap::addListener<IGameplayControlProvider>(this);
84 cap::addListener<IGameplayInstanceCatalog>(this);
85}
86
88 cap::removeListener<IGameplayInstanceCatalog>(this);
89 cap::removeListener<IGameplayControlProvider>(this);
90}
91
92std::string_view ProductControl::gameplayDomain() const noexcept { return "rpg.product"; }
93
94std::vector<SubjectRef> ProductControl::gameplayInstances() const {
95 // This adapter serves exactly one instance: the identity it was constructed
96 // with is the only one it can answer for.
97 if (!instance_.isValid()) return {};
98 return {instance_};
99}
100
101bool ProductControl::controls(const GameplaySession& session) const {
102 return session.access != GameplayAccess::PlayerEquivalent ||
103 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), instance_) !=
104 session.controlledSubjects.end();
105}
106
107Result<Value> ProductControl::stateProjection() const {
108 if (!instance_.isValid() || !gameState_ || !tracker_ || !bag_)
110 Diagnostic::error(DiagnosticCode::StaleHandle, "RPG product control participants are invalid", "instance"));
111 auto gameJson = gameState_->snapshotJson();
112 if (!gameJson) return Result<Value>::failure(gameJson.status());
113 auto questJson = tracker_->snapshotJson();
114 if (!questJson) return Result<Value>::failure(questJson.status());
115 auto game = Value::fromJson(gameJson.value());
116 if (!game) return Result<Value>::failure(game.status());
117 auto quests = Value::fromJson(questJson.value());
118 if (!quests) return Result<Value>::failure(quests.status());
120 slots.reserve(static_cast<std::size_t>(bag_->getSlotCount()));
121 for (int index = 0; index < bag_->getSlotCount(); ++index) {
122 slots.emplace_back(Value::Object{
123 {"index", Value(index)},
124 {"itemId", Value(bag_->getSlotItemId(index))},
125 {"quantity", Value(bag_->getSlotQuantity(index))},
126 });
127 }
129 {"bag", Value(Value::Object{{"id", Value(bag_->getId())},
130 {"kind", Value(bag_->getKind())},
131 {"slots", Value(std::move(slots))}})},
132 {"gameState", std::move(game).takeValue()},
133 {"quests", std::move(quests).takeValue()},
134 }));
135}
136
137Result<std::uint64_t> ProductControl::refreshRevision() const {
138 auto state = stateProjection();
139 if (!state) return Result<std::uint64_t>::failure(state.status());
140 auto json = state.value().toJson();
141 if (!json) return Result<std::uint64_t>::failure(json.status());
142 if (stateFingerprint_ != json.value()) {
143 stateFingerprint_ = json.value();
144 ++revision_;
145 }
146 return Result<std::uint64_t>::success(revision_);
147}
148
150 SubjectRef instance) const {
151 if (instance != instance_ || !instance_.isValid())
153 Diagnostic::error(DiagnosticCode::NotFound, "RPG product gameplay instance was not found", "instance"));
154 if (!controls(session))
156 DiagnosticCode::PreconditionViolation, "session does not control this RPG product state", "instance"));
157 auto revision = refreshRevision();
159 auto state = stateProjection();
160 if (!state) return Result<GameplayObservation>::failure(state.status());
161 GameplayObservation observation;
162 observation.domain = gameplayId("gameplay:rpg-product");
163 observation.instance = instance_;
164 observation.tick = tick_;
165 observation.revision = revision.value();
166 observation.state = std::move(state).takeValue();
167 return Result<GameplayObservation>::success(std::move(observation));
168}
169
171 const GameplaySession& session, SubjectRef instance, SubjectRef subject) const {
172 auto observed = observeGameplay(session, instance);
173 if (!observed)
174 return Result<std::vector<GameplayActionDescriptor>>::failure(observed.status());
175 std::move(observed).takeValue();
176 if (subject != instance_)
179 "RPG product actions are owned by the product-state subject", "subject"));
180 const Value stringType = schema({{"type", Value("string")}});
181 const Value integerType = schema({{"type", Value("integer")}});
182 const Value numberType = schema({{"type", Value("number")}});
184 {gameplayId("rpg:buy-offer"),
185 schema({{"currencyId", stringType}, {"offerId", stringType}, {"quantity", integerType}})},
186 {gameplayId("rpg:sell-offer"),
187 schema({{"currencyId", stringType}, {"offerId", stringType}, {"quantity", integerType}})},
188 {gameplayId("rpg:claim-quest"), schema({{"questId", stringType}})},
189 {gameplayId("rpg:collect-loot"),
190 schema({{"attributeAmount", numberType}, {"attributeId", stringType},
191 {"itemId", stringType}, {"itemQuantity", integerType},
192 {"mapId", stringType}, {"notifyAmount", integerType},
193 {"notifyTarget", stringType}, {"notifyTopic", stringType},
194 {"objectId", stringType}, {"requiredQuestId", stringType}})},
195 });
196}
197
198void ProductControl::publishEvent(std::string type, const GameplayCommand& command,
199 Value payload) {
200 GameplayEvent event;
201 event.sequence = nextEventSequence_++;
202 event.tick = tick_;
203 event.type = std::move(type);
204 event.subject = instance_;
205 event.causationCommandId = command.id;
206 event.correlationId = command.id;
207 event.payload = std::move(payload);
208 events_.push_back(std::move(event));
209}
210
212 SubjectRef instance,
213 const GameplayCommand& command) {
214 if (instance != instance_)
216 Diagnostic::error(DiagnosticCode::NotFound, "RPG product gameplay instance was not found", "instance"));
217 if (!controls(session) || command.subject != instance_)
219 Diagnostic::error(DiagnosticCode::PreconditionViolation, "session does not control this RPG product state",
220 "command.subject"));
221 if (command.id.empty())
223 Diagnostic::error(DiagnosticCode::InvalidArgument, "command id must not be empty", "command.id"));
224 auto currentRevision = refreshRevision();
225 if (!currentRevision) return Result<GameplayCommandReceipt>::failure(currentRevision.status());
226 if (command.observedTick != tick_ || command.expectedRevision != currentRevision.value())
228 Diagnostic::error(DiagnosticCode::Conflict, "RPG product command was based on a stale observation",
229 "command.expectedRevision"));
230
231 std::optional<Result<int>> applied;
232 std::string operation;
233 if (command.action == gameplayId("rpg:buy-offer") ||
234 command.action == gameplayId("rpg:sell-offer")) {
235 auto currency = stringParameter(command.parameters, "currencyId");
236 auto offer = stringParameter(command.parameters, "offerId");
237 auto quantity = integerParameter(command.parameters, "quantity");
238 if (!currency) return Result<GameplayCommandReceipt>::failure(currency.status());
239 if (!offer) return Result<GameplayCommandReceipt>::failure(offer.status());
240 if (!quantity) return Result<GameplayCommandReceipt>::failure(quantity.status());
241 if (command.action == gameplayId("rpg:buy-offer")) {
242 operation = "buy-offer";
243 applied.emplace(ShopTransaction::buyOffer(*gameState_, *bag_, currency.value(),
244 offer.value(), quantity.value()));
245 } else {
246 operation = "sell-offer";
247 applied.emplace(ShopTransaction::sellOffer(*gameState_, *bag_, currency.value(),
248 offer.value(), quantity.value()));
249 }
250 } else if (command.action == gameplayId("rpg:claim-quest")) {
251 auto quest = stringParameter(command.parameters, "questId");
252 if (!quest) return Result<GameplayCommandReceipt>::failure(quest.status());
253 operation = "claim-quest";
254 applied.emplace(QuestReward::claim(*tracker_, *gameState_, *bag_, quest.value()));
255 } else if (command.action == gameplayId("rpg:collect-loot")) {
257 auto map = stringParameter(command.parameters, "mapId");
258 auto object = stringParameter(command.parameters, "objectId");
259 auto requiredQuest = stringParameter(command.parameters, "requiredQuestId", true);
260 auto item = stringParameter(command.parameters, "itemId", true);
261 auto itemQuantity = integerParameter(command.parameters, "itemQuantity", 0, true);
262 auto attribute = stringParameter(command.parameters, "attributeId", true);
263 auto attributeAmount = numericParameter(command.parameters, "attributeAmount", 0.0, true);
264 auto topic = stringParameter(command.parameters, "notifyTopic", true);
265 auto target = stringParameter(command.parameters, "notifyTarget", true);
266 auto amount = integerParameter(command.parameters, "notifyAmount", 0, true);
267 if (!map) return Result<GameplayCommandReceipt>::failure(map.status());
268 if (!object) return Result<GameplayCommandReceipt>::failure(object.status());
269 if (!requiredQuest) return Result<GameplayCommandReceipt>::failure(requiredQuest.status());
270 if (!item) return Result<GameplayCommandReceipt>::failure(item.status());
271 if (!itemQuantity) return Result<GameplayCommandReceipt>::failure(itemQuantity.status());
272 if (!attribute) return Result<GameplayCommandReceipt>::failure(attribute.status());
273 if (!attributeAmount) return Result<GameplayCommandReceipt>::failure(attributeAmount.status());
274 if (!topic) return Result<GameplayCommandReceipt>::failure(topic.status());
276 if (!amount) return Result<GameplayCommandReceipt>::failure(amount.status());
277 request.mapId = map.value();
278 request.objectId = object.value();
279 request.requiredQuestId = requiredQuest.value();
280 request.itemId = item.value();
281 request.itemQuantity = itemQuantity.value();
282 request.attributeId = attribute.value();
283 request.attributeAmount = attributeAmount.value();
284 request.notifyTopic = topic.value();
285 request.notifyTarget = target.value();
286 request.notifyAmount = amount.value();
287 operation = "collect-loot";
288 applied.emplace(WorldInteraction::collectLoot(*gameState_, *tracker_, *bag_, request));
289 }
290 if (!applied)
292 Diagnostic::error(DiagnosticCode::Unsupported, "unsupported RPG product action", "command.action"));
293 if (!*applied) return Result<GameplayCommandReceipt>::failure(applied->status());
294 const int effectCount = std::move(*applied).takeValue();
295 stateFingerprint_.clear();
296 auto resultingRevision = refreshRevision();
297 if (!resultingRevision)
298 return Result<GameplayCommandReceipt>::failure(resultingRevision.status());
299 publishEvent("rpg.product." + operation, command,
300 Value(Value::Object{{"effectCount", Value(effectCount)},
301 {"revision", Value(static_cast<std::int64_t>(resultingRevision.value()))}}));
303 receipt.commandId = command.id;
304 receipt.executionId = operation + "-" + std::to_string(nextEventSequence_ - 1);
305 receipt.acceptedTick = tick_;
306 receipt.resultingRevision = resultingRevision.value();
307 receipt.details = Value(Value::Object{{"effectCount", Value(effectCount)},
308 {"operation", Value(operation)}});
309 return Result<GameplayCommandReceipt>::success(std::move(receipt),
311}
312
314 SubjectRef instance,
315 const SimulationStep& step) {
316 if (step.tick <= tick_)
318 Diagnostic::error(DiagnosticCode::Conflict, "RPG product simulation tick must increase", "step.tick"));
319 auto observed = observeGameplay(session, instance);
320 if (!observed) return Result<GameplayObservation>::failure(observed.status());
321 std::move(observed).takeValue();
322 tick_ = step.tick;
323 return observeGameplay(session, instance);
324}
325
327 SubjectRef instance,
328 std::uint64_t afterSequence) const {
329 auto observed = observeGameplay(session, instance);
330 if (!observed) return Result<std::vector<GameplayEvent>>::failure(observed.status());
331 std::move(observed).takeValue();
332 std::vector<GameplayEvent> result;
333 for (const auto& event : events_)
334 if (event.sequence > afterSequence) result.push_back(event);
335 const bool empty = result.empty();
336 return Result<std::vector<GameplayEvent>>::success(
337 std::move(result), Status::success(empty ? StatusCode::NoOp : StatusCode::Applied));
338}
339
340} // namespace eve::rpg
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
int subject
Definition AnimSmr.cpp:163
全局游戏状态:开关(bool)、变量(数字)、独立变量(按作用域)。
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::int32_t second
std::string name
std::uint64_t revision
std::vector< std::string > fields
Definition PlayHost.cpp:111
Player-equivalent control adapter for persistent RPG product loops.
Atomic orchestration for claiming data-driven quest rewards.
double number
bool found
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
Atomic RPG currency and inventory purchase/sale orchestration.
std::optional< InteractionSession > session
Definition Tactics.cpp:39
目标追踪器运行时实例(玩家日志 / 成就 / 教程可各建一份)。
float step
Definition TreeMesh.cpp:314
uint32_t index
Atomic settlement of persistent world loot interactions.
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
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
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
格子型物品容器(脚本可直接操作)。
Definition Bag.h:21
std::string getId() const
容器 id(变更事件定位用)。
Definition Bag.h:35
std::string getKind() const
容器种类(bag / chest / shop 等)。
Definition Bag.h:40
std::string getSlotItemId(int slot) const
Returns the slot item id.
Definition Bag.cpp:112
int getSlotCount() const
格数。
Definition Bag.h:45
int getSlotQuantity(int slot) const
Returns the slot quantity.
Definition Bag.cpp:117
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
一份游戏状态(开关/变量/独立变量)。
Definition GameState.h:25
eve::Result< std::string > snapshotJson() const
Serialize this state as the canonical versioned RPG game-state JSON document.
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.
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
~ProductControl() override
Unpublish the capability without destroying borrowed authorities.
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
ProductControl(SubjectRef instance, GameState &gameState, Tracker &tracker, inventory::Bag &bag)
Construct over borrowed product-loop authorities and a stable instance identity.
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
static eve::Result< int > claim(Tracker &tracker, GameState &gameState, inventory::Bag &bag, const std::string &questId)
Validate and atomically claim every reward declared by one ready quest.
static eve::Result< int > sellOffer(GameState &gameState, inventory::Bag &bag, const std::string &currencyId, const std::string &offerId, int quantity)
Sell a quantity using the authoritative price and item in ShopCatalogue.
static eve::Result< int > buyOffer(GameState &gameState, inventory::Bag &bag, const std::string &currencyId, const std::string &offerId, int quantity)
Buy a quantity using the authoritative price and item in ShopCatalogue.
一份任务进度 + 本实例事件队列。
Definition Tracker.h:26
eve::Result< std::string > snapshotJson() const
Serialize persistent quest progress as deterministic versioned JSON.
Definition Tracker.cpp:44
static eve::Result< int > collectLoot(GameState &gameState, Tracker &tracker, inventory::Bag &bag, const WorldLootRequest &request)
Atomically grant loot, notify quest progress, and consume one world object.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
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
Complete data needed to settle one persistent loot object.