载入中...
搜索中...
未找到
RpgDialect.cpp
浏览该文件的文档.
1#include "rpg/RpgDialect.h"
2
6#include "inventory/Bag.h"
8#include "rpg/GameState.h"
9#include "rpg/Party.h"
10#include "rpg/RPGActor.h"
11
12#include <cmath>
13#include <cstdint>
14#include <map>
15#include <string>
16#include <utility>
17#include <vector>
18
19namespace eve::rpg {
20
21namespace {
22
34
35constexpr double kNumberEpsilon = 1e-9;
36
37StepOutcome completed() { return StepOutcome{}; }
38
39StepOutcome failed(std::string message) {
40 StepOutcome outcome;
41 outcome.status = StepStatus::Failed;
42 outcome.error = std::move(message);
43 return outcome;
44}
45
46const eve::Value* field(const SequenceNode& node, const char* name) { return node.payload.find(name); }
47
48std::string stringField(const SequenceNode& node, const char* name) {
49 const eve::Value* value = node.payload.find(name);
50 return value && value->isString() ? value->asString() : std::string{};
51}
52
53double numberField(const SequenceNode& node, const char* name, double fallback) {
54 const eve::Value* value = node.payload.find(name);
55 if (!value || !value->isNumeric()) return fallback;
56 return value->isInt64() ? static_cast<double>(value->asInt()) : value->asDouble();
57}
58
59std::int64_t integerField(const SequenceNode& node, const char* name, std::int64_t fallback) {
60 const eve::Value* value = node.payload.find(name);
61 if (!value || !value->isInt64()) return fallback;
62 return value->asInt();
63}
64
65bool boolField(const SequenceNode& node, const char* name, bool fallback) {
66 const eve::Value* value = node.payload.find(name);
67 if (!value || !value->isBool()) return fallback;
68 return value->asBool();
69}
70
72std::function<eve::Result<void>(const SequenceNode&)> exactlyOneOf(std::vector<std::string> names,
73 std::string stepType) {
74 return [names = std::move(names), stepType = std::move(stepType)](const SequenceNode& node) -> eve::Result<void> {
75 int present = 0;
76 for (const auto& name : names) {
77 if (const eve::Value* value = node.payload.find(name); value && !value->isNull()) ++present;
78 }
79 if (present == 1) return eve::Result<void>::success();
80 std::string expected;
81 for (std::size_t index = 0; index < names.size(); ++index) {
82 if (index != 0) expected += " or ";
83 expected += "'" + names[index] + "'";
84 }
86 eve::DiagnosticCode::InvalidArgument, "step '" + stepType + "' requires exactly one of " + expected, {}, {}, "rpg.dnut-story"));
87 };
88}
89
91std::function<eve::Result<void>(const SequenceNode&)> oneOfValues(std::string fieldName,
92 std::vector<std::string> allowed,
93 std::string stepType) {
94 return [fieldName = std::move(fieldName), allowed = std::move(allowed),
95 stepType = std::move(stepType)](const SequenceNode& node) -> eve::Result<void> {
96 const eve::Value* value = node.payload.find(fieldName);
97 if (!value || !value->isString()) return eve::Result<void>::success();
98 for (const auto& candidate : allowed) {
99 if (value->asString() == candidate) return eve::Result<void>::success();
100 }
101 std::string accepted;
102 for (std::size_t index = 0; index < allowed.size(); ++index) {
103 if (index != 0) accepted += ", ";
104 accepted += allowed[index];
105 }
107 eve::DiagnosticCode::InvalidArgument, "step '" + stepType + "' field '" + fieldName + "' must be one of " + accepted, {}, {}, "rpg.dnut-story"));
108 };
109}
110
112RPGActor* resolveActor(const RpgStoryBinding* binding, const std::string& actorId) {
113 if (!binding) return nullptr;
114 if (binding->resolveActor) return binding->resolveActor(actorId);
115 if (!binding->party || actorId.empty()) return nullptr;
116 return binding->party->findMemberActor(actorId);
117}
118
119// --- domain step handlers --------------------------------------------------------------------
120
121StepOutcome applySkill(const SequenceNode& node, const StepContext& context) {
122 auto* binding = static_cast<RpgStoryBinding*>(context.host);
123 const std::string actorId = stringField(node, "actor");
124 RPGActor* actor = resolveActor(binding, actorId);
125 if (!actor) return failed("skill step: actor '" + actorId + "' cannot be resolved");
126 if (const eve::Value* learn = field(node, "learn"); learn && learn->isString()) {
127 actor->learnSkill(learn->asString());
128 return completed();
129 }
130 const eve::Value* forget = field(node, "forget");
131 if (!forget || !forget->isString()) return failed("skill step: neither learn nor forget is set");
132 if (!actor->forgetSkill(forget->asString()))
133 return failed("skill step: actor '" + actorId + "' does not know skill '" + forget->asString() + "'");
134 return completed();
135}
136
137StepOutcome applyAttribute(const SequenceNode& node, const StepContext& context) {
138 auto* binding = static_cast<RpgStoryBinding*>(context.host);
139 const std::string actorId = stringField(node, "actor");
140 RPGActor* actor = resolveActor(binding, actorId);
141 if (!actor) return failed("attribute step: actor '" + actorId + "' cannot be resolved");
142 const std::string name = stringField(node, "name");
143 const std::string op = stringField(node, "op");
144 const double value = numberField(node, "value", 0.0);
145 if (op == "set") {
146 actor->setBaseAttribute(name, value);
147 } else if (op == "add") {
148 actor->modifyBaseAttribute(name, value);
149 } else if (op == "sub") {
150 actor->modifyBaseAttribute(name, -value);
151 } else if (op == "mul") {
152 actor->setBaseAttribute(name, actor->getBaseAttribute(name) * value);
153 } else {
154 return failed("attribute step: unsupported op '" + op + "'");
155 }
156 return completed();
157}
158
159StepOutcome applyItem(const SequenceNode& node, const StepContext& context) {
160 auto* binding = static_cast<RpgStoryBinding*>(context.host);
161 if (!binding || !binding->bag) return failed("item step: no bag is bound");
162 const std::int64_t requested = integerField(node, "count", 1);
163 if (requested < 1) return failed("item step: count must be at least 1");
164 const auto quantity = static_cast<int>(requested);
165
166 if (const eve::Value* add = field(node, "add"); add && add->isString()) {
167 if (!binding->bag->canAddItem(add->asString(), quantity)) {
168 const std::string reason = binding->bag->canAddItemReason(add->asString(), quantity);
169 return failed("item step: bag refuses '" + add->asString() + "' (" +
170 (reason.empty() ? std::string("no capacity") : reason) + ")");
171 }
172 if (binding->bag->addItem(add->asString(), quantity) != quantity)
173 return failed("item step: bag did not accept the whole stack of '" + add->asString() + "'");
174 return completed();
175 }
176 const eve::Value* remove = field(node, "remove");
177 if (!remove || !remove->isString()) return failed("item step: neither add nor remove is set");
178 if (binding->bag->countItem(remove->asString()) < quantity)
179 return failed("item step: bag does not hold " + std::to_string(quantity) + " of '" + remove->asString() +
180 "'");
181 if (binding->bag->removeItem(remove->asString(), quantity) != quantity)
182 return failed("item step: bag did not remove the whole stack of '" + remove->asString() + "'");
183 return completed();
184}
185
186StepOutcome applyEquipment(const SequenceNode& node, const StepContext& context) {
187 auto* binding = static_cast<RpgStoryBinding*>(context.host);
188 if (!binding || !binding->equipment) return failed("equipment step: no equipment set is bound");
189 if (const eve::Value* equip = field(node, "equip"); equip && equip->isString()) {
190 if (!binding->bag) return failed("equipment step: equipping requires a bound bag");
191 const std::string slotName = stringField(node, "slot");
192 const int bagSlot = binding->bag->findItem(equip->asString());
193 if (bagSlot < 0) return failed("equipment step: bag does not hold '" + equip->asString() + "'");
194 if (!binding->equipment->equipFromBag(slotName, binding->bag, bagSlot))
195 return failed("equipment step: cannot equip '" + equip->asString() + "' into slot '" + slotName + "'");
196 return completed();
197 }
198 const eve::Value* unequip = field(node, "unequip");
199 if (!unequip || !unequip->isString()) return failed("equipment step: neither equip nor unequip is set");
200 if (!binding->bag) return failed("equipment step: unequipping requires a bound bag");
201 if (!binding->equipment->unequipToBag(unequip->asString(), binding->bag))
202 return failed("equipment step: cannot unequip slot '" + unequip->asString() + "'");
203 return completed();
204}
205
206StepOutcome applyFlag(const SequenceNode& node, const StepContext& context) {
207 auto* binding = static_cast<RpgStoryBinding*>(context.host);
208 if (!binding || !binding->gameState) return failed("flag step: no game state is bound");
209 binding->gameState->setSwitch(stringField(node, "name"), boolField(node, "state", true));
210 return completed();
211}
212
213StepOutcome applyVariable(const SequenceNode& node, const StepContext& context) {
214 auto* binding = static_cast<RpgStoryBinding*>(context.host);
215 if (!binding || !binding->gameState) return failed("variable step: no game state is bound");
216 const std::string name = stringField(node, "name");
217 const std::string op = stringField(node, "op");
218 const double value = numberField(node, "value", 0.0);
219 if (op == "set") {
220 binding->gameState->setVariable(name, value);
221 } else if (op == "add") {
222 binding->gameState->addVariable(name, value);
223 } else {
224 return failed("variable step: unsupported op '" + op + "'");
225 }
226 return completed();
227}
228
229// --- movement controller ---------------------------------------------------------------------
230
232std::string formatCoordinate(double value) {
233 if (std::fabs(value - std::round(value)) <= kNumberEpsilon)
234 return std::to_string(static_cast<long long>(std::llround(value)));
235 return std::to_string(value);
236}
237
247StepOutcome performMove(const SequenceNode& node, const StepContext& context) {
248 auto* binding = static_cast<RpgStoryBinding*>(context.host);
249 if (!binding || !binding->moveActor) {
250 StepOutcome presented;
251 presented.status = StepStatus::Blocked;
252 return presented;
253 }
254
255 StoryMoveRequest request;
256 request.actorId = stringField(node, "actor");
257 request.actor = resolveActor(binding, request.actorId);
258 request.x = numberField(node, "x", 0.0);
259 request.y = numberField(node, "y", 0.0);
260 if (const eve::Value* duration = field(node, "duration"); duration && duration->isNumeric()) {
261 request.hasDuration = true;
262 request.duration = numberField(node, "duration", 0.0);
263 }
264
265 auto started = binding->moveActor(request);
266 if (!started.ok()) {
267 const auto* diagnostic = started.error();
268 return failed(diagnostic ? diagnostic->message() : "move step: the movement controller failed");
269 }
270 switch (started.value()) {
271 case StoryMoveStatus::Arrived: return completed();
273 return failed("move step: actor '" + request.actorId + "' cannot reach (" + formatCoordinate(request.x) +
274 ", " + formatCoordinate(request.y) + ")");
275 case StoryMoveStatus::Travelling: break;
276 }
277 StepOutcome travelling;
278 travelling.status = StepStatus::Blocked;
279 return travelling;
280}
281
282// --- animation controller --------------------------------------------------------------------
283
297StepOutcome performAnimation(const SequenceNode& node, const StepContext& context) {
298 auto* binding = static_cast<RpgStoryBinding*>(context.host);
299 if (!binding || !binding->playAnimation) {
300 StepOutcome presented;
301 presented.status = StepStatus::Blocked;
302 return presented;
303 }
304
305 StoryAnimationRequest request;
306 request.targetId = stringField(node, "target");
307 request.actor = resolveActor(binding, request.targetId);
308 request.clip = stringField(node, "clip");
309 request.loop = boolField(node, "loop", false);
310 request.hold = boolField(node, "hold", false);
311
312 auto started = binding->playAnimation(request);
313 if (!started.ok()) {
314 const auto* diagnostic = started.error();
315 return failed(diagnostic ? diagnostic->message() : "animation step: the playback controller failed");
316 }
317 switch (started.value()) {
318 case StoryAnimationStatus::Finished: return completed();
320 return failed("animation step: target '" + request.targetId + "' cannot play clip '" + request.clip + "'");
322 }
323 StepOutcome playing;
324 playing.status = StepStatus::Blocked;
325 return playing;
326}
327
328// --- condition evaluation --------------------------------------------------------------------
329
330struct ResolvedState {
331 bool found = false;
332 double number = 0.0;
333};
334
335ResolvedState resolveStateName(const std::string& name, const RpgStoryBinding* binding) {
336 ResolvedState resolved;
337 if (!binding || !binding->gameState) return resolved;
338 if (name.rfind("switch.", 0) == 0) {
339 resolved.found = true;
340 resolved.number = binding->gameState->isSwitchOn(name.substr(7)) ? 1.0 : 0.0;
341 return resolved;
342 }
343 if (name.rfind("variable.", 0) == 0) {
344 resolved.found = true;
345 resolved.number = binding->gameState->getVariable(name.substr(9));
346 return resolved;
347 }
348 if (name.rfind("self.", 0) == 0) {
349 const std::string rest = name.substr(5);
350 const auto dot = rest.rfind('.');
351 if (dot == std::string::npos) return resolved;
352 const std::string scope = rest.substr(0, dot);
353 const std::string key = rest.substr(dot + 1);
354 if (!binding->gameState->hasSelfVariable(scope, key)) return resolved;
355 resolved.found = true;
356 resolved.number = binding->gameState->getSelfVariable(scope, key);
357 return resolved;
358 }
359 return resolved;
360}
361
362bool compareNumbers(double left, double right, const std::string& op) {
363 if (op == "eq") return std::fabs(left - right) <= kNumberEpsilon;
364 if (op == "ne") return std::fabs(left - right) > kNumberEpsilon;
365 if (op == "gt") return left > right + kNumberEpsilon;
366 if (op == "lt") return left < right - kNumberEpsilon;
367 if (op == "ge") return left >= right - kNumberEpsilon;
368 if (op == "le") return left <= right + kNumberEpsilon;
369 return false;
370}
371
372SequenceConditionOutcome evaluateCondition(const eve::Value& condition, const RpgStoryBinding* binding) {
373 if (!condition.isObject()) return {false, "invalid-condition"};
374
375 if (const eve::Value* all = condition.find("all"); all && all->isArray()) {
376 for (std::size_t index = 0; index < all->arraySize(); ++index) {
377 if (!evaluateCondition(all->at(index), binding).passed) return {false, "child-failed"};
378 }
379 return {true, {}};
380 }
381 if (const eve::Value* any = condition.find("any"); any && any->isArray()) {
382 for (std::size_t index = 0; index < any->arraySize(); ++index) {
383 if (evaluateCondition(any->at(index), binding).passed) return {true, {}};
384 }
385 return {false, "no-child-passed"};
386 }
387 if (const eve::Value* negated = condition.find("not"); negated) {
388 return evaluateCondition(*negated, binding).passed ? SequenceConditionOutcome{false, "negated"}
389 : SequenceConditionOutcome{true, {}};
390 }
391
392 const eve::Value* variable = condition.find("var");
393 const eve::Value* operation = condition.find("op");
394 const eve::Value* expected = condition.find("value");
395 if (!variable || !variable->isString() || !operation || !operation->isString() || !expected)
396 return {false, "invalid-condition"};
397
398 const ResolvedState resolved = resolveStateName(variable->asString(), binding);
399 if (!resolved.found) return {false, "missing-value"};
400
401 double expectedNumber = 0.0;
402 if (expected->isBool()) {
403 expectedNumber = expected->asBool() ? 1.0 : 0.0;
404 } else if (expected->isNumeric()) {
405 expectedNumber = expected->isInt64() ? static_cast<double>(expected->asInt()) : expected->asDouble();
406 } else {
407 return {false, "unsupported-operand"};
408 }
409 if (compareNumbers(resolved.number, expectedNumber, operation->asString())) return {true, {}};
410 return {false, "value-mismatch"};
411}
412
413// --- vocabulary ------------------------------------------------------------------------------
414
415StepField requiredField(std::string name, StepFieldType type) { return StepField{std::move(name), type, true}; }
416StepField optionalField(std::string name, StepFieldType type) { return StepField{std::move(name), type, false}; }
417
418std::function<eve::Result<void>(const SequenceNode&)> equipRequiresSlot() {
419 return [](const SequenceNode& node) -> eve::Result<void> {
420 if (const eve::Value* equip = node.payload.find("equip"); equip && !equip->isNull()) {
421 const eve::Value* slot = node.payload.find("slot");
422 if (!slot || !slot->isString() || slot->asString().empty())
424 eve::DiagnosticCode::InvalidArgument, "step 'equipment' requires 'slot' when 'equip' is set", {}, {}, "rpg.dnut-story"));
425 }
427 };
428}
429
430StepKindRegistry buildRpgStoryRegistry() {
431 StepKindRegistry registry;
432 const auto declare = [&registry](StepKindDescriptor descriptor) {
433 registry.registerStep(std::move(descriptor)).expect("RPG story step descriptor must register");
434 };
435 const auto bind = [&registry](const char* type, StepHandler handler) {
436 registry.registerHandler(type, std::move(handler)).expect("RPG story step handler must bind");
437 };
438
439 // `move` carries a handler so a host can install a movement controller. With
440 // no controller bound the handler presents the step itself, so both paths
441 // suspend the story and acknowledge with advance().
442 declare({"move", "Move an actor", "presentation", StepShape::Await,
443 {requiredField("actor", StepFieldType::String), requiredField("x", StepFieldType::Number),
444 requiredField("y", StepFieldType::Number), optionalField("duration", StepFieldType::Number)}});
445 bind("move", performMove);
446
447 // `animation` carries a handler for the same reason `move` does: a host can
448 // install a playback controller and stop branching on the step kind. With no
449 // controller bound the handler presents the step itself, so it takes the
450 // same host-presented path as the steps below.
451 declare({"animation", "Play an animation clip", "presentation", StepShape::Await,
452 {requiredField("target", StepFieldType::String), requiredField("clip", StepFieldType::String),
453 optionalField("loop", StepFieldType::Boolean), optionalField("hold", StepFieldType::Boolean)}});
454 bind("animation", performAnimation);
455
456 // Host-presented steps: registered without a handler, so the runtime suspends
457 // and the host presents the payload before acknowledging with advance().
458 declare({"select", "Select a world object", "presentation", StepShape::Await,
459 {requiredField("object", StepFieldType::String), optionalField("prompt", StepFieldType::String)}});
460 declare({"camera", "Move the camera", "presentation", StepShape::Await,
461 {requiredField("x", StepFieldType::Number), requiredField("y", StepFieldType::Number),
462 optionalField("duration", StepFieldType::Number)}});
463 declare({"dialogue", "Play a conversation", "dialogue", StepShape::Await,
464 {requiredField("id", StepFieldType::String)}});
465 declare({"message", "Show a message", "dialogue", StepShape::Await,
466 {requiredField("text", StepFieldType::String)}});
467
468 StepKindDescriptor skill;
469 skill.type = "skill";
470 skill.displayName = "Learn or forget a skill";
471 skill.category = "progression";
472 skill.shape = StepShape::Instant;
473 skill.fields = {requiredField("actor", StepFieldType::String), optionalField("learn", StepFieldType::String),
474 optionalField("forget", StepFieldType::String)};
475 skill.validate = exactlyOneOf({"learn", "forget"}, "skill");
476 declare(std::move(skill));
477 bind("skill", applySkill);
478
479 StepKindDescriptor attribute;
480 attribute.type = "attribute";
481 attribute.displayName = "Modify a base attribute";
482 attribute.category = "progression";
483 attribute.shape = StepShape::Instant;
484 attribute.fields = {requiredField("actor", StepFieldType::String), requiredField("name", StepFieldType::String),
485 requiredField("op", StepFieldType::String), requiredField("value", StepFieldType::Number)};
486 attribute.validate = oneOfValues("op", {"set", "add", "sub", "mul"}, "attribute");
487 declare(std::move(attribute));
488 bind("attribute", applyAttribute);
489
490 StepKindDescriptor item;
491 item.type = "item";
492 item.displayName = "Add or remove items";
493 item.category = "inventory";
494 item.shape = StepShape::Instant;
495 item.fields = {optionalField("add", StepFieldType::String), optionalField("remove", StepFieldType::String),
496 optionalField("count", StepFieldType::Integer)};
497 item.validate = exactlyOneOf({"add", "remove"}, "item");
498 declare(std::move(item));
499 bind("item", applyItem);
500
501 StepKindDescriptor equipment;
502 equipment.type = "equipment";
503 equipment.displayName = "Equip or unequip an item";
504 equipment.category = "inventory";
505 equipment.shape = StepShape::Instant;
506 equipment.fields = {requiredField("actor", StepFieldType::String), optionalField("equip", StepFieldType::String),
507 optionalField("unequip", StepFieldType::String), optionalField("slot", StepFieldType::String)};
508 equipment.validate = [](const SequenceNode& node) -> eve::Result<void> {
509 auto exclusive = exactlyOneOf({"equip", "unequip"}, "equipment")(node);
510 if (!exclusive.ok()) return exclusive;
511 return equipRequiresSlot()(node);
512 };
513 declare(std::move(equipment));
514 bind("equipment", applyEquipment);
515
516 StepKindDescriptor flag;
517 flag.type = "flag";
518 flag.displayName = "Set a story switch";
519 flag.category = "state";
520 flag.shape = StepShape::Instant;
521 flag.fields = {requiredField("name", StepFieldType::String), requiredField("state", StepFieldType::Boolean)};
522 declare(std::move(flag));
523 bind("flag", applyFlag);
524
525 StepKindDescriptor variable;
526 variable.type = "variable";
527 variable.displayName = "Set or add a story variable";
528 variable.category = "state";
529 variable.shape = StepShape::Instant;
530 variable.fields = {requiredField("name", StepFieldType::String), requiredField("op", StepFieldType::String),
531 requiredField("value", StepFieldType::Number)};
532 variable.validate = oneOfValues("op", {"set", "add"}, "variable");
533 declare(std::move(variable));
534 bind("variable", applyVariable);
535
536 return registry;
537}
538
539std::map<std::string, eve::dnut::SequenceAsset>& storyCatalogue() {
540 static std::map<std::string, eve::dnut::SequenceAsset> catalogue;
541 return catalogue;
542}
543
544std::string completionScope(const std::string& storyId) { return "story." + storyId; }
545
546} // namespace
547
548StepKindRegistry& rpgStoryStepRegistry() {
549 static StepKindRegistry registry = buildRpgStoryRegistry();
550 return registry;
551}
552
555 if (output.hasErrors()) {
556 std::string message = "story document contains errors";
557 for (const auto& diagnostic : output.diagnostics) {
558 if (diagnostic.severity == eve::dnut::DnutSeverity::Error) {
559 message = diagnostic.message;
560 break;
561 }
562 }
565 }
566 if (output.assets.empty())
568 eve::DiagnosticCode::InvalidArgument, "document declares no 'story' block", path, {}, "rpg.dnut-story"));
569
570 std::map<std::string, eve::dnut::SequenceAsset> proposed;
571 for (auto& asset : output.assets) {
572 const std::string id = asset.id;
573 proposed.emplace(id, std::move(asset));
574 }
575 storyCatalogue() = std::move(proposed);
576 return eve::Result<int>::success(static_cast<int>(storyCatalogue().size()));
577}
578
579void RpgStoryCatalogue::clear() { storyCatalogue().clear(); }
580
581int RpgStoryCatalogue::count() { return static_cast<int>(storyCatalogue().size()); }
582
583bool RpgStoryCatalogue::contains(const std::string& storyId) { return storyCatalogue().contains(storyId); }
584
585std::vector<std::string> RpgStoryCatalogue::storyIds() {
586 std::vector<std::string> ids;
587 ids.reserve(storyCatalogue().size());
588 for (const auto& entry : storyCatalogue()) ids.push_back(entry.first);
589 return ids;
590}
591
593public:
594 std::map<std::string, eve::dnut::SequenceAsset> assets;
598 std::string storyId;
600
603 Impl* self = this;
604 runtime.setAssetResolver([self](const std::string& id) -> const eve::dnut::SequenceAsset* {
605 const auto found = self->assets.find(id);
606 return found == self->assets.end() ? nullptr : &found->second;
607 });
611 [self](const eve::Value& condition) { return evaluateCondition(condition, self->binding); });
612 }
613
615 [[nodiscard]] bool snapshot(const std::string& id) {
616 assets.clear();
617 for (const auto& entry : storyCatalogue()) assets.emplace(entry.first, entry.second);
618 return assets.contains(id);
619 }
620
623 if (!binding || !binding->gameState || storyId.empty()) return;
624 const std::string scope = completionScope(storyId);
625 if (!runtime.isActive()) {
627 binding->gameState->setSelfVariable(scope, "completed", 1.0);
628 return;
629 }
630 eve::Value captured;
631 if (auto result = runtime.captureState(captured); !result.ok()) return;
632 auto encoded = captured.toJson();
633 if (!encoded.ok()) return;
634 binding->gameState->setSelfString(scope, "cursor", std::move(encoded).takeValue());
635 if (!binding->gameState->hasSelfVariable(scope, "completed"))
636 binding->gameState->setSelfVariable(scope, "completed", 0.0);
637 }
638
641};
642
643RpgStorySession::RpgStorySession() : impl_(std::make_unique<Impl>()) {}
646RpgStorySession& RpgStorySession::operator=(RpgStorySession&&) noexcept = default;
647
648eve::Result<void> RpgStorySession::begin(const std::string& storyId, GameState* gameState, Party* party,
649 eve::inventory::Bag* bag, eve::inventory::EquipmentSet* equipment) {
650 impl_->ownedBinding = RpgStoryBinding{};
651 impl_->ownedBinding.gameState = gameState;
652 impl_->ownedBinding.party = party;
653 impl_->ownedBinding.bag = bag;
654 impl_->ownedBinding.equipment = equipment;
655 return begin(storyId, &impl_->ownedBinding);
656}
657
659 stop();
660 impl_->binding = binding;
661 if (!impl_->snapshot(storyId))
663 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "story '" + storyId + "' is not published", "storyId", {}, "rpg.dnut-story"));
664
665 if (binding && binding->gameState) {
666 const std::string scope = completionScope(storyId);
667 const bool hasCompleted = binding->gameState->hasSelfVariable(scope, "completed");
668 const double completed = hasCompleted ? binding->gameState->getSelfVariable(scope, "completed") : 0.0;
669 if (hasCompleted && completed != 0.0 && completed != 1.0)
671 eve::Diagnostic::error(eve::DiagnosticCode::InvariantViolation, "persisted story-completion flag is invalid", scope + ".completed", {}, "rpg.dnut-story"));
672 if (completed == 1.0) {
673 const auto definition = impl_->assets.find(storyId);
674 if (definition != impl_->assets.end() && !definition->second.repeatable)
676 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "non-repeatable story '" + storyId + "' is already complete", "storyId", {}, "rpg.dnut-story"));
677 binding->gameState->setSelfVariable(scope, "completed", 0.0);
678 binding->gameState->clearSelfString(scope, "cursor");
679 }
680
681 if (binding->gameState->hasSelfString(scope, "cursor")) {
682 const std::string encoded = binding->gameState->getSelfString(scope, "cursor");
683 auto parsed = eve::Value::fromJson(encoded);
684 if (!parsed.ok())
686 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "persisted story cursor is not valid JSON", scope + ".cursor", {}, "rpg.dnut-story"));
687 impl_->storyId = storyId;
688 impl_->configureRuntime();
689 if (auto restored = impl_->runtime.restoreState(parsed.value()); !restored.ok()) {
690 const auto* diagnostic = restored.error();
691 const std::string message = diagnostic ? diagnostic->message() : "story cursor could not be restored";
692 stop();
694 eve::Diagnostic::error(eve::DiagnosticCode::Failed, message, scope + ".cursor", {}, "rpg.dnut-story"));
695 }
696 impl_->persistProgress();
698 }
699 }
700
701 impl_->storyId = storyId;
702 impl_->configureRuntime();
703 const auto definition = impl_->assets.find(storyId);
704 if (definition == impl_->assets.end())
706 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "story '" + storyId + "' is not published", "storyId", {}, "rpg.dnut-story"));
707 if (auto started = impl_->runtime.start(&definition->second); !started.ok()) {
708 const auto* diagnostic = started.error();
709 const std::string message = diagnostic ? diagnostic->message() : "story could not start";
710 stop();
712 eve::Diagnostic::error(eve::DiagnosticCode::Failed, message, "storyId", {}, "rpg.dnut-story"));
713 }
714 impl_->publishCompletion();
716}
717
719 if (!impl_->runtime.isActive())
721 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "story session is not active", "advance", {}, "rpg.dnut-story"));
722 if (auto result = impl_->runtime.advance(); !result.ok()) {
723 const auto* diagnostic = result.error();
725 eve::Diagnostic::error(eve::DiagnosticCode::Failed, diagnostic ? diagnostic->message() : "story could not advance", "advance", {}, "rpg.dnut-story"));
726 }
727 impl_->publishCompletion();
729}
730
731eve::Result<void> RpgStorySession::select(const std::string& routeLabel) {
732 if (!impl_->runtime.isActive())
734 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "story session is not active", "select", {}, "rpg.dnut-story"));
735 if (auto result = impl_->runtime.select(routeLabel); !result.ok()) {
736 const auto* diagnostic = result.error();
738 eve::Diagnostic::error(eve::DiagnosticCode::Failed, diagnostic ? diagnostic->message() : "choice could not be selected", "select", {}, "rpg.dnut-story"));
739 }
740 impl_->publishCompletion();
742}
743
745 if (!impl_->runtime.isActive())
747 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "story session is not active", "resume", {}, "rpg.dnut-story"));
748 if (auto applied = impl_->runtime.resumeStep(std::move(result)); !applied.ok()) {
749 const auto* diagnostic = applied.error();
751 eve::Diagnostic::error(eve::DiagnosticCode::Failed, diagnostic ? diagnostic->message() : "story could not resume", "resume", {}, "rpg.dnut-story"));
752 }
753 impl_->publishCompletion();
755}
756
758 impl_->runtime.stop();
759 impl_->assets.clear();
760 impl_->storyId.clear();
761 impl_->binding = nullptr;
762}
763
764bool RpgStorySession::isActive() const { return impl_->runtime.isActive(); }
765bool RpgStorySession::isBlocked() const { return impl_->runtime.isBlocked(); }
766bool RpgStorySession::isWaitingStep() const { return impl_->runtime.isWaitingStep(); }
767std::string RpgStorySession::getStoryId() const { return impl_->storyId; }
768
769std::string RpgStorySession::getStepKind() const {
770 const eve::dnut::SequenceNode* node = impl_->runtime.currentNode();
771 return node ? node->type : std::string{};
772}
773
775 const eve::dnut::SequenceNode* node = impl_->runtime.currentNode();
776 return node ? node->payload : impl_->emptyPayload;
777}
778
779std::vector<std::string> RpgStorySession::getChoiceLabels() const {
780 std::vector<std::string> labels;
781 const eve::dnut::SequenceNode* node = impl_->runtime.currentNode();
782 if (!node || node->type != "choice") return labels;
783 labels.reserve(node->routes.size());
784 for (const auto& route : node->routes) labels.push_back(route.label);
785 return labels;
786}
787
788eve::Result<void> RpgStorySession::captureState(eve::Value& out) const { return impl_->runtime.captureState(out); }
789
790eve::Result<void> RpgStorySession::restoreState(const std::string& storyId, const eve::Value& in,
792 stop();
793 impl_->binding = binding;
794 if (!impl_->snapshot(storyId))
796 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "story '" + storyId + "' is not published", "storyId", {}, "rpg.dnut-story"));
797 impl_->storyId = storyId;
798 impl_->configureRuntime();
799 if (auto restored = impl_->runtime.restoreState(in); !restored.ok()) {
800 const auto* diagnostic = restored.error();
801 const std::string message = diagnostic ? diagnostic->message() : "story cursor could not be restored";
802 stop();
804 eve::Diagnostic::error(eve::DiagnosticCode::Failed, message, "storyId", {}, "rpg.dnut-story"));
805 }
807}
808
809} // namespace eve::rpg
ActionParameterOperation operation
double value
float duration
std::string output
std::string descriptor
eve::Value condition
Registry-driven compiler for the `.dnut` story dialect.
全局游戏状态:开关(bool)、变量(数字)、独立变量(按作用域)。
std::string message
const GltfImportRequest & request
std::uint32_t key
glm::uvec4 ids
HexVec3 left
HexVec3 right
JobScope scope
HSQOBJECT handler
std::string name
eve::action::ActionVfxBinding binding
Generation-safe ordered RPG party relationships.
std::string path
Definition PlayHost.cpp:110
float begin
const RoadNode * node
double number
bool found
RPG vocabulary for the .dnut sequence language, plus its catalogue and session.
Cross-frame interpreter for compiled `.dnut` sequences.
Extensible step vocabulary, payload schema and dispatch.
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
const VegetationPresetContext & context
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
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
Definition Value.cpp:87
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
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
bool isNumeric() const noexcept
Return whether this value is an integer or finite number.
Definition Value.h:159
bool isString() const noexcept
Return true when this value is a string.
Definition Value.h:95
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
bool asBool() const
Return the boolean payload; the caller must have checked the kind.
Definition Value.cpp:81
bool isBool() const noexcept
Return true when this value is a boolean.
Definition Value.h:89
bool isObject() const noexcept
Return true when this value is an object.
Definition Value.h:99
Type type() const noexcept
Return the active value kind.
Definition Value.h:83
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
Caller-owned interpreter over a compiled sequence asset.
bool isActive() const
Whether a sequence is currently loaded.
void setHostContext(void *host)
Install the opaque consumer context forwarded to every step handler.
void setConditionEvaluator(ConditionEvaluator evaluator)
eve::Result< void > captureState(eve::Value &out) const
Capture the complete cursor, bindings, locals and call stack.
void setStepRegistry(const StepKindRegistry *registry)
Bind the step vocabulary; passing nullptr disables non-core steps.
void setAssetResolver(AssetResolver resolver)
Canonical step vocabulary owner and runtime handler router.
bool isArray() const
True when array.
Definition Json.cpp:332
bool isString() const
True when string.
Definition Json.cpp:330
bool isNull() const
True when null.
Definition Json.cpp:325
int asInt(int fallback=0) const
As int.
Definition Json.cpp:392
bool asBool(bool fallback=false) const
As bool.
Definition Json.cpp:364
bool isInt64() const
True when the JSON number was an integer literal that fits Int64.
Definition Json.cpp:328
一份游戏状态(开关/变量/独立变量)。
Definition GameState.h:25
void clearSelfString(const std::string &scope, const std::string &name)
Remove one scoped string fact; no-op when absent.
void setSelfString(const std::string &scope, const std::string &name, std::string value)
Scoped string facts (for example a .dnut story cursor JSON blob).
Definition GameState.cpp:88
bool hasSelfVariable(const std::string &scope, const std::string &name) const
True when self variable.
Definition GameState.cpp:82
void setSelfVariable(const std::string &scope, const std::string &name, double value)
独立变量:scope(如 "map:1:event:3")下按名字存数字。
Definition GameState.cpp:71
Authoritative ordered relationship between stable party member IDs and RPG actors....
Definition Party.h:24
static eve::Result< int > replaceFromDnutStrict(const std::string &source, const std::string &path)
Compile and atomically publish every story block of one document.
static std::vector< std::string > storyIds()
Return every published story id in lexical order.
static int count()
Return the published story count.
static bool contains(const std::string &storyId)
Return whether an exact story id is published.
static void clear()
Discard every published story.
eve::dnut::SequenceRuntime runtime
void publishCompletion()
Record completion once a successful run leaves the runtime inactive.
bool snapshot(const std::string &id)
Snapshot the whole catalogue so nothing borrows it after a hot replacement.
void persistProgress()
Publish or clear the GameState-backed cursor after a transition.
std::map< std::string, eve::dnut::SequenceAsset > assets
void configureRuntime()
Install the resolver, vocabulary, host binding and condition evaluator.
Caller-owned cursor over one published .dnut story.
Definition RpgDialect.h:275
bool isBlocked() const
Whether the story is suspended and awaiting acknowledgement.
std::vector< std::string > getChoiceLabels() const
Authored labels of the current choice step, empty for other step types.
std::string getStepKind() const
Current step type (move, choice, wait, …), or an empty string.
eve::Result< void > resume(eve::Value result)
Resume a step whose handler suspended the story.
eve::Result< void > captureState(eve::Value &out) const
Capture the complete cursor for persistence.
void stop()
Stop and clear the session.
eve::Result< void > begin(const std::string &storyId, RpgStoryBinding *binding)
Start a published story and run it to its first suspension point.
RpgStorySession()
Rpg story session.
eve::Result< void > advance()
Acknowledge the presented move / animation / select / dialogue / message / camera / wait step and con...
eve::Result< void > restoreState(const std::string &storyId, const eve::Value &in, RpgStoryBinding *binding)
Restore a captured cursor into a fresh session.
~RpgStorySession()
Rpg story session.
std::string getStoryId() const
Owning copy of the running story id, or an empty string.
eve::Result< void > select(const std::string &routeLabel)
Answer a blocked choice step by its authored label.
const eve::Value & getStepPayload() const
Current step payload.
bool isWaitingStep() const
Whether the suspension expects resume rather than advance.
bool isActive() const
Whether a story is loaded.
StepFieldType
Accepted value kind of one declared payload field.
std::function< StepOutcome(const SequenceNode &, const StepContext &)> StepHandler
Consumer-owned handler for one registered step type.
StepShape
Declares whether a step completes inside its call or suspends the runtime.
StepStatus
Terminal state of one dispatched step.
DnutCompileOutput compileDnut(std::string_view source, const std::string &path, const StepKindRegistry &registry)
Compile every story block of one .dnut document.
Result< T > failed(Status status, eve::DiagnosticCode code, RuleId rule, std::string message)
Construct a failed editing result with explicit status and diagnostic categories.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
const EditorValue * field(const EditorValue &value, const char *name)
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
StepKindRegistry & rpgStoryStepRegistry()
Process-local step vocabulary for the RPG story dialect.
@ Unavailable
The target or the clip cannot be played, so the animation step fails.
@ Finished
The clip was applied instantaneously, so the step completes inline.
@ Playing
Playback was accepted and is still running; the host acknowledges the step when it ends.
@ Arrived
The actor is already at the destination, so the step completes inline.
@ Unreachable
The destination cannot be reached, so the move step fails.
@ Travelling
The move was accepted and is still running; the host acknowledges the step when it ends.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Owning outcome of compiling one .dnut document.
Immutable, parameterized and versioned compiled sequence.
Outcome of one authored condition, as seen by the language core.
One immutable step of a compiled sequence.
Borrowed execution context handed to one step handler.
One declared payload field of a step type.
Owning authoring/runtime contract for one step type.
Structured outcome of one dispatched step.
Borrowed domain objects an RPG story step runs against.
Definition RpgDialect.h:169
GameState * gameState
Owner of switches, variables and per-story completion facts.
Definition RpgDialect.h:171
bool started
Definition Graphics.cpp:183