yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
custom_object.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <iterator>
8#include <memory>
9#include <string>
10#include <string_view>
11#include <tuple>
12#include <unordered_set>
13#include <utility>
14
15#include "absl/strings/str_format.h"
17#include "util/log.h"
18#include "util/macro.h"
20
21namespace yaze {
22namespace zelda3 {
23
24namespace {
25
26namespace fs = std::filesystem;
27
28constexpr int kBufferStrideBytes = 128;
30constexpr int kBufferHeightTiles = 64;
32constexpr int kMaxSegmentTiles = 0x20;
33
35 .subject = "custom object asset",
36 .published_file = "published custom object asset",
37};
38
39absl::StatusOr<std::vector<uint8_t>> ReadBinaryFileAtPath(
40 const fs::path& path) {
41 std::ifstream input(path, std::ios::binary);
42 if (!input.is_open()) {
43 return absl::NotFoundError(absl::StrFormat(
44 "Could not open custom object file: %s", path.string()));
45 }
46 std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(input)),
47 std::istreambuf_iterator<char>());
48 if (input.bad()) {
49 return absl::InternalError(absl::StrFormat(
50 "Could not read custom object file: %s", path.string()));
51 }
52 return bytes;
53}
54
55bool PathStartsWith(const fs::path& path, const fs::path& root) {
56 auto path_it = path.begin();
57 for (auto root_it = root.begin(); root_it != root.end();
58 ++root_it, ++path_it) {
59 if (path_it == path.end() || *path_it != *root_it) {
60 return false;
61 }
62 }
63 return true;
64}
65
66bool IsPortableAssetComponent(std::string_view component) {
67 if (component.empty() ||
68 std::isspace(static_cast<unsigned char>(component.front())) ||
69 std::isspace(static_cast<unsigned char>(component.back())) ||
70 component.back() == '.') {
71 return false;
72 }
73 for (const unsigned char character : component) {
74 if (character < 0x20 || character == 0x7F || character == ',' ||
75 character == '<' || character == '>' || character == ':' ||
76 character == '"' || character == '\\' || character == '|' ||
77 character == '?' || character == '*') {
78 return false;
79 }
80 }
81 std::string basename(component.substr(0, component.find('.')));
82 std::transform(basename.begin(), basename.end(), basename.begin(),
83 [](unsigned char character) {
84 return static_cast<char>(std::toupper(character));
85 });
86 static const std::unordered_set<std::string> kWindowsReservedNames = {
87 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4",
88 "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3",
89 "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
90 };
91 return !kWindowsReservedNames.contains(basename);
92}
93
94std::vector<CustomObject::TileMapEntry> VisibleRuntimeTiles(
95 const CustomObject& object) {
96 std::vector<CustomObject::TileMapEntry> visible;
97 visible.reserve(object.tiles.size());
98 for (const auto& tile : object.tiles) {
99 if (tile.tile_data != 0) {
100 visible.push_back(tile);
101 }
102 }
103 std::sort(
104 visible.begin(), visible.end(), [](const auto& lhs, const auto& rhs) {
105 return std::tie(lhs.rel_y, lhs.rel_x) < std::tie(rhs.rel_y, rhs.rel_x);
106 });
107 return visible;
108}
109
110} // namespace
111
112absl::StatusOr<CustomObject> DecodeCustomObjectBinary(
113 const std::vector<uint8_t>& data) {
114 if (data.empty()) {
115 return absl::DataLossError("Custom object data is empty");
116 }
117
118 CustomObject object;
119 std::unordered_set<int> occupied_positions;
120 size_t cursor = 0;
121 int current_buffer_pos = 0;
122 bool found_terminator = false;
123
124 while (cursor + 1 < data.size()) {
125 const uint16_t header = static_cast<uint16_t>(data[cursor]) |
126 (static_cast<uint16_t>(data[cursor + 1]) << 8);
127 cursor += 2;
128 if (header == 0) {
129 found_terminator = true;
130 break;
131 }
132 const int encoded_count = header & 0x001F;
133 const int count = encoded_count == 0 ? kMaxSegmentTiles : encoded_count;
134 // The runtime decrements the complete header before testing its low five
135 // count bits. A 32-tile segment is encoded as count zero, so its final
136 // decrement borrows from the stored high byte.
137 const int jump_offset = ((header - count) >> 8) & 0xFF;
138 if ((jump_offset & 1) != 0) {
139 return absl::DataLossError(
140 "Custom object segment jump is not tile-aligned");
141 }
142 if (cursor + static_cast<size_t>(count * 2) > data.size()) {
143 return absl::DataLossError("Custom object ends inside a tile segment");
144 }
145
146 const int segment_start = current_buffer_pos;
147 for (int index = 0; index < count; ++index) {
148 if (current_buffer_pos < 0 ||
149 current_buffer_pos + 1 >= kBufferSizeBytes ||
150 (current_buffer_pos & 1) != 0) {
151 return absl::OutOfRangeError(
152 "Custom object tile is outside the 64x64 dungeon buffer");
153 }
154 if (!occupied_positions.insert(current_buffer_pos).second) {
155 return absl::DataLossError(
156 "Custom object segments overlap the same tile position");
157 }
158
159 const uint16_t tile_data = static_cast<uint16_t>(data[cursor]) |
160 (static_cast<uint16_t>(data[cursor + 1]) << 8);
161 cursor += 2;
162 object.tiles.push_back({(current_buffer_pos % kBufferStrideBytes) / 2,
163 current_buffer_pos / kBufferStrideBytes,
164 tile_data});
165 current_buffer_pos += 2;
166 }
167 current_buffer_pos = segment_start + jump_offset;
168 }
169
170 if (!found_terminator) {
171 return absl::DataLossError("Custom object is missing its terminator");
172 }
173 if (cursor != data.size()) {
174 return absl::DataLossError(
175 "Custom object contains trailing bytes after its terminator");
176 }
177 return object;
178}
179
180absl::StatusOr<std::vector<uint8_t>> EncodeCustomObjectBinary(
181 const CustomObject& object) {
182 if (object.tiles.empty()) {
183 return std::vector<uint8_t>{0, 0};
184 }
185
186 struct OrderedTile {
187 int position;
188 uint16_t tile_data;
189 };
190 std::vector<OrderedTile> ordered_tiles;
191 ordered_tiles.reserve(object.tiles.size());
192 for (const auto& tile : object.tiles) {
193 if (tile.rel_x < 0 || tile.rel_x >= kBufferWidthTiles || tile.rel_y < 0 ||
194 tile.rel_y >= kBufferHeightTiles) {
195 return absl::OutOfRangeError(
196 "Custom object tile is outside the 64x64 dungeon buffer");
197 }
198 ordered_tiles.push_back(
199 {tile.rel_y * kBufferWidthTiles + tile.rel_x, tile.tile_data});
200 }
201 std::sort(ordered_tiles.begin(), ordered_tiles.end(),
202 [](const OrderedTile& lhs, const OrderedTile& rhs) {
203 return lhs.position < rhs.position;
204 });
205 for (size_t index = 1; index < ordered_tiles.size(); ++index) {
206 if (ordered_tiles[index - 1].position == ordered_tiles[index].position) {
207 return absl::InvalidArgumentError(
208 "Custom object contains duplicate tile positions");
209 }
210 }
211 struct Segment {
212 int start_position;
213 std::vector<uint16_t> tile_words;
214 };
215 std::vector<Segment> segments;
216 size_t tile_index = 0;
217 int segment_start = 0;
218 while (tile_index < ordered_tiles.size()) {
219 Segment segment{segment_start, {}};
220 if (ordered_tiles[tile_index].position == segment_start) {
221 int next_position = segment_start;
222 while (tile_index < ordered_tiles.size() &&
223 ordered_tiles[tile_index].position == next_position &&
224 segment.tile_words.size() < kMaxSegmentTiles) {
225 segment.tile_words.push_back(ordered_tiles[tile_index].tile_data);
226 ++tile_index;
227 ++next_position;
228 }
229 } else {
230 // Oracle advances for a zero payload word without touching the dungeon
231 // tilemap. One no-op word gives a distant target a legal segment from
232 // which the next 8-bit jump can continue.
233 segment.tile_words.push_back(0);
234 }
235 segments.push_back(std::move(segment));
236
237 if (tile_index >= ordered_tiles.size()) {
238 break;
239 }
240 const int next_target = ordered_tiles[tile_index].position;
241 const int minimum_next_start =
242 segment_start + static_cast<int>(segments.back().tile_words.size());
243 const int maximum_next_start = segment_start + 0x7F;
244 segment_start = std::min(next_target, maximum_next_start);
245 if (segment_start < minimum_next_start) {
246 return absl::DataLossError(
247 "Custom object segment planner produced an overlapping jump");
248 }
249 }
250
251 std::vector<uint8_t> binary;
252 binary.reserve(object.tiles.size() * 2 + segments.size() * 2 + 2);
253 for (size_t index = 0; index < segments.size(); ++index) {
254 int jump_bytes = 0;
255 if (index + 1 < segments.size()) {
256 jump_bytes = (segments[index + 1].start_position -
257 segments[index].start_position) *
258 2;
259 if (jump_bytes <= 0 || jump_bytes > 0xFE) {
260 return absl::DataLossError(
261 "Custom object segment planner exceeded the runtime jump range");
262 }
263 }
264 const bool encodes_thirty_two_tiles =
265 segments[index].tile_words.size() == kMaxSegmentTiles;
266 int stored_jump = jump_bytes;
267 if (encodes_thirty_two_tiles) {
268 ++stored_jump;
269 if (stored_jump > 0xFF) {
270 return absl::InvalidArgumentError(
271 "Custom object gap after a 32-tile segment cannot be represented "
272 "by its adjusted 8-bit jump");
273 }
274 }
275 const uint16_t encoded_count =
276 static_cast<uint16_t>(segments[index].tile_words.size()) & 0x001F;
277 const uint16_t header =
278 encoded_count | (static_cast<uint16_t>(stored_jump) << 8);
279 binary.push_back(static_cast<uint8_t>(header & 0xFF));
280 binary.push_back(static_cast<uint8_t>(header >> 8));
281 for (uint16_t tile_word : segments[index].tile_words) {
282 binary.push_back(static_cast<uint8_t>(tile_word & 0xFF));
283 binary.push_back(static_cast<uint8_t>(tile_word >> 8));
284 }
285 }
286 binary.push_back(0);
287 binary.push_back(0);
288
289 auto decoded_or = DecodeCustomObjectBinary(binary);
290 if (!decoded_or.ok()) {
291 return absl::DataLossError(
292 absl::StrFormat("Encoded custom object failed strict validation: %s",
293 decoded_or.status().message()));
294 }
295 if (VisibleRuntimeTiles(*decoded_or) != VisibleRuntimeTiles(object)) {
296 return absl::DataLossError(
297 "Encoded custom object did not preserve its visible runtime layout");
298 }
299 return binary;
300}
301
302absl::StatusOr<fs::path> ResolveCustomObjectAssetPath(
303 const std::string& custom_objects_folder, const std::string& filename) {
304 if (custom_objects_folder.empty()) {
305 return absl::FailedPreconditionError(
306 "Custom objects folder is not configured");
307 }
308 if (filename.empty()) {
309 return absl::InvalidArgumentError("Custom object filename is empty");
310 }
311 // Windows treats backslashes as separators before component validation can
312 // inspect them. Validate the stored mapping syntax before host path parsing.
313 if (filename.find('\\') != std::string::npos) {
314 return absl::InvalidArgumentError(
315 "Custom object filename must use forward slashes");
316 }
317
318 const fs::path relative_path(filename);
319 if (relative_path.is_absolute() || relative_path.has_root_directory() ||
320 relative_path.has_root_name()) {
321 return absl::InvalidArgumentError(
322 "Custom object filename must be project-relative");
323 }
324 for (const auto& component : relative_path) {
325 if (component == "..") {
326 return absl::InvalidArgumentError(
327 "Custom object filename cannot contain parent traversal");
328 }
329 if (component != "." &&
330 !IsPortableAssetComponent(component.generic_string())) {
331 return absl::InvalidArgumentError(
332 "Custom object filename contains a non-portable path component");
333 }
334 }
335 const fs::path normalized = relative_path.lexically_normal();
336 if (normalized.empty() || normalized.filename().empty() ||
337 normalized.extension() != ".bin") {
338 return absl::InvalidArgumentError(
339 "Custom object filename must end in .bin");
340 }
341
342 std::error_code canonical_error;
343 const fs::path canonical_root =
344 fs::canonical(fs::path(custom_objects_folder), canonical_error);
345 if (canonical_error || !fs::is_directory(canonical_root, canonical_error) ||
346 canonical_error) {
347 return absl::FailedPreconditionError(absl::StrFormat(
348 "Custom objects folder is unavailable: %s", custom_objects_folder));
349 }
350 const fs::path candidate_parent = canonical_root / normalized.parent_path();
351 const fs::path canonical_parent =
352 fs::canonical(candidate_parent, canonical_error);
353 if (canonical_error || !fs::is_directory(canonical_parent, canonical_error) ||
354 canonical_error) {
355 return absl::FailedPreconditionError(
356 absl::StrFormat("Custom object parent folder is unavailable: %s",
357 candidate_parent.string()));
358 }
359 if (!PathStartsWith(canonical_parent, canonical_root)) {
360 return absl::PermissionDeniedError(
361 "Custom object filename resolves outside the configured folder");
362 }
363
364 const fs::path target = canonical_parent / normalized.filename();
365 std::error_code exists_error;
366 const bool target_exists = fs::exists(target, exists_error);
367 if (exists_error) {
368 return absl::FailedPreconditionError(
369 absl::StrFormat("Could not inspect custom object target %s: %s",
370 target.string(), exists_error.message()));
371 }
372 if (target_exists) {
373 const fs::file_status status = fs::symlink_status(target, exists_error);
374 if (exists_error || fs::is_symlink(status) ||
375 !fs::is_regular_file(status)) {
376 return absl::PermissionDeniedError(
377 "Custom object target must be a regular non-symlink file");
378 }
379 const fs::path canonical_target = fs::canonical(target, exists_error);
380 if (exists_error || !PathStartsWith(canonical_target, canonical_root)) {
381 return absl::PermissionDeniedError(
382 "Custom object target resolves outside the configured folder");
383 }
384 return canonical_target;
385 }
386 return target;
387}
388
389absl::StatusOr<CustomObjectAsset> LoadCustomObjectAsset(
390 const std::string& custom_objects_folder, const std::string& filename) {
391 fs::path resolved_path;
393 custom_objects_folder, filename));
394 std::vector<uint8_t> source_bytes;
395 ASSIGN_OR_RETURN(source_bytes, ReadBinaryFileAtPath(resolved_path));
396 CustomObject object;
397 ASSIGN_OR_RETURN(object, DecodeCustomObjectBinary(source_bytes));
398 return CustomObjectAsset{.object = std::move(object),
399 .source_bytes = std::move(source_bytes),
400 .resolved_path = std::move(resolved_path)};
401}
402
403absl::StatusOr<std::vector<uint8_t>> PublishCustomObjectBinary(
404 const std::string& custom_objects_folder, const std::string& filename,
405 const CustomObject& object,
406 const std::vector<uint8_t>& expected_source_bytes,
407 const fs::path& expected_resolved_path) {
408#if defined(__EMSCRIPTEN__)
409 return absl::FailedPreconditionError(
410 "Custom object publishing is unavailable in browser builds because "
411 "durable atomic project-file publication cannot be guaranteed");
412#else
413 if (expected_source_bytes.empty()) {
414 return absl::FailedPreconditionError(
415 "Custom object editing requires an exact source-byte snapshot");
416 }
417 if (expected_resolved_path.empty()) {
418 return absl::FailedPreconditionError(
419 "Custom object editing requires its original resolved asset path");
420 }
421 // Reject a corrupt or mismatched capture before acquiring a write lock.
422 RETURN_IF_ERROR(DecodeCustomObjectBinary(expected_source_bytes).status());
423
424 std::vector<uint8_t> binary;
426 fs::path target;
428 target, ResolveCustomObjectAssetPath(custom_objects_folder, filename));
429 if (target != expected_resolved_path) {
430 return absl::AbortedError(
431 "Custom object project or asset path changed after it was opened; "
432 "edits were kept");
433 }
434
435 std::unique_ptr<core::SourceArtifactPublicationLock> publication_lock;
436 ASSIGN_OR_RETURN(publication_lock,
438 {target}, kCustomObjectPublisherLabels));
439
440 fs::path locked_target;
442 custom_objects_folder, filename));
443 if (locked_target != target) {
444 return absl::AbortedError(
445 "Custom object path changed while acquiring its publication lock");
446 }
447 std::vector<uint8_t> source_before;
448 ASSIGN_OR_RETURN(source_before, ReadBinaryFileAtPath(locked_target));
449 if (source_before != expected_source_bytes) {
450 return absl::AbortedError(
451 "Custom object source changed after it was opened; edits were kept");
452 }
453
454 const std::string before(source_before.begin(), source_before.end());
455 const std::string after(binary.begin(), binary.end());
456 const std::string expected_sha256 = core::ComputeSourceArtifactSha256(before);
458 *publication_lock,
459 {{.target = locked_target, .before = before, .after = after}},
460 expected_sha256, [&]() -> absl::Status {
461 std::vector<uint8_t> reopened;
462 ASSIGN_OR_RETURN(reopened, ReadBinaryFileAtPath(locked_target));
463 if (reopened != binary) {
464 return absl::DataLossError(
465 "Published custom object failed exact byte readback");
466 }
467 CustomObject decoded;
468 ASSIGN_OR_RETURN(decoded, DecodeCustomObjectBinary(reopened));
469 if (VisibleRuntimeTiles(decoded) != VisibleRuntimeTiles(object)) {
470 return absl::DataLossError(
471 "Published custom object changed its visible runtime layout");
472 }
473 return absl::OkStatus();
474 }));
475 return binary;
476#endif
477}
478
479uint16_t CustomObjectRuntimeTileWord(int object_id, uint16_t source_word) {
480 if (source_word == 0) {
481 return 0;
482 }
483
484 // Oracle's SpriteObjectsDraw handler forces Kydreeok/Manhandla body tiles
485 // into character page 0x300 after checking for a zero/no-op source word.
486 if (object_id == 0x54) {
487 return source_word | 0x0300;
488 }
489 return source_word;
490}
491
492// These are subtypes of custom object 0x31 itself. The corner-named assets do
493// not override standard wall-corner objects 0x100-0x103.
494const std::vector<std::string> CustomObjectManager::kSubtype1Filenames = {
495 "track_LR.bin", // 00
496 "track_UD.bin", // 01
497 "track_corner_TL.bin", // 02
498 "track_corner_TR.bin", // 03
499 "track_corner_BL.bin", // 04
500 "track_corner_BR.bin", // 05
501 "track_floor_UD.bin", // 06
502 "track_floor_LR.bin", // 07
503 "track_floor_corner_TL.bin", // 08
504 "track_floor_corner_TR.bin", // 09
505 "track_floor_corner_BL.bin", // 10
506 "track_floor_corner_BR.bin", // 11
507 "track_floor_any.bin", // 12
508 "wall_sword_house.bin", // 13
509 "track_any.bin", // 14
510 "small_statue.bin", // 15
511};
512
513const std::vector<std::string> CustomObjectManager::kSubtype2Filenames = {
514 "furnace.bin", // 00
515 "firewood.bin", // 01
516 "ice_chair.bin", // 02
517};
518
519const std::vector<std::string> CustomObjectManager::kSubtype54Filenames = {
520 "kydreeok_body.bin", // 00
521 "manhandla_body_1a.bin", // 01
522};
523
525 static CustomObjectManager instance;
526 return instance;
527}
528
532
539
547
551
553 auto& context = ActiveContext();
554 context.cache.clear();
555 context.asset_generation = NextAssetGeneration();
557}
558
559void CustomObjectManager::Initialize(const std::string& custom_objects_folder) {
560 ActiveContext().state.base_path = custom_objects_folder;
562#if !defined(NDEBUG)
563 LOG_INFO("CustomObjectManager", "Initialize: base_path='%s'",
564 GetBasePath().c_str());
565 if (const auto* list = ResolveFileList(0x31)) {
566 LOG_INFO("CustomObjectManager", "Object 0x31 file list has %zu entries",
567 list->size());
568 }
569#endif
570}
571
573 const std::unordered_map<int, std::vector<std::string>>& map) {
576}
577
582
586
588 const State& state) {
589 auto [context_it, inserted] = runtime_contexts_.try_emplace(context_id);
590 auto& context = context_it->second;
591 if (inserted || context.state != state) {
592 context.state = state;
593 context.cache.clear();
594 context.asset_generation = NextAssetGeneration();
596 }
597 active_runtime_context_id_ = context_id;
598}
599
603
605 if (active_runtime_context_id_ == context_id) {
607 }
608 runtime_contexts_.erase(context_id);
609}
610
611const std::vector<std::string>* CustomObjectManager::ResolveFileList(
612 int object_id) const {
613 const auto& custom_file_map = ActiveContext().state.custom_file_map;
614 auto custom_it = custom_file_map.find(object_id);
615 if (custom_it != custom_file_map.end()) {
616 return &custom_it->second;
617 }
618 if (object_id == 0x31) {
619 return &kSubtype1Filenames;
620 }
621 if (object_id == 0x32) {
622 return &kSubtype2Filenames;
623 }
624 if (object_id == 0x54) {
625 return &kSubtype54Filenames;
626 }
627 return nullptr;
628}
629
630absl::StatusOr<std::shared_ptr<CustomObject>> CustomObjectManager::LoadObject(
631 const std::string& filename) {
632 auto& cache = ActiveContext().cache;
633 if (const auto cached = cache.find(filename); cached != cache.end()) {
634 return cached->second;
635 }
636
637 auto asset_or = LoadCustomObjectAsset(GetBasePath(), filename);
638 if (!asset_or.ok()) {
639 LOG_ERROR("CustomObjectManager", "%s",
640 asset_or.status().ToString().c_str());
641 cache.emplace(filename, asset_or.status());
642 return asset_or.status();
643 }
644
645 auto object_ptr = std::make_shared<CustomObject>(std::move(asset_or->object));
646 cache.emplace(filename, object_ptr);
647
648 return object_ptr;
649}
650
651absl::StatusOr<std::shared_ptr<CustomObject>>
652CustomObjectManager::GetObjectInternal(int object_id, int subtype) {
653 const std::vector<std::string>* list = ResolveFileList(object_id);
654 if (!list) {
655 return absl::NotFoundError("Object ID not mapped to custom object");
656 }
657
658 const int runtime_count = RuntimeSubtypeCountForObject(object_id);
659 if (subtype < 0 || (runtime_count > 0 && subtype >= runtime_count) ||
660 subtype >= static_cast<int>(list->size())) {
661 return absl::OutOfRangeError("Subtype index out of range");
662 }
663
664 return LoadObject((*list)[subtype]);
665}
666
667int CustomObjectManager::GetSubtypeCount(int object_id) const {
668 const int runtime_count = RuntimeSubtypeCountForObject(object_id);
669 if (runtime_count > 0) {
670 return runtime_count;
671 }
672 if (const auto* list = ResolveFileList(object_id)) {
673 return static_cast<int>(list->size());
674 }
675 return 0;
676}
677
679 return static_cast<int>(DefaultSubtypeFilenamesForObject(object_id).size());
680}
681
682const std::array<int, 3>& CustomObjectManager::RuntimeObjectIds() {
683 static constexpr std::array<int, 3> kRuntimeObjectIds = {0x31, 0x32, 0x54};
684 return kRuntimeObjectIds;
685}
686
688 int object_id) const {
689 const auto* list = ResolveFileList(object_id);
690 if (list)
691 return *list;
692 return {};
693}
694
695const std::vector<std::string>&
697 static const std::vector<std::string> kEmpty;
698 if (object_id == 0x31) {
699 return kSubtype1Filenames;
700 }
701 if (object_id == 0x32) {
702 return kSubtype2Filenames;
703 }
704 if (object_id == 0x54) {
705 return kSubtype54Filenames;
706 }
707 return kEmpty;
708}
709
713
714absl::StatusOr<CustomObjectSlotBinding> CustomObjectManager::ResolveSlotBinding(
715 int object_id, int subtype) const {
716 const int runtime_count = RuntimeSubtypeCountForObject(object_id);
717 if (runtime_count <= 0) {
718 return absl::NotFoundError("Object ID has no fixed custom runtime slots");
719 }
720 if (subtype < 0 || subtype >= runtime_count) {
721 return absl::OutOfRangeError("Custom runtime subtype is out of range");
722 }
723
725 const auto& custom_file_map = ActiveContext().state.custom_file_map;
726 const auto mapped_it = custom_file_map.find(object_id);
727 if (mapped_it == custom_file_map.end()) {
728 const auto& defaults = DefaultSubtypeFilenamesForObject(object_id);
729 binding.filename = defaults[static_cast<size_t>(subtype)];
731 } else if (subtype >= static_cast<int>(mapped_it->second.size()) ||
732 mapped_it->second[static_cast<size_t>(subtype)].empty()) {
734 } else {
735 binding.filename = mapped_it->second[static_cast<size_t>(subtype)];
737 }
738 return binding;
739}
740
741std::string CustomObjectManager::ResolveFilename(int object_id,
742 int subtype) const {
743 if (RuntimeSubtypeCountForObject(object_id) > 0) {
744 const auto binding = ResolveSlotBinding(object_id, subtype);
745 return binding.ok() ? binding->filename : "";
746 }
747 const auto* list = ResolveFileList(object_id);
748 const int runtime_count = RuntimeSubtypeCountForObject(object_id);
749 if (list && subtype >= 0 && (runtime_count == 0 || subtype < runtime_count) &&
750 subtype < static_cast<int>(list->size())) {
751 return (*list)[subtype];
752 }
753 return "";
754}
755
759
761 ActiveContext().state = state;
763}
764
765const std::string& CustomObjectManager::GetBasePath() const {
767}
768
772
773} // namespace zelda3
774} // namespace yaze
Manages loading and caching of custom object binary files.
int GetSubtypeCount(int object_id) const
void RestoreState(const State &state)
std::unordered_map< uint64_t, RuntimeContext > runtime_contexts_
absl::StatusOr< CustomObjectSlotBinding > ResolveSlotBinding(int object_id, int subtype) const
const std::string & GetBasePath() const
static const std::array< int, 3 > & RuntimeObjectIds()
static const std::vector< std::string > kSubtype54Filenames
void RemoveRuntimeContext(uint64_t context_id)
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
void SetObjectFileMap(const std::unordered_map< int, std::vector< std::string > > &map)
static int RuntimeSubtypeCountForObject(int object_id)
std::optional< uint64_t > active_runtime_context_id_
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< CustomObject > > LoadObject(const std::string &filename)
absl::StatusOr< std::shared_ptr< CustomObject > > GetObjectInternal(int object_id, int subtype)
static const std::vector< std::string > kSubtype2Filenames
void Initialize(const std::string &custom_objects_folder)
const std::vector< std::string > * ResolveFileList(int object_id) const
std::string ResolveFilename(int object_id, int subtype) const
static const std::vector< std::string > kSubtype1Filenames
void ActivateRuntimeContext(uint64_t context_id, const State &state)
std::vector< std::string > GetEffectiveFileList(int object_id) const
static ObjectGeometry & Get()
#define LOG_ERROR(category, format,...)
Definition log.h:110
#define LOG_INFO(category, format,...)
Definition log.h:106
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
bool PathStartsWith(const fs::path &path, const fs::path &root)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFileAtPath(const fs::path &path)
std::vector< CustomObject::TileMapEntry > VisibleRuntimeTiles(const CustomObject &object)
bool IsPortableAssetComponent(std::string_view component)
const core::SourceArtifactPublisherLabels kCustomObjectPublisherLabels
absl::StatusOr< CustomObject > DecodeCustomObjectBinary(const std::vector< uint8_t > &data)
absl::StatusOr< std::vector< uint8_t > > EncodeCustomObjectBinary(const CustomObject &object)
uint16_t CustomObjectRuntimeTileWord(int object_id, uint16_t source_word)
absl::StatusOr< fs::path > ResolveCustomObjectAssetPath(const std::string &custom_objects_folder, const std::string &filename)
absl::StatusOr< std::vector< uint8_t > > PublishCustomObjectBinary(const std::string &custom_objects_folder, const std::string &filename, const CustomObject &object, const std::vector< uint8_t > &expected_source_bytes, const fs::path &expected_resolved_path)
absl::StatusOr< CustomObjectAsset > LoadCustomObjectAsset(const std::string &custom_objects_folder, const std::string &filename)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::unordered_map< std::string, absl::StatusOr< std::shared_ptr< CustomObject > > > cache
std::unordered_map< int, std::vector< std::string > > custom_file_map
CustomObjectMappingOrigin origin
Represents a decoded custom object (from binary format)