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