yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
zspr_loader.cc
Go to the documentation of this file.
2
3#include <cstring>
4#include <fstream>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
8#include "rom/rom.h"
9#include "util/log.h"
10
11namespace yaze {
12namespace gfx {
13
14absl::StatusOr<ZsprData> ZsprLoader::LoadFromFile(const std::string& path) {
15 std::ifstream file(path, std::ios::binary | std::ios::ate);
16 if (!file.is_open()) {
17 return absl::NotFoundError(
18 absl::StrFormat("Could not open ZSPR file: %s", path));
19 }
20
21 // Get file size
22 std::streamsize size = file.tellg();
23 file.seekg(0, std::ios::beg);
24
25 // Read entire file
26 std::vector<uint8_t> data(size);
27 if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
28 return absl::InternalError(
29 absl::StrFormat("Failed to read ZSPR file: %s", path));
30 }
31
32 return LoadFromData(data);
33}
34
35absl::StatusOr<ZsprData> ZsprLoader::LoadFromData(
36 const std::vector<uint8_t>& data) {
37 // Minimum header size check (magic + version + checksum + offsets + type)
38 constexpr size_t kMinHeaderSize = 0x13;
39 if (data.size() < kMinHeaderSize) {
40 return absl::InvalidArgumentError(
41 absl::StrFormat("ZSPR file too small: %zu bytes (minimum %zu)",
42 data.size(), kMinHeaderSize));
43 }
44
45 // Check magic bytes "ZSPR"
46 if (data[0] != 'Z' || data[1] != 'S' || data[2] != 'P' || data[3] != 'R') {
47 return absl::InvalidArgumentError(
48 "Invalid ZSPR file: missing magic bytes 'ZSPR'");
49 }
50
51 ZsprData zspr;
52
53 // Parse header
54 zspr.metadata.version = data[0x04];
55 uint32_t checksum = ReadU32LE(&data[0x05]);
56 uint16_t sprite_offset = ReadU16LE(&data[0x09]);
57 uint16_t sprite_size = ReadU16LE(&data[0x0B]);
58 uint16_t palette_offset = ReadU16LE(&data[0x0D]);
59 uint16_t palette_size = ReadU16LE(&data[0x0F]);
60 zspr.metadata.sprite_type = ReadU16LE(&data[0x11]);
61
63 "ZsprLoader",
64 "ZSPR v%d: sprite@0x%04X (%d bytes), palette@0x%04X (%d bytes), type=%d",
65 zspr.metadata.version, sprite_offset, sprite_size, palette_offset,
66 palette_size, zspr.metadata.sprite_type);
67
68 // Validate checksum (covers sprite and palette data)
69 // Note: Some ZSPR files may have checksum=0 if not computed
70 if (checksum != 0) {
71 size_t checksum_start = sprite_offset;
72 size_t checksum_length =
73 sprite_size + palette_size + 4; // +4 for glove colors
74 if (checksum_start + checksum_length <= data.size()) {
75 if (!ValidateChecksum(std::vector<uint8_t>(data.begin() + checksum_start,
76 data.begin() + checksum_start +
77 checksum_length),
78 checksum)) {
79 LOG_WARN("ZsprLoader", "ZSPR checksum mismatch (expected 0x%08X)",
80 checksum);
81 // Continue anyway - some files have incorrect checksums
82 }
83 }
84 }
85
86 // Parse null-terminated strings starting at offset 0x13
87 size_t string_offset = 0x13;
88 size_t bytes_read = 0;
89
90 // Display name
91 if (string_offset < data.size()) {
93 &data[string_offset], data.size() - string_offset, bytes_read);
94 string_offset += bytes_read;
95 }
96
97 // Author name
98 if (string_offset < data.size()) {
100 &data[string_offset], data.size() - string_offset, bytes_read);
101 string_offset += bytes_read;
102 }
103
104 // Author ROM name (optional in some files)
105 if (string_offset < data.size() && string_offset < sprite_offset) {
107 &data[string_offset], data.size() - string_offset, bytes_read);
108 string_offset += bytes_read;
109 }
110
111 LOG_INFO("ZsprLoader", "ZSPR: '%s' by %s", zspr.metadata.display_name.c_str(),
112 zspr.metadata.author.c_str());
113
114 // Extract sprite data
115 if (sprite_offset + sprite_size > data.size()) {
116 return absl::InvalidArgumentError(
117 absl::StrFormat("ZSPR sprite data extends beyond file: offset=%d, "
118 "size=%d, file_size=%zu",
119 sprite_offset, sprite_size, data.size()));
120 }
121 zspr.sprite_data.assign(data.begin() + sprite_offset,
122 data.begin() + sprite_offset + sprite_size);
123
124 // Validate sprite data size for Link sprites
125 if (zspr.is_link_sprite() && sprite_size != kExpectedSpriteDataSize) {
126 LOG_WARN("ZsprLoader",
127 "Unexpected sprite data size for Link sprite: %d (expected %zu)",
128 sprite_size, kExpectedSpriteDataSize);
129 }
130
131 // Extract palette data
132 if (palette_offset + palette_size > data.size()) {
133 return absl::InvalidArgumentError(
134 absl::StrFormat("ZSPR palette data extends beyond file: offset=%d, "
135 "size=%d, file_size=%zu",
136 palette_offset, palette_size, data.size()));
137 }
138 zspr.palette_data.assign(data.begin() + palette_offset,
139 data.begin() + palette_offset + palette_size);
140
141 // Extract glove colors (4 bytes after palette data)
142 size_t glove_offset = palette_offset + palette_size;
143 if (glove_offset + 4 <= data.size()) {
144 zspr.glove_colors[0] = ReadU16LE(&data[glove_offset]);
145 zspr.glove_colors[1] = ReadU16LE(&data[glove_offset + 2]);
146 LOG_INFO("ZsprLoader", "Glove colors: 0x%04X, 0x%04X", zspr.glove_colors[0],
147 zspr.glove_colors[1]);
148 }
149
150 return zspr;
151}
152
153absl::Status ZsprLoader::ApplyToRom(Rom& rom, const ZsprData& zspr) {
154 if (!rom.is_loaded()) {
155 return absl::FailedPreconditionError("ROM not loaded");
156 }
157
158 if (!zspr.is_link_sprite()) {
159 return absl::InvalidArgumentError("ZSPR is not a Link sprite (type != 0)");
160 }
161
162 if (zspr.sprite_data.size() != kExpectedSpriteDataSize) {
163 return absl::InvalidArgumentError(
164 absl::StrFormat("Invalid sprite data size: %zu (expected %zu)",
166 }
167
168 // Link's graphics are stored at specific ROM offsets
169 // The ZSPR data is already in 4BPP SNES format, so we write directly
170 //
171 // Link graphics locations (US ROM):
172 // Sheet 0: 0x80000 - 0x807FF (2048 bytes)
173 // Sheet 1: 0x80800 - 0x80FFF
174 // ... (14 sheets total)
175 //
176 // These addresses may vary by ROM version, so we use the version constants
177
178 constexpr uint32_t kLinkGfxBaseUS = 0x80000;
179 constexpr size_t kBytesPerSheet = 2048; // 64 tiles × 32 bytes
180
181 LOG_INFO("ZsprLoader", "Applying ZSPR '%s' to ROM (%zu bytes of sprite data)",
182 zspr.metadata.display_name.c_str(), zspr.sprite_data.size());
183
184 // Write each sheet to ROM
185 for (size_t sheet = 0; sheet < kLinkSheetCount; sheet++) {
186 uint32_t rom_offset = kLinkGfxBaseUS + (sheet * kBytesPerSheet);
187 size_t data_offset = sheet * kBytesPerSheet;
188
189 // Bounds check
190 if (data_offset + kBytesPerSheet > zspr.sprite_data.size()) {
191 LOG_WARN("ZsprLoader", "Sheet %zu data incomplete, stopping", sheet);
192 break;
193 }
194
195 // Write sheet data to ROM
196 for (size_t i = 0; i < kBytesPerSheet; i++) {
197 auto status =
198 rom.WriteByte(rom_offset + i, zspr.sprite_data[data_offset + i]);
199 if (!status.ok()) {
200 return absl::InternalError(
201 absl::StrFormat("Failed to write byte at 0x%06X: %s",
202 rom_offset + i, status.message()));
203 }
204 }
205
206 LOG_INFO("ZsprLoader", "Wrote Link sheet %zu at ROM offset 0x%06X", sheet,
207 rom_offset);
208 }
209
210 return absl::OkStatus();
211}
212
213absl::Status ZsprLoader::ApplyPaletteToRom(Rom& rom, const ZsprData& zspr) {
214 if (!rom.is_loaded()) {
215 return absl::FailedPreconditionError("ROM not loaded");
216 }
217
218 if (zspr.palette_data.size() < kExpectedPaletteDataSize) {
219 return absl::InvalidArgumentError(
220 absl::StrFormat("Invalid palette data size: %zu (expected %zu)",
222 }
223
224 // Link palette locations in US ROM:
225 // Green Mail: 0x0DD308 (30 bytes = 15 colors × 2)
226 // Blue Mail: 0x0DD318
227 // Red Mail: 0x0DD328
228 // Bunny: 0x0DD338
229 //
230 // Glove colors are at separate locations
231
232 constexpr uint32_t kLinkPaletteBase = 0x0DD308;
233 constexpr size_t kPaletteSize = 30; // 15 colors × 2 bytes
234 constexpr size_t kNumPalettes = 4;
235
236 LOG_INFO("ZsprLoader", "Applying ZSPR palette data (%zu bytes)",
237 zspr.palette_data.size());
238
239 // Write each palette
240 for (size_t pal = 0; pal < kNumPalettes; pal++) {
241 uint32_t rom_offset = kLinkPaletteBase + (pal * kPaletteSize);
242 size_t data_offset = pal * kPaletteSize;
243
244 for (size_t i = 0; i < kPaletteSize; i++) {
245 auto status =
246 rom.WriteByte(rom_offset + i, zspr.palette_data[data_offset + i]);
247 if (!status.ok()) {
248 return absl::InternalError(absl::StrFormat(
249 "Failed to write palette byte at 0x%06X", rom_offset + i));
250 }
251 }
252
253 LOG_INFO("ZsprLoader", "Wrote palette %zu at ROM offset 0x%06X", pal,
254 rom_offset);
255 }
256
257 // Write glove colors
258 // Glove 1: 0x0DExx (power glove)
259 // Glove 2: 0x0DExx (titan's mitt)
260 // TODO: Find exact glove color offsets for US ROM
261 LOG_INFO("ZsprLoader",
262 "Glove colors loaded but not yet written (TODO: find offsets)");
263
264 return absl::OkStatus();
265}
266
267bool ZsprLoader::ValidateChecksum(const std::vector<uint8_t>& data,
268 uint32_t expected_checksum) {
269 uint32_t computed = CalculateAdler32(data.data(), data.size());
270 return computed == expected_checksum;
271}
272
273uint32_t ZsprLoader::CalculateAdler32(const uint8_t* data, size_t length) {
274 constexpr uint32_t MOD_ADLER = 65521;
275 uint32_t a = 1, b = 0;
276
277 for (size_t i = 0; i < length; i++) {
278 a = (a + data[i]) % MOD_ADLER;
279 b = (b + a) % MOD_ADLER;
280 }
281
282 return (b << 16) | a;
283}
284
285std::string ZsprLoader::ReadNullTerminatedString(const uint8_t* data,
286 size_t max_length,
287 size_t& bytes_read) {
288 std::string result;
289 bytes_read = 0;
290
291 for (size_t i = 0; i < max_length; i++) {
292 bytes_read++;
293 if (data[i] == 0) {
294 break;
295 }
296 result += static_cast<char>(data[i]);
297 }
298
299 return result;
300}
301
302uint16_t ZsprLoader::ReadU16LE(const uint8_t* data) {
303 return static_cast<uint16_t>(data[0]) | (static_cast<uint16_t>(data[1]) << 8);
304}
305
306uint32_t ZsprLoader::ReadU32LE(const uint8_t* data) {
307 return static_cast<uint32_t>(data[0]) |
308 (static_cast<uint32_t>(data[1]) << 8) |
309 (static_cast<uint32_t>(data[2]) << 16) |
310 (static_cast<uint32_t>(data[3]) << 24);
311}
312
313} // namespace gfx
314} // 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:24
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:290
bool is_loaded() const
Definition rom.h:128
static uint16_t ReadU16LE(const uint8_t *data)
Read 16-bit little-endian value.
static absl::Status ApplyToRom(Rom &rom, const ZsprData &zspr)
Apply loaded ZSPR sprite data to ROM's Link graphics.
static bool ValidateChecksum(const std::vector< uint8_t > &data, uint32_t expected_checksum)
Validate ZSPR checksum (Adler-32)
static constexpr size_t kExpectedPaletteDataSize
Definition zspr_loader.h:76
static absl::StatusOr< ZsprData > LoadFromData(const std::vector< uint8_t > &data)
Load ZSPR data from a byte buffer.
static absl::Status ApplyPaletteToRom(Rom &rom, const ZsprData &zspr)
Apply ZSPR palette data to ROM.
static constexpr size_t kExpectedSpriteDataSize
Definition zspr_loader.h:75
static std::string ReadNullTerminatedString(const uint8_t *data, size_t max_length, size_t &bytes_read)
Read null-terminated string from buffer.
static absl::StatusOr< ZsprData > LoadFromFile(const std::string &path)
Load ZSPR data from a file path.
static uint32_t ReadU32LE(const uint8_t *data)
Read 32-bit little-endian value.
static uint32_t CalculateAdler32(const uint8_t *data, size_t length)
Calculate Adler-32 checksum.
static constexpr size_t kLinkSheetCount
Definition zspr_loader.h:79
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
constexpr int kNumPalettes
Complete data loaded from a ZSPR file.
Definition zspr_loader.h:38
std::vector< uint8_t > sprite_data
Definition zspr_loader.h:40
std::vector< uint8_t > palette_data
Definition zspr_loader.h:41
bool is_link_sprite() const
Definition zspr_loader.h:45
ZsprMetadata metadata
Definition zspr_loader.h:39
std::array< uint16_t, 2 > glove_colors
Definition zspr_loader.h:42
std::string author_rom_name
Definition zspr_loader.h:24