载入中...
搜索中...
未找到
ConversationPersistence.cpp
浏览该文件的文档.
2
3#include <Poco/Dynamic/Var.h>
4#include <Poco/Exception.h>
5#include <Poco/JSON/Array.h>
6#include <Poco/JSON/Object.h>
7#include <Poco/JSON/Parser.h>
8#include <Poco/JSON/Stringifier.h>
9
10#include <algorithm>
11#include <cctype>
12#include <sstream>
13#include <typeinfo>
14
15namespace eve::dialogue {
16namespace {
17
18Poco::Dynamic::Var stateToVar(const StateValue& state) {
19 switch (state.kind()) {
20 case StateValue::Kind::Null: return {};
21 case StateValue::Kind::Int: return Poco::Dynamic::Var(static_cast<Poco::Int64>(state.asInt()));
22 case StateValue::Kind::Float: return Poco::Dynamic::Var(state.asDouble());
23 case StateValue::Kind::Bool: return Poco::Dynamic::Var(state.asBool());
24 case StateValue::Kind::String: return Poco::Dynamic::Var(state.asString());
26 Poco::JSON::Array::Ptr array(new Poco::JSON::Array());
27 for (size_t i = 0; i < state.arraySize(); ++i) array->add(stateToVar(state.at(i)));
28 return Poco::Dynamic::Var(array);
29 }
31 Poco::JSON::Object::Ptr object(new Poco::JSON::Object());
32 for (const auto& key : state.keys()) object->set(key, stateToVar(*state.find(key)));
33 return Poco::Dynamic::Var(object);
34 }
35 }
36 return {};
37}
38
39StateValue varToState(const Poco::Dynamic::Var& value) {
40 if (value.isEmpty()) return StateValue::null();
41 if (value.isBoolean()) return StateValue::boolean(value.convert<bool>());
42 if (value.isInteger()) return StateValue::integer(value.convert<Poco::Int64>());
43 if (value.isNumeric()) return StateValue::number(value.convert<double>());
44 if (value.isString()) return StateValue::string(value.convert<std::string>());
45 if (value.type() == typeid(Poco::JSON::Array::Ptr)) {
46 StateValue result = StateValue::array();
47 Poco::JSON::Array::Ptr array = value.extract<Poco::JSON::Array::Ptr>();
48 if (array)
49 for (size_t i = 0; i < array->size(); ++i)
50 result.pushBack(varToState(array->get(static_cast<unsigned int>(i))));
51 return result;
52 }
53 if (value.type() == typeid(Poco::JSON::Object::Ptr)) {
54 StateValue result = StateValue::object();
55 Poco::JSON::Object::Ptr object = value.extract<Poco::JSON::Object::Ptr>();
56 if (object)
57 for (const auto& key : object->getNames()) result.set(key, varToState(object->get(key)));
58 return result;
59 }
60 return StateValue::null();
61}
62
63std::string trim(std::string text) {
64 const auto first = std::find_if_not(text.begin(), text.end(), [](unsigned char c) { return std::isspace(c); });
65 const auto last = std::find_if_not(text.rbegin(), text.rend(), [](unsigned char c) { return std::isspace(c); });
66 return first >= last.base() ? std::string{} : std::string(first, last.base());
67}
68
69} // namespace
70
72 try {
73 std::ostringstream output;
74 Poco::JSON::Stringifier::stringify(stateToVar(state), output);
76 } catch (const Poco::Exception& e) {
78 eve::Diagnostic::error(eve::DiagnosticCode::SerializationError, e.displayText(), "save", {}, "dialogue.persistence"));
79 } catch (const std::exception& e) {
81 eve::Diagnostic::error(eve::DiagnosticCode::SerializationError, e.what(), "save", {}, "dialogue.persistence"));
82 }
83}
84
86 try {
87 StateValue state = varToState(Poco::JSON::Parser().parse(json));
88 if (!state.isObject())
90 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "conversation: save JSON root must be an object", "save", {}, "dialogue.persistence"));
91 return eve::Result<StateValue>::success(std::move(state));
92 } catch (const Poco::Exception& e) {
94 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, e.displayText(), "save", {}, "dialogue.persistence"));
95 } catch (const std::exception& e) {
97 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, e.what(), "save", {}, "dialogue.persistence"));
98 }
99}
100
101eve::Result<void> ConversationSaveMigrations::registerMigration(const std::string& assetId, int fromVersion,
102 const std::string& currentAssetId,
103 const std::string& nodeMap) {
104 if (assetId.empty() || currentAssetId.empty() || fromVersion < 0)
106 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "conversation: migration requires asset IDs and a non-negative version", "migration", {}, "dialogue.persistence"));
107 Rule rule{assetId, fromVersion, currentAssetId, {}};
108 std::istringstream mappings(nodeMap);
109 for (std::string item; std::getline(mappings, item, ',');) {
110 item = trim(std::move(item));
111 const size_t pos = item.find(':');
112 if (item.empty()) continue;
113 if (pos == std::string::npos || trim(item.substr(0, pos)).empty() || trim(item.substr(pos + 1)).empty())
115 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "conversation: node migration must use old:new pairs", "migration.nodeMap", {}, "dialogue.persistence"));
116 rule.nodes[trim(item.substr(0, pos))] = trim(item.substr(pos + 1));
117 }
118 auto existing = std::find_if(rules_.begin(), rules_.end(), [&](const Rule& candidate) {
119 return candidate.assetId == assetId && candidate.fromVersion == fromVersion;
120 });
121 if (existing == rules_.end())
122 rules_.push_back(std::move(rule));
123 else
124 *existing = std::move(rule);
126}
127
129 StateValue candidate = state;
130 std::string error;
131 const auto failFrame = [&](std::string message) {
132 error = std::move(message);
133 return false;
134 };
135 auto migrateFrame = [&](StateValue& frame) {
136 StateValue* asset = frame.find("asset");
137 StateValue* version = frame.find("version");
138 StateValue* node = frame.find("node");
139 if (!asset || !asset->isString() || !version || !version->isInt() || !node || !node->isString())
140 return failFrame("conversation: saved frame is malformed");
141 const eve::dnut::SequenceAsset* current = resolve(asset->asString());
142 if (current && current->version == version->asInt()) return true;
143 const auto rule = std::find_if(rules_.begin(), rules_.end(), [&](const Rule& item) {
144 return item.assetId == asset->asString() && item.fromVersion == version->asInt();
145 });
146 if (rule == rules_.end())
147 return failFrame("conversation: no migration for '" + asset->asString() + "' version " +
148 std::to_string(version->asInt()));
149 current = resolve(rule->currentAssetId);
150 if (!current) {
151 error = "conversation: migration target '" + rule->currentAssetId + "' is missing";
152 return false;
153 }
154 std::string currentNode = node->asString();
155 if (const auto renamed = rule->nodes.find(currentNode); renamed != rule->nodes.end())
156 currentNode = renamed->second;
157 if (!current->findNode(currentNode))
158 return failFrame("conversation: migrated node '" + currentNode + "' is missing");
159 frame.set("asset", StateValue::string(current->id));
160 frame.set("version", StateValue::integer(current->version));
161 frame.set("node", StateValue::string(std::move(currentNode)));
162 return true;
163 };
164
165 StateValue* active = candidate.find("active");
166 if (!active || !active->isBool())
168 eve::Diagnostic::error(eve::DiagnosticCode::SerializationError, "conversation: state is missing active", "save.active", {}, "dialogue.persistence"));
169 if (active->asBool()) {
170 StateValue* current = candidate.find("current");
171 StateValue* stack = candidate.find("stack");
172 if (!current || !current->isObject() || !stack || !stack->isArray())
174 eve::Diagnostic::error(eve::DiagnosticCode::SerializationError, "conversation: state frames are malformed", "save.frames", {}, "dialogue.persistence"));
175 if (!migrateFrame(*current))
177 eve::Diagnostic::error(eve::DiagnosticCode::UnknownVersion, error, "save.current", {}, "dialogue.persistence"));
178 for (size_t i = 0; i < stack->arraySize(); ++i)
179 if (!migrateFrame(stack->at(i)))
181 eve::Diagnostic::error(eve::DiagnosticCode::UnknownVersion, error, "save.stack", {}, "dialogue.persistence"));
182 }
183 return eve::Result<StateValue>::success(std::move(candidate));
184}
185
186} // namespace eve::dialogue
double value
bool & active
std::string output
std::string message
std::uint32_t key
std::int32_t c
std::int32_t first
std::string text
std::string error
Definition Package.cpp:60
const RoadNode * node
double current
Json object
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
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
int64_t asInt() const
Integer payload; only valid when kind() == Kind::Int.
Definition StateValue.h:54
static StateValue integer(int64_t v)
Integer value.
size_t arraySize() const
Element count; only valid on arrays.
Definition StateValue.h:65
bool isString() const
Definition StateValue.h:49
bool isArray() const
Definition StateValue.h:51
const std::string & asString() const
String payload; only valid when kind() == Kind::String.
Definition StateValue.h:60
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.
std::function< const eve::dnut::SequenceAsset *(const std::string &)> Resolver
eve::Result< void > registerMigration(const std::string &assetId, int fromVersion, const std::string &currentAssetId, const std::string &nodeMap)
Register a direct migration from one saved asset version to the currently loaded asset.
eve::Result< StateValue > migrate(const StateValue &state, const Resolver &resolve) const
Migrate the current frame and every saved call frame transactionally.
eve::Result< std::string > conversationStateToJson(const StateValue &state)
Serialize a dialogue state tree as JSON.
eve::Result< StateValue > conversationStateFromJson(const std::string &json)
Parse a JSON dialogue state tree.
Immutable, parameterized and versioned compiled sequence.