载入中...
搜索中...
未找到
DialogueGraph.cpp
浏览该文件的文档.
2
3#include <map>
4#include <set>
5#include <utility>
6
7namespace eve::dialogue_editing {
8namespace {
9
10const EditorValue* field(const EditorValue& value, const char* key) {
11 const auto* object = value.getIf<EditorValue::Object>();
12 if (!object) return nullptr;
13 const auto found = object->find(key);
14 return found == object->end() ? nullptr : &found->second;
15}
16
17void diagnostic(DialogueGraphCompileResult& result, const char* rule, std::string message) {
19 DiagnosticSeverity::Error, std::move(message)));
20}
21
22bool isDialogueKind(const std::string& kind) {
23 static const std::set<std::string> kinds{"line", "branch", "choice", "call", "command", "wait", "end"};
24 return kinds.contains(kind);
25}
26
27const std::set<std::string>& propertyKeys(const std::string& kind) {
28 static const std::set<std::string> line{"speaker", "text", "pool", "i18nKey", "voice", "expression"};
29 static const std::set<std::string> call{"target", "returnNode", "arguments"};
30 static const std::set<std::string> command{"target", "expression", "arguments"};
31 static const std::set<std::string> route{"label"};
32 static const std::set<std::string> empty;
33 if (kind == "line") return line;
34 if (kind == "call") return call;
35 if (kind == "command") return command;
36 if (kind == "route") return route;
37 return empty;
38}
39
40EditorValue::Object defaultProperties(const std::string& kind) {
42 if (kind == "line") {
43 properties["speaker"] = "";
44 properties["text"] = "";
45 properties["pool"] = "";
46 properties["i18nKey"] = "";
47 properties["voice"] = "";
48 properties["expression"] = "";
49 } else if (kind == "call") {
50 properties["target"] = "";
51 properties["returnNode"] = "";
52 properties["arguments"] = "{}";
53 } else if (kind == "command") {
54 properties["target"] = "";
55 properties["expression"] = "";
56 properties["arguments"] = "{}";
57 }
58 return properties;
59}
60
61} // namespace
62
64 const GraphPinRecord& to) const {
66 decision.allowed = from.direction == GraphPinDirection::Output &&
67 to.direction == GraphPinDirection::Input && from.type == "dialogue.flow" &&
68 to.type == "dialogue.flow" && from.node != to.node;
69 if (!decision.allowed)
71 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.dialogue.invalid-connection"),
72 DiagnosticSeverity::Error, "Dialogue flow requires output-to-input pins on distinct nodes"));
73 return decision;
74}
75
77 const std::string& kind) const {
78 if (id.empty() || !isDialogueKind(kind))
79 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId("editor.dialogue.invalid-node"),
80 "Dialogue node requires an id and supported kind");
82 node.id = id;
83 node.type = "dialogue." + kind;
84 node.properties = EditorValue(defaultProperties(kind));
85 node.pins.push_back({GraphPinId(id.value() + ".in"), id, "dialogue.flow", GraphPinDirection::Input});
86 if (kind != "end")
87 node.pins.push_back({GraphPinId(id.value() + ".out"), id, "dialogue.flow",
88 GraphPinDirection::Output});
89 return eve::editing::applied<GraphNodeRecord>(std::move(node));
90}
91
93 const std::string& label) const {
94 if (id.empty())
95 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId("editor.dialogue.invalid-route"),
96 "Dialogue route requires an id");
98 node.id = id;
99 node.type = "dialogue.route";
100 node.properties = EditorValue::Object{{"label", label}};
101 node.pins = {{GraphPinId(id.value() + ".in"), id, "dialogue.flow", GraphPinDirection::Input},
102 {GraphPinId(id.value() + ".out"), id, "dialogue.flow", GraphPinDirection::Output}};
103 return eve::editing::applied<GraphNodeRecord>(std::move(node));
104}
105
108 result.documentRevision = graph.revision;
109 if (graph.domain != domain()) {
110 result.status = EditorStatus::Rejected;
111 diagnostic(result, "editor.dialogue.wrong-domain", "Graph is not a dialogue conversation document");
112 return result;
113 }
114 if (graph.schemaVersion != 1) {
115 result.status = EditorStatus::Unsupported;
116 diagnostic(result, "editor.dialogue.unsupported-schema", "Dialogue graph schema version is unsupported");
117 return result;
118 }
119 const auto* idValue = field(graph.parameters, "id");
120 const auto* versionValue = field(graph.parameters, "version");
121 const auto* entryValue = field(graph.parameters, "entry");
122 const auto* parametersValue = field(graph.parameters, "parameters");
123 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
124 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr;
125 const auto* entry = entryValue ? entryValue->getIf<std::string>() : nullptr;
126 const auto* parameters = parametersValue ? parametersValue->getIf<EditorValue::Array>() : nullptr;
127 if (!id || id->empty() || !version || *version <= 0 || !entry || entry->empty() || !parameters) {
128 result.status = EditorStatus::Failed;
129 diagnostic(result, "editor.dialogue.invalid-document",
130 "Dialogue parameters require id, positive version, entry and a parameter array");
131 return result;
132 }
133
134 std::map<GraphNodeId, const GraphNodeRecord*> nodes;
135 std::map<GraphPinId, const GraphNodeRecord*> pinOwners;
136 for (const GraphNodeRecord& node : graph.nodes) {
137 const std::string kind = node.type.starts_with("dialogue.") ? node.type.substr(9) : "";
138 if (!isDialogueKind(kind) && kind != "route")
139 diagnostic(result, "editor.dialogue.unknown-node", "Unknown dialogue node type: " + node.type);
140 if (!nodes.emplace(node.id, &node).second)
141 diagnostic(result, "editor.dialogue.duplicate-node", "Duplicate dialogue node id: " + node.id.value());
142 for (const GraphPinRecord& pin : node.pins)
143 if (!pinOwners.emplace(pin.id, &node).second)
144 diagnostic(result, "editor.dialogue.duplicate-pin", "Duplicate dialogue pin id: " + pin.id.value());
145 }
146 const auto entryNode = nodes.find(GraphNodeId(*entry));
147 if (entryNode == nodes.end() || entryNode->second->type == "dialogue.route")
148 diagnostic(result, "editor.dialogue.invalid-entry", "Entry must reference a dialogue business node");
149
150 std::map<GraphNodeId, std::vector<GraphNodeId>> outgoing;
151 std::map<GraphNodeId, std::vector<GraphNodeId>> incoming;
152 for (const GraphEdgeRecord& edge : graph.edges) {
153 const auto from = pinOwners.find(edge.from);
154 const auto to = pinOwners.find(edge.to);
155 if (from == pinOwners.end() || to == pinOwners.end()) {
156 diagnostic(result, "editor.dialogue.dangling-edge", "Dialogue edge references a missing pin");
157 continue;
158 }
159 const GraphPinRecord* fromPin = nullptr;
160 const GraphPinRecord* toPin = nullptr;
161 for (const GraphPinRecord& pin : from->second->pins)
162 if (pin.id == edge.from) fromPin = &pin;
163 for (const GraphPinRecord& pin : to->second->pins)
164 if (pin.id == edge.to) toPin = &pin;
165 if (!fromPin || !toPin || !canConnect(*fromPin, *toPin).allowed) {
166 diagnostic(result, "editor.dialogue.invalid-edge", "Dialogue edge has invalid direction or type");
167 continue;
168 }
169 outgoing[from->second->id].push_back(to->second->id);
170 incoming[to->second->id].push_back(from->second->id);
171 }
172
173 EditorValue::Array compiledNodes;
174 for (const auto& [nodeId, node] : nodes) {
175 const std::string kind = node->type.substr(9);
176 const auto* properties = node->properties.getIf<EditorValue::Object>();
177 if (!properties) {
178 diagnostic(result, "editor.dialogue.invalid-properties",
179 "Dialogue node properties must be an object: " + nodeId.value());
180 } else {
181 const auto& allowed = propertyKeys(kind);
182 for (const auto& [key, value] : *properties)
183 if (!allowed.contains(key) || !value.getIf<std::string>())
184 diagnostic(result, "editor.dialogue.invalid-property",
185 "Dialogue property is unknown or not a string: " + nodeId.value() + "." + key);
186 for (const std::string& key : allowed)
187 if (!properties->contains(key))
188 diagnostic(result, "editor.dialogue.missing-property",
189 "Dialogue node is missing property: " + nodeId.value() + "." + key);
190 }
191 if (kind == "route") {
192 if (incoming[nodeId].size() != 1 || outgoing[nodeId].size() != 1)
193 diagnostic(result, "editor.dialogue.incomplete-route",
194 "Route requires exactly one source and target: " + nodeId.value());
195 else {
196 const auto source = nodes.at(incoming[nodeId].front());
197 if (source->type != "dialogue.branch" && source->type != "dialogue.choice")
198 diagnostic(result, "editor.dialogue.invalid-route-source",
199 "Routes may only originate from branch or choice nodes: " + nodeId.value());
200 if (nodes.at(outgoing[nodeId].front())->type == "dialogue.route")
201 diagnostic(result, "editor.dialogue.invalid-route-target",
202 "A route must target a business node: " + nodeId.value());
203 }
204 continue;
205 }
206 const bool routed = kind == "branch" || kind == "choice";
207 for (const GraphNodeId& target : outgoing[nodeId]) {
208 const bool targetIsRoute = nodes.at(target)->type == "dialogue.route";
209 if (routed != targetIsRoute)
210 diagnostic(result, "editor.dialogue.invalid-flow",
211 routed ? "Branch and choice outputs must connect through route nodes"
212 : "Only branch and choice nodes may connect to route nodes");
213 }
214 if (kind == "end" && !outgoing[nodeId].empty())
215 diagnostic(result, "editor.dialogue.end-has-output", "End node cannot have outgoing flow");
216 if (!routed && kind != "end" && outgoing[nodeId].size() != 1)
217 diagnostic(result, "editor.dialogue.next-required",
218 "Dialogue node requires exactly one next target: " + nodeId.value());
219 if (routed && outgoing[nodeId].empty())
220 diagnostic(result, "editor.dialogue.route-required",
221 "Branch or choice requires at least one route: " + nodeId.value());
222
223 EditorValue::Object compiled{{"id", nodeId.value()}, {"kind", kind}, {"properties", node->properties}};
224 if (!routed && kind != "end" && outgoing[nodeId].size() == 1)
225 compiled["next"] = outgoing[nodeId].front().value();
226 EditorValue::Array routes;
227 if (routed) {
228 for (const GraphNodeId& routeId : outgoing[nodeId]) {
229 if (nodes.at(routeId)->type != "dialogue.route" || outgoing[routeId].size() != 1) continue;
230 const EditorValue* labelValue = field(nodes.at(routeId)->properties, "label");
231 const auto* label = labelValue ? labelValue->getIf<std::string>() : nullptr;
232 if (!label)
233 diagnostic(result, "editor.dialogue.invalid-route-label",
234 "Route label must be a string: " + routeId.value());
235 else
236 routes.emplace_back(EditorValue::Object{{"id", routeId.value()},
237 {"label", *label},
238 {"target", outgoing[routeId].front().value()}});
239 }
240 }
241 compiled["routes"] = std::move(routes);
242 compiledNodes.emplace_back(std::move(compiled));
243 }
244 std::set<std::string> parameterNames;
245 for (const EditorValue& parameter : *parameters) {
246 const auto* name = parameter.getIf<std::string>();
247 if (!name || name->empty() || !parameterNames.insert(*name).second)
248 diagnostic(result, "editor.dialogue.invalid-parameter",
249 "Conversation parameters must be unique non-empty strings");
250 }
251 if (!result.diagnostics.empty()) {
252 result.status = EditorStatus::Failed;
253 return result;
254 }
255 result.definition = EditorValue::Object{{"id", *id}, {"version", *version}, {"entry", *entry},
256 {"parameters", *parameters}, {"nodes", std::move(compiledNodes)}};
257 result.status = EditorStatus::Applied;
258 return result;
259}
260
261} // namespace eve::dialogue_editing
LogicalId target
double value
std::string from
std::string label
std::string nodeId
std::string message
std::uint32_t key
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
TokenKind kind
std::map< std::string, PropertyRule > properties
std::string name
std::vector< BvhNode > nodes
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::string id
Definition PlayHost.cpp:108
const RoadNode * node
const RoadEdge * edge
bool found
float size
Definition TreeMesh.cpp:156
const UnitySourceAsset & source
Move-only operation result carrying either a value or Status.
Definition Result.h:155
Result< GraphNodeRecord > makeNode(const GraphNodeId &id, const std::string &kind) const
Construct a line, branch, choice, call, command, wait, or end node.
GraphConnectionDecision canConnect(const GraphPinRecord &from, const GraphPinRecord &to) const override
Can connect.
Result< GraphNodeRecord > makeRouteNode(const GraphNodeId &id, const std::string &label={}) const
Construct one stable labelled route for a branch or choice node.
std::string domain() const override
Domain.
DialogueGraphCompileResult compile(const GraphDocumentData &graph) const
Validate and compile the graph into a deterministic value definition.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
const EditorValue * field(const EditorValue &value, const char *name)
Validated, runtime-neutral conversation definition compiled from a graph.
std::vector< EditorDiagnostic > diagnostics
Result of domain-specific connection validation.
std::vector< Diagnostic > diagnostics
Immutable graph document value sent to a domain compiler.
Directed connection between an output and input pin.
Generic graph node independent of its domain.
Stable typed pin owned by a graph node.