12std::string
sha256(std::span<const std::uint8_t>
bytes) {
13 data::HashFunction::Value
digest{};
16 static constexpr char hex[] =
"0123456789abcdef";
17 std::string result =
"sha256:";
20 const auto byte =
static_cast<std::uint8_t
>(
digest.data[
index]);
21 result.push_back(hex[
byte >> 4]);
22 result.push_back(hex[
byte & 0x0f]);
27Result<Value> migrateImageV2ToV3(
const Value&
input) {
32 const auto schema =
object->find(
"schema");
33 const auto version =
object->find(
"schemaVersion");
34 const auto color =
object->find(
"color");
36 if (schema ==
object->end() || !schema->second.isString() || schema->second.asString() !=
"eve.image" ||
37 version ==
object->end() || !version->second.isInt64() || version->second.asInt() != 2 ||
38 object->contains(
"mipCount") || !colorObject || !colorObject->contains(
"transfer") ||
39 !colorObject->at(
"transfer").isString() ||
40 (colorObject->at(
"transfer").asString() !=
"srgb" && colorObject->at(
"transfer").asString() !=
"linear"))
45 migrated[
"schemaVersion"] =
Value(std::int64_t(3));
46 migrated[
"mipCount"] =
Value(std::int64_t(1));
51 const Value& definition) {
55 "asset definition is newer than this reader", std::string(
type),
56 {},
"asset.migration"));
57 if (
type ==
"eve.mesh" &&
current.value() == 3 && (
from.value() == 1 ||
from.value() == 2)) {
59 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
60 object->at(
"schema").asString() !=
"eve.mesh" || !
object->contains(
"schemaVersion") ||
61 !
object->at(
"schemaVersion").isInt64() ||
62 object->at(
"schemaVersion").asInt() !=
static_cast<std::int64_t
>(
from.value()))
66 if (
from.value() == 1) {
67 if (migrated.contains(
"texcoordSets") || migrated.contains(
"colors"))
70 if (
auto uv = migrated.find(
"texcoord0");
uv != migrated.end()) {
71 if (!
uv->second.isBool())
75 if (
uv->second.asBool()) sets.emplace_back(int64_t(0));
77 migrated[
"texcoordSets"] =
Value(std::move(sets));
79 }
else if (migrated.contains(
"colors")) {
83 migrated[
"colors"] =
Value(
false);
84 migrated[
"schemaVersion"] =
Value(int64_t(3));
89 "asset definition is older than the N-1 compatibility window",
90 std::string(
type), {},
"asset.migration"));
91 if (
type ==
"eve.instance-set" &&
from.value() == 1 &&
current.value() >= 2) {
93 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
94 object->at(
"schema").asString() !=
"eve.instance-set" || !
object->contains(
"schemaVersion") ||
95 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 1 ||
96 object->contains(
"prototypes"))
100 migrated[
"schemaVersion"] =
Value(int64_t(2));
106 if (
type ==
"eve.instance-set" &&
from.value() == 2 &&
current.value() >= 3) {
108 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
109 object->at(
"schema").asString() !=
"eve.instance-set" || !
object->contains(
"schemaVersion") ||
110 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 2 ||
111 !
object->contains(
"prototypes") || !
object->at(
"prototypes").isArray())
115 auto prototypes = *migrated[
"prototypes"].getIf<
Value::Array>();
116 for (
auto&
value : prototypes) {
118 if (!prototype || prototype->contains(
"resourceAsset"))
120 "eve.instance-set/2 prototype is malformed", {}, {},
122 (*prototype)[
"resourceAsset"] =
Value(
"");
124 migrated[
"prototypes"] =
Value(std::move(prototypes));
125 migrated[
"schemaVersion"] =
Value(int64_t(3));
130 if (
type ==
"eve.instance-set" &&
from.value() == 3 &&
current.value() >= 4) {
132 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
133 object->at(
"schema").asString() !=
"eve.instance-set" || !
object->contains(
"schemaVersion") ||
134 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 3 ||
135 !
object->contains(
"prototypes") || !
object->at(
"prototypes").isArray())
139 auto prototypes = *migrated[
"prototypes"].getIf<
Value::Array>();
140 for (
auto&
value : prototypes) {
142 if (!prototype || prototype->contains(
"healthyColor") || prototype->contains(
"dryColor") ||
143 prototype->contains(
"bendFactor") || prototype->contains(
"holeEdgePadding") ||
144 prototype->contains(
"useDensityScaling"))
146 "eve.instance-set/3 prototype is malformed", {}, {},
150 (*prototype)[
"bendFactor"] =
Value(0.0);
151 (*prototype)[
"holeEdgePadding"] =
Value(0.0);
152 (*prototype)[
"useDensityScaling"] =
Value(
true);
154 migrated[
"prototypes"] =
Value(std::move(prototypes));
155 migrated[
"schemaVersion"] =
Value(int64_t(4));
160 if (
type ==
"eve.instance-set" &&
from.value() == 4 &&
current.value() == 5) {
162 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
163 object->at(
"schema").asString() !=
"eve.instance-set" || !
object->contains(
"schemaVersion") ||
164 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 4 ||
165 object->contains(
"wavingGrass"))
170 {
"amount",
Value(0.0)}, {
"speed",
Value(0.0)}, {
"strength",
Value(0.0)},
172 migrated[
"schemaVersion"] =
Value(int64_t(5));
175 if (
type ==
"eve.material" &&
from.value() == 5 &&
current.value() == 6) {
177 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
178 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
179 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 5 ||
180 object->contains(
"vegetationSurface"))
185 migrated[
"schemaVersion"] =
Value(int64_t(6));
188 if (
type ==
"eve.material" &&
from.value() == 6 &&
current.value() == 7) {
190 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
191 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
192 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 6)
194 {}, {},
"asset.migration"));
196 migrated[
"schemaVersion"] =
Value(int64_t(7));
197 if (
auto surface = migrated.find(
"vegetationSurface"); surface != migrated.end()) {
199 if (!vegetation || vegetation->contains(
"colors"))
201 "eve.material/6 has unversioned global colors", {}, {},
203 (*vegetation)[
"colors"] =
Value(1.0);
204 (*vegetation)[
"colorsIntensity"] =
Value(1.0);
205 (*vegetation)[
"colorsMask"] =
Value(1.0);
206 (*vegetation)[
"colorsVariation"] =
Value(.5);
207 (*vegetation)[
"vertexOcclusionMinimum"] =
Value(0.0);
208 (*vegetation)[
"vertexOcclusionMaximum"] =
Value(1.0);
209 (*vegetation)[
"invertVertexOcclusionColors"] =
Value(
false);
213 if (
type ==
"eve.material" &&
from.value() == 7 &&
current.value() == 8) {
215 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
216 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
217 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 7 ||
218 object->contains(
"vegetationDetail"))
223 migrated[
"schemaVersion"] =
Value(int64_t(8));
226 if (
type ==
"eve.material" &&
from.value() == 8 &&
current.value() == 9) {
228 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
229 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
230 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 8 ||
231 (
object->contains(
"vegetationAlpha") ||
object->contains(
"vegetationFields")))
234 {},
"asset.migration"));
236 migrated[
"schemaVersion"] =
Value(int64_t(9));
239 if (
type ==
"eve.material" &&
from.value() == 9 &&
current.value() == 10) {
241 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
242 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
243 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 9)
245 {}, {},
"asset.migration"));
247 if (
auto at = migrated.find(
"vegetationFields");
at != migrated.end()) {
250 !
fields->contains(
"colorsUsePivotPosition") || !
fields->contains(
"extrasLayer") ||
251 !
fields->contains(
"extrasUsePivotPosition"))
255 (*fields)[
"motionLayer"] =
Value(int64_t(0));
256 (*fields)[
"vertexLayer"] =
Value(int64_t(0));
257 (*fields)[
"globalSize"] =
Value(1.0);
258 (*fields)[
"sizeFadeStart"] =
Value(0.0);
259 (*fields)[
"sizeFadeEnd"] =
Value(100.0);
261 migrated[
"schemaVersion"] =
Value(int64_t(10));
264 if (
type ==
"eve.material" &&
from.value() == 10 &&
current.value() == 11) {
266 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
267 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
268 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 10 ||
269 object->contains(
"vegetationMotion"))
274 migrated[
"schemaVersion"] =
Value(int64_t(11));
277 if (
type ==
"eve.material" &&
from.value() == 11 &&
current.value() == 12) {
279 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
280 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
281 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 11)
283 {}, {},
"asset.migration"));
285 if (
auto at = migrated.find(
"vegetationAlpha");
at != migrated.end()) {
287 if (!alpha || alpha->size() != 3 || !alpha->contains(
"global") || !alpha->contains(
"variation") ||
288 !alpha->contains(
"detailFade"))
291 {}, {},
"asset.migration"));
292 (*alpha)[
"glancing"] =
Value(0.0);
293 (*alpha)[
"camera"] =
Value(1.0);
294 (*alpha)[
"constant"] =
Value(0.0);
296 migrated[
"schemaVersion"] =
Value(int64_t(12));
299 if (
type ==
"eve.material" &&
from.value() == 12 &&
current.value() == 13) {
301 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
302 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
303 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 12 ||
304 object->contains(
"vegetationEmission"))
307 {},
"asset.migration"));
309 migrated[
"schemaVersion"] =
Value(int64_t(13));
312 if (
type ==
"eve.material" &&
from.value() == 13 &&
current.value() == 14) {
314 const auto* vegetation =
object &&
object->contains(
"vegetationSurface")
317 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
318 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
319 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 13 ||
320 object->contains(
"vegetationGradient") ||
object->contains(
"cullMode") ||
322 (vegetation->contains(
"vertexOcclusionColor") || vegetation->contains(
"backfaceNormalMode"))))
325 "eve.material/13 is malformed or has unversioned vegetation gradient or occlusion color", {}, {},
328 migrated[
"schemaVersion"] =
Value(int64_t(14));
331 if (
type ==
"eve.material" &&
from.value() == 14 &&
current.value() == 15) {
333 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
334 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
335 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 14 ||
336 object->contains(
"alphaToCoverage"))
341 migrated[
"schemaVersion"] =
Value(int64_t(15));
342 migrated[
"alphaToCoverage"] =
Value(
false);
345 if (
type ==
"eve.terrain-material" &&
from.value() == 2 &&
current.value() == 3) {
347 const auto* layersValue =
object &&
object->contains(
"layers") ? &
object->at(
"layers") :
nullptr;
348 const auto* layers = layersValue ? layersValue->getIf<
Value::Array>() : nullptr;
349 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
350 object->at(
"schema").asString() !=
"eve.terrain-material" || !
object->contains(
"schemaVersion") ||
351 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 2 || !layers ||
352 object->contains(
"holesAsset") ||
object->contains(
"controlAssets"))
356 auto migratedLayers = *layers;
357 for (
auto& encoded : migratedLayers) {
359 if (!
layer ||
layer->contains(
"diffuseAsset") ||
layer->contains(
"normalAsset") ||
360 layer->contains(
"weightAsset") ||
layer->contains(
"maskAsset"))
364 (*layer)[
"diffuseAsset"] =
Value(
"");
365 (*layer)[
"normalAsset"] =
Value(
"");
366 (*layer)[
"weightAsset"] =
Value(
"");
367 (*layer)[
"maskAsset"] =
Value(
"");
370 migrated[
"schemaVersion"] =
Value(int64_t(3));
371 migrated[
"layers"] =
Value(std::move(migratedLayers));
372 migrated[
"holesAsset"] =
Value(
"");
376 if (
type ==
"eve.terrain-material" &&
from.value() == 1 &&
current.value() == 2) {
379 object &&
object->contains(
"layers") ?
object->at(
"layers").getIf<
Value::Array>() : nullptr;
380 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
381 object->at(
"schema").asString() !=
"eve.terrain-material" || !
object->contains(
"schemaVersion") ||
382 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 1 || !layers ||
383 object->contains(
"holesSource") ||
object->contains(
"controlSources") ||
384 object->contains(
"boundsMultiplier"))
388 auto migratedLayers = *layers;
389 for (
auto& encoded : migratedLayers) {
391 if (!
layer ||
layer->contains(
"maskSource") ||
layer->contains(
"tileScaleMeters") ||
392 layer->contains(
"tileOffsetMeters") ||
layer->contains(
"maskRemapMinimum") ||
393 layer->contains(
"maskRemapMaximum") ||
layer->contains(
"specular") ||
layer->contains(
"metallic") ||
394 layer->contains(
"normalScale") ||
layer->contains(
"smoothness"))
400 if (!
found->second.isNumeric())
402 "eve.terrain-material/1 tile size is invalid", {},
403 {},
"asset.migration"));
406 (*layer)[
"maskSource"] =
Value(
"");
412 (*layer)[
"metallic"] =
Value(0.0);
413 (*layer)[
"normalScale"] =
Value(1.0);
414 (*layer)[
"smoothness"] =
Value(0.0);
417 migrated[
"schemaVersion"] =
Value(int64_t(2));
418 migrated[
"layers"] =
Value(std::move(migratedLayers));
419 migrated[
"holesSource"] =
Value(
"");
421 migrated[
"boundsMultiplier"] =
Value(1.0);
424 if (
type ==
"eve.material" &&
from.value() == 4 &&
current.value() == 5) {
426 if (!
object || !
object->contains(
"schema") || !
object->at(
"schema").isString() ||
427 object->at(
"schema").asString() !=
"eve.material" || !
object->contains(
"schemaVersion") ||
428 !
object->at(
"schemaVersion").isInt64() ||
object->at(
"schemaVersion").asInt() != 4 ||
429 object->contains(
"motionHighlightColor"))
434 migrated[
"schemaVersion"] =
Value(int64_t(5));
437 if (
type ==
"eve.image" &&
from.value() == 2 &&
current.value() == 3)
return migrateImageV2ToV3(definition);
438 if (
type ==
"eve.scene-template" &&
from.value() == 2 &&
current.value() == 3) {
440 if (!
object || !
object->contains(
"nodes") || !
object->contains(
"renderers") || !
object->contains(
"schema") ||
441 !
object->at(
"schema").isString() ||
object->at(
"schema").asString() !=
"eve.scene-template" ||
442 !
object->contains(
"schemaVersion") || !
object->at(
"schemaVersion").isInt64() ||
443 object->at(
"schemaVersion").asInt() != 2 || !
object->at(
"renderers").isArray())
448 migrated[
"schemaVersion"] =
Value(std::int64_t(3));
449 auto renderers = *migrated[
"renderers"].getIf<
Value::Array>();
450 for (
auto& renderer : renderers) {
454 "scene-template/2 renderer shadow state is malformed",
455 {}, {},
"asset.migration"));
456 (*binding)[
"castShadows"] =
Value(
true);
457 (*binding)[
"receiveShadows"] =
Value(
true);
459 migrated[
"renderers"] =
Value(std::move(renderers));
463 "asset definition has no registered migration", std::string(
type),
464 {},
"asset.migration"));
467Result<void> refreshImportReport(EvaArchive& archive) {
468 const auto path = archive.manifest.provenance.find(
"path");
470 if (!
path->second.isString() ||
path->second.asString() !=
"reports/import.json")
473 const auto entry = std::lower_bound(
474 archive.entries.begin(), archive.entries.end(),
path->second.asString(),
475 [](
const EvaArchiveEntry& candidate, std::string_view
value) { return candidate.path < value; });
476 if (entry == archive.entries.end() || entry->path !=
path->second.asString())
478 path->second.asString(), {},
"asset.migration"));
479 auto parsed =
Value::fromJson(std::string(entry->bytes.begin(), entry->bytes.end()));
486 for (
const auto& asset : archive.manifest.assets)
489 {
"type",
Value(asset.type +
"/" + std::to_string(asset.schemaVersion.value()))},
490 {
"contentHash",
Value(asset.contentHash)}});
491 (*report)[
"canonicalAssets"] =
Value(std::move(assets));
492 report->erase(
"importKey");
493 auto facts = parsed.value().toJson();
495 const std::string
key =
sha256(std::span<const std::uint8_t>(
496 reinterpret_cast<const std::uint8_t*
>(facts.value().data()), facts.value().size()));
497 (*report)[
"importKey"] =
Value(
key);
498 auto encoded = parsed.value().toJson();
500 entry->bytes.assign(encoded.value().begin(), encoded.value().end());
501 archive.manifest.provenance[
"importKey"] =
Value(
key);
508 static const std::map<std::string_view, std::uint64_t> versions = {
510 {
"eve.volume-texture", 1},
514 {
"eve.animation-clip", 1},
516 {
"eve.material", 15},
517 {
"eve.scene-template", 3},
519 {
"eve.terrain-material", 3},
520 {
"eve.pcg-graph", 1},
521 {
"eve.instance-set", 5},
524 {
"eve.sprite-animation", 1},
526 {
"eve.stylize.mesh-vfx", 1},
527 {
"eve.vegetation-conversion-preset", 1},
528 {
"eve.vegetation-scene", 1},
531 if (
found == versions.end())
533 DiagnosticCode::Unsupported,
"unknown canonical asset type", std::string(
type), {},
"asset.migration"));
538 for (
auto& asset :
source.manifest.assets) {
541 if (asset.schemaVersion.value() >
current.value().value())
543 "asset definition is newer than this reader",
544 asset.type, {},
"asset.migration"));
545 const auto entry = std::lower_bound(
546 source.entries.begin(),
source.entries.end(), asset.definition,
547 [](
const EvaArchiveEntry& candidate, std::string_view
path) { return candidate.path < path; });
548 if (entry ==
source.entries.end() || entry->path != asset.definition)
550 DiagnosticCode::NotFound,
"asset definition is missing", asset.definition, {},
"asset.migration"));
551 const std::string_view
text(
reinterpret_cast<const char*
>(entry->bytes.data()), entry->bytes.size());
552 auto parsed = Value::fromJson(
text);
554 auto migrated = migrateDefinition(asset.type, asset.schemaVersion,
current.value(), parsed.value());
556 auto encoded = migrated.value().toJson();
558 entry->bytes.assign(encoded.value().begin(), encoded.value().end());
559 if (asset.schemaVersion !=
current.value()) {
560 const std::string oldType = asset.type +
"/" + std::to_string(asset.schemaVersion.value());
561 const std::string newType = asset.type +
"/" + std::to_string(
current.value().value());
562 for (
auto& dependency :
source.manifest.dependencies)
563 if (dependency.to.id() == asset.asset.id() && dependency.expectedType == oldType)
564 dependency.expectedType = newType;
566 asset.schemaVersion =
current.value();
567 asset.contentHash = sha256(entry->bytes);
569 auto report = refreshImportReport(
source);
Transactional migrations for canonical definitions stored in .eva.
eve::action::ActionVfxBinding binding
std::vector< std::string > fields
const UnitySourceAsset & source
const AssetImportLimits & limits
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.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
std::vector< Value > Array
static HashFunction * getHashFunction(std::string function)
Get a HashFunction instance for the given function.
virtual void hash(std::string function, const char *input, uint64_t length, Value &output) const =0
Hash the input, producing an set of bytes as output.
std::string sha256(std::span< const std::uint8_t > bytes)
Sha 256.
Result< std::vector< std::uint8_t > > buildEvaArchive(const EvaManifest &manifest, std::vector< EvaArchiveEntry > entries, const EvaArchiveLimits &limits)
Build a deterministic ZIP64 .eva, selecting raw Deflate only when smaller than Store.
Result< EvaArchive > parseEvaArchive(std::span< const std::uint8_t > bytes, const EvaArchiveLimits &limits)
Parse and fully verify an untrusted .eva ZIP64 image.
Result< EvaArchive > migrateEvaArchive(EvaArchive source, const EvaArchiveLimits &limits)
Migrate every definition in an owning archive to its current schema.
Result< SchemaVersion > currentAssetSchemaVersion(std::string_view type)
Return the current supported definition version for a canonical asset type.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
One owning regular-file entry inside an .eva archive.
Admission budgets applied before allocating decoded archive entries.
Fully admitted source archive with its typed manifest and payloads.