yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_tile_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstring>
6#include <limits>
7#include <unordered_map>
8#include <unordered_set>
9
10#include "absl/strings/str_format.h"
12#include "core/features.h"
13#include "rom/transaction.h"
14#include "util/log.h"
15#include "util/macro.h"
20
21namespace yaze {
22namespace zelda3 {
23
24namespace {
25
26constexpr int kPaletteBankSize = 16;
27
29 gfx::SnesPalette padded;
30 for (size_t i = 0; i < static_cast<size_t>(kPaletteBankSize); ++i) {
31 if (i < source.size()) {
32 padded.AddColor(source[i]);
33 } else {
34 padded.AddColor(gfx::SnesColor());
35 }
36 }
37 return padded;
38}
39
41 gfx::SnesPalette combined;
42 for (int i = 0; i < palette.size(); ++i) {
43 const auto padded = BuildPaddedPaletteBank(palette.palette_ref(i));
44 for (size_t color = 0; color < padded.size(); ++color) {
45 combined.AddColor(padded[color]);
46 }
47 }
48 return combined;
49}
50
51int ResolvePaletteIndex(const gfx::PaletteGroup& palette, int requested) {
52 if (palette.empty()) {
53 return 0;
54 }
55 if (requested >= 0 && requested < static_cast<int>(palette.size())) {
56 return requested;
57 }
58 return 0;
59}
60
61constexpr std::array<int16_t, 2> kEditableStandardObjectIds = {0x11F, 0x120};
62constexpr size_t kFixed2x2SourceWordCount = 4;
63// The subtype tile-descriptor and routine-pointer tables are contiguous from
64// the first Type-1 descriptor through the two 128-word Type-3 tables. None of
65// this metadata is tile data, so an audited object source must never alias it.
67constexpr int64_t kObjectMetadataEnd = kRoomObjectSubtype3 + 0x200;
68
69bool RangesOverlap(int64_t lhs_start, int64_t lhs_end, int64_t rhs_start,
70 int64_t rhs_end) {
71 return lhs_start < rhs_end && rhs_start < lhs_end;
72}
73
74bool WordRangesOverlap(int64_t lhs_address, int64_t rhs_address) {
75 return RangesOverlap(lhs_address, lhs_address + 2, rhs_address,
76 rhs_address + 2);
77}
78
79absl::StatusOr<size_t> ResolveFixed2x2SourceWordIndex(
80 const ObjectTileLayout::Cell& cell) {
81 if (cell.rel_x < 0 || cell.rel_x > 1 || cell.rel_y < 0 || cell.rel_y > 1) {
82 return absl::FailedPreconditionError(
83 "Editable object cell is outside the audited 2x2 layout");
84 }
85
86 // RoomDraw_Single2x2 consumes its four source words in column-major order:
87 // visual grid [0 2; 1 3].
88 return static_cast<size_t>(cell.rel_x * 2 + cell.rel_y);
89}
90
91} // namespace
92
93// =============================================================================
94// ObjectTileLayout
95// =============================================================================
96
98 const std::vector<ObjectDrawer::TileTrace>& traces) {
99 ObjectTileLayout layout;
100 if (traces.empty()) {
101 return layout;
102 }
103
104 layout.object_id = static_cast<int16_t>(traces[0].object_id);
105
106 // Find bounding box
107 int min_x = traces[0].x_tile;
108 int min_y = traces[0].y_tile;
109 int max_x = min_x;
110 int max_y = min_y;
111 for (const auto& t : traces) {
112 min_x = std::min(min_x, static_cast<int>(t.x_tile));
113 min_y = std::min(min_y, static_cast<int>(t.y_tile));
114 max_x = std::max(max_x, static_cast<int>(t.x_tile));
115 max_y = std::max(max_y, static_cast<int>(t.y_tile));
116 }
117
118 layout.origin_tile_x = min_x;
119 layout.origin_tile_y = min_y;
120 layout.bounds_width = max_x - min_x + 1;
121 layout.bounds_height = max_y - min_y + 1;
122
123 std::unordered_map<uint32_t, size_t> last_trace_by_cell;
124 last_trace_by_cell.reserve(traces.size());
125 for (size_t i = 0; i < traces.size(); ++i) {
126 const auto& t = traces[i];
127 const uint32_t cell_key =
128 (static_cast<uint32_t>(static_cast<uint16_t>(t.x_tile)) << 16) |
129 static_cast<uint16_t>(t.y_tile);
130 last_trace_by_cell[cell_key] = i;
131 }
132
133 std::vector<size_t> surviving_trace_indices;
134 surviving_trace_indices.reserve(last_trace_by_cell.size());
135 for (const auto& [_, trace_index] : last_trace_by_cell) {
136 surviving_trace_indices.push_back(trace_index);
137 }
138 std::sort(surviving_trace_indices.begin(), surviving_trace_indices.end());
139
140 layout.cells.reserve(surviving_trace_indices.size());
141 for (size_t trace_index : surviving_trace_indices) {
142 const auto& t = traces[trace_index];
143 Cell cell;
144 cell.rel_x = t.x_tile - min_x;
145 cell.rel_y = t.y_tile - min_y;
146
147 // Reconstruct TileInfo from trace fields
148 bool h_mirror = (t.flags & 0x1) != 0;
149 bool v_mirror = (t.flags & 0x2) != 0;
150 bool priority = (t.flags & 0x4) != 0;
151 uint8_t palette = (t.flags >> 3) & 0x7;
152 cell.tile_info =
153 gfx::TileInfo(t.tile_id, palette, v_mirror, h_mirror, priority);
155 cell.write_index = static_cast<int>(trace_index);
156 cell.modified = false;
157
158 layout.cells.push_back(cell);
159 }
160
161 return layout;
162}
163
165 for (auto& cell : cells) {
166 if (cell.rel_x == rel_x && cell.rel_y == rel_y)
167 return &cell;
168 }
169 return nullptr;
170}
171
173 int rel_y) const {
174 for (const auto& cell : cells) {
175 if (cell.rel_x == rel_x && cell.rel_y == rel_y)
176 return &cell;
177 }
178 return nullptr;
179}
180
182 for (const auto& cell : cells) {
183 if (cell.modified)
184 return true;
185 }
186 return false;
187}
188
190 for (auto& cell : cells) {
191 if (cell.modified) {
192 cell.tile_info = gfx::WordToTileInfo(cell.original_word);
193 cell.modified = false;
194 }
195 }
196}
197
198// =============================================================================
199// ObjectTileEditor
200// =============================================================================
201
203
204absl::StatusOr<ObjectTileLayout> ObjectTileEditor::LoadCustomObjectLayout(
205 int16_t object_id, int subtype) {
206 const int runtime_count =
208 if (runtime_count == 0) {
209 return absl::UnimplementedError(
210 "Custom tile editing supports only registered fixed runtime objects");
211 }
212 if (subtype < 0 || subtype >= runtime_count) {
213 return absl::OutOfRangeError(
214 "Custom object subtype is outside the runtime dispatch table");
215 }
216
217 auto& manager = CustomObjectManager::Get();
218 const std::string filename = manager.ResolveFilename(object_id, subtype);
219 if (filename.empty()) {
220 return absl::NotFoundError(
221 "Custom object runtime slot has no mapped asset filename");
222 }
223 CustomObjectAsset asset;
224 ASSIGN_OR_RETURN(asset,
225 LoadCustomObjectAsset(manager.GetBasePath(), filename));
226
227 ObjectTileLayout layout;
228 layout.object_id = object_id;
229 layout.origin_tile_x = 0;
230 layout.origin_tile_y = 0;
231 layout.tile_data_address = -1;
232 layout.is_custom = true;
233 layout.custom_subtype = subtype;
234 layout.custom_filename = filename;
235 layout.custom_resolved_path = std::move(asset.resolved_path);
236 layout.custom_source_bytes = std::move(asset.source_bytes);
237
238 const auto bounds = asset.object.GetBoundingBox();
239 layout.bounds_width = bounds.max_x + 1;
240 layout.bounds_height = bounds.max_y + 1;
241 layout.cells.reserve(asset.object.tiles.size());
242 for (const auto& tile : asset.object.tiles) {
244 cell.rel_x = tile.rel_x;
245 cell.rel_y = tile.rel_y;
246 cell.tile_info = gfx::WordToTileInfo(tile.tile_data);
247 cell.original_word = tile.tile_data;
248 cell.write_index = static_cast<int>(layout.cells.size());
249 layout.cells.push_back(cell);
250 }
251 return layout;
252}
253
254absl::StatusOr<ObjectTileLayout> ObjectTileEditor::CaptureObjectLayout(
255 int16_t object_id, const Room& room, const gfx::PaletteGroup& palette) {
256 return CaptureObjectLayout(object_id, room, palette,
258}
259
260absl::StatusOr<ObjectTileLayout> ObjectTileEditor::CaptureObjectLayout(
261 int16_t object_id, const Room& room, const gfx::PaletteGroup& palette,
262 uint8_t object_size) {
263 if (!rom_ || !rom_->is_loaded()) {
264 return absl::FailedPreconditionError("ROM not loaded");
265 }
266
267 // Resolve the canvas anchor through ObjectGeometry so routines that
268 // draw upward or leftward (acute diagonals 0x09-0x14 / 0x15-0x20,
269 // diagonal ceilings 0xA0-0xAC) replay their full extent without
270 // clipping. Hardcoded (2, 2) previously
271 // dropped tile writes at negative tile coordinates because
272 // DrawRoutineUtils::WriteTile8 short-circuits via IsValidTilePosition
273 // before invoking the trace hook.
274 const uint8_t preview_size = CanonicalRoomObjectSize(object_id, object_size);
275 auto [anchor_x, anchor_y] =
276 ObjectGeometry::Get().ResolveAnchor(object_id, preview_size);
277 RoomObject obj(object_id, anchor_x, anchor_y, preview_size, 0);
278 obj.SetRom(rom_);
279 obj.EnsureTilesLoaded();
280
281 // Check if this is a custom object
282 bool is_custom = false;
283 std::string custom_filename;
284 int subtype = obj.size_ & 0x1F;
285 if (core::FeatureFlags::get().kEnableCustomObjects) {
286 auto custom_result =
287 CustomObjectManager::Get().GetObjectInternal(object_id, subtype);
288 if (custom_result.ok()) {
289 is_custom = true;
290 custom_filename =
291 CustomObjectManager::Get().ResolveFilename(object_id, subtype);
292 }
293 }
294
295 // Create drawer and set up trace collection
296 ObjectDrawer drawer(rom_, room.id(), room.get_gfx_buffer().data());
297 drawer.SetRoomFloorGraphics(room.floor1(), room.floor2());
298
299 std::vector<ObjectDrawer::TileTrace> traces;
300 traces.reserve(256);
301 drawer.SetTraceCollector(&traces, /*trace_only=*/true);
302
303 // Draw the object to collect traces
304 gfx::BackgroundBuffer dummy_bg1(512, 512);
305 gfx::BackgroundBuffer dummy_bg2(512, 512);
306 auto status = drawer.DrawObject(obj, dummy_bg1, dummy_bg2, palette);
307 drawer.ClearTraceCollector();
308
309 if (!status.ok()) {
310 return status;
311 }
312
313 if (traces.empty()) {
314 return absl::NotFoundError("Object produced no tile traces");
315 }
316
317 // Build layout from traces
319 // A draw trace proves visual placement, not ROM source ownership. Keep
320 // generic captures preview-only even when RoomObject happens to expose a
321 // legacy tile-data pointer.
322 layout.tile_data_address = -1;
323 layout.source_provenance.reset();
324 layout.is_custom = is_custom;
325 layout.custom_subtype = is_custom ? subtype : -1;
326 layout.custom_filename = custom_filename;
327
328 return layout;
329}
330
332 return std::find(kEditableStandardObjectIds.begin(),
333 kEditableStandardObjectIds.end(),
334 object_id) != kEditableStandardObjectIds.end();
335}
336
337absl::StatusOr<ObjectTileLayout> ObjectTileEditor::CaptureEditableObjectLayout(
338 int16_t object_id, const Room& room, const gfx::PaletteGroup& palette) {
339 if (!IsEditableStandardObject(object_id)) {
340 return absl::UnimplementedError(
341 "Standard object tile editing is not supported for this object");
342 }
343 if (rom_ == nullptr || !rom_->is_loaded()) {
344 return absl::FailedPreconditionError("ROM not loaded");
345 }
346
347 const uint32_t descriptor_pc_address =
348 static_cast<uint32_t>(kRoomObjectSubtype2 + (object_id - 0x100) * 2);
349 auto descriptor_or = rom_->ReadWord(static_cast<int>(descriptor_pc_address));
350 if (!descriptor_or.ok()) {
351 return descriptor_or.status();
352 }
353 const uint16_t descriptor_word = *descriptor_or;
354 const int64_t source_address =
355 static_cast<int64_t>(kRoomObjectTileAddress) + descriptor_word;
356 if (source_address < 0 ||
357 source_address + static_cast<int64_t>(kFixed2x2SourceWordCount * 2) >
358 static_cast<int64_t>(rom_->size()) ||
359 source_address > std::numeric_limits<int>::max()) {
360 return absl::OutOfRangeError(
361 "Editable object tile source is outside the loaded ROM");
362 }
363 const int64_t source_end =
364 source_address + static_cast<int64_t>(kFixed2x2SourceWordCount * 2);
365 if (RangesOverlap(source_address, source_end, kObjectMetadataStart,
366 kObjectMetadataEnd)) {
367 return absl::FailedPreconditionError(
368 "Editable object tile source overlaps object descriptor or routine "
369 "metadata");
370 }
371
372 auto layout_or = CaptureObjectLayout(object_id, room, palette);
373 if (!layout_or.ok()) {
374 return layout_or.status();
375 }
376 ObjectTileLayout layout = std::move(*layout_or);
377 if (layout.is_custom) {
378 return absl::FailedPreconditionError(
379 "Custom objects do not use standard ROM source provenance");
380 }
381 if (layout.object_id != object_id ||
382 layout.cells.size() != kFixed2x2SourceWordCount ||
383 layout.bounds_width != 2 || layout.bounds_height != 2) {
384 return absl::FailedPreconditionError(
385 "Object draw does not match the audited fixed 2x2 layout");
386 }
387
389 span.pc_address = static_cast<uint32_t>(source_address);
390 span.expected_words.reserve(kFixed2x2SourceWordCount);
391 for (size_t word_index = 0; word_index < kFixed2x2SourceWordCount;
392 ++word_index) {
393 auto word_or =
394 rom_->ReadWord(static_cast<int>(span.pc_address + word_index * 2));
395 if (!word_or.ok()) {
396 return word_or.status();
397 }
398 span.expected_words.push_back(*word_or);
399 }
400
401 std::array<bool, kFixed2x2SourceWordCount> mapped_words{};
402 for (auto& cell : layout.cells) {
403 auto source_word_index_or = ResolveFixed2x2SourceWordIndex(cell);
404 if (!source_word_index_or.ok()) {
405 return source_word_index_or.status();
406 }
407 const size_t source_word_index = *source_word_index_or;
408 if (mapped_words[source_word_index]) {
409 return absl::FailedPreconditionError(
410 "Editable object layout maps multiple cells to one source word");
411 }
412 if (cell.original_word != span.expected_words[source_word_index]) {
413 return absl::FailedPreconditionError(
414 "Draw trace does not match the resolved ROM source word");
415 }
416 mapped_words[source_word_index] = true;
417 cell.source_ref = ObjectTileSourceRef{/*span_index=*/0, source_word_index};
418 }
419 if (std::find(mapped_words.begin(), mapped_words.end(), false) !=
420 mapped_words.end()) {
421 return absl::FailedPreconditionError(
422 "Editable object layout does not cover every source word");
423 }
424
426 provenance.object_id = object_id;
427 provenance.descriptor_pc_address = descriptor_pc_address;
428 provenance.expected_descriptor_word = descriptor_word;
429 provenance.spans.push_back(std::move(span));
430 layout.tile_data_address = static_cast<int>(source_address);
431 layout.source_provenance = std::move(provenance);
432 return layout;
433}
434
436 const ObjectTileLayout& layout, gfx::Bitmap& bitmap,
437 const uint8_t* room_gfx_buffer, const gfx::PaletteGroup& palette) {
438 if (!room_gfx_buffer) {
439 return absl::FailedPreconditionError("No room graphics buffer");
440 }
441 if (layout.cells.empty()) {
442 return absl::OkStatus();
443 }
444
445 int bmp_w = layout.bounds_width * 8;
446 int bmp_h = layout.bounds_height * 8;
447
448 // Create or resize bitmap
449 std::vector<uint8_t> pixel_data(bmp_w * bmp_h, 255);
450 bitmap.Create(bmp_w, bmp_h, 8, pixel_data);
451
452 // Preview rendering uses tile palette bank offsets (pal * 16), so the bitmap
453 // needs a combined banked palette rather than a single sub-palette.
454 if (!palette.empty()) {
455 bitmap.SetPalette(BuildCombinedPaletteBanks(palette));
456 }
457 if (bitmap.surface()) {
458 SDL_SetColorKey(bitmap.surface(), SDL_TRUE, 255);
459 SDL_SetSurfaceBlendMode(bitmap.surface(), SDL_BLENDMODE_BLEND);
460 }
461
462 // Use a temporary ObjectDrawer just for its DrawTileToBitmap utility
463 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
464
465 for (const auto& cell : layout.cells) {
466 gfx::TileInfo preview_tile = cell.tile_info;
467 if (layout.is_custom) {
468 const uint16_t runtime_word = CustomObjectRuntimeTileWord(
469 layout.object_id, gfx::TileInfoToWord(cell.tile_info));
470 if (runtime_word == 0) {
471 continue;
472 }
473 preview_tile = gfx::WordToTileInfo(runtime_word);
474 }
475 int px = cell.rel_x * 8;
476 int py = cell.rel_y * 8;
477 drawer.DrawTileToBitmap(bitmap, preview_tile, px, py, room_gfx_buffer);
478 }
479
480 return absl::OkStatus();
481}
482
484 gfx::Bitmap& atlas, const uint8_t* room_gfx_buffer,
485 const gfx::PaletteGroup& palette, int display_palette,
486 std::optional<int16_t> custom_object_id, uint16_t retained_attributes) {
487 if (!room_gfx_buffer) {
488 return absl::FailedPreconditionError("No room graphics buffer");
489 }
490
491 std::vector<uint8_t> pixel_data(kAtlasWidthPx * kAtlasHeightPx,
492 custom_object_id.has_value() ? 255 : 0);
493 atlas.Create(kAtlasWidthPx, kAtlasHeightPx, 8, pixel_data);
494
495 const int resolved_palette = ResolvePaletteIndex(palette, display_palette);
496 if (!palette.empty()) {
497 atlas.SetPalette(
498 BuildPaddedPaletteBank(palette.palette_ref(resolved_palette)));
499 }
500 if (atlas.surface()) {
501 SDL_SetColorKey(atlas.surface(), SDL_TRUE, 255);
502 SDL_SetSurfaceBlendMode(atlas.surface(), SDL_BLENDMODE_BLEND);
503 }
504
505 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
506
507 for (int tile_id = 0; tile_id < kAtlasTileCount; ++tile_id) {
508 int col = tile_id % kAtlasTilesPerRow;
509 int row = tile_id / kAtlasTilesPerRow;
510 int px = col * 8;
511 int py = row * 8;
512
513 // The atlas bitmap already contains the selected palette bank, so tiles
514 // should draw into palette row 0 inside that local 16-color palette.
515 gfx::TileInfo info(static_cast<uint16_t>(tile_id), /*palette=*/0, false,
516 false, false);
517 if (custom_object_id.has_value()) {
518 // Atlas selection retains the selected cell's H/V/priority bits. Include
519 // them and the source palette in no-op detection, but do not flip the
520 // atlas image or apply its priority as a display effect.
521 const uint16_t source_word =
522 static_cast<uint16_t>(tile_id | ((display_palette & 7) << 10) |
523 (retained_attributes & 0xE000));
524 const uint16_t runtime_word =
525 CustomObjectRuntimeTileWord(*custom_object_id, source_word);
526 if (runtime_word == 0) {
527 continue;
528 }
529 info.id_ = runtime_word & 0x03FF;
530 }
531 drawer.DrawTileToBitmap(atlas, info, px, py, room_gfx_buffer);
532 }
533
534 return absl::OkStatus();
535}
536
538 if (!layout.HasModifications()) {
539 return absl::OkStatus();
540 }
541
542 if (layout.is_custom) {
543 if (layout.custom_filename.empty()) {
544 return absl::FailedPreconditionError(
545 "Custom object has no filename for write-back");
546 }
547 if (layout.custom_source_bytes.empty()) {
548 return absl::FailedPreconditionError(
549 "Custom object has no source snapshot for safe write-back");
550 }
551 const int runtime_count =
553 if (layout.custom_subtype < 0 || layout.custom_subtype >= runtime_count) {
554 return absl::FailedPreconditionError(
555 "Custom object layout no longer identifies a fixed runtime slot");
556 }
557
558 CustomObject object;
559 object.tiles.reserve(layout.cells.size());
560 for (const auto& cell : layout.cells) {
561 object.tiles.push_back(
562 {cell.rel_x, cell.rel_y, gfx::TileInfoToWord(cell.tile_info)});
563 }
564
565 auto& mgr = CustomObjectManager::Get();
566 if (mgr.ResolveFilename(layout.object_id, layout.custom_subtype) !=
567 layout.custom_filename) {
568 return absl::AbortedError(
569 "Custom object slot mapping changed after it was opened; edits were "
570 "kept");
571 }
572 auto published_or = PublishCustomObjectBinary(
573 mgr.GetBasePath(), layout.custom_filename, object,
575 if (!published_or.ok()) {
576 return published_or.status();
577 }
578
579 layout.custom_source_bytes = std::move(*published_or);
580 mgr.ReloadAll();
581 return absl::OkStatus();
582 }
583
584 auto plan_or = BuildStandardWritePlan(layout);
585 if (!plan_or.ok()) {
586 return plan_or.status();
587 }
588 return ApplyStandardWritePlan(*plan_or);
589}
590
591absl::StatusOr<ObjectTileWritePlan> ObjectTileEditor::BuildStandardWritePlan(
592 const ObjectTileLayout& layout) const {
593 if (layout.is_custom) {
594 return absl::InvalidArgumentError(
595 "Cannot build a standard ROM write plan for a custom object");
596 }
597
599 if (rom_ == nullptr || !rom_->is_loaded()) {
600 return absl::FailedPreconditionError("ROM not loaded");
601 }
602 if (!layout.source_provenance.has_value()) {
603 return absl::FailedPreconditionError(
604 "Standard object layout has no editable source provenance");
605 }
606 if (!IsEditableStandardObject(layout.object_id)) {
607 return absl::UnimplementedError(
608 "Standard object tile editing is not supported for this object");
609 }
610
611 const auto& provenance = *layout.source_provenance;
612 if (provenance.object_id != layout.object_id) {
613 return absl::FailedPreconditionError(
614 "Object tile source provenance does not match the layout object");
615 }
616 const uint32_t expected_descriptor_address = static_cast<uint32_t>(
617 kRoomObjectSubtype2 + (layout.object_id - 0x100) * 2);
618 if (provenance.descriptor_pc_address != expected_descriptor_address) {
619 return absl::FailedPreconditionError(
620 "Object tile source descriptor address does not match the object");
621 }
622 if (provenance.spans.size() != 1 ||
623 provenance.spans.front().expected_words.size() !=
624 kFixed2x2SourceWordCount) {
625 return absl::FailedPreconditionError(
626 "Object tile source provenance does not match the audited source map");
627 }
628
629 const int64_t rom_size = static_cast<int64_t>(rom_->size());
630 if (provenance.descriptor_pc_address >
631 static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
632 static_cast<int64_t>(provenance.descriptor_pc_address) + 2 > rom_size) {
633 return absl::OutOfRangeError(
634 "Object tile source descriptor is outside the loaded ROM");
635 }
636 auto current_descriptor_or =
637 rom_->ReadWord(static_cast<int>(provenance.descriptor_pc_address));
638 if (!current_descriptor_or.ok()) {
639 return current_descriptor_or.status();
640 }
641 if (*current_descriptor_or != provenance.expected_descriptor_word) {
642 return absl::FailedPreconditionError(
643 "Object tile source descriptor changed after capture");
644 }
645
646 const auto& span = provenance.spans.front();
647 const int64_t expected_span_address =
648 static_cast<int64_t>(kRoomObjectTileAddress) +
649 provenance.expected_descriptor_word;
650 if (span.pc_address != expected_span_address) {
651 return absl::FailedPreconditionError(
652 "Object tile source span does not match its descriptor");
653 }
654 if (span.pc_address >
655 static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
656 static_cast<int64_t>(span.pc_address) +
657 static_cast<int64_t>(span.expected_words.size() * 2) >
658 rom_size) {
659 return absl::OutOfRangeError(
660 "Object tile source span is outside the loaded ROM");
661 }
662
663 const int64_t span_start = static_cast<int64_t>(span.pc_address);
664 const int64_t span_end =
665 span_start + static_cast<int64_t>(span.expected_words.size() * 2);
666 if (RangesOverlap(span_start, span_end, kObjectMetadataStart,
667 kObjectMetadataEnd)) {
668 return absl::FailedPreconditionError(
669 "Object tile source span overlaps object descriptor or routine "
670 "metadata");
671 }
672
673 std::vector<int> precondition_addresses;
674 const int descriptor_address =
675 static_cast<int>(provenance.descriptor_pc_address);
676 precondition_addresses.push_back(descriptor_address);
677 plan.preconditions_.push_back(
678 {descriptor_address, provenance.expected_descriptor_word});
679 for (size_t word_index = 0; word_index < span.expected_words.size();
680 ++word_index) {
681 const int address = static_cast<int>(span.pc_address + word_index * 2);
682 if (std::any_of(precondition_addresses.begin(),
683 precondition_addresses.end(), [&](int existing_address) {
684 return WordRangesOverlap(existing_address, address);
685 })) {
686 return absl::FailedPreconditionError(
687 "Object tile descriptor and source preconditions overlap");
688 }
689 precondition_addresses.push_back(address);
690 auto current_word_or = rom_->ReadWord(address);
691 if (!current_word_or.ok()) {
692 return current_word_or.status();
693 }
694 if (*current_word_or != span.expected_words[word_index]) {
695 return absl::FailedPreconditionError(
696 "Object tile source word changed after capture");
697 }
698 plan.preconditions_.push_back({address, span.expected_words[word_index]});
699 }
700
701 if (layout.cells.size() != kFixed2x2SourceWordCount ||
702 layout.bounds_width != 2 || layout.bounds_height != 2) {
703 return absl::FailedPreconditionError(
704 "Object tile layout does not match the audited fixed 2x2 shape");
705 }
706
707 std::unordered_set<int> resolved_addresses;
708 for (const auto& cell : layout.cells) {
709 if (!cell.source_ref.has_value()) {
710 return absl::FailedPreconditionError(
711 "Object tile cell has no editable source reference");
712 }
713 const auto& source_ref = *cell.source_ref;
714 if (source_ref.span_index >= provenance.spans.size()) {
715 return absl::FailedPreconditionError(
716 "Object tile source span reference is out of bounds");
717 }
718 const auto& referenced_span = provenance.spans[source_ref.span_index];
719 if (source_ref.word_index >= referenced_span.expected_words.size()) {
720 return absl::FailedPreconditionError(
721 "Object tile source word reference is out of bounds");
722 }
723
724 auto expected_source_index_or = ResolveFixed2x2SourceWordIndex(cell);
725 if (!expected_source_index_or.ok()) {
726 return expected_source_index_or.status();
727 }
728 if (source_ref.span_index != 0 ||
729 source_ref.word_index != *expected_source_index_or) {
730 return absl::FailedPreconditionError(
731 "Object tile cell source reference does not match the audited map");
732 }
733
734 const int64_t address64 = static_cast<int64_t>(referenced_span.pc_address) +
735 static_cast<int64_t>(source_ref.word_index) * 2;
736 if (address64 < 0 || address64 + 2 > rom_size ||
737 address64 > std::numeric_limits<int>::max()) {
738 return absl::OutOfRangeError(
739 "Object tile write range is outside the loaded ROM");
740 }
741 const int address = static_cast<int>(address64);
742 if (!resolved_addresses.insert(address).second) {
743 return absl::FailedPreconditionError(
744 "Object tile cells resolve to a duplicate ROM address");
745 }
746
747 const uint16_t expected_word =
748 referenced_span.expected_words[source_ref.word_index];
749 if (cell.original_word != expected_word) {
750 return absl::FailedPreconditionError(
751 "Object tile cell original word does not match its source");
752 }
753 const uint16_t current_layout_word = gfx::TileInfoToWord(cell.tile_info);
754 if (!cell.modified && current_layout_word != cell.original_word) {
755 return absl::FailedPreconditionError(
756 "Object tile cell changed without being marked modified");
757 }
758 if (!cell.modified) {
759 continue;
760 }
761
762 plan.writes_.push_back({address, expected_word, current_layout_word});
763 plan.write_ranges_.emplace_back(static_cast<uint32_t>(address),
764 static_cast<uint32_t>(address + 2));
765 }
766
767 return plan;
768}
769
771 const ObjectTileWritePlan& plan) {
772 if (plan.writes_.empty()) {
773 return absl::OkStatus();
774 }
775 if (plan.write_ranges_.size() != plan.writes_.size()) {
776 return absl::FailedPreconditionError(
777 "Object tile write plan ranges do not match its writes");
778 }
779 if (plan.preconditions_.empty()) {
780 return absl::FailedPreconditionError(
781 "Object tile write plan has no source provenance preconditions");
782 }
783 if (rom_ == nullptr || !rom_->is_loaded()) {
784 return absl::FailedPreconditionError("ROM not loaded");
785 }
786
787 const int64_t rom_size = static_cast<int64_t>(rom_->size());
788 std::unordered_map<int, uint16_t> precondition_words;
789 std::vector<int> precondition_addresses;
790 for (const auto& precondition : plan.preconditions_) {
791 const int64_t address = static_cast<int64_t>(precondition.address);
792 if (address < 0 || address + 2 > rom_size) {
793 return absl::OutOfRangeError(
794 "Object tile precondition is outside the loaded ROM");
795 }
796 if (std::any_of(precondition_addresses.begin(),
797 precondition_addresses.end(), [&](int existing_address) {
798 return WordRangesOverlap(existing_address,
799 precondition.address);
800 })) {
801 return absl::FailedPreconditionError(
802 "Object tile plan has overlapping CAS preconditions");
803 }
804 precondition_addresses.push_back(precondition.address);
805 if (!precondition_words
806 .emplace(precondition.address, precondition.expected_word)
807 .second) {
808 return absl::FailedPreconditionError(
809 "Object tile plan has duplicate CAS preconditions");
810 }
811 }
812
813 std::unordered_set<int> write_addresses;
814 std::vector<int> validated_write_addresses;
815 for (size_t write_index = 0; write_index < plan.writes_.size();
816 ++write_index) {
817 const auto& write = plan.writes_[write_index];
818 const int64_t address = static_cast<int64_t>(write.address);
819 if (address < 0 || address + 2 > rom_size) {
820 return absl::OutOfRangeError(
821 "Object tile write range is outside the loaded ROM");
822 }
823 if (std::any_of(validated_write_addresses.begin(),
824 validated_write_addresses.end(), [&](int existing_address) {
825 return WordRangesOverlap(existing_address, write.address);
826 })) {
827 return absl::FailedPreconditionError(
828 "Object tile plan has overlapping write ranges");
829 }
830 validated_write_addresses.push_back(write.address);
831 if (!write_addresses.insert(write.address).second) {
832 return absl::FailedPreconditionError(
833 "Object tile plan has duplicate write addresses");
834 }
835 const std::pair<uint32_t, uint32_t> expected_range = {
836 static_cast<uint32_t>(write.address),
837 static_cast<uint32_t>(write.address + 2)};
838 if (plan.write_ranges_[write_index] != expected_range) {
839 return absl::FailedPreconditionError(
840 "Object tile write plan range does not match its write address");
841 }
842 const auto precondition = precondition_words.find(write.address);
843 if (precondition == precondition_words.end() ||
844 precondition->second != write.expected_word) {
845 return absl::FailedPreconditionError(
846 "Object tile write has no matching source precondition");
847 }
848 }
849
850 // Recheck every captured descriptor/source word immediately before the
851 // transaction. This is the compare-and-swap boundary for a prepared plan.
852 for (const auto& precondition : plan.preconditions_) {
853 auto current_word_or = rom_->ReadWord(precondition.address);
854 if (!current_word_or.ok()) {
855 return current_word_or.status();
856 }
857 if (*current_word_or != precondition.expected_word) {
858 return absl::FailedPreconditionError(
859 "Object tile source changed after the write plan was built");
860 }
861 }
862 for (const auto& write : plan.writes_) {
863 auto current_word_or = rom_->ReadWord(write.address);
864 if (!current_word_or.ok()) {
865 return current_word_or.status();
866 }
867 if (*current_word_or != write.expected_word) {
868 return absl::FailedPreconditionError(
869 "Object tile write target changed after the plan was built");
870 }
871 }
872
873 const bool was_dirty = rom_->dirty();
874 Transaction transaction(*rom_);
875 for (const auto& write : plan.writes_) {
876 transaction.WriteWord(write.address, write.word);
877 }
878 const absl::Status status = transaction.Commit();
879 if (!status.ok()) {
880 rom_->set_dirty(was_dirty);
881 return status;
882 }
884 return absl::OkStatus();
885}
886
887absl::StatusOr<ObjectTileSourceImpact>
889 const ObjectTileLayout& layout) const {
890 if (rom_ == nullptr || !rom_->is_loaded()) {
891 return absl::FailedPreconditionError("ROM not loaded");
892 }
893 if (layout.is_custom) {
894 return absl::InvalidArgumentError(
895 "Custom objects do not use standard ROM tile sources");
896 }
897 if (!layout.source_provenance.has_value()) {
898 return absl::FailedPreconditionError(
899 "Standard object layout has no editable source provenance");
900 }
901
902 // Reuse the write-plan validator as the provenance/CAS gate. This validates
903 // the audited object ID, descriptor, complete source snapshot, and cell map
904 // without mutating the ROM, regardless of whether the layout has edits.
905 const auto plan_or = BuildStandardWritePlan(layout);
906 if (!plan_or.ok()) {
907 return plan_or.status();
908 }
909
910 std::vector<ObjectTileReadRange> target_ranges;
911 for (const auto& span : layout.source_provenance->spans) {
912 if (span.expected_words.empty()) {
913 return absl::FailedPreconditionError(
914 "Editable object tile source span is empty");
915 }
916 const uint64_t end = static_cast<uint64_t>(span.pc_address) +
917 static_cast<uint64_t>(span.expected_words.size()) * 2;
918 if (end > rom_->size() || end > std::numeric_limits<uint32_t>::max()) {
919 return absl::OutOfRangeError(
920 "Editable object tile source span is outside the loaded ROM");
921 }
922 target_ranges.push_back({span.pc_address, static_cast<uint32_t>(end)});
923 }
924
925 const auto overlaps_target =
926 [&target_ranges](const ObjectTileReadRange& candidate) {
927 return std::any_of(target_ranges.begin(), target_ranges.end(),
928 [&candidate](const ObjectTileReadRange& target) {
929 return candidate.begin < target.end &&
930 target.begin < candidate.end;
931 });
932 };
933
934 ObjectParser parser(rom_);
936 const auto inspect_object = [&](int object_id) -> absl::Status {
937 auto ranges_or =
938 parser.ResolveTileReadRanges(static_cast<int16_t>(object_id));
939 if (!ranges_or.ok()) {
940 return absl::FailedPreconditionError(absl::StrFormat(
941 "Unable to resolve tile sources for object 0x%03X: %s", object_id,
942 ranges_or.status().message()));
943 }
944
946 entry.object_id = static_cast<int16_t>(object_id);
947 for (const auto& range : *ranges_or) {
948 if (overlaps_target(range)) {
949 entry.overlapping_ranges.push_back(range);
950 }
951 }
952 if (!entry.overlapping_ranges.empty()) {
953 impact.affected_objects.push_back(std::move(entry));
954 }
955 return absl::OkStatus();
956 };
957
958 // Only IDs representable in the dungeon object stream are relevant. Keep the
959 // three families explicit so invalid aliases (Type 1 0xF8..0xFF and Type 2
960 // 0x140..0x1FF) cannot silently inflate or hide the impact set.
961 for (int object_id = 0x000; object_id <= 0x0F7; ++object_id) {
962 const absl::Status status = inspect_object(object_id);
963 if (!status.ok()) {
964 return status;
965 }
966 }
967 for (int object_id = 0x100; object_id <= 0x13F; ++object_id) {
968 const absl::Status status = inspect_object(object_id);
969 if (!status.ok()) {
970 return status;
971 }
972 }
973 for (int object_id = 0xF80; object_id <= 0xFFF; ++object_id) {
974 const absl::Status status = inspect_object(object_id);
975 if (!status.ok()) {
976 return status;
977 }
978 }
979
980 // Room::RenderObjectsToBackground synthesizes lightable torches as global
981 // object 0x150, outside the persisted stream-ID families above. Both room
982 // rendering and the in-game lighting-change path read these literal blocks.
983 // In particular, the lit block aliases editable object 0x120 exactly.
984 constexpr int kLightableTorchTileCount = 4;
985 constexpr ObjectTileReadRange kLightableTorchSources[] = {
986 {kRoomObjectTileAddress + 0x0EC2,
987 kRoomObjectTileAddress + 0x0EC2 + kLightableTorchTileCount * 2},
988 {kRoomObjectTileAddress + 0x0ECA,
989 kRoomObjectTileAddress + 0x0ECA + kLightableTorchTileCount * 2},
990 };
991 for (const auto consumer :
994 ObjectTileRuntimeConsumerImpactEntry torch_impact{consumer, {}};
995 for (const auto& range : kLightableTorchSources) {
996 if (overlaps_target(range)) {
997 torch_impact.overlapping_ranges.push_back(range);
998 }
999 }
1000 if (!torch_impact.overlapping_ranges.empty()) {
1001 impact.runtime_consumers.push_back(std::move(torch_impact));
1002 }
1003 }
1004
1005 const bool includes_edited_object = std::any_of(
1006 impact.affected_objects.begin(), impact.affected_objects.end(),
1007 [&](const auto& entry) { return entry.object_id == layout.object_id; });
1008 if (!includes_edited_object) {
1009 return absl::FailedPreconditionError(
1010 "Edited object is missing from its tile source impact inventory");
1011 }
1012
1013 return impact;
1014}
1015
1016} // namespace zelda3
1017} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
void set_dirty(bool dirty)
Definition rom.h:157
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:571
auto size() const
Definition rom.h:168
bool dirty() const
Definition rom.h:156
void AdvanceObjectTileRevision()
Definition rom.h:165
bool is_loaded() const
Definition rom.h:155
Transaction & WriteWord(int address, uint16_t value)
Definition transaction.h:43
absl::Status Commit()
static Flags & get()
Definition features.h:119
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:202
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
SDL_Surface * surface() const
Definition bitmap.h:402
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static int RuntimeSubtypeCountForObject(int object_id)
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< CustomObject > > GetObjectInternal(int object_id, int subtype)
std::string ResolveFilename(int object_id, int subtype) const
Draws dungeon objects to background buffers using game patterns.
void DrawTileToBitmap(gfx::Bitmap &bitmap, const gfx::TileInfo &tile_info, int pixel_x, int pixel_y, const uint8_t *tiledata)
Draw a single tile directly to bitmap.
void SetRoomFloorGraphics(uint8_t floor1, uint8_t floor2)
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a room object to background buffers.
void SetTraceCollector(std::vector< TileTrace > *collector, bool trace_only=false)
std::pair< int, int > ResolveAnchor(int16_t object_id, uint8_t size_byte) const
Resolve the canvas anchor (x, y) for a given object's draw routine.
static ObjectGeometry & Get()
Direct ROM parser for dungeon objects.
absl::StatusOr< std::vector< ObjectTileReadRange > > ResolveTileReadRanges(int16_t object_id)
Resolve every tile-data range consumed by ParseObject.
absl::StatusOr< ObjectTileSourceImpact > AnalyzeStandardTileSourceImpact(const ObjectTileLayout &layout) const
absl::Status ApplyStandardWritePlan(const ObjectTileWritePlan &plan)
static constexpr int kAtlasTilesPerRow
absl::Status RenderLayoutToBitmap(const ObjectTileLayout &layout, gfx::Bitmap &bitmap, const uint8_t *room_gfx_buffer, const gfx::PaletteGroup &palette)
static bool IsEditableStandardObject(int16_t object_id)
absl::Status BuildTile8Atlas(gfx::Bitmap &atlas, const uint8_t *room_gfx_buffer, const gfx::PaletteGroup &palette, int display_palette=2, std::optional< int16_t > custom_object_id=std::nullopt, uint16_t retained_attributes=0)
absl::StatusOr< ObjectTileLayout > CaptureObjectLayout(int16_t object_id, const Room &room, const gfx::PaletteGroup &palette)
absl::Status WriteBack(ObjectTileLayout &layout)
absl::StatusOr< ObjectTileLayout > CaptureEditableObjectLayout(int16_t object_id, const Room &room, const gfx::PaletteGroup &palette)
absl::StatusOr< ObjectTileWritePlan > BuildStandardWritePlan(const ObjectTileLayout &layout) const
absl::StatusOr< ObjectTileLayout > LoadCustomObjectLayout(int16_t object_id, int subtype)
Fully resolved standard-object ROM writes.
std::vector< std::pair< uint32_t, uint32_t > > write_ranges_
std::vector< WordPrecondition > preconditions_
void SetRom(Rom *rom)
Definition room_object.h:78
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:1019
uint8_t floor2() const
Definition room.h:976
uint8_t floor1() const
Definition room.h:975
int id() const
Definition room.h:948
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
gfx::SnesPalette BuildCombinedPaletteBanks(const gfx::PaletteGroup &palette)
bool WordRangesOverlap(int64_t lhs_address, int64_t rhs_address)
absl::StatusOr< size_t > ResolveFixed2x2SourceWordIndex(const ObjectTileLayout::Cell &cell)
gfx::SnesPalette BuildPaddedPaletteBank(const gfx::SnesPalette &source)
int ResolvePaletteIndex(const gfx::PaletteGroup &palette, int requested)
bool RangesOverlap(int64_t lhs_start, int64_t lhs_end, int64_t rhs_start, int64_t rhs_end)
constexpr std::array< int16_t, 2 > kEditableStandardObjectIds
uint8_t DefaultRoomObjectSizeForPlacement(int object_id)
constexpr int kRoomObjectSubtype3
Definition room_object.h:47
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
constexpr int kRoomObjectSubtype1
Definition room_object.h:45
constexpr int kRoomObjectSubtype2
Definition room_object.h:46
uint16_t CustomObjectRuntimeTileWord(int object_id, uint16_t source_word)
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
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)
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
std::filesystem::path resolved_path
std::vector< uint8_t > source_bytes
Represents a decoded custom object (from binary format)
BoundingBox GetBoundingBox() const
std::vector< TileMapEntry > tiles
Editable tile8 layout captured from an object's draw trace.
std::vector< uint8_t > custom_source_bytes
std::optional< ObjectTileSourceProvenance > source_provenance
std::filesystem::path custom_resolved_path
Cell * FindCell(int rel_x, int rel_y)
static ObjectTileLayout FromTraces(const std::vector< ObjectDrawer::TileTrace > &traces)
One half-open PC range consumed while parsing an object's tile data.
std::vector< ObjectTileReadRange > overlapping_ranges
One standard object whose parser-visible tile sources overlap an editable layout's authorized source ...
std::vector< ObjectTileReadRange > overlapping_ranges
Fail-closed impact inventory within ObjectParser's source model plus explicitly modeled runtime consu...
std::vector< ObjectTileRuntimeConsumerImpactEntry > runtime_consumers
std::vector< ObjectTileSourceImpactEntry > affected_objects
ROM descriptor and source snapshot authorizing an editable capture.
std::vector< ObjectTileSourceSpan > spans
A cell's exact word within an authorized source span.
One contiguous ROM-backed source span for an object's tile words.
std::vector< uint16_t > expected_words