19#include <simplesquirrel/simplesquirrel.hpp>
28std::string floatToString(
double v) {
30 std::snprintf(buf,
sizeof(buf),
"%g",
v);
36 if (!converted.ok()) {
37 converted.ignore(
"Dialogue scalar compatibility adapter rejected the value");
41 if (
value.isNull() ||
value.isArray() ||
value.isObject())
return false;
42 out = std::move(
value);
48 if (!
vm ||
object.isEmpty())
return false;
50 if (!converted.ok()) {
51 converted.ignore(
"Dialogue data compatibility adapter rejected the value");
54 out = std::move(converted).takeValue();
58bool objectToVarParams(
HSQUIRRELVM vm,
const ssq::Object &
object,
59 std::unordered_map<std::string, Dialogue::VarValue> &out) {
61 if (!
vm ||
object.isEmpty() ||
object.getType() != ssq::Type::TABLE)
return true;
63 if (!converted.ok()) {
64 converted.ignore(
"Dialogue parameter compatibility adapter rejected the table");
70 for (
const auto &[
key, field] : *
fields) {
79 pushed.ignore(
"Dialogue scalar compatibility adapter could not push the value");
84void pushVarTable(
HSQUIRRELVM vm,
const std::unordered_map<std::string, Dialogue::VarValue> &vars) {
89 pushed.ignore(
"Dialogue parameter compatibility adapter could not push the table");
94std::vector<Dialogue*>& liveDialogues() {
116 default:
return false;
123 const char* stateKind()
const override {
return "dialogue"; }
125 bool captureState(StateValue& out)
override {
127 for (Dialogue*
d : liveDialogues()) {
129 if (
d->captureState(sv)) arr.pushBack(std::move(sv));
131 out = std::move(arr);
135 bool restoreState(
const StateValue& in, std::string* err)
override {
137 if (err) *err =
"dialogue: expected array of instances";
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;
147 bool resetToDefaults()
override {
148 for (Dialogue*
d : liveDialogues())
d->reset();
155 static DialogueStateProvider provider;
156 eve::cap::addListener<eve::caps::IStateProvider>(&provider);
165 liveDialogues().erase(std::remove(liveDialogues().
begin(), liveDialogues().
end(),
this), liveDialogues().
end());
167 for (
auto &kv : predicates_) sq_release(vm_, &kv.second);
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_;
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_;
191 const std::string &
scope) {
192 auto *
m = varsForScope(
scope);
193 if (!
m ||
name.empty())
return false;
199 if (!vm_)
return false;
200 const SQInteger
top = sq_gettop(vm_);
201 sq_pushobject(vm_,
value.getRaw());
203 const bool ok = squirrelToVarValueAt(vm_, -1,
v);
205 if (!
ok)
return false;
210 const auto *
m = varsForScope(
scope);
212 const auto it =
m->find(
name);
229 const std::string &
scope)
const {
238 const std::string &
scope)
const {
247 const std::string &
scope)
const {
253 const auto *
m = varsForScope(
scope);
254 return m &&
m->find(
name) !=
m->end();
258 auto *
m = varsForScope(
scope);
259 return m &&
m->erase(
name) > 0;
263 if (
scope ==
"global") {
265 }
else if (
scope ==
"scene") {
267 }
else if (
scope ==
"all") {
278 if (!vm_ ||
name.empty())
return false;
279 HSQOBJECT raw =
fn.getRaw();
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);
287 sq_addref(vm_, &raw);
288 predicates_[
name] = raw;
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);
302 if (!objectToDataValue(vm_, table,
v))
return false;
306bool Dialogue::parseCondition(
const DataValue &
v, Condition &out, std::string &
error) {
308 out.kind = Condition::Kind::All;
309 for (
size_t i = 0; i <
v.arraySize(); ++i) {
310 const auto &item =
v.at(i);
312 if (!parseCondition(item, child,
error))
return false;
313 out.
children.push_back(std::move(child));
318 error =
"condition must be a table";
323 error =
"script condition name must be a string";
326 out.kind = Condition::Kind::Script;
327 out.script =
script->asString();
332 error =
"all must be an array";
335 out.kind = Condition::Kind::All;
336 for (
size_t i = 0; i < all->arraySize(); ++i) {
337 const auto &item = all->at(i);
339 if (!parseCondition(item, child,
error))
return false;
340 out.
children.push_back(std::move(child));
346 error =
"any must be an array";
349 out.kind = Condition::Kind::Any;
350 for (
size_t i = 0; i < any->arraySize(); ++i) {
351 const auto &item = any->at(i);
353 if (!parseCondition(item, child,
error))
return false;
354 out.
children.push_back(std::move(child));
359 out.kind = Condition::Kind::Not;
360 out.children.emplace_back();
361 return parseCondition(*notc, out.children.back(),
error);
366 error =
"var/op condition requires string var and op";
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;
375 out.kind = Condition::Kind::Cmp;
376 out.var = var->asString();
380 switch (val->kind()) {
391 error =
"unrecognized condition table";
396 if (
a.isNumeric() &&
b.isNumeric()) {
406bool Dialogue::compareOrder(
const std::string &op,
const VarValue &
a,
const VarValue &
b)
const {
407 if (!
a.isNumeric() || !
b.isNumeric())
return false;
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;
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 {
422 case Condition::Kind::Always:
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);
434 case Condition::Kind::All:
436 if (!evalConditionInternal(
child, merged,
params, lineId)) return false;
438 case Condition::Kind::Any:
440 if (evalConditionInternal(
child, merged,
params, lineId)) return true;
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);
454 if (!parseCondition(cond,
c,
error))
return false;
455 return evalConditionInternal(
c, mergedVars({}), {},
"");
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;
466 const SQInteger
top = sq_gettop(vm_);
467 sq_pushobject(vm_, it->second);
470 sq_pushstring(vm_,
"vars", -1);
471 pushVarTable(vm_, merged);
472 sq_newslot(vm_, -3, SQFalse);
474 sq_pushstring(vm_,
"params", -1);
475 pushVarTable(vm_,
params);
476 sq_newslot(vm_, -3, SQFalse);
478 sq_pushstring(vm_,
"lineId", -1);
479 sq_pushstring(vm_, lineId.c_str(), lineId.size());
480 sq_newslot(vm_, -3, SQFalse);
482 if (SQ_FAILED(sq_call(vm_, 1, SQTrue, SQTrue))) {
486 SQBool result = SQFalse;
487 if (SQ_FAILED(sq_getbool(vm_, -1, &result))) {
501 if (!objectToDataValue(vm_, table,
root)) {
502 lastPoolsError_ =
"pools root must be a table";
508bool Dialogue::parseLineData(
const std::string &poolId,
const DataValue &
v,
int index, Line &
line,
509 std::string &
error) {
511 error =
"line must be a table";
516 if (
line.id.empty())
line.id = poolId +
"." + std::to_string(
index);
524 if (
line.text.empty() &&
line.i18nKey.empty()) {
525 error =
"line '" +
line.id +
"': text or i18n required";
531 line.weight = double(
w->asInt());
533 line.weight =
w->asDouble();
535 error =
"line '" +
line.id +
"': weight must be a number";
541 if (!parseCondition(*when,
line.when,
error)) {
549 error =
"line '" +
line.id +
"': meta must be a table";
553 for (
const auto &mkv : *
fields) {
555 switch (mkv.second.kind()) {
563 line.meta[mkv.first] = val;
569 error =
"line '" +
line.id +
"': tags must be an array";
572 for (
size_t i = 0; i < tags->arraySize(); ++i) {
573 const auto &
t = tags->at(i);
584int Dialogue::applyPoolsFromData(
const DataValue &
root,
bool replace) {
585 lastPoolsError_.clear();
587 lastPoolsError_ =
"pools root must be an object";
592 lastPoolsError_ =
"missing pools object";
597 std::vector<Pool> candidate = replace ? std::vector<Pool>{} : pools_;
599 for (
const auto &kv : *poolFields) {
603 lastPoolsError_ =
"pool '" + kv.first +
"': expected object";
608 pool.noRepeat = int(nr->asInt());
610 pool.noRepeat = int(nr->asDouble());
611 if (pool.noRepeat < 0) pool.noRepeat = 0;
615 lastPoolsError_ =
"pool '" + kv.first +
"': missing lines array";
619 for (
size_t i = 0; i <
lines->arraySize(); ++i) {
620 const auto &lv =
lines->at(i);
623 if (!parseLineData(pool.id, lv, lineIndex,
line,
error)) {
624 lastPoolsError_ =
error;
627 pool.lines.push_back(std::move(
line));
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));
636 pools_ = std::move(candidate);
645 if (
index < 0 ||
size_t(
index) >= pools_.size())
return {};
646 return pools_[size_t(
index)].id;
651Dialogue::Pool *Dialogue::findPool(
const std::string &
id) {
652 for (
auto &
p : pools_)
653 if (
p.
id ==
id) return &
p;
657const Dialogue::Pool *Dialogue::findPool(
const std::string &
id)
const {
658 for (
const auto &
p : pools_)
659 if (
p.
id ==
id) return &
p;
663Dialogue::Line *Dialogue::findLine(
const std::string &
id) {
664 for (
auto &
p : pools_)
666 if (l.
id ==
id) return &l;
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;
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;
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();
696std::string Dialogue::interpolate(
const std::string &tpl,
697 const std::unordered_map<std::string, VarValue> &vars)
const {
699 out.reserve(tpl.size());
700 for (
size_t i = 0; i < tpl.size();) {
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();
722uint32_t Dialogue::nextRandom() {
723 uint32_t
x = rngState_;
731double Dialogue::nextUnit() {
return (nextRandom() & 0xFFFFFFu) / 16777216.0; }
734 rngState_ =
seed ? uint32_t(
seed) : 1u;
735 for (
auto &
p : pools_)
p.recent.clear();
739 std::unordered_map<std::string, VarValue>
p;
740 objectToVarParams(vm_,
params,
p);
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);
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())
762 std::vector<size_t> candidates;
763 collect(candidates,
false);
764 if (candidates.empty()) collect(candidates,
true);
765 if (candidates.empty())
return "";
768 for (
const size_t i : candidates) total += pool->lines[i].weight;
769 double roll = nextUnit() * total;
770 size_t picked = candidates.front();
772 for (
const size_t i : candidates) {
773 acc += pool->lines[i].weight;
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());
783 return pool->lines[picked].id;
787 std::unordered_map<std::string, VarValue>
p;
788 objectToVarParams(vm_,
params,
p);
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);
799 if (!
line->text.empty()) {
800 text = interpolate(
line->text, merged);
802 auto *i18n = eve::ModuleManager::getInstance<eve::i18n::I18n>(
"I18n");
803 text = i18n ? i18n->getWithParams(
line->i18nKey, stringParams(merged)) :
line->i18nKey;
809 currentLineId_ =
line->id;
810 currentLineMeta_ =
line->meta;
811 currentLineTags_ =
line->tags;
812 applyLineMeta(*
line);
817 std::unordered_map<std::string, VarValue>
p;
818 objectToVarParams(vm_,
params,
p);
823 const std::unordered_map<std::string, VarValue> &
params) {
829 const auto it = currentLineMeta_.find(field);
830 return it == currentLineMeta_.end() ? std::string{} : it->second;
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);
850void Dialogue::pollSceneChange() {
851 auto *scn = eve::ModuleManager::getInstance<eve::scene::Scene>(
"Scene");
855 if (!
name.empty() &&
name != lastSceneName_) sceneVars_.clear();
856 if (!
name.empty()) lastSceneName_ =
name;
859Dialogue::Character *Dialogue::findCharacter(
const std::string &
id) {
860 for (
auto &
c : characters_)
861 if (
c.
id ==
id) return &
c;
865const Dialogue::Character *Dialogue::findCharacter(
const std::string &
id)
const {
866 for (
const auto &
c : characters_)
867 if (
c.
id ==
id) return &
c;
872 if (
id.empty())
return false;
873 if (
auto *
c = findCharacter(
id)) {
874 c->displayName = displayName.empty() ?
id : displayName;
879 c.displayName = displayName.empty() ?
id : displayName;
880 characters_.push_back(
c);
887 const Character *
c = findCharacter(
id);
888 return c ?
c->displayName : std::string{};
892 Character *
c = findCharacter(
id);
893 if (!
c)
return false;
896 if (
c->avatar &&
c->avatarHook) {
897 c->avatar->removeDestroyHook(*
c->avatarHook);
898 c->avatarHook.reset();
902 const std::string
key =
id;
904 if (Character *ch = findCharacter(
key))
905 ch->avatar =
nullptr;
912 const Character *
c = findCharacter(
id);
913 return c ?
c->avatar :
nullptr;
919 if (
index < 0 ||
size_t(
index) >= characters_.size())
return {};
920 return characters_[size_t(
index)].id;
924 Character *
c = findCharacter(
id);
925 if (!
c)
return false;
927 c->slot = slot.empty() ?
"center" : slot;
928 if (
c->avatar)
c->avatar->setVisible(
true);
933 Character *
c = findCharacter(
id);
934 if (!
c)
return false;
936 if (
c->avatar)
c->avatar->setVisible(
false);
941 const Character *
c = findCharacter(
id);
942 return c &&
c->shown;
946 const Character *
c = findCharacter(
id);
947 return c ?
c->slot : std::string{};
951 if (slot.empty())
return;
952 slotX_[slot] = xNorm;
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;
965 Character *
c = findCharacter(
id);
966 if (!
c || !
c->avatar)
return false;
967 c->avatar->setExpression(expression);
972 Character *
c = findCharacter(
id);
973 if (!
c || !
c->avatar)
return false;
974 c->avatar->setMotion(motion);
980 for (Character &
c : characters_) {
981 if (!
c.avatar)
continue;
982 c.avatar->setVisible(
c.shown);
988 const float x = xn * stageWidth;
990 c.avatar->setPosition(
x,
c.avatar->getY());
995void Dialogue::beginLine(
const std::string &speakerId,
const std::string &
text) {
996 speakerId_ = speakerId;
999 selectedChoiceId_.clear();
1001 if (typeSpeed_ <= 0.f) {
1003 phase_ = Phase::WaitingAdvance;
1004 lipSyncValue_ = 0.f;
1006 phase_ = Phase::Typing;
1011 beginLine(speakerId,
text);
1019 if (phase_ == Phase::Typing) {
1021 phase_ = Phase::WaitingAdvance;
1032 if (phase_ == Phase::Typing) {
1036 if (phase_ == Phase::WaitingAdvance) phase_ = Phase::Idle;
1040 if (speakerId_.empty())
return {};
1045 if (fullText_.empty())
return {};
1047 size_t n = size_t(std::floor(typed_ + 1e-4f));
1048 if (
n == 0)
return {};
1049 if (
n >= total)
return fullText_;
1059 case Phase::WaitingAdvance:
1060 return "waiting_advance";
1061 case Phase::WaitingChoice:
1062 return "waiting_choice";
1069 selectedChoiceId_.clear();
1073 if (
id.empty())
return false;
1074 for (
auto &ch : choices_) {
1080 choices_.push_back(Choice{
id,
label});
1085 if (choices_.empty()) {
1086 phase_ = Phase::Idle;
1090 phase_ = Phase::WaitingChoice;
1098 if (
index < 0 ||
size_t(
index) >= choices_.size())
return {};
1099 return choices_[size_t(
index)].id;
1103 if (
index < 0 ||
size_t(
index) >= choices_.size())
return {};
1104 return choices_[size_t(
index)].label;
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;
1118 if (!
name.empty()) lipSyncParameter_ =
name;
1122 if (amplitude < 0.f) amplitude = 0.f;
1123 if (amplitude > 2.f) amplitude = 2.f;
1124 lipSyncAmplitude_ = amplitude;
1127void Dialogue::updateLipSync(
float dt) {
1128 if (dt < 0.f) dt = 0.f;
1129 if (lipSyncEnabled_ && phase_ == Phase::Typing && !speakerId_.empty()) {
1132 const float wave = std::fabs(std::sin(lipSyncTime_ * 14.f));
1133 lipSyncValue_ = lipSyncAmplitude_ * (0.25f + 0.75f * wave);
1136 lipSyncValue_ *= std::max(0.f, 1.f - dt * 8.f);
1137 if (lipSyncValue_ < 0.01f) lipSyncValue_ = 0.f;
1139 applyLipSyncToSpeaker();
1142void Dialogue::applyLipSyncToSpeaker() {
1143 if (!lipSyncEnabled_ || speakerId_.empty() || lipSyncParameter_.empty())
return;
1145 if (!
c || !
c->avatar)
return;
1146 c->avatar->setParameter(lipSyncParameter_, lipSyncValue_);
1150 if (dt < 0.f) dt = 0.f;
1151 if (phase_ == Phase::Typing) {
1153 typed_ += typeSpeed_ * dt;
1154 if (typed_ >= total) {
1156 phase_ = Phase::WaitingAdvance;
1164 phase_ = Phase::Idle;
1169 selectedChoiceId_.clear();
1170 lipSyncValue_ = 0.f;
1172 currentLineId_.clear();
1173 currentLineMeta_.clear();
1174 currentLineTags_.clear();
1175 for (Character &
c : characters_) {
1177 if (
c.avatar)
c.avatar->setVisible(
false);
1196 for (
const auto& kv : globalVars_)
global.set(kv.first, varValueToState(kv.second));
1197 out.
set(
"globalVars", std::move(
global));
1200 for (
const auto& kv : sceneVars_)
scene.set(kv.first, varValueToState(kv.second));
1201 out.
set(
"sceneVars", std::move(
scene));
1204 for (
const auto&
c : choices_) {
1208 choices.pushBack(std::move(item));
1213 for (
const auto&
c : characters_) {
1221 out.
set(
"characters", std::move(chars));
1225 out.
set(
"slotX", std::move(slotX));
1231 if (err) *err =
"dialogue: state is not an object";
1236 if (err) *err =
"dialogue: missing phase";
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;
1249 if (err) *err =
"dialogue: unknown phase '" + phaseName +
"'";
1253 if (
const StateValue*
v = in.
find(
"speakerId");
v &&
v->isString()) speakerId_ =
v->asString();
1256 typed_ =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
1258 typeSpeed_ =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
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());
1267 globalVars_.clear();
1269 for (
const auto&
key : vars->keys()) {
1271 if (stateToVarValue(*vars->find(
key), val)) globalVars_[
key] = val;
1276 for (
const auto&
key : vars->keys()) {
1278 if (stateToVarValue(*vars->find(
key), val)) sceneVars_[
key] = val;
1284 for (
size_t i = 0; i <
choices->arraySize(); ++i) {
1288 if (
id &&
id->isString() &&
label &&
label->isString()) {
1289 choices_.push_back(Choice{
id->asString(),
label->asString()});
1295 for (
size_t i = 0; i < chars->arraySize(); ++i) {
1298 if (!
id || !
id->isString())
continue;
1299 Character *
c = findCharacter(
id->asString());
1301 characters_.push_back(Character{});
1302 c = &characters_.back();
1303 c->id =
id->asString();
1305 c->displayName =
name->asString();
1308 c->slot = slot->asString();
1310 c->shown = shown->asBool();
1311 if (
c->avatar)
c->avatar->setVisible(
c->shown);
1318 for (
const auto &
key :
slots->keys()) {
1320 if (
v && (
v->isInt() ||
v->isFloat()))
1321 slotX_[
key] =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
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);
1338void Dialogue::expose(ssq::Class &
cls) {
std::vector< BuildingInstanceSnapshot > instances
struct SQVM * HSQUIRRELVM
std::set< std::string > choices
#define Module_IMPL(ModuleName, newExpr)
std::vector< std::string > fields
std::weak_ptr< PrimitiveScene > scene
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
The single Squirrel projection for common Result, Status and Value.
std::vector< std::string_view > lines
virtual std::string getName() const =0
Returns the name.
JSON-compatible state value tree used by state hot reload.
static StateValue object()
Empty object.
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
static StateValue integer(int64_t v)
Integer value.
void pushBack(StateValue v)
Append an element; only valid on arrays.
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.
StateValue & at(size_t index)
Indexed element (bounds-checked); only valid on arrays.
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.
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
std::string typeName() const
Return the compatibility type name used by dialogue scripts.
std::map< std::string, Value > Object
static Value number(double value)
Compatibility factory for a Double value.
static Value string(std::string value)
Compatibility factory for a string value.
@ Int
Compatibility spelling used by the former dialogue value.
@ Float
Compatibility spelling used by the former dialogue value.
std::vector< Value > Array
static Value integer(std::int64_t value)
Compatibility factory for an Int64 value.
static Value boolean(bool value)
Compatibility factory for a boolean value.
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
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();
std::string pickLineWithParams(const std::string &poolId, const std::unordered_map< std::string, VarValue > ¶ms)
Pick line with params.
bool setExpression(const std::string &id, const std::string &expression)
Sets the expression.
bool setMotion(const std::string &id, const std::string &motion)
Sets the motion.
std::string getSpeakerName() const
Returns the speaker name.
void presentChoices()
Present choices.
std::string getLastPoolsError() const
Returns the last pools error.
float getLipSyncAmplitude() const
Returns the lip sync amplitude.
bool clearVar(const std::string &name, const std::string &scope)
Clears var.
bool playLineWithParams(const std::string &lineId, const std::unordered_map< std::string, VarValue > ¶ms)
Play line with params.
std::string getSpeakerId() const
Returns the speaker id.
int getCharacterCount() const
Returns the character count.
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.
float getSlotX(const std::string &slot) const
Returns the slot x.
bool isIdle() const
True when idle.
bool evalCondition(ssq::Object table)
Eval condition.
std::string getCurrentLineId() const
Returns the current line id.
std::string getFullText() const
Returns the full text.
bool getVarBool(const std::string &name, bool defaultValue, const std::string &scope) const
Returns the var bool.
bool registerCharacter(const std::string &id, const std::string &displayName)
角色:注册 / 查询 / 绑定 Avatar。
bool playPool(const std::string &poolId, ssq::Object params)
Play pool.
std::vector< std::string > getCurrentLineTags() const
Returns the current line tags.
bool playLine(const std::string &lineId, ssq::Object params)
Play line.
void narrate(const std::string &text)
Narrate.
int loadPoolsFromData(const DataValue &root)
Loads pools from data.
void clearChoices()
选项:清空/添加/展示与选择。
int replacePoolsFromData(const DataValue &root)
Atomically replace all pools from a validated unified dnut workspace.
bool isTyping() const
True when typing.
int getPoolCount() const
Returns the pool count.
bool setVar(const std::string &name, ssq::Object value, const std::string &scope)
Sets the var.
bool selectChoice(int index)
Select choice.
bool hasCharacter(const std::string &id) const
True when character.
std::string getCharacterId(int index) const
Returns the character id.
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.
bool show(const std::string &id, const std::string &slot)
舞台:显示/隐藏角色、槽位与表情/动作。
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.
int loadPoolsFromTable(ssq::Object table)
Loads pools from table.
bool hasPool(const std::string &id) const
True when pool.
std::string getChoiceId(int index) const
Returns the choice id.
avatar::AvatarInstance * getAvatar(const std::string &id) const
Returns the avatar.
bool playPoolWithParams(const std::string &poolId, const std::unordered_map< std::string, VarValue > ¶ms)
Play pool with params.
bool unregisterCondition(const std::string &name)
Unregisters condition.
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.
bool isWaitingChoice() const
True when waiting choice.
float getLipSyncValue() const
Returns the lip sync value.
std::string pickLine(const std::string &poolId, ssq::Object params)
Pick line.
std::string getChoiceLabel(int index) const
Returns the choice label.
bool bindAvatar(const std::string &id, avatar::AvatarInstance *av)
Binds avatar.
void setRandomSeed(int seed)
Sets the random seed.
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.
void setSlotX(const std::string &slot, float xNorm)
Sets the slot x.
bool registerCondition(const std::string &name, ssq::Object fn)
Registers condition.
std::string getVarString(const std::string &name, const std::string &defaultValue, const std::string &scope) const
Returns the var string.
int getRandomSeed() const
Returns the random seed.
bool isShown(const std::string &id) const
True when shown.
std::string getLipSyncParameter() const
Returns the lip sync parameter.
void syncStage(float stageWidth, float stageHeight)
Synchronizes stage.
bool setVarValue(const std::string &name, const VarValue &value, const std::string &scope)
Sets the var value.
~Dialogue() override
Dialogue.
std::string getSlot(const std::string &id) const
Returns the slot.
int getChoiceCount() const
Returns the choice count.
void setLipSyncEnabled(bool enabled)
口型同步:打字时驱动说话者 Avatar 参数。
std::string getSelectedChoiceId() const
Returns the selected choice id.
int getVarInt(const std::string &name, int defaultValue, const std::string &scope) const
Returns the var int.
void setLipSyncAmplitude(float amplitude)
Sets the lip sync amplitude.
std::string getPoolId(int index) const
Returns the pool id.
bool hide(const std::string &id)
Hide.
void clearVars(const std::string &scope)
Clears vars.
bool isWaitingAdvance() const
True when waiting advance.
bool isLipSyncEnabled() const
True when lip sync enabled.
std::string getDisplayName(const std::string &id) const
Returns the display name.
std::string getVisibleText() const
Returns the visible text.
float getTypeSpeed() const
Returns the type speed.
void clearPools()
Clears pools.
bool hasVar(const std::string &name, const std::string &scope) const
True when var.
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...
const std::string & getName()
const char * defaultValue
std::vector< ParamSpec > params
eve::Value DataValue
Canonical data value used by dialogue pools, conditions and scripts.
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
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.
size_t utf8_codepoint_count(const std::string &s)
Count UTF-8 code points in a string. Invalid / truncated sequences stop the scan.
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....
SettlementPipeline::Stage fn