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 <cstring>
5#include <filesystem>
6#include <fstream>
7#include <unordered_map>
8
10#include "core/features.h"
11#include "util/log.h"
16
17namespace yaze {
18namespace zelda3 {
19
20namespace {
21
22constexpr int kPaletteBankSize = 16;
23
25 gfx::SnesPalette padded;
26 for (size_t i = 0; i < static_cast<size_t>(kPaletteBankSize); ++i) {
27 if (i < source.size()) {
28 padded.AddColor(source[i]);
29 } else {
30 padded.AddColor(gfx::SnesColor());
31 }
32 }
33 return padded;
34}
35
37 gfx::SnesPalette combined;
38 for (int i = 0; i < palette.size(); ++i) {
39 const auto padded = BuildPaddedPaletteBank(palette.palette_ref(i));
40 for (size_t color = 0; color < padded.size(); ++color) {
41 combined.AddColor(padded[color]);
42 }
43 }
44 return combined;
45}
46
47int ResolvePaletteIndex(const gfx::PaletteGroup& palette, int requested) {
48 if (palette.empty()) {
49 return 0;
50 }
51 if (requested >= 0 && requested < static_cast<int>(palette.size())) {
52 return requested;
53 }
54 return 0;
55}
56
57} // namespace
58
59// =============================================================================
60// ObjectTileLayout
61// =============================================================================
62
64 const std::vector<ObjectDrawer::TileTrace>& traces) {
65 ObjectTileLayout layout;
66 if (traces.empty()) {
67 return layout;
68 }
69
70 layout.object_id = static_cast<int16_t>(traces[0].object_id);
71
72 // Find bounding box
73 int min_x = traces[0].x_tile;
74 int min_y = traces[0].y_tile;
75 int max_x = min_x;
76 int max_y = min_y;
77 for (const auto& t : traces) {
78 min_x = std::min(min_x, static_cast<int>(t.x_tile));
79 min_y = std::min(min_y, static_cast<int>(t.y_tile));
80 max_x = std::max(max_x, static_cast<int>(t.x_tile));
81 max_y = std::max(max_y, static_cast<int>(t.y_tile));
82 }
83
84 layout.origin_tile_x = min_x;
85 layout.origin_tile_y = min_y;
86 layout.bounds_width = max_x - min_x + 1;
87 layout.bounds_height = max_y - min_y + 1;
88
89 std::unordered_map<uint32_t, size_t> last_trace_by_cell;
90 last_trace_by_cell.reserve(traces.size());
91 for (size_t i = 0; i < traces.size(); ++i) {
92 const auto& t = traces[i];
93 const uint32_t cell_key =
94 (static_cast<uint32_t>(static_cast<uint16_t>(t.x_tile)) << 16) |
95 static_cast<uint16_t>(t.y_tile);
96 last_trace_by_cell[cell_key] = i;
97 }
98
99 std::vector<size_t> surviving_trace_indices;
100 surviving_trace_indices.reserve(last_trace_by_cell.size());
101 for (const auto& [_, trace_index] : last_trace_by_cell) {
102 surviving_trace_indices.push_back(trace_index);
103 }
104 std::sort(surviving_trace_indices.begin(), surviving_trace_indices.end());
105
106 layout.cells.reserve(surviving_trace_indices.size());
107 for (size_t trace_index : surviving_trace_indices) {
108 const auto& t = traces[trace_index];
109 Cell cell;
110 cell.rel_x = t.x_tile - min_x;
111 cell.rel_y = t.y_tile - min_y;
112
113 // Reconstruct TileInfo from trace fields
114 bool h_mirror = (t.flags & 0x1) != 0;
115 bool v_mirror = (t.flags & 0x2) != 0;
116 bool priority = (t.flags & 0x4) != 0;
117 uint8_t palette = (t.flags >> 3) & 0x7;
118 cell.tile_info =
119 gfx::TileInfo(t.tile_id, palette, v_mirror, h_mirror, priority);
121 cell.write_index = static_cast<int>(trace_index);
122 cell.modified = false;
123
124 layout.cells.push_back(cell);
125 }
126
127 return layout;
128}
129
131 int16_t object_id,
132 const std::string& filename) {
133 ObjectTileLayout layout;
134 layout.object_id = object_id;
135 layout.origin_tile_x = 0;
136 layout.origin_tile_y = 0;
137 layout.bounds_width = width;
138 layout.bounds_height = height;
139 layout.tile_data_address = -1;
140 layout.is_custom = true;
141 layout.custom_filename = filename;
142
143 layout.cells.reserve(width * height);
144 for (int y = 0; y < height; ++y) {
145 for (int x = 0; x < width; ++x) {
146 Cell cell;
147 cell.rel_x = x;
148 cell.rel_y = y;
149 cell.tile_info = gfx::TileInfo(0, 2, false, false, false);
151 cell.write_index = static_cast<int>(layout.cells.size());
152 cell.modified = true;
153 layout.cells.push_back(cell);
154 }
155 }
156
157 return layout;
158}
159
161 for (auto& cell : cells) {
162 if (cell.rel_x == rel_x && cell.rel_y == rel_y)
163 return &cell;
164 }
165 return nullptr;
166}
167
169 int rel_y) const {
170 for (const auto& cell : cells) {
171 if (cell.rel_x == rel_x && cell.rel_y == rel_y)
172 return &cell;
173 }
174 return nullptr;
175}
176
178 for (const auto& cell : cells) {
179 if (cell.modified)
180 return true;
181 }
182 return false;
183}
184
186 for (auto& cell : cells) {
187 if (cell.modified) {
188 cell.tile_info = gfx::WordToTileInfo(cell.original_word);
189 cell.modified = false;
190 }
191 }
192}
193
194// =============================================================================
195// ObjectTileEditor
196// =============================================================================
197
199
200absl::StatusOr<ObjectTileLayout> ObjectTileEditor::CaptureObjectLayout(
201 int16_t object_id, const Room& room, const gfx::PaletteGroup& palette) {
202 if (!rom_ || !rom_->is_loaded()) {
203 return absl::FailedPreconditionError("ROM not loaded");
204 }
205
206 // Resolve the canvas anchor through ObjectGeometry so routines that
207 // draw upward or leftward (acute diagonals 0x09-0x14 / 0x15-0x20,
208 // diagonal ceilings 0xA0-0xAC) replay their full extent without
209 // clipping. Hardcoded (2, 2) previously
210 // dropped tile writes at negative tile coordinates because
211 // DrawRoutineUtils::WriteTile8 short-circuits via IsValidTilePosition
212 // before invoking the trace hook.
213 constexpr uint8_t kPreviewSizeByte = 0x12;
214 auto [anchor_x, anchor_y] =
215 ObjectGeometry::Get().ResolveAnchor(object_id, kPreviewSizeByte);
216 RoomObject obj(object_id, anchor_x, anchor_y, kPreviewSizeByte, 0);
217 obj.SetRom(rom_);
218 obj.EnsureTilesLoaded();
219
220 // Check if this is a custom object
221 bool is_custom = false;
222 std::string custom_filename;
223 int subtype = obj.size_ & 0x1F;
224 if (core::FeatureFlags::get().kEnableCustomObjects) {
225 auto custom_result =
226 CustomObjectManager::Get().GetObjectInternal(object_id, subtype);
227 if (custom_result.ok()) {
228 is_custom = true;
229 custom_filename =
230 CustomObjectManager::Get().ResolveFilename(object_id, subtype);
231 }
232 }
233
234 // Create drawer and set up trace collection
235 ObjectDrawer drawer(rom_, room.id(), room.get_gfx_buffer().data());
236
237 std::vector<ObjectDrawer::TileTrace> traces;
238 traces.reserve(256);
239 drawer.SetTraceCollector(&traces, /*trace_only=*/true);
240
241 // Draw the object to collect traces
242 gfx::BackgroundBuffer dummy_bg1(512, 512);
243 gfx::BackgroundBuffer dummy_bg2(512, 512);
244 auto status = drawer.DrawObject(obj, dummy_bg1, dummy_bg2, palette);
245 drawer.ClearTraceCollector();
246
247 if (!status.ok()) {
248 return status;
249 }
250
251 if (traces.empty()) {
252 return absl::NotFoundError("Object produced no tile traces");
253 }
254
255 // Build layout from traces
258 layout.is_custom = is_custom;
259 layout.custom_filename = custom_filename;
260
261 return layout;
262}
263
265 const ObjectTileLayout& layout, gfx::Bitmap& bitmap,
266 const uint8_t* room_gfx_buffer, const gfx::PaletteGroup& palette) {
267 if (!room_gfx_buffer) {
268 return absl::FailedPreconditionError("No room graphics buffer");
269 }
270 if (layout.cells.empty()) {
271 return absl::OkStatus();
272 }
273
274 int bmp_w = layout.bounds_width * 8;
275 int bmp_h = layout.bounds_height * 8;
276
277 // Create or resize bitmap
278 std::vector<uint8_t> pixel_data(bmp_w * bmp_h, 0);
279 bitmap.Create(bmp_w, bmp_h, 8, pixel_data);
280
281 // Preview rendering uses tile palette bank offsets (pal * 16), so the bitmap
282 // needs a combined banked palette rather than a single sub-palette.
283 if (!palette.empty()) {
284 bitmap.SetPalette(BuildCombinedPaletteBanks(palette));
285 }
286
287 // Use a temporary ObjectDrawer just for its DrawTileToBitmap utility
288 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
289
290 for (const auto& cell : layout.cells) {
291 int px = cell.rel_x * 8;
292 int py = cell.rel_y * 8;
293 drawer.DrawTileToBitmap(bitmap, cell.tile_info, px, py, room_gfx_buffer);
294 }
295
296 return absl::OkStatus();
297}
298
300 const uint8_t* room_gfx_buffer,
301 const gfx::PaletteGroup& palette,
302 int display_palette) {
303 if (!room_gfx_buffer) {
304 return absl::FailedPreconditionError("No room graphics buffer");
305 }
306
307 std::vector<uint8_t> pixel_data(kAtlasWidthPx * kAtlasHeightPx, 0);
308 atlas.Create(kAtlasWidthPx, kAtlasHeightPx, 8, pixel_data);
309
310 const int resolved_palette = ResolvePaletteIndex(palette, display_palette);
311 if (!palette.empty()) {
312 atlas.SetPalette(
313 BuildPaddedPaletteBank(palette.palette_ref(resolved_palette)));
314 }
315
316 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
317
318 for (int tile_id = 0; tile_id < kAtlasTileCount; ++tile_id) {
319 int col = tile_id % kAtlasTilesPerRow;
320 int row = tile_id / kAtlasTilesPerRow;
321 int px = col * 8;
322 int py = row * 8;
323
324 // The atlas bitmap already contains the selected palette bank, so tiles
325 // should draw into palette row 0 inside that local 16-color palette.
326 gfx::TileInfo info(static_cast<uint16_t>(tile_id), /*palette=*/0, false,
327 false, false);
328 drawer.DrawTileToBitmap(atlas, info, px, py, room_gfx_buffer);
329 }
330
331 return absl::OkStatus();
332}
333
335 if (!layout.HasModifications()) {
336 return absl::OkStatus();
337 }
338
339 if (layout.is_custom) {
340 // Custom object: serialize to binary format and write .bin file
341 if (layout.custom_filename.empty()) {
342 return absl::FailedPreconditionError(
343 "Custom object has no filename for write-back");
344 }
345
346 // Group cells by rel_y to form segments
347 std::map<int, std::vector<const ObjectTileLayout::Cell*>> rows;
348 for (const auto& cell : layout.cells) {
349 rows[cell.rel_y].push_back(&cell);
350 }
351
352 // Serialize to binary matching CustomObjectManager::ParseBinaryData format
353 std::vector<uint8_t> binary;
354 constexpr int kBufferStride = 128;
355
356 int prev_buffer_pos = 0;
357 for (auto& [row_y, row_cells] : rows) {
358 // Sort cells by rel_x
359 std::sort(
360 row_cells.begin(), row_cells.end(),
361 [](const auto* a, const auto* b) { return a->rel_x < b->rel_x; });
362
363 int count = static_cast<int>(row_cells.size());
364 int buffer_pos_for_row = row_y * kBufferStride + row_cells[0]->rel_x * 2;
365 int jump_offset = (row_y == rows.rbegin()->first)
366 ? 0
367 : kBufferStride; // Jump to next row
368
369 // Header: low 5 bits = count, high byte = jump_offset
370 uint16_t header = (count & 0x1F) | ((jump_offset & 0xFF) << 8);
371 binary.push_back(header & 0xFF);
372 binary.push_back((header >> 8) & 0xFF);
373
374 for (const auto* cell : row_cells) {
375 uint16_t word = gfx::TileInfoToWord(cell->tile_info);
376 binary.push_back(word & 0xFF);
377 binary.push_back((word >> 8) & 0xFF);
378 }
379
380 prev_buffer_pos = buffer_pos_for_row + count * 2;
381 }
382
383 // Terminator
384 binary.push_back(0);
385 binary.push_back(0);
386
387 // Write to file
388 auto& mgr = CustomObjectManager::Get();
389 std::filesystem::path full_path =
390 std::filesystem::path(mgr.GetBasePath()) / layout.custom_filename;
391 std::ofstream file(full_path, std::ios::binary);
392 if (!file) {
393 return absl::InternalError("Failed to open file for writing: " +
394 full_path.string());
395 }
396 file.write(reinterpret_cast<const char*>(binary.data()), binary.size());
397 file.close();
398
399 // Reload cache
400 mgr.ReloadAll();
401 return absl::OkStatus();
402 }
403
404 // Standard object: patch ROM at tile_data_address
405 if (layout.tile_data_address < 0) {
406 return absl::FailedPreconditionError(
407 "No tile data address for ROM write-back");
408 }
409
410 for (size_t i = 0; i < layout.cells.size(); ++i) {
411 const auto& cell = layout.cells[i];
412 if (!cell.modified)
413 continue;
414
415 const int write_index =
416 cell.write_index >= 0 ? cell.write_index : static_cast<int>(i);
417 int addr = layout.tile_data_address + write_index * 2;
418 uint16_t word = gfx::TileInfoToWord(cell.tile_info);
419
420 // Write 2 bytes (little-endian SNES tilemap word)
421 auto low = static_cast<uint8_t>(word & 0xFF);
422 auto high = static_cast<uint8_t>((word >> 8) & 0xFF);
423 RETURN_IF_ERROR(rom_->WriteByte(addr, low));
424 RETURN_IF_ERROR(rom_->WriteByte(addr + 1, high));
425 }
426
427 return absl::OkStatus();
428}
429
431 if (!rom_ || !rom_->is_loaded())
432 return 0;
433
434 // Create temporary object to get its tile_data_ptr_
435 RoomObject test_obj(object_id, 0, 0, 0, 0);
436 test_obj.SetRom(rom_);
437 test_obj.EnsureTilesLoaded();
438 int target_ptr = test_obj.tile_data_ptr_;
439 if (target_ptr < 0)
440 return 0;
441
442 // Scan type 1 objects (0x00-0xFF) for shared pointers
443 int count = 0;
444 for (int id = 0; id < 0x100; ++id) {
445 RoomObject scan_obj(static_cast<int16_t>(id), 0, 0, 0, 0);
446 scan_obj.SetRom(rom_);
447 scan_obj.EnsureTilesLoaded();
448 if (scan_obj.tile_data_ptr_ == target_ptr) {
449 ++count;
450 }
451 }
452
453 return count;
454}
455
456} // namespace zelda3
457} // 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
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:586
bool is_loaded() const
Definition rom.h:144
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:201
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
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()
absl::Status WriteBack(const ObjectTileLayout &layout)
static constexpr int kAtlasTilesPerRow
absl::Status RenderLayoutToBitmap(const ObjectTileLayout &layout, gfx::Bitmap &bitmap, const uint8_t *room_gfx_buffer, const gfx::PaletteGroup &palette)
int CountObjectsSharingTileData(int16_t object_id) const
absl::StatusOr< ObjectTileLayout > CaptureObjectLayout(int16_t object_id, const Room &room, const gfx::PaletteGroup &palette)
static constexpr int kAtlasTileCount
static constexpr int kAtlasHeightPx
absl::Status BuildTile8Atlas(gfx::Bitmap &atlas, const uint8_t *room_gfx_buffer, const gfx::PaletteGroup &palette, int display_palette=2)
void SetRom(Rom *rom)
Definition room_object.h:79
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:970
int id() const
Definition room.h:899
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)
gfx::SnesPalette BuildPaddedPaletteBank(const gfx::SnesPalette &source)
int ResolvePaletteIndex(const gfx::PaletteGroup &palette, int requested)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
Editable tile8 layout captured from an object's draw trace.
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)