载入中...
搜索中...
未找到
ProcgenScriptTarget.cpp
浏览该文件的文档.
2
5
6#include <charconv>
7#include <cmath>
8#include <cstdlib>
9#include <utility>
10
11namespace eve::procgen_editing {
12namespace {
13
14EditorDiagnostic diagnostic(const char* rule, editing::DiagnosticSeverity severity, std::string message) {
16 std::move(message));
17}
18
19const EditorValue* field(const EditorValue& value, const char* key) {
20 const auto* object = value.getIf<EditorValue::Object>();
21 if (!object) return nullptr;
22 const auto found = object->find(key);
23 return found == object->end() ? nullptr : &found->second;
24}
25
26bool hasErrors(const std::vector<EditorDiagnostic>& diagnostics) {
27 for (const auto& item : diagnostics)
28 if (item.severity() == editing::DiagnosticSeverity::Error) return true;
29 return false;
30}
31
32std::string requireString(const EditorValue& object, const char* key, std::string fallback = {}) {
33 const EditorValue* entry = field(object, key);
34 if (!entry) return fallback;
35 if (const auto* text = entry->getIf<std::string>()) return *text;
36 return fallback;
37}
38
39bool readNumber(const EditorValue& object, const char* key, double& output) {
40 const EditorValue* entry = field(object, key);
41 if (!entry) return false;
42 if (const auto* real = entry->getIf<double>()) {
43 output = *real;
44 return std::isfinite(output);
45 }
46 if (const auto* integer = entry->getIf<std::int64_t>()) {
47 output = static_cast<double>(*integer);
48 return true;
49 }
50 return false;
51}
52
53procgen::ParamKind parseKind(const std::string& kind) {
54 if (kind == "int" || kind == "integer") return procgen::ParamKind::Integer;
55 if (kind == "float" || kind == "number") return procgen::ParamKind::Float;
56 if (kind == "bool" || kind == "boolean") return procgen::ParamKind::Boolean;
57 if (kind == "choice" || kind == "enum") return procgen::ParamKind::Choice;
59}
60
61std::string encodeDefault(const procgen::ParamDescriptor& param, const EditorValue& value) {
62 if (const auto* integer = value.getIf<std::int64_t>()) return std::to_string(*integer);
63 if (const auto* real = value.getIf<double>()) {
64 return std::to_string(*real);
65 }
66 if (const auto* flag = value.getIf<bool>()) return *flag ? "1" : "0";
67 if (const auto* text = value.getIf<std::string>()) return *text;
68 return param.defaultValue;
69}
70
71} // namespace
72
74
76 return {editing::TargetId(id_),
77 "procgen-script",
78 revision_,
79 false,
81}
82
84 return capability == propertyCapabilityId() ? static_cast<editing::IPropertyProvider*>(this) : nullptr;
85}
86
87bool ProcgenScriptDocumentTarget::matches(const editing::SelectionSnapshot& selection) const {
88 return selection.items.size() == 1 && selection.items[0].target == editing::TargetId(id_) &&
89 selection.items[0].type == "procgen-script";
90}
91
93 const editing::SelectionSnapshot& selection) const {
94 if (!matches(selection))
96 eve::DiagnosticCode::InvalidArgument, "Procgen script selection mismatch",
97 "editor.procgen-script.selection"));
99}
100
101EditorValue ProcgenScriptDocumentTarget::defaultValue(const procgen::ParamDescriptor& param) {
102 try {
103 switch (param.kind) {
105 return std::int64_t{std::stoll(param.defaultValue.empty() ? "0" : param.defaultValue)};
107 return std::stod(param.defaultValue.empty() ? "0" : param.defaultValue);
109 return param.defaultValue == "1" || param.defaultValue == "true";
112 return param.defaultValue;
113 }
114 } catch (...) {
115 return {};
116 }
117 return {};
118}
119
120editing::PropertyType ProcgenScriptDocumentTarget::propertyType(procgen::ParamKind kind) {
121 switch (kind) {
132 }
134}
135
137 for (const auto& param : params_)
138 if (param.key == key) return &param;
139 return nullptr;
140}
141
144 out.typeId = "procgen-script";
145 for (const auto& param : params_) {
147 descriptor.path = editing::PropertyPath("param." + param.key);
148 descriptor.displayNameKey = param.displayName.empty() ? param.key : param.displayName;
149 descriptor.descriptionKey = param.description;
150 descriptor.category = param.category.empty() ? "Generator" : param.category;
151 descriptor.type = propertyType(param.kind);
152 descriptor.defaultValue = defaultValue(param);
155 descriptor.enumItems = param.choices;
156 if (param.hasMinimum) descriptor.numeric.minimum = param.minimum;
157 if (param.hasMaximum) descriptor.numeric.maximum = param.maximum;
158 if (param.step != 0.0) descriptor.numeric.step = param.step;
159 out.properties.push_back(std::move(descriptor));
160 }
161 return out;
162}
163
165 const editing::PropertyPath& path) const {
166 if (!matches(selection) || path.value().rfind("param.", 0) != 0) return {};
167 const auto found = values_.find(path.value().substr(6));
168 return found == values_.end() ? editing::PropertyReadResult{}
170}
171
172EditorValue ProcgenScriptDocumentTarget::schemaValue() const {
174 rows.reserve(params_.size());
175 for (const auto& param : params_) {
177 row["key"] = param.key;
178 row["displayName"] = param.displayName;
179 row["description"] = param.description;
180 row["category"] = param.category;
181 switch (param.kind) {
183 row["kind"] = std::string("int");
184 break;
186 row["kind"] = std::string("float");
187 break;
189 row["kind"] = std::string("bool");
190 break;
192 row["kind"] = std::string("choice");
193 break;
195 row["kind"] = std::string("string");
196 break;
197 }
198 row["defaultValue"] = param.defaultValue;
199 row["advanced"] = param.advanced;
200 if (param.hasMinimum) row["min"] = param.minimum;
201 if (param.hasMaximum) row["max"] = param.maximum;
202 if (param.step != 0.0) row["step"] = param.step;
203 if (!param.choices.empty()) {
205 for (const auto& choice : param.choices) choices.emplace_back(choice);
206 row["choices"] = std::move(choices);
207 }
208 rows.emplace_back(std::move(row));
209 }
210 return rows;
211}
212
213EditorValue ProcgenScriptDocumentTarget::contentValue() const {
214 return EditorValue::Object{{"uri", uri_},
215 {"id", moduleId_},
216 {"displayName", displayName_},
217 {"kind", kind_},
218 {"schema", schemaValue()},
219 {"values", values_}};
220}
221
222void ProcgenScriptDocumentTarget::applySpec(ProcgenScriptModuleSpec spec) {
224 for (const auto& param : spec.params) {
225 const auto existing = values_.find(param.key);
226 if (existing != values_.end()) {
227 editing::PropertyDescriptor descriptor;
228 descriptor.path = editing::PropertyPath("param." + param.key);
229 descriptor.type = propertyType(param.kind);
230 descriptor.defaultValue = defaultValue(param);
231 descriptor.enumItems = param.choices;
232 if (param.hasMinimum) descriptor.numeric.minimum = param.minimum;
233 if (param.hasMaximum) descriptor.numeric.maximum = param.maximum;
234 if (editing::validatePropertyValue(descriptor, existing->second).ok()) {
235 next[param.key] = existing->second;
236 continue;
237 }
238 }
239 next[param.key] = defaultValue(param);
240 }
241 uri_ = std::move(spec.uri);
242 moduleId_ = std::move(spec.id);
243 displayName_ = std::move(spec.displayName);
244 kind_ = std::move(spec.kind);
245 params_ = std::move(spec.params);
246 values_ = std::move(next);
247}
248
250 std::string displayName, std::string kind,
251 const EditorValue& schema) {
252 if (id.empty())
253 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
254 editing::RuleId("editor.procgen-script.id"),
255 "Generator module id must not be empty");
256 if (kind.empty()) kind = "points";
257 if (kind != "points")
258 return eve::editing::failed<ProcgenScriptModuleSpec>(
259 EditorStatus::Unsupported, editing::RuleId("editor.procgen-script.kind"),
260 "Only kind=points generators are supported in this editor version");
261 const auto* rows = schema.getIf<EditorValue::Array>();
262 if (!rows)
263 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
264 editing::RuleId("editor.procgen-script.schema"),
265 "Generator schema must be an array");
266 if (rows->size() > 128)
267 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
268 editing::RuleId("editor.procgen-script.schema-size"),
269 "Generator schema exceeds 128 parameters");
270
272 spec.uri = std::move(uri);
273 spec.id = std::move(id);
274 spec.displayName = displayName.empty() ? spec.id : std::move(displayName);
275 spec.kind = std::move(kind);
276 spec.params.reserve(rows->size());
277 for (const auto& rowValue : *rows) {
278 const auto* row = rowValue.getIf<EditorValue::Object>();
279 if (!row)
280 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
281 editing::RuleId("editor.procgen-script.param"),
282 "Each schema entry must be an object");
284 param.key = requireString(rowValue, "key");
285 if (param.key.empty())
286 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
287 editing::RuleId("editor.procgen-script.param-key"),
288 "Parameter key must not be empty");
289 for (const auto& existing : spec.params)
290 if (existing.key == param.key)
291 return eve::editing::failed<ProcgenScriptModuleSpec>(EditorStatus::Rejected,
292 editing::RuleId("editor.procgen-script.param-dup"),
293 "Duplicate parameter key: " + param.key);
294 param.displayName = requireString(rowValue, "displayName", requireString(rowValue, "label", param.key));
295 param.description = requireString(rowValue, "description");
296 param.category = requireString(rowValue, "category", "Generator");
297 param.kind = parseKind(requireString(rowValue, "kind", "string"));
298 param.advanced = false;
299 if (const auto* advanced = field(rowValue, "advanced"))
300 if (const auto* flag = advanced->getIf<bool>()) param.advanced = *flag;
301
302 double numeric = 0.0;
303 if (readNumber(rowValue, "min", numeric)) {
304 param.hasMinimum = true;
305 param.minimum = numeric;
306 }
307 if (readNumber(rowValue, "max", numeric)) {
308 param.hasMaximum = true;
309 param.maximum = numeric;
310 }
311 if (readNumber(rowValue, "step", numeric)) param.step = numeric;
312
313 if (const EditorValue* choices = field(rowValue, "choices")) {
314 if (const auto* array = choices->getIf<EditorValue::Array>()) {
315 for (const auto& choice : *array) {
316 const auto* text = choice.getIf<std::string>();
317 if (!text)
318 return eve::editing::failed<ProcgenScriptModuleSpec>(
319 EditorStatus::Rejected, editing::RuleId("editor.procgen-script.choice"),
320 "Choice values must be strings");
321 param.choices.push_back(*text);
322 }
323 }
324 }
325
326 EditorValue def;
327 if (const EditorValue* defaultField = field(rowValue, "defaultValue")) {
328 def = *defaultField;
329 } else if (const EditorValue* defaultField = field(rowValue, "default")) {
330 def = *defaultField;
331 } else {
332 switch (param.kind) {
334 def = std::int64_t{0};
335 break;
337 def = 0.0;
338 break;
340 def = false;
341 break;
344 def = param.choices.empty() ? std::string{} : param.choices.front();
345 break;
346 }
347 }
348 param.defaultValue = encodeDefault(param, def);
349 spec.params.push_back(std::move(param));
350 }
351 return eve::editing::applied(std::move(spec));
352}
353
355 ProcgenScriptModuleSpec spec) const {
356 if (spec.id.empty())
357 return eve::editing::failed<editing::DomainOperation>(EditorStatus::Rejected,
358 editing::RuleId("editor.procgen-script.id"),
359 "Generator module id must not be empty");
360 if (spec.kind != "points")
361 return eve::editing::failed<editing::DomainOperation>(
362 EditorStatus::Unsupported, editing::RuleId("editor.procgen-script.kind"),
363 "Only kind=points generators are supported in this editor version");
364 if (spec.params.size() > 128)
365 return eve::editing::failed<editing::DomainOperation>(EditorStatus::Rejected,
366 editing::RuleId("editor.procgen-script.schema-size"),
367 "Generator schema exceeds 128 parameters");
368
369 ProcgenScriptDocumentTarget candidate = *this;
370 candidate.applySpec(std::move(spec));
371 if (hasErrors(candidate.validate()))
372 return eve::editing::failed<editing::DomainOperation>(EditorStatus::Rejected,
373 editing::RuleId("editor.procgen-script.invalid"),
374 "Generator module validation failed");
375
377 operation.type = "procgen.script.replace.v1";
378 operation.inverseType = operation.type;
379 operation.target = editing::TargetId(id_);
380 operation.payload = candidate.contentValue();
381 operation.inverse = contentValue();
382 operation.hasInverse = true;
383 operation.mergeKey = "procgen-script:" + id_ + ":module";
384 return eve::editing::applied(std::move(operation));
385}
386
389 editing::PropertySetMode mode) const {
390 if (mode == editing::PropertySetMode::Reset) return makeReset(selection, path);
391 const auto descriptor = schema(selection).find(path);
392 if (!matches(selection) || !descriptor || mode != editing::PropertySetMode::Absolute ||
394 return eve::editing::failed<editing::DomainOperation>(EditorStatus::Rejected,
395 editing::RuleId("editor.procgen-script.set"),
396 "Generator parameter edit is invalid");
397 auto values = values_;
398 values[path.value().substr(6)] = value;
400 operation.type = "procgen.script.replace.v1";
401 operation.inverseType = operation.type;
402 operation.target = editing::TargetId(id_);
403 EditorValue content = contentValue();
406 payload["values"] = std::move(values);
407 operation.payload = std::move(payload);
408 operation.inverse = contentValue();
409 operation.hasInverse = true;
410 operation.affectedProperties.push_back(path.value());
411 operation.mergeKey = "procgen-script:" + id_ + ":" + path.value();
412 return eve::editing::applied(std::move(operation));
413}
414
416 const editing::SelectionSnapshot& selection, const editing::PropertyPath& path) const {
417 const auto descriptor = schema(selection).find(path);
418 if (!descriptor)
419 return eve::editing::failed<editing::DomainOperation>(EditorStatus::Unsupported,
420 editing::RuleId("editor.procgen-script.property"),
421 "Unknown generator parameter");
422 return makeSet(selection, path, descriptor->defaultValue, editing::PropertySetMode::Absolute);
423}
424
425std::vector<EditorDiagnostic> ProcgenScriptDocumentTarget::validate() const {
426 std::vector<EditorDiagnostic> diagnostics;
427 if (!kind_.empty() && kind_ != "points")
428 diagnostics.push_back(diagnostic("editor.procgen-script.kind", editing::DiagnosticSeverity::Error,
429 "Only kind=points generators are supported"));
430 const auto propertySchema = schema({});
431 for (const auto& param : params_) {
432 const auto found = values_.find(param.key);
433 const auto descriptor = propertySchema.find(editing::PropertyPath("param." + param.key));
434 if (found == values_.end() || !descriptor ||
436 diagnostics.push_back(diagnostic("editor.procgen-script.parameter", editing::DiagnosticSeverity::Error,
437 "Invalid generator parameter: " + param.key));
438 }
439 if (values_.size() != params_.size())
440 diagnostics.push_back(diagnostic("editor.procgen-script.extra", editing::DiagnosticSeverity::Error,
441 "Generator document contains unknown parameters"));
442 return diagnostics;
443}
444
446 if (operation.target != editing::TargetId(id_) || operation.type != "procgen.script.replace.v1")
447 return eve::editing::failed<void>(EditorStatus::Rejected, editing::RuleId("editor.procgen-script.operation"),
448 "Generator operation is invalid");
449 const auto parsed = parseSpec(requireString(operation.payload, "uri"), requireString(operation.payload, "id"),
450 requireString(operation.payload, "displayName"),
451 requireString(operation.payload, "kind", "points"),
452 field(operation.payload, "schema") ? *field(operation.payload, "schema")
453 : EditorValue{});
454 if (!parsed.ok()) return Result<void>::failure(parsed.status());
455 const EditorValue* valuesField = field(operation.payload, "values");
456 const auto* values = valuesField ? valuesField->getIf<EditorValue::Object>() : nullptr;
457 if (!values || values->size() > 128)
458 return eve::editing::failed<void>(EditorStatus::Rejected, editing::RuleId("editor.procgen-script.payload"),
459 "Generator payload is invalid");
460
461 ProcgenScriptDocumentTarget candidate = *this;
462 candidate.applySpec(parsed.value());
463 candidate.values_ = *values;
464 if (hasErrors(candidate.validate()))
465 return eve::editing::failed<void>(EditorStatus::Rejected, editing::RuleId("editor.procgen-script.invalid"),
466 "Generator validation failed");
467 *this = std::move(candidate);
468 ++revision_;
469 dirty_.include(0, 0);
470 return eve::editing::applied<void>();
471}
472
473std::unique_ptr<editing::IDomainOperationTarget> ProcgenScriptDocumentTarget::cloneDomainState() const {
474 return std::make_unique<ProcgenScriptDocumentTarget>(*this);
475}
476
478 std::unique_ptr<editing::IDomainOperationTarget> candidate) {
479 auto* typed = dynamic_cast<ProcgenScriptDocumentTarget*>(candidate.get());
480 if (!typed || typed->id_ != id_)
481 return eve::editing::failed<void>(EditorStatus::Conflict, editing::RuleId("editor.procgen-script.candidate"),
482 "Generator candidate mismatch");
483 *this = *typed;
484 return eve::editing::applied<void>();
485}
486
488 return EditorValue::Object{{"schemaVersion", std::int64_t{1}}, {"content", contentValue()}};
489}
490
492 const EditorValue* versionField = field(snapshot, "schemaVersion");
493 const EditorValue* content = field(snapshot, "content");
494 const auto* version = versionField ? versionField->getIf<std::int64_t>() : nullptr;
495 if (!version || *version != 1 || !content)
496 return eve::editing::failed<void>(EditorStatus::Unsupported, editing::RuleId("editor.procgen-script.snapshot"),
497 "Unsupported generator snapshot");
500 operation.type = "procgen.script.replace.v1";
501 operation.payload = *content;
502 auto result = applyDomainOperation(operation);
503 if (result.ok()) dirty_.clear();
504 return result;
505}
506
507} // namespace eve::procgen_editing
ActionParameterOperation operation
double value
Value::Object payload
std::string output
std::string descriptor
int rows
std::string message
std::uint32_t key
std::string text
TokenKind kind
std::set< std::string > choices
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
bool found
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
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
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
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
UI-independent property access for an immutable selection snapshot.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Revisioned Params document for a script-hosted procedural generator.
Result< void > applyDomainOperation(const editing::DomainOperation &) override
Applies domain operation.
Result< void > loadSnapshot(const EditorValue &)
Loads snapshot.
Result< editing::DomainOperation > makeSet(const editing::SelectionSnapshot &, const editing::PropertyPath &, const EditorValue &, editing::PropertySetMode) const override
Make set.
Result< editing::DomainOperation > makeLoadModule(ProcgenScriptModuleSpec spec) const
Replace module identity and schema, remapping values (drop unknown, fill defaults).
eve::Result< eve::Revision > currentRevision(const editing::SelectionSnapshot &) const override
Current revision.
const procgen::ParamDescriptor * findParam(const std::string &key) const
Find one reflected parameter by stable key.
editing::PropertySchema schema(const editing::SelectionSnapshot &) const override
Schema.
std::unique_ptr< editing::IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
const std::string & displayName() const
Display name.
Result< void > commitDomainState(std::unique_ptr< editing::IDomainOperationTarget >) override
Commits domain state.
editing::PropertyReadResult read(const editing::SelectionSnapshot &, const editing::PropertyPath &) const override
Reads .
static editing::CapabilityId propertyCapabilityId()
Capability identity published by describe() for Inspector editing.
std::vector< EditorDiagnostic > validate() const
Validate.
const EditorValue::Object & values() const
Values.
void * queryCapability(const editing::CapabilityId &) override
Query Inspector property capability.
Result< editing::DomainOperation > makeReset(const editing::SelectionSnapshot &, const editing::PropertyPath &) const override
Make reset.
const std::vector< procgen::ParamDescriptor > & params() const
Params.
static Result< ProcgenScriptModuleSpec > parseSpec(std::string uri, std::string id, std::string displayName, std::string kind, const EditorValue &schema)
Parse a schema array into a module spec.
ProcgenScriptDocumentTarget(std::string id)
Construct an empty generator document.
editing::TargetDescriptor describe() const override
Describe.
const char * defaultValue
EVENGINE_API_PLATFORM bool hasErrors(const std::vector< DnutDiagnostic > &diagnostics)
Return whether any diagnostic is an error.
eve::Severity DiagnosticSeverity
StrongId< TargetIdTag > TargetId
Definition EditingIds.h:57
PropertyType
PropertyType public API.
StrongId< RuleIdTag > RuleId
Definition EditingIds.h:60
PropertySetMode
PropertySetMode public API.
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
Result< void > validatePropertyValue(const PropertyDescriptor &descriptor, const Value &value)
Validate property value.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
const EditorValue * field(const EditorValue &value, const char *name)
bool readNumber(const EditorValue &value, const char *name, float &output)
editing::Diagnostic EditorDiagnostic
ParamKind
Semantic type used by schema-driven recipe parameter editors.
Definition ParamSchema.h:11
DomainOperation public API.
void include(int x, int y)
Include.
void clear()
Clears clear.
PropertyDescriptor public API.
PropertyReadResult public API.
EVENGINE_API_PLATFORM public API.
std::vector< PropertyDescriptor > properties
PropertyDescriptorLookup find(const PropertyPath &path) const
Finds find.
SelectionSnapshot public API.
std::vector< SelectionItem > items
Stable discovery metadata exposed by every editable target.
UI-independent metadata for one procedural recipe parameter.
Definition ParamSchema.h:14
std::vector< std::string > choices
Definition ParamSchema.h:26
Authored identity and reflected parameter schema for one script generator.
std::vector< procgen::ParamDescriptor > params