载入中...
搜索中...
未找到
EditorDiskAssetCatalog.cpp
浏览该文件的文档.
2
4
5#include <fstream>
6#include <iomanip>
7#include <iterator>
8#include <set>
9#include <sstream>
10
11namespace eve::editor {
12namespace {
13
14Result<DiskAssetScanResult> scanError(EditorStatus status, const char* rule, std::string message) {
15 return eve::editing::failed<DiskAssetScanResult>(status, RuleId(rule), std::move(message));
16}
17
18std::string textMember(const EditorValue::Object& object, const char* key) {
19 const auto found = object.find(key);
20 const auto* value = found == object.end() ? nullptr : found->second.getIf<std::string>();
21 return value ? *value : std::string{};
22}
23
24} // namespace
25
26DiskAssetCatalog::DiskAssetCatalog(std::filesystem::path projectRoot, MemoryAssetDatabase* database)
27 : database_(database) {
28 std::error_code ec;
29 contentRoot_ = std::filesystem::weakly_canonical(std::move(projectRoot), ec) / "Content";
30 if (ec) contentRoot_.clear();
31}
32
33std::filesystem::path DiskAssetCatalog::resolveContent(const std::filesystem::path& relative) const {
34 if (contentRoot_.empty() || relative.empty() || relative.is_absolute()) return {};
35 const auto candidate = (contentRoot_ / relative).lexically_normal();
36 const auto scoped = candidate.lexically_relative(contentRoot_);
37 if (scoped.empty()) return {};
38 for (const auto& part : scoped)
39 if (part == "..") return {};
40 return candidate;
41}
42
43Result<void> DiskAssetCatalog::writeSidecar(const std::filesystem::path& contentRelativePath, AssetGuid guid,
44 std::string typeId, std::uint32_t schemaVersion) {
45 const auto source = resolveContent(contentRelativePath);
46 if (source.empty() || guid.empty() || typeId.empty())
47 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.asset.sidecar-invalid"),
48 "Content path, GUID and type are required");
49 EditorValue::Object metadata;
50 metadata["guid"] = EditorValue(guid.value());
51 metadata["type"] = EditorValue(std::move(typeId));
52 metadata["schemaVersion"] = EditorValue(static_cast<std::int64_t>(schemaVersion));
53 const std::filesystem::path sidecar = source.string() + ".evmeta";
54 std::error_code ec;
55 std::filesystem::create_directories(sidecar.parent_path(), ec);
56 if (ec)
57 return eve::editing::failed<void>(EditorStatus::Failed, RuleId("editor.asset.sidecar-directory"), ec.message());
58 const std::filesystem::path temporary = sidecar.string() + ".tmp";
59 {
60 std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
61 if (!output)
62 return eve::editing::failed<void>(EditorStatus::Failed, RuleId("editor.asset.sidecar-write"),
63 "Cannot open asset sidecar temp file");
64 output << editorValueToJson(EditorValue(std::move(metadata)));
65 }
66 std::filesystem::rename(temporary, sidecar, ec);
67 if (ec) {
68 std::filesystem::remove(sidecar, ec);
69 ec.clear();
70 std::filesystem::rename(temporary, sidecar, ec);
71 }
72 if (ec)
73 return eve::editing::failed<void>(EditorStatus::Failed, RuleId("editor.asset.sidecar-replace"), ec.message());
74 return eve::editing::applied<void>();
75}
76
78 if (!database_ || contentRoot_.empty())
79 return scanError(EditorStatus::Rejected, "editor.asset.catalog-invalid",
80 "Asset database and Content root are required");
82 std::error_code ec;
83 if (!std::filesystem::exists(contentRoot_)) return eve::editing::applied<DiskAssetScanResult>(std::move(result));
84 std::set<AssetGuid> discovered;
85 for (std::filesystem::recursive_directory_iterator iterator(contentRoot_, ec), end; iterator != end && !ec;
86 iterator.increment(ec)) {
87 if (!iterator->is_regular_file() || iterator->path().extension() != ".evmeta") continue;
88 std::ifstream input(iterator->path(), std::ios::binary);
89 const std::string json{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
90 auto parsed = editorValueFromJson(json);
91 const auto* metadata = parsed.ok() ? parsed.value().getIf<EditorValue::Object>() : nullptr;
92 const AssetGuid guid(metadata ? textMember(*metadata, "guid") : std::string{});
93 const std::string type = metadata ? textMember(*metadata, "type") : std::string{};
94 if (!parsed.ok() || !metadata || guid.empty() || type.empty()) {
95 result.diagnostics.push_back(editing::ruleDiagnostic(
96 eve::DiagnosticCode::InvalidArgument, RuleId("editor.asset.sidecar-invalid"),
97 DiagnosticSeverity::Error,
98 "Invalid asset sidecar: " + iterator->path().generic_string()));
99 continue;
100 }
101 if (!discovered.emplace(guid).second) {
102 result.diagnostics.push_back(editing::ruleDiagnostic(
103 eve::DiagnosticCode::Conflict, RuleId("editor.asset.duplicate-guid"),
104 DiagnosticSeverity::Error, "Duplicate asset GUID: " + guid.value()));
105 continue;
106 }
107 std::filesystem::path source = iterator->path();
108 source.replace_extension();
109 if (!std::filesystem::is_regular_file(source)) {
110 result.diagnostics.push_back(editing::ruleDiagnostic(
111 eve::DiagnosticCode::NotFound, RuleId("editor.asset.source-missing"),
112 DiagnosticSeverity::Error,
113 "Asset sidecar source is missing: " + source.generic_string()));
114 continue;
115 }
116 const auto relative = source.lexically_relative(contentRoot_);
117 const std::string hash = fileHash(source);
118 const std::string fingerprint = relative.generic_string() + ":" + hash + ":" + json;
119 AssetRecord record;
120 record.guid = guid;
121 record.logicalUri = "content://" + relative.generic_string();
122 record.typeId = type;
123 record.sourceUri = source.generic_string();
124 record.sourceHash = hash;
125 record.schemaVersion = 1;
126 auto published = database_->publish(std::move(record));
127 if (!published.ok()) {
128 const auto& diagnostics = published.diagnostics();
129 result.diagnostics.insert(result.diagnostics.end(), diagnostics.begin(), diagnostics.end());
130 continue;
131 }
132 ++result.indexed;
133 const auto previous = fingerprints_.find(guid);
134 if (previous == fingerprints_.end() || previous->second != fingerprint) ++result.changed;
135 fingerprints_.insert_or_assign(guid, fingerprint);
136 }
137 if (ec) return scanError(EditorStatus::Failed, "editor.asset.scan-failed", ec.message());
138 auto diagnostics = result.diagnostics;
139 return eve::editing::applied<DiskAssetScanResult>(std::move(result), std::move(diagnostics));
140}
141
143
144std::string DiskAssetCatalog::fileHash(const std::filesystem::path& path) {
145 std::ifstream input(path, std::ios::binary);
146 std::uint64_t hash = 1469598103934665603ULL;
147 char buffer[8192];
148 while (input) {
149 input.read(buffer, sizeof(buffer));
150 for (std::streamsize index = 0; index < input.gcount(); ++index) {
151 hash ^= static_cast<unsigned char>(buffer[index]);
152 hash *= 1099511628211ULL;
153 }
154 }
155 std::ostringstream text;
156 text << std::hex << std::setfill('0') << std::setw(16) << hash;
157 return text.str();
158}
159
160} // namespace eve::editor
double value
Vec3 relative
Definition AnimSmr.cpp:164
std::string output
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
graphics::Canvas * previous
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string path
Definition PlayHost.cpp:110
bool found
uint32_t index
const UnitySourceAsset & source
Move-only operation result carrying either a value or Status.
Definition Result.h:155
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
std::map< std::string, Value > Object
Result< DiskAssetScanResult > scan()
Scan every sidecar and atomically publish valid records.
Result< void > writeSidecar(const std::filesystem::path &contentRelativePath, AssetGuid guid, std::string typeId, std::uint32_t schemaVersion=1)
Create/replace a sidecar for a Content-relative source file.
DiskAssetCatalog(std::filesystem::path projectRoot, MemoryAssetDatabase *database)
Create a host-driven disk asset catalog.
Result< DiskAssetScanResult > poll()
Rescan and report only content/sidecar fingerprints that changed.
Thread-neutral in-memory asset index used by editor hosts and tests.
Result< AssetRecord > publish(AssetRecord record, std::vector< AssetDependency > dependencies={})
Atomically publish one validated record and its dependencies.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::StatusCode EditorStatus
Definition EditorResult.h:8
Value snapshot stored by the editor asset index.
Result of one disk asset sidecar scan/poll.
std::vector< EditorDiagnostic > diagnostics