15Result<std::string> textFile(
const UnityProjectImportRequest&
request,
const std::string&
path) {
22 "Unity source exceeds budget",
path, {},
"asset.import"));
23 if (std::find(
found->second.begin(),
found->second.end(), std::uint8_t(0)) !=
found->second.end())
29std::optional<std::string> firstMatch(std::string_view
text,
const std::regex& expression, std::size_t
group = 1) {
30 std::match_results<std::string_view::const_iterator> match;
31 if (!std::regex_search(
text.begin(),
text.end(), match, expression) ||
group >= match.size())
return std::nullopt;
35std::optional<std::uint64_t> parseUnsignedText(std::string_view
text) {
36 std::uint64_t
value = 0;
37 const auto result = std::from_chars(
text.data(),
text.data() +
text.size(),
value);
38 return result.ec == std::errc{} && result.ptr ==
text.data() +
text.size() ? std::optional<std::uint64_t>(
value)
42Result<float> parseFloat(std::string_view
text, std::string
path) {
49 path, {},
"asset.import"));
54 const auto found =
object.find(std::string(
name));
55 return found ==
object.end() ? nullptr : &
found->second;
58std::string terrainDetailsPath(
const UnityProjectImportRequest&
request) {
59 if (!
request.terrainDetailsPath.empty())
return request.terrainDetailsPath;
60 if (
request.terrainDataPath.empty())
return {};
61 const auto adjacent =
request.terrainDataPath +
".eve-details.json";
62 return request.files.contains(adjacent) ? adjacent : std::string{};
65Result<void> appendTerrainDetailInstances(
const UnityProjectImportRequest&
request, CanonicalTerrainInput&
terrain,
66 std::vector<ImportFinding>&
findings) {
67 const auto detailsPath = terrainDetailsPath(
request);
79 const auto* prototypes = prototypesValue ? prototypesValue->getIf<
Value::Array>() : nullptr;
80 if (!schema || !schema->isString() || schema->asString() !=
"eve.unity-terrain-details" || !version ||
81 !version->isInt64() || (version->asInt() != 1 && version->asInt() != 2 && version->asInt() != 3) ||
85 detailsPath, {},
"asset.import"));
89 "terrain detail instance count exceeds import budget",
90 detailsPath, {},
"asset.import"));
96 "$.prototypes[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
97 const double parsed =
value->isInt64() ? double(
value->asInt()) :
value->asDouble();
98 if (!std::isfinite(parsed) || parsed < -std::numeric_limits<float>::max() ||
99 parsed > std::numeric_limits<float>::max())
102 "$.prototypes[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
105 if (version->asInt() == 3) {
107 const auto* wind = windValue ? windValue->getIf<
Value::Object>() : nullptr;
108 auto rootNumber = [&](std::string_view
name) -> Result<float> {
113 "$.wavingGrass." + std::string(
name), {},
"asset.import"));
114 const double parsed =
value->isInt64() ? double(
value->asInt()) :
value->asDouble();
115 if (!std::isfinite(parsed) || parsed < 0.0 || parsed > std::numeric_limits<float>::max())
118 "$.wavingGrass." + std::string(
name), {},
"asset.import"));
121 auto amount = rootNumber(
"amount");
122 auto speed = rootNumber(
"speed");
123 auto strength = rootNumber(
"strength");
124 const Value* tintValue = wind ?
member(*wind,
"tint") : nullptr;
128 !amount ? amount.status()
132 "terrain waving grass tint is invalid",
133 "$.wavingGrass.tint", {},
"asset.import"))
135 for (std::size_t component = 0; component < 4; ++component) {
136 if (!(*
tint)[component].isNumeric())
138 "terrain waving grass tint is invalid",
139 "$.wavingGrass.tint", {},
"asset.import"));
140 const double value = (*tint)[component].isInt64() ? double((*
tint)[component].asInt())
141 : (*
tint)[component].asDouble();
142 if (!std::isfinite(
value) || value < 0.0 || value > 1.0)
144 "terrain waving grass tint must be normalized RGBA",
145 "$.wavingGrass.tint", {},
"asset.import"));
146 terrain.wavingGrassTint[component] =
static_cast<float>(
value);
149 terrain.wavingGrassAmount = amount.value();
150 terrain.wavingGrassSpeed = speed.value();
153 std::vector<CanonicalTerrainDetailPrototype> detachedPrototypes;
154 detachedPrototypes.reserve(prototypes->size());
155 std::set<std::string> prototypeIds;
158 const Value*
id =
object ?
member(*
object,
"prototype") : nullptr;
159 const Value* mode =
object ?
member(*
object,
"renderMode") : nullptr;
160 const Value*
mesh =
object ?
member(*
object,
"usePrototypeMesh") : nullptr;
163 if (!
object || !
id || !
id->isString() ||
id->asString().empty() ||
166 !mode || !mode->isString() ||
167 (mode->asString() !=
"GrassBillboard" && mode->asString() !=
"Grass" && mode->asString() !=
"VertexLit") ||
171 "$.prototypes[" + std::to_string(
index) +
"]", {},
"asset.import"));
172 CanonicalTerrainDetailPrototype prototype;
173 prototype.prototype =
id->asString();
174 prototype.renderMode = mode->asString();
175 prototype.usePrototypeMesh =
mesh->asBool();
176 prototype.useInstancing =
instancing->asBool();
177 prototype.noiseSeed =
seed->asInt();
178 const std::string texturePrefix =
"unity-texture-guid:";
179 const std::string objectPrefix =
"unity-guid:";
180 if (!prototype.usePrototypeMesh && prototype.prototype.starts_with(texturePrefix)) {
184 prototype.resourceAsset =
resource.value().format();
185 }
else if (prototype.usePrototypeMesh && prototype.prototype.starts_with(objectPrefix)) {
189 prototype.resourceAsset =
resource.value().format();
193 "$.prototypes[" + std::to_string(
index) +
"].prototype", {},
"asset.import"));
195 auto minWidth =
number(*
object,
"minWidth",
index);
196 auto maxWidth =
number(*
object,
"maxWidth",
index);
197 auto minHeight =
number(*
object,
"minHeight",
index);
198 auto maxHeight =
number(*
object,
"maxHeight",
index);
199 auto noiseSpread =
number(*
object,
"noiseSpread",
index);
201 auto align =
number(*
object,
"alignToGround",
index);
202 auto jitter =
number(*
object,
"positionJitter",
index);
203 if (!minWidth || !maxWidth || !minHeight || !maxHeight || !noiseSpread || !density || !align || !jitter)
205 : !maxWidth ? maxWidth.
status()
206 : !minHeight ? minHeight.
status()
207 : !maxHeight ? maxHeight.
status()
208 : !noiseSpread ? noiseSpread.
status()
209 : !density ? density.
status()
212 prototype.minWidth = minWidth.
value();
213 prototype.maxWidth = maxWidth.value();
214 prototype.minHeight = minHeight.value();
215 prototype.maxHeight = maxHeight.value();
216 prototype.noiseSpread = noiseSpread.value();
217 prototype.density = density.value();
218 prototype.alignToGround = align.value();
219 prototype.positionJitter = jitter.value();
220 if (version->asInt() >= 2) {
221 auto color = [&](std::string_view
name, std::array<float, 4>&
output) -> Result<void> {
224 if (!array || array->size() != 4)
227 "$.prototypes[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
228 for (std::size_t component = 0; component < 4; ++component) {
229 if (!(*array)[component].isNumeric())
232 const double value = (*array)[component].isInt64() ? double((*array)[component].asInt())
233 : (*array)[component].asDouble();
234 if (!std::isfinite(
value) || value < 0.0 || value > 1.0)
237 {},
"asset.import"));
242 auto healthy =
color(
"healthyColor", prototype.healthyColor);
243 auto dry =
color(
"dryColor", prototype.dryColor);
245 auto padding =
number(*
object,
"holeEdgePadding",
index);
246 const Value* densityScaling =
member(*
object,
"useDensityScaling");
247 if (!healthy || !dry || !bend || !padding || !densityScaling || !densityScaling->isBool())
249 !healthy ? healthy.status()
252 : !padding ? padding.
status()
254 "terrain detail density scaling is invalid",
255 {}, {},
"asset.import"))
257 prototype.bendFactor = bend.
value();
258 prototype.holeEdgePadding = padding.value();
259 prototype.useDensityScaling = densityScaling->asBool();
261 detachedPrototypes.push_back(std::move(prototype));
264 std::size_t
index) -> Result<void> {
267 if (!array || array->size() !=
count)
270 "$.instances[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
271 for (std::size_t component = 0; component <
count; ++component) {
272 if (!(*array)[component].isNumeric())
275 "$.instances[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
277 (*array)[component].isInt64() ? double((*array)[component].asInt()) : (*array)[component].asDouble();
278 if (!std::isfinite(
number) ||
number < -std::numeric_limits<float>::max() ||
279 number > std::numeric_limits<float>::max())
282 "$.instances[" + std::to_string(
index) +
"]." + std::string(
name), {},
"asset.import"));
287 std::vector<CanonicalTerrainInstance> detached;
291 const Value* prototype =
object ?
member(*
object,
"prototype") : nullptr;
292 if (!
object || !prototype || !prototype->isString() || prototype->asString().empty() ||
297 "$.instances[" + std::to_string(
index) +
"].prototype", {},
"asset.import"));
298 if (!prototypeIds.contains(prototype->asString()))
301 "$.instances[" + std::to_string(
index) +
"].prototype", {},
"asset.import"));
302 CanonicalTerrainInstance instance;
303 instance.prototype = prototype->asString();
304 auto position = vector(*
object,
"position", 3, instance.position,
index);
305 auto rotation = vector(*
object,
"rotation", 4, instance.rotation,
index);
306 auto scale = vector(*
object,
"scale", 3, instance.scale,
index);
311 detached.push_back(std::move(instance));
313 terrain.detailPrototypes.insert(
terrain.detailPrototypes.end(), std::make_move_iterator(detachedPrototypes.begin()),
314 std::make_move_iterator(detachedPrototypes.end()));
315 terrain.instances.insert(
terrain.instances.end(), std::make_move_iterator(detached.begin()),
316 std::make_move_iterator(detached.end()));
318 "Unity public ComputeDetailInstanceTransforms output imported as canonical instances"});
322std::optional<std::string> guidFromMeta(
const UnityProjectImportRequest&
request,
const std::string& assetPath) {
323 const auto found =
request.files.find(assetPath +
".meta");
325 const std::string
text(
found->second.begin(),
found->second.end());
326 auto guid = firstMatch(
text, std::regex(R
"((?:^|\n)guid:\s*([0-9a-fA-F]{32})(?:\r?\n|\r?$))"));
331std::map<std::string, std::string> guidPaths(
const UnityProjectImportRequest&
request) {
332 std::map<std::string, std::string> result;
334 if (!
path.ends_with(
".meta"))
continue;
336 auto guid = firstMatch(
text, std::regex(R
"((?:^|\n)guid:\s*([0-9a-fA-F]{32})(?:\r?\n|\r?$))"));
342std::vector<std::string> sectionEntries(std::string_view yaml, std::string_view
heading) {
344 if (
start == std::string_view::npos)
return {};
345 const auto lineEnd = yaml.find(
'\n',
start);
346 if (lineEnd == std::string_view::npos)
return {};
347 std::vector<std::string>
entries;
349 std::istringstream
lines(std::string(yaml.substr(lineEnd + 1)));
352 if (
line.starts_with(
" m_") && !
line.starts_with(
" "))
break;
353 if (
line.starts_with(
" - ") ||
line.starts_with(
" - ")) {
364Result<CanonicalTerrainInput> parseTerrain(
const UnityProjectImportRequest&
request,
365 const std::map<std::string, std::string>&
paths,
366 std::vector<ImportFinding>&
findings) {
369 const std::string yaml = std::move(textResult).takeValue();
370 auto resolutionText = firstMatch(yaml, std::regex(R
"((?:m_HeightmapResolution|m_Resolution):\s*([0-9]+))"));
372 firstMatch(yaml, std::regex(R
"(m_HeightmapScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})"), 1);
374 firstMatch(yaml, std::regex(R
"(m_HeightmapScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})"), 2);
376 firstMatch(yaml, std::regex(R
"(m_HeightmapScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})"), 3);
377 auto heightsHex = firstMatch(yaml, std::regex(R
"(m_Heights:\s*([0-9a-fA-F]+))"));
378 if (!resolutionText || !scaleMatchX || !scaleMatchY || !scaleMatchZ || !heightsHex)
381 request.terrainDataPath, {},
"asset.import"));
382 std::uint64_t resolution = 0;
383 for (
char digit : *resolutionText) {
384 if (resolution > (std::numeric_limits<std::uint32_t>::max() - (digit -
'0')) / 10)
387 resolution = resolution * 10 + (digit -
'0');
390 resolution > std::numeric_limits<std::uint64_t>::max() / resolution)
393 const std::uint64_t samples = resolution * resolution;
394 if (heightsHex->size() != samples * 4)
398 auto spacingX = parseFloat(*scaleMatchX,
"m_HeightmapScale.x");
399 auto scaleY = parseFloat(*scaleMatchY,
"m_HeightmapScale.y");
400 auto spacingZ = parseFloat(*scaleMatchZ,
"m_HeightmapScale.z");
404 const auto nibble = [](
char value) ->
int {
410 std::vector<std::uint16_t> sourceHeights(samples);
412 const std::size_t
at =
index * 4;
413 sourceHeights[
index] =
414 static_cast<std::uint16_t
>((nibble((*heightsHex)[
at]) << 4) | nibble((*heightsHex)[
at + 1]) |
415 (nibble((*heightsHex)[
at + 2]) << 12) | (nibble((*heightsHex)[
at + 3]) << 8));
418 const auto imageAsset = [&](std::string_view guid) {
419 return std::string(
"asset://") +
421 .
child(
"image:default")
424 terrain.width =
terrain.height =
static_cast<std::uint32_t
>(resolution);
427 terrain.heightsMeters.resize(samples);
432 float(sourceHeights[std::size_t(
terrain.height - 1 - row) *
terrain.width +
column]) / 65535.0f *
434 terrain.sourceTransform = {{
"source",
Value(
"unity-left-handed-x-right-y-up-z-forward")},
435 {
"conversion",
Value(
"reflect-z-and-reverse-height-rows")},
436 {
"heightScaleMeters",
Value(
double(scaleY.value()))}};
438 if (
auto holes = firstMatch(yaml, std::regex(R
"(m_HolesTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
440 terrain.holesAsset = imageAsset(*holes);
442 const auto controls = sectionEntries(yaml,
"m_AlphamapTextures:");
443 if (controls.size() >
terrain.controlSources.size())
446 request.terrainDataPath, {},
"asset.import"));
448 if (
auto guid = firstMatch(controls[
index], std::regex(R
"(guid:\s*([0-9a-fA-F]{32}))"))) {
453 for (
const auto& entry : sectionEntries(yaml,
"m_TerrainLayers:")) {
454 auto guid = firstMatch(entry, std::regex(R
"(guid:\s*([0-9a-fA-F]{32}))"));
456 CanonicalTerrainLayer
layer;
458 layer.name = resolved ==
paths.end() ? *guid : resolved->second;
459 if (resolved !=
paths.end()) {
460 auto layerText = textFile(
request, resolved->second);
463 firstMatch(layerText.value(), std::regex(R
"(m_DiffuseTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
464 layer.diffuseSource = "unity-guid:" + *diffuse;
465 layer.diffuseAsset = imageAsset(*diffuse);
467 if (
auto normal = firstMatch(layerText.value(),
468 std::regex(R
"(m_NormalMapTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
473 firstMatch(layerText.value(), std::regex(R
"(m_MaskMapTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
474 layer.maskSource = "unity-guid:" + *
mask;
477 auto parseVec = [&](
const char*
name,
auto&
output) -> Result<void> {
478 const std::string expression =
479 std::string(
name) + R
"(:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^,]+),\s*w:\s*([^}]+)\})";
480 std::match_results<std::string::const_iterator> match;
481 if (!std::regex_search(layerText.value(), match, std::regex(expression)))
483 for (std::size_t component = 0; component < 4; ++component) {
484 auto parsed = parseFloat(match[component + 1].str(), resolved->second +
"." +
name);
486 output[component] = parsed.value();
490 for (
auto [
name,
value] : {std::pair{
"m_MaskMapRemapMin", &
layer.maskRemapMinimum},
491 {
"m_MaskMapRemapMax", &
layer.maskRemapMaximum},
492 {
"m_Specular", &
layer.specular}}) {
496 const std::regex tilePattern(R
"(m_TileSize:\s*\{x:\s*([^,]+),\s*y:\s*([^}]+)\})");
497 std::match_results<std::string::const_iterator> tileMatch;
498 if (std::regex_search(layerText.value(), tileMatch, tilePattern)) {
499 auto x = parseFloat(tileMatch[1].str(), resolved->second +
".m_TileSize.x");
500 auto y = parseFloat(tileMatch[2].str(), resolved->second +
".m_TileSize.y");
504 layer.tileScaleMeters = {
x.value(),
y.value()};
505 layer.tileSizeMeters =
x.value();
507 const std::regex offsetPattern(R
"(m_TileOffset:\s*\{x:\s*([^,]+),\s*y:\s*([^}]+)\})");
508 std::match_results<std::string::const_iterator> offsetMatch;
509 if (std::regex_search(layerText.value(), offsetMatch, offsetPattern)) {
510 auto x = parseFloat(offsetMatch[1].str(), resolved->second +
".m_TileOffset.x");
511 auto y = parseFloat(offsetMatch[2].str(), resolved->second +
".m_TileOffset.y");
515 {},
"asset.import"));
516 layer.tileOffsetMeters = {
x.value(),
y.value()};
519 {
"m_NormalScale", &
layer.normalScale},
520 {
"m_Smoothness", &
layer.smoothness}}) {
522 firstMatch(layerText.value(), std::regex(std::string(
name) + R
"(:\s*([^\r\n]+))"))) {
523 auto parsed = parseFloat(*encoded, resolved->second +
"." +
name);
533 std::vector<std::string> prototypes;
534 for (
const auto& entry : sectionEntries(yaml,
"m_TreePrototypes:")) {
535 auto guid = firstMatch(entry, std::regex(R
"(guid:\s*([0-9a-fA-F]{32}))"));
536 prototypes.push_back(guid ? "unity-guid:" + *guid :
"unity-missing-prototype");
538 for (
const auto& entry : sectionEntries(yaml,
"m_TreeInstances:")) {
539 auto position = std::regex(R
"(position:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})");
543 auto prototype = firstMatch(entry, std::regex(R
"(prototypeIndex:\s*([0-9]+))"));
544 if (!
x || !
y || !
z || !prototype)
continue;
545 const auto prototypeValue = parseUnsignedText(*prototype);
546 if (!prototypeValue || *prototypeValue >= prototypes.size())
549 const std::size_t prototypeIndex =
static_cast<std::size_t
>(*prototypeValue);
550 auto px = parseFloat(*
x,
"tree.position.x");
551 auto py = parseFloat(*
y,
"tree.position.y");
552 auto pz = parseFloat(*
z,
"tree.position.z");
556 CanonicalTerrainInstance instance;
557 instance.prototype = prototypes[prototypeIndex];
558 instance.position[0] =
px.value() *
spacingX.value() * float(resolution - 1);
559 instance.position[1] =
py.value() * scaleY.value();
560 instance.position[2] = -
pz.value() *
spacingZ.value() * float(resolution - 1);
561 if (
auto rotation = firstMatch(entry, std::regex(R
"(rotation:\s*([^\r\n]+))"))) {
564 instance.rotation[1] = std::sin(-
value.value() * 0.5f);
565 instance.rotation[3] = std::cos(-
value.value() * 0.5f);
568 if (
auto width = firstMatch(entry, std::regex(R
"(widthScale:\s*([^\r\n]+))"))) {
569 auto value = parseFloat(*
width,
"tree.widthScale");
570 if (
value) instance.scale[0] = instance.scale[2] =
value.value();
572 if (
auto height = firstMatch(entry, std::regex(R
"(heightScale:\s*([^\r\n]+))"))) {
573 auto value = parseFloat(*
height,
"tree.heightScale");
576 terrain.instances.push_back(std::move(instance));
578 if (yaml.find(
"m_DetailPrototypes:") != std::string::npos) {
579 const bool hasDetailSidecar = !terrainDetailsPath(
request).empty();
581 {
request.terrainDataPath,
"TerrainData.detail-prototypes",
584 ?
"detail data requires an eve.unity-terrain-details/3 public-API export"
585 :
"detail prototypes and placement are supplied by the public-API sidecar"});
588 "UInt16 heights converted to metres and canonical Z"});
598std::vector<UnityObject> documents(std::string_view yaml) {
599 const std::regex
header(R
"(^---\s*!u!([0-9]+)\s*&(-?[0-9]+).*$)", std::regex::multiline);
605 std::vector<Start> starts;
606 for (std::cregex_iterator it(yaml.data(), yaml.data() + yaml.size(), header),
end; it !=
end; ++it) {
607 const std::string signedText = (*it)[2].str();
608 std::int64_t signedId = 0;
609 const auto parsedId = std::from_chars(signedText.data(), signedText.data() + signedText.size(), signedId);
610 const auto classText = (*it)[1].str();
611 const auto classId = parseUnsignedText(classText);
612 if (parsedId.ec != std::errc{} || parsedId.ptr != signedText.data() + signedText.size() || !
classId ||
613 *
classId > std::numeric_limits<std::uint32_t>::max())
615 starts.push_back({
static_cast<std::size_t
>((*it).position()),
616 static_cast<std::size_t
>((*it).position() + (*it).length()),
617 {signedId,
static_cast<std::uint32_t
>(*classId), {}}});
619 std::vector<UnityObject> result;
621 const std::size_t
end =
index + 1 < starts.size() ? starts[
index + 1].header : yaml.size();
622 starts[
index].object.body = std::string(yaml.substr(starts[
index].body,
end - starts[
index].body));
623 result.push_back(std::move(starts[
index].
object));
628Result<void> appendPrefab(
const UnityProjectImportRequest&
request, PreparedAssetImport&
output,
629 std::vector<ImportFinding>&
findings,
const std::string& prefabPath) {
633 if (std::regex_search(
text.value(), std::regex(R
"((?:^|\n)---\s*!u!(?:1|4|224)\s*&-)")))
636 "prefab signed hierarchy IDs are indexed but cannot yet be represented by scene-template",
637 prefabPath, {},
"asset.import"));
638 const auto prefabGuid = guidFromMeta(
request, prefabPath);
641 prefabPath +
".meta", {},
"asset.import.unity"));
642 const auto objects = documents(
text.value());
645 "Unity prefab object count exceeds budget", prefabPath, {},
647 std::map<std::uint64_t, std::string> gameNames;
648 std::map<std::uint64_t, bool> gameVisibility;
649 for (
const auto&
object : objects) {
651 auto name = firstMatch(
object.
body, std::regex(R
"(m_Name:\s*([^\r\n]+))"));
652 gameNames[object.fileId] =
name.value_or(
"GameObject");
653 gameVisibility[
object.fileId] = !std::regex_search(
object.
body, std::regex(R
"(m_IsActive:\s*0(?:\s|$))"));
654 } else if (
object.
classId == 114) {
655 findings.push_back({prefabPath,
"MonoBehaviour:" + std::to_string(
object.
fileId),
657 "C# behaviour is not executed or translated by the data importer"});
658 }
else if (
object.
classId != 4) {
660 {prefabPath,
"Prefab.component:" + std::to_string(
object.
classId) +
":" + std::to_string(
object.
fileId),
662 "component behavior is not converted; only GameObject/Transform hierarchy is available"});
666 for (
const auto&
object : objects) {
668 auto game = firstMatch(
object.
body, std::regex(R
"(m_GameObject:\s*\{fileID:\s*(-?[0-9]+)\})"));
669 auto father = firstMatch(
object.
body, std::regex(R
"(m_Father:\s*\{fileID:\s*(-?[0-9]+)\})"));
670 const std::regex
position(R
"(m_LocalPosition:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})");
672 R
"(m_LocalRotation:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^,]+),\s*w:\s*([^}]+)\})");
673 const std::regex
scale(R
"(m_LocalScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})");
675 auto vector = [&](
const std::regex& regex, std::size_t
count,
float*
target,
bool reflect) -> Result<void> {
676 for (std::size_t component = 0; component <
count; ++component) {
677 auto field = firstMatch(
object.
body, regex, component + 1);
680 "Unity transform field is missing", prefabPath, {},
681 "asset.import.unity"));
682 auto parsed = parseFloat(*field, prefabPath);
684 target[component] = parsed.value();
689 float p[3],
q[4],
s[3];
690 auto pResult = vector(
position, 3,
p,
true);
691 if (!pResult)
return pResult;
692 auto qResult = vector(
rotation, 4,
q,
false);
693 if (!qResult)
return qResult;
696 auto sResult = vector(
scale, 3,
s,
false);
697 if (!sResult)
return sResult;
698 const auto gameIdValue = parseUnsignedText(*game);
699 const auto parentIdValue = father ? parseUnsignedText(*father) :
std::optional<
std::uint64_t>(0);
700 if (!gameIdValue || !parentIdValue)
703 const auto gameId = *gameIdValue;
704 const std::uint64_t
parentId = *parentIdValue;
708 node[
"sourceFileId"] =
Value(
static_cast<std::int64_t
>(
object.
fileId));
709 node[
"name"] =
Value(gameNames.contains(gameId) ? gameNames[gameId] :
"GameObject");
710 node[
"visible"] =
Value(!gameVisibility.contains(gameId) || gameVisibility.at(gameId));
721 "prefab has no directly convertible Transform nodes; nested prefab expansion is required",
722 prefabPath, {},
"asset.import"));
725 definition[
"schema"] =
Value(
"eve.scene-template");
726 definition[
"schemaVersion"] =
Value(std::int64_t(3));
727 definition[
"sourceGuid"] =
Value(*prefabGuid);
728 definition[
"nodes"] =
Value(std::move(
nodes));
730 definition[
"coordinateSystem"] =
Value(
"right-handed-x-right-y-up-minus-z-forward");
731 auto encoded =
Value(std::move(definition)).toJson();
733 std::string definitionText = std::move(encoded).takeValue();
734 const std::string
path =
"assets/" + sceneId.format() +
"/asset.json";
737 output.manifest.assets.push_back(
739 "eve.scene-template",
742 detail::sha256(std::span<const std::uint8_t>(
reinterpret_cast<const std::uint8_t*
>(definitionText.data()),
743 definitionText.size())),
744 {
"scene",
"source:unity-prefab"}});
745 output.entries.push_back({
path, {definitionText.begin(), definitionText.end()}});
746 if (
output.manifest.entrypoints.empty()) {
749 output.manifest.entrypoints.emplace(
"default", std::move(entrypoint).takeValue());
752 "GUID/fileID hierarchy converted to canonical TRS"});
765 for (
const auto& asset :
output.manifest.assets)
output.sourceMappings.push_back({
"Prefab#Transform", asset.asset});
772 Diagnostic::error(DiagnosticCode::InvalidArgument,
"Unity project files and package identity are required",
773 {}, {},
"asset.import"));
776 if (
request.terrainDataPath.empty() &&
request.prefabPath.empty())
778 std::vector<ImportFinding>
findings = sourceIndex.value().findings;
780 if (!
request.terrainDataPath.empty()) {
788 output = std::move(prepared).takeValue();
789 if (!
terrain.value().detailPrototypes.empty()) {
792 std::set<PersistentId> selected;
793 for (
const auto& prototype :
terrain.value().detailPrototypes) {
794 auto resource = AssetRef::parse(prototype.resourceAsset);
800 for (
const auto& dependency : collection.value().manifest.dependencies)
801 if (selected.contains(dependency.from.id()) && selected.emplace(dependency.to.id()).second)
804 std::set<std::string> runtimePrefixes;
805 for (
const auto& asset : collection.value().manifest.assets) {
806 if (!selected.contains(asset.asset.id()))
continue;
807 const bool duplicate = std::any_of(
output.manifest.assets.begin(),
output.manifest.assets.end(),
808 [&](
const auto& existing) {
809 return existing.asset.id() == asset.asset.id();
811 if (!duplicate)
output.manifest.assets.push_back(asset);
812 runtimePrefixes.emplace(
"assets/" + asset.asset.id().format() +
"/");
814 for (
const auto& dependency : collection.value().manifest.dependencies)
815 if (selected.contains(dependency.from.id()) && selected.contains(dependency.to.id()))
816 output.manifest.dependencies.push_back(dependency);
817 for (
const auto& entry : collection.value().entries)
818 if (std::any_of(runtimePrefixes.begin(), runtimePrefixes.end(), [&](
const auto& prefix) {
819 return entry.path.starts_with(prefix);
821 output.entries.push_back(entry);
825 auto manifest = detail::baseManifest(
request.package,
"eve.unity-prefab/1");
827 output.manifest = std::move(manifest).takeValue();
831 output.manifest.provenance[
"sourceEngine"] =
Value(
"unity");
832 output.manifest.provenance[
"sourceCoordinateSystem"] =
Value(
"left-handed-x-right-y-up-z-forward");
834 std::make_move_iterator(
findings.end()));
835 for (
const auto& asset :
output.manifest.assets) {
836 std::string sourceObject =
request.terrainDataPath +
"#" + asset.type;
837 if (asset.type ==
"eve.scene-template") sourceObject =
request.prefabPath +
"#Transform";
838 output.sourceMappings.push_back({std::move(sourceObject), asset.asset});
840 auto report = detail::finalizeImportReport(
output,
request.package,
"unity",
"force-text",
841 {{
"serialization", Value(
"force-text")}});
AuthorityStoreHandleRef reference
std::unordered_map< std::string, QuestRuntime > entries
building::EdgeCurveGroup group
std::vector< BuildingInstanceSnapshot > instances
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::array< double, 10 > q
std::array< float, 4 > rotation
std::array< float, 3 > position
std::array< float, 3 > scale
AbilityInstancingPolicy instancing
float(ui::Theme::* member)[4]
Direct Unity text-serialization adapter for TerrainData and Prefab assets.
std::map< std::string, std::string > paths
std::vector< ImportFinding > findings
std::vector< std::string_view > lines
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.
const T & value() const &
Borrow the value from a const lvalue after checking success.
static Result failure(Status status)
Construct a failed result from a structured status.
The canonical owning dynamic value used by data-facing protocols.
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
std::string format() const
Formats the ID in lower-case canonical UUID text.
Id128 child(std::string_view role) const noexcept
Derives a deterministic child UUID for a hierarchical identity.
std::string sha256(std::span< const std::uint8_t > bytes)
Sha 256.
Result< asset::EvaManifest > baseManifest(const ImportPackageIdentity &package, std::string_view importer)
Base manifest.
Result< AssetRef > assetRef(PersistentId id)
Asset ref.
std::string foldAscii(std::string value)
Fold ascii.
Result< PreparedAssetImport > prepareCanonicalTerrainImport(const ImportPackageIdentity &package, const CanonicalTerrainInput &terrain, std::string_view importer, const AssetImportLimits &limits)
Build eve.terrain, terrain material, PCG and instance assets from canonical input.
Result< PreparedAssetImport > prepareUnityPrefab(const UnityProjectImportRequest &request, const std::string &path)
Prepare one prefab without copying or re-indexing the source collection.
Result< PreparedAssetImport > prepareUnityCollection(const UnityProjectImportRequest &request, const UnitySourceIndex &index)
Prepare an owning collection candidate using a validated source index.
Result< UnitySourceIndex > indexUnitySources(const UnitySourceFiles &files, const AssetImportLimits &limits)
Validate project paths and .meta identities and index serialized GUID/fileID references.
Result< PreparedAssetImport > prepareUnityProjectImport(const UnityProjectImportRequest &request)
Import selected Unity assets, or discover a collection when both selectors are empty.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
eve::Diagnostic Diagnostic
const EditorValue * field(const EditorValue &value, const char *name)
std::string header(int localX, int localY)
Header.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
DiagnosticCode
Stable machine-readable diagnostic codes.
StrongUuid< detail::PersistentIdTag > PersistentId
Stable instance identity for persistence, networking and process boundaries.
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
bool isValidUtf8(std::string_view text, Utf8NullPolicy nullPolicy) noexcept
Validate shortest-form UTF-8, Unicode scalar range and optional NUL exclusion.
std::uint32_t maximumAssets
std::uint32_t maximumVerticesPerPrimitive
std::uint64_t maximumSourceBytes
std::uint32_t maximumStringBytes
ImportPackageIdentity package
Owning, validated source archive candidate not yet published to disk/database.
asset::EvaManifest manifest
In-memory Unity project slice; keys are normalized project-relative paths.