载入中...
搜索中...
未找到
ConversationImporter.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cctype>
7#include <iterator>
8#include <sstream>
9#include <string>
10#include <unordered_set>
11#include <utility>
12#include <vector>
13
14namespace eve::dialogue {
15namespace {
16
17struct Passage {
18 std::string title;
19 int line = 0;
20 std::vector<std::pair<int, std::string>> body;
21};
22
23std::string trim(std::string value) {
24 const auto first =
25 std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
26 const auto last =
27 std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch) != 0; }).base();
28 return first < last ? std::string(first, last) : std::string{};
29}
30
31std::vector<std::pair<int, std::string>> splitLines(const std::string& source) {
32 std::vector<std::pair<int, std::string>> output;
33 std::istringstream stream(source);
34 std::string line;
35 int number = 0;
36 while (std::getline(stream, line)) {
37 ++number;
38 if (!line.empty() && line.back() == '\r') line.pop_back();
39 output.emplace_back(number, std::move(line));
40 }
41 return output;
42}
43
44std::string assetIdFromPath(const std::string& path) {
45 const std::size_t slash = path.find_last_of("/\\");
46 const std::size_t dot = path.find_last_of('.');
47 const std::size_t begin = slash == std::string::npos ? 0 : slash + 1;
48 const std::size_t end = dot == std::string::npos || dot < begin ? path.size() : dot;
49 std::string id = path.substr(begin, end - begin);
50 for (char& character : id)
51 if (!std::isalnum(static_cast<unsigned char>(character)) && character != '_' && character != '-' &&
52 character != '.')
53 character = '_';
54 return id.empty() ? "imported" : id;
55}
56
57bool parseLink(const std::string& text, std::string& label, std::string& target) {
58 if (text.size() < 4 || text.substr(0, 2) != "[[" || text.substr(text.size() - 2) != "]]") return false;
59 const std::string body = trim(text.substr(2, text.size() - 4));
60 std::size_t split = body.find("->");
61 std::size_t width = 2;
62 if (split == std::string::npos) {
63 split = body.find('|');
64 width = 1;
65 }
66 if (split == std::string::npos) {
67 split = body.find("<-");
68 if (split != std::string::npos) {
69 target = trim(body.substr(0, split));
70 label = trim(body.substr(split + 2));
71 return !label.empty() && !target.empty();
72 }
73 }
74 if (split == std::string::npos)
75 label = target = trim(body);
76 else {
77 label = trim(body.substr(0, split));
78 target = trim(body.substr(split + width));
79 }
80 return !label.empty() && !target.empty();
81}
82
83std::string commandArgument(const std::string& text, const std::string& command) {
84 const std::string prefix = "<<" + command;
85 if (text.rfind(prefix, 0) != 0 || text.size() < prefix.size() + 2 || text.substr(text.size() - 2) != ">>")
86 return {};
87 return trim(text.substr(prefix.size(), text.size() - prefix.size() - 2));
88}
89
90void consumeLineTags(std::string& text, eve::dnut::SequenceNode& node) {
91 std::size_t tag = text.find(" #");
92 while (tag != std::string::npos) {
93 const std::size_t end = text.find(' ', tag + 2);
94 const std::string value = text.substr(tag + 2, end == std::string::npos ? std::string::npos : end - tag - 2);
95 if (value.rfind("line:", 0) == 0) node.payload.set("i18n", eve::Value::string(value.substr(5)));
96 if (value.rfind("voice:", 0) == 0) node.payload.set("voice", eve::Value::string(value.substr(6)));
97 text.erase(tag, end == std::string::npos ? std::string::npos : end - tag);
98 tag = text.find(" #", tag);
99 }
100 text = trim(text);
101}
102
103void addDiagnostic(std::vector<ConversationDiagnostic>& diagnostics, ConversationDiagnostic::Severity severity,
104 const std::string& path, int line, const std::string& message) {
105 diagnostics.push_back({severity, path, line, message});
106}
107
108eve::dnut::SequenceRoute route(std::string label, std::string target) {
110 value.label = std::move(label);
111 value.target = std::move(target);
112 return value;
113}
114
115bool isSequential(const std::string& type) { return type == "line" || type == "command" || type == "wait"; }
116
117bool buildAsset(const std::vector<Passage>& passages, const std::string& path,
118 std::vector<eve::dnut::SequenceAsset>& assets, std::vector<ConversationDiagnostic>& diagnostics) {
119 if (passages.empty()) {
120 addDiagnostic(diagnostics, ConversationDiagnostic::Severity::Error, path, 0, "no dialogue passages found");
121 return false;
122 }
124 asset.id = assetIdFromPath(path);
125 asset.entry = passages.front().title;
126 std::unordered_set<std::string> titles;
127 for (const auto& passage : passages)
128 if (passage.title.empty() || !titles.insert(passage.title).second)
129 addDiagnostic(diagnostics, ConversationDiagnostic::Severity::Error, path, passage.line,
130 "empty or duplicate passage title: " + passage.title);
131
132 for (const auto& passage : passages) {
133 std::vector<eve::dnut::SequenceNode> nodes;
134 std::size_t index = 0;
135 for (std::size_t bodyIndex = 0; bodyIndex < passage.body.size();) {
136 const auto& [lineNumber, raw] = passage.body[bodyIndex];
137 const std::string text = trim(raw);
138 if (text.empty() || text == "---" || text == "===") {
139 ++bodyIndex;
140 continue;
141 }
142 std::string label;
143 std::string target;
144 if (parseLink(text, label, target) || text.rfind("->", 0) == 0) {
146 node.type = "choice";
147 node.id = index == 0 ? passage.title : passage.title + "." + std::to_string(index);
148 ++index;
149 while (bodyIndex < passage.body.size()) {
150 const std::string candidate = trim(passage.body[bodyIndex].second);
151 if (!parseLink(candidate, label, target)) {
152 if (candidate.rfind("->", 0) != 0) break;
153 label = trim(candidate.substr(2));
154 target.clear();
155 if (bodyIndex + 1 < passage.body.size())
156 target = commandArgument(trim(passage.body[bodyIndex + 1].second), "jump");
157 if (target.empty()) {
158 addDiagnostic(diagnostics, ConversationDiagnostic::Severity::Error, path,
159 passage.body[bodyIndex].first,
160 "Yarn shortcut option requires a following <<jump Target>>");
161 break;
162 }
163 ++bodyIndex;
164 }
165 node.routes.push_back(route(label, target));
166 ++bodyIndex;
167 }
168 nodes.push_back(std::move(node));
169 continue;
170 }
171
173 node.id = index == 0 ? passage.title : passage.title + "." + std::to_string(index);
174 ++index;
175 const std::string jump = commandArgument(text, "jump");
176 const std::string wait = commandArgument(text, "wait");
177 const std::string call = commandArgument(text, "call");
178 const std::string set = commandArgument(text, "set");
179 if (!jump.empty()) {
180 node.type = "branch";
181 node.routes.push_back(route("else", jump));
182 } else if (text == "<<stop>>") {
183 node.type = "end";
184 } else if (!wait.empty()) {
185 node.type = "wait";
186 node.payload.set("duration", eve::Value::string(wait));
187 } else if (!call.empty() || !set.empty()) {
188 node.type = "command";
189 node.payload.set("name", eve::Value::string(!call.empty() ? "call" : "set"));
190 node.payload.set("resultLocal", eve::Value::string(!call.empty() ? call : set));
191 } else {
192 node.type = "line";
193 std::string lineText = text;
194 consumeLineTags(lineText, node);
195 const std::size_t colon = lineText.find(':');
196 if (colon != std::string::npos && colon > 0 && lineText.find("[[") == std::string::npos) {
197 node.payload.set("speaker", eve::Value::string(trim(lineText.substr(0, colon))));
198 node.payload.set("text", eve::Value::string(trim(lineText.substr(colon + 1))));
199 } else {
200 node.payload.set("text", eve::Value::string(lineText));
201 if (lineText.find("[[") != std::string::npos)
202 addDiagnostic(diagnostics, ConversationDiagnostic::Severity::Warning, path, lineNumber,
203 "inline passage links are imported as text");
204 }
205 }
206 nodes.push_back(std::move(node));
207 ++bodyIndex;
208 }
209 if (nodes.empty()) {
211 end.id = passage.title;
212 end.type = "end";
213 nodes.push_back(std::move(end));
214 }
215 for (std::size_t nodeIndex = 0; nodeIndex + 1 < nodes.size(); ++nodeIndex)
216 if (isSequential(nodes[nodeIndex].type)) nodes[nodeIndex].next = nodes[nodeIndex + 1].id;
217 if (isSequential(nodes.back().type)) {
219 end.id = passage.title + ".end";
220 end.type = "end";
221 nodes.back().next = end.id;
222 nodes.push_back(std::move(end));
223 }
224 asset.nodes.insert(asset.nodes.end(), std::make_move_iterator(nodes.begin()),
225 std::make_move_iterator(nodes.end()));
226 }
227 if (std::any_of(diagnostics.begin(), diagnostics.end(), [](const auto& diagnostic) {
228 return diagnostic.severity == ConversationDiagnostic::Severity::Error;
229 }))
230 return false;
231 assets.push_back(std::move(asset));
232 return lintConversations(assets, path, diagnostics).ok();
233}
234
235std::vector<Passage> parseYarnPassages(const std::string& source) {
236 std::vector<Passage> passages;
237 Passage current;
238 bool inBody = false;
239 for (const auto& [number, raw] : splitLines(source)) {
240 const std::string text = trim(raw);
241 if (text.rfind("title:", 0) == 0) {
242 if (!current.title.empty()) passages.push_back(std::move(current));
243 current = {};
244 current.title = trim(text.substr(6));
245 current.line = number;
246 inBody = false;
247 } else if (text == "---")
248 inBody = !current.title.empty();
249 else if (text == "===") {
250 if (!current.title.empty()) passages.push_back(std::move(current));
251 current = {};
252 inBody = false;
253 } else if (inBody)
254 current.body.emplace_back(number, raw);
255 }
256 if (!current.title.empty()) passages.push_back(std::move(current));
257 return passages;
258}
259
260} // namespace
261
263 const std::string& source, const std::string& path, std::vector<ConversationDiagnostic>& diagnostics) {
264 std::vector<eve::dnut::SequenceAsset> assets;
265 if (!buildAsset(parseYarnPassages(source), path, assets, diagnostics)) {
266 const std::string message = diagnostics.empty() ? "Yarn import failed" : diagnostics.front().message;
269 }
270 return eve::Result<std::vector<eve::dnut::SequenceAsset>>::success(std::move(assets));
271}
272
274 const std::string& source, const std::string& path, std::vector<ConversationDiagnostic>& diagnostics) {
275 std::vector<Passage> passages;
276 Passage current;
277 for (const auto& [number, raw] : splitLines(source)) {
278 const std::string text = trim(raw);
279 if (text.rfind("::", 0) == 0) {
280 if (!current.title.empty()) passages.push_back(std::move(current));
281 current = {};
282 current.title = trim(text.substr(2));
283 const std::size_t tags = current.title.find(" [");
284 if (tags != std::string::npos) current.title = trim(current.title.substr(0, tags));
285 current.line = number;
286 } else if (!current.title.empty()) {
287 current.body.emplace_back(number, raw);
288 }
289 }
290 if (!current.title.empty()) passages.push_back(std::move(current));
291 passages.erase(std::remove_if(passages.begin(), passages.end(),
292 [](const Passage& passage) {
293 return passage.title == "StoryTitle" || passage.title == "StoryData";
294 }),
295 passages.end());
296 std::vector<eve::dnut::SequenceAsset> assets;
297 if (!buildAsset(passages, path, assets, diagnostics)) {
298 const std::string message = diagnostics.empty() ? "Twee import failed" : diagnostics.front().message;
301 }
302 return eve::Result<std::vector<eve::dnut::SequenceAsset>>::success(std::move(assets));
303}
304
305} // namespace eve::dialogue
LogicalId target
double value
std::string output
bool split
Definition CaveMesh.cpp:123
std::string title
Dialogue payload codecs for L1 sequence assets.
std::string label
std::string message
std::int32_t second
std::int32_t first
std::uint32_t width
std::string text
std::vector< BvhNode > nodes
const std::string * tag
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float begin
const RoadNode * node
double number
double current
std::string body
uint32_t index
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
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Value string(std::string value)
Compatibility factory for a string value.
Definition Value.h:110
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< 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::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.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
Severity
Stable severity of a diagnostic, independent of its display text.
Definition Diagnostic.h:20
Immutable, parameterized and versioned compiled sequence.
std::vector< SequenceNode > nodes
One immutable step of a compiled sequence.
One outgoing edge of a sequence node.