12 if (!
object)
return nullptr;
13 const auto found =
object->find(
key);
14 return found ==
object->end() ? nullptr : &
found->second;
17void diagnostic(DialogueGraphCompileResult& result,
const char* rule, std::string
message) {
19 DiagnosticSeverity::Error, std::move(
message)));
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);
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;
34 if (
kind ==
"call")
return call;
35 if (
kind ==
"command")
return command;
36 if (
kind ==
"route")
return route;
49 }
else if (
kind ==
"call") {
53 }
else if (
kind ==
"command") {
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;
72 DiagnosticSeverity::Error,
"Dialogue flow requires output-to-input pins on distinct nodes"));
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");
85 node.pins.push_back({GraphPinId(
id.
value() +
".in"),
id,
"dialogue.flow", GraphPinDirection::Input});
87 node.pins.push_back({GraphPinId(
id.
value() +
".out"),
id,
"dialogue.flow",
88 GraphPinDirection::Output});
89 return eve::editing::applied<GraphNodeRecord>(std::move(
node));
93 const std::string&
label)
const {
95 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId(
"editor.dialogue.invalid-route"),
96 "Dialogue route requires an id");
99 node.type =
"dialogue.route";
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));
110 result.
status = EditorStatus::Rejected;
111 diagnostic(result,
"editor.dialogue.wrong-domain",
"Graph is not a dialogue conversation document");
114 if (
graph.schemaVersion != 1) {
115 result.
status = EditorStatus::Unsupported;
116 diagnostic(result,
"editor.dialogue.unsupported-schema",
"Dialogue graph schema version is unsupported");
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");
134 std::map<GraphNodeId, const GraphNodeRecord*>
nodes;
135 std::map<GraphPinId, const GraphNodeRecord*> pinOwners;
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);
141 diagnostic(result,
"editor.dialogue.duplicate-node",
"Duplicate dialogue node id: " +
node.id.value());
143 if (!pinOwners.emplace(pin.id, &
node).second)
144 diagnostic(result,
"editor.dialogue.duplicate-pin",
"Duplicate dialogue pin id: " + pin.id.value());
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");
150 std::map<GraphNodeId, std::vector<GraphNodeId>> outgoing;
151 std::map<GraphNodeId, std::vector<GraphNodeId>> incoming;
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");
162 if (pin.id ==
edge.from) fromPin = &pin;
164 if (pin.id ==
edge.to) toPin = &pin;
166 diagnostic(result,
"editor.dialogue.invalid-edge",
"Dialogue edge has invalid direction or type");
169 outgoing[
from->second->id].push_back(
to->second->id);
170 incoming[
to->second->id].push_back(
from->second->id);
175 const std::string
kind =
node->type.substr(9);
178 diagnostic(result,
"editor.dialogue.invalid-properties",
179 "Dialogue node properties must be an object: " +
nodeId.value());
181 const auto& allowed = propertyKeys(
kind);
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)
188 diagnostic(result,
"editor.dialogue.missing-property",
189 "Dialogue node is missing property: " +
nodeId.value() +
"." +
key);
191 if (
kind ==
"route") {
193 diagnostic(result,
"editor.dialogue.incomplete-route",
194 "Route requires exactly one source and target: " +
nodeId.value());
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());
206 const bool routed =
kind ==
"branch" ||
kind ==
"choice";
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");
214 if (
kind ==
"end" && !outgoing[
nodeId].empty())
215 diagnostic(result,
"editor.dialogue.end-has-output",
"End node cannot have outgoing flow");
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());
225 compiled[
"next"] = outgoing[
nodeId].front().value();
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;
233 diagnostic(result,
"editor.dialogue.invalid-route-label",
234 "Route label must be a string: " + routeId.value());
238 {
"target", outgoing[routeId].front().
value()}});
241 compiled[
"routes"] = std::move(routes);
242 compiledNodes.emplace_back(std::move(compiled));
244 std::set<std::string> parameterNames;
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");
252 result.
status = EditorStatus::Failed;
256 {
"parameters", *parameters}, {
"nodes", std::move(compiledNodes)}};
257 result.
status = EditorStatus::Applied;
HexCoordinates to
Cell the unit walks towards on this segment.
std::map< std::string, PropertyRule > properties
std::map< std::string, std::vector< std::string > > graph
const UnitySourceAsset & source
Move-only operation result carrying either a value or Status.
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.
editing::Value EditorValue
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.
Revision documentRevision
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.