载入中...
搜索中...
未找到
Quest.cpp
浏览该文件的文档.
1#include "rpg/Quest.h"
2
3#include "common/Json.h"
4
5#include <algorithm>
6#include <climits>
7#include <cmath>
8#include <functional>
9#include <unordered_set>
10
11namespace eve::rpg {
12
14
15bool QuestDefinition::hasTag(const std::string &tag) const {
16 return std::find(tags.begin(), tags.end(), tag) != tags.end();
17}
18
19std::string QuestDefinition::getExtra(const std::string &key, const std::string &fallback) const {
20 auto it = extra.find(key);
21 return it == extra.end() ? fallback : it->second;
22}
23
24std::unordered_map<std::string, QuestDefinition> &questTable() {
25 static std::unordered_map<std::string, QuestDefinition> t;
26 return t;
27}
28
29namespace {
30
31std::string normalizeStart(const std::string &policy) {
32 return policy == "auto" ? "auto" : "manual";
33}
34
35std::string normalizeComplete(const std::string &policy) {
36 return policy == "claim" ? "claim" : "auto";
37}
38
39// 环检测:图的边是 id -> requires[i];未在表中的前置 id 不算环。自依赖是环。
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 {
44 if (visiting.count(id)) return true;
45 if (visited.count(id)) return false;
46 auto it = table.find(id);
47 if (it == table.end()) return false;
48 visiting.insert(id);
49 for (const auto &req : it->second.requiresIds) {
50 if (dfs(req)) return true;
51 }
52 visiting.erase(id);
53 visited.insert(id);
54 return false;
55 };
56 return dfs(start);
57}
58
59QuestObjective parseObjective(Value o) {
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;
67 return obj;
68}
69
70RewardSpec parseReward(Value o) {
71 RewardSpec reward;
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);
76 return reward;
77}
78
79// 返回 id 为空的定义表示该 JSON 对象被拒绝(缺 id / 缺目标 id / 目标 id 重复)。
80QuestDefinition parseQuestObject(Value o) {
81 QuestDefinition def;
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");
89
90 const Value objectives = o.get("objectives");
91 std::unordered_set<std::string> seen;
92 bool invalid = false;
93 for (size_t i = 0; i < objectives.size(); ++i) {
94 QuestObjective obj = parseObjective(objectives.at(i));
95 if (obj.id.empty() || !seen.insert(obj.id).second) {
96 invalid = true;
97 break;
98 }
99 def.objectives.push_back(std::move(obj));
100 }
101 if (invalid) {
102 def.id.clear(); // 拒绝整条定义
103 return def;
104 }
105
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));
110 }
111 return def;
112}
113
114eve::Result<int> strictFailure(std::string message, std::string path) {
116 eve::DiagnosticCode::ParseError, message, path, {}, "rpg.quest-content"));
117}
118
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; });
122}
123
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;
128 return true;
129}
130
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;
135 return true;
136}
137
138} // namespace
139
141 if (def.id.empty()) return;
142 auto &t = questTable();
143 auto old = t.find(def.id);
144 auto saved = old != t.end() ? old->second : QuestDefinition{};
145 bool hadOld = old != t.end();
146
147 QuestDefinition normalized = def;
148 normalized.startPolicy = normalizeStart(def.startPolicy);
149 normalized.completePolicy = normalizeComplete(def.completePolicy);
150 t[def.id] = normalized;
151 if (idOnCycle(t, def.id)) {
152 if (hadOld) {
153 t[def.id] = saved;
154 } else {
155 t.erase(def.id);
156 }
157 }
158}
159
160const QuestDefinition *QuestRegistry::find(const std::string &id) {
161 auto &t = questTable();
162 auto it = t.find(id);
163 return it == t.end() ? nullptr : &it->second;
164}
165
166bool QuestRegistry::remove(const std::string &id) { return questTable().erase(id) > 0; }
167
168void QuestRegistry::clear() { questTable().clear(); }
169
170int QuestRegistry::count() { return int(questTable().size()); }
171
172std::vector<std::string> QuestRegistry::ids() {
173 std::vector<std::string> out;
174 for (const auto &[id, unused] : questTable()) {
175 (void)unused;
176 out.push_back(id);
177 }
178 return out;
179}
180
181bool QuestRegistry::contains(const std::string &id) { return questTable().count(id) != 0; }
182
183int QuestRegistry::loadFromJson(const std::string &json, std::string *error) {
184 std::string err;
185 const eve::json::Document doc = eve::json::Document::parse(json, &err);
186 if (!doc.valid()) {
187 if (error) *error = err.empty() ? "invalid json" : err;
188 return 0;
189 }
190
191 const Value root = doc.root();
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);
197 }
198
199 auto &t = questTable();
200 std::vector<QuestDefinition> parsed;
201 std::vector<std::string> candidateIds;
202 for (const auto &o : objects) {
203 QuestDefinition def = parseQuestObject(o);
204 if (def.id.empty()) continue;
205 parsed.push_back(def);
206 candidateIds.push_back(def.id);
207 }
208
209 // proposed = 当前表 + 本批候选;对本批候选在 proposed 上做环检测。
210 auto proposed = t;
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);
215 }
216
217 int n = 0;
218 for (const auto &def : parsed) {
219 if (rejected.count(def.id)) continue;
220 t[def.id] = def;
221 ++n;
222 }
223 return n;
224}
225
227 std::string parseError;
228 const eve::json::Document doc = eve::json::Document::parse(json, &parseError);
229 if (!doc.valid()) return strictFailure(parseError.empty() ? "invalid JSON" : parseError, "$");
230
231 const Value root = doc.root();
232 if (!root.isArray() && !root.isObject())
233 return strictFailure("quest catalogue must be an object or array", "$");
234
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));
239 } else {
240 objects.push_back(root);
241 }
242
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);
248
249 const Value idValue = object.get("id");
250 if (!idValue.isString() || !validContentId(idValue.asString()))
251 return strictFailure("quest id must be a non-empty stable id of at most 256 bytes", base + ".id");
252
253 QuestDefinition definition;
254 definition.id = idValue.asString();
255 if (proposed.count(definition.id) != 0)
256 return strictFailure("duplicate quest id", base + ".id");
257
258 const Value startPolicy = object.get("startPolicy");
259 if (startPolicy && (!startPolicy.isString() ||
260 (startPolicy.asString() != "manual" && startPolicy.asString() != "auto")))
261 return strictFailure("startPolicy must be 'manual' or 'auto'", base + ".startPolicy");
262 definition.startPolicy = startPolicy ? startPolicy.asString() : "manual";
263
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";
269
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];
277 if (!validContentId(required) || !seenRequirements.insert(required).second)
278 return strictFailure("requires contains an invalid or duplicate quest id",
279 base + ".requires[" + std::to_string(i) + "]");
280 }
281
282 const Value objectives = object.get("objectives");
283 if (objectives && !objectives.isArray())
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) {
287 const Value item = objectives.at(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");
292 if (!objectiveId.isString() || !validContentId(objectiveId.asString()) ||
293 !seenObjectives.insert(objectiveId.asString()).second)
294 return strictFailure("objective id must be unique and stable", path + ".id");
295 if (!topic.isString() || !validContentId(topic.asString()))
296 return strictFailure("objective topic must be a stable id", path + ".topic");
297 const Value target = item.get("target");
298 if (target && (!target.isString() || (!target.asString().empty() && !validContentId(target.asString()))))
299 return strictFailure("objective target must be empty or a stable id", path + ".target");
300 const Value count = item.get("count");
301 if (count && (!count.isInt64() || count.asInt64() <= 0 || count.asInt64() > INT_MAX))
302 return strictFailure("objective count must be a positive integer", path + ".count");
303 definition.objectives.push_back(
304 {objectiveId.asString(), topic.asString(), target ? target.asString() : std::string{},
305 count ? int(count.asInt64()) : 1});
306 }
307
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);
314 const Value type = item.get("type");
315 const Value id = item.get("id");
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");
321 if (!amount.isNumber() || !std::isfinite(amount.asDouble()) || amount.asDouble() <= 0.0)
322 return strictFailure("reward amount must be finite and positive", path + ".amount");
323 definition.rewards.push_back({type.asString(), id.asString(), amount.asDouble()});
324 }
325
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) + "]");
333
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");
337 if (extra) {
338 for (const auto &key : extra.keys()) definition.extra[key] = extra.get(key.c_str()).asString();
339 }
340 proposed.emplace(definition.id, std::move(definition));
341 }
342
343 for (const auto &[id, definition] : proposed) {
344 for (const auto &required : definition.requiresIds)
345 if (proposed.count(required) == 0)
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");
348 }
349
350 questTable() = std::move(proposed);
352}
353
354} // namespace eve::rpg
LogicalId target
double value
Duration start
int root
Definition AnimSmr.cpp:119
std::string message
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
std::int32_t second
std::int32_t c
bool required
const std::string * tag
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float begin
float t
任务 / 成就 / 教程的通用定义模板与进程级注册表。
Battle::Objectives objectives
float size
Definition TreeMesh.cpp:156
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.
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
EVENGINE_API_FOUNDATION public API.
Definition Json.h:136
bool valid() const
Valid.
Definition Json.h:157
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
Value root() const
Root.
Definition Json.h:159
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
bool isObject() const
True when object.
Definition Json.cpp:331
double asDouble(double fallback=0.0) const
As double.
Definition Json.cpp:377
Value get(const char *key) const
Returns the value.
Definition Json.cpp:336
size_t size() const
Returns the size of .
Definition Json.cpp:351
bool isArray() const
True when array.
Definition Json.cpp:332
Value at(size_t index) const
At.
Definition Json.cpp:358
bool isString() const
True when string.
Definition Json.cpp:330
bool isNumber() const
True when number.
Definition Json.cpp:327
std::vector< std::string > keys() const
Keys.
Definition Json.cpp:343
std::string asString(const std::string &fallback={}) const
As string.
Definition Json.cpp:411
std::vector< std::string > toStringArray() const
To string array.
Definition Json.cpp:431
static int loadFromJson(const std::string &json, std::string *error=nullptr)
从 JSON 数组/对象批量注册;缺 id、重复 objective id、环形 requires 的定义被拒绝。
Definition Quest.cpp:183
static void registerQuest(const QuestDefinition &def)
Registers quest.
Definition Quest.cpp:140
static std::vector< std::string > ids()
Ids.
Definition Quest.cpp:172
static int count()
Returns the number of .
Definition Quest.cpp:170
static void clear()
Clears .
Definition Quest.cpp:168
static eve::Result< int > replaceFromJsonStrict(const std::string &json)
Strictly validate and atomically replace every registered quest definition.
Definition Quest.cpp:226
static bool remove(const std::string &id)
Remove a quest by id (compatibility facade returning whether it was present).
Definition Quest.cpp:166
static bool contains(const std::string &id)
Return whether one exact stable quest id is registered.
Definition Quest.cpp:181
static const QuestDefinition * find(const std::string &id)
Find a registered quest definition by id.
Definition Quest.cpp:160
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
Result< int > invalid(std::string message)
Invalid.
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
std::unordered_map< std::string, QuestDefinition > & questTable()
Definition Quest.cpp:24
std::unordered_map< std::string, SkillDefinition > & table()
Definition Skill.cpp:65
数据驱动的任务模板。
Definition Quest.h:24
bool hasTag(const std::string &tag) const
True when tag.
Definition Quest.cpp:15
std::vector< std::string > tags
Definition Quest.h:34
std::unordered_map< std::string, std::string > extra
Definition Quest.h:35
std::string startPolicy
"manual" | "auto",缺省 "manual"。
Definition Quest.h:27
std::string completePolicy
"auto" | "claim",缺省 "auto"。
Definition Quest.h:29
std::string getExtra(const std::string &key, const std::string &fallback={}) const
Returns the extra.
Definition Quest.cpp:19