载入中...
搜索中...
未找到
DialogueFlow.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
6#include "common/SubjectRef.h"
13#include "i18n/I18n.h"
14
15#include <algorithm>
16#include <array>
17#include <simplesquirrel/simplesquirrel.hpp>
18#include <utility>
19
20namespace eve::dialogue {
21
23
24namespace {
25
26bool buildPoolWorkspace(const std::unordered_map<std::string, DataValue>& sources, DataValue& root,
27 std::string& error) {
28 DataValue::Object combined;
29 for (const auto& [sourceId, sourceRoot] : sources) {
30 const DataValue* pools = sourceRoot.find("pools");
31 if (!pools || !pools->isObject()) continue;
32 for (const auto& poolId : pools->keys()) {
33 if (combined.contains(poolId)) {
34 error = "pool '" + poolId + "' has multiple source owners (including '" + sourceId + "')";
35 return false;
36 }
37 combined.emplace(poolId, *pools->find(poolId));
38 }
39 }
40 root = DataValue::object({{"pools", DataValue::object(std::move(combined))}});
41 return true;
42}
43
44bool squirrelToState(HSQUIRRELVM vm, SQInteger index, StateValue& out) {
45 switch (sq_gettype(vm, index)) {
46 case OT_NULL: out = StateValue::null(); return true;
47 case OT_INTEGER: {
48 SQInteger value = 0;
49 if (SQ_FAILED(sq_getinteger(vm, index, &value))) return false;
51 return true;
52 }
53 case OT_FLOAT: {
54 SQFloat value = 0;
55 if (SQ_FAILED(sq_getfloat(vm, index, &value))) return false;
57 return true;
58 }
59 case OT_BOOL: {
60 SQBool value = SQFalse;
61 if (SQ_FAILED(sq_getbool(vm, index, &value))) return false;
62 out = StateValue::boolean(value != 0);
63 return true;
64 }
65 case OT_STRING: {
66 const SQChar* value = nullptr;
67 if (SQ_FAILED(sq_getstring(vm, index, &value))) return false;
68 out = StateValue::string(value ? value : "");
69 return true;
70 }
71 case OT_TABLE: {
72 out = StateValue::object();
73 const SQInteger absolute = index > 0 ? index : sq_gettop(vm) + index + 1;
74 sq_pushnull(vm);
75 while (SQ_SUCCEEDED(sq_next(vm, absolute))) {
76 const SQChar* key = nullptr;
77 StateValue value;
78 const bool ok = sq_gettype(vm, -2) == OT_STRING && SQ_SUCCEEDED(sq_getstring(vm, -2, &key)) && key &&
79 squirrelToState(vm, -1, value);
80 sq_pop(vm, 2);
81 if (!ok) {
82 sq_pop(vm, 1);
83 return false;
84 }
85 out.set(key, std::move(value));
86 }
87 sq_pop(vm, 1);
88 return true;
89 }
90 default: return false;
91 }
92}
93
94void pushState(HSQUIRRELVM vm, const StateValue& value) {
95 switch (value.kind()) {
96 case StateValue::Kind::Null: sq_pushnull(vm); break;
97 case StateValue::Kind::Int: sq_pushinteger(vm, value.asInt()); break;
98 case StateValue::Kind::Float: sq_pushfloat(vm, static_cast<SQFloat>(value.asDouble())); break;
99 case StateValue::Kind::Bool: sq_pushbool(vm, value.asBool() ? SQTrue : SQFalse); break;
100 case StateValue::Kind::String: sq_pushstring(vm, value.asString().c_str(), value.asString().size()); break;
102 sq_newarray(vm, 0);
103 for (size_t i = 0; i < value.arraySize(); ++i) {
104 pushState(vm, value.at(i));
105 sq_arrayappend(vm, -2);
106 }
107 break;
109 sq_newtable(vm);
110 for (const auto& key : value.keys()) {
111 sq_pushstring(vm, key.c_str(), key.size());
112 pushState(vm, *value.find(key));
113 sq_newslot(vm, -3, SQFalse);
114 }
115 break;
116 }
117}
118
119class DialogueSelectionParticipant final : public eve::transaction::ITransactionParticipant {
120public:
121 DialogueSelectionParticipant(eve::dnut::SequenceRuntime& runner, std::string routeId, eve::Value before)
122 : runner_(runner), routeId_(std::move(routeId)), before_(std::move(before)) {}
123
124 [[nodiscard]] std::string_view name() const noexcept override { return "dialogue.choice"; }
125 [[nodiscard]] eve::Result<void> prepare(const eve::transaction::TransactionContext& context) override {
126 if (context.transactionId().empty())
128 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue choice transaction requires a transaction id", "transactionId", {}, "dialogue.flow"));
129 if (phase_ != Phase::Idle)
131 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "dialogue choice is not idle", "transaction.lifecycle", {}, "dialogue.flow"));
132 phase_ = Phase::Prepared;
134 }
135 [[nodiscard]] eve::Result<void> commit(const eve::transaction::TransactionContext&) override {
136 if (phase_ != Phase::Prepared)
138 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "dialogue choice has no prepared stage", "transaction.lifecycle", {}, "dialogue.flow"));
139 auto selected = runner_.selectRouteForTransaction(routeId_);
140 if (!selected.ok()) {
141 std::string error = selected.status().describe();
142 auto restored = runner_.restoreState(before_);
143 if (!restored && error.empty()) error = restored.status().describe();
145 eve::Diagnostic::error(eve::DiagnosticCode::Failed, error.empty() ? "dialogue choice selection failed" : error, "route", {}, "dialogue.flow"));
146 }
147 phase_ = Phase::Committed;
149 }
150 [[nodiscard]] eve::Result<void> rollback(const eve::transaction::TransactionContext&) override {
151 if (phase_ != Phase::Prepared)
153 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "dialogue choice has no prepared stage to roll back", "transaction.lifecycle", {}, "dialogue.flow"));
154 phase_ = Phase::RolledBack;
156 }
157 [[nodiscard]] eve::Result<void> compensate(const eve::transaction::TransactionContext&) override {
158 if (phase_ != Phase::Committed)
160 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "dialogue choice is not committed", "transaction.lifecycle", {}, "dialogue.flow"));
161 auto restored = runner_.restoreState(before_);
162 if (!restored)
164 eve::Diagnostic::error(eve::DiagnosticCode::Failed, restored.status().describe(), "route", {}, "dialogue.flow"));
165 phase_ = Phase::RolledBack;
167 }
168
169private:
170 enum class Phase : std::uint8_t { Idle, Prepared, Committed, RolledBack };
172 std::string routeId_;
173 eve::Value before_;
174 Phase phase_ = Phase::Idle;
175};
176
177} // namespace
178
180 registerDialogueSequenceSteps(stepRegistry_).expect("register dialogue sequence vocabulary");
181 runner_.setStepRegistry(&stepRegistry_);
182 runner_.setAssetResolver([this](const std::string& id) { return find(id); });
184}
185
191
193 if (vm_ == nullptr) return startChecked(id, ssq::Object());
194 ssq::Table bindings(vm_);
195 return startChecked(id, ssq::Object(bindings));
196}
197
198eve::Result<void> DialogueFlow::publishGameplay(const std::string& instanceId, const std::string& ownerId) {
199 if (instanceId.empty() || ownerId.empty())
201 "publishGameplay needs an instance id and an owner id",
202 "instanceId"));
203 const auto instance = eve::PersistentId::parse(instanceId);
204 const auto owner = eve::PersistentId::parse(ownerId);
205 if (!instance.has_value() || !owner.has_value())
208 "instance and owner ids must be canonical persistent ids", "instanceId"));
209 if (!gameplay_) gameplay_ = std::make_unique<DialogueControl>(*this);
210 return gameplay_->publish(eve::SubjectRef::fromPersistentId(*instance), eve::SubjectRef::fromPersistentId(*owner));
211}
212
214 if (!gameplay_)
216 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "no dialogue instance is published", "instanceId"));
217 const auto instance = eve::PersistentId::parse(instanceId);
218 if (!instance.has_value())
220 eve::DiagnosticCode::InvalidArgument, "the instance id must be a canonical persistent id", "instanceId"));
221 return gameplay_->unpublish(eve::SubjectRef::fromPersistentId(*instance));
222}
223
225 if (gameplay_) gameplay_->clear();
226}
227
228int DialogueFlow::gameplayControlCount() const { return gameplay_ ? gameplay_->count() : 0; }
229
230std::vector<std::string> DialogueFlow::gameplayInstances() const {
231 std::vector<std::string> result;
232 if (!gameplay_) return result;
233 for (const auto& instance : gameplay_->instances()) result.push_back(instance.format());
234 return result;
235}
236
238 stateContext_.setSubject(std::move(config.subject));
239 stateContext_.setQueryProvider(config.stateQuery);
240 stateContext_.setMutationProvider(config.stateMutation);
241 configuredConditionEvaluator_ = std::move(config.conditionEvaluator);
242 operationRequestHandler_ = std::move(config.operationHandler);
243 gameplayActionHandler_ = std::move(config.gameplayActionHandler);
244 commandParticipantFactory_ = std::move(config.commandParticipantFactory);
245 contentReader_ = std::move(config.contentReader);
246 stateMutationProvider_ = config.stateMutation;
247 paymentAdapter_.setBindings(std::move(config.accounts));
248
249 runner_.setConditionEvaluator([this](const eve::Value& specification) {
250 if (specification.isString()) {
251 auto evaluated = evaluate(specification.asString(), toDialogueStateValue(runner_.bindings()),
252 toDialogueStateValue(runner_.locals()));
253 if (!evaluated.ok())
254 return eve::dnut::SequenceConditionOutcome{false, "expression_failed", evaluated.status().describe()};
255 StateValue value = std::move(evaluated).takeValue();
256 if (!value.isBool())
257 return eve::dnut::SequenceConditionOutcome{false, "expression_type",
258 "dialogue expression did not return bool"};
259 return eve::dnut::SequenceConditionOutcome{value.asBool(), value.asBool() ? "" : "expression_false", {}};
260 }
261 eve::decision::ConditionResult result = configuredConditionEvaluator_
262 ? configuredConditionEvaluator_(specification)
263 : stateContext_.evaluate(specification);
264 lastConditionResult_ = result;
266 result.passed(), std::string(eve::decision::conditionReasonCodeName(result.reasonCode())), {}};
267 });
268
269 updateRuntimeCommandDispatcher();
270}
271
272void DialogueFlow::updateRuntimeCommandDispatcher() {
273 if (operationRequestHandler_ || gameplayActionHandler_ || commandParticipantFactory_ || stateMutationProvider_ ||
274 manualCommandMode_) {
277 auto decoded = decodeDialogueCommandRequest(request);
278 if (!decoded.ok()) {
280 response.error = decoded.status().describe();
281 return response;
282 }
283 CommandResponse domain = dispatchCommand(std::move(decoded).takeValue());
284 response.value = std::move(domain.value);
285 response.error = std::move(domain.error);
286 if (domain.status == CommandResponse::Status::Blocked)
288 else if (domain.status == CommandResponse::Status::Failed)
290 return response;
291 });
292 } else {
293 runner_.clearCommandDispatcher();
294 }
295}
296
297void DialogueFlow::clearIntegration() { configureIntegration({}); }
298
299eve::Result<eve::MutationReceipt> DialogueFlow::applyStateMutations(std::span<const eve::StateMutation> mutations,
300 const eve::MutationContext& context) const {
301 return stateContext_.apply(mutations, context);
302}
303
304const eve::dnut::SequenceAsset* DialogueFlow::find(const std::string& id) const {
305 for (const auto& asset : assets_)
306 if (asset.id == id) return &asset;
307 return nullptr;
308}
309
310int DialogueFlow::loadDnutImpl(const std::string& source, const std::string& path) {
311 if (const auto it = sourceTexts_.find(path); it != sourceTexts_.end() && it->second == source) {
312 lastLoadChanged_ = false;
313 failureMessage_.clear();
314 return static_cast<int>(sourceAssets_[path].size());
315 }
316 diagnostics_.clear();
317 auto compiledDocument = compileDnutDocument(source, path, diagnostics_);
318 if (!compiledDocument) {
319 failureMessage_ = diagnostics_.empty() ? "conversation compilation failed" : diagnostics_.front().message;
320 return 0;
321 }
322 DnutDocument document = std::move(compiledDocument).takeValue();
323 std::vector<eve::dnut::SequenceAsset> compiled = std::move(document.conversations);
324 for (const auto& asset : compiled) {
325 const auto owner = assetSources_.find(asset.id);
326 if (owner != assetSources_.end() && owner->second != path) {
327 diagnostics_.push_back({ConversationDiagnostic::Severity::Error, path, 0,
328 "conversation '" + asset.id + "' is already owned by '" + owner->second + "'"});
329 failureMessage_ = diagnostics_.back().message;
330 return 0;
331 }
332 }
333 if (runner_.isActive()) {
334 failureMessage_ = "cannot load a dnut source while a conversation is active; use transactional reload";
335 return 0;
336 }
337 auto candidatePoolSources = sourcePools_;
338 candidatePoolSources[path] = std::move(document.poolRoot);
339 DataValue poolWorkspace;
340 if (!buildPoolWorkspace(candidatePoolSources, poolWorkspace, failureMessage_)) return 0;
341 Dialogue* dialogue = Dialogue::create();
342 if (!dialogue || (dialogue->replacePoolsFromData(poolWorkspace) == 0 && !dialogue->getLastPoolsError().empty())) {
343 failureMessage_ = dialogue ? dialogue->getLastPoolsError() : "dialogue pool runtime is unavailable";
344 return 0;
345 }
346 runner_.stop();
347 if (const auto old = sourceAssets_.find(path); old != sourceAssets_.end()) {
348 assets_.erase(std::remove_if(assets_.begin(), assets_.end(),
349 [&](const auto& asset) {
350 return std::find(old->second.begin(), old->second.end(), asset.id) !=
351 old->second.end();
352 }),
353 assets_.end());
354 }
355 std::vector<std::string> compiledIds;
356 for (auto& asset : compiled) {
357 compiledIds.push_back(asset.id);
358 auto it = std::find_if(assets_.begin(), assets_.end(), [&](const auto& old) { return old.id == asset.id; });
359 if (it == assets_.end())
360 assets_.push_back(std::move(asset));
361 else
362 *it = std::move(asset);
363 }
364 if (const auto old = sourceAssets_.find(path); old != sourceAssets_.end())
365 for (const auto& id : old->second) assetSources_.erase(id);
366 sourceTexts_[path] = source;
367 sourcePools_ = std::move(candidatePoolSources);
368 sourceAssets_[path] = std::move(compiledIds);
369 for (const auto& id : sourceAssets_[path]) assetSources_[id] = path;
370 lastLoadChanged_ = true;
371 failureMessage_.clear();
372 return static_cast<int>(compiled.size());
373}
374
375int DialogueFlow::reloadDnutImpl(const std::string& source, const std::string& path) {
376 if (const auto cached = sourceTexts_.find(path); cached != sourceTexts_.end() && cached->second == source) {
377 lastLoadChanged_ = false;
378 failureMessage_.clear();
379 return static_cast<int>(sourceAssets_[path].size());
380 }
381 std::vector<ConversationDiagnostic> candidateDiagnostics;
382 auto compiledDocument = compileDnutDocument(source, path, candidateDiagnostics);
383 if (!compiledDocument) {
384 diagnostics_ = std::move(candidateDiagnostics);
385 failureMessage_ = diagnostics_.empty() ? "conversation compilation failed" : diagnostics_.front().message;
386 lastLoadChanged_ = false;
387 return 0;
388 }
389 DnutDocument document = std::move(compiledDocument).takeValue();
390 std::vector<eve::dnut::SequenceAsset> compiled = std::move(document.conversations);
391 for (const auto& asset : compiled) {
392 const auto owner = assetSources_.find(asset.id);
393 if (owner != assetSources_.end() && owner->second != path) {
394 candidateDiagnostics.push_back({ConversationDiagnostic::Severity::Error, path, 0,
395 "conversation '" + asset.id + "' is already owned by '" +
396 owner->second + "'"});
397 diagnostics_ = std::move(candidateDiagnostics);
398 failureMessage_ = diagnostics_.back().message;
399 lastLoadChanged_ = false;
400 return 0;
401 }
402 }
403
404 std::vector<eve::dnut::SequenceAsset> candidate = assets_;
405 if (const auto old = sourceAssets_.find(path); old != sourceAssets_.end()) {
406 candidate.erase(std::remove_if(candidate.begin(), candidate.end(),
407 [&](const auto& asset) {
408 return std::find(old->second.begin(), old->second.end(), asset.id) !=
409 old->second.end();
410 }),
411 candidate.end());
412 }
413 std::vector<std::string> compiledIds;
414 for (auto& asset : compiled) {
415 compiledIds.push_back(asset.id);
416 auto existing =
417 std::find_if(candidate.begin(), candidate.end(), [&](const auto& old) { return old.id == asset.id; });
418 if (existing == candidate.end())
419 candidate.push_back(std::move(asset));
420 else
421 *existing = std::move(asset);
422 }
423 if (!lintConversationWorkspace(candidate, path, candidateDiagnostics)) {
424 diagnostics_ = std::move(candidateDiagnostics);
425 failureMessage_ = diagnostics_.empty() ? "conversation workspace lint failed" : diagnostics_.front().message;
426 lastLoadChanged_ = false;
427 return 0;
428 }
429
430 StateValue activeState;
431 const bool hadActive = runner_.isActive();
432 if (hadActive) {
433 auto captured = captureStateChecked();
434 if (!captured) {
435 failureMessage_ = captured.status().describe();
436 return 0;
437 }
438 activeState = std::move(captured).takeValue();
439 }
440 std::vector<eve::dnut::SequenceAsset> previous = assets_;
441 auto candidatePoolSources = sourcePools_;
442 candidatePoolSources[path] = std::move(document.poolRoot);
443 DataValue candidatePoolWorkspace;
444 if (!buildPoolWorkspace(candidatePoolSources, candidatePoolWorkspace, failureMessage_)) return 0;
445 Dialogue* dialogue = Dialogue::create();
446 if (!dialogue ||
447 (dialogue->replacePoolsFromData(candidatePoolWorkspace) == 0 && !dialogue->getLastPoolsError().empty())) {
448 failureMessage_ = dialogue ? dialogue->getLastPoolsError() : "dialogue pool runtime is unavailable";
449 return 0;
450 }
451 runner_.stop();
452 assets_ = std::move(candidate);
453 if (hadActive) {
454 StateValue migrated = activeState;
455 std::string restoreError;
456 auto migratedResult = migrations_.migrate(
457 migrated, [this](const std::string& id) { return find(id); });
458 bool restored = migratedResult.ok();
459 if (restored) {
460 migrated = std::move(migratedResult).takeValue();
461 auto restoreResult = runner_.restoreState(toCanonicalValue(migrated));
462 restored = restoreResult.ok();
463 if (!restored) restoreError = restoreResult.status().describe();
464 } else {
465 restoreError = migratedResult.status().describe();
466 }
467 if (!restored) {
468 assets_ = std::move(previous);
469 runner_.restoreState(toCanonicalValue(activeState))
470 .ignore("restore prior validated state after reload rollback");
471 DataValue previousPools;
472 std::string ignored;
473 if (dialogue && buildPoolWorkspace(sourcePools_, previousPools, ignored))
474 dialogue->replacePoolsFromData(previousPools);
475 failureMessage_ = "conversation hot reload rolled back: " + restoreError;
476 lastLoadChanged_ = false;
477 return 0;
478 }
479 }
480 diagnostics_ = std::move(candidateDiagnostics);
481 if (const auto old = sourceAssets_.find(path); old != sourceAssets_.end())
482 for (const auto& id : old->second) assetSources_.erase(id);
483 sourceTexts_[path] = source;
484 sourcePools_ = std::move(candidatePoolSources);
485 sourceAssets_[path] = std::move(compiledIds);
486 for (const auto& id : sourceAssets_[path]) assetSources_[id] = path;
487 lastLoadChanged_ = true;
488 failureMessage_.clear();
489 return static_cast<int>(compiled.size());
490}
491
492eve::Result<void> DialogueFlow::removeSourceChecked(const std::string& path) {
493 const auto source = sourceAssets_.find(path);
494 if (source == sourceAssets_.end())
496 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "dnut source is not loaded", path, {}, "dialogue.flow"));
497 if (runner_.isActive()) {
498 failureMessage_ = "cannot remove a dnut source while a conversation is active";
500 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, failureMessage_, path, {}, "dialogue.flow"));
501 }
502 auto candidatePoolSources = sourcePools_;
503 candidatePoolSources.erase(path);
504 DataValue poolWorkspace;
505 if (!buildPoolWorkspace(candidatePoolSources, poolWorkspace, failureMessage_))
507 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, failureMessage_, path, {}, "dialogue.flow"));
508 Dialogue* dialogue = Dialogue::create();
509 if (!dialogue || (dialogue->replacePoolsFromData(poolWorkspace) == 0 && !dialogue->getLastPoolsError().empty())) {
510 failureMessage_ = dialogue ? dialogue->getLastPoolsError() : "dialogue pool runtime is unavailable";
512 eve::Diagnostic::error(eve::DiagnosticCode::Failed, failureMessage_, path, {}, "dialogue.flow"));
513 }
514 assets_.erase(std::remove_if(assets_.begin(), assets_.end(),
515 [&](const auto& asset) {
516 return std::find(source->second.begin(), source->second.end(), asset.id) !=
517 source->second.end();
518 }),
519 assets_.end());
520 const std::vector<std::string> removedIds = source->second;
521 sourceAssets_.erase(source);
522 sourceTexts_.erase(path);
523 sourcePools_ = std::move(candidatePoolSources);
524 for (const auto& id : removedIds) assetSources_.erase(id);
525 lastLoadChanged_ = true;
527}
528
529eve::Result<void> DialogueFlow::lintAllChecked() {
530 diagnostics_.clear();
531 bool valid = lintConversationWorkspace(assets_, "<dialogue-workspace>", diagnostics_).ok();
532 for (const auto& asset : assets_) {
533 for (const auto& node : asset.nodes) {
534 const std::string assetPath = asset.id + "/" + node.id;
535 if (node.type == "line") {
536 if (!sequencePayloadString(node, "text").empty() && sequencePayloadString(node, "i18n").empty())
537 diagnostics_.push_back({ConversationDiagnostic::Severity::Warning, "<dialogue-workspace>",
538 node.sourceLine, "line has display text but no localization key",
539 "MissingLocalizationReference", node.sourceColumn, assetPath});
540 if (!sequencePayloadString(node, "speaker").empty() && sequencePayloadString(node, "voice").empty())
541 diagnostics_.push_back({ConversationDiagnostic::Severity::Warning, "<dialogue-workspace>",
542 node.sourceLine, "spoken line has no voice reference",
543 "MissingVoiceReference", node.sourceColumn, assetPath});
544 }
545 if (node.type == "choice") {
546 for (const auto& route : node.routes)
547 if (!sequenceRoutePayloadString(route, "text").empty() &&
548 sequenceRoutePayloadString(route, "i18n").empty())
549 diagnostics_.push_back(
550 {ConversationDiagnostic::Severity::Warning, "<dialogue-workspace>", route.sourceLine,
551 "choice route has display text but no localization key", "MissingLocalizationReference",
552 route.sourceColumn, assetPath + "/" + route.label});
553 }
554 if (node.type != "command") continue;
555 auto payment = decodeSequencePayment(node.payload);
556 auto mutations = decodeSequenceStateMutations(node.payload);
557 const bool paymentOk = payment.ok();
558 const bool mutationsOk = mutations.ok();
559 if (!paymentOk || !mutationsOk) {
560 diagnostics_.push_back({ConversationDiagnostic::Severity::Error, "<dialogue-workspace>",
561 node.sourceLine, "command has invalid transaction payload",
562 "InvalidCommandPayload", node.sourceColumn, assetPath});
563 valid = false;
564 continue;
565 }
566 const PaymentSpec decodedPayment = std::move(payment).takeValue();
567 const std::vector<eve::StateMutation> decodedMutations = std::move(mutations).takeValue();
568 const bool transactional = !decodedPayment.empty() || !decodedMutations.empty();
569 const bool gameplay = sequencePayloadString(node, "kind") == "gameplay";
570 const bool legacyHandler =
571 !gameplay ? static_cast<bool>(operationRequestHandler_) : static_cast<bool>(gameplayActionHandler_);
572 bool handled = false;
573 if (!transactional)
574 handled = legacyHandler || manualCommandMode_ || static_cast<bool>(commandParticipantFactory_);
575 else
576 handled = (commandParticipantFactory_ || !decodedMutations.empty()) &&
577 (decodedMutations.empty() || stateMutationProvider_) &&
578 (decodedPayment.empty() || commandParticipantFactory_ || !decodedMutations.empty());
579 if (!handled) {
580 diagnostics_.push_back({ConversationDiagnostic::Severity::Error, "<dialogue-workspace>",
581 node.sourceLine, "command has no compatible runtime handler",
582 "MissingCommandHandler", node.sourceColumn, assetPath});
583 valid = false;
584 }
585 }
586 }
587 failureMessage_ = valid || diagnostics_.empty() ? std::string{} : diagnostics_.front().message;
589 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, "<dialogue-workspace>", {}, "dialogue.flow"));
591}
592
593eve::Result<void> DialogueFlow::renameConversationChecked(const std::string& oldId, const std::string& newId) {
594 if (runner_.isActive()) {
595 failureMessage_ = "cannot rename a conversation while a conversation is active";
597 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, failureMessage_, oldId, {}, "dialogue.flow"));
598 }
599 std::vector<eve::dnut::SequenceAsset> candidate = assets_;
600 auto renamed = renameConversationAsset(candidate, oldId, newId);
601 if (!renamed) {
602 failureMessage_ = renamed.status().describe();
603 return eve::Result<void>::failure(renamed.status());
604 }
605 std::vector<ConversationDiagnostic> candidateDiagnostics;
606 if (!lintConversationWorkspace(candidate, "<dialogue-workspace>", candidateDiagnostics)) {
607 diagnostics_ = std::move(candidateDiagnostics);
608 failureMessage_ = diagnostics_.empty() ? "conversation rename validation failed" : diagnostics_.front().message;
610 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, oldId, {}, "dialogue.flow"));
611 }
612 assets_ = std::move(candidate);
613 if (const auto owner = assetSources_.find(oldId); owner != assetSources_.end()) {
614 const std::string path = owner->second;
615 assetSources_.erase(owner);
616 assetSources_[newId] = path;
617 }
618 for (auto& [path, ids] : sourceAssets_)
619 for (auto& id : ids)
620 if (id == oldId) id = newId;
622}
623
624eve::Result<void> DialogueFlow::renameNodeChecked(const std::string& conversationId, const std::string& oldId,
625 const std::string& newId) {
626 if (runner_.isActive()) {
627 failureMessage_ = "cannot rename a node while a conversation is active";
629 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, failureMessage_, conversationId + "/" + oldId, {}, "dialogue.flow"));
630 }
631 std::vector<eve::dnut::SequenceAsset> candidate = assets_;
632 auto renamed = renameConversationNode(candidate, conversationId, oldId, newId);
633 if (!renamed) {
634 failureMessage_ = renamed.status().describe();
635 return eve::Result<void>::failure(renamed.status());
636 }
637 std::vector<ConversationDiagnostic> candidateDiagnostics;
638 if (!lintConversationWorkspace(candidate, "<dialogue-workspace>", candidateDiagnostics)) {
639 diagnostics_ = std::move(candidateDiagnostics);
640 failureMessage_ = diagnostics_.empty() ? "node rename validation failed" : diagnostics_.front().message;
642 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, conversationId + "/" + oldId, {}, "dialogue.flow"));
643 }
644 assets_ = std::move(candidate);
646}
647
648int DialogueFlow::loadDnutFileImpl(const std::string& path) {
649 std::string source;
650 if (contentReader_) {
651 auto content = contentReader_(path);
652 if (!content) {
653 failureMessage_ = content.status().describe();
654 return 0;
655 }
656 source = std::move(content).takeValue();
657 } else {
658 auto* filesystem = eve::cap::query<eve::service::IFileSystem>();
659 std::vector<std::uint8_t> bytes;
660 if (!filesystem || !filesystem->readFile(path, bytes)) {
661 failureMessage_ = path + ": dialogue content read failed";
662 return 0;
663 }
664 source.assign(reinterpret_cast<const char*>(bytes.data()), bytes.size());
665 }
666 return loadDnutImpl(source, path);
667}
668
669namespace {
670eve::Result<int> dnutLoadResult(int count, const std::string& error, const std::string& sourceId) {
671 if (count > 0 || error.empty()) return eve::Result<int>::success(count);
673 eve::DiagnosticCode::Failed, error.empty() ? "dnut load failed" : error, sourceId, {}, "dialogue.dnut"));
674}
675} // namespace
676
677eve::Result<int> DialogueFlow::loadDnutChecked(const std::string& source, const std::string& sourceId) {
678 return dnutLoadResult(loadDnutImpl(source, sourceId), failureMessage_, sourceId);
679}
680
681eve::Result<int> DialogueFlow::reloadDnutChecked(const std::string& source, const std::string& sourceId) {
682 return dnutLoadResult(reloadDnutImpl(source, sourceId), failureMessage_, sourceId);
683}
684
685eve::Result<int> DialogueFlow::loadDnutFileChecked(const std::string& path) {
686 return dnutLoadResult(loadDnutFileImpl(path), failureMessage_, path);
687}
688
689eve::Result<int> DialogueFlow::mergeImported(std::vector<eve::dnut::SequenceAsset> imported) {
690 runner_.stop();
691 const int count = static_cast<int>(imported.size());
692 for (auto& asset : imported) {
693 auto it = std::find_if(assets_.begin(), assets_.end(), [&](const auto& old) { return old.id == asset.id; });
694 if (it == assets_.end())
695 assets_.push_back(std::move(asset));
696 else
697 *it = std::move(asset);
698 }
699 failureMessage_.clear();
701}
702
703eve::Result<int> DialogueFlow::importYarnChecked(const std::string& source, const std::string& path) {
704 diagnostics_.clear();
705 auto imported = importYarnConversation(source, path, diagnostics_);
706 if (!imported) {
707 failureMessage_ = diagnostics_.empty() ? "Yarn import failed" : diagnostics_.front().message;
708 return eve::Result<int>::failure(imported.status());
709 }
710 return mergeImported(std::move(imported).takeValue());
711}
712
713eve::Result<int> DialogueFlow::importTweeChecked(const std::string& source, const std::string& path) {
714 diagnostics_.clear();
715 auto imported = importTweeConversation(source, path, diagnostics_);
716 if (!imported) {
717 failureMessage_ = diagnostics_.empty() ? "Twee import failed" : diagnostics_.front().message;
718 return eve::Result<int>::failure(imported.status());
719 }
720 return mergeImported(std::move(imported).takeValue());
721}
722
723void DialogueFlow::clear() {
724 runner_.stop();
725 assets_.clear();
726 sourceTexts_.clear();
727 sourceAssets_.clear();
728 assetSources_.clear();
729 sourcePools_.clear();
730 if (Dialogue* dialogue = Dialogue::create())
731 dialogue->replacePoolsFromData(DataValue::object({{"pools", DataValue::object({})}}));
732 localization_.clear();
733 migrations_.clear();
734 textRenderer_.clearToneRules();
735 locale_.clear();
736 diagnostics_.clear();
737 failureMessage_.clear();
738}
739
740int DialogueFlow::getConversationCount() const { return static_cast<int>(assets_.size()); }
741
742std::string DialogueFlow::getConversationId(int index) const {
743 return index >= 0 && static_cast<size_t>(index) < assets_.size() ? assets_[static_cast<size_t>(index)].id
744 : std::string{};
745}
746
747bool DialogueFlow::hasConversation(const std::string& id) const { return find(id) != nullptr; }
748
749std::string DialogueFlow::exportLocalizationCsv() const { return exportConversationLocalizationCsv(assets_); }
750
751eve::Result<int> DialogueFlow::importLocalizationCsvChecked(const std::string& csv,
752 const std::string& defaultLocale) {
753 diagnostics_.clear();
754 const int count = localization_.importCsv(csv, defaultLocale, diagnostics_);
755 failureMessage_ = count > 0 || diagnostics_.empty() ? std::string{} : diagnostics_.front().message;
756 if (!failureMessage_.empty())
758 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, failureMessage_, "dialogue.localization.csv", {}, "dialogue.flow"));
760}
761
762std::string DialogueFlow::exportMissingLocalizationCsv(const std::string& locale) const {
763 return localization_.exportMissingCsv(assets_, locale);
764}
765
766std::string DialogueFlow::exportVoiceRecordingCsv(const std::string& locale) const {
767 return localization_.exportVoiceRecordingCsv(assets_, locale);
768}
769
770eve::Result<int> DialogueFlow::validateLocalization(const eve::i18n::I18n& localization,
771 const std::string& locale) const {
772 if (locale.empty() || !localization.hasLanguage(locale))
774 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "dialogue localization locale is unavailable",
775 "dialogue.localization." + locale, {}, "dialogue.localization"));
776 int validated = 0;
777 for (const auto& asset : assets_)
778 for (const auto& node : asset.nodes) {
779 const std::string key = sequencePayloadString(node, "i18n");
780 if (key.empty()) continue;
781 if (!localization.hasInLanguage(locale, key))
783 eve::DiagnosticCode::NotFound, "dialogue localization key is missing from the exact locale: " + key,
784 "dialogue." + asset.id + "." + node.id + ".i18n", {}, "dialogue.localization"));
785 ++validated;
786 }
788}
789
790int DialogueFlow::getDiagnosticCount() const { return static_cast<int>(diagnostics_.size()); }
791
792std::string DialogueFlow::getDiagnosticSeverity(int index) const {
793 if (index < 0 || static_cast<size_t>(index) >= diagnostics_.size()) return {};
794 return diagnostics_[static_cast<size_t>(index)].severity == ConversationDiagnostic::Severity::Error ? "error"
795 : "warning";
796}
797
798std::string DialogueFlow::getDiagnosticPath(int index) const {
799 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<size_t>(index)].path : std::string{};
800}
801
802int DialogueFlow::getDiagnosticLine(int index) const {
803 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<size_t>(index)].line : 0;
804}
805
806ConversationDocument* DialogueFlow::newDocument(const std::string& id) const { return new ConversationDocument(id); }
807
808ConversationDocument* DialogueFlow::getDocument(const std::string& id) const {
809 const auto* asset = find(id);
810 return asset ? new ConversationDocument(*asset) : nullptr;
811}
812
813eve::Result<void> DialogueFlow::applyDocumentChecked(ConversationDocument* document) {
814 if (!document) {
815 failureMessage_ = "conversation document must not be null";
817 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, "authoring", {}, "dialogue.flow"));
818 }
819 std::vector<eve::dnut::SequenceAsset> candidate = assets_;
820 const auto existing = std::find_if(candidate.begin(), candidate.end(),
821 [&](const auto& asset) { return asset.id == document->getId(); });
822 if (existing == candidate.end())
823 candidate.push_back(document->asset());
824 else
825 *existing = document->asset();
826 std::vector<ConversationDiagnostic> candidateDiagnostics;
827 if (!lintConversationWorkspace(candidate, "authoring", candidateDiagnostics)) {
828 diagnostics_ = std::move(candidateDiagnostics);
829 failureMessage_ =
830 diagnostics_.empty() ? "conversation document validation failed" : diagnostics_.front().message;
832 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, "authoring", {}, "dialogue.flow"));
833 }
834 assets_ = std::move(candidate);
835 diagnostics_.clear();
836 failureMessage_.clear();
838}
839
840int DialogueFlow::getDiagnosticColumn(int index) const {
841 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<size_t>(index)].column : 0;
842}
843
844std::string DialogueFlow::getDiagnosticCode(int index) const {
845 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<size_t>(index)].code : std::string{};
846}
847
848std::string DialogueFlow::getDiagnosticAssetPath(int index) const {
849 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<size_t>(index)].assetPath
850 : std::string{};
851}
852
853std::string DialogueFlow::getDiagnosticMessage(int index) const {
854 return index >= 0 && static_cast<size_t>(index) < diagnostics_.size()
855 ? diagnostics_[static_cast<size_t>(index)].message
856 : std::string{};
857}
858
859eve::Result<void> DialogueFlow::startChecked(const std::string& id, ssq::Object bindings) {
860 StateValue converted = StateValue::object();
861 if (vm_) {
862 const SQInteger top = sq_gettop(vm_);
863 sq_pushobject(vm_, bindings.getRaw());
864 const bool ok = squirrelToState(vm_, -1, converted) && converted.isObject();
865 sq_settop(vm_, top);
866 if (!ok) {
867 failureMessage_ = "conversation bindings must be a scalar-only table";
869 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, failureMessage_, "dialogue.bindings", {}, "dialogue.flow"));
870 }
871 }
872 const eve::dnut::SequenceAsset* asset = find(id);
873 if (!asset)
875 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "conversation was not found: " + id, "dialogue." + id, {}, "dialogue.flow"));
876 auto started = runner_.start(asset, toCanonicalValue(converted));
877 if (!started) return eve::Result<void>::failure(started.status());
879}
880
881eve::Result<void> DialogueFlow::advanceChecked() {
882 auto advanced = runner_.advance();
883 if (!advanced) return eve::Result<void>::failure(advanced.status());
885}
886
887eve::Result<void> DialogueFlow::resumeCommandChecked(const std::string& requestId, eve::Value value) {
888 return runner_.resumeCommand(requestId, std::move(value));
889}
890
891eve::Result<void> DialogueFlow::select(const std::string& routeId) {
892 const auto* node = runner_.currentNode();
893 if (!node) return runner_.selectRouteForTransaction(routeId);
894 const eve::dnut::SequenceRoute* route = nullptr;
895 for (const auto& candidate : node->routes) {
896 if (candidate.label == routeId) {
897 route = &candidate;
898 break;
899 }
900 }
901 if (!route) return runner_.selectRouteForTransaction(routeId);
902 auto paymentResult = decodeSequencePayment(route->payload);
903 if (!paymentResult.ok()) return eve::Result<void>::failure(paymentResult.status());
904 auto mutationResult = decodeSequenceStateMutations(route->payload);
905 if (!mutationResult.ok()) return eve::Result<void>::failure(mutationResult.status());
906 PaymentSpec payment = std::move(paymentResult).takeValue();
907 std::vector<eve::StateMutation> mutations = std::move(mutationResult).takeValue();
908 if (payment.empty() && mutations.empty()) return runner_.selectRouteForTransaction(routeId);
909 if (!stateMutationProvider_ && !mutations.empty()) {
911 eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "dialogue choice state mutations require a StatePatch-compatible provider", "route.stateMutations", {}, "dialogue.flow"));
912 }
913
914 eve::Value before;
915 auto captured = runner_.captureState(before);
916 if (!captured.ok()) return eve::Result<void>::failure(captured.status());
917 DialogueSelectionParticipant selection(runner_, routeId, std::move(before));
918 std::unique_ptr<DialogueStateMutationParticipant> state;
919 std::vector<eve::transaction::ITransactionParticipant*> effects{&selection};
920 if (!mutations.empty()) {
921 state = std::make_unique<DialogueStateMutationParticipant>(*stateMutationProvider_, mutations);
922 effects.push_back(state.get());
923 }
924 auto committed = paymentAdapter_.execute(nextTransactionId("choice"), payment, effects);
925 if (!committed) return eve::Result<void>::failure(committed.status());
926 (void)std::move(committed).takeValue();
928}
929
930std::string DialogueFlow::nextTransactionId(const char* purpose) {
931 return std::string("dialogue-") + purpose + "-" + std::to_string(transactionSequence_++);
932}
933
934CommandResponse DialogueFlow::dispatchCommand(const CommandRequest& request) {
935 const bool transactional = !request.payment.empty() || !request.stateMutations.empty();
936 CommandRequestHandler* legacy =
937 request.kind == CommandRequestKind::Operation ? &operationRequestHandler_ : &gameplayActionHandler_;
938 if (!transactional && *legacy) return (*legacy)(request);
939
940 if (!transactional && manualCommandMode_) {
941 CommandResponse response;
942 response.status = CommandResponse::Status::Blocked;
943 return response;
944 }
945
946 if (!transactional && !commandParticipantFactory_) {
947 CommandResponse response;
948 response.status = CommandResponse::Status::Failed;
949 response.error = request.kind == CommandRequestKind::Operation
950 ? "dialogue operation handler is not configured"
951 : "dialogue gameplay-action handler is not configured";
952 return response;
953 }
954
955 if (!request.payment.empty() && !commandParticipantFactory_ && request.stateMutations.empty()) {
956 CommandResponse response;
957 response.status = CommandResponse::Status::Failed;
958 response.error = "payment-bearing dialogue command requires an Action/operation participant";
959 return response;
960 }
961
962 std::vector<std::unique_ptr<eve::transaction::ITransactionParticipant>> owned;
963 std::vector<eve::transaction::ITransactionParticipant*> effects;
964 if (commandParticipantFactory_) {
965 auto participant = commandParticipantFactory_(request);
966 if (!participant) {
967 CommandResponse response;
968 response.status = CommandResponse::Status::Failed;
969 response.error = participant.status().describe();
970 return response;
971 }
972 auto action = std::move(participant).takeValue();
973 if (!action) {
974 CommandResponse response;
975 response.status = CommandResponse::Status::Failed;
976 response.error = "dialogue command participant factory returned null";
977 return response;
978 }
979 effects.push_back(action.get());
980 owned.push_back(std::move(action));
981 }
982 if (!request.stateMutations.empty()) {
983 if (!stateMutationProvider_) {
984 CommandResponse response;
985 response.status = CommandResponse::Status::Failed;
986 response.error = "dialogue command state mutations require a StatePatch-compatible provider";
987 return response;
988 }
989 auto state =
990 std::make_unique<DialogueStateMutationParticipant>(*stateMutationProvider_, request.stateMutations);
991 effects.push_back(state.get());
992 owned.push_back(std::move(state));
993 }
994 auto committed = paymentAdapter_.execute(nextTransactionId("command"), request.payment, effects);
995 if (!committed) {
996 CommandResponse response;
997 response.status = CommandResponse::Status::Failed;
998 response.error = committed.status().describe();
999 return response;
1000 }
1001 (void)std::move(committed).takeValue();
1002 CommandResponse response;
1003 response.status = CommandResponse::Status::Completed;
1004 return response;
1005}
1006
1007std::string DialogueFlow::getConversationId() const { return runner_.asset() ? runner_.asset()->id : std::string{}; }
1008
1009std::string DialogueFlow::getNodeKind() const {
1010 const auto* node = runner_.currentNode();
1011 return node ? node->type : std::string{};
1012}
1013
1014#define EVE_FLOW_NODE_STRING(method, field) \
1015 std::string DialogueFlow::method() const { \
1016 const auto* node = runner_.currentNode(); \
1017 return node ? sequencePayloadString(*node, field) : std::string{}; \
1018 }
1019EVE_FLOW_NODE_STRING(getSpeaker, "speaker")
1020EVE_FLOW_NODE_STRING(getPool, "pool")
1021EVE_FLOW_NODE_STRING(getI18nKey, "i18n")
1022#undef EVE_FLOW_NODE_STRING
1023
1024std::string DialogueFlow::getText() {
1025 const auto* node = runner_.currentNode();
1026 if (!node) return {};
1027 const std::string localized =
1028 localization_.resolveText(sequencePayloadString(*node, "i18n"), locale_, sequencePayloadString(*node, "text"));
1029 return textRenderer_.render(localized, runner_.bindings(), runner_.locals(), [this](const std::string& rule) {
1030 auto result = evaluate(rule, toDialogueStateValue(runner_.bindings()), toDialogueStateValue(runner_.locals()));
1031 if (!result) {
1032 failureMessage_ = result.status().describe();
1033 return false;
1034 }
1035 StateValue value = std::move(result).takeValue();
1036 return value.isBool() && value.asBool();
1037 });
1038}
1039
1040std::string DialogueFlow::getVoice() const {
1041 const auto* node = runner_.currentNode();
1042 return node ? localization_.resolveVoice(sequencePayloadString(*node, "i18n"), locale_,
1043 sequencePayloadString(*node, "voice"))
1044 : std::string{};
1045}
1046
1047std::string DialogueFlow::getVoiceStatus() const {
1048 const auto* node = runner_.currentNode();
1049 return node ? localization_.resolveStatus(sequencePayloadString(*node, "i18n"), locale_) : std::string{};
1050}
1051
1052float DialogueFlow::getVoiceDuration() const {
1053 const auto* node = runner_.currentNode();
1054 return node ? static_cast<float>(localization_.resolveDuration(sequencePayloadString(*node, "i18n"), locale_))
1055 : 0.0F;
1056}
1057
1058int DialogueFlow::getRouteCount() const {
1059 const auto* node = runner_.currentNode();
1060 return node ? static_cast<int>(node->routes.size()) : 0;
1061}
1062
1063std::string DialogueFlow::getRouteId(int index) const {
1064 const auto* node = runner_.currentNode();
1065 return node && index >= 0 && static_cast<size_t>(index) < node->routes.size()
1066 ? node->routes[static_cast<size_t>(index)].label
1067 : std::string{};
1068}
1069
1070eve::Result<void> DialogueFlow::setExpressionEvaluatorChecked(ssq::Object fn) {
1071 if (!vm_ || fn.getRaw()._type != OT_CLOSURE)
1073 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue expression evaluator must be a closure owned by the active VM", "dialogue.expressionEvaluator", {}, "dialogue.flow"));
1074 clearExpressionEvaluator();
1075 evaluator_ = fn.getRaw();
1076 sq_addref(vm_, &evaluator_);
1077 hasEvaluator_ = true;
1079}
1080
1081void DialogueFlow::clearExpressionEvaluator() {
1082 if (vm_ && hasEvaluator_) sq_release(vm_, &evaluator_);
1083 evaluator_ = {};
1084 hasEvaluator_ = false;
1085}
1086
1087eve::Result<StateValue> DialogueFlow::evaluate(const std::string& expression, const StateValue& bindings,
1088 const StateValue& locals) {
1089 if (expression == "else") return eve::Result<StateValue>::success(StateValue::boolean(true));
1090 if (!vm_ || !hasEvaluator_)
1092 eve::DiagnosticCode::Unsupported, "dialogue expression evaluator is not configured", "expression", {},
1093 "dialogue.expression"));
1094 const SQInteger top = sq_gettop(vm_);
1095 sq_pushobject(vm_, evaluator_);
1096 sq_pushroottable(vm_);
1097 sq_newtable(vm_);
1098 sq_pushstring(vm_, "expression", -1);
1099 sq_pushstring(vm_, expression.c_str(), expression.size());
1100 sq_newslot(vm_, -3, SQFalse);
1101 sq_pushstring(vm_, "bindings", -1);
1102 pushState(vm_, bindings);
1103 sq_newslot(vm_, -3, SQFalse);
1104 sq_pushstring(vm_, "locals", -1);
1105 pushState(vm_, locals);
1106 sq_newslot(vm_, -3, SQFalse);
1107 if (SQ_FAILED(sq_call(vm_, 2, SQTrue, SQTrue))) {
1108 sq_settop(vm_, top);
1110 eve::DiagnosticCode::Failed, "dialogue expression evaluator raised an exception", "expression", {},
1111 "dialogue.expression"));
1112 }
1113 StateValue result;
1114 if (!squirrelToState(vm_, -1, result)) {
1115 sq_settop(vm_, top);
1117 eve::DiagnosticCode::InvalidArgument, "dialogue expression returned an unsupported value", "expression",
1118 {}, "dialogue.expression"));
1119 }
1120 sq_settop(vm_, top);
1121 return eve::Result<StateValue>::success(std::move(result));
1122}
1123
1124std::string DialogueFlow::getRouteText(int index) const {
1125 const auto* node = runner_.currentNode();
1126 if (!node || index < 0 || static_cast<size_t>(index) >= node->routes.size()) return {};
1127 const auto& route = node->routes[static_cast<size_t>(index)];
1128 const std::string text = sequenceRoutePayloadString(route, "text");
1129 return localization_.resolveText(sequenceRoutePayloadString(route, "i18n"), locale_,
1130 text.empty() ? route.label : text);
1131}
1132
1133void DialogueFlow::setManualCommandMode(bool enabled) {
1134 manualCommandMode_ = enabled;
1135 updateRuntimeCommandDispatcher();
1136}
1137
1138eve::Result<void> DialogueFlow::restoreStateChecked(const StateValue& in) {
1139 return runner_.restoreState(toCanonicalValue(in));
1140}
1141
1142eve::Result<StateValue> DialogueFlow::captureStateChecked() const {
1143 eve::Value captured;
1144 auto result = runner_.captureState(captured);
1145 if (!result.ok()) return eve::Result<StateValue>::failure(result.status());
1147}
1148
1149eve::Result<std::string> DialogueFlow::captureStateJsonChecked() const {
1150 auto captured = captureStateChecked();
1151 if (!captured) return eve::Result<std::string>::failure(captured.status());
1152 return conversationStateToJson(std::move(captured).takeValue());
1153}
1154
1155eve::Result<void> DialogueFlow::restoreStateJsonChecked(const std::string& json) {
1156 auto parsed = conversationStateFromJson(json);
1157 if (!parsed) return eve::Result<void>::failure(parsed.status());
1158 auto migrated = migrations_.migrate(std::move(parsed).takeValue(),
1159 [this](const std::string& id) { return find(id); });
1160 if (!migrated) return eve::Result<void>::failure(migrated.status());
1161 return runner_.restoreState(toCanonicalValue(std::move(migrated).takeValue()));
1162}
1163
1164eve::Result<void> DialogueFlow::registerMigrationChecked(const std::string& assetId, int fromVersion,
1165 const std::string& currentAssetId,
1166 const std::string& nodeMap) {
1167 return migrations_.registerMigration(assetId, fromVersion, currentAssetId, nodeMap);
1168}
1169
1170void DialogueFlow::addToneRule(const std::string& expression, const std::string& prefix, const std::string& suffix,
1171 const std::string& find, const std::string& replacement) {
1172 textRenderer_.addToneRule(expression, prefix, suffix, find, replacement);
1173}
1174
1175void DialogueFlow::expose(ssq::Table& table) {
1176 if (DialogueFlow* self = DialogueFlow::create()) self->vm_ = table.getHandle();
1177 auto document = table.addClass<ConversationDocument>(
1178 "ConversationDocument",
1179 std::function<ConversationDocument*()>([]() -> ConversationDocument* { return new ConversationDocument(); }),
1180 true);
1181 document.addFunc("getId", &ConversationDocument::getId);
1182 document.addFunc("setId", &ConversationDocument::setId);
1183 document.addFunc("getVersion", &ConversationDocument::getVersion);
1184 document.addFunc("setVersion", &ConversationDocument::setVersion);
1185 document.addFunc("getEntry", &ConversationDocument::getEntry);
1186 document.addFunc("setEntry", &ConversationDocument::setEntry);
1187 document.addFunc("getParameterCount", &ConversationDocument::getParameterCount);
1188 document.addFunc("getParameter", &ConversationDocument::getParameter);
1189 document.addFunc("addParameter", &ConversationDocument::addParameter);
1190 document.addFunc("removeParameter", &ConversationDocument::removeParameter);
1191 document.addFunc("getNodeCount", &ConversationDocument::getNodeCount);
1192 document.addFunc("getNodeId", &ConversationDocument::getNodeId);
1193 document.addFunc("hasNode", &ConversationDocument::hasNode);
1194 document.addFunc("addNode", &ConversationDocument::addNode);
1195 document.addFunc("removeNode", &ConversationDocument::removeNode);
1196 document.addFunc("renameNode", &ConversationDocument::renameNode);
1197 document.addFunc("getNodeKind", &ConversationDocument::getNodeKind);
1198 document.addFunc("setNodeKind", &ConversationDocument::setNodeKind);
1199 document.addFunc("getFieldCount", &ConversationDocument::getFieldCount);
1200 document.addFunc("getFieldName", &ConversationDocument::getFieldName);
1201 document.addFunc("getFieldKind", &ConversationDocument::getFieldKind);
1202 document.addFunc("getField", &ConversationDocument::getField);
1203 document.addFunc("setField", &ConversationDocument::setField);
1204 document.addFunc("getRouteCount", &ConversationDocument::getRouteCount);
1205 document.addFunc("getRouteLabel", &ConversationDocument::getRouteLabel);
1206 document.addFunc("getRouteTarget", &ConversationDocument::getRouteTarget);
1207 document.addFunc("addRoute", &ConversationDocument::addRoute);
1208 document.addFunc("setRoute", &ConversationDocument::setRoute);
1209 document.addFunc("removeRoute", &ConversationDocument::removeRoute);
1210 document.addFunc("validate", &ConversationDocument::validate);
1211 document.addFunc("getDiagnosticCount", &ConversationDocument::getDiagnosticCount);
1212 document.addFunc("getDiagnosticSeverity", &ConversationDocument::getDiagnosticSeverity);
1213 document.addFunc("getDiagnosticPath", &ConversationDocument::getDiagnosticPath);
1214 document.addFunc("getDiagnosticLine", &ConversationDocument::getDiagnosticLine);
1215 document.addFunc("getDiagnosticMessage", &ConversationDocument::getDiagnosticMessage);
1216 auto cls = table.addClass(name, DialogueFlow::create, false);
1217 expose(cls);
1218}
1219
1220void DialogueFlow::expose(ssq::Class& cls) {
1221 cls.addFunc("getName", &DialogueFlow::getName);
1222 const auto projectCount = [](int count) { return eve::Value(count); };
1223 cls.addFunc("loadDnutChecked", [vm = cls.getHandle(), projectCount](DialogueFlow* value,
1224 const std::string& source,
1225 const std::string& sourceId) {
1226 return eve::script::projectResult(vm, value->loadDnutChecked(source, sourceId), projectCount);
1227 });
1228 cls.addFunc("reloadDnutChecked", [vm = cls.getHandle(), projectCount](DialogueFlow* value,
1229 const std::string& source,
1230 const std::string& sourceId) {
1231 return eve::script::projectResult(vm, value->reloadDnutChecked(source, sourceId), projectCount);
1232 });
1233 cls.addFunc("loadDnutFileChecked", [vm = cls.getHandle(), projectCount](DialogueFlow* value,
1234 const std::string& path) {
1235 return eve::script::projectResult(vm, value->loadDnutFileChecked(path), projectCount);
1236 });
1237 cls.addFunc("importYarnChecked", [vm = cls.getHandle(), projectCount](DialogueFlow* value,
1238 const std::string& source,
1239 const std::string& path) {
1240 return eve::script::projectResult(vm, value->importYarnChecked(source, path), projectCount);
1241 });
1242 cls.addFunc("importTweeChecked", [vm = cls.getHandle(), projectCount](DialogueFlow* value,
1243 const std::string& source,
1244 const std::string& path) {
1245 return eve::script::projectResult(vm, value->importTweeChecked(source, path), projectCount);
1246 });
1247 cls.addFunc("removeSourceChecked", [vm = cls.getHandle()](DialogueFlow* value, const std::string& path) {
1248 return eve::script::projectResult(vm, value->removeSourceChecked(path));
1249 });
1250 cls.addFunc("lintAllChecked", [vm = cls.getHandle()](DialogueFlow* value) {
1251 return eve::script::projectResult(vm, value->lintAllChecked());
1252 });
1253 cls.addFunc("renameConversationChecked", [vm = cls.getHandle()](DialogueFlow* value,
1254 const std::string& oldId,
1255 const std::string& newId) {
1256 return eve::script::projectResult(vm, value->renameConversationChecked(oldId, newId));
1257 });
1258 cls.addFunc("renameNodeChecked", [vm = cls.getHandle()](DialogueFlow* value,
1259 const std::string& conversationId,
1260 const std::string& oldId,
1261 const std::string& newId) {
1262 return eve::script::projectResult(vm, value->renameNodeChecked(conversationId, oldId, newId));
1263 });
1264 cls.addFunc("getLastLoadChanged", &DialogueFlow::getLastLoadChanged);
1265 cls.addFunc("clear", &DialogueFlow::clear);
1266 cls.addFunc("getConversationCount", &DialogueFlow::getConversationCount);
1267 cls.addFunc("getConversationId",
1268 static_cast<std::string (DialogueFlow::*)(int) const>(&DialogueFlow::getConversationId));
1269 cls.addFunc("hasConversation", &DialogueFlow::hasConversation);
1270 cls.addFunc("exportLocalizationCsv", &DialogueFlow::exportLocalizationCsv);
1271 cls.addFunc("importLocalizationCsvChecked", [vm = cls.getHandle()](DialogueFlow* value, const std::string& csv,
1272 const std::string& defaultLocale) {
1273 return eve::script::projectResult(vm, value->importLocalizationCsvChecked(csv, defaultLocale),
1274 [](int count) { return eve::Value(count); });
1275 });
1276 cls.addFunc("exportMissingLocalizationCsv", &DialogueFlow::exportMissingLocalizationCsv);
1277 cls.addFunc("exportVoiceRecordingCsv", &DialogueFlow::exportVoiceRecordingCsv);
1278 cls.addFunc("validateLocalization", [vm = cls.getHandle()](DialogueFlow* value, eve::i18n::I18n* localization,
1279 const std::string& locale) {
1280 if (!value || !localization)
1282 vm,
1284 eve::DiagnosticCode::InvalidArgument, "validateLocalization requires dialogue and i18n modules",
1285 "dialogue.localization", {}, "dialogue.squirrel")),
1286 [](int count) { return eve::Value(count); });
1287 return eve::script::projectResult(vm, value->validateLocalization(*localization, locale),
1288 [](int count) { return eve::Value(count); });
1289 });
1290 cls.addFunc("setLocale", &DialogueFlow::setLocale);
1291 cls.addFunc("getLocale", &DialogueFlow::getLocale);
1292 cls.addFunc("getDiagnosticCount", &DialogueFlow::getDiagnosticCount);
1293 cls.addFunc("getDiagnosticSeverity", &DialogueFlow::getDiagnosticSeverity);
1294 cls.addFunc("getDiagnosticPath", &DialogueFlow::getDiagnosticPath);
1295 cls.addFunc("getDiagnosticLine", &DialogueFlow::getDiagnosticLine);
1296 cls.addFunc("getDiagnosticColumn", &DialogueFlow::getDiagnosticColumn);
1297 cls.addFunc("getDiagnosticCode", &DialogueFlow::getDiagnosticCode);
1298 cls.addFunc("getDiagnosticAssetPath", &DialogueFlow::getDiagnosticAssetPath);
1299 cls.addFunc("getDiagnosticMessage", &DialogueFlow::getDiagnosticMessage);
1300 cls.addFunc("newDocument", &DialogueFlow::newDocument);
1301 cls.addFunc("getDocument", &DialogueFlow::getDocument);
1302 cls.addFunc("applyDocumentChecked", [vm = cls.getHandle()](DialogueFlow* value,
1303 ConversationDocument* document) {
1304 return eve::script::projectResult(vm, value->applyDocumentChecked(document));
1305 });
1306 cls.addFunc(
1307 "startChecked", [vm = cls.getHandle()](DialogueFlow* value, const std::string& id, ssq::Object bindings) {
1308 if (!value)
1310 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue flow must not be null", "dialogue", {}, "dialogue.flow")));
1311 return eve::script::projectResult(vm, value->startChecked(id, std::move(bindings)));
1312 });
1313 cls.addFunc("advanceChecked", [vm = cls.getHandle()](DialogueFlow* value) {
1314 if (!value)
1316 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue flow must not be null", "dialogue", {}, "dialogue.flow")));
1317 return eve::script::projectResult(vm, value->advanceChecked());
1318 });
1319 cls.addFunc("resumeCommandChecked", [vm = cls.getHandle()](DialogueFlow* value,
1320 const std::string& requestId,
1321 ssq::Object result) {
1322 if (!value)
1324 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue flow must not be null", "dialogue", {}, "dialogue.flow")));
1325 auto converted = eve::script::valueFromSquirrel(result, {.source = "dialogue.resumeCommand"});
1326 if (!converted)
1327 return eve::script::projectResult(vm, eve::Result<void>::failure(converted.status()));
1329 value->resumeCommandChecked(requestId, std::move(converted).takeValue()));
1330 });
1331 cls.addFunc("select", [vm = cls.getHandle()](DialogueFlow* value, const std::string& routeId) {
1332 if (!value)
1334 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "dialogue flow must not be null", "dialogue", {}, "dialogue.flow")));
1335 return eve::script::projectResult(vm, value->select(routeId));
1336 });
1337 cls.addFunc("isActive", &DialogueFlow::isActive);
1338 cls.addFunc("isBlocked", &DialogueFlow::isBlocked);
1339 cls.addFunc("getActiveConversationId",
1340 static_cast<std::string (DialogueFlow::*)() const>(&DialogueFlow::getConversationId));
1341 cls.addFunc("getNodeId", &DialogueFlow::getNodeId);
1342 cls.addFunc("getPendingCommandRequestId", &DialogueFlow::getPendingCommandRequestId);
1343 cls.addFunc("getNodeKind", &DialogueFlow::getNodeKind);
1344 cls.addFunc("getSpeaker", &DialogueFlow::getSpeaker);
1345 cls.addFunc("getText", &DialogueFlow::getText);
1346 cls.addFunc("getPool", &DialogueFlow::getPool);
1347 cls.addFunc("getI18nKey", &DialogueFlow::getI18nKey);
1348 cls.addFunc("getVoice", &DialogueFlow::getVoice);
1349 cls.addFunc("getVoiceStatus", &DialogueFlow::getVoiceStatus);
1350 cls.addFunc("getVoiceDuration", &DialogueFlow::getVoiceDuration);
1351 cls.addFunc("getRouteCount", &DialogueFlow::getRouteCount);
1352 cls.addFunc("getRouteId", &DialogueFlow::getRouteId);
1353 cls.addFunc("getRouteText", &DialogueFlow::getRouteText);
1354 cls.addFunc("setManualCommandMode", &DialogueFlow::setManualCommandMode);
1355 cls.addFunc("setExpressionEvaluatorChecked", [vm = cls.getHandle()](DialogueFlow* value, ssq::Object fn) {
1356 return eve::script::projectResult(vm, value->setExpressionEvaluatorChecked(fn));
1357 });
1358 cls.addFunc("clearExpressionEvaluator", &DialogueFlow::clearExpressionEvaluator);
1359 cls.addFunc("captureStateJsonChecked", [vm = cls.getHandle()](DialogueFlow* value) {
1360 return eve::script::projectResult(vm, value->captureStateJsonChecked(),
1361 [](std::string json) { return eve::Value(std::move(json)); });
1362 });
1363 cls.addFunc("restoreStateJsonChecked", [vm = cls.getHandle()](DialogueFlow* value, const std::string& json) {
1364 return eve::script::projectResult(vm, value->restoreStateJsonChecked(json));
1365 });
1366 cls.addFunc("registerMigrationChecked", [vm = cls.getHandle()](DialogueFlow* value,
1367 const std::string& assetId, int fromVersion,
1368 const std::string& currentAssetId,
1369 const std::string& nodeMap) {
1371 vm, value->registerMigrationChecked(assetId, fromVersion, currentAssetId, nodeMap));
1372 });
1373 cls.addFunc("clearMigrations", &DialogueFlow::clearMigrations);
1374 cls.addFunc("addToneRule", &DialogueFlow::addToneRule);
1375 cls.addFunc("clearToneRules", &DialogueFlow::clearToneRules);
1376
1377 // 把对话运行器发布到共享玩法协议(`eve_gameplay` / MCP)。返回 {ok, message}:
1378 // 失败原因(非规范持久 id、运行器已发布)不被丢弃。
1379 cls.addFunc("publishGameplay",
1380 [vm = cls.getHandle()](DialogueFlow* self, const std::string& instanceId, const std::string& ownerId) {
1381 ssq::Table result(vm);
1382 if (self == nullptr) {
1383 result.set("ok", false);
1384 result.set("message", std::string("dialogue flow unavailable"));
1385 return result;
1386 }
1387 const auto published = self->publishGameplay(instanceId, ownerId);
1388 result.set("ok", published.ok());
1389 result.set("message", published.ok() ? std::string("published") : published.status().describe());
1390 return result;
1391 });
1392 cls.addFunc("unpublishGameplay", [vm = cls.getHandle()](DialogueFlow* self, const std::string& instanceId) {
1393 ssq::Table result(vm);
1394 const auto unpublished = self == nullptr
1396 eve::DiagnosticCode::Failed, "dialogue flow unavailable", "self"))
1397 : self->unpublishGameplay(instanceId);
1398 result.set("ok", unpublished.ok());
1399 result.set("message", unpublished.ok() ? std::string("unpublished") : unpublished.status().describe());
1400 return result;
1401 });
1402 cls.addFunc("clearGameplayControls", &DialogueFlow::clearGameplayControls);
1403 cls.addFunc("getGameplayControlCount", &DialogueFlow::gameplayControlCount);
1404}
1405
1406} // namespace eve::dialogue
double value
SQInteger top
int root
Definition AnimSmr.cpp:119
ActiveSource owned
struct SQVM * HSQUIRRELVM
#define EVE_FLOW_NODE_STRING(method, field)
Dialogue payload codecs for L1 sequence assets.
std::string label
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::uint64_t sourceId
std::string message
const GltfImportRequest & request
std::uint32_t key
wgpu::PopErrorScopeStatus status
glm::uvec4 ids
std::int32_t second
std::string text
std::string imported
std::uint64_t bytes
std::string name
bool valid
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
graphics::Canvas * previous
std::vector< std::shared_ptr< DeviceBytes > > bindings
Definition OnnxGpgpu.cpp:38
std::string error
Definition Package.cpp:60
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
const RoadNode * node
std::uint32_t count
The single Squirrel projection for common Result, Status and Value.
Stable reference to a runtime gameplay subject.
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
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
void expect(std::string_view message) const
Require success for a void operation.
Definition Result.h:548
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:487
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
JSON-compatible state value tree used by state hot reload.
Definition StateValue.h:20
static StateValue object()
Empty object.
Definition StateValue.h:40
static StateValue boolean(bool v)
Boolean value.
static StateValue integer(int64_t v)
Integer value.
bool isObject() const
Definition StateValue.h:50
static StateValue number(double v)
Floating-point value.
static StateValue null()
Null value.
Definition StateValue.h:28
static StateValue string(std::string v)
String value.
std::string describe() const
Render a compact human-readable summary.
Definition Status.h:143
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static SubjectRef fromPersistentId(PersistentId id) noexcept
Wrap a persistent identity without changing its bytes.
Definition SubjectRef.h:32
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
bool isString() const noexcept
Return true when this value is a string.
Definition Value.h:95
static Value object(Object value)
Compatibility factory for an object value.
Definition Value.h:114
bool isObject() const noexcept
Return true when this value is an object.
Definition Value.h:99
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
Explain one condition evaluation in a UI- and log-friendly form.
Definition Condition.h:129
bool passed() const noexcept
Whether the condition passed.
Definition Condition.h:137
ConditionReasonCode reasonCode() const noexcept
Stable machine-readable reason code.
Definition Condition.h:139
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
Definition Identity.h:151
Mutable, UI-neutral conversation document for custom editor composition.
const eve::dnut::SequenceAsset & asset() const
Return the editable runtime asset.
Script-facing registry and runner for compiled .dnut conversations.
eve::Result< void > startChecked(const std::string &id, ssq::Object bindings)
Start a conversation after validating script bindings.
int gameplayControlCount() const
已发布的玩法实例数量(0 或 1)。
void clearIntegration()
Remove explicit providers and callbacks from the facade.
std::vector< std::string > gameplayInstances() const
已发布的玩法实例标识(发布顺序)。
void configureIntegration(IntegrationConfig config)
Configure every runner in this module facade in one operation.
eve::Result< void > publishGameplay(const std::string &instanceId, const std::string &ownerId)
把本模块的对话运行器发布到共享玩法协议(eve_gameplay / MCP)。
void clearGameplayControls()
取消发布本模块持有的全部玩法实例。
eve::Result< void > unpublishGameplay(const std::string &instanceId)
取消发布一个实例;该实例未发布(或 id 非法)时返回诊断。
void setBindings(DialogueAccountBindings bindings)
Replace borrowed account bindings; no balance is copied.
void setMutationProvider(eve::IStateMutation *provider) noexcept
Bind a borrowed mutation provider for this context's lifetime.
eve::decision::ConditionResult evaluate(const eve::decision::Condition &condition) const
Evaluate an already compiled shared condition with explanation.
void setQueryProvider(eve::IStateQuery *provider) noexcept
Bind a borrowed query provider for this context's lifetime.
void setSubject(std::string subject)
Set the subject used for all future condition queries.
Visual-novel style dialogue stage. Script: dlg <- eve.Dialogue();
Definition Dialogue.h:46
std::string getLastPoolsError() const
Returns the last pools error.
Definition Dialogue.h:211
int replacePoolsFromData(const DataValue &root)
Atomically replace all pools from a validated unified dnut workspace.
Definition Dialogue.cpp:582
Caller-owned interpreter over a compiled sequence asset.
void setCommandDispatcher(SequenceCommandHandler dispatcher)
Install the catch-all command dispatcher used after named lookup.
const SequenceNode * currentNode() const
Borrowed current node, or nullptr. @lifetime Invalidated by the next transition.
bool isActive() const
Whether a sequence is currently loaded.
void setConditionEvaluator(ConditionEvaluator evaluator)
void stop()
Stop and clear the active sequence without emitting Ended.
void clearCommandDispatcher()
Remove the catch-all command dispatcher.
eve::Result< void > captureState(eve::Value &out) const
Capture the complete cursor, bindings, locals and call stack.
eve::Result< void > advance()
Acknowledge a host-presented wait or handler-less Await step.
eve::Result< void > resumeCommand(std::string_view requestId, eve::StateValue result)
Resume the exact blocked command with a legacy state value.
void setStepRegistry(const StepKindRegistry *registry)
Bind the step vocabulary; passing nullptr disables non-core steps.
eve::Result< void > start(const SequenceAsset *asset, eve::Value bindings=eve::Value::Object{})
Begin a sequence at the asset entry node.
const eve::Value & locals() const
const SequenceAsset * asset() const
Borrowed active asset, or nullptr. @lifetime Owned by the caller of start.
const eve::Value & bindings() const
eve::Result< void > selectRouteForTransaction(std::string_view routeLabel)
Enter one already prepared choice route transactionally.
eve::Result< void > restoreState(const eve::Value &in)
Restore a captured cursor through the configured asset resolver.
void setAssetResolver(AssetResolver resolver)
Game localization (i18n) module.
Definition I18n.h:38
bool hasLanguage(const std::string &lang) const
True when language.
Definition I18n.cpp:482
bool hasInLanguage(const std::string &lang, const std::string &key) const
Return whether one exact locale owns key, without default-language substitution.
Definition I18n.cpp:484
Synchronous participant contract for one coordinator transaction.
Immutable context shared by every participant of one transaction.
Definition Transaction.h:30
const char * conditionReasonCodeName(ConditionReasonCode code) noexcept
Return the stable lowercase spelling of a condition reason code. @ownership Borrowed process-static t...
Definition Condition.cpp:82
eve::Result< std::vector< eve::StateMutation > > decodeSequenceStateMutations(const eve::Value &payload)
Decode dialogue state mutations from one canonical payload object.
eve::Result< void > renameConversationNode(std::vector< eve::dnut::SequenceAsset > &assets, const std::string &assetId, const std::string &oldId, const std::string &newId)
Rename one stable node and rewrite all references inside its asset.
eve::Value toCanonicalValue(const eve::StateValue &value)
Convert the legacy reload value into the canonical common value.
std::string sequenceRoutePayloadString(const eve::dnut::SequenceRoute &route, std::string_view key)
Read an optional string from a route payload; missing or ill-typed values yield empty.
eve::Result< void > lintConversationWorkspace(const std::vector< eve::dnut::SequenceAsset > &assets, const std::string &label, std::vector< ConversationDiagnostic > &diagnostics)
Lint a complete multi-file registry, including cross-asset call targets.
eve::StateValue toDialogueStateValue(const eve::Value &value)
Convert a canonical value to the legacy reload representation.
eve::Result< DnutDocument > compileDnutDocument(const std::string &source, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Compile and lint the complete pools-plus-conversations dnut document.
std::string sequencePayloadString(const eve::dnut::SequenceNode &node, std::string_view key)
Read an optional string from a node payload; missing or ill-typed values yield empty.
std::string exportConversationLocalizationCsv(const std::vector< eve::dnut::SequenceAsset > &assets)
Export stable line IDs and localization keys as RFC4180 CSV.
eve::Result< PaymentSpec > decodeSequencePayment(const eve::Value &payload)
Decode an optional dialogue payment from one canonical payload object.
eve::Result< std::string > conversationStateToJson(const StateValue &state)
Serialize a dialogue state tree as JSON.
eve::Result< StateValue > conversationStateFromJson(const std::string &json)
Parse a JSON dialogue state tree.
eve::Result< std::vector< eve::dnut::SequenceAsset > > importYarnConversation(const std::string &source, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Import Yarn Spinner source through the canonical validated conversation schema.
eve::Value DataValue
Canonical data value used by dialogue pools, conditions and scripts.
Definition Dialogue.h:36
eve::Result< std::vector< eve::dnut::SequenceAsset > > importTweeConversation(const std::string &source, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Import Twee 3 passage source through the canonical validated conversation schema.
eve::Result< void > renameConversationAsset(std::vector< eve::dnut::SequenceAsset > &assets, const std::string &oldId, const std::string &newId)
Rename an asset and rewrite every cross-asset call target.
eve::Result< void > registerDialogueSequenceSteps(eve::dnut::StepKindRegistry &registry)
Register dialogue's line, choice, and command contracts and validators.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
Result< Value > valueFromSquirrel(HSQUIRRELVM vm, SQInteger index, const SquirrelValueOptions &options)
Convert one Squirrel value into the canonical owning Value tree.
SettlementPipeline::Stage fn
bool enabled
Borrowed world and cross-domain callbacks used by all conversations in this facade.
std::string subject
Stable world subject used by state-backed conditions.
std::function< eve::Result< std::string >(const std::string &)> contentReader
Injected UTF-8 content reader used by loadDnutFileChecked.
CommandRequestHandler operationHandler
Handler for requests whose kind is Operation.
CommandParticipantFactory commandParticipantFactory
Optional factory used to stage an atomic operation or Action.
std::function< eve::decision::ConditionResult(const eve::Value &)> conditionEvaluator
Optional custom condition evaluator; empty uses the shared adapter.
eve::IStateQuery * stateQuery
Borrowed read-only world state provider.
DialogueAccountBindings accounts
Borrowed authoritative money/reputation account adapters.
CommandRequestHandler gameplayActionHandler
Handler for requests whose kind is GameplayAction.
eve::IStateMutation * stateMutation
Borrowed all-or-nothing world mutation provider.
Canonical money/reputation cost attached to a dialogue choice or command.
bool empty() const noexcept
Whether this specification contains no charge.
Immutable, parameterized and versioned compiled sequence.
Immutable request emitted by a generic command node.
Structured response returned by a generic command handler.
Outcome of one authored condition, as seen by the language core.
std::vector< SequenceRoute > routes
One outgoing edge of a sequence node.
Correlation metadata shared by a mutation transaction.
Definition StateAccess.h:57
bool started
Definition Graphics.cpp:183