载入中...
搜索中...
未找到
EconomyControl.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
6
7#include <algorithm>
8#include <limits>
9#include <set>
10#include <string>
11#include <utility>
12
13namespace eve::economy {
14namespace {
15
16template <typename T>
17Result<T> failure(DiagnosticCode code, std::string message, std::string path) {
19}
20
21LogicalId id(std::string_view text) { return LogicalId::parse(text).value(); }
22
23Result<const Value::Object*> object(const Value& value) {
24 const auto* result = value.getIf<Value::Object>();
25 if (!result)
26 return failure<const Value::Object*>(DiagnosticCode::InvalidArgument, "economy parameters must be an object",
27 "parameters");
29}
30
31Result<std::string> text(const Value::Object& value, std::string_view name, std::string fallback = {}) {
32 const auto found = value.find(std::string(name));
33 if (found == value.end()) return Result<std::string>::success(std::move(fallback));
34 if (!found->second.isString())
35 return failure<std::string>(DiagnosticCode::InvalidArgument, "economy parameter must be a string",
36 "parameters." + std::string(name));
37 return Result<std::string>::success(found->second.asString());
38}
39
40Result<int> integer(const Value::Object& value, std::string_view name, int fallback) {
41 const auto found = value.find(std::string(name));
42 if (found == value.end()) return Result<int>::success(fallback);
43 if (!found->second.isInt64() || found->second.asInt() < std::numeric_limits<int>::min() ||
44 found->second.asInt() > std::numeric_limits<int>::max())
45 return failure<int>(DiagnosticCode::InvalidArgument, "economy parameter must be an integer",
46 "parameters." + std::string(name));
47 return Result<int>::success(static_cast<int>(found->second.asInt()));
48}
49
50const std::string kCredit{"economy:credit"};
51const std::string kDebit{"economy:debit"};
52
53std::string depletionName(DepletionModel model) {
54 switch (model) {
55 case DepletionModel::Renewable: return "renewable";
56 case DepletionModel::Infinite: return "infinite";
57 case DepletionModel::Growing: return "growing";
58 case DepletionModel::Finite: break;
59 }
60 return "finite";
61}
62
63} // namespace
64
66 cap::addListener<IGameplayControlProvider>(this);
67 cap::addListener<IGameplayInstanceCatalog>(this);
68}
69
71 cap::removeListener<IGameplayInstanceCatalog>(this);
72 cap::removeListener<IGameplayControlProvider>(this);
73}
74
76 if (!instance.isValid() || !owner.isValid())
78 DiagnosticCode::InvalidArgument, "economy publication needs valid instance and owner ids", "instance"));
79 if (find(instance) != nullptr)
81 DiagnosticCode::Conflict, "a gameplay control already owns that economy instance", "instance"));
82 Entry entry;
83 entry.instance = instance;
84 entry.owner = owner;
85 entry.player = player;
86 entries_.push_back(std::move(entry));
87 return Result<void>::success();
88}
89
91 const auto found = std::find_if(entries_.begin(), entries_.end(),
92 [instance](const Entry& entry) { return entry.instance == instance; });
93 if (found == entries_.end())
95 Diagnostic::error(DiagnosticCode::NotFound, "that economy instance is not published", "instance"));
96 entries_.erase(found);
97 return Result<void>::success();
98}
99
100void EconomyControl::clear() { entries_.clear(); }
101
102int EconomyControl::count() const { return static_cast<int>(entries_.size()); }
103
104std::vector<SubjectRef> EconomyControl::instances() const {
105 std::vector<SubjectRef> result;
106 result.reserve(entries_.size());
107 for (const auto& entry : entries_) result.push_back(entry.instance);
108 return result;
109}
110
111std::string_view EconomyControl::gameplayDomain() const noexcept { return "economy"; }
112
113std::vector<SubjectRef> EconomyControl::gameplayInstances() const { return instances(); }
114
115EconomyControl::Entry* EconomyControl::find(SubjectRef instance) {
116 const auto found = std::find_if(entries_.begin(), entries_.end(),
117 [instance](const Entry& entry) { return entry.instance == instance; });
118 return found == entries_.end() ? nullptr : &*found;
119}
120
121const EconomyControl::Entry* EconomyControl::find(SubjectRef instance) const {
122 const auto found = std::find_if(entries_.begin(), entries_.end(),
123 [instance](const Entry& entry) { return entry.instance == instance; });
124 return found == entries_.end() ? nullptr : &*found;
125}
126
127bool EconomyControl::controls(const GameplaySession& session, const Entry& entry) const {
128 return session.access != GameplayAccess::PlayerEquivalent ||
129 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), entry.owner) !=
130 session.controlledSubjects.end();
131}
132
133Value EconomyControl::ledgerState(const Entry& entry) const {
134 const EconomyLedger::Snapshot snapshot = EconomySystem::snapshot(entry.player);
135 // Registered types come first so a freshly published instance still reports the
136 // resources the game declared, even before any balance moves.
137 std::set<std::string> types;
138 const int registered = ResourceTypeRegistry::count();
139 for (int index = 0; index < registered; ++index) {
140 const ResourceTypeDef* def = ResourceTypeRegistry::typeAt(index);
141 if (def != nullptr) types.insert(def->id);
142 }
143 for (const auto& [type, amount] : snapshot.current) {
144 (void)amount;
145 types.insert(type);
146 }
147 for (const auto& [type, amount] : snapshot.income) {
148 (void)amount;
149 types.insert(type);
150 }
151 for (const auto& [type, amount] : snapshot.expense) {
152 (void)amount;
153 types.insert(type);
154 }
155 for (const auto& [type, amount] : snapshot.wasted) {
156 (void)amount;
157 types.insert(type);
158 }
159
161 for (const auto& type : types) {
162 const ResourceTypeDef* def = ResourceTypeRegistry::find(type);
163 const int balance = EconomySystem::get(entry.player, type);
165 {"type", Value(type)},
166 {"amount", Value(static_cast<std::int64_t>(balance))},
167 {"cap", Value(static_cast<std::int64_t>(EconomySystem::getCap(entry.player, type)))},
168 {"wasted", Value(static_cast<std::int64_t>(EconomySystem::getWasted(entry.player, type)))},
169 {"income", Value(static_cast<std::int64_t>(EconomySystem::getIncome(entry.player, type)))},
170 {"expense", Value(static_cast<std::int64_t>(EconomySystem::getExpense(entry.player, type)))}};
171 if (def != nullptr) {
172 resource.emplace("category", Value(def->category));
173 resource.emplace("depletion", Value(depletionName(def->depletion)));
174 }
175 resources.emplace_back(Value(std::move(resource)));
176 }
177 return Value(Value::Object{{"player", Value(static_cast<std::int64_t>(entry.player))},
178 {"resources", Value(std::move(resources))}});
179}
180
182 const Entry* entry = find(instance);
183 if (entry == nullptr)
184 return failure<GameplayObservation>(DiagnosticCode::NotFound, "economy gameplay instance was not found",
185 "instance");
186 if (!controls(session, *entry))
187 return failure<GameplayObservation>(DiagnosticCode::PreconditionViolation,
188 "session does not control the economy owner", "instance");
189 GameplayObservation observation;
190 observation.domain = id("gameplay:economy");
191 observation.instance = entry->instance;
192 observation.tick = entry->tick;
193 observation.revision = entry->revision;
194 observation.state = ledgerState(*entry);
195 return Result<GameplayObservation>::success(std::move(observation));
196}
197
199 SubjectRef instance,
200 SubjectRef subject) const {
201 auto observed = observeGameplay(session, instance);
202 if (!observed) return Result<std::vector<GameplayActionDescriptor>>::failure(observed.status());
203 const Entry* entry = find(instance);
204 if (entry == nullptr)
205 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::NotFound,
206 "economy gameplay instance was not found", "instance");
207 if (subject != entry->owner)
208 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::PreconditionViolation,
209 "economy action subject must be its owner", "subject");
210
211 const Value stringType(Value::Object{{"type", Value("string")}});
212 const Value integerType(Value::Object{{"type", Value("integer")}});
213 std::vector<GameplayActionDescriptor> actions{
214 {id(kDebit), Value(Value::Object{{"type", stringType}, {"amount", integerType}})},
215 };
216 // A grant is not a player action; it is advertised only to the profiles allowed
217 // to use it, so discovery never offers an action the same session would refuse.
219 actions.push_back({id(kCredit), Value(Value::Object{{"type", stringType}, {"amount", integerType}})});
220 return Result<std::vector<GameplayActionDescriptor>>::success(std::move(actions));
221}
222
224 const GameplayCommand& command) {
225 Entry* entry = find(instance);
226 if (entry == nullptr)
227 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "economy gameplay instance was not found",
228 "instance");
229 if (!controls(session, *entry) || command.subject != entry->owner)
230 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
231 "session does not control the economy owner", "command.subject");
232 if (command.id.empty())
233 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument, "command id must not be empty",
234 "command.id");
235 if (command.observedTick != entry->tick || command.expectedRevision != entry->revision)
236 return failure<GameplayCommandReceipt>(
237 DiagnosticCode::Conflict, "economy command was based on a stale observation", "command.expectedRevision");
238 auto values = object(command.parameters);
240 const Value::Object& parameters = *values.value();
241
242 auto type = text(parameters, "type");
243 auto amount = integer(parameters, "amount", 0);
245 if (!amount) return Result<GameplayCommandReceipt>::failure(amount.status());
246 if (type.value().empty() || amount.value() <= 0)
247 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
248 "economy commands need a resource type and a positive amount",
249 "command.parameters");
250
251 const std::string action = command.action.format();
252 std::string detail;
253 std::string eventType;
254 std::int64_t applied = 0;
255 std::string wastedType;
256 std::int64_t wastedAmount = 0;
257 if (action == kDebit) {
258 const EconomyLedger::Snapshot snapshot = EconomySystem::snapshot(entry->player);
259 const bool known = ResourceTypeRegistry::find(type.value()) != nullptr ||
260 snapshot.current.contains(type.value()) || snapshot.income.contains(type.value()) ||
261 snapshot.expense.contains(type.value());
262 if (!known)
263 return failure<GameplayCommandReceipt>(
264 DiagnosticCode::NotFound, "the ledger does not know that resource type", "command.parameters.type");
265 if (EconomySystem::get(entry->player, type.value()) < amount.value())
266 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
267 "the ledger cannot afford that spend", "command.parameters.amount");
268 if (!EconomySystem::debit(entry->player, type.value(), amount.value()))
269 return failure<GameplayCommandReceipt>(DiagnosticCode::Failed, "the ledger rejected that spend",
270 "command.parameters");
271 detail = "spent " + std::to_string(amount.value()) + " " + type.value();
272 eventType = "economy.debited";
273 applied = amount.value();
274 } else if (action == kCredit) {
276 return failure<GameplayCommandReceipt>(
278 "granting resources requires the test-driver or developer-cheat profile", "session.access");
279 const int accepted = EconomySystem::credit(entry->player, type.value(), amount.value());
280 // A capped ledger stores what fits and books the rest as waste; both are
281 // disclosed instead of reporting the requested amount as applied.
282 wastedType = type.value();
283 wastedAmount = amount.value() - accepted;
284 detail = "granted " + std::to_string(accepted) + " " + type.value();
285 if (wastedAmount > 0) detail += " (" + std::to_string(wastedAmount) + " wasted at cap)";
286 eventType = "economy.credited";
287 applied = accepted;
288 } else {
289 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported, "unsupported economy action",
290 "command.action");
291 }
292
293 ++entry->revision;
294 GameplayEvent event;
295 event.sequence = entry->nextEventSequence++;
296 event.tick = entry->tick;
297 event.type = eventType;
298 event.subject = entry->owner;
299 event.causationCommandId = command.id;
300 event.correlationId = command.id;
301 event.payload = Value(Value::Object{
302 {"instance", Value(entry->instance.format())}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
303 entry->events.push_back(std::move(event));
304 if (wastedAmount > 0) {
305 GameplayEvent overflow;
306 overflow.sequence = entry->nextEventSequence++;
307 overflow.tick = entry->tick;
308 overflow.type = "economy.wasted";
309 overflow.subject = entry->owner;
310 overflow.causationCommandId = command.id;
311 overflow.correlationId = command.id;
312 overflow.payload =
313 Value(Value::Object{{"instance", Value(entry->instance.format())},
314 {"detail", Value("wasted " + std::to_string(wastedAmount) + " " + wastedType)},
315 {"quantity", Value(wastedAmount)}});
316 entry->events.push_back(std::move(overflow));
317 }
318
320 receipt.commandId = command.id;
321 receipt.executionId = command.id + ".executed";
322 receipt.acceptedTick = entry->tick;
323 receipt.resultingRevision = entry->revision;
324 receipt.details =
325 Value(Value::Object{{"action", Value(action)}, {"detail", Value(detail)}, {"quantity", Value(applied)}});
327}
328
330 const SimulationStep& step) {
331 Entry* entry = find(instance);
332 if (entry == nullptr)
333 return failure<GameplayObservation>(DiagnosticCode::NotFound, "economy gameplay instance was not found",
334 "instance");
335 if (step.tick <= entry->tick)
336 return failure<GameplayObservation>(DiagnosticCode::Conflict, "economy simulation tick must increase",
337 "step.tick");
338 auto observed = observeGameplay(session, instance);
339 if (!observed) return Result<GameplayObservation>::failure(observed.status());
340 entry->tick = step.tick;
341 return observeGameplay(session, instance);
342}
343
345 std::uint64_t afterSequence) const {
346 auto observed = observeGameplay(session, instance);
347 if (!observed) return Result<std::vector<GameplayEvent>>::failure(observed.status());
348 const Entry* entry = find(instance);
349 if (entry == nullptr)
350 return failure<std::vector<GameplayEvent>>(DiagnosticCode::NotFound, "economy gameplay instance was not found",
351 "instance");
352 std::vector<GameplayEvent> result;
353 for (const auto& event : entry->events)
354 if (event.sequence > afterSequence) result.push_back(event);
355 const bool empty = result.empty();
356 return Result<std::vector<GameplayEvent>>::success(std::move(result),
358}
359
360} // namespace eve::economy
double value
int subject
Definition AnimSmr.cpp:163
std::map< std::string, Var > values
std::string message
DiagnosticCode code
std::string text
std::string name
std::vector< std::weak_ptr< DeviceBytes > > resources
Definition OnnxGpgpu.cpp:55
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
glm::mat4 model
bool found
double current
std::string resource
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
EconomyControl()
Create an unpublished adapter and register it as the economy domain.
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
Result< GameplayObservation > observeGameplay(const GameplaySession &session, SubjectRef instance) const override
Observe one provider-owned gameplay instance without mutation.
Result< void > unpublish(SubjectRef instance)
Drop one publication; NotFound when that instance is not published.
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
~EconomyControl() override
Unpublish every instance.
Result< void > publish(SubjectRef instance, SubjectRef owner, int player)
Publish one player ledger under a stable instance identity.
int count() const
Number of published instances.
void clear()
Drop every publication.
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
std::vector< SubjectRef > instances() const
Published instance identities, in publication order.
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
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.
static int credit(int player, const std::string &type, int amount)
入账并广播事件。
static int getWasted(int player, const std::string &type)
因满仓浪费的累计量。
static int getExpense(int player, const std::string &type)
累计出账量。
static bool debit(int player, const std::string &type, int amount)
出账并广播事件;余额不足返回 false。
static EconomyLedger::Snapshot snapshot(int player)
取一个玩家账本的完整只读快照(含未注册类型的余额与流水)。
static int getIncome(int player, const std::string &type)
累计入账量。
static int getCap(int player, const std::string &type)
玩家该类型持有上限(0=不限)。
static int get(int player, const std::string &type)
玩家当前持有量。
static const ResourceTypeDef * find(const std::string &id)
按 id 查询;未注册时返回 nullptr。
static int count()
已注册类型数量。
static const ResourceTypeDef * typeAt(int index)
按 id 字典序返回第 index 个类型定义。
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
DepletionModel
资源节点的供给模型。
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.
SimulationTick tick
std::uint64_t sequence
std::string causationCommandId
std::string correlationId
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
Owning deterministic copy of all balance and accounting counters.
std::map< std::string, int > income
std::map< std::string, int > current
std::map< std::string, int > expense