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