载入中...
搜索中...
未找到
ConversationAuthoring.cpp
浏览该文件的文档.
2
5
6#include <algorithm>
7#include <array>
8#include <string>
9#include <utility>
10#include <vector>
11
12namespace eve::dialogue {
13namespace {
14
15using Field = std::pair<const char*, const char*>;
16
17bool isKnownType(const std::string& type) {
18 static constexpr std::array<const char*, 7> types{"line", "branch", "choice", "call", "command", "wait", "end"};
19 return std::find(types.begin(), types.end(), type) != types.end();
20}
21
22const std::vector<Field>& fieldsFor(const std::string& type) {
23 // Authoring / editor field names stay stable; setField maps them onto the
24 // SequenceAsset payload keys used by L1 dialogue vocabulary.
25 static const std::vector<Field> line{{"next", "node"}, {"speaker", "string"}, {"text", "multiline"},
26 {"pool", "asset"}, {"i18nKey", "string"}, {"voice", "asset"},
27 {"expression", "string"}};
28 static const std::vector<Field> branch;
29 static const std::vector<Field> choice;
30 static const std::vector<Field> call{
31 {"target", "asset"}, {"returnNode", "node"}, {"next", "node"}, {"arguments", "json"}};
32 static const std::vector<Field> command{
33 {"target", "string"}, {"expression", "string"}, {"next", "node"}, {"arguments", "json"}};
34 static const std::vector<Field> wait{{"next", "node"}};
35 static const std::vector<Field> end;
36 if (type == "line") return line;
37 if (type == "branch") return branch;
38 if (type == "choice") return choice;
39 if (type == "call") return call;
40 if (type == "command") return command;
41 if (type == "wait") return wait;
42 return end;
43}
44
46std::string payloadKeyForField(const std::string& field) {
47 if (field == "i18nKey") return "i18n";
48 if (field == "returnNode") return "return";
49 return field;
50}
51
52} // namespace
53
55 asset_.id = std::move(id);
56 asset_.entry = "end";
58 end.id = "end";
59 end.type = "end";
60 asset_.nodes.push_back(std::move(end));
61}
62
64
65bool ConversationDocument::fail(const std::string& message) {
66 failureMessage_ = message;
67 return false;
68}
69
70eve::dnut::SequenceNode* ConversationDocument::findNode(const std::string& nodeId) {
71 for (auto& node : asset_.nodes)
72 if (node.id == nodeId) return &node;
73 return nullptr;
74}
75
76const eve::dnut::SequenceNode* ConversationDocument::findNode(const std::string& nodeId) const {
77 return asset_.findNode(nodeId);
78}
79
80bool ConversationDocument::setId(const std::string& id) {
81 if (id.empty()) return fail("conversation ID must not be empty");
82 asset_.id = id;
83 failureMessage_.clear();
84 return true;
85}
86
88 if (version <= 0) return fail("conversation version must be positive");
89 asset_.version = version;
90 failureMessage_.clear();
91 return true;
92}
93
94bool ConversationDocument::setEntry(const std::string& nodeId) {
95 if (!findNode(nodeId)) return fail("entry node not found: " + nodeId);
96 asset_.entry = nodeId;
97 failureMessage_.clear();
98 return true;
99}
100
101int ConversationDocument::getParameterCount() const { return static_cast<int>(asset_.parameters.size()); }
102
104 return index >= 0 && index < getParameterCount() ? asset_.parameters[static_cast<std::size_t>(index)].name
105 : std::string{};
106}
107
108bool ConversationDocument::addParameter(const std::string& name) {
109 if (name.empty()) return fail("parameter name must not be empty");
110 if (std::any_of(asset_.parameters.begin(), asset_.parameters.end(),
111 [&](const auto& parameter) { return parameter.name == name; }))
112 return fail("parameter already exists: " + name);
113 asset_.parameters.emplace_back(name);
114 failureMessage_.clear();
115 return true;
116}
117
119 const auto found = std::find_if(asset_.parameters.begin(), asset_.parameters.end(),
120 [&](const auto& parameter) { return parameter.name == name; });
121 if (found == asset_.parameters.end()) return false;
122 asset_.parameters.erase(found);
123 return true;
124}
125
126int ConversationDocument::getNodeCount() const { return static_cast<int>(asset_.nodes.size()); }
127
129 return index >= 0 && index < getNodeCount() ? asset_.nodes[static_cast<std::size_t>(index)].id : std::string{};
130}
131
132bool ConversationDocument::hasNode(const std::string& nodeId) const { return findNode(nodeId) != nullptr; }
133
134bool ConversationDocument::addNode(const std::string& nodeId, const std::string& kind) {
135 if (nodeId.empty()) return fail("node ID must not be empty");
136 if (findNode(nodeId)) return fail("node already exists: " + nodeId);
137 if (!isKnownType(kind)) return fail("unknown node kind: " + kind);
139 node.id = nodeId;
140 node.type = kind;
141 asset_.nodes.push_back(std::move(node));
142 failureMessage_.clear();
143 return true;
144}
145
146bool ConversationDocument::removeNode(const std::string& nodeId) {
147 const auto found =
148 std::find_if(asset_.nodes.begin(), asset_.nodes.end(), [&](const auto& node) { return node.id == nodeId; });
149 if (found == asset_.nodes.end()) return false;
150 asset_.nodes.erase(found);
151 if (asset_.entry == nodeId) asset_.entry.clear();
152 for (auto& node : asset_.nodes) {
153 if (node.next == nodeId) node.next.clear();
154 if (node.type == "call" && sequencePayloadString(node, "return") == nodeId)
155 node.payload.set("return", eve::Value::string(""));
156 node.routes.erase(std::remove_if(node.routes.begin(), node.routes.end(),
157 [&](const auto& route) { return route.target == nodeId; }),
158 node.routes.end());
159 }
160 return true;
161}
162
163bool ConversationDocument::renameNode(const std::string& oldId, const std::string& newId) {
164 std::vector<eve::dnut::SequenceAsset> assets{asset_};
165 auto renamed = renameConversationNode(assets, asset_.id, oldId, newId);
166 if (!renamed.ok()) {
167 failureMessage_ = renamed.status().describe();
168 return false;
169 }
170 asset_ = std::move(assets.front());
171 return true;
172}
173
174std::string ConversationDocument::getNodeKind(const std::string& nodeId) const {
175 const auto* node = findNode(nodeId);
176 return node ? node->type : std::string{};
177}
178
179bool ConversationDocument::setNodeKind(const std::string& nodeId, const std::string& kind) {
180 auto* node = findNode(nodeId);
181 if (!node) return fail("node not found: " + nodeId);
182 if (!isKnownType(kind)) return fail("unknown node kind: " + kind);
183 node->type = kind;
184 failureMessage_.clear();
185 return true;
186}
187
188int ConversationDocument::getFieldCount(const std::string& nodeId) const {
189 const auto* node = findNode(nodeId);
190 return node ? static_cast<int>(fieldsFor(node->type).size()) : 0;
191}
192
193std::string ConversationDocument::getFieldName(const std::string& nodeId, int index) const {
194 const auto* node = findNode(nodeId);
195 if (!node) return {};
196 const auto& fields = fieldsFor(node->type);
197 return index >= 0 && index < static_cast<int>(fields.size()) ? fields[static_cast<std::size_t>(index)].first : "";
198}
199
200std::string ConversationDocument::getFieldKind(const std::string& nodeId, int index) const {
201 const auto* node = findNode(nodeId);
202 if (!node) return {};
203 const auto& fields = fieldsFor(node->type);
204 return index >= 0 && index < static_cast<int>(fields.size()) ? fields[static_cast<std::size_t>(index)].second : "";
205}
206
207std::string ConversationDocument::getField(const std::string& nodeId, const std::string& field) const {
208 const auto* node = findNode(nodeId);
209 if (!node) return {};
210 if (field == "next") return node->next;
211 const std::string key = payloadKeyForField(field);
212 const eve::Value* value = node->payload.find(key);
213 if (!value) return {};
214 if (field == "arguments") {
215 auto json = value->toJson();
216 return json.ok() ? std::move(json).takeValue() : std::string{};
217 }
218 return value->isString() ? value->asString() : std::string{};
219}
220
221bool ConversationDocument::setField(const std::string& nodeId, const std::string& field, const std::string& value) {
222 auto* node = findNode(nodeId);
223 if (!node) return fail("node not found: " + nodeId);
224 if (field == "next") {
225 node->next = value;
226 } else if (field == "arguments") {
227 auto parsed = eve::Value::fromJson(value);
228 if (!parsed.ok()) {
229 failureMessage_ = parsed.status().describe();
230 return false;
231 }
232 eve::Value arguments = std::move(parsed).takeValue();
233 if (!arguments.isObject()) return fail("arguments must be a JSON object");
234 node->payload.set("arguments", std::move(arguments));
235 } else {
236 bool known = false;
237 for (const auto& [name, kind] : fieldsFor(node->type)) {
238 (void)kind;
239 if (field == name) {
240 known = true;
241 break;
242 }
243 }
244 if (!known) return fail("unknown node field: " + field);
245 node->payload.set(payloadKeyForField(field), eve::Value::string(value));
246 }
247 failureMessage_.clear();
248 return true;
249}
250
251int ConversationDocument::getRouteCount(const std::string& nodeId) const {
252 const auto* node = findNode(nodeId);
253 return node ? static_cast<int>(node->routes.size()) : 0;
254}
255
256std::string ConversationDocument::getRouteLabel(const std::string& nodeId, int index) const {
257 const auto* node = findNode(nodeId);
258 return node && index >= 0 && index < static_cast<int>(node->routes.size())
259 ? node->routes[static_cast<std::size_t>(index)].label
260 : std::string{};
261}
262
263std::string ConversationDocument::getRouteTarget(const std::string& nodeId, int index) const {
264 const auto* node = findNode(nodeId);
265 return node && index >= 0 && index < static_cast<int>(node->routes.size())
266 ? node->routes[static_cast<std::size_t>(index)].target
267 : std::string{};
268}
269
270bool ConversationDocument::addRoute(const std::string& nodeId, const std::string& label, const std::string& target) {
271 auto* node = findNode(nodeId);
272 if (!node) return fail("node not found: " + nodeId);
273 if (node->type != "branch" && node->type != "choice") return fail("routes require a branch or choice node");
275 route.label = label;
276 route.target = target;
277 node->routes.push_back(std::move(route));
278 failureMessage_.clear();
279 return true;
280}
281
282bool ConversationDocument::setRoute(const std::string& nodeId, int index, const std::string& label,
283 const std::string& target) {
284 auto* node = findNode(nodeId);
285 if (!node || index < 0 || index >= static_cast<int>(node->routes.size())) return false;
286 auto& route = node->routes[static_cast<std::size_t>(index)];
287 route.label = label;
288 route.target = target;
289 return true;
290}
291
292bool ConversationDocument::removeRoute(const std::string& nodeId, int index) {
293 auto* node = findNode(nodeId);
294 if (!node || index < 0 || index >= static_cast<int>(node->routes.size())) return false;
295 node->routes.erase(node->routes.begin() + index);
296 return true;
297}
298
300 diagnostics_.clear();
301 const bool valid = lintConversations({asset_}, asset_.id, diagnostics_).ok();
302 failureMessage_ = valid || diagnostics_.empty() ? std::string{} : diagnostics_.front().message;
303 return valid;
304}
305
306int ConversationDocument::getDiagnosticCount() const { return static_cast<int>(diagnostics_.size()); }
307
309 if (index < 0 || index >= getDiagnosticCount()) return {};
310 return diagnostics_[static_cast<std::size_t>(index)].severity == ConversationDiagnostic::Severity::Error
311 ? "error"
312 : "warning";
313}
314
316 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<std::size_t>(index)].path
317 : std::string{};
318}
319
321 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<std::size_t>(index)].line : 0;
322}
323
325 return index >= 0 && index < getDiagnosticCount() ? diagnostics_[static_cast<std::size_t>(index)].message
326 : std::string{};
327}
328
329} // namespace eve::dialogue
LogicalId target
double value
Dialogue payload codecs for L1 sequence assets.
std::string label
std::string nodeId
std::string message
std::uint32_t key
std::int32_t second
std::int32_t first
TokenKind kind
std::string name
bool valid
std::vector< BvhNode > nodes
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::string id
Definition PlayHost.cpp:108
const RoadNode * node
bool found
uint32_t index
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
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
static Value string(std::string value)
Compatibility factory for a string value.
Definition Value.h:110
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
bool validate()
Validate the graph and refresh structured diagnostics.
bool addNode(const std::string &nodeId, const std::string &kind)
Add a node of kind line, branch, choice, call, command, wait, or end.
std::string getParameter(int index) const
Return a declared parameter by insertion order.
std::string getNodeKind(const std::string &nodeId) const
Return a node kind name.
int getDiagnosticLine(int index) const
Return a diagnostic line, or zero for graph diagnostics.
bool removeRoute(const std::string &nodeId, int index)
Remove an outgoing route.
bool setEntry(const std::string &nodeId)
Set the entry node identifier.
std::string getField(const std::string &nodeId, const std::string &field) const
Return a scalar field value by key.
bool addParameter(const std::string &name)
Add a unique parameter.
bool removeParameter(const std::string &name)
Remove a parameter.
int getNodeCount() const
Return the number of graph nodes.
std::string getRouteLabel(const std::string &nodeId, int index) const
Return a route label or expression.
int getRouteCount(const std::string &nodeId) const
Return the number of outgoing conditional or choice routes.
std::string getRouteTarget(const std::string &nodeId, int index) const
Return a route destination.
int getFieldCount(const std::string &nodeId) const
Return the reflected inspector field count for a node kind.
std::string getFieldName(const std::string &nodeId, int index) const
Return a reflected field key.
bool addRoute(const std::string &nodeId, const std::string &label, const std::string &target)
Append an outgoing route.
bool setRoute(const std::string &nodeId, int index, const std::string &label, const std::string &target)
Replace an outgoing route.
std::string getDiagnosticMessage(int index) const
Return a diagnostic message.
std::string getFieldKind(const std::string &nodeId, int index) const
Return a field UI hint such as string, multiline, expression, asset, or json.
bool setNodeKind(const std::string &nodeId, const std::string &kind)
Change a node kind while preserving compatible fields.
bool removeNode(const std::string &nodeId)
Remove a node and references to it.
bool setVersion(int version)
Set the asset schema version to a positive value.
std::string getNodeId(int index) const
Return a stable node identifier by insertion order.
std::string getDiagnosticSeverity(int index) const
Return error or warning.
std::string getDiagnosticPath(int index) const
Return a diagnostic source path.
bool renameNode(const std::string &oldId, const std::string &newId)
Rename a node and rewrite graph references.
int getDiagnosticCount() const
Return the current diagnostic count.
bool hasNode(const std::string &nodeId) const
Test whether a node exists.
bool setField(const std::string &nodeId, const std::string &field, const std::string &value)
Set a scalar field value by key.
bool setId(const std::string &id)
Rename the document identifier.
ConversationDocument(std::string id={})
Create an empty document with one end node.
int getParameterCount() const
Return the number of declared parameters.
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::Result< void > lintConversations(const std::vector< eve::dnut::SequenceAsset > &assets, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Validate references and report unreachable nodes.
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.
Immutable, parameterized and versioned compiled sequence.
const SequenceNode * findNode(const std::string &nodeId) const noexcept
Find one node by its stable identifier.
std::vector< SequenceNode > nodes
std::vector< SequenceParameter > parameters
One immutable step of a compiled sequence.
One outgoing edge of a sequence node.