10#include <unordered_set>
20 std::vector<std::pair<int, std::string>>
body;
23std::string trim(std::string
value) {
25 std::find_if_not(
value.begin(),
value.end(), [](
unsigned char ch) { return std::isspace(ch) != 0; });
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{};
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);
36 while (std::getline(stream,
line)) {
38 if (!
line.empty() &&
line.back() ==
'\r')
line.pop_back();
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;
50 for (
char& character :
id)
51 if (!
std::isalnum(static_cast<unsigned char>(character)) && character !=
'_' && character !=
'-' &&
54 return id.empty() ?
"imported" :
id;
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));
61 std::size_t
width = 2;
62 if (
split == std::string::npos) {
66 if (
split == std::string::npos) {
68 if (
split != std::string::npos) {
74 if (
split == std::string::npos)
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) !=
">>")
87 return trim(
text.substr(prefix.size(),
text.size() - prefix.size() - 2));
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);
115bool isSequential(
const std::string&
type) {
return type ==
"line" ||
type ==
"command" ||
type ==
"wait"; }
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()) {
124 asset.
id = assetIdFromPath(
path);
125 asset.
entry = passages.front().title;
126 std::unordered_set<std::string> titles;
127 for (
const auto& passage : passages)
130 "empty or duplicate passage title: " + passage.
title);
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);
147 node.id =
index == 0 ? passage.title : passage.title +
"." + std::to_string(
index);
149 while (bodyIndex < passage.body.size()) {
150 const std::string candidate = trim(passage.body[bodyIndex].second);
152 if (candidate.rfind(
"->", 0) != 0)
break;
153 label = trim(candidate.substr(2));
155 if (bodyIndex + 1 < passage.body.size())
156 target = commandArgument(trim(passage.body[bodyIndex + 1].second),
"jump");
159 passage.body[bodyIndex].first,
160 "Yarn shortcut option requires a following <<jump Target>>");
173 node.
id =
index == 0 ? passage.title : passage.title +
"." + std::to_string(
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");
180 node.type =
"branch";
181 node.routes.push_back(route(
"else", jump));
182 }
else if (
text ==
"<<stop>>") {
184 }
else if (!wait.empty()) {
187 }
else if (!call.empty() || !set.empty()) {
188 node.type =
"command";
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) {
201 if (lineText.find(
"[[") != std::string::npos)
203 "inline passage links are imported as text");
211 end.
id = passage.title;
217 if (isSequential(
nodes.back().type)) {
219 end.
id = passage.title +
".end";
224 asset.
nodes.insert(asset.
nodes.end(), std::make_move_iterator(
nodes.begin()),
225 std::make_move_iterator(
nodes.end()));
227 if (std::any_of(diagnostics.begin(), diagnostics.end(), [](
const auto& diagnostic) {
228 return diagnostic.severity == ConversationDiagnostic::Severity::Error;
231 assets.push_back(std::move(asset));
235std::vector<Passage> parseYarnPassages(
const std::string&
source) {
236 std::vector<Passage> passages;
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));
247 }
else if (
text ==
"---")
248 inBody = !
current.title.empty();
249 else if (
text ==
"===") {
250 if (!
current.title.empty()) passages.push_back(std::move(
current));
256 if (!
current.title.empty()) passages.push_back(std::move(
current));
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;
274 const std::string&
source,
const std::string&
path, std::vector<ConversationDiagnostic>& diagnostics) {
275 std::vector<Passage> passages;
278 const std::string
text = trim(raw);
279 if (
text.rfind(
"::", 0) == 0) {
280 if (!
current.title.empty()) passages.push_back(std::move(
current));
283 const std::size_t tags =
current.title.find(
" [");
284 if (tags != std::string::npos)
current.title = trim(
current.title.substr(0, tags));
286 }
else if (!
current.title.empty()) {
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";
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;
Dialogue payload codecs for L1 sequence assets.
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.
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Move-only operation result carrying either a value or Status.
static Value string(std::string value)
Compatibility factory for a string value.
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.
Severity
Stable severity of a diagnostic, independent of its display text.
Severity
Severity public API.
Immutable, parameterized and versioned compiled sequence.
std::vector< SequenceNode > nodes
One immutable step of a compiled sequence.
One outgoing edge of a sequence node.