9#include <unordered_set>
21 return it ==
extra.end() ? fallback : it->second;
24std::unordered_map<std::string, QuestDefinition> &
questTable() {
25 static std::unordered_map<std::string, QuestDefinition>
t;
31std::string normalizeStart(
const std::string &policy) {
32 return policy ==
"auto" ?
"auto" :
"manual";
35std::string normalizeComplete(
const std::string &policy) {
36 return policy ==
"claim" ?
"claim" :
"auto";
40bool idOnCycle(
const std::unordered_map<std::string, QuestDefinition> &
table,
const std::string &
start) {
41 std::unordered_set<std::string>
visiting;
42 std::unordered_set<std::string> visited;
43 std::function<bool(
const std::string &)> dfs = [&](
const std::string &
id) ->
bool {
45 if (visited.count(
id))
return false;
46 auto it =
table.find(
id);
47 if (it ==
table.end())
return false;
49 for (
const auto &req : it->
second.requiresIds) {
50 if (dfs(req))
return true;
61 if (!o.isObject())
return obj;
62 obj.id = o.getString(
"id");
63 obj.topic = o.getString(
"topic");
64 obj.target = o.getString(
"target");
65 obj.count = o.getInt(
"count", 1);
66 if (obj.count <= 0) obj.count = 1;
70RewardSpec parseReward(Value o) {
72 if (!o.isObject())
return reward;
73 reward.type = o.getString(
"type");
74 reward.id = o.getString(
"id");
75 reward.amount = o.getDouble(
"amount", 0.0);
80QuestDefinition parseQuestObject(Value o) {
82 if (!o.isObject())
return def;
83 def.id = o.getString(
"id");
84 def.startPolicy = normalizeStart(o.getString(
"startPolicy",
"manual"));
85 def.completePolicy = normalizeComplete(o.getString(
"completePolicy",
"auto"));
86 def.requiresIds = o.getStringArray(
"requires");
87 def.tags = o.getStringArray(
"tags");
88 def.extra = o.getStringMap(
"extra");
91 std::unordered_set<std::string> seen;
93 for (
size_t i = 0; i <
objectives.size(); ++i) {
95 if (obj.id.empty() || !seen.insert(obj.id).second) {
99 def.objectives.push_back(std::move(obj));
106 const Value rewards = o.get(
"rewards");
107 for (
size_t i = 0; i < rewards.size(); ++i) {
108 RewardSpec reward = parseReward(rewards.at(i));
109 def.rewards.push_back(std::move(reward));
119bool validContentId(
const std::string &
id) {
120 if (
id.empty() ||
id.
size() > 256)
return false;
121 return std::none_of(
id.
begin(),
id.
end(), [](
unsigned char c) {
return c < 0x20 ||
c == 0x7f; });
124bool stringArrayIsStrict(Value
value) {
125 if (!
value.isArray())
return false;
126 for (
size_t i = 0; i <
value.size(); ++i)
127 if (!
value.at(i).isString())
return false;
131bool stringMapIsStrict(Value
value) {
132 if (!
value.isObject())
return false;
133 for (
const auto &
key :
value.keys())
134 if (!
value.get(
key.c_str()).isString()) return false;
141 if (def.
id.empty())
return;
143 auto old =
t.find(def.
id);
145 bool hadOld = old !=
t.end();
150 t[def.
id] = normalized;
151 if (idOnCycle(
t, def.
id)) {
162 auto it =
t.find(
id);
163 return it ==
t.end() ? nullptr : &it->second;
173 std::vector<std::string> out;
174 for (
const auto &[
id, unused] :
questTable()) {
187 if (
error) *
error = err.empty() ?
"invalid json" : err;
192 std::vector<Value> objects;
193 if (
root.isArray()) {
194 for (
size_t i = 0; i <
root.size(); ++i) objects.push_back(
root.at(i));
195 }
else if (
root.isObject()) {
196 objects.push_back(
root);
200 std::vector<QuestDefinition> parsed;
201 std::vector<std::string> candidateIds;
202 for (
const auto &o : objects) {
204 if (def.
id.empty())
continue;
205 parsed.push_back(def);
206 candidateIds.push_back(def.
id);
211 for (
const auto &def : parsed) proposed[def.id] = def;
212 std::unordered_set<std::string> rejected;
213 for (
const auto &
id : candidateIds) {
214 if (idOnCycle(proposed,
id)) rejected.insert(
id);
218 for (
const auto &def : parsed) {
219 if (rejected.count(def.id))
continue;
227 std::string parseError;
229 if (!doc.
valid())
return strictFailure(parseError.empty() ?
"invalid JSON" : parseError,
"$");
232 if (!
root.isArray() && !
root.isObject())
233 return strictFailure(
"quest catalogue must be an object or array",
"$");
235 std::vector<Value> objects;
236 if (
root.isArray()) {
237 if (
root.size() == 0)
return strictFailure(
"quest catalogue must not be empty",
"$");
238 for (
size_t i = 0; i <
root.size(); ++i) objects.push_back(
root.at(i));
240 objects.push_back(
root);
243 std::unordered_map<std::string, QuestDefinition> proposed;
244 for (
size_t questIndex = 0; questIndex < objects.size(); ++questIndex) {
245 const Value object = objects[questIndex];
246 const std::string base =
"$[" + std::to_string(questIndex) +
"]";
247 if (!
object.isObject())
return strictFailure(
"quest entry must be an object", base);
249 const Value idValue =
object.
get(
"id");
251 return strictFailure(
"quest id must be a non-empty stable id of at most 256 bytes", base +
".id");
255 if (proposed.count(definition.id) != 0)
256 return strictFailure(
"duplicate quest id", base +
".id");
258 const Value startPolicy =
object.
get(
"startPolicy");
259 if (startPolicy && (!startPolicy.
isString() ||
261 return strictFailure(
"startPolicy must be 'manual' or 'auto'", base +
".startPolicy");
262 definition.startPolicy = startPolicy ? startPolicy.
asString() :
"manual";
264 const Value completePolicy =
object.
get(
"completePolicy");
265 if (completePolicy && (!completePolicy.
isString() ||
266 (completePolicy.
asString() !=
"auto" && completePolicy.
asString() !=
"claim")))
267 return strictFailure(
"completePolicy must be 'auto' or 'claim'", base +
".completePolicy");
268 definition.completePolicy = completePolicy ? completePolicy.
asString() :
"auto";
270 const Value prerequisites =
object.
get(
"requires");
271 if (prerequisites && !stringArrayIsStrict(prerequisites))
272 return strictFailure(
"requires must be an array of quest ids", base +
".requires");
273 std::unordered_set<std::string> seenRequirements;
274 definition.requiresIds = prerequisites ? prerequisites.
toStringArray() : std::vector<std::string>{};
275 for (
size_t i = 0; i < definition.requiresIds.size(); ++i) {
276 const auto &
required = definition.requiresIds[i];
278 return strictFailure(
"requires contains an invalid or duplicate quest id",
279 base +
".requires[" + std::to_string(i) +
"]");
284 return strictFailure(
"objectives must be an array", base +
".objectives");
285 std::unordered_set<std::string> seenObjectives;
286 for (
size_t i = 0; i <
objectives.size(); ++i) {
288 const auto path = base +
".objectives[" + std::to_string(i) +
"]";
289 if (!item.
isObject())
return strictFailure(
"objective must be an object",
path);
290 const Value objectiveId = item.
get(
"id");
291 const Value topic = item.
get(
"topic");
293 !seenObjectives.insert(objectiveId.
asString()).second)
294 return strictFailure(
"objective id must be unique and stable",
path +
".id");
296 return strictFailure(
"objective topic must be a stable id",
path +
".topic");
299 return strictFailure(
"objective target must be empty or a stable id",
path +
".target");
302 return strictFailure(
"objective count must be a positive integer",
path +
".count");
303 definition.objectives.push_back(
308 const Value rewards =
object.
get(
"rewards");
309 if (rewards && !rewards.
isArray())
return strictFailure(
"rewards must be an array", base +
".rewards");
310 for (
size_t i = 0; i < rewards.
size(); ++i) {
311 const Value item = rewards.
at(i);
312 const auto path = base +
".rewards[" + std::to_string(i) +
"]";
313 if (!item.
isObject())
return strictFailure(
"reward must be an object",
path);
316 const Value amount = item.
get(
"amount");
317 if (!
type.isString() || !validContentId(
type.asString()))
318 return strictFailure(
"reward type must be a stable id",
path +
".type");
319 if (!
id.isString() || !validContentId(
id.asString()))
320 return strictFailure(
"reward id must be a stable id",
path +
".id");
322 return strictFailure(
"reward amount must be finite and positive",
path +
".amount");
323 definition.rewards.push_back({
type.asString(),
id.asString(), amount.
asDouble()});
326 const Value tags =
object.
get(
"tags");
327 if (tags && !stringArrayIsStrict(tags))
328 return strictFailure(
"tags must be an array of strings", base +
".tags");
329 definition.tags = tags ? tags.
toStringArray() : std::vector<std::string>{};
330 for (
size_t i = 0; i < definition.tags.size(); ++i)
331 if (!validContentId(definition.tags[i]))
332 return strictFailure(
"tag must be a stable id", base +
".tags[" + std::to_string(i) +
"]");
334 const Value extra =
object.
get(
"extra");
335 if (extra && !stringMapIsStrict(extra))
336 return strictFailure(
"extra must be an object containing only string values", base +
".extra");
338 for (
const auto &
key : extra.
keys()) definition.extra[
key] = extra.
get(
key.c_str()).asString();
340 proposed.emplace(definition.id, std::move(definition));
343 for (
const auto &[
id, definition] : proposed) {
344 for (
const auto &
required : definition.requiresIds)
346 return strictFailure(
"required quest is missing from replacement catalogue",
id +
".requires");
347 if (idOnCycle(proposed,
id))
return strictFailure(
"quest prerequisite cycle detected",
id +
".requires");
任务 / 成就 / 教程的通用定义模板与进程级注册表。
Battle::Objectives objectives
std::set< std::string > visiting
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
EVENGINE_API_FOUNDATION public API.
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
EVENGINE_API_FOUNDATION public API.
bool isObject() const
True when object.
double asDouble(double fallback=0.0) const
As double.
Value get(const char *key) const
Returns the value.
size_t size() const
Returns the size of .
bool isArray() const
True when array.
Value at(size_t index) const
At.
bool isString() const
True when string.
bool isNumber() const
True when number.
std::vector< std::string > keys() const
Keys.
std::string asString(const std::string &fallback={}) const
As string.
std::vector< std::string > toStringArray() const
To string array.
static int loadFromJson(const std::string &json, std::string *error=nullptr)
从 JSON 数组/对象批量注册;缺 id、重复 objective id、环形 requires 的定义被拒绝。
static void registerQuest(const QuestDefinition &def)
Registers quest.
static std::vector< std::string > ids()
Ids.
static int count()
Returns the number of .
static void clear()
Clears .
static eve::Result< int > replaceFromJsonStrict(const std::string &json)
Strictly validate and atomically replace every registered quest definition.
static bool remove(const std::string &id)
Remove a quest by id (compatibility facade returning whether it was present).
static bool contains(const std::string &id)
Return whether one exact stable quest id is registered.
static const QuestDefinition * find(const std::string &id)
Find a registered quest definition by id.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Result< int > invalid(std::string message)
Invalid.
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
std::unordered_map< std::string, QuestDefinition > & questTable()
std::unordered_map< std::string, SkillDefinition > & table()
bool hasTag(const std::string &tag) const
True when tag.
std::vector< std::string > tags
std::unordered_map< std::string, std::string > extra
std::string startPolicy
"manual" | "auto",缺省 "manual"。
std::string completePolicy
"auto" | "claim",缺省 "auto"。
std::string getExtra(const std::string &key, const std::string &fallback={}) const
Returns the extra.