载入中...
搜索中...
未找到
UnityImporter.cpp
浏览该文件的文档.
2
5
6#include <charconv>
7#include <cmath>
8#include <regex>
9#include <set>
10#include <sstream>
11
12namespace eve::asset_import {
13namespace {
14
15Result<std::string> textFile(const UnityProjectImportRequest& request, const std::string& path) {
16 const auto found = request.files.find(path);
17 if (found == request.files.end())
19 DiagnosticCode::NotFound, "Unity source file was not supplied", path, {}, "asset.import"));
20 if (found->second.size() > request.limits.maximumSourceBytes)
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())
25 DiagnosticCode::Unsupported, "Unity adapter requires Force Text serialization", path, {}, "asset.import"));
26 return Result<std::string>::success({found->second.begin(), found->second.end()});
27}
28
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;
32 return std::string(match[group].first, match[group].second);
33}
34
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)
39 : std::nullopt;
40}
41
42Result<float> parseFloat(std::string_view text, std::string path) {
43 std::string owned(text);
44 char* end = nullptr;
45 errno = 0;
46 const float value = std::strtof(owned.c_str(), &end);
47 if (errno != 0 || end != owned.c_str() + owned.size() || !std::isfinite(value))
48 return Result<float>::failure(Diagnostic::error(DiagnosticCode::ParseError, "Unity numeric value is invalid",
49 path, {}, "asset.import"));
51}
52
53const Value* member(const Value::Object& object, std::string_view name) {
54 const auto found = object.find(std::string(name));
55 return found == object.end() ? nullptr : &found->second;
56}
57
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{};
63}
64
65Result<void> appendTerrainDetailInstances(const UnityProjectImportRequest& request, CanonicalTerrainInput& terrain,
66 std::vector<ImportFinding>& findings) {
67 const auto detailsPath = terrainDetailsPath(request);
68 if (detailsPath.empty()) return Result<void>::success();
69 auto text = textFile(request, detailsPath);
70 if (!text) return Result<void>::failure(text.status());
71 auto parsed = Value::fromJson(text.value());
72 if (!parsed) return Result<void>::failure(parsed.status());
73 const auto* root = parsed.value().getIf<Value::Object>();
74 const Value* schema = root ? member(*root, "schema") : nullptr;
75 const Value* version = root ? member(*root, "schemaVersion") : nullptr;
76 const Value* instancesValue = root ? member(*root, "instances") : nullptr;
77 const Value* prototypesValue = root ? member(*root, "prototypes") : nullptr;
78 const auto* instances = instancesValue ? instancesValue->getIf<Value::Array>() : nullptr;
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) ||
82 !instances || !prototypes)
84 DiagnosticCode::Unsupported, "terrain detail sidecar must use eve.unity-terrain-details/1, /2 or /3",
85 detailsPath, {}, "asset.import"));
86 if (instances->size() > request.limits.maximumAssets || prototypes->size() > request.limits.maximumAssets ||
87 terrain.instances.size() > request.limits.maximumAssets - instances->size())
89 "terrain detail instance count exceeds import budget",
90 detailsPath, {}, "asset.import"));
91 auto number = [&](const Value::Object& object, std::string_view name, std::size_t index) -> Result<float> {
92 const Value* value = member(object, name);
93 if (!value || !value->isNumeric())
95 DiagnosticCode::ParseError, "terrain detail number is invalid",
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())
101 DiagnosticCode::ParseError, "terrain detail number is non-finite",
102 "$.prototypes[" + std::to_string(index) + "]." + std::string(name), {}, "asset.import"));
103 return Result<float>::success(static_cast<float>(parsed));
104 };
105 if (version->asInt() == 3) {
106 const Value* windValue = member(*root, "wavingGrass");
107 const auto* wind = windValue ? windValue->getIf<Value::Object>() : nullptr;
108 auto rootNumber = [&](std::string_view name) -> Result<float> {
109 const Value* value = wind ? member(*wind, name) : nullptr;
110 if (!value || !value->isNumeric())
112 Diagnostic::error(DiagnosticCode::ParseError, "terrain waving grass number is invalid",
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())
117 DiagnosticCode::InvalidArgument, "terrain waving grass number must be finite and nonnegative",
118 "$.wavingGrass." + std::string(name), {}, "asset.import"));
119 return Result<float>::success(static_cast<float>(parsed));
120 };
121 auto amount = rootNumber("amount");
122 auto speed = rootNumber("speed");
123 auto strength = rootNumber("strength");
124 const Value* tintValue = wind ? member(*wind, "tint") : nullptr;
125 const auto* tint = tintValue ? tintValue->getIf<Value::Array>() : nullptr;
126 if (!amount || !speed || !strength || !tint || tint->size() != 4)
128 !amount ? amount.status()
129 : !speed ? speed.status()
132 "terrain waving grass tint is invalid",
133 "$.wavingGrass.tint", {}, "asset.import"))
134 .status());
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);
147 }
148 terrain.hasWavingGrass = true;
149 terrain.wavingGrassAmount = amount.value();
150 terrain.wavingGrassSpeed = speed.value();
151 terrain.wavingGrassStrength = strength.value();
152 }
153 std::vector<CanonicalTerrainDetailPrototype> detachedPrototypes;
154 detachedPrototypes.reserve(prototypes->size());
155 std::set<std::string> prototypeIds;
156 for (std::size_t index = 0; index < prototypes->size(); ++index) {
157 const auto* object = (*prototypes)[index].getIf<Value::Object>();
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;
161 const Value* instancing = object ? member(*object, "useInstancing") : nullptr;
162 const Value* seed = object ? member(*object, "noiseSeed") : nullptr;
163 if (!object || !id || !id->isString() || id->asString().empty() ||
164 id->asString().size() > request.limits.maximumStringBytes ||
165 !isValidUtf8(id->asString(), Utf8NullPolicy::Reject) || !prototypeIds.emplace(id->asString()).second ||
166 !mode || !mode->isString() ||
167 (mode->asString() != "GrassBillboard" && mode->asString() != "Grass" && mode->asString() != "VertexLit") ||
168 !mesh || !mesh->isBool() || !instancing || !instancing->isBool() || !seed || !seed->isInt64())
170 Diagnostic::error(DiagnosticCode::ParseError, "terrain detail prototype metadata is invalid",
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)) {
181 const auto guid = unity_detail::foldAscii(prototype.prototype.substr(texturePrefix.size()));
182 auto resource = detail::assetRef(request.package.packageId.child("unity:" + guid).child("image:default"));
183 if (!resource) return Result<void>::failure(resource.status());
184 prototype.resourceAsset = resource.value().format();
185 } else if (prototype.usePrototypeMesh && prototype.prototype.starts_with(objectPrefix)) {
186 const auto guid = unity_detail::foldAscii(prototype.prototype.substr(objectPrefix.size()));
187 auto resource = detail::assetRef(request.package.packageId.child("unity-prefab:" + guid));
188 if (!resource) return Result<void>::failure(resource.status());
189 prototype.resourceAsset = resource.value().format();
190 } else {
192 Diagnostic::error(DiagnosticCode::ParseError, "terrain detail prototype resource kind is inconsistent",
193 "$.prototypes[" + std::to_string(index) + "].prototype", {}, "asset.import"));
194 }
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);
200 auto density = number(*object, "density", 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)
204 return Result<void>::failure(!minWidth ? minWidth.status()
205 : !maxWidth ? maxWidth.status()
206 : !minHeight ? minHeight.status()
207 : !maxHeight ? maxHeight.status()
208 : !noiseSpread ? noiseSpread.status()
209 : !density ? density.status()
210 : !align ? align.status()
211 : jitter.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> {
222 const Value* value = member(*object, name);
223 const auto* array = value ? value->getIf<Value::Array>() : nullptr;
224 if (!array || array->size() != 4)
226 DiagnosticCode::ParseError, "terrain detail color is invalid",
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())
231 DiagnosticCode::ParseError, "terrain detail color is invalid", {}, {}, "asset.import"));
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)
236 DiagnosticCode::InvalidArgument, "terrain detail color must be finite normalized RGBA", {},
237 {}, "asset.import"));
238 output[component] = static_cast<float>(value);
239 }
240 return Result<void>::success();
241 };
242 auto healthy = color("healthyColor", prototype.healthyColor);
243 auto dry = color("dryColor", prototype.dryColor);
244 auto bend = number(*object, "bendFactor", index);
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()
250 : !dry ? dry.status()
251 : !bend ? bend.status()
252 : !padding ? padding.status()
254 "terrain detail density scaling is invalid",
255 {}, {}, "asset.import"))
256 .status());
257 prototype.bendFactor = bend.value();
258 prototype.holeEdgePadding = padding.value();
259 prototype.useDensityScaling = densityScaling->asBool();
260 }
261 detachedPrototypes.push_back(std::move(prototype));
262 }
263 auto vector = [&](const Value::Object& object, std::string_view name, std::size_t count, float* output,
264 std::size_t index) -> Result<void> {
265 const Value* value = member(object, name);
266 const auto* array = value ? value->getIf<Value::Array>() : nullptr;
267 if (!array || array->size() != count)
269 DiagnosticCode::ParseError, "terrain detail vector is invalid",
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())
274 DiagnosticCode::ParseError, "terrain detail vector is non-numeric",
275 "$.instances[" + std::to_string(index) + "]." + std::string(name), {}, "asset.import"));
276 const double number =
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())
281 DiagnosticCode::ParseError, "terrain detail vector is non-finite",
282 "$.instances[" + std::to_string(index) + "]." + std::string(name), {}, "asset.import"));
283 output[component] = static_cast<float>(number);
284 }
285 return Result<void>::success();
286 };
287 std::vector<CanonicalTerrainInstance> detached;
288 detached.reserve(instances->size());
289 for (std::size_t index = 0; index < instances->size(); ++index) {
290 const auto* object = (*instances)[index].getIf<Value::Object>();
291 const Value* prototype = object ? member(*object, "prototype") : nullptr;
292 if (!object || !prototype || !prototype->isString() || prototype->asString().empty() ||
293 prototype->asString().size() > request.limits.maximumStringBytes ||
294 !isValidUtf8(prototype->asString(), Utf8NullPolicy::Reject))
296 Diagnostic::error(DiagnosticCode::ParseError, "terrain detail prototype is invalid",
297 "$.instances[" + std::to_string(index) + "].prototype", {}, "asset.import"));
298 if (!prototypeIds.contains(prototype->asString()))
300 Diagnostic::error(DiagnosticCode::NotFound, "terrain detail instance prototype is undeclared",
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);
307 if (!position || !rotation || !scale)
308 return Result<void>::failure(!position ? position.status()
310 : scale.status());
311 detached.push_back(std::move(instance));
312 }
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()));
317 findings.push_back({detailsPath, "TerrainData.detail-instances", ImportDisposition::Translated,
318 "Unity public ComputeDetailInstanceTransforms output imported as canonical instances"});
319 return Result<void>::success();
320}
321
322std::optional<std::string> guidFromMeta(const UnityProjectImportRequest& request, const std::string& assetPath) {
323 const auto found = request.files.find(assetPath + ".meta");
324 if (found == request.files.end()) return std::nullopt;
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?$))"));
327 if (guid) *guid = unity_detail::foldAscii(std::move(*guid));
328 return guid;
329}
330
331std::map<std::string, std::string> guidPaths(const UnityProjectImportRequest& request) {
332 std::map<std::string, std::string> result;
333 for (const auto& [path, bytes] : request.files) {
334 if (!path.ends_with(".meta")) continue;
335 const std::string text(bytes.begin(), bytes.end());
336 auto guid = firstMatch(text, std::regex(R"((?:^|\n)guid:\s*([0-9a-fA-F]{32})(?:\r?\n|\r?$))"));
337 if (guid) result.emplace(unity_detail::foldAscii(*guid), path.substr(0, path.size() - 5));
338 }
339 return result;
340}
341
342std::vector<std::string> sectionEntries(std::string_view yaml, std::string_view heading) {
343 const auto start = yaml.find(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;
348 std::string current;
349 std::istringstream lines(std::string(yaml.substr(lineEnd + 1)));
350 std::string line;
351 while (std::getline(lines, line)) {
352 if (line.starts_with(" m_") && !line.starts_with(" ")) break;
353 if (line.starts_with(" - ") || line.starts_with(" - ")) {
354 if (!current.empty()) entries.push_back(std::move(current));
355 current = line + "\n";
356 } else if (!current.empty()) {
357 current += line + "\n";
358 }
359 }
360 if (!current.empty()) entries.push_back(std::move(current));
361 return entries;
362}
363
364Result<CanonicalTerrainInput> parseTerrain(const UnityProjectImportRequest& request,
365 const std::map<std::string, std::string>& paths,
366 std::vector<ImportFinding>& findings) {
367 auto textResult = textFile(request, request.terrainDataPath);
368 if (!textResult) return Result<CanonicalTerrainInput>::failure(textResult.status());
369 const std::string yaml = std::move(textResult).takeValue();
370 auto resolutionText = firstMatch(yaml, std::regex(R"((?:m_HeightmapResolution|m_Resolution):\s*([0-9]+))"));
371 auto scaleMatchX =
372 firstMatch(yaml, std::regex(R"(m_HeightmapScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})"), 1);
373 auto scaleMatchY =
374 firstMatch(yaml, std::regex(R"(m_HeightmapScale:\s*\{x:\s*([^,]+),\s*y:\s*([^,]+),\s*z:\s*([^}]+)\})"), 2);
375 auto scaleMatchZ =
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)
380 Diagnostic::error(DiagnosticCode::ParseError, "Unity TerrainData heightmap fields are incomplete",
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)
386 DiagnosticCode::InvalidArgument, "Unity terrain resolution overflows", {}, {}, "asset.import"));
387 resolution = resolution * 10 + (digit - '0');
388 }
389 if (resolution < 2 || resolution > request.limits.maximumVerticesPerPrimitive ||
390 resolution > std::numeric_limits<std::uint64_t>::max() / resolution)
392 DiagnosticCode::InvalidArgument, "Unity terrain resolution is outside limits", {}, {}, "asset.import"));
393 const std::uint64_t samples = resolution * resolution;
394 if (heightsHex->size() != samples * 4)
396 DiagnosticCode::ParseError, "Unity m_Heights must contain one little-endian UInt16 per sample", {}, {},
397 "asset.import"));
398 auto spacingX = parseFloat(*scaleMatchX, "m_HeightmapScale.x");
399 auto scaleY = parseFloat(*scaleMatchY, "m_HeightmapScale.y");
400 auto spacingZ = parseFloat(*scaleMatchZ, "m_HeightmapScale.z");
401 if (!spacingX || !scaleY || !spacingZ)
403 Diagnostic::error(DiagnosticCode::ParseError, "Unity terrain scale is invalid", {}, {}, "asset.import"));
404 const auto nibble = [](char value) -> int {
405 if (value >= '0' && value <= '9') return value - '0';
406 if (value >= 'a' && value <= 'f') return value - 'a' + 10;
407 if (value >= 'A' && value <= 'F') return value - 'A' + 10;
408 return -1;
409 };
410 std::vector<std::uint16_t> sourceHeights(samples);
411 for (std::size_t index = 0; index < samples; ++index) {
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));
416 }
417 CanonicalTerrainInput terrain;
418 const auto imageAsset = [&](std::string_view guid) {
419 return std::string("asset://") +
420 request.package.packageId.child("unity:" + unity_detail::foldAscii(std::string(guid)))
421 .child("image:default")
422 .format();
423 };
424 terrain.width = terrain.height = static_cast<std::uint32_t>(resolution);
425 terrain.spacingX = spacingX.value();
426 terrain.spacingZ = spacingZ.value();
427 terrain.heightsMeters.resize(samples);
428 // Unity rows advance +Z; canonical forward is -Z, so reverse rows while preserving winding.
429 for (std::uint32_t row = 0; row < terrain.height; ++row)
430 for (std::uint32_t column = 0; column < terrain.width; ++column)
431 terrain.heightsMeters[std::size_t(row) * terrain.width + column] =
432 float(sourceHeights[std::size_t(terrain.height - 1 - row) * terrain.width + column]) / 65535.0f *
433 scaleY.value();
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()))}};
437
438 if (auto holes = firstMatch(yaml, std::regex(R"(m_HolesTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
439 terrain.holesSource = "unity-guid:" + unity_detail::foldAscii(*holes);
440 terrain.holesAsset = imageAsset(*holes);
441 }
442 const auto controls = sectionEntries(yaml, "m_AlphamapTextures:");
443 if (controls.size() > terrain.controlSources.size())
445 Diagnostic::error(DiagnosticCode::Unsupported, "TVE terrain supports at most four control textures",
446 request.terrainDataPath, {}, "asset.import"));
447 for (std::size_t index = 0; index < controls.size(); ++index) {
448 if (auto guid = firstMatch(controls[index], std::regex(R"(guid:\s*([0-9a-fA-F]{32}))"))) {
449 terrain.controlSources[index] = "unity-guid:" + unity_detail::foldAscii(*guid);
450 terrain.controlAssets[index] = imageAsset(*guid);
451 }
452 }
453 for (const auto& entry : sectionEntries(yaml, "m_TerrainLayers:")) {
454 auto guid = firstMatch(entry, std::regex(R"(guid:\s*([0-9a-fA-F]{32}))"));
455 if (!guid) continue;
456 CanonicalTerrainLayer layer;
457 const auto resolved = paths.find(unity_detail::foldAscii(*guid));
458 layer.name = resolved == paths.end() ? *guid : resolved->second;
459 if (resolved != paths.end()) {
460 auto layerText = textFile(request, resolved->second);
461 if (layerText) {
462 if (auto diffuse =
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);
466 }
467 if (auto normal = firstMatch(layerText.value(),
468 std::regex(R"(m_NormalMapTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
469 layer.normalSource = "unity-guid:" + *normal;
470 layer.normalAsset = imageAsset(*normal);
471 }
472 if (auto mask =
473 firstMatch(layerText.value(), std::regex(R"(m_MaskMapTexture:.*guid:\s*([0-9a-fA-F]{32}))"))) {
474 layer.maskSource = "unity-guid:" + *mask;
475 layer.maskAsset = imageAsset(*mask);
476 }
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)))
482 return Result<void>::success();
483 for (std::size_t component = 0; component < 4; ++component) {
484 auto parsed = parseFloat(match[component + 1].str(), resolved->second + "." + name);
485 if (!parsed) return Result<void>::failure(parsed.status());
486 output[component] = parsed.value();
487 }
488 return Result<void>::success();
489 };
490 for (auto [name, value] : {std::pair{"m_MaskMapRemapMin", &layer.maskRemapMinimum},
491 {"m_MaskMapRemapMax", &layer.maskRemapMaximum},
492 {"m_Specular", &layer.specular}}) {
493 auto parsed = parseVec(name, *value);
494 if (!parsed) return Result<CanonicalTerrainInput>::failure(parsed.status());
495 }
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");
501 if (!x || !y)
503 DiagnosticCode::ParseError, "Unity terrain tile size is invalid", {}, {}, "asset.import"));
504 layer.tileScaleMeters = {x.value(), y.value()};
505 layer.tileSizeMeters = x.value();
506 }
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");
512 if (!x || !y)
514 Diagnostic::error(DiagnosticCode::ParseError, "Unity terrain tile offset is invalid", {},
515 {}, "asset.import"));
516 layer.tileOffsetMeters = {x.value(), y.value()};
517 }
518 for (auto [name, target] : {std::pair{"m_Metallic", &layer.metallic},
519 {"m_NormalScale", &layer.normalScale},
520 {"m_Smoothness", &layer.smoothness}}) {
521 if (auto encoded =
522 firstMatch(layerText.value(), std::regex(std::string(name) + R"(:\s*([^\r\n]+))"))) {
523 auto parsed = parseFloat(*encoded, resolved->second + "." + name);
524 if (!parsed) return Result<CanonicalTerrainInput>::failure(parsed.status());
525 *target = parsed.value();
526 }
527 }
528 }
529 }
530 terrain.layers.push_back(std::move(layer));
531 }
532
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");
537 }
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*([^}]+)\})");
540 auto x = firstMatch(entry, position, 1);
541 auto y = firstMatch(entry, position, 2);
542 auto z = firstMatch(entry, position, 3);
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())
548 DiagnosticCode::ParseError, "Unity tree prototype index is invalid", {}, {}, "asset.import"));
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");
553 if (!px || !py || !pz)
555 DiagnosticCode::ParseError, "Unity tree position is invalid", {}, {}, "asset.import"));
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]+))"))) {
562 auto value = parseFloat(*rotation, "tree.rotation");
563 if (value) {
564 instance.rotation[1] = std::sin(-value.value() * 0.5f);
565 instance.rotation[3] = std::cos(-value.value() * 0.5f);
566 }
567 }
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();
571 }
572 if (auto height = firstMatch(entry, std::regex(R"(heightScale:\s*([^\r\n]+))"))) {
573 auto value = parseFloat(*height, "tree.heightScale");
574 if (value) instance.scale[1] = value.value();
575 }
576 terrain.instances.push_back(std::move(instance));
577 }
578 if (yaml.find("m_DetailPrototypes:") != std::string::npos) {
579 const bool hasDetailSidecar = !terrainDetailsPath(request).empty();
580 findings.push_back(
581 {request.terrainDataPath, "TerrainData.detail-prototypes",
583 !hasDetailSidecar
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"});
586 }
587 findings.push_back({request.terrainDataPath, "TerrainData.heightmap", ImportDisposition::Translated,
588 "UInt16 heights converted to metres and canonical Z"});
590}
591
592struct UnityObject {
593 std::int64_t fileId = 0;
594 std::uint32_t classId = 0;
595 std::string body;
596};
597
598std::vector<UnityObject> documents(std::string_view yaml) {
599 const std::regex header(R"(^---\s*!u!([0-9]+)\s*&(-?[0-9]+).*$)", std::regex::multiline);
600 struct Start {
601 std::size_t header;
602 std::size_t body;
603 UnityObject object;
604 };
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())
614 continue;
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), {}}});
618 }
619 std::vector<UnityObject> result;
620 for (std::size_t index = 0; index < starts.size(); ++index) {
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));
624 }
625 return result;
626}
627
628Result<void> appendPrefab(const UnityProjectImportRequest& request, PreparedAssetImport& output,
629 std::vector<ImportFinding>& findings, const std::string& prefabPath) {
630 if (prefabPath.empty()) return Result<void>::success();
631 auto text = textFile(request, prefabPath);
632 if (!text) return Result<void>::failure(text.status());
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);
639 if (!prefabGuid)
640 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Unity prefab .meta GUID is required",
641 prefabPath + ".meta", {}, "asset.import.unity"));
642 const auto objects = documents(text.value());
643 if (objects.size() > request.limits.maximumAssets)
645 "Unity prefab object count exceeds budget", prefabPath, {},
646 "asset.import"));
647 std::map<std::uint64_t, std::string> gameNames;
648 std::map<std::uint64_t, bool> gameVisibility;
649 for (const auto& object : objects) {
650 if (object.classId == 1) {
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) {
659 findings.push_back(
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"});
663 }
664 }
666 for (const auto& object : objects) {
667 if (object.classId != 4 && object.classId != 224) continue;
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*([^}]+)\})");
671 const std::regex rotation(
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*([^}]+)\})");
674 if (!game) continue;
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);
678 if (!field)
680 "Unity transform field is missing", prefabPath, {},
681 "asset.import.unity"));
682 auto parsed = parseFloat(*field, prefabPath);
683 if (!parsed) return Result<void>::failure(parsed.status());
684 target[component] = parsed.value();
685 }
686 if (reflect) target[2] = -target[2];
687 return Result<void>::success();
688 };
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;
694 q[0] = -q[0];
695 q[1] = -q[1];
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)
702 DiagnosticCode::ParseError, "Unity transform fileID overflows", prefabPath, {}, "asset.import.unity"));
703 const auto gameId = *gameIdValue;
704 const std::uint64_t parentId = *parentIdValue;
706 node["objectId"] = Value(
707 request.package.packageId.child("unity:" + *prefabGuid + ":" + std::to_string(object.fileId)).format());
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));
711 node["parentSourceFileId"] = Value(static_cast<std::int64_t>(parentId));
712 node["position"] = Value(Value::Array{Value(double(p[0])), Value(double(p[1])), Value(double(p[2]))});
713 node["rotation"] =
714 Value(Value::Array{Value(double(q[0])), Value(double(q[1])), Value(double(q[2])), Value(double(q[3]))});
715 node["scale"] = Value(Value::Array{Value(double(s[0])), Value(double(s[1])), Value(double(s[2]))});
716 nodes.emplace_back(std::move(node));
717 }
718 if (nodes.empty())
721 "prefab has no directly convertible Transform nodes; nested prefab expansion is required",
722 prefabPath, {}, "asset.import"));
723 const PersistentId sceneId = request.package.packageId.child("unity-prefab:" + *prefabGuid);
724 Value::Object definition;
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));
729 definition["renderers"] = Value(Value::Array{});
730 definition["coordinateSystem"] = Value("right-handed-x-right-y-up-minus-z-forward");
731 auto encoded = Value(std::move(definition)).toJson();
732 if (!encoded) return Result<void>::failure(encoded.status());
733 std::string definitionText = std::move(encoded).takeValue();
734 const std::string path = "assets/" + sceneId.format() + "/asset.json";
735 auto reference = detail::assetRef(sceneId);
736 if (!reference) return Result<void>::failure(reference.status());
737 output.manifest.assets.push_back(
738 {std::move(reference).takeValue(),
739 "eve.scene-template",
740 SchemaVersion(3),
741 path,
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()) {
747 auto entrypoint = detail::assetRef(sceneId);
748 if (!entrypoint) return Result<void>::failure(entrypoint.status());
749 output.manifest.entrypoints.emplace("default", std::move(entrypoint).takeValue());
750 }
751 findings.push_back({prefabPath, "Prefab.GameObject.Transform", ImportDisposition::Translated,
752 "GUID/fileID hierarchy converted to canonical TRS"});
753 return Result<void>::success();
754}
755
756} // namespace
757
759 auto manifest = detail::baseManifest(request.package, "eve.unity-prefab/1");
760 if (!manifest) return Result<PreparedAssetImport>::failure(manifest.status());
762 output.manifest = std::move(manifest).takeValue();
763 auto prefab = appendPrefab(request, output, output.findings, path);
765 for (const auto& asset : output.manifest.assets) output.sourceMappings.push_back({"Prefab#Transform", asset.asset});
767}
768
770 if (request.files.empty() || request.package.packageId.isNil())
772 Diagnostic::error(DiagnosticCode::InvalidArgument, "Unity project files and package identity are required",
773 {}, {}, "asset.import"));
774 auto sourceIndex = indexUnitySources(request.files, request.limits);
775 if (!sourceIndex) return Result<PreparedAssetImport>::failure(sourceIndex.status());
776 if (request.terrainDataPath.empty() && request.prefabPath.empty())
777 return prepareUnityCollection(request, sourceIndex.value());
778 std::vector<ImportFinding> findings = sourceIndex.value().findings;
780 if (!request.terrainDataPath.empty()) {
781 auto terrain = parseTerrain(request, guidPaths(request), findings);
783 auto details = appendTerrainDetailInstances(request, terrain.value(), findings);
784 if (!details) return Result<PreparedAssetImport>::failure(details.status());
785 auto prepared =
786 prepareCanonicalTerrainImport(request.package, terrain.value(), "eve.unity-terrain/1", request.limits);
787 if (!prepared) return Result<PreparedAssetImport>::failure(prepared.status());
788 output = std::move(prepared).takeValue();
789 if (!terrain.value().detailPrototypes.empty()) {
790 auto collection = prepareUnityCollection(request, sourceIndex.value());
791 if (collection) {
792 std::set<PersistentId> selected;
793 for (const auto& prototype : terrain.value().detailPrototypes) {
794 auto resource = AssetRef::parse(prototype.resourceAsset);
795 if (resource) selected.emplace(resource.value().id());
796 }
797 bool changed = true;
798 while (changed) {
799 changed = false;
800 for (const auto& dependency : collection.value().manifest.dependencies)
801 if (selected.contains(dependency.from.id()) && selected.emplace(dependency.to.id()).second)
802 changed = true;
803 }
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();
810 });
811 if (!duplicate) output.manifest.assets.push_back(asset);
812 runtimePrefixes.emplace("assets/" + asset.asset.id().format() + "/");
813 }
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);
820 }))
821 output.entries.push_back(entry);
822 }
823 }
824 } else {
825 auto manifest = detail::baseManifest(request.package, "eve.unity-prefab/1");
826 if (!manifest) return Result<PreparedAssetImport>::failure(manifest.status());
827 output.manifest = std::move(manifest).takeValue();
828 }
829 auto prefab = appendPrefab(request, output, findings, request.prefabPath);
831 output.manifest.provenance["sourceEngine"] = Value("unity");
832 output.manifest.provenance["sourceCoordinateSystem"] = Value("left-handed-x-right-y-up-z-forward");
833 output.findings.insert(output.findings.end(), std::make_move_iterator(findings.begin()),
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});
839 }
840 auto report = detail::finalizeImportReport(output, request.package, "unity", "force-text",
841 {{"serialization", Value("force-text")}});
842 if (!report) return Result<PreparedAssetImport>::failure(report.status());
844}
845
846} // namespace eve::asset_import
LogicalId target
double value
Duration start
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string prefab
std::string output
std::string terrain
ActiveSource owned
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
std::unordered_map< std::string, QuestRuntime > entries
int mask
building::EdgeCurveGroup group
std::vector< BuildingInstanceSnapshot > instances
float py
float pz
glm::vec4 p[6]
int column
std::uint64_t parentId
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
glm::vec4 tint
std::array< double, 10 > q
std::int32_t second
std::int32_t first
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
std::string text
std::array< float, 4 > rotation
std::array< float, 3 > position
std::array< float, 3 > scale
std::uint64_t bytes
std::string name
std::vector< BvhNode > nodes
Texture * normal
std::string error
Definition Package.cpp:60
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
Mesh * mesh
const RoadNode * node
double number
bool found
double current
std::string resource
std::uint32_t count
AbilityInstancingPolicy instancing
double spacingZ
double spacingX
Json object
V3 heading
Definition TreeMesh.cpp:292
float(ui::Theme::* member)[4]
std::string body
std::int64_t fileId
std::uint32_t classId
Direct Unity text-serialization adapter for TerrainData and Prefab assets.
std::map< std::string, std::string > paths
std::vector< ImportFinding > findings
uint32_t index
std::vector< std::string_view > lines
std::size_t at
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
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
std::string format() const
Formats the ID in lower-case canonical UUID text.
Definition Identity.h:173
Id128 child(std::string_view role) const noexcept
Derives a deterministic child UUID for a hierarchical identity.
Definition Identity.h:192
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
Definition Database.h:26
eve::Diagnostic Diagnostic
eve::Result< T > Result
const EditorValue * field(const EditorValue &value, const char *name)
std::string header(int localX, int localY)
Header.
Definition KernelGen.cpp:13
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
StrongUuid< detail::PersistentIdTag > PersistentId
Stable instance identity for persistence, networking and process boundaries.
Definition Identity.h:254
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.
Owning, validated source archive candidate not yet published to disk/database.
In-memory Unity project slice; keys are normalized project-relative paths.
glm::vec4 color