yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_sheet_png.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5#include <string>
6
7#include "absl/status/status.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
11#include "util/macro.h"
12#include "zelda3/dungeon/room.h"
13#include "zelda3/game_data.h"
16
17namespace yaze::zelda3 {
18namespace {
19
20constexpr int kSheetWidth = 128;
21constexpr int kSheetHeight = 32;
22constexpr int kBlockSize = 16;
23
24std::pair<int, int> BlockOrigin(int block) {
25 return {(block % 8) * kBlockSize, (block / 8) * kBlockSize};
26}
27
28SheetColor ToSheetColor(const SDL_Color& color) {
29 return {color.r, color.g, color.b};
30}
31
32absl::StatusOr<Room> LoadRoomWithPalettes(Rom& rom, GameData& data, int room) {
33 if (room < 0 || room >= kNumberOfRooms) {
34 return absl::InvalidArgumentError(
35 absl::StrFormat("Room 0x%X is out of range", room));
36 }
37 if (data.palette_groups.dungeon_main.size() == 0) {
38 return absl::FailedPreconditionError("Dungeon palettes are not loaded");
39 }
40 Room loaded = LoadRoomHeaderFromRom(&rom, room);
41 loaded.SetGameData(&data);
42 return loaded;
43}
44
45} // namespace
46
48 SheetPalette palette;
49 for (int i = 0; i < 8; ++i) {
50 const auto level = static_cast<uint8_t>(i * 255 / 7);
51 palette[i] = {level, level, level};
52 }
53 return palette;
54}
55
56absl::StatusOr<SheetPalette> RoomBackgroundSheetPalette(Rom& rom,
57 GameData& data,
58 int room, int row) {
59 if (row < 0 || row > 7) {
60 return absl::InvalidArgumentError("Background palette row must be 0-7");
61 }
62 ASSIGN_OR_RETURN(Room loaded, LoadRoomWithPalettes(rom, data, room));
63 const int palette_id = loaded.ResolveDungeonPaletteId();
65 data.palette_groups.dungeon_main.palette_ref(palette_id), &data);
66 if (row >= static_cast<int>(group.size()) ||
67 group.palette_ref(row).size() < 8) {
68 return absl::FailedPreconditionError(absl::StrFormat(
69 "Room 0x%X has no background palette row %d", room, row));
70 }
71 SheetPalette palette;
72 for (int i = 0; i < 8; ++i) {
73 const auto rgb = group.palette_ref(row)[i].rgb();
74 palette[i] = {static_cast<uint8_t>(rgb.x), static_cast<uint8_t>(rgb.y),
75 static_cast<uint8_t>(rgb.z)};
76 }
77 return palette;
78}
79
80absl::StatusOr<SheetPalette> RoomSpriteSheetPalette(Rom& rom, GameData& data,
81 int room, int row) {
82 if (row < 0 || row > 7) {
83 return absl::InvalidArgumentError("Sprite palette row must be 0-7");
84 }
85 ASSIGN_OR_RETURN(Room loaded, LoadRoomWithPalettes(rom, data, room));
86 const auto cgram = BuildDungeonSpriteRenderPalette(loaded, &data);
87 SheetPalette palette;
88 for (int i = 0; i < 8; ++i) {
89 palette[i] = ToSheetColor(cgram[(8 + row) * 16 + i]);
90 }
91 return palette;
92}
93
94absl::StatusOr<std::vector<uint8_t>> ExportSheetBlocksPng(
95 const std::vector<uint8_t>& snes_3bpp, const SheetPalette& palette,
96 int first_block, int block_count) {
97 if (snes_3bpp.size() != kGfxSheet3bppBytes) {
98 return absl::InvalidArgumentError("A 3bpp sheet is 0x600 bytes");
99 }
100 if (first_block < 0 || block_count <= 0 ||
101 first_block + block_count > kGfxSheetBlockCount) {
102 return absl::InvalidArgumentError(
103 absl::StrFormat("Blocks %d-%d are outside the sheet's 16 blocks",
104 first_block, first_block + block_count - 1));
105 }
106 const auto sheet = gfx::SnesTo8bppSheet(snes_3bpp, 3);
107 const int columns = std::min(block_count, 8);
108 const int rows = (block_count + 7) / 8;
109 const int width = columns * kBlockSize;
110 const int height = rows * kBlockSize;
111 std::vector<uint8_t> indices(static_cast<size_t>(width) * height, 0);
112 for (int k = 0; k < block_count; ++k) {
113 const auto [sx, sy] = BlockOrigin(first_block + k);
114 const int dx = (k % 8) * kBlockSize;
115 const int dy = (k / 8) * kBlockSize;
116 for (int y = 0; y < kBlockSize; ++y) {
117 for (int x = 0; x < kBlockSize; ++x) {
118 indices[(dy + y) * width + dx + x] =
119 sheet[(sy + y) * kSheetWidth + sx + x] & 0x07;
120 }
121 }
122 }
123 std::vector<std::array<uint8_t, 4>> png_palette;
124 for (int i = 0; i < 8; ++i) {
125 png_palette.push_back({palette[i][0], palette[i][1], palette[i][2],
126 static_cast<uint8_t>(i == 0 ? 0 : 255)});
127 }
128 return util::EncodeIndexedPng(width, height, indices, png_palette);
129}
130
131absl::StatusOr<SheetImportResult> ImportSheetBlocksPng(
132 const std::vector<uint8_t>& snes_3bpp, const util::PngImage& png,
133 int first_block, const SheetPalette& palette) {
134 if (snes_3bpp.size() != kGfxSheet3bppBytes) {
135 return absl::InvalidArgumentError("A 3bpp sheet is 0x600 bytes");
136 }
137 if (png.width % kBlockSize != 0 || png.height % kBlockSize != 0) {
138 return absl::InvalidArgumentError(absl::StrFormat(
139 "PNG is %dx%d; width and height must be multiples of 16", png.width,
140 png.height));
141 }
142 const int columns = png.width / kBlockSize;
143 const int block_count = columns * (png.height / kBlockSize);
144 if (first_block < 0 || first_block + block_count > kGfxSheetBlockCount) {
145 return absl::InvalidArgumentError(absl::StrFormat(
146 "The PNG holds %d 16x16 blocks; starting at block %d it runs past "
147 "block 15",
148 block_count, first_block));
149 }
150
151 // Resolve every pixel to a 3bpp index before touching the sheet.
152 std::vector<uint8_t> values(static_cast<size_t>(png.width) * png.height);
153 std::vector<std::string> offenders;
154 int bad_pixels = 0;
155 for (int y = 0; y < png.height; ++y) {
156 for (int x = 0; x < png.width; ++x) {
157 const size_t i = static_cast<size_t>(y) * png.width + x;
158 int value = -1;
159 std::string seen;
160 if (png.indexed) {
161 value = png.indices[i];
162 seen = absl::StrFormat("index %d", value);
163 if (value > 7) {
164 value = -1;
165 }
166 } else {
167 const uint8_t* p = &png.rgba[i * 4];
168 seen = absl::StrFormat("#%02X%02X%02X", p[0], p[1], p[2]);
169 if (p[3] == 0) {
170 value = 0;
171 } else if (p[3] == 255) {
172 for (int c = 0; c < 8; ++c) {
173 if (palette[c][0] == p[0] && palette[c][1] == p[1] &&
174 palette[c][2] == p[2]) {
175 value = c;
176 break;
177 }
178 }
179 }
180 }
181 if (value < 0) {
182 ++bad_pixels;
183 if (offenders.size() < 8) {
184 offenders.push_back(absl::StrFormat("(%d,%d) %s", x, y, seen));
185 }
186 continue;
187 }
188 values[i] = static_cast<uint8_t>(value);
189 }
190 }
191 if (bad_pixels > 0) {
192 return absl::InvalidArgumentError(absl::StrFormat(
193 "%d pixel(s) are not 3bpp indices 0-7 of the sheet palette; nothing "
194 "was imported. First: %s",
195 bad_pixels, absl::StrJoin(offenders, ", ")));
196 }
197
198 const auto old_sheet = gfx::SnesTo8bppSheet(snes_3bpp, 3);
199 std::vector<uint8_t> sheet = old_sheet;
200 for (int k = 0; k < block_count; ++k) {
201 const auto [dx, dy] = BlockOrigin(first_block + k);
202 const int sx = (k % columns) * kBlockSize;
203 const int sy = (k / columns) * kBlockSize;
204 for (int y = 0; y < kBlockSize; ++y) {
205 for (int x = 0; x < kBlockSize; ++x) {
206 sheet[(dy + y) * kSheetWidth + dx + x] =
207 values[(sy + y) * png.width + sx + x];
208 }
209 }
210 }
211
212 SheetImportResult result;
213 result.snes_3bpp = gfx::IndexedToSnesSheet(sheet, 3);
214 result.snes_3bpp.resize(kGfxSheet3bppBytes);
215 for (int block = 0; block < kGfxSheetBlockCount; ++block) {
216 const auto [bx, by] = BlockOrigin(block);
217 bool changed = false;
218 for (int y = 0; y < kBlockSize && !changed; ++y) {
219 for (int x = 0; x < kBlockSize; ++x) {
220 const size_t i = (by + y) * kSheetWidth + bx + x;
221 if ((sheet[i] & 7) != (old_sheet[i] & 7)) {
222 changed = true;
223 break;
224 }
225 }
226 }
227 if (changed) {
228 result.changed_blocks.push_back(block);
229 }
230 }
231 for (int tile = 0; tile < 64; ++tile) {
232 if (!std::equal(result.snes_3bpp.begin() + tile * 24,
233 result.snes_3bpp.begin() + tile * 24 + 24,
234 snes_3bpp.begin() + tile * 24)) {
235 result.changed_tiles.push_back(tile);
236 }
237 }
238 for (size_t i = 0; i < snes_3bpp.size();) {
239 if (result.snes_3bpp[i] == snes_3bpp[i]) {
240 ++i;
241 continue;
242 }
243 size_t end = i;
244 while (end < snes_3bpp.size() && result.snes_3bpp[end] != snes_3bpp[end]) {
245 ++end;
246 }
247 result.changed_ranges.emplace_back(static_cast<int>(i),
248 static_cast<int>(end));
249 i = end;
250 }
251 return result;
252}
253
255 uint8_t header_blockset) {
257 set.room = room;
258 set.header_blockset = header_blockset;
259 set.main_blockset =
260 ResolveRoomMainBlockset(room, header_blockset, 0xFF, data).main_blockset;
261 for (int i = 0; i < 8; ++i) {
262 set.sheets[i] = data.main_blockset_ids[set.main_blockset][i];
263 if (i >= 3 && i <= 6 && header_blockset < data.room_blockset_ids.size()) {
264 const uint8_t room_sheet = data.room_blockset_ids[header_blockset][i - 3];
265 if (room_sheet != 0) {
266 set.sheets[i] = room_sheet;
267 set.from_room_blockset[i] = true;
268 }
269 }
270 }
271 return set;
272}
273
274absl::StatusOr<RoomSheetPaletteContext> ResolveRoomSheetPalette(
275 Rom& rom, GameData& data, int room, int slot, int row) {
276 if (slot < 0 || slot >= 8 || row < 0 || row >= 8) {
277 return absl::InvalidArgumentError("Background slot and row must be 0-7");
278 }
279 ASSIGN_OR_RETURN(Room loaded, LoadRoomWithPalettes(rom, data, room));
281 result.background = ResolveRoomBackgroundSet(data, room, loaded.blockset());
282 result.main = ResolveRoomMainBlockset(room, loaded.blockset(), 0xFF, data);
285 result.entrance = data.room_default_entrances[room];
286 }
287 result.slot = slot;
288 result.row = row;
289 result.palette_offset =
293 loaded.ResolveDungeonPaletteId()),
294 &data);
295 if (row >= static_cast<int>(group.size()) ||
296 group.palette_ref(row).size() < 16) {
297 return absl::FailedPreconditionError(
298 "Room background palette is incomplete");
299 }
300 for (int index = 0; index < 8; ++index) {
301 const int column = index == 0 ? 0 : index + result.palette_offset;
302 result.cgram_indices[index] = row * 16 + column;
303 const auto rgb = group.palette_ref(row)[column].rgb();
304 result.colors[index] = {static_cast<uint8_t>(rgb.x),
305 static_cast<uint8_t>(rgb.y),
306 static_cast<uint8_t>(rgb.z)};
307 }
308 return result;
309}
310
311std::vector<RoomBackgroundSet> ResolveAllRoomBackgroundSets(
312 const Rom& rom, const GameData& data) {
313 std::vector<RoomBackgroundSet> sets;
314 for (const auto& [room, info] : CollectRoomGfx(rom)) {
315 sets.push_back(ResolveRoomBackgroundSet(data, room, info.blockset));
316 }
317 return sets;
318}
319
320absl::StatusOr<std::vector<uint8_t>> ExportRoomBackgroundPng(
321 const Rom& rom, const RoomBackgroundSet& set, const SheetPalette& palette) {
322 constexpr int kHeight = kSheetHeight * 8;
323 std::vector<uint8_t> indices(static_cast<size_t>(kSheetWidth) * kHeight, 0);
324 for (int slot = 0; slot < 8; ++slot) {
325 const uint16_t sheet = set.sheets[slot];
326 if (sheet >= kGfxSheetCount ||
328 continue;
329 }
330 ASSIGN_OR_RETURN(const auto data, ReadGfxSheetData(rom, sheet));
331 const auto pixels = gfx::SnesTo8bppSheet(data, 3);
332 for (int y = 0; y < kSheetHeight; ++y) {
333 for (int x = 0; x < kSheetWidth; ++x) {
334 indices[(slot * kSheetHeight + y) * kSheetWidth + x] =
335 pixels[y * kSheetWidth + x] & 0x07;
336 }
337 }
338 }
339 std::vector<std::array<uint8_t, 4>> png_palette;
340 for (int i = 0; i < 8; ++i) {
341 png_palette.push_back({palette[i][0], palette[i][1], palette[i][2],
342 static_cast<uint8_t>(i == 0 ? 0 : 255)});
343 }
344 return util::EncodeIndexedPng(kSheetWidth, kHeight, indices, png_palette);
345}
346
347absl::StatusOr<std::vector<RoomSheetImport>> ImportRoomBackgroundPng(
348 const Rom& rom, const RoomBackgroundSet& set, const util::PngImage& png,
349 const SheetPalette& palette) {
350 if (png.width != kSheetWidth || png.height != kSheetHeight * 8) {
351 return absl::InvalidArgumentError(absl::StrFormat(
352 "A room background PNG is 128x256 (8 stacked sheets); got %dx%d",
353 png.width, png.height));
354 }
355 std::vector<RoomSheetImport> imports;
356 std::map<uint16_t, std::pair<int, std::vector<uint8_t>>> seen;
357 for (int slot = 0; slot < 8; ++slot) {
358 const uint16_t sheet = set.sheets[slot];
359 // Crop this slot's 128x32 band.
360 util::PngImage band;
361 band.width = kSheetWidth;
362 band.height = kSheetHeight;
363 band.indexed = png.indexed;
364 band.palette = png.palette;
365 const size_t begin = static_cast<size_t>(slot) * kSheetHeight * kSheetWidth;
366 const size_t count = static_cast<size_t>(kSheetHeight) * kSheetWidth;
367 if (png.indexed) {
368 band.indices.assign(png.indices.begin() + begin,
369 png.indices.begin() + begin + count);
370 } else {
371 band.rgba.assign(png.rgba.begin() + begin * 4,
372 png.rgba.begin() + (begin + count) * 4);
373 }
374
375 const bool writable =
376 sheet < kGfxSheetCount &&
378 std::vector<uint8_t> current(kGfxSheet3bppBytes, 0);
379 if (writable) {
380 ASSIGN_OR_RETURN(current, ReadGfxSheetData(rom, sheet));
381 }
382 auto result = ImportSheetBlocksPng(current, band, 0, palette);
383 if (!result.ok()) {
384 return absl::Status(result.status().code(),
385 absl::StrFormat("Slot %d (sheet 0x%02X): %s", slot,
386 sheet, result.status().message()));
387 }
388 if (!writable) {
389 if (!result->changed_blocks.empty()) {
390 return absl::FailedPreconditionError(absl::StrFormat(
391 "Slot %d shows 2bpp sheet 0x%02X, which cannot be edited here",
392 slot, sheet));
393 }
394 continue;
395 }
396 // Every slot showing the same sheet must end up with the same pixels,
397 // including slots left unedited.
398 auto first = seen.find(sheet);
399 if (first != seen.end()) {
400 if (first->second.second != result->snes_3bpp) {
401 return absl::InvalidArgumentError(absl::StrFormat(
402 "Sheet 0x%02X appears in slots %d and %d with different pixels",
403 sheet, first->second.first, slot));
404 }
405 } else {
406 seen.emplace(sheet, std::make_pair(slot, result->snes_3bpp));
407 }
408 if (result->changed_blocks.empty()) {
409 continue;
410 }
411 auto existing = std::find_if(
412 imports.begin(), imports.end(),
413 [sheet](const RoomSheetImport& other) { return other.sheet == sheet; });
414 if (existing != imports.end()) {
415 existing->slots.push_back(slot);
416 continue;
417 }
418 imports.push_back({sheet, {slot}, std::move(*result)});
419 }
420 return imports;
421}
422
423} // namespace yaze::zelda3
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
uint8_t blockset() const
Definition room.h:1015
int ResolveDungeonPaletteId() const
Definition room.cc:921
void SetGameData(GameData *data)
Definition room.h:1088
Which graphics sheets exist, which tables use them, and which 16x16 blocks are free.
3bpp graphics sheets to and from indexed PNGs.
Locates graphics sheets in the ROM and writes edited sheets back.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:133
std::vector< uint8_t > IndexedToSnesSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:204
absl::StatusOr< std::vector< uint8_t > > EncodeIndexedPng(int width, int height, const std::vector< uint8_t > &indices, const std::vector< std::array< uint8_t, 4 > > &palette)
SheetColor ToSheetColor(const SDL_Color &color)
absl::StatusOr< Room > LoadRoomWithPalettes(Rom &rom, GameData &data, int room)
Zelda 3 specific classes and functions.
std::vector< RoomBackgroundSet > ResolveAllRoomBackgroundSets(const Rom &rom, const GameData &data)
All 296 rooms' background sets (header bytes read from the ROM).
absl::StatusOr< RoomSheetPaletteContext > ResolveRoomSheetPalette(Rom &rom, GameData &data, int room, int slot, int row)
absl::StatusOr< std::vector< uint8_t > > ExportRoomBackgroundPng(const Rom &rom, const RoomBackgroundSet &set, const SheetPalette &palette)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
absl::StatusOr< SheetImportResult > ImportSheetBlocksPng(const std::vector< uint8_t > &snes_3bpp, const util::PngImage &png, int first_block, const SheetPalette &palette)
Places a PNG's 16x16 blocks over a sheet from first_block.
constexpr size_t kGfxSheet3bppBytes
RoomBackgroundSet ResolveRoomBackgroundSet(const GameData &data, int room, uint8_t header_blockset)
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
gfx::PaletteGroup BuildDungeonRenderPaletteGroupFromGameData(const gfx::SnesPalette &dungeon_palette, const GameData *game_data)
Definition room.cc:191
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
std::array< uint8_t, 3 > SheetColor
int DungeonBackgroundPaletteOffset(uint8_t main_blockset, int destination_slot)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr size_t kGfxSheetCount
constexpr int kGfxSheetBlockCount
16x16 blocks per sheet: 8 columns x 2 rows over the 128x32 sheet.
RoomMainBlocksetResolution ResolveRoomMainBlockset(int room_id, uint8_t header_blockset, uint8_t requested_main_blockset, const GameData &game_data)
std::array< SheetColor, 8 > SheetPalette
Colors for 3bpp indices 0-7. Index 0 is exported as transparent.
absl::StatusOr< SheetPalette > RoomSpriteSheetPalette(Rom &rom, GameData &data, int room, int row)
Sprite palette row (OAM palette 0-7, CGRAM row 8 + row) of room room.
absl::StatusOr< std::vector< uint8_t > > ExportSheetBlocksPng(const std::vector< uint8_t > &snes_3bpp, const SheetPalette &palette, int first_block, int block_count)
SheetPalette GrayscaleSheetPalette()
absl::StatusOr< SheetPalette > RoomBackgroundSheetPalette(Rom &rom, GameData &data, int room, int row)
constexpr int kNumberOfRooms
std::array< SDL_Color, 256 > BuildDungeonSpriteRenderPalette(const Room &room, const GameData *game_data)
Definition room.cc:200
absl::StatusOr< std::vector< RoomSheetImport > > ImportRoomBackgroundPng(const Rom &rom, const RoomBackgroundSet &set, const util::PngImage &png, const SheetPalette &palette)
const SnesPalette & palette_ref(int i) const
A decoded PNG: palette indices for indexed files, RGBA otherwise.
Definition indexed_png.h:21
std::vector< std::array< uint8_t, 4 > > palette
Definition indexed_png.h:28
std::vector< uint8_t > indices
Definition indexed_png.h:26
std::vector< uint8_t > rgba
Definition indexed_png.h:30
std::vector< RoomDefaultEntrance > room_default_entrances
Definition game_data.h:118
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97
std::array< uint8_t, 8 > sheets
std::array< bool, 8 > from_room_blockset
std::array< uint8_t, 8 > cgram_indices
RoomMainBlocksetResolution main
std::vector< std::pair< int, int > > changed_ranges
std::vector< uint8_t > snes_3bpp
std::vector< int > changed_tiles
std::vector< int > changed_blocks