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
62 LOG_INFO("ZsprLoader", "ZSPR v%d: sprite@0x%04X (%d bytes), palette@0x%04X (%d bytes), type=%d",
63 zspr.metadata.version, sprite_offset, sprite_size,
64 palette_offset, palette_size, zspr.metadata.sprite_type);
65
66 // Validate checksum (covers sprite and palette data)
67 // Note: Some ZSPR files may have checksum=0 if not computed
68 if (checksum != 0) {
69 size_t checksum_start = sprite_offset;
70 size_t checksum_length = sprite_size + palette_size + 4; // +4 for glove colors
71 if (checksum_start + checksum_length <= data.size()) {
73 std::vector<uint8_t>(data.begin() + checksum_start,
74 data.begin() + checksum_start + checksum_length),
75 checksum)) {
76 LOG_WARN("ZsprLoader", "ZSPR checksum mismatch (expected 0x%08X)", checksum);
77 // Continue anyway - some files have incorrect checksums
78 }
79 }
80 }
81
82 // Parse null-terminated strings starting at offset 0x13
83 size_t string_offset = 0x13;
84 size_t bytes_read = 0;
85
86 // Display name
87 if (string_offset < data.size()) {
89 &data[string_offset], data.size() - string_offset, bytes_read);
90 string_offset += bytes_read;
91 }
92
93 // Author name
94 if (string_offset < data.size()) {
96 &data[string_offset], data.size() - string_offset, bytes_read);
97 string_offset += bytes_read;
98 }
99
100 // Author ROM name (optional in some files)
101 if (string_offset < data.size() && string_offset < sprite_offset) {
103 &data[string_offset], data.size() - string_offset, bytes_read);
104 string_offset += bytes_read;
105 }
106
107 LOG_INFO("ZsprLoader", "ZSPR: '%s' by %s",
108 zspr.metadata.display_name.c_str(),
109 zspr.metadata.author.c_str());
110
111 // Extract sprite data
112 if (sprite_offset + sprite_size > data.size()) {
113 return absl::InvalidArgumentError(
114 absl::StrFormat("ZSPR sprite data extends beyond file: offset=%d, size=%d, file_size=%zu",
115 sprite_offset, sprite_size, data.size()));
116 }
117 zspr.sprite_data.assign(data.begin() + sprite_offset,
118 data.begin() + sprite_offset + sprite_size);
119
120 // Validate sprite data size for Link sprites
121 if (zspr.is_link_sprite() && sprite_size != kExpectedSpriteDataSize) {
122 LOG_WARN("ZsprLoader", "Unexpected sprite data size for Link sprite: %d (expected %zu)",
123 sprite_size, kExpectedSpriteDataSize);
124 }
125
126 // Extract palette data
127 if (palette_offset + palette_size > data.size()) {
128 return absl::InvalidArgumentError(
129 absl::StrFormat("ZSPR palette data extends beyond file: offset=%d, size=%d, file_size=%zu",
130 palette_offset, palette_size, data.size()));
131 }
132 zspr.palette_data.assign(data.begin() + palette_offset,
133 data.begin() + palette_offset + palette_size);
134
135 // Extract glove colors (4 bytes after palette data)
136 size_t glove_offset = palette_offset + palette_size;
137 if (glove_offset + 4 <= data.size()) {
138 zspr.glove_colors[0] = ReadU16LE(&data[glove_offset]);
139 zspr.glove_colors[1] = ReadU16LE(&data[glove_offset + 2]);
140 LOG_INFO("ZsprLoader", "Glove colors: 0x%04X, 0x%04X",
141 zspr.glove_colors[0], zspr.glove_colors[1]);
142 }
143
144 return zspr;
145}
146
147absl::Status ZsprLoader::ApplyToRom(Rom& rom, const ZsprData& zspr) {
148 if (!rom.is_loaded()) {
149 return absl::FailedPreconditionError("ROM not loaded");
150 }
151
152 if (!zspr.is_link_sprite()) {
153 return absl::InvalidArgumentError("ZSPR is not a Link sprite (type != 0)");
154 }
155
156 if (zspr.sprite_data.size() != kExpectedSpriteDataSize) {
157 return absl::InvalidArgumentError(
158 absl::StrFormat("Invalid sprite data size: %zu (expected %zu)",
160 }
161
162 // Link's graphics are stored at specific ROM offsets
163 // The ZSPR data is already in 4BPP SNES format, so we write directly
164 //
165 // Link graphics locations (US ROM):
166 // Sheet 0: 0x80000 - 0x807FF (2048 bytes)
167 // Sheet 1: 0x80800 - 0x80FFF
168 // ... (14 sheets total)
169 //
170 // These addresses may vary by ROM version, so we use the version constants
171
172 constexpr uint32_t kLinkGfxBaseUS = 0x80000;
173 constexpr size_t kBytesPerSheet = 2048; // 64 tiles × 32 bytes
174
175 LOG_INFO("ZsprLoader", "Applying ZSPR '%s' to ROM (%zu bytes of sprite data)",
176 zspr.metadata.display_name.c_str(), zspr.sprite_data.size());
177
178 // Write each sheet to ROM
179 for (size_t sheet = 0; sheet < kLinkSheetCount; sheet++) {
180 uint32_t rom_offset = kLinkGfxBaseUS + (sheet * kBytesPerSheet);
181 size_t data_offset = sheet * kBytesPerSheet;
182
183 // Bounds check
184 if (data_offset + kBytesPerSheet > zspr.sprite_data.size()) {
185 LOG_WARN("ZsprLoader", "Sheet %zu data incomplete, stopping", sheet);
186 break;
187 }
188
189 // Write sheet data to ROM
190 for (size_t i = 0; i < kBytesPerSheet; i++) {
191 auto status = rom.WriteByte(rom_offset + i, zspr.sprite_data[data_offset + i]);
192 if (!status.ok()) {
193 return absl::InternalError(
194 absl::StrFormat("Failed to write byte at 0x%06X: %s",
195 rom_offset + i, status.message()));
196 }
197 }
198
199 LOG_INFO("ZsprLoader", "Wrote Link sheet %zu at ROM offset 0x%06X",
200 sheet, rom_offset);
201 }
202
203 return absl::OkStatus();
204}
205
206absl::Status ZsprLoader::ApplyPaletteToRom(Rom& rom, const ZsprData& zspr) {
207 if (!rom.is_loaded()) {
208 return absl::FailedPreconditionError("ROM not loaded");
209 }
210
211 if (zspr.palette_data.size() < kExpectedPaletteDataSize) {
212 return absl::InvalidArgumentError(
213 absl::StrFormat("Invalid palette data size: %zu (expected %zu)",
215 }
216
217 // Link palette locations in US ROM:
218 // Green Mail: 0x0DD308 (30 bytes = 15 colors × 2)
219 // Blue Mail: 0x0DD318
220 // Red Mail: 0x0DD328
221 // Bunny: 0x0DD338
222 //
223 // Glove colors are at separate locations
224
225 constexpr uint32_t kLinkPaletteBase = 0x0DD308;
226 constexpr size_t kPaletteSize = 30; // 15 colors × 2 bytes
227 constexpr size_t kNumPalettes = 4;
228
229 LOG_INFO("ZsprLoader", "Applying ZSPR palette data (%zu bytes)",
230 zspr.palette_data.size());
231
232 // Write each palette
233 for (size_t pal = 0; pal < kNumPalettes; pal++) {
234 uint32_t rom_offset = kLinkPaletteBase + (pal * kPaletteSize);
235 size_t data_offset = pal * kPaletteSize;
236
237 for (size_t i = 0; i < kPaletteSize; i++) {
238 auto status = rom.WriteByte(rom_offset + i, zspr.palette_data[data_offset + i]);
239 if (!status.ok()) {
240 return absl::InternalError(
241 absl::StrFormat("Failed to write palette byte at 0x%06X", rom_offset + i));
242 }
243 }
244
245 LOG_INFO("ZsprLoader", "Wrote palette %zu at ROM offset 0x%06X", pal, rom_offset);
246 }
247
248 // Write glove colors
249 // Glove 1: 0x0DExx (power glove)
250 // Glove 2: 0x0DExx (titan's mitt)
251 // TODO: Find exact glove color offsets for US ROM
252 LOG_INFO("ZsprLoader", "Glove colors loaded but not yet written (TODO: find offsets)");
253
254 return absl::OkStatus();
255}
256
257bool ZsprLoader::ValidateChecksum(const std::vector<uint8_t>& data,
258 uint32_t expected_checksum) {
259 uint32_t computed = CalculateAdler32(data.data(), data.size());
260 return computed == expected_checksum;
261}
262
263uint32_t ZsprLoader::CalculateAdler32(const uint8_t* data, size_t length) {
264 constexpr uint32_t MOD_ADLER = 65521;
265 uint32_t a = 1, b = 0;
266
267 for (size_t i = 0; i < length; i++) {
268 a = (a + data[i]) % MOD_ADLER;
269 b = (b + a) % MOD_ADLER;
270 }
271
272 return (b << 16) | a;
273}
274
275std::string ZsprLoader::ReadNullTerminatedString(const uint8_t* data,
276 size_t max_length,
277 size_t& bytes_read) {
278 std::string result;
279 bytes_read = 0;
280
281 for (size_t i = 0; i < max_length; i++) {
282 bytes_read++;
283 if (data[i] == 0) {
284 break;
285 }
286 result += static_cast<char>(data[i]);
287 }
288
289 return result;
290}
291
292uint16_t ZsprLoader::ReadU16LE(const uint8_t* data) {
293 return static_cast<uint16_t>(data[0]) |
294 (static_cast<uint16_t>(data[1]) << 8);
295}
296
297uint32_t ZsprLoader::ReadU32LE(const uint8_t* data) {
298 return static_cast<uint32_t>(data[0]) |
299 (static_cast<uint32_t>(data[1]) << 8) |
300 (static_cast<uint32_t>(data[2]) << 16) |
301 (static_cast<uint32_t>(data[3]) << 24);
302}
303
304} // namespace gfx
305} // 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:286
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