载入中...
搜索中...
未找到
StatusSystem.cpp
浏览该文件的文档.
1#include "rpg/StatusSystem.h"
3#include "rpg/Effect.h"
4#include "rpg/RPGActor.h"
5
6#include <algorithm>
7#include <cmath>
8#include <limits>
9#include <utility>
10
11namespace eve::rpg {
12
13namespace {
14
15using LifecycleDefinition = ::eve::effects::EffectDefinition;
16using LifecycleInstance = ::eve::effects::EffectInstance;
17using StatusMetadata = StatusExecutorMetadata;
18using ModifierBinding = std::pair<std::string, std::string>;
19
20std::vector<StatusTickEvent> &tickQueue() {
21 static std::vector<StatusTickEvent> q;
22 return q;
23}
24
25std::vector<StatusChangeEvent> &changeQueue() {
26 static std::vector<StatusChangeEvent> q;
27 return q;
28}
29
30eve::Status rejectedStatus(eve::DiagnosticCode code, std::string message,
33}
34
35template <typename T>
38 return eve::Result<T>::failure(rejectedStatus(code, std::move(message), statusCode));
39}
40
41bool isBuiltinStackPolicy(const std::string &policy) {
42 return policy == "none" || policy == "refresh" || policy == "extend" || policy == "stack";
43}
44
45eve::Result<void> validateRpgDefinition(const EffectDefinition &definition) {
46 if (definition.id.empty())
47 return rejected<void>(eve::DiagnosticCode::InvalidArgument, "effect definition id must not be empty");
48 if (definition.durationPolicy != "instant" && definition.durationPolicy != "duration" &&
49 definition.durationPolicy != "infinite") {
50 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
51 "unknown effect duration policy: " + definition.durationPolicy);
52 }
53 if (!std::isfinite(definition.duration) || definition.duration < 0.0f)
54 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
55 "effect definition duration must be finite and non-negative");
56 if (!std::isfinite(definition.period) || definition.period < 0.0f)
57 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
58 "effect definition period must be finite and non-negative");
59 if (definition.maxStacks < 0)
60 return rejected<void>(eve::DiagnosticCode::InvalidArgument, "effect definition maxStacks must be non-negative");
61 if (!isBuiltinStackPolicy(definition.stackPolicy) && !StatusSystem::hasStackPolicy(definition.stackPolicy)) {
62 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
63 "unknown effect stack policy: " + definition.stackPolicy);
64 }
65
66 for (const auto &modifier : definition.modifiers) {
67 if (modifier.attribute.empty())
68 return rejected<void>(eve::DiagnosticCode::InvalidArgument, "effect modifier attribute must not be empty");
69 if (!std::isfinite(modifier.value))
70 return rejected<void>(eve::DiagnosticCode::InvalidArgument, "effect modifier value must be finite");
71
72 auto parsed = ::eve::attributes::parseAttributeOperation(modifier.op, modifier.value);
73 if (parsed.ok()) {
74 std::move(parsed).takeValue();
75 continue;
76 }
77 const eve::Status diagnostic = parsed.status();
78 if (!AttributeSystem::customOps().has(modifier.op)) return eve::Result<void>::failure(diagnostic);
79 parsed.ignore("custom RPG attribute operation is validated by the registry");
80 }
81
83}
84
85LifecycleDefinition makeLifecycleDefinition(const EffectDefinition &definition, ::eve::effects::StackMode stackMode,
86 ::eve::effects::StackCountPolicy stackCountPolicy,
87 ::eve::effects::DurationPolicy durationPolicy, std::uint32_t stackCount = 1,
88 double duration = -1.0) {
89 LifecycleDefinition result;
90 result.id = definition.id;
91 // One RPG actor owns one container, so the actor-local subject is enough;
92 // the canonical string instance id remains the identity across all maps.
93 result.stackKey = definition.id;
94 result.priority = 0;
95 result.duration =
96 duration >= 0.0 ? duration : (definition.durationPolicy == "duration" ? double(definition.duration) : 0.0);
97 result.magnitude = 0.0;
98 result.stackCount = stackCount;
99 result.policy.stackMode = stackMode;
100 result.policy.stackCount = stackCountPolicy;
101 result.policy.duration = durationPolicy;
102 result.policy.magnitude = ::eve::effects::MagnitudePolicy::Keep;
103 result.policy.overflow = ::eve::effects::OverflowPolicy::Reject;
104 result.policy.maxStacks = definition.maxStacks == 0 ? 0u : static_cast<std::uint32_t>(definition.maxStacks);
105 result.tags = definition.tags;
106 for (const auto &[key, value] : definition.extra) result.payload.setString(key, value);
107 return result;
108}
109
110LifecycleDefinition makeNewLifecycleDefinition(const EffectDefinition &definition) {
111 auto result =
112 makeLifecycleDefinition(definition, ::eve::effects::StackMode::NewInstance,
114 result.policy.magnitude = ::eve::effects::MagnitudePolicy::Replace;
115 return result;
116}
117
118LifecycleInstance *findByType(RPGActor::Statuses &statuses, const std::string &effectId) {
119 for (int i = 0; i < statuses.container.effectCount(); ++i) {
120 LifecycleInstance *effect = statuses.container.effectAt(i);
121 if (effect && effect->type == effectId) return effect;
122 }
123 return nullptr;
124}
125
126int legacyIdFor(const RPGActor::Statuses &statuses, const std::string &canonicalId) {
127 const auto it = statuses.legacyIdByEffect.find(canonicalId);
128 return it == statuses.legacyIdByEffect.end() ? 0 : it->second;
129}
130
131LifecycleInstance *findByLegacyId(RPGActor::Statuses &statuses, int instanceId) {
132 const auto mapping = statuses.effectByLegacyId.find(instanceId);
133 if (mapping == statuses.effectByLegacyId.end()) return nullptr;
134 return statuses.container.find(mapping->second);
135}
136
137StatusInstance project(const LifecycleInstance &effect, const StatusMetadata &metadata, int legacyId) {
138 StatusInstance result;
139 result.instanceId = legacyId;
140 result.effectId = effect.type;
141 result.source = effect.source;
142 result.stacks = effect.stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
143 ? std::numeric_limits<int>::max()
144 : static_cast<int>(effect.stackCount);
145 result.remaining = static_cast<float>(effect.remaining);
146 result.periodAccum = static_cast<float>(metadata.periodAccum);
147 result.appliedModifiers = metadata.appliedModifiers;
148 result.props = metadata.props;
149 return result;
150}
151
152eve::Result<int> allocateLegacyId(RPGActor::Statuses &statuses) {
153 if (statuses.nextInstanceId <= 0)
154 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
155 "RPG status legacy instance id space is exhausted", eve::StatusCode::Failed);
156
157 const int allocated = statuses.nextInstanceId;
158 if (allocated == std::numeric_limits<int>::max())
159 statuses.nextInstanceId = 0;
160 else
161 ++statuses.nextInstanceId;
163}
164
165eve::Result<void> validateCandidate(const StatusInstance &candidate, const StatusInstance &current,
166 const EffectDefinition &definition) {
167 (void)definition;
168 if (candidate.instanceId != current.instanceId || candidate.effectId != current.effectId)
169 return rejected<void>(eve::DiagnosticCode::InvariantViolation,
170 "custom stack policy cannot change the status identity", eve::StatusCode::Failed);
171 if (candidate.stacks <= 0)
172 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
173 "custom stack policy must leave a positive stack count");
174 if (!std::isfinite(candidate.remaining) || candidate.remaining < -1.0f)
175 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
176 "custom stack policy produced an invalid remaining duration");
177 if (!std::isfinite(candidate.periodAccum) || candidate.periodAccum < 0.0f)
178 return rejected<void>(eve::DiagnosticCode::InvalidArgument,
179 "custom stack policy produced an invalid period accumulator");
181}
182
183eve::Result<void> removeBindings(::eve::attributes::AttributeSet &attributes, const StatusMetadata &metadata) {
184 for (const auto &[attribute, modifierId] : metadata.appliedModifiers) {
185 (void)attribute;
186 auto removed = attributes.removeModifier(modifierId);
187 if (!removed.ok()) return eve::Result<void>::failure(removed.status());
188 }
190}
191
193 const EffectDefinition &definition,
194 const LifecycleInstance &effect,
195 const StatusMetadata &oldMetadata) {
196 auto removed = removeBindings(attributes, oldMetadata);
197 if (!removed.ok()) return eve::Result<std::vector<ModifierBinding>>::failure(removed.status());
198
199 // A definition reload or adapter re-application may change an effect from
200 // a direct modifier into a periodic settlement effect. Remove bindings
201 // first, then leave the periodic effect to the RPG tick/settlement path.
202 if (definition.period > 0.0f) return eve::Result<std::vector<ModifierBinding>>::success({});
203
204 std::vector<ModifierBinding> bindings;
205 bindings.reserve(definition.modifiers.size());
206 const double stackScale = static_cast<double>(effect.stackCount);
207 for (std::size_t index = 0; index < definition.modifiers.size(); ++index) {
208 const auto &spec = definition.modifiers[index];
209 const double value = spec.value * stackScale;
210 if (!std::isfinite(value))
211 return rejected<std::vector<ModifierBinding>>(eve::DiagnosticCode::InvalidArgument,
212 "effect modifier value overflowed during stack scaling");
213
214 AttributeModifier modifier;
215 modifier.id = "status:" + effect.id + ":modifier:" + std::to_string(index);
216 modifier.attribute = spec.attribute;
217 modifier.source = "status:" + definition.id + ":" + effect.id;
218 modifier.priority = spec.priority;
219
221 if (parsed.ok()) {
222 const auto normalized = std::move(parsed).takeValue();
223 modifier.operation = normalized.operation;
224 modifier.value = normalized.value;
225 } else {
226 const eve::Status diagnostic = parsed.status();
227 if (!AttributeSystem::customOps().has(spec.op))
228 return eve::Result<std::vector<ModifierBinding>>::failure(diagnostic);
229 parsed.ignore("custom RPG attribute operation is resolved by AttributeSet");
230 modifier.operation = AttributeOperation::Custom;
231 modifier.policyId = spec.op;
232 modifier.value = value;
233 }
234
235 auto added = attributes.addModifier(std::move(modifier));
236 if (!added.ok()) return eve::Result<std::vector<ModifierBinding>>::failure(added.status());
237 const std::string modifierId = std::move(added).takeValue();
238 bindings.emplace_back(spec.attribute, modifierId);
239 }
240 return eve::Result<std::vector<ModifierBinding>>::success(std::move(bindings));
241}
242
243void commit(RPGActor *actor, ::eve::effects::EffectContainer &&container,
244 std::unordered_map<std::string, StatusMetadata> &&metadata,
245 std::unordered_map<std::string, int> &&legacyIdByEffect,
246 std::unordered_map<int, std::string> &&effectByLegacyId, int nextInstanceId,
247 ::eve::attributes::AttributeSet &&attributes) {
248 auto statuses = actor->statuses();
249 statuses->container = std::move(container);
250 statuses->metadata = std::move(metadata);
251 statuses->legacyIdByEffect = std::move(legacyIdByEffect);
252 statuses->effectByLegacyId = std::move(effectByLegacyId);
253 statuses->nextInstanceId = nextInstanceId;
254 actor->attributes()->values = std::move(attributes);
255}
256
257} // namespace
258
259std::unordered_map<std::string, StatusSystem::ApplyCondition> &StatusSystem::applyConditions() {
260 static std::unordered_map<std::string, ApplyCondition> t;
261 return t;
262}
263
264std::unordered_map<std::string, StatusSystem::StackPolicyFn> &StatusSystem::stackPolicies() {
265 static std::unordered_map<std::string, StackPolicyFn> t;
266 return t;
267}
268
269std::unordered_map<std::string, StatusSystem::LifecycleHook> &StatusSystem::lifecycleHooks() {
270 static std::unordered_map<std::string, LifecycleHook> t;
271 return t;
272}
273
275 if (!name.empty()) applyConditions()[name] = std::move(fn);
276}
277
278void StatusSystem::unregisterApplyCondition(const std::string &name) { applyConditions().erase(name); }
279
280bool StatusSystem::hasApplyCondition(const std::string &name) { return applyConditions().count(name) > 0; }
281
282void StatusSystem::clearApplyConditions() { applyConditions().clear(); }
283
285 if (!name.empty()) stackPolicies()[name] = std::move(fn);
286}
287
288void StatusSystem::unregisterStackPolicy(const std::string &name) { stackPolicies().erase(name); }
289
290bool StatusSystem::hasStackPolicy(const std::string &name) { return stackPolicies().count(name) > 0; }
291
292void StatusSystem::clearStackPolicies() { stackPolicies().clear(); }
293
295 if (!name.empty()) lifecycleHooks()[name] = std::move(fn);
296}
297
298void StatusSystem::unregisterLifecycleHook(const std::string &name) { lifecycleHooks().erase(name); }
299
300bool StatusSystem::hasLifecycleHook(const std::string &name) { return lifecycleHooks().count(name) > 0; }
301
302void StatusSystem::clearLifecycleHooks() { lifecycleHooks().clear(); }
303
304void StatusSystem::emitChange(StatusChangeEvent event) {
305 changeQueue().push_back(event);
306 std::vector<LifecycleHook> hooks;
307 hooks.reserve(lifecycleHooks().size());
308 for (const auto &[name, hook] : lifecycleHooks()) {
309 (void)name;
310 if (hook) hooks.push_back(hook);
311 }
312 for (const auto &hook : hooks) hook(event);
313}
314
315eve::Result<int> StatusSystem::apply(RPGActor *actor, const std::string &effectId, const std::string &source) {
316 if (!actor) return rejected<int>(eve::DiagnosticCode::InvalidArgument, "status application requires an actor");
317
318 const EffectDefinition *definition = EffectRegistry::find(effectId);
319 if (!definition)
320 return rejected<int>(eve::DiagnosticCode::NotFound, "effect definition was not found: " + effectId,
322
323 auto definitionValidation = validateRpgDefinition(*definition);
324 if (!definitionValidation.ok()) return eve::Result<int>::failure(definitionValidation.status());
325
326 for (const auto &[name, condition] : applyConditions()) {
327 if (!condition) continue;
328 std::string reason;
329 if (!condition(actor, *definition, source, reason)) {
330 StatusChangeEvent event;
331 event.actor = actor;
332 event.effectId = effectId;
333 event.source = source;
334 event.action = "reject";
335 event.reason = reason.empty() ? name : reason;
336 const std::string eventReason = event.reason;
337 emitChange(std::move(event));
339 "status application condition rejected the effect: " + eventReason);
340 }
341 }
342
343 if (definition->durationPolicy == "instant") {
344 auto candidateAttributes = actor->attributes()->values;
345 for (const auto &modifier : definition->modifiers) {
346 const double next = candidateAttributes.getBase(modifier.attribute) + modifier.value;
347 if (!std::isfinite(next))
348 return rejected<int>(eve::DiagnosticCode::InvalidArgument,
349 "instant effect modifier overflowed the base attribute");
350 candidateAttributes.modifyBase(modifier.attribute, modifier.value);
351 }
352 actor->attributes()->values = std::move(candidateAttributes);
353
354 StatusChangeEvent event;
355 event.actor = actor;
356 event.effectId = effectId;
357 event.source = source;
358 event.action = "apply";
359 emitChange(std::move(event));
361 }
362
363 auto statuses = actor->statuses();
364 LifecycleInstance *existing = findByType(*statuses, effectId);
365 if (existing) {
366 const std::string canonicalId = existing->id;
367 const int legacyId = legacyIdFor(*statuses, canonicalId);
368 const auto metadataIt = statuses->metadata.find(canonicalId);
369 if (legacyId <= 0 || metadataIt == statuses->metadata.end())
370 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
371 "RPG status adapter mapping is missing for an active effect", eve::StatusCode::Failed);
372
373 const StatusMetadata oldMetadata = metadataIt->second;
374 const StatusInstance current = project(*existing, oldMetadata, legacyId);
375 const std::string &policy = definition->stackPolicy;
376
377 if (!isBuiltinStackPolicy(policy)) {
378 const auto custom = stackPolicies().find(policy);
379 if (custom == stackPolicies().end() || !custom->second)
380 return rejected<int>(eve::DiagnosticCode::InvalidArgument,
381 "effect stack policy is not callable: " + policy);
382
383 StatusInstance candidate = current;
384 const int policyResult = custom->second(actor, candidate, *definition, source);
385 if (policyResult < 0) {
386 StatusChangeEvent event;
387 event.actor = actor;
388 event.instanceId = legacyId;
389 event.effectId = effectId;
390 event.source = source;
391 event.action = "reject";
392 event.stacks = current.stacks;
393 event.reason = "stackPolicy:" + policy;
394 emitChange(std::move(event));
395 return rejected<int>(eve::DiagnosticCode::Conflict,
396 "custom effect stack policy rejected the application", eve::StatusCode::Conflict);
397 }
399
400 auto candidateValidation = validateCandidate(candidate, current, *definition);
401 if (!candidateValidation.ok()) return eve::Result<int>::failure(candidateValidation.status());
402 if (policyResult != legacyId)
403 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
404 "custom effect stack policy returned a foreign status id",
406
407 const double candidateDuration = candidate.remaining < 0.0f ? 0.0 : candidate.remaining;
408 auto lifecycle =
409 makeLifecycleDefinition(*definition, ::eve::effects::StackMode::Reuse,
411 static_cast<std::uint32_t>(candidate.stacks), candidateDuration);
412 // A registered custom policy owns its own stack limit semantics.
413 // The generic container still supplies atomic instance replacement,
414 // but must not impose the legacy RPG default maxStacks of one.
415 lifecycle.policy.maxStacks = 0;
416 auto candidateContainer = statuses->container;
417 auto applied = candidateContainer.apply(lifecycle, "actor", source);
418 if (!applied.ok()) return eve::Result<int>::failure(applied.status());
419 const std::string updatedId = std::move(applied).takeValue();
420 if (updatedId != canonicalId)
421 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
422 "effect container changed an existing status identity", eve::StatusCode::Failed);
423 LifecycleInstance *updated = candidateContainer.find(canonicalId);
424 if (!updated)
425 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
426 "effect container lost an updated status instance", eve::StatusCode::Failed);
427 updated->source = candidate.source;
428 const int updatedStacks = candidate.stacks;
429
430 auto candidateAttributes = actor->attributes()->values;
431 auto bindings = prepareModifierBindings(candidateAttributes, *definition, *updated, oldMetadata);
432 if (!bindings.ok()) return eve::Result<int>::failure(bindings.status());
433
434 auto candidateMetadata = statuses->metadata;
435 auto &newMetadata = candidateMetadata[canonicalId];
436 newMetadata.periodAccum = candidate.periodAccum;
437 newMetadata.props = candidate.props;
438 newMetadata.appliedModifiers = std::move(bindings).takeValue();
439
440 auto candidateLegacyIdByEffect = statuses->legacyIdByEffect;
441 auto candidateEffectByLegacyId = statuses->effectByLegacyId;
442 commit(actor, std::move(candidateContainer), std::move(candidateMetadata),
443 std::move(candidateLegacyIdByEffect), std::move(candidateEffectByLegacyId), statuses->nextInstanceId,
444 std::move(candidateAttributes));
445
446 StatusChangeEvent event;
447 event.actor = actor;
448 event.instanceId = legacyId;
449 event.effectId = effectId;
450 event.source = source;
451 event.action = "stack";
452 event.stacks = updatedStacks;
453 emitChange(std::move(event));
455 }
456
457 if (policy == "none") {
458 StatusChangeEvent event;
459 event.actor = actor;
460 event.instanceId = legacyId;
461 event.effectId = effectId;
462 event.source = source;
463 event.action = "reject";
464 event.stacks = current.stacks;
465 event.reason = "stackPolicy:none";
466 emitChange(std::move(event));
467 return rejected<int>(eve::DiagnosticCode::Conflict, "effect is already active and uses stackPolicy:none",
469 }
470
474 std::string action = policy;
475 if (policy == "refresh") {
477 } else if (policy == "extend") {
479 } else if (policy == "stack") {
482 } else {
483 return rejected<int>(eve::DiagnosticCode::InvalidArgument,
484 "unknown built-in effect stack policy: " + policy);
485 }
486
487 auto lifecycle =
488 makeLifecycleDefinition(*definition, ::eve::effects::StackMode::Reuse, stackCount, durationPolicy);
489 lifecycle.policy.overflow = overflow;
490 auto candidateContainer = statuses->container;
491 auto applied = candidateContainer.apply(lifecycle, "actor", source);
492 if (!applied.ok()) return eve::Result<int>::failure(applied.status());
493 const std::string updatedId = std::move(applied).takeValue();
494 if (updatedId != canonicalId)
495 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
496 "effect container changed an existing status identity", eve::StatusCode::Failed);
497 LifecycleInstance *updated = candidateContainer.find(canonicalId);
498 if (!updated)
499 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
500 "effect container lost an updated status instance", eve::StatusCode::Failed);
501
502 auto candidateAttributes = actor->attributes()->values;
503 auto bindings = prepareModifierBindings(candidateAttributes, *definition, *updated, oldMetadata);
504 if (!bindings.ok()) return eve::Result<int>::failure(bindings.status());
505 const int updatedStacks = updated->stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
506 ? std::numeric_limits<int>::max()
507 : static_cast<int>(updated->stackCount);
508 auto candidateMetadata = statuses->metadata;
509 candidateMetadata[canonicalId].appliedModifiers = std::move(bindings).takeValue();
510 auto candidateLegacyIdByEffect = statuses->legacyIdByEffect;
511 auto candidateEffectByLegacyId = statuses->effectByLegacyId;
512 commit(actor, std::move(candidateContainer), std::move(candidateMetadata), std::move(candidateLegacyIdByEffect),
513 std::move(candidateEffectByLegacyId), statuses->nextInstanceId, std::move(candidateAttributes));
514
515 StatusChangeEvent event;
516 event.actor = actor;
517 event.instanceId = legacyId;
518 event.effectId = effectId;
519 event.source = source;
520 event.action = action;
521 event.stacks = updatedStacks;
522 emitChange(std::move(event));
524 }
525
526 auto candidateContainer = statuses->container;
527 const auto lifecycle = makeNewLifecycleDefinition(*definition);
528 auto applied = candidateContainer.apply(lifecycle, "actor", source);
529 if (!applied.ok()) return eve::Result<int>::failure(applied.status());
530 const std::string canonicalId = std::move(applied).takeValue();
531 LifecycleInstance *created = candidateContainer.find(canonicalId);
532 if (!created)
533 return rejected<int>(eve::DiagnosticCode::InvariantViolation,
534 "effect container did not retain a newly applied status", eve::StatusCode::Failed);
535
536 auto candidateMetadata = statuses->metadata;
537 auto candidateLegacyIdByEffect = statuses->legacyIdByEffect;
538 auto candidateEffectByLegacyId = statuses->effectByLegacyId;
539 int candidateNextInstanceId = statuses->nextInstanceId;
540 RPGActor::Statuses mappingState;
541 mappingState.nextInstanceId = candidateNextInstanceId;
542 mappingState.effectByLegacyId = candidateEffectByLegacyId;
543 auto legacy = allocateLegacyId(mappingState);
544 if (!legacy.ok()) return eve::Result<int>::failure(legacy.status());
545 const int legacyId = std::move(legacy).takeValue();
546 candidateNextInstanceId = mappingState.nextInstanceId;
547 candidateLegacyIdByEffect[canonicalId] = legacyId;
548 candidateEffectByLegacyId[legacyId] = canonicalId;
549
550 auto candidateAttributes = actor->attributes()->values;
551 StatusMetadata emptyMetadata;
552 auto bindings = prepareModifierBindings(candidateAttributes, *definition, *created, emptyMetadata);
553 if (!bindings.ok()) return eve::Result<int>::failure(bindings.status());
554 const int createdStacks = created->stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
555 ? std::numeric_limits<int>::max()
556 : static_cast<int>(created->stackCount);
557 candidateMetadata[canonicalId].appliedModifiers = std::move(bindings).takeValue();
558
559 commit(actor, std::move(candidateContainer), std::move(candidateMetadata), std::move(candidateLegacyIdByEffect),
560 std::move(candidateEffectByLegacyId), candidateNextInstanceId, std::move(candidateAttributes));
561
562 StatusChangeEvent event;
563 event.actor = actor;
564 event.instanceId = legacyId;
565 event.effectId = effectId;
566 event.source = source;
567 event.action = "apply";
568 event.stacks = createdStacks;
569 emitChange(std::move(event));
571}
572
574 if (!actor) return rejected<void>(eve::DiagnosticCode::InvalidArgument, "status removal requires an actor");
575 auto statuses = actor->statuses();
576 auto mapping = statuses->effectByLegacyId.find(instanceId);
577 if (mapping == statuses->effectByLegacyId.end())
578 return rejected<void>(eve::DiagnosticCode::NotFound, "status legacy instance id was not found",
580 const std::string canonicalId = mapping->second;
581 const LifecycleInstance *effect = statuses->container.find(canonicalId);
582 const auto metadataIt = statuses->metadata.find(canonicalId);
583 if (!effect || metadataIt == statuses->metadata.end())
584 return rejected<void>(eve::DiagnosticCode::InvariantViolation,
585 "RPG status adapter mapping points to stale state", eve::StatusCode::Failed);
586 const StatusInstance old = project(*effect, metadataIt->second, instanceId);
587
588 auto candidateContainer = statuses->container;
589 auto removed = candidateContainer.remove(canonicalId);
590 if (!removed.ok()) return eve::Result<void>::failure(removed.status());
591 auto candidateAttributes = actor->attributes()->values;
592 auto bindingsRemoved = removeBindings(candidateAttributes, metadataIt->second);
593 if (!bindingsRemoved.ok()) return eve::Result<void>::failure(bindingsRemoved.status());
594
595 auto candidateMetadata = statuses->metadata;
596 auto candidateLegacyIdByEffect = statuses->legacyIdByEffect;
597 auto candidateEffectByLegacyId = statuses->effectByLegacyId;
598 candidateMetadata.erase(canonicalId);
599 candidateLegacyIdByEffect.erase(canonicalId);
600 candidateEffectByLegacyId.erase(instanceId);
601 commit(actor, std::move(candidateContainer), std::move(candidateMetadata), std::move(candidateLegacyIdByEffect),
602 std::move(candidateEffectByLegacyId), statuses->nextInstanceId, std::move(candidateAttributes));
603
604 StatusChangeEvent event;
605 event.actor = actor;
606 event.instanceId = old.instanceId;
607 event.effectId = old.effectId;
608 event.source = old.source;
609 event.action = "remove";
610 event.stacks = old.stacks;
611 emitChange(std::move(event));
613}
614
615std::vector<std::string> matchingIds(const RPGActor::Statuses &statuses,
616 const std::function<bool(const LifecycleInstance &)> &predicate) {
617 std::vector<std::string> result;
618 result.reserve(static_cast<std::size_t>(statuses.container.effectCount()));
619 for (int i = 0; i < statuses.container.effectCount(); ++i) {
620 const LifecycleInstance *effect = statuses.container.effectAt(i);
621 if (effect && predicate(*effect)) result.push_back(effect->id);
622 }
623 return result;
624}
625
626int removeMatching(RPGActor *actor, const std::vector<std::string> &canonicalIds) {
627 int removed = 0;
628 for (const auto &canonicalId : canonicalIds) {
629 if (!actor) continue;
630 auto statuses = actor->statuses();
631 const auto mapping = statuses->legacyIdByEffect.find(canonicalId);
632 if (mapping == statuses->legacyIdByEffect.end()) continue;
633 auto didRemove = StatusSystem::remove(actor, mapping->second);
634 if (didRemove.ok()) ++removed;
635 }
636 return removed;
637}
638
639int StatusSystem::removeByEffect(RPGActor *actor, const std::string &effectId) {
640 if (!actor) return 0;
641 const auto ids = matchingIds(*actor->statuses(),
642 [&effectId](const LifecycleInstance &effect) { return effect.type == effectId; });
643 return removeMatching(actor, ids);
644}
645
646int StatusSystem::removeBySource(RPGActor *actor, const std::string &source) {
647 if (!actor) return 0;
648 const auto ids =
649 matchingIds(*actor->statuses(), [&source](const LifecycleInstance &effect) { return effect.source == source; });
650 return removeMatching(actor, ids);
651}
652
653int StatusSystem::removeByTag(RPGActor *actor, const std::string &tag) {
654 if (!actor) return 0;
655 const auto ids =
656 matchingIds(*actor->statuses(), [&tag](const LifecycleInstance &effect) { return effect.hasTag(tag); });
657 return removeMatching(actor, ids);
658}
659
660bool StatusSystem::hasEffect(RPGActor *actor, const std::string &effectId) {
661 return actor && findByType(*actor->statuses(), effectId) != nullptr;
662}
663
664bool StatusSystem::hasTag(RPGActor *actor, const std::string &tag) {
665 if (!actor) return false;
666 for (int i = 0; i < actor->statuses()->container.effectCount(); ++i) {
667 const LifecycleInstance *effect = actor->statuses()->container.effectAt(i);
668 if (effect && effect->hasTag(tag)) return true;
669 }
670 return false;
671}
672
673int StatusSystem::getActiveCount(RPGActor *actor) { return actor ? actor->statuses()->container.effectCount() : 0; }
674
676 if (!actor) return {};
677 const LifecycleInstance *effect = actor->statuses()->container.effectAt(index);
678 return effect ? effect->type : std::string{};
679}
680
682 if (!actor) return 0;
683 const LifecycleInstance *effect = actor->statuses()->container.effectAt(index);
684 if (!effect) return 0;
685 return effect->stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
686 ? std::numeric_limits<int>::max()
687 : static_cast<int>(effect->stackCount);
688}
689
691 if (!actor) return 0.0f;
692 const LifecycleInstance *effect = actor->statuses()->container.effectAt(index);
693 return effect ? static_cast<float>(effect->remaining) : 0.0f;
694}
695
697 if (!actor) return 0;
698 const LifecycleInstance *effect = actor->statuses()->container.effectAt(index);
699 return effect ? legacyIdFor(*actor->statuses(), effect->id) : 0;
700}
701
703 if (!actor) return {};
704 const LifecycleInstance *effect = actor->statuses()->container.effectAt(index);
705 return effect ? effect->source : std::string{};
706}
707
708std::string StatusSystem::getProp(RPGActor *actor, int instanceId, const std::string &key,
709 const std::string &fallback) {
710 if (!actor) return fallback;
711 const LifecycleInstance *effect = findByLegacyId(*actor->statuses(), instanceId);
712 if (!effect) return fallback;
713 const auto metadata = actor->statuses()->metadata.find(effect->id);
714 if (metadata == actor->statuses()->metadata.end()) return fallback;
715 const auto property = metadata->second.props.find(key);
716 return property == metadata->second.props.end() ? fallback : property->second;
717}
718
719bool StatusSystem::setProp(RPGActor *actor, int instanceId, const std::string &key, const std::string &value) {
720 if (!actor) return false;
721 LifecycleInstance *effect = findByLegacyId(*actor->statuses(), instanceId);
722 if (!effect) return false;
723 auto metadata = actor->statuses()->metadata.find(effect->id);
724 if (metadata == actor->statuses()->metadata.end()) return false;
725 metadata->second.props[key] = value;
726 return true;
727}
728
730 if (!std::isfinite(dt) || dt < 0.0)
731 return rejected<StatusUpdateSummary>(eve::DiagnosticCode::InvalidArgument,
732 "status update delta must be finite and non-negative");
733
735 total.elapsedSeconds = dt;
736
737 for (RPGActor *actor : RPGActor::liveActors()) {
738 if (!actor) continue;
739 auto statuses = actor->statuses();
740 std::vector<std::string> activeIds;
741 activeIds.reserve(static_cast<std::size_t>(statuses->container.effectCount()));
742 for (int i = 0; i < statuses->container.effectCount(); ++i) {
743 const LifecycleInstance *effect = statuses->container.effectAt(i);
744 if (effect) activeIds.push_back(effect->id);
745 }
746
747 auto candidateContainer = statuses->container;
748 auto candidateMetadata = statuses->metadata;
749 auto candidateLegacyIdByEffect = statuses->legacyIdByEffect;
750 auto candidateEffectByLegacyId = statuses->effectByLegacyId;
751 auto candidateAttributes = actor->attributes()->values;
752 std::vector<StatusTickEvent> pendingTicks;
753 std::vector<StatusChangeEvent> pendingChanges;
754
755 for (const auto &canonicalId : activeIds) {
756 const LifecycleInstance *effect = statuses->container.find(canonicalId);
757 auto metadataIt = candidateMetadata.find(canonicalId);
758 if (!effect || metadataIt == candidateMetadata.end())
759 return rejected<StatusUpdateSummary>(eve::DiagnosticCode::InvariantViolation,
760 "RPG status executor metadata is out of sync",
762 const EffectDefinition *definition = EffectRegistry::find(effect->type);
763 if (!definition || definition->period <= 0.0f) continue;
764
765 double available = dt;
766 if (effect->remaining >= 0.0) available = std::min(available, effect->remaining);
767 double accumulated = metadataIt->second.periodAccum + available;
768 const double period = static_cast<double>(definition->period);
769 const int legacyId = legacyIdFor(*statuses, canonicalId);
770 if (legacyId <= 0)
771 return rejected<StatusUpdateSummary>(eve::DiagnosticCode::InvariantViolation,
772 "RPG status executor has no legacy id mapping",
774 while (accumulated >= period) {
775 accumulated -= period;
777 tick.actor = actor;
778 tick.instanceId = legacyId;
779 tick.effectId = effect->type;
780 tick.source = effect->source;
781 tick.stacks = effect->stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
782 ? std::numeric_limits<int>::max()
783 : static_cast<int>(effect->stackCount);
784 pendingTicks.push_back(std::move(tick));
785 }
786 metadataIt->second.periodAccum = accumulated;
787 }
788
789 auto advanced = candidateContainer.update(dt);
790 if (!advanced.ok()) return eve::Result<StatusUpdateSummary>::failure(advanced.status());
791 const auto updateSummary = std::move(advanced).takeValue();
792
793 for (const auto &canonicalId : activeIds) {
794 if (candidateContainer.find(canonicalId)) continue;
795 const LifecycleInstance *oldEffect = statuses->container.find(canonicalId);
796 const auto oldMetadata = statuses->metadata.find(canonicalId);
797 const auto mapping = statuses->legacyIdByEffect.find(canonicalId);
798 if (!oldEffect || oldMetadata == statuses->metadata.end() || mapping == statuses->legacyIdByEffect.end())
799 return rejected<StatusUpdateSummary>(eve::DiagnosticCode::InvariantViolation,
800 "expired RPG status has incomplete adapter metadata",
802
803 auto bindingsRemoved = removeBindings(candidateAttributes, oldMetadata->second);
804 if (!bindingsRemoved.ok()) return eve::Result<StatusUpdateSummary>::failure(bindingsRemoved.status());
805
806 StatusChangeEvent expired;
807 expired.actor = actor;
808 expired.instanceId = mapping->second;
809 expired.effectId = oldEffect->type;
810 expired.source = oldEffect->source;
811 expired.action = "expire";
812 expired.stacks = oldEffect->stackCount > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
813 ? std::numeric_limits<int>::max()
814 : static_cast<int>(oldEffect->stackCount);
815 pendingChanges.push_back(std::move(expired));
816 candidateMetadata.erase(canonicalId);
817 candidateLegacyIdByEffect.erase(canonicalId);
818 candidateEffectByLegacyId.erase(mapping->second);
819 }
820
821 commit(actor, std::move(candidateContainer), std::move(candidateMetadata), std::move(candidateLegacyIdByEffect),
822 std::move(candidateEffectByLegacyId), statuses->nextInstanceId, std::move(candidateAttributes));
823 for (auto &tick : pendingTicks) {
824 tickQueue().push_back(std::move(tick));
825 ++total.ticks;
826 }
827 for (auto &change : pendingChanges) {
828 emitChange(std::move(change));
829 ++total.expired;
830 }
831 (void)updateSummary;
832 }
834}
835
836void StatusSystem::pollTicks(std::vector<StatusTickEvent> &out) {
837 auto &queue = tickQueue();
838 for (auto &event : queue) out.push_back(std::move(event));
839 queue.clear();
840}
841
842void StatusSystem::pollChanges(std::vector<StatusChangeEvent> &out) {
843 auto &queue = changeQueue();
844 for (auto &event : queue) out.push_back(std::move(event));
845 queue.clear();
846}
847
848} // namespace eve::rpg
double value
Value::Object payload
float duration
RPG's thin compatibility facade over the attributes module.
eve::Value condition
std::string message
DiagnosticCode code
std::uint32_t key
glm::uvec4 ids
float u
Definition Grass.cpp:233
std::array< double, 10 > q
std::string name
const std::string * tag
std::vector< std::shared_ptr< DeviceBytes > > bindings
Definition OnnxGpgpu.cpp:38
std::unique_ptr< ParticleEffect > effect
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::string action
Definition PlayHost.cpp:117
float t
int created
int removed
double current
SimulationTick tick
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
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
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
T takeValue() &&
Move the value out and remove it from this Result.
Definition Result.h:339
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Generic owning collection of canonical attributes and modifiers.
eve::Result< ModifierId > addModifier(AttributeModifier modifier)
Add or replace a canonical modifier and return its stable id.
eve::Result< void > removeModifier(const ModifierId &id)
Remove one modifier with a structured result.
Owns active effect instances and their deterministic lifecycle events.
EffectInstance * effectAt(int index)
Returns an active instance by deterministic index, or nullptr. @ownership Borrowed from this containe...
EffectInstance * find(const std::string &id)
Returns an active instance by stable id, or nullptr when absent. @ownership Borrowed from this contai...
int effectCount() const
Returns active instance count in deterministic creation order.
static AttributeOpTable & customOps()
Process-wide custom policy registry used only for Custom modifiers.
static const EffectDefinition * find(const std::string &id)
Find a registered effect definition by id.
Definition Effect.cpp:30
属性 / 状态 / 技能三表合一的 ECS 实体。
Definition RPGActor.h:34
static const std::vector< RPGActor * > & liveActors()
返回所有通过 createActor() 创建、且当前仍存活的 actor。 StatusSystem::update / SkillSystem::update 用它遍历所有 actor 逐帧推进。 ...
Definition RPGActor.cpp:42
static bool setProp(RPGActor *actor, int instanceId, const std::string &key, const std::string &value)
Sets the prop.
static void clearStackPolicies()
Clears stack policies.
static std::string getActiveSource(RPGActor *actor, int index)
Returns the active source.
static void unregisterLifecycleHook(const std::string &name)
Unregisters lifecycle hook.
static eve::Result< void > remove(RPGActor *actor, int instanceId)
Removal of one status by its legacy adapter id.
std::function< bool(RPGActor *actor, const EffectDefinition &def, const std::string &source, std::string &outReason)> ApplyCondition
施加前条件:返回 false 表示拒绝施加。 全部已注册条件按注册表遍历(AND);任一失败则 apply 返回 -1 并产生 action="reject" 事件。
std::function< int(RPGActor *actor, StatusInstance &existing, const EffectDefinition &def, const std::string &source)> StackPolicyFn
自定义叠加策略:在已有效果的临时 StatusInstance projection 上运行。
static int getActiveStacks(RPGActor *actor, int index)
Returns the active stacks.
static bool hasApplyCondition(const std::string &name)
True when apply condition.
static bool hasStackPolicy(const std::string &name)
True when stack policy.
static void pollChanges(std::vector< StatusChangeEvent > &out)
取出并清空自上次调用以来累积的生命周期变更事件。
static bool hasLifecycleHook(const std::string &name)
True when lifecycle hook.
static int getActiveInstanceId(RPGActor *actor, int index)
Returns the active instance id.
static bool hasEffect(RPGActor *actor, const std::string &effectId)
True when effect.
static void unregisterApplyCondition(const std::string &name)
Unregisters apply condition.
static void clearLifecycleHooks()
Clears lifecycle hooks.
static void pollTicks(std::vector< StatusTickEvent > &out)
取出并清空自上次调用以来累积的周期 tick 事件(push/poll 风格,见 event 模块)。
static bool hasTag(RPGActor *actor, const std::string &tag)
是否存在任一效果定义带有该 tag 的活动实例。
static int removeBySource(RPGActor *actor, const std::string &source)
移除该 actor 身上所有 source 匹配的实例;返回移除数量。
static void registerApplyCondition(const std::string &name, ApplyCondition fn)
Registers apply condition.
static void registerStackPolicy(const std::string &name, StackPolicyFn fn)
Registers stack policy.
static int getActiveCount(RPGActor *actor)
Returns the active count.
static std::string getProp(RPGActor *actor, int instanceId, const std::string &key, const std::string &fallback={})
按实例 id 读写 props;找不到实例时 get 返回 fallback,set 返回 false。
static void registerLifecycleHook(const std::string &name, LifecycleHook fn)
Registers lifecycle hook.
static int removeByEffect(RPGActor *actor, const std::string &effectId)
移除该 actor 身上所有 effectId 匹配的实例;返回移除数量。
static eve::Result< StatusUpdateSummary > update(double dt)
Advance all live RPG status executors with an injected simulation delta.
static void unregisterStackPolicy(const std::string &name)
Unregisters stack policy.
static void clearApplyConditions()
Clears apply conditions.
std::function< void(const StatusChangeEvent &ev)> LifecycleHook
生命周期钩子:每次产生 StatusChangeEvent 时同步回调(在事件入队之后)。
static int removeByTag(RPGActor *actor, const std::string &tag)
移除该 actor 身上所有 tag 匹配(效果定义带该 tag)的实例;返回移除数量。
static std::string getActiveEffectId(RPGActor *actor, int index)
Returns the active effect id.
static eve::Result< int > apply(RPGActor *actor, const std::string &effectId, const std::string &source="")
Status application backed by the generic EffectContainer.
static float getActiveRemaining(RPGActor *actor, int index)
Returns the active remaining.
eve::Result< ParsedAttributeOperation > parseAttributeOperation(std::string_view operation, double value)
Parse a built-in operation spelling used by existing data and APIs.
Result< T > rejected(RuleId rule, std::string message, eve::DiagnosticCode code=eve::DiagnosticCode::PreconditionViolation)
Construct a Rejected result for invalid editing input or preconditions.
ssq::Table project(HSQUIRRELVM vm, const eve::Result< void > &result)
Project a completed editing result that has no payload.
StackMode
Determines whether a matching application replaces, reuses, or creates an instance.
Definition EffectTypes.h:31
OverflowPolicy
Determines what happens when a stack-count limit would be exceeded.
Definition EffectTypes.h:43
DurationPolicy
Determines how an application changes an existing remaining duration.
Definition EffectTypes.h:37
StackCountPolicy
Determines how an application changes an existing stack count.
Definition EffectTypes.h:34
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
::eve::attributes::AttributeModifier AttributeModifier
std::vector< std::string > matchingIds(const RPGActor::Statuses &statuses, const std::function< bool(const LifecycleInstance &)> &predicate)
int removeMatching(RPGActor *actor, const std::vector< std::string > &canonicalIds)
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
SettlementPipeline::Stage fn
Immutable-by-convention template used to create effect instances.
One owned runtime effect fact.
EVENGINE_API_PLATFORM public API.
Definition Effect.h:35
Status lifecycle component backed by the generic effects container.
Definition RPGActor.h:58
std::unordered_map< std::string, int > legacyIdByEffect
Definition RPGActor.h:61
::eve::effects::EffectContainer container
Definition RPGActor.h:59
std::unordered_map< std::string, StatusExecutorMetadata > metadata
Definition RPGActor.h:60
std::unordered_map< int, std::string > effectByLegacyId
Definition RPGActor.h:62
状态生命周期变更事件(施加 / 刷新 / 叠层 / 延长 / 移除 / 到期 / 拒绝)。 与 StatusTickEvent 分开:tick 是周期性数值触发,change 是实例结构变化。
Definition StatusTypes.h:84
Legacy RPG projection of one effects::EffectInstance.
Definition StatusTypes.h:26
float remaining
剩余时间(秒);-1 表示 infinite(永久,直到手动移除)
Definition StatusTypes.h:31
std::string source
施加来源标签(施法者 id / 技能 id / 自定义),供查询与批量移除
Definition StatusTypes.h:29
std::unordered_map< std::string, std::string > props
运行时自定义键值(图标路径、UI 着色、脚本标记……)。 与 EffectDefinition::extra(定义侧)互补:extra 是模板数据,props 是实例数据。
Definition StatusTypes.h:41
float periodAccum
周期效果:距离上次 tick 的累积时间
Definition StatusTypes.h:32
周期性状态触发的 tick 事件。周期效果(period > 0)不会自动修改属性, 而是每个周期产生一个 tick 事件交给上层(脚本或 C++ 结算系统)处理—— 这样伤害/治疗类周期效果可以完整地...
Definition StatusTypes.h:69
class RPGActor * actor
Definition StatusTypes.h:70
Summary returned by the checked RPG status executor update.
Definition StatusTypes.h:57