载入中...
搜索中...
未找到
Dialogue.cpp
浏览该文件的文档.
1#include "dialogue/Dialogue.h"
3
5#include "common/Capability.h"
8#include "common/utf8.h"
11#include "i18n/I18n.h"
12#include "scene/Scene.h"
13#include "scene/SceneHost.h"
14
15#include <algorithm>
16#include <cmath>
17#include <cstdio>
18#include <iterator>
19#include <simplesquirrel/simplesquirrel.hpp>
20#include <squirrel.h>
21
22namespace eve::dialogue {
23
25
26namespace {
27
28std::string floatToString(double v) {
29 char buf[64];
30 std::snprintf(buf, sizeof(buf), "%g", v);
31 return buf;
32}
33
34bool squirrelToVarValueAt(HSQUIRRELVM vm, SQInteger index, Dialogue::VarValue &out) {
35 auto converted = eve::script::valueFromSquirrel(vm, index);
36 if (!converted.ok()) {
37 converted.ignore("Dialogue scalar compatibility adapter rejected the value");
38 return false;
39 }
40 Dialogue::VarValue value = std::move(converted).takeValue();
41 if (value.isNull() || value.isArray() || value.isObject()) return false;
42 out = std::move(value);
43 return true;
44}
45
47bool objectToDataValue(HSQUIRRELVM vm, const ssq::Object &object, DataValue &out) {
48 if (!vm || object.isEmpty()) return false;
49 auto converted = eve::script::valueFromSquirrel(object);
50 if (!converted.ok()) {
51 converted.ignore("Dialogue data compatibility adapter rejected the value");
52 return false;
53 }
54 out = std::move(converted).takeValue();
55 return true;
56}
57
58bool objectToVarParams(HSQUIRRELVM vm, const ssq::Object &object,
59 std::unordered_map<std::string, Dialogue::VarValue> &out) {
60 out.clear();
61 if (!vm || object.isEmpty() || object.getType() != ssq::Type::TABLE) return true;
62 auto converted = eve::script::valueFromSquirrel(object);
63 if (!converted.ok()) {
64 converted.ignore("Dialogue parameter compatibility adapter rejected the table");
65 return true;
66 }
67 Dialogue::VarValue value = std::move(converted).takeValue();
68 const auto *fields = value.getIf<eve::Value::Object>();
69 if (!fields) return true;
70 for (const auto &[key, field] : *fields) {
71 if (!field.isNull() && !field.isArray() && !field.isObject()) out[key] = field;
72 }
73 return true;
74}
75
76void pushVarValue(HSQUIRRELVM vm, const Dialogue::VarValue &value) {
77 auto pushed = eve::script::pushValue(vm, value);
78 if (!pushed.ok()) {
79 pushed.ignore("Dialogue scalar compatibility adapter could not push the value");
80 sq_pushnull(vm);
81 }
82}
83
84void pushVarTable(HSQUIRRELVM vm, const std::unordered_map<std::string, Dialogue::VarValue> &vars) {
86 for (const auto &[key, value] : vars) fields.emplace(key, value);
87 auto pushed = eve::script::pushValue(vm, eve::Value(std::move(fields)));
88 if (!pushed.ok()) {
89 pushed.ignore("Dialogue parameter compatibility adapter could not push the table");
90 sq_pushnull(vm);
91 }
92}
93
94std::vector<Dialogue*>& liveDialogues() {
95 static std::vector<Dialogue*> instances;
96 return instances;
97}
98
99StateValue varValueToState(const Dialogue::VarValue& v) {
100 switch (v.type()) {
105 default: break;
106 }
107 return StateValue::null();
108}
109
110bool stateToVarValue(const StateValue& v, Dialogue::VarValue& out) {
111 switch (v.kind()) {
112 case StateValue::Kind::Int: out = Dialogue::VarValue::integer(v.asInt()); return true;
113 case StateValue::Kind::Float: out = Dialogue::VarValue::number(v.asDouble()); return true;
114 case StateValue::Kind::Bool: out = Dialogue::VarValue::boolean(v.asBool()); return true;
115 case StateValue::Kind::String: out = Dialogue::VarValue::string(v.asString()); return true;
116 default: return false;
117 }
118}
119
121class DialogueStateProvider : public eve::caps::IStateProvider {
122public:
123 const char* stateKind() const override { return "dialogue"; }
124
125 bool captureState(StateValue& out) override {
126 StateValue arr = StateValue::array();
127 for (Dialogue* d : liveDialogues()) {
128 StateValue sv;
129 if (d->captureState(sv)) arr.pushBack(std::move(sv));
130 }
131 out = std::move(arr);
132 return true;
133 }
134
135 bool restoreState(const StateValue& in, std::string* err) override {
136 if (!in.isArray()) {
137 if (err) *err = "dialogue: expected array of instances";
138 return false;
139 }
140 const size_t n = std::min(in.arraySize(), liveDialogues().size());
141 for (size_t i = 0; i < n; ++i) {
142 if (!liveDialogues()[i]->restoreState(in.at(i), err)) return false;
143 }
144 return true;
145 }
146
147 bool resetToDefaults() override {
148 for (Dialogue* d : liveDialogues()) d->reset();
149 return true;
150 }
151};
152
153struct Register {
154 Register() {
155 static DialogueStateProvider provider;
156 eve::cap::addListener<eve::caps::IStateProvider>(&provider);
157 }
158} g_register;
159
160} // namespace
161
162Dialogue::Dialogue() { liveDialogues().push_back(this); }
163
165 liveDialogues().erase(std::remove(liveDialogues().begin(), liveDialogues().end(), this), liveDialogues().end());
166 if (vm_) {
167 for (auto &kv : predicates_) sq_release(vm_, &kv.second);
168 predicates_.clear();
169 }
170}
171
172// ---------------------------------------------------------------------------
173// Variables (global / scene)
174// ---------------------------------------------------------------------------
175
176std::unordered_map<std::string, Dialogue::VarValue> *Dialogue::varsForScope(
177 const std::string &scope) {
178 if (scope == "global") return &globalVars_;
179 if (scope == "scene") return &sceneVars_;
180 return nullptr;
181}
182
183const std::unordered_map<std::string, Dialogue::VarValue> *Dialogue::varsForScope(
184 const std::string &scope) const {
185 if (scope == "global") return &globalVars_;
186 if (scope == "scene") return &sceneVars_;
187 return nullptr;
188}
189
190bool Dialogue::setVarValue(const std::string &name, const VarValue &value,
191 const std::string &scope) {
192 auto *m = varsForScope(scope);
193 if (!m || name.empty()) return false;
194 (*m)[name] = value;
195 return true;
196}
197
198bool Dialogue::setVar(const std::string &name, ssq::Object value, const std::string &scope) {
199 if (!vm_) return false;
200 const SQInteger top = sq_gettop(vm_);
201 sq_pushobject(vm_, value.getRaw());
202 VarValue v;
203 const bool ok = squirrelToVarValueAt(vm_, -1, v);
204 sq_settop(vm_, top);
205 if (!ok) return false;
206 return setVarValue(name, v, scope);
207}
208
209Dialogue::VarValue Dialogue::getVarValue(const std::string &name, const std::string &scope) const {
210 const auto *m = varsForScope(scope);
211 if (!m) return VarValue::string("");
212 const auto it = m->find(name);
213 return it == m->end() ? VarValue::string("") : it->second;
214}
215
216std::string Dialogue::getVarType(const std::string &name, const std::string &scope) const {
217 return hasVar(name, scope) ? getVarValue(name, scope).typeName() : "";
218}
219
220int Dialogue::getVarInt(const std::string &name, int defaultValue, const std::string &scope) const {
221 const VarValue v = getVarValue(name, scope);
222 if (v.type() == VarValue::Type::Int) return int(v.asInt());
223 if (v.type() == VarValue::Type::Float) return int(v.asDouble());
224 if (v.type() == VarValue::Type::Bool) return v.asBool() ? 1 : 0;
225 return defaultValue;
226}
227
228float Dialogue::getVarFloat(const std::string &name, float defaultValue,
229 const std::string &scope) const {
230 const VarValue v = getVarValue(name, scope);
231 if (v.type() == VarValue::Type::Int) return float(v.asInt());
232 if (v.type() == VarValue::Type::Float) return float(v.asDouble());
233 if (v.type() == VarValue::Type::Bool) return v.asBool() ? 1.f : 0.f;
234 return defaultValue;
235}
236
237bool Dialogue::getVarBool(const std::string &name, bool defaultValue,
238 const std::string &scope) const {
239 const VarValue v = getVarValue(name, scope);
240 if (v.type() == VarValue::Type::Int) return v.asInt() != 0;
241 if (v.type() == VarValue::Type::Float) return v.asDouble() != 0.0;
242 if (v.type() == VarValue::Type::Bool) return v.asBool();
243 return defaultValue;
244}
245
246std::string Dialogue::getVarString(const std::string &name, const std::string &defaultValue,
247 const std::string &scope) const {
248 const VarValue v = getVarValue(name, scope);
249 return v.type() == VarValue::Type::String ? v.asString() : defaultValue;
250}
251
252bool Dialogue::hasVar(const std::string &name, const std::string &scope) const {
253 const auto *m = varsForScope(scope);
254 return m && m->find(name) != m->end();
255}
256
257bool Dialogue::clearVar(const std::string &name, const std::string &scope) {
258 auto *m = varsForScope(scope);
259 return m && m->erase(name) > 0;
260}
261
262void Dialogue::clearVars(const std::string &scope) {
263 if (scope == "global") {
264 globalVars_.clear();
265 } else if (scope == "scene") {
266 sceneVars_.clear();
267 } else if (scope == "all") {
268 globalVars_.clear();
269 sceneVars_.clear();
270 }
271}
272
273// ---------------------------------------------------------------------------
274// Conditions (structured tables + script predicates)
275// ---------------------------------------------------------------------------
276
277bool Dialogue::registerCondition(const std::string &name, ssq::Object fn) {
278 if (!vm_ || name.empty()) return false;
279 HSQOBJECT raw = fn.getRaw();
280 if (raw._type == OT_NULL) return unregisterCondition(name);
281 if (raw._type != OT_CLOSURE && raw._type != OT_NATIVECLOSURE) return false;
282 const auto it = predicates_.find(name);
283 if (it != predicates_.end()) {
284 sq_release(vm_, &it->second);
285 predicates_.erase(it);
286 }
287 sq_addref(vm_, &raw);
288 predicates_[name] = raw;
289 return true;
290}
291
292bool Dialogue::unregisterCondition(const std::string &name) {
293 const auto it = predicates_.find(name);
294 if (it == predicates_.end()) return false;
295 if (vm_) sq_release(vm_, &it->second);
296 predicates_.erase(it);
297 return true;
298}
299
300bool Dialogue::evalCondition(ssq::Object table) {
301 DataValue v;
302 if (!objectToDataValue(vm_, table, v)) return false;
303 return evalConditionData(v);
304}
305
306bool Dialogue::parseCondition(const DataValue &v, Condition &out, std::string &error) {
307 if (v.kind() == DataValue::Kind::Array) {
308 out.kind = Condition::Kind::All;
309 for (size_t i = 0; i < v.arraySize(); ++i) {
310 const auto &item = v.at(i);
311 Condition child;
312 if (!parseCondition(item, child, error)) return false;
313 out.children.push_back(std::move(child));
314 }
315 return true;
316 }
317 if (v.kind() != DataValue::Kind::Object) {
318 error = "condition must be a table";
319 return false;
320 }
321 if (const DataValue *script = v.find("script")) {
322 if (script->kind() != DataValue::Kind::String) {
323 error = "script condition name must be a string";
324 return false;
325 }
326 out.kind = Condition::Kind::Script;
327 out.script = script->asString();
328 return true;
329 }
330 if (const DataValue *all = v.find("all")) {
331 if (all->kind() != DataValue::Kind::Array) {
332 error = "all must be an array";
333 return false;
334 }
335 out.kind = Condition::Kind::All;
336 for (size_t i = 0; i < all->arraySize(); ++i) {
337 const auto &item = all->at(i);
338 Condition child;
339 if (!parseCondition(item, child, error)) return false;
340 out.children.push_back(std::move(child));
341 }
342 return true;
343 }
344 if (const DataValue *any = v.find("any")) {
345 if (any->kind() != DataValue::Kind::Array) {
346 error = "any must be an array";
347 return false;
348 }
349 out.kind = Condition::Kind::Any;
350 for (size_t i = 0; i < any->arraySize(); ++i) {
351 const auto &item = any->at(i);
352 Condition child;
353 if (!parseCondition(item, child, error)) return false;
354 out.children.push_back(std::move(child));
355 }
356 return true;
357 }
358 if (const DataValue *notc = v.find("not")) {
359 out.kind = Condition::Kind::Not;
360 out.children.emplace_back();
361 return parseCondition(*notc, out.children.back(), error);
362 }
363 if (const DataValue *var = v.find("var")) {
364 const DataValue *op = v.find("op");
365 if (var->kind() != DataValue::Kind::String || !op || op->kind() != DataValue::Kind::String) {
366 error = "var/op condition requires string var and op";
367 return false;
368 }
369 static const char *kOps[] = {"eq", "ne", "gt", "ge", "lt", "le", "has", "missing"};
370 const std::string opStr = op->asString();
371 if (std::find(std::begin(kOps), std::end(kOps), opStr) == std::end(kOps)) {
372 error = "unknown condition op: " + opStr;
373 return false;
374 }
375 out.kind = Condition::Kind::Cmp;
376 out.var = var->asString();
377 out.op = opStr;
378 out.value = VarValue::string("");
379 if (const DataValue *val = v.find("value")) {
380 switch (val->kind()) {
381 case DataValue::Kind::Int: out.value = VarValue::integer(val->asInt()); break;
382 case DataValue::Kind::Float: out.value = VarValue::number(val->asDouble()); break;
383 case DataValue::Kind::Bool: out.value = VarValue::boolean(val->asBool()); break;
384 case DataValue::Kind::String: out.value = VarValue::string(val->asString()); break;
385 default:
386 break;
387 }
388 }
389 return true;
390 }
391 error = "unrecognized condition table";
392 return false;
393}
394
395bool Dialogue::compareEq(const VarValue &a, const VarValue &b) const {
396 if (a.isNumeric() && b.isNumeric()) {
397 const double x = a.type() == VarValue::Type::Int ? double(a.asInt()) : a.asDouble();
398 const double y = b.type() == VarValue::Type::Int ? double(b.asInt()) : b.asDouble();
399 return x == y;
400 }
401 if (a.type() == VarValue::Type::Bool && b.type() == VarValue::Type::Bool) return a.asBool() == b.asBool();
402 if (a.type() == VarValue::Type::String && b.type() == VarValue::Type::String) return a.asString() == b.asString();
403 return false;
404}
405
406bool Dialogue::compareOrder(const std::string &op, const VarValue &a, const VarValue &b) const {
407 if (!a.isNumeric() || !b.isNumeric()) return false;
408 const double x = a.type() == VarValue::Type::Int ? double(a.asInt()) : a.asDouble();
409 const double y = b.type() == VarValue::Type::Int ? double(b.asInt()) : b.asDouble();
410 if (op == "gt") return x > y;
411 if (op == "ge") return x >= y;
412 if (op == "lt") return x < y;
413 if (op == "le") return x <= y;
414 return false;
415}
416
417bool Dialogue::evalConditionInternal(const Condition &c,
418 const std::unordered_map<std::string, VarValue> &merged,
419 const std::unordered_map<std::string, VarValue> &params,
420 const std::string &lineId) const {
421 switch (c.kind) {
422 case Condition::Kind::Always:
423 return true;
424 case Condition::Kind::Cmp: {
425 const auto it = merged.find(c.var);
426 const bool present = it != merged.end();
427 if (c.op == "has") return present;
428 if (c.op == "missing") return !present;
429 if (!present) return false;
430 if (c.op == "eq") return compareEq(it->second, c.value);
431 if (c.op == "ne") return !compareEq(it->second, c.value);
432 return compareOrder(c.op, it->second, c.value);
433 }
434 case Condition::Kind::All:
435 for (const auto &child : c.children)
436 if (!evalConditionInternal(child, merged, params, lineId)) return false;
437 return true;
438 case Condition::Kind::Any:
439 for (const auto &child : c.children)
440 if (evalConditionInternal(child, merged, params, lineId)) return true;
441 return false;
442 case Condition::Kind::Not:
443 return c.children.empty() ||
444 !evalConditionInternal(c.children.front(), merged, params, lineId);
445 case Condition::Kind::Script:
446 return evalScriptPredicate(c.script, merged, params, lineId);
447 }
448 return false;
449}
450
452 Condition c;
453 std::string error;
454 if (!parseCondition(cond, c, error)) return false;
455 return evalConditionInternal(c, mergedVars({}), {}, "");
456}
457
458bool Dialogue::evalScriptPredicate(const std::string &name,
459 const std::unordered_map<std::string, VarValue> &merged,
460 const std::unordered_map<std::string, VarValue> &params,
461 const std::string &lineId) const {
462 if (!vm_) return false;
463 const auto it = predicates_.find(name);
464 if (it == predicates_.end()) return false;
465
466 const SQInteger top = sq_gettop(vm_);
467 sq_pushobject(vm_, it->second); // closure first; sq_call expects fn below its args
468 sq_newtable(vm_); // ctx
469
470 sq_pushstring(vm_, "vars", -1);
471 pushVarTable(vm_, merged);
472 sq_newslot(vm_, -3, SQFalse); // fn at -4, key at -2, var table at -1 -> slot into ctx(-3)
473
474 sq_pushstring(vm_, "params", -1);
475 pushVarTable(vm_, params);
476 sq_newslot(vm_, -3, SQFalse);
477
478 sq_pushstring(vm_, "lineId", -1);
479 sq_pushstring(vm_, lineId.c_str(), lineId.size());
480 sq_newslot(vm_, -3, SQFalse);
481
482 if (SQ_FAILED(sq_call(vm_, 1, SQTrue, SQTrue))) {
483 sq_settop(vm_, top);
484 return false;
485 }
486 SQBool result = SQFalse;
487 if (SQ_FAILED(sq_getbool(vm_, -1, &result))) {
488 sq_settop(vm_, top);
489 return false;
490 }
491 sq_settop(vm_, top);
492 return result != 0;
493}
494
495// ---------------------------------------------------------------------------
496// Content pools
497// ---------------------------------------------------------------------------
498
499int Dialogue::loadPoolsFromTable(ssq::Object table) {
501 if (!objectToDataValue(vm_, table, root)) {
502 lastPoolsError_ = "pools root must be a table";
503 return 0;
504 }
505 return loadPoolsFromData(root);
506}
507
508bool Dialogue::parseLineData(const std::string &poolId, const DataValue &v, int index, Line &line,
509 std::string &error) {
510 if (v.kind() != DataValue::Kind::Object) {
511 error = "line must be a table";
512 return false;
513 }
514 if (const DataValue *id = v.find("id"))
515 if (id->kind() == DataValue::Kind::String) line.id = id->asString();
516 if (line.id.empty()) line.id = poolId + "." + std::to_string(index);
517
518 if (const DataValue *sp = v.find("speaker"))
519 if (sp->kind() == DataValue::Kind::String) line.speaker = sp->asString();
520 if (const DataValue *tx = v.find("text"))
521 if (tx->kind() == DataValue::Kind::String) line.text = tx->asString();
522 if (const DataValue *ik = v.find("i18n"))
523 if (ik->kind() == DataValue::Kind::String) line.i18nKey = ik->asString();
524 if (line.text.empty() && line.i18nKey.empty()) {
525 error = "line '" + line.id + "': text or i18n required";
526 return false;
527 }
528
529 if (const DataValue *w = v.find("weight")) {
530 if (w->kind() == DataValue::Kind::Int)
531 line.weight = double(w->asInt());
532 else if (w->kind() == DataValue::Kind::Float)
533 line.weight = w->asDouble();
534 else {
535 error = "line '" + line.id + "': weight must be a number";
536 return false;
537 }
538 }
539
540 if (const DataValue *when = v.find("when")) {
541 if (!parseCondition(*when, line.when, error)) {
542 error = "line '" + line.id + "': " + error;
543 return false;
544 }
545 }
546
547 if (const DataValue *meta = v.find("meta")) {
548 if (meta->kind() != DataValue::Kind::Object) {
549 error = "line '" + line.id + "': meta must be a table";
550 return false;
551 }
552 const auto *fields = meta->getIf<DataValue::Object>();
553 for (const auto &mkv : *fields) {
554 std::string val;
555 switch (mkv.second.kind()) {
556 case DataValue::Kind::String: val = mkv.second.asString(); break;
557 case DataValue::Kind::Int: val = std::to_string(mkv.second.asInt()); break;
558 case DataValue::Kind::Float: val = floatToString(mkv.second.asDouble()); break;
559 case DataValue::Kind::Bool: val = mkv.second.asBool() ? "true" : "false"; break;
560 default:
561 break;
562 }
563 line.meta[mkv.first] = val;
564 }
565 }
566
567 if (const DataValue *tags = v.find("tags")) {
568 if (tags->kind() != DataValue::Kind::Array) {
569 error = "line '" + line.id + "': tags must be an array";
570 return false;
571 }
572 for (size_t i = 0; i < tags->arraySize(); ++i) {
573 const auto &t = tags->at(i);
574 if (t.kind() == DataValue::Kind::String) line.tags.push_back(t.asString());
575 }
576 }
577 return true;
578}
579
580int Dialogue::loadPoolsFromData(const DataValue &root) { return applyPoolsFromData(root, false); }
581
582int Dialogue::replacePoolsFromData(const DataValue& root) { return applyPoolsFromData(root, true); }
583
584int Dialogue::applyPoolsFromData(const DataValue &root, bool replace) {
585 lastPoolsError_.clear();
586 if (root.kind() != DataValue::Kind::Object) {
587 lastPoolsError_ = "pools root must be an object";
588 return 0;
589 }
590 const DataValue *pools = root.find("pools");
591 if (!pools || pools->kind() != DataValue::Kind::Object) {
592 lastPoolsError_ = "missing pools object";
593 return 0;
594 }
595
596 int registered = 0;
597 std::vector<Pool> candidate = replace ? std::vector<Pool>{} : pools_;
598 const auto *poolFields = pools->getIf<DataValue::Object>();
599 for (const auto &kv : *poolFields) {
600 Pool pool;
601 pool.id = kv.first;
602 if (kv.second.kind() != DataValue::Kind::Object) {
603 lastPoolsError_ = "pool '" + kv.first + "': expected object";
604 return 0;
605 }
606 if (const DataValue *nr = kv.second.find("noRepeat")) {
607 if (nr->kind() == DataValue::Kind::Int)
608 pool.noRepeat = int(nr->asInt());
609 else if (nr->kind() == DataValue::Kind::Float)
610 pool.noRepeat = int(nr->asDouble());
611 if (pool.noRepeat < 0) pool.noRepeat = 0;
612 }
613 const DataValue *lines = kv.second.find("lines");
614 if (!lines || lines->kind() != DataValue::Kind::Array) {
615 lastPoolsError_ = "pool '" + kv.first + "': missing lines array";
616 return 0;
617 }
618 int lineIndex = 1;
619 for (size_t i = 0; i < lines->arraySize(); ++i) {
620 const auto &lv = lines->at(i);
621 Line line;
622 std::string error;
623 if (!parseLineData(pool.id, lv, lineIndex, line, error)) {
624 lastPoolsError_ = error;
625 return 0;
626 }
627 pool.lines.push_back(std::move(line));
628 ++lineIndex;
629 }
630 auto existing = std::find_if(candidate.begin(), candidate.end(),
631 [&](const Pool& item) { return item.id == pool.id; });
632 if (existing != candidate.end()) *existing = std::move(pool);
633 else candidate.push_back(std::move(pool));
634 ++registered;
635 }
636 pools_ = std::move(candidate);
637 return registered;
638}
639
640void Dialogue::clearPools() { pools_.clear(); }
641
642int Dialogue::getPoolCount() const { return int(pools_.size()); }
643
644std::string Dialogue::getPoolId(int index) const {
645 if (index < 0 || size_t(index) >= pools_.size()) return {};
646 return pools_[size_t(index)].id;
647}
648
649bool Dialogue::hasPool(const std::string &id) const { return findPool(id) != nullptr; }
650
651Dialogue::Pool *Dialogue::findPool(const std::string &id) {
652 for (auto &p : pools_)
653 if (p.id == id) return &p;
654 return nullptr;
655}
656
657const Dialogue::Pool *Dialogue::findPool(const std::string &id) const {
658 for (const auto &p : pools_)
659 if (p.id == id) return &p;
660 return nullptr;
661}
662
663Dialogue::Line *Dialogue::findLine(const std::string &id) {
664 for (auto &p : pools_)
665 for (auto &l : p.lines)
666 if (l.id == id) return &l;
667 return nullptr;
668}
669
670const Dialogue::Line *Dialogue::findLine(const std::string &id) const {
671 for (const auto &p : pools_)
672 for (const auto &l : p.lines)
673 if (l.id == id) return &l;
674 return nullptr;
675}
676
677// ---------------------------------------------------------------------------
678// Merged variable context / interpolation
679// ---------------------------------------------------------------------------
680
681std::unordered_map<std::string, Dialogue::VarValue> Dialogue::mergedVars(
682 const std::unordered_map<std::string, VarValue> &params) const {
683 std::unordered_map<std::string, VarValue> m = globalVars_;
684 for (const auto &kv : sceneVars_) m[kv.first] = kv.second;
685 for (const auto &kv : params) m[kv.first] = kv.second;
686 return m;
687}
688
689std::unordered_map<std::string, std::string> Dialogue::stringParams(
690 const std::unordered_map<std::string, VarValue> &vars) const {
691 std::unordered_map<std::string, std::string> out;
692 for (const auto &kv : vars) out[kv.first] = kv.second.toString();
693 return out;
694}
695
696std::string Dialogue::interpolate(const std::string &tpl,
697 const std::unordered_map<std::string, VarValue> &vars) const {
698 std::string out;
699 out.reserve(tpl.size());
700 for (size_t i = 0; i < tpl.size();) {
701 if (tpl[i] == '{') {
702 const size_t close = tpl.find('}', i + 1);
703 if (close != std::string::npos) {
704 const std::string name = tpl.substr(i + 1, close - i - 1);
705 const auto it = vars.find(name);
706 if (it != vars.end()) {
707 out += it->second.toString();
708 i = close + 1;
709 continue;
710 }
711 }
712 }
713 out += tpl[i++];
714 }
715 return out;
716}
717
718// ---------------------------------------------------------------------------
719// RNG / weighted selection / play
720// ---------------------------------------------------------------------------
721
722uint32_t Dialogue::nextRandom() {
723 uint32_t x = rngState_;
724 x ^= x << 13;
725 x ^= x >> 17;
726 x ^= x << 5;
727 rngState_ = x;
728 return x;
729}
730
731double Dialogue::nextUnit() { return (nextRandom() & 0xFFFFFFu) / 16777216.0; }
732
734 rngState_ = seed ? uint32_t(seed) : 1u;
735 for (auto &p : pools_) p.recent.clear();
736}
737
738std::string Dialogue::pickLine(const std::string &poolId, ssq::Object params) {
739 std::unordered_map<std::string, VarValue> p;
740 objectToVarParams(vm_, params, p);
741 return pickLineWithParams(poolId, p);
742}
743
745 const std::string &poolId, const std::unordered_map<std::string, VarValue> &params) {
746 Pool *pool = findPool(poolId);
747 if (!pool) return "";
748 const auto merged = mergedVars(params);
749
750 auto collect = [&](std::vector<size_t> &out, bool allowRecent) {
751 for (size_t i = 0; i < pool->lines.size(); ++i) {
752 const Line &line = pool->lines[i];
753 if (line.weight <= 0.0) continue;
754 if (!evalConditionInternal(line.when, merged, params, line.id)) continue;
755 if (!allowRecent && pool->noRepeat > 0 &&
756 std::find(pool->recent.begin(), pool->recent.end(), i) != pool->recent.end())
757 continue;
758 out.push_back(i);
759 }
760 };
761
762 std::vector<size_t> candidates;
763 collect(candidates, false);
764 if (candidates.empty()) collect(candidates, true); // noRepeat must not deadlock
765 if (candidates.empty()) return "";
766
767 double total = 0.0;
768 for (const size_t i : candidates) total += pool->lines[i].weight;
769 double roll = nextUnit() * total;
770 size_t picked = candidates.front();
771 double acc = 0.0;
772 for (const size_t i : candidates) {
773 acc += pool->lines[i].weight;
774 if (roll < acc) {
775 picked = i;
776 break;
777 }
778 }
779 if (pool->noRepeat > 0) {
780 pool->recent.push_back(picked);
781 if (pool->recent.size() > size_t(pool->noRepeat)) pool->recent.erase(pool->recent.begin());
782 }
783 return pool->lines[picked].id;
784}
785
786bool Dialogue::playLine(const std::string &lineId, ssq::Object params) {
787 std::unordered_map<std::string, VarValue> p;
788 objectToVarParams(vm_, params, p);
789 return playLineWithParams(lineId, p);
790}
791
792bool Dialogue::playLineWithParams(const std::string &lineId,
793 const std::unordered_map<std::string, VarValue> &params) {
794 Line *line = findLine(lineId);
795 if (!line) return false;
796 const auto merged = mergedVars(params);
797
798 std::string text;
799 if (!line->text.empty()) {
800 text = interpolate(line->text, merged);
801 } else {
802 auto *i18n = eve::ModuleManager::getInstance<eve::i18n::I18n>("I18n");
803 text = i18n ? i18n->getWithParams(line->i18nKey, stringParams(merged)) : line->i18nKey;
804 }
805
806 if (line->speaker.empty()) narrate(text);
807 else say(line->speaker, text);
808
809 currentLineId_ = line->id;
810 currentLineMeta_ = line->meta;
811 currentLineTags_ = line->tags;
812 applyLineMeta(*line);
813 return true;
814}
815
816bool Dialogue::playPool(const std::string &poolId, ssq::Object params) {
817 std::unordered_map<std::string, VarValue> p;
818 objectToVarParams(vm_, params, p);
819 return playPoolWithParams(poolId, p);
820}
821
822bool Dialogue::playPoolWithParams(const std::string &poolId,
823 const std::unordered_map<std::string, VarValue> &params) {
824 const std::string lineId = pickLineWithParams(poolId, params);
825 return !lineId.empty() && playLineWithParams(lineId, params);
826}
827
828std::string Dialogue::getCurrentLineMeta(const std::string &field) const {
829 const auto it = currentLineMeta_.find(field);
830 return it == currentLineMeta_.end() ? std::string{} : it->second;
831}
832
833std::vector<std::string> Dialogue::getCurrentLineTags() const { return currentLineTags_; }
834
835void Dialogue::applyLineMeta(const Line &line) {
836 if (line.speaker.empty()) return;
837 const auto expr = line.meta.find("expression");
838 const auto motion = line.meta.find("motion");
839 if (expr == line.meta.end() && motion == line.meta.end()) return;
840 Character *c = findCharacter(line.speaker);
841 if (!c || !c->avatar) return;
842 if (expr != line.meta.end()) c->avatar->setExpression(expr->second);
843 if (motion != line.meta.end()) c->avatar->setMotion(motion->second);
844}
845
846// ---------------------------------------------------------------------------
847// Scene-scoped variables: clear when the selected Scene host changes
848// ---------------------------------------------------------------------------
849
850void Dialogue::pollSceneChange() {
851 auto *scn = eve::ModuleManager::getInstance<eve::scene::Scene>("Scene");
852 if (!scn) return;
853 eve::scene::SceneHost *host = scn->current();
854 const std::string name = host ? host->getName() : "";
855 if (!name.empty() && name != lastSceneName_) sceneVars_.clear();
856 if (!name.empty()) lastSceneName_ = name;
857}
858
859Dialogue::Character *Dialogue::findCharacter(const std::string &id) {
860 for (auto &c : characters_)
861 if (c.id == id) return &c;
862 return nullptr;
863}
864
865const Dialogue::Character *Dialogue::findCharacter(const std::string &id) const {
866 for (const auto &c : characters_)
867 if (c.id == id) return &c;
868 return nullptr;
869}
870
871bool Dialogue::registerCharacter(const std::string &id, const std::string &displayName) {
872 if (id.empty()) return false;
873 if (auto *c = findCharacter(id)) {
874 c->displayName = displayName.empty() ? id : displayName;
875 return true;
876 }
877 Character c;
878 c.id = id;
879 c.displayName = displayName.empty() ? id : displayName;
880 characters_.push_back(c);
881 return true;
882}
883
884bool Dialogue::hasCharacter(const std::string &id) const { return findCharacter(id) != nullptr; }
885
886std::string Dialogue::getDisplayName(const std::string &id) const {
887 const Character *c = findCharacter(id);
888 return c ? c->displayName : std::string{};
889}
890
891bool Dialogue::bindAvatar(const std::string &id, avatar::AvatarInstance *av) {
892 Character *c = findCharacter(id);
893 if (!c) return false;
894 // Drop the previous binding's destroy hook before replacing the pointer,
895 // otherwise the old avatar's hook would null the new binding on destroy.
896 if (c->avatar && c->avatarHook) {
897 c->avatar->removeDestroyHook(*c->avatarHook);
898 c->avatarHook.reset();
899 }
900 c->avatar = av;
901 if (av) {
902 const std::string key = id;
903 c->avatarHook = av->addDestroyHook([this, key](avatar::AvatarInstance *) {
904 if (Character *ch = findCharacter(key))
905 ch->avatar = nullptr;
906 });
907 }
908 return true;
909}
910
911avatar::AvatarInstance *Dialogue::getAvatar(const std::string &id) const {
912 const Character *c = findCharacter(id);
913 return c ? c->avatar : nullptr;
914}
915
916int Dialogue::getCharacterCount() const { return int(characters_.size()); }
917
918std::string Dialogue::getCharacterId(int index) const {
919 if (index < 0 || size_t(index) >= characters_.size()) return {};
920 return characters_[size_t(index)].id;
921}
922
923bool Dialogue::show(const std::string &id, const std::string &slot) {
924 Character *c = findCharacter(id);
925 if (!c) return false;
926 c->shown = true;
927 c->slot = slot.empty() ? "center" : slot;
928 if (c->avatar) c->avatar->setVisible(true);
929 return true;
930}
931
932bool Dialogue::hide(const std::string &id) {
933 Character *c = findCharacter(id);
934 if (!c) return false;
935 c->shown = false;
936 if (c->avatar) c->avatar->setVisible(false);
937 return true;
938}
939
940bool Dialogue::isShown(const std::string &id) const {
941 const Character *c = findCharacter(id);
942 return c && c->shown;
943}
944
945std::string Dialogue::getSlot(const std::string &id) const {
946 const Character *c = findCharacter(id);
947 return c ? c->slot : std::string{};
948}
949
950void Dialogue::setSlotX(const std::string &slot, float xNorm) {
951 if (slot.empty()) return;
952 slotX_[slot] = xNorm;
953}
954
955float Dialogue::getSlotX(const std::string &slot) const {
956 auto it = slotX_.find(slot);
957 if (it != slotX_.end()) return it->second;
958 if (slot == "left") return 0.25f;
959 if (slot == "right") return 0.75f;
960 if (slot == "center") return 0.5f;
961 return 0.5f;
962}
963
964bool Dialogue::setExpression(const std::string &id, const std::string &expression) {
965 Character *c = findCharacter(id);
966 if (!c || !c->avatar) return false;
967 c->avatar->setExpression(expression);
968 return true;
969}
970
971bool Dialogue::setMotion(const std::string &id, const std::string &motion) {
972 Character *c = findCharacter(id);
973 if (!c || !c->avatar) return false;
974 c->avatar->setMotion(motion);
975 return true;
976}
977
978void Dialogue::syncStage(float stageWidth, float stageHeight) {
979 (void)stageHeight;
980 for (Character &c : characters_) {
981 if (!c.avatar) continue;
982 c.avatar->setVisible(c.shown);
983 if (!c.shown) {
984 c.avatar->sync();
985 continue;
986 }
987 const float xn = getSlotX(c.slot);
988 const float x = xn * stageWidth;
989 // Keep current Y; only drive X from slot.
990 c.avatar->setPosition(x, c.avatar->getY());
991 c.avatar->sync();
992 }
993}
994
995void Dialogue::beginLine(const std::string &speakerId, const std::string &text) {
996 speakerId_ = speakerId;
997 fullText_ = text;
998 typed_ = 0.f;
999 selectedChoiceId_.clear();
1000 lipSyncTime_ = 0.f;
1001 if (typeSpeed_ <= 0.f) {
1002 typed_ = float(utf8_codepoint_count(fullText_));
1003 phase_ = Phase::WaitingAdvance;
1004 lipSyncValue_ = 0.f;
1005 } else {
1006 phase_ = Phase::Typing;
1007 }
1008}
1009
1010void Dialogue::say(const std::string &speakerId, const std::string &text) {
1011 beginLine(speakerId, text);
1012}
1013
1014void Dialogue::narrate(const std::string &text) { beginLine("", text); }
1015
1016void Dialogue::setTypeSpeed(float charsPerSecond) { typeSpeed_ = charsPerSecond; }
1017
1019 if (phase_ == Phase::Typing) {
1020 typed_ = float(utf8_codepoint_count(fullText_));
1021 phase_ = Phase::WaitingAdvance;
1022 }
1023}
1024
1025bool Dialogue::isTyping() const { return phase_ == Phase::Typing; }
1026
1027bool Dialogue::isWaitingAdvance() const { return phase_ == Phase::WaitingAdvance; }
1028
1029bool Dialogue::isIdle() const { return phase_ == Phase::Idle; }
1030
1032 if (phase_ == Phase::Typing) {
1033 skipTyping();
1034 return;
1035 }
1036 if (phase_ == Phase::WaitingAdvance) phase_ = Phase::Idle;
1037}
1038
1039std::string Dialogue::getSpeakerName() const {
1040 if (speakerId_.empty()) return {};
1041 return getDisplayName(speakerId_);
1042}
1043
1044std::string Dialogue::getVisibleText() const {
1045 if (fullText_.empty()) return {};
1046 const size_t total = utf8_codepoint_count(fullText_);
1047 size_t n = size_t(std::floor(typed_ + 1e-4f));
1048 if (n == 0) return {};
1049 if (n >= total) return fullText_;
1050 return fullText_.substr(0, utf8_byte_offset_for_codepoints(fullText_, n));
1051}
1052
1053std::string Dialogue::getPhase() const {
1054 switch (phase_) {
1055 case Phase::Idle:
1056 return "idle";
1057 case Phase::Typing:
1058 return "typing";
1059 case Phase::WaitingAdvance:
1060 return "waiting_advance";
1061 case Phase::WaitingChoice:
1062 return "waiting_choice";
1063 }
1064 return "idle";
1065}
1066
1068 choices_.clear();
1069 selectedChoiceId_.clear();
1070}
1071
1072bool Dialogue::addChoice(const std::string &id, const std::string &label) {
1073 if (id.empty()) return false;
1074 for (auto &ch : choices_) {
1075 if (ch.id == id) {
1076 ch.label = label;
1077 return true;
1078 }
1079 }
1080 choices_.push_back(Choice{id, label});
1081 return true;
1082}
1083
1085 if (choices_.empty()) {
1086 phase_ = Phase::Idle;
1087 return;
1088 }
1089 if (phase_ == Phase::Typing) skipTyping();
1090 phase_ = Phase::WaitingChoice;
1091}
1092
1093bool Dialogue::isWaitingChoice() const { return phase_ == Phase::WaitingChoice; }
1094
1095int Dialogue::getChoiceCount() const { return int(choices_.size()); }
1096
1097std::string Dialogue::getChoiceId(int index) const {
1098 if (index < 0 || size_t(index) >= choices_.size()) return {};
1099 return choices_[size_t(index)].id;
1100}
1101
1102std::string Dialogue::getChoiceLabel(int index) const {
1103 if (index < 0 || size_t(index) >= choices_.size()) return {};
1104 return choices_[size_t(index)].label;
1105}
1106
1108 if (phase_ != Phase::WaitingChoice) return false;
1109 if (index < 0 || size_t(index) >= choices_.size()) return false;
1110 selectedChoiceId_ = choices_[size_t(index)].id;
1111 phase_ = Phase::Idle;
1112 return true;
1113}
1114
1115void Dialogue::setLipSyncEnabled(bool enabled) { lipSyncEnabled_ = enabled; }
1116
1117void Dialogue::setLipSyncParameter(const std::string &name) {
1118 if (!name.empty()) lipSyncParameter_ = name;
1119}
1120
1121void Dialogue::setLipSyncAmplitude(float amplitude) {
1122 if (amplitude < 0.f) amplitude = 0.f;
1123 if (amplitude > 2.f) amplitude = 2.f;
1124 lipSyncAmplitude_ = amplitude;
1125}
1126
1127void Dialogue::updateLipSync(float dt) {
1128 if (dt < 0.f) dt = 0.f;
1129 if (lipSyncEnabled_ && phase_ == Phase::Typing && !speakerId_.empty()) {
1130 lipSyncTime_ += dt;
1131 // Simple mouth envelope while characters appear (no audio dependency).
1132 const float wave = std::fabs(std::sin(lipSyncTime_ * 14.f));
1133 lipSyncValue_ = lipSyncAmplitude_ * (0.25f + 0.75f * wave);
1134 } else {
1135 // Ease shut when not typing.
1136 lipSyncValue_ *= std::max(0.f, 1.f - dt * 8.f);
1137 if (lipSyncValue_ < 0.01f) lipSyncValue_ = 0.f;
1138 }
1139 applyLipSyncToSpeaker();
1140}
1141
1142void Dialogue::applyLipSyncToSpeaker() {
1143 if (!lipSyncEnabled_ || speakerId_.empty() || lipSyncParameter_.empty()) return;
1144 Character *c = findCharacter(speakerId_);
1145 if (!c || !c->avatar) return;
1146 c->avatar->setParameter(lipSyncParameter_, lipSyncValue_);
1147}
1148
1149void Dialogue::update(float dt) {
1150 if (dt < 0.f) dt = 0.f;
1151 if (phase_ == Phase::Typing) {
1152 const float total = float(utf8_codepoint_count(fullText_));
1153 typed_ += typeSpeed_ * dt;
1154 if (typed_ >= total) {
1155 typed_ = total;
1156 phase_ = Phase::WaitingAdvance;
1157 }
1158 }
1159 updateLipSync(dt);
1160 pollSceneChange();
1161}
1162
1164 phase_ = Phase::Idle;
1165 speakerId_.clear();
1166 fullText_.clear();
1167 typed_ = 0.f;
1168 choices_.clear();
1169 selectedChoiceId_.clear();
1170 lipSyncValue_ = 0.f;
1171 lipSyncTime_ = 0.f;
1172 currentLineId_.clear();
1173 currentLineMeta_.clear();
1174 currentLineTags_.clear();
1175 for (Character &c : characters_) {
1176 c.shown = false;
1177 if (c.avatar) c.avatar->setVisible(false);
1178 }
1179}
1180
1182 out = StateValue::object();
1183 out.set("phase", StateValue::string(getPhase()));
1184 out.set("speakerId", StateValue::string(speakerId_));
1185 out.set("fullText", StateValue::string(fullText_));
1186 out.set("typed", StateValue::number(typed_));
1187 out.set("typeSpeed", StateValue::number(typeSpeed_));
1188 out.set("lipSyncEnabled", StateValue::boolean(lipSyncEnabled_));
1189 out.set("lipSyncParameter", StateValue::string(lipSyncParameter_));
1190 out.set("lipSyncAmplitude", StateValue::number(lipSyncAmplitude_));
1191 out.set("rngState", StateValue::integer(static_cast<int64_t>(rngState_)));
1192 out.set("currentLineId", StateValue::string(currentLineId_));
1193 out.set("selectedChoiceId", StateValue::string(selectedChoiceId_));
1194
1196 for (const auto& kv : globalVars_) global.set(kv.first, varValueToState(kv.second));
1197 out.set("globalVars", std::move(global));
1198
1200 for (const auto& kv : sceneVars_) scene.set(kv.first, varValueToState(kv.second));
1201 out.set("sceneVars", std::move(scene));
1202
1204 for (const auto& c : choices_) {
1206 item.set("id", StateValue::string(c.id));
1207 item.set("label", StateValue::string(c.label));
1208 choices.pushBack(std::move(item));
1209 }
1210 out.set("choices", std::move(choices));
1211
1212 StateValue chars = StateValue::array();
1213 for (const auto& c : characters_) {
1215 item.set("id", StateValue::string(c.id));
1216 item.set("displayName", StateValue::string(c.displayName));
1217 item.set("slot", StateValue::string(c.slot));
1218 item.set("shown", StateValue::boolean(c.shown));
1219 chars.pushBack(std::move(item));
1220 }
1221 out.set("characters", std::move(chars));
1222
1224 for (const auto& kv : slotX_) slotX.set(kv.first, StateValue::number(kv.second));
1225 out.set("slotX", std::move(slotX));
1226 return true;
1227}
1228
1229bool Dialogue::restoreState(const StateValue& in, std::string* err) {
1230 if (!in.isObject()) {
1231 if (err) *err = "dialogue: state is not an object";
1232 return false;
1233 }
1234 const StateValue* phase = in.find("phase");
1235 if (!phase || !phase->isString()) {
1236 if (err) *err = "dialogue: missing phase";
1237 return false;
1238 }
1239 const std::string phaseName = phase->asString();
1240 if (phaseName == "idle")
1241 phase_ = Phase::Idle;
1242 else if (phaseName == "typing")
1243 phase_ = Phase::Typing;
1244 else if (phaseName == "waiting_advance")
1245 phase_ = Phase::WaitingAdvance;
1246 else if (phaseName == "waiting_choice")
1247 phase_ = Phase::WaitingChoice;
1248 else {
1249 if (err) *err = "dialogue: unknown phase '" + phaseName + "'";
1250 return false;
1251 }
1252
1253 if (const StateValue* v = in.find("speakerId"); v && v->isString()) speakerId_ = v->asString();
1254 if (const StateValue* v = in.find("fullText"); v && v->isString()) fullText_ = v->asString();
1255 if (const StateValue* v = in.find("typed"); v && (v->isInt() || v->isFloat()))
1256 typed_ = static_cast<float>(v->isInt() ? double(v->asInt()) : v->asDouble());
1257 if (const StateValue* v = in.find("typeSpeed"); v && (v->isInt() || v->isFloat()))
1258 typeSpeed_ = static_cast<float>(v->isInt() ? double(v->asInt()) : v->asDouble());
1259 if (const StateValue* v = in.find("lipSyncEnabled"); v && v->isBool()) lipSyncEnabled_ = v->asBool();
1260 if (const StateValue* v = in.find("lipSyncParameter"); v && v->isString()) lipSyncParameter_ = v->asString();
1261 if (const StateValue* v = in.find("lipSyncAmplitude"); v && (v->isInt() || v->isFloat()))
1262 lipSyncAmplitude_ = static_cast<float>(v->isInt() ? double(v->asInt()) : v->asDouble());
1263 if (const StateValue* v = in.find("rngState"); v && v->isInt()) rngState_ = static_cast<uint32_t>(v->asInt());
1264 if (const StateValue* v = in.find("currentLineId"); v && v->isString()) currentLineId_ = v->asString();
1265 if (const StateValue* v = in.find("selectedChoiceId"); v && v->isString()) selectedChoiceId_ = v->asString();
1266
1267 globalVars_.clear();
1268 if (const StateValue* vars = in.find("globalVars"); vars && vars->isObject()) {
1269 for (const auto& key : vars->keys()) {
1271 if (stateToVarValue(*vars->find(key), val)) globalVars_[key] = val;
1272 }
1273 }
1274 sceneVars_.clear();
1275 if (const StateValue* vars = in.find("sceneVars"); vars && vars->isObject()) {
1276 for (const auto& key : vars->keys()) {
1278 if (stateToVarValue(*vars->find(key), val)) sceneVars_[key] = val;
1279 }
1280 }
1281
1282 choices_.clear();
1283 if (const StateValue *choices = in.find("choices"); choices && choices->isArray()) {
1284 for (size_t i = 0; i < choices->arraySize(); ++i) {
1285 const StateValue &item = choices->at(i);
1286 const StateValue *id = item.find("id");
1287 const StateValue *label = item.find("label");
1288 if (id && id->isString() && label && label->isString()) {
1289 choices_.push_back(Choice{id->asString(), label->asString()});
1290 }
1291 }
1292 }
1293
1294 if (const StateValue *chars = in.find("characters"); chars && chars->isArray()) {
1295 for (size_t i = 0; i < chars->arraySize(); ++i) {
1296 const StateValue &item = chars->at(i);
1297 const StateValue *id = item.find("id");
1298 if (!id || !id->isString()) continue;
1299 Character *c = findCharacter(id->asString());
1300 if (!c) {
1301 characters_.push_back(Character{});
1302 c = &characters_.back();
1303 c->id = id->asString();
1304 if (const StateValue *name = item.find("displayName"); name && name->isString())
1305 c->displayName = name->asString();
1306 }
1307 if (const StateValue *slot = item.find("slot"); slot && slot->isString())
1308 c->slot = slot->asString();
1309 if (const StateValue *shown = item.find("shown"); shown && shown->isBool()) {
1310 c->shown = shown->asBool();
1311 if (c->avatar) c->avatar->setVisible(c->shown);
1312 }
1313 }
1314 }
1315
1316 slotX_.clear();
1317 if (const StateValue *slots = in.find("slotX"); slots && slots->isObject()) {
1318 for (const auto &key : slots->keys()) {
1319 const StateValue *v = slots->find(key);
1320 if (v && (v->isInt() || v->isFloat()))
1321 slotX_[key] = static_cast<float>(v->isInt() ? double(v->asInt()) : v->asDouble());
1322 }
1323 }
1324 return true;
1325}
1326
1328 reset();
1329 return true;
1330}
1331
1332void Dialogue::expose(ssq::Table &table) {
1333 if (Dialogue *self = Dialogue::create()) self->vm_ = table.getHandle();
1334 auto cls = table.addClass(name, Dialogue::create, false);
1335 expose(cls);
1336}
1337
1338void Dialogue::expose(ssq::Class &cls) {
1339 cls.addFunc("getName", &Dialogue::getName);
1340 cls.addFunc("registerCharacter", &Dialogue::registerCharacter);
1341 cls.addFunc("hasCharacter", &Dialogue::hasCharacter);
1342 cls.addFunc("getDisplayName", &Dialogue::getDisplayName);
1343 cls.addFunc("bindAvatar", &Dialogue::bindAvatar);
1344 cls.addFunc("getAvatar", &Dialogue::getAvatar);
1345 cls.addFunc("getCharacterCount", &Dialogue::getCharacterCount);
1346 cls.addFunc("getCharacterId", &Dialogue::getCharacterId);
1347
1348 cls.addFunc("show", &Dialogue::show);
1349 cls.addFunc("hide", &Dialogue::hide);
1350 cls.addFunc("isShown", &Dialogue::isShown);
1351 cls.addFunc("getSlot", &Dialogue::getSlot);
1352 cls.addFunc("setSlotX", &Dialogue::setSlotX);
1353 cls.addFunc("getSlotX", &Dialogue::getSlotX);
1354 cls.addFunc("setExpression", &Dialogue::setExpression);
1355 cls.addFunc("setMotion", &Dialogue::setMotion);
1356 cls.addFunc("syncStage", &Dialogue::syncStage);
1357
1358 cls.addFunc("say", &Dialogue::say);
1359 cls.addFunc("narrate", &Dialogue::narrate);
1360 cls.addFunc("setTypeSpeed", &Dialogue::setTypeSpeed);
1361 cls.addFunc("getTypeSpeed", &Dialogue::getTypeSpeed);
1362 cls.addFunc("skipTyping", &Dialogue::skipTyping);
1363 cls.addFunc("isTyping", &Dialogue::isTyping);
1364 cls.addFunc("isWaitingAdvance", &Dialogue::isWaitingAdvance);
1365 cls.addFunc("isIdle", &Dialogue::isIdle);
1366 cls.addFunc("advance", &Dialogue::advance);
1367 cls.addFunc("getSpeakerId", &Dialogue::getSpeakerId);
1368 cls.addFunc("getSpeakerName", &Dialogue::getSpeakerName);
1369 cls.addFunc("getFullText", &Dialogue::getFullText);
1370 cls.addFunc("getVisibleText", &Dialogue::getVisibleText);
1371 cls.addFunc("getPhase", &Dialogue::getPhase);
1372
1373 cls.addFunc("setLipSyncEnabled", &Dialogue::setLipSyncEnabled);
1374 cls.addFunc("isLipSyncEnabled", &Dialogue::isLipSyncEnabled);
1375 cls.addFunc("setLipSyncParameter", &Dialogue::setLipSyncParameter);
1376 cls.addFunc("getLipSyncParameter", &Dialogue::getLipSyncParameter);
1377 cls.addFunc("setLipSyncAmplitude", &Dialogue::setLipSyncAmplitude);
1378 cls.addFunc("getLipSyncAmplitude", &Dialogue::getLipSyncAmplitude);
1379 cls.addFunc("getLipSyncValue", &Dialogue::getLipSyncValue);
1380
1381 cls.addFunc("clearChoices", &Dialogue::clearChoices);
1382 cls.addFunc("addChoice", &Dialogue::addChoice);
1383 cls.addFunc("presentChoices", &Dialogue::presentChoices);
1384 cls.addFunc("isWaitingChoice", &Dialogue::isWaitingChoice);
1385 cls.addFunc("getChoiceCount", &Dialogue::getChoiceCount);
1386 cls.addFunc("getChoiceId", &Dialogue::getChoiceId);
1387 cls.addFunc("getChoiceLabel", &Dialogue::getChoiceLabel);
1388 cls.addFunc("selectChoice", &Dialogue::selectChoice);
1389 cls.addFunc("getSelectedChoiceId", &Dialogue::getSelectedChoiceId);
1390
1391 cls.addFunc("setVar", &Dialogue::setVar);
1392 cls.addFunc("getVarType", &Dialogue::getVarType);
1393 cls.addFunc("getVarInt", &Dialogue::getVarInt);
1394 cls.addFunc("getVarFloat", &Dialogue::getVarFloat);
1395 cls.addFunc("getVarBool", &Dialogue::getVarBool);
1396 cls.addFunc("getVarString", &Dialogue::getVarString);
1397 cls.addFunc("hasVar", &Dialogue::hasVar);
1398 cls.addFunc("clearVar", &Dialogue::clearVar);
1399 cls.addFunc("clearVars", &Dialogue::clearVars);
1400
1401 cls.addFunc("registerCondition", &Dialogue::registerCondition);
1402 cls.addFunc("unregisterCondition", &Dialogue::unregisterCondition);
1403 cls.addFunc("evalCondition", &Dialogue::evalCondition);
1404
1405 cls.addFunc("clearPools", &Dialogue::clearPools);
1406 cls.addFunc("getPoolCount", &Dialogue::getPoolCount);
1407 cls.addFunc("getPoolId", &Dialogue::getPoolId);
1408 cls.addFunc("hasPool", &Dialogue::hasPool);
1409 cls.addFunc("getLastPoolsError", &Dialogue::getLastPoolsError);
1410
1411 cls.addFunc("setRandomSeed", &Dialogue::setRandomSeed);
1412 cls.addFunc("getRandomSeed", &Dialogue::getRandomSeed);
1413
1414 cls.addFunc("pickLine", &Dialogue::pickLine);
1415 cls.addFunc("playLine", &Dialogue::playLine);
1416 cls.addFunc("playPool", &Dialogue::playPool);
1417 cls.addFunc("getCurrentLineId", &Dialogue::getCurrentLineId);
1418 cls.addFunc("getCurrentLineMeta", &Dialogue::getCurrentLineMeta);
1419 cls.addFunc("getCurrentLineTags", &Dialogue::getCurrentLineTags);
1420
1421 cls.addFunc("update", &Dialogue::update);
1422 cls.addFunc("reset", &Dialogue::reset);
1423}
1424
1425} // namespace eve::dialogue
double value
SQInteger top
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::vector< BuildingInstanceSnapshot > instances
float phase
Definition CaveMesh.cpp:58
struct SQVM * HSQUIRRELVM
glm::vec4 p[6]
std::string label
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t second
std::int32_t c
std::int32_t first
JobScope scope
std::string text
std::set< std::string > choices
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
std::string error
Definition Package.cpp:60
std::string script
Definition PlayHost.cpp:116
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
std::weak_ptr< PrimitiveScene > scene
float begin
float d
float t
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
The single Squirrel projection for common Result, Status and Value.
Heightmap global
int children
Definition TreeMesh.cpp:295
float size
Definition TreeMesh.cpp:156
UIHostHandle host
uint32_t index
std::vector< std::string_view > lines
float m[16]
virtual std::string getName() const =0
Returns the name.
JSON-compatible state value tree used by state hot reload.
Definition StateValue.h:20
static StateValue object()
Empty object.
Definition StateValue.h:40
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
Definition StateValue.h:38
static StateValue integer(int64_t v)
Integer value.
void pushBack(StateValue v)
Append an element; only valid on arrays.
bool isString() const
Definition StateValue.h:49
bool isBool() const
Definition StateValue.h:48
bool isObject() const
Definition StateValue.h:50
bool isArray() const
Definition StateValue.h:51
static StateValue number(double v)
Floating-point value.
const StateValue * find(const std::string &key) const
Look up key; nullptr when absent.
static StateValue null()
Null value.
Definition StateValue.h:28
StateValue & at(size_t index)
Indexed element (bounds-checked); only valid on arrays.
Definition StateValue.h:67
bool isInt() const
Definition StateValue.h:46
static StateValue string(std::string v)
String value.
void set(const std::string &key, StateValue v)
Insert or replace key; only valid on objects.
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
Definition Value.cpp:87
std::string typeName() const
Return the compatibility type name used by dialogue scripts.
Definition Value.cpp:150
std::map< std::string, Value > Object
Definition Value.h:34
static Value number(double value)
Compatibility factory for a Double value.
Definition Value.h:106
static Value string(std::string value)
Compatibility factory for a string value.
Definition Value.h:110
@ Int
Compatibility spelling used by the former dialogue value.
@ Float
Compatibility spelling used by the former dialogue value.
std::vector< Value > Array
Definition Value.h:33
static Value integer(std::int64_t value)
Compatibility factory for an Int64 value.
Definition Value.h:104
static Value boolean(bool value)
Compatibility factory for a boolean value.
Definition Value.h:108
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
Unified avatar instance. Kind is a string: "image" | "live2d" | "vroid". Script-facing API avoids ove...
size_t addDestroyHook(DestroyHook hook)
Register a destruction hook; returns an id usable with removeDestroyHook().
Runtime-state serialization for state hot reload.
Visual-novel style dialogue stage. Script: dlg <- eve.Dialogue();
Definition Dialogue.h:46
std::string pickLineWithParams(const std::string &poolId, const std::unordered_map< std::string, VarValue > &params)
Pick line with params.
Definition Dialogue.cpp:744
bool setExpression(const std::string &id, const std::string &expression)
Sets the expression.
Definition Dialogue.cpp:964
bool setMotion(const std::string &id, const std::string &motion)
Sets the motion.
Definition Dialogue.cpp:971
std::string getSpeakerName() const
Returns the speaker name.
void reset()
重置舞台与台词状态。
void advance()
Advance.
void presentChoices()
Present choices.
std::string getLastPoolsError() const
Returns the last pools error.
Definition Dialogue.h:211
float getLipSyncAmplitude() const
Returns the lip sync amplitude.
Definition Dialogue.h:134
bool clearVar(const std::string &name, const std::string &scope)
Clears var.
Definition Dialogue.cpp:257
bool playLineWithParams(const std::string &lineId, const std::unordered_map< std::string, VarValue > &params)
Play line with params.
Definition Dialogue.cpp:792
std::string getSpeakerId() const
Returns the speaker id.
Definition Dialogue.h:113
int getCharacterCount() const
Returns the character count.
Definition Dialogue.cpp:916
bool captureState(StateValue &out) const
Serialize conversation state (vars, rng, phase, choices, stage).
bool restoreState(const StateValue &in, std::string *err=nullptr)
Restore conversation state captured by captureState().
std::string getCurrentLineMeta(const std::string &field) const
Returns the current line meta.
Definition Dialogue.cpp:828
float getSlotX(const std::string &slot) const
Returns the slot x.
Definition Dialogue.cpp:955
bool isIdle() const
True when idle.
bool evalCondition(ssq::Object table)
Eval condition.
Definition Dialogue.cpp:300
std::string getCurrentLineId() const
Returns the current line id.
Definition Dialogue.h:237
std::string getFullText() const
Returns the full text.
Definition Dialogue.h:117
bool getVarBool(const std::string &name, bool defaultValue, const std::string &scope) const
Returns the var bool.
Definition Dialogue.cpp:237
bool registerCharacter(const std::string &id, const std::string &displayName)
角色:注册 / 查询 / 绑定 Avatar。
Definition Dialogue.cpp:871
bool playPool(const std::string &poolId, ssq::Object params)
Play pool.
Definition Dialogue.cpp:816
std::vector< std::string > getCurrentLineTags() const
Returns the current line tags.
Definition Dialogue.cpp:833
bool playLine(const std::string &lineId, ssq::Object params)
Play line.
Definition Dialogue.cpp:786
void narrate(const std::string &text)
Narrate.
int loadPoolsFromData(const DataValue &root)
Loads pools from data.
Definition Dialogue.cpp:580
void clearChoices()
选项:清空/添加/展示与选择。
int replacePoolsFromData(const DataValue &root)
Atomically replace all pools from a validated unified dnut workspace.
Definition Dialogue.cpp:582
bool isTyping() const
True when typing.
int getPoolCount() const
Returns the pool count.
Definition Dialogue.cpp:642
bool setVar(const std::string &name, ssq::Object value, const std::string &scope)
Sets the var.
Definition Dialogue.cpp:198
bool selectChoice(int index)
Select choice.
bool hasCharacter(const std::string &id) const
True when character.
Definition Dialogue.cpp:884
std::string getCharacterId(int index) const
Returns the character id.
Definition Dialogue.cpp:918
void setLipSyncParameter(const std::string &name)
Sets the lip sync parameter.
bool addChoice(const std::string &id, const std::string &label)
Adds choice.
VarValue getVarValue(const std::string &name, const std::string &scope) const
Returns the var value.
Definition Dialogue.cpp:209
bool show(const std::string &id, const std::string &slot)
舞台:显示/隐藏角色、槽位与表情/动作。
Definition Dialogue.cpp:923
void setTypeSpeed(float charsPerSecond)
Sets the type speed.
float getVarFloat(const std::string &name, float defaultValue, const std::string &scope) const
Returns the var float.
Definition Dialogue.cpp:228
int loadPoolsFromTable(ssq::Object table)
Loads pools from table.
Definition Dialogue.cpp:499
bool hasPool(const std::string &id) const
True when pool.
Definition Dialogue.cpp:649
std::string getChoiceId(int index) const
Returns the choice id.
avatar::AvatarInstance * getAvatar(const std::string &id) const
Returns the avatar.
Definition Dialogue.cpp:911
bool playPoolWithParams(const std::string &poolId, const std::unordered_map< std::string, VarValue > &params)
Play pool with params.
Definition Dialogue.cpp:822
bool unregisterCondition(const std::string &name)
Unregisters condition.
Definition Dialogue.cpp:292
void skipTyping()
Skip typing.
void update(float dt)
推进打字机 / 口型同步 / 阶段机;每帧调用。
void say(const std::string &speakerId, const std::string &text)
台词:说话/旁白、打字机效果与推进。
bool evalConditionData(const DataValue &cond)
Eval condition data.
Definition Dialogue.cpp:451
bool isWaitingChoice() const
True when waiting choice.
float getLipSyncValue() const
Returns the lip sync value.
Definition Dialogue.h:136
std::string pickLine(const std::string &poolId, ssq::Object params)
Pick line.
Definition Dialogue.cpp:738
std::string getChoiceLabel(int index) const
Returns the choice label.
bool bindAvatar(const std::string &id, avatar::AvatarInstance *av)
Binds avatar.
Definition Dialogue.cpp:891
void setRandomSeed(int seed)
Sets the random seed.
Definition Dialogue.cpp:733
bool resetToDefaults()
Reset stage and line state (restore fallback).
std::string getVarType(const std::string &name, const std::string &scope) const
Returns the var type.
Definition Dialogue.cpp:216
void setSlotX(const std::string &slot, float xNorm)
Sets the slot x.
Definition Dialogue.cpp:950
bool registerCondition(const std::string &name, ssq::Object fn)
Registers condition.
Definition Dialogue.cpp:277
std::string getVarString(const std::string &name, const std::string &defaultValue, const std::string &scope) const
Returns the var string.
Definition Dialogue.cpp:246
int getRandomSeed() const
Returns the random seed.
Definition Dialogue.h:218
bool isShown(const std::string &id) const
True when shown.
Definition Dialogue.cpp:940
std::string getLipSyncParameter() const
Returns the lip sync parameter.
Definition Dialogue.h:130
void syncStage(float stageWidth, float stageHeight)
Synchronizes stage.
Definition Dialogue.cpp:978
bool setVarValue(const std::string &name, const VarValue &value, const std::string &scope)
Sets the var value.
Definition Dialogue.cpp:190
~Dialogue() override
Dialogue.
Definition Dialogue.cpp:164
std::string getSlot(const std::string &id) const
Returns the slot.
Definition Dialogue.cpp:945
int getChoiceCount() const
Returns the choice count.
void setLipSyncEnabled(bool enabled)
口型同步:打字时驱动说话者 Avatar 参数。
std::string getSelectedChoiceId() const
Returns the selected choice id.
Definition Dialogue.h:155
int getVarInt(const std::string &name, int defaultValue, const std::string &scope) const
Returns the var int.
Definition Dialogue.cpp:220
void setLipSyncAmplitude(float amplitude)
Sets the lip sync amplitude.
std::string getPoolId(int index) const
Returns the pool id.
Definition Dialogue.cpp:644
bool hide(const std::string &id)
Hide.
Definition Dialogue.cpp:932
void clearVars(const std::string &scope)
Clears vars.
Definition Dialogue.cpp:262
bool isWaitingAdvance() const
True when waiting advance.
bool isLipSyncEnabled() const
True when lip sync enabled.
Definition Dialogue.h:126
std::string getDisplayName(const std::string &id) const
Returns the display name.
Definition Dialogue.cpp:886
std::string getVisibleText() const
Returns the visible text.
float getTypeSpeed() const
Returns the type speed.
Definition Dialogue.h:100
void clearPools()
Clears pools.
Definition Dialogue.cpp:640
bool hasVar(const std::string &name, const std::string &scope) const
True when var.
Definition Dialogue.cpp:252
std::string getPhase() const
Returns the phase.
ECS mount point for one scene graph (full scene or nested subtree root). Isomorphic to eve::ui::UIHos...
Definition SceneHost.h:89
const std::string & getName()
const char * defaultValue
std::vector< ParamSpec > params
eve::Value DataValue
Canonical data value used by dialogue pools, conditions and scripts.
Definition Dialogue.h:36
eve::Value VarValue
Definition Dialogue.h:37
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
Definition wgpu_types.h:20
const EditorValue * field(const EditorValue &value, const char *name)
Result< void > pushValue(HSQUIRRELVM vm, const Value &value, const SquirrelValueOptions &options)
Push a canonical Value into the active Squirrel VM.
Result< Value > valueFromSquirrel(HSQUIRRELVM vm, SQInteger index, const SquirrelValueOptions &options)
Convert one Squirrel value into the canonical owning Value tree.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
size_t utf8_codepoint_count(const std::string &s)
Count UTF-8 code points in a string. Invalid / truncated sequences stop the scan.
Definition utf8.cpp:23
size_t utf8_byte_offset_for_codepoints(const std::string &s, size_t codepoints)
Byte offset of the N-th UTF-8 code point (0-based count of code points). Returns s....
Definition utf8.cpp:37
SettlementPipeline::Stage fn
bool enabled
std::vector< WidgetDesc > children
Definition Widget.h:111