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 <fstream>
6#include <filesystem>
7
9#include "core/features.h"
10#include "util/log.h"
14
15namespace yaze {
16namespace zelda3 {
17
18// =============================================================================
19// ObjectTileLayout
20// =============================================================================
21
23 const std::vector<ObjectDrawer::TileTrace>& traces) {
24 ObjectTileLayout layout;
25 if (traces.empty()) return layout;
26
27 layout.object_id = static_cast<int16_t>(traces[0].object_id);
28
29 // Find bounding box
30 int min_x = traces[0].x_tile;
31 int min_y = traces[0].y_tile;
32 int max_x = min_x;
33 int max_y = min_y;
34 for (const auto& t : traces) {
35 min_x = std::min(min_x, static_cast<int>(t.x_tile));
36 min_y = std::min(min_y, static_cast<int>(t.y_tile));
37 max_x = std::max(max_x, static_cast<int>(t.x_tile));
38 max_y = std::max(max_y, static_cast<int>(t.y_tile));
39 }
40
41 layout.origin_tile_x = min_x;
42 layout.origin_tile_y = min_y;
43 layout.bounds_width = max_x - min_x + 1;
44 layout.bounds_height = max_y - min_y + 1;
45
46 layout.cells.reserve(traces.size());
47 for (const auto& t : traces) {
48 Cell cell;
49 cell.rel_x = t.x_tile - min_x;
50 cell.rel_y = t.y_tile - min_y;
51
52 // Reconstruct TileInfo from trace fields
53 bool h_mirror = (t.flags & 0x1) != 0;
54 bool v_mirror = (t.flags & 0x2) != 0;
55 bool priority = (t.flags & 0x4) != 0;
56 uint8_t palette = (t.flags >> 3) & 0x7;
57 cell.tile_info = gfx::TileInfo(t.tile_id, palette, v_mirror, h_mirror, priority);
59 cell.modified = false;
60
61 layout.cells.push_back(cell);
62 }
63
64 return layout;
65}
66
68 int16_t object_id,
69 const std::string& filename) {
70 ObjectTileLayout layout;
71 layout.object_id = object_id;
72 layout.origin_tile_x = 0;
73 layout.origin_tile_y = 0;
74 layout.bounds_width = width;
75 layout.bounds_height = height;
76 layout.tile_data_address = -1;
77 layout.is_custom = true;
78 layout.custom_filename = filename;
79
80 layout.cells.reserve(width * height);
81 for (int y = 0; y < height; ++y) {
82 for (int x = 0; x < width; ++x) {
83 Cell cell;
84 cell.rel_x = x;
85 cell.rel_y = y;
86 cell.tile_info = gfx::TileInfo(0, 2, false, false, false);
88 cell.modified = true;
89 layout.cells.push_back(cell);
90 }
91 }
92
93 return layout;
94}
95
97 for (auto& cell : cells) {
98 if (cell.rel_x == rel_x && cell.rel_y == rel_y) return &cell;
99 }
100 return nullptr;
101}
102
104 int rel_y) const {
105 for (const auto& cell : cells) {
106 if (cell.rel_x == rel_x && cell.rel_y == rel_y) return &cell;
107 }
108 return nullptr;
109}
110
112 for (const auto& cell : cells) {
113 if (cell.modified) return true;
114 }
115 return false;
116}
117
119 for (auto& cell : cells) {
120 if (cell.modified) {
121 cell.tile_info = gfx::WordToTileInfo(cell.original_word);
122 cell.modified = false;
123 }
124 }
125}
126
127// =============================================================================
128// ObjectTileEditor
129// =============================================================================
130
132
133absl::StatusOr<ObjectTileLayout> ObjectTileEditor::CaptureObjectLayout(
134 int16_t object_id, const Room& room,
135 const gfx::PaletteGroup& palette) {
136 if (!rom_ || !rom_->is_loaded()) {
137 return absl::FailedPreconditionError("ROM not loaded");
138 }
139
140 // Create a temporary object at a known position for tracing
141 RoomObject obj(object_id, 2, 2, 0x12, 0);
142 obj.SetRom(rom_);
143 obj.EnsureTilesLoaded();
144
145 // Check if this is a custom object
146 bool is_custom = false;
147 std::string custom_filename;
148 int subtype = obj.size_ & 0x1F;
149 if (core::FeatureFlags::get().kEnableCustomObjects) {
150 auto custom_result =
151 CustomObjectManager::Get().GetObjectInternal(object_id, subtype);
152 if (custom_result.ok()) {
153 is_custom = true;
154 custom_filename =
155 CustomObjectManager::Get().ResolveFilename(object_id, subtype);
156 }
157 }
158
159 // Create drawer and set up trace collection
160 ObjectDrawer drawer(rom_, room.id(), room.get_gfx_buffer().data());
161
162 std::vector<ObjectDrawer::TileTrace> traces;
163 traces.reserve(256);
164 drawer.SetTraceCollector(&traces, /*trace_only=*/true);
165
166 // Draw the object to collect traces
167 gfx::BackgroundBuffer dummy_bg1(512, 512);
168 gfx::BackgroundBuffer dummy_bg2(512, 512);
169 auto status = drawer.DrawObject(obj, dummy_bg1, dummy_bg2, palette);
170 drawer.ClearTraceCollector();
171
172 if (!status.ok()) {
173 return status;
174 }
175
176 if (traces.empty()) {
177 return absl::NotFoundError("Object produced no tile traces");
178 }
179
180 // Build layout from traces
183 layout.is_custom = is_custom;
184 layout.custom_filename = custom_filename;
185
186 return layout;
187}
188
190 const ObjectTileLayout& layout, gfx::Bitmap& bitmap,
191 const uint8_t* room_gfx_buffer,
192 const gfx::PaletteGroup& palette) {
193 if (!room_gfx_buffer) {
194 return absl::FailedPreconditionError("No room graphics buffer");
195 }
196 if (layout.cells.empty()) {
197 return absl::OkStatus();
198 }
199
200 int bmp_w = layout.bounds_width * 8;
201 int bmp_h = layout.bounds_height * 8;
202
203 // Create or resize bitmap
204 std::vector<uint8_t> pixel_data(bmp_w * bmp_h, 0);
205 bitmap.Create(bmp_w, bmp_h, 8, pixel_data);
206
207 // Apply palette from the room's palette group (use dungeon palette index 2)
208 if (palette.size() > 2) {
209 bitmap.SetPalette(palette[2]);
210 } else if (palette.size() > 0) {
211 bitmap.SetPalette(palette[0]);
212 }
213
214 // Use a temporary ObjectDrawer just for its DrawTileToBitmap utility
215 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
216
217 for (const auto& cell : layout.cells) {
218 int px = cell.rel_x * 8;
219 int py = cell.rel_y * 8;
220 drawer.DrawTileToBitmap(bitmap, cell.tile_info, px, py, room_gfx_buffer);
221 }
222
223 return absl::OkStatus();
224}
225
227 gfx::Bitmap& atlas, const uint8_t* room_gfx_buffer,
228 const gfx::PaletteGroup& palette, int display_palette) {
229 if (!room_gfx_buffer) {
230 return absl::FailedPreconditionError("No room graphics buffer");
231 }
232
233 std::vector<uint8_t> pixel_data(
235 atlas.Create(kAtlasWidthPx, kAtlasHeightPx, 8, pixel_data);
236
237 if (palette.size() > static_cast<size_t>(display_palette)) {
238 atlas.SetPalette(palette[display_palette]);
239 } else if (palette.size() > 0) {
240 atlas.SetPalette(palette[0]);
241 }
242
243 ObjectDrawer drawer(rom_, 0, room_gfx_buffer);
244
245 for (int tile_id = 0; tile_id < kAtlasTileCount; ++tile_id) {
246 int col = tile_id % kAtlasTilesPerRow;
247 int row = tile_id / kAtlasTilesPerRow;
248 int px = col * 8;
249 int py = row * 8;
250
251 gfx::TileInfo info(static_cast<uint16_t>(tile_id),
252 static_cast<uint8_t>(display_palette),
253 false, false, false);
254 drawer.DrawTileToBitmap(atlas, info, px, py, room_gfx_buffer);
255 }
256
257 return absl::OkStatus();
258}
259
261 if (!layout.HasModifications()) {
262 return absl::OkStatus();
263 }
264
265 if (layout.is_custom) {
266 // Custom object: serialize to binary format and write .bin file
267 if (layout.custom_filename.empty()) {
268 return absl::FailedPreconditionError(
269 "Custom object has no filename for write-back");
270 }
271
272 // Group cells by rel_y to form segments
273 std::map<int, std::vector<const ObjectTileLayout::Cell*>> rows;
274 for (const auto& cell : layout.cells) {
275 rows[cell.rel_y].push_back(&cell);
276 }
277
278 // Serialize to binary matching CustomObjectManager::ParseBinaryData format
279 std::vector<uint8_t> binary;
280 constexpr int kBufferStride = 128;
281
282 int prev_buffer_pos = 0;
283 for (auto& [row_y, row_cells] : rows) {
284 // Sort cells by rel_x
285 std::sort(row_cells.begin(), row_cells.end(),
286 [](const auto* a, const auto* b) {
287 return a->rel_x < b->rel_x;
288 });
289
290 int count = static_cast<int>(row_cells.size());
291 int buffer_pos_for_row = row_y * kBufferStride + row_cells[0]->rel_x * 2;
292 int jump_offset =
293 (row_y == rows.rbegin()->first)
294 ? 0
295 : kBufferStride; // Jump to next row
296
297 // Header: low 5 bits = count, high byte = jump_offset
298 uint16_t header = (count & 0x1F) | ((jump_offset & 0xFF) << 8);
299 binary.push_back(header & 0xFF);
300 binary.push_back((header >> 8) & 0xFF);
301
302 for (const auto* cell : row_cells) {
303 uint16_t word = gfx::TileInfoToWord(cell->tile_info);
304 binary.push_back(word & 0xFF);
305 binary.push_back((word >> 8) & 0xFF);
306 }
307
308 prev_buffer_pos = buffer_pos_for_row + count * 2;
309 }
310
311 // Terminator
312 binary.push_back(0);
313 binary.push_back(0);
314
315 // Write to file
316 auto& mgr = CustomObjectManager::Get();
317 std::filesystem::path full_path =
318 std::filesystem::path(mgr.GetBasePath()) / layout.custom_filename;
319 std::ofstream file(full_path, std::ios::binary);
320 if (!file) {
321 return absl::InternalError("Failed to open file for writing: " +
322 full_path.string());
323 }
324 file.write(reinterpret_cast<const char*>(binary.data()), binary.size());
325 file.close();
326
327 // Reload cache
328 mgr.ReloadAll();
329 return absl::OkStatus();
330 }
331
332 // Standard object: patch ROM at tile_data_address
333 if (layout.tile_data_address < 0) {
334 return absl::FailedPreconditionError(
335 "No tile data address for ROM write-back");
336 }
337
338 for (size_t i = 0; i < layout.cells.size(); ++i) {
339 const auto& cell = layout.cells[i];
340 if (!cell.modified) continue;
341
342 int addr = layout.tile_data_address + static_cast<int>(i) * 2;
343 uint16_t word = gfx::TileInfoToWord(cell.tile_info);
344
345 // Write 2 bytes (little-endian SNES tilemap word)
346 auto low = static_cast<uint8_t>(word & 0xFF);
347 auto high = static_cast<uint8_t>((word >> 8) & 0xFF);
348 RETURN_IF_ERROR(rom_->WriteByte(addr, low));
349 RETURN_IF_ERROR(rom_->WriteByte(addr + 1, high));
350 }
351
352 return absl::OkStatus();
353}
354
356 if (!rom_ || !rom_->is_loaded()) return 0;
357
358 // Create temporary object to get its tile_data_ptr_
359 RoomObject test_obj(object_id, 0, 0, 0, 0);
360 test_obj.SetRom(rom_);
361 test_obj.EnsureTilesLoaded();
362 int target_ptr = test_obj.tile_data_ptr_;
363 if (target_ptr < 0) return 0;
364
365 // Scan type 1 objects (0x00-0xFF) for shared pointers
366 int count = 0;
367 for (int id = 0; id < 0x100; ++id) {
368 RoomObject scan_obj(static_cast<int16_t>(id), 0, 0, 0, 0);
369 scan_obj.SetRom(rom_);
370 scan_obj.EnsureTilesLoaded();
371 if (scan_obj.tile_data_ptr_ == target_ptr) {
372 ++count;
373 }
374 }
375
376 return count;
377}
378
379} // namespace zelda3
380} // 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:476
bool is_loaded() const
Definition rom.h:132
static Flags & get()
Definition features.h:118
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 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)
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:70
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:625
int id() const
Definition room.h:566
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Represents a group of palettes.
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)