载入中...
搜索中...
未找到
ConversationCompiler.cpp
浏览该文件的文档.
2
5
6#include <algorithm>
7#include <deque>
8#include <string>
9#include <unordered_set>
10#include <utility>
11
12namespace eve::dialogue {
13namespace {
14
15std::string csv(std::string value) {
16 std::size_t position = 0;
17 while ((position = value.find('"', position)) != std::string::npos) {
18 value.insert(position, 1, '"');
19 position += 2;
20 }
21 return '"' + value + '"';
22}
23
24} // namespace
25
26eve::Result<DnutDocument> compileDnutDocument(const std::string& source, const std::string& path,
27 std::vector<ConversationDiagnostic>& diagnostics) {
28 auto parsed = parseDnutDocument(source, path, diagnostics);
29 if (!parsed.ok()) return eve::Result<DnutDocument>::failure(parsed.status());
30 DnutDocument document = std::move(parsed).takeValue();
31 auto linted = lintConversations(document.conversations, path, diagnostics);
32 if (!linted.ok()) return eve::Result<DnutDocument>::failure(linted.status());
33 return eve::Result<DnutDocument>::success(std::move(document));
34}
35
36eve::Result<void> lintConversations(const std::vector<eve::dnut::SequenceAsset>& assets, const std::string& path,
37 std::vector<ConversationDiagnostic>& diagnostics) {
38 bool valid = true;
39 std::unordered_set<std::string> assetIds;
41 auto registered = registerDialogueSequenceSteps(registry);
42 if (!registered.ok()) return registered;
43
44 for (const auto& asset : assets) {
45 if (!assetIds.insert(asset.id).second) {
46 diagnostics.push_back({ConversationDiagnostic::Severity::Error, path, asset.sourceLine,
47 "duplicate conversation id '" + asset.id + "'", "DuplicateAssetId",
48 asset.sourceColumn, asset.id});
49 valid = false;
50 }
51 auto validated = validateDialogueSequenceAsset(asset, registry);
52 if (!validated.ok()) {
53 diagnostics.push_back({ConversationDiagnostic::Severity::Error, path, asset.sourceLine,
54 validated.status().describe(), "InvalidConversation", asset.sourceColumn, asset.id});
55 valid = false;
56 continue;
57 }
58
59 std::unordered_set<std::string> reached;
60 std::deque<std::string> pending{asset.entry};
61 while (!pending.empty()) {
62 const std::string id = pending.front();
63 pending.pop_front();
64 if (!reached.insert(id).second) continue;
65 const auto* node = asset.findNode(id);
66 if (!node) continue;
67 if (!node->next.empty()) pending.push_back(node->next);
68 if (node->type == "call") {
69 const std::string returnNode = sequencePayloadString(*node, "return");
70 if (!returnNode.empty()) pending.push_back(returnNode);
71 }
72 for (const auto& route : node->routes) pending.push_back(route.target);
73 }
74 for (const auto& node : asset.nodes)
75 if (!reached.contains(node.id))
76 diagnostics.push_back({ConversationDiagnostic::Severity::Warning, path, node.sourceLine,
77 "conversation '" + asset.id + "': unreachable node '" + node.id + "'",
78 "UnreachableNode", node.sourceColumn, asset.id + "/" + node.id});
79
80 std::unordered_set<std::string> canExit;
81 for (const auto& node : asset.nodes)
82 if (node.type == "end") canExit.insert(node.id);
83 bool changed = true;
84 while (changed) {
85 changed = false;
86 for (const auto& node : asset.nodes) {
87 if (canExit.contains(node.id)) continue;
88 bool exits = !node.next.empty() && canExit.contains(node.next);
89 if (node.type == "call") {
90 const std::string returnNode = sequencePayloadString(node, "return");
91 exits = exits || (!returnNode.empty() && canExit.contains(returnNode));
92 }
93 for (const auto& route : node.routes) exits = exits || canExit.contains(route.target);
94 if (exits) changed = canExit.insert(node.id).second;
95 }
96 }
97 bool allReachableHaveOutgoing = true;
98 for (const auto& node : asset.nodes) {
99 if (!reached.contains(node.id) || canExit.contains(node.id)) continue;
100 const bool hasReturn = node.type == "call" && !sequencePayloadString(node, "return").empty();
101 if (node.next.empty() && !hasReturn && node.routes.empty()) {
102 allReachableHaveOutgoing = false;
103 break;
104 }
105 }
106 if (!canExit.contains(asset.entry) && allReachableHaveOutgoing) {
107 diagnostics.push_back({ConversationDiagnostic::Severity::Error, path, asset.sourceLine,
108 "conversation '" + asset.id + "' contains a reachable loop with no exit",
109 "NoExitLoop", asset.sourceColumn, asset.id});
110 valid = false;
111 }
112 }
113 valid = valid && std::none_of(diagnostics.begin(), diagnostics.end(), [](const auto& item) {
114 return item.severity == ConversationDiagnostic::Severity::Error;
115 });
116 if (!valid) {
117 const std::string message = diagnostics.empty() ? "conversation lint failed" : diagnostics.front().message;
119 eve::DiagnosticCode::InvalidArgument, message, path, {}, "dialogue.lint"));
120 }
122}
123
124std::string exportConversationLocalizationCsv(const std::vector<eve::dnut::SequenceAsset>& assets) {
125 std::string output = "conversation_id,node_id,i18n_key,speaker,source_text,voice\r\n";
126 for (const auto& asset : assets)
127 for (const auto& node : asset.nodes) {
128 if (node.type != "line") continue;
129 output += csv(asset.id) + ',' + csv(node.id) + ',' + csv(sequencePayloadString(node, "i18n")) + ',' +
130 csv(sequencePayloadString(node, "speaker")) + ',' + csv(sequencePayloadString(node, "text")) +
131 ',' + csv(sequencePayloadString(node, "voice")) + "\r\n";
132 }
133 return output;
134}
135
136} // namespace eve::dialogue
double value
std::string output
std::vector< QuestEvent > pending
Dialogue payload codecs for L1 sequence assets.
std::string message
std::array< float, 3 > position
bool valid
std::string path
Definition PlayHost.cpp:110
std::set< Cell > exits
const RoadNode * node
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Canonical step vocabulary owner and runtime handler router.
eve::Result< DnutDocument > parseDnutDocument(const std::string &source, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Parse pools and conversations from one versioned dnut token stream.
eve::Result< void > lintConversations(const std::vector< eve::dnut::SequenceAsset > &assets, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Validate references and report unreachable nodes.
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< void > validateDialogueSequenceAsset(const eve::dnut::SequenceAsset &asset, const eve::dnut::StepKindRegistry &registry)
Validate every dialogue-owned payload without taking control-flow ownership.
eve::Result< void > registerDialogueSequenceSteps(eve::dnut::StepKindRegistry &registry)
Register dialogue's line, choice, and command contracts and validators.
UI-neutral syntax tree produced by the single dnut parser.
std::vector< eve::dnut::SequenceAsset > conversations