yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_sprite_io.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5#include "absl/strings/str_format.h"
6#include "rom/transaction.h"
7#include "rom/write_fence.h"
8#include "util/macro.h"
10
11namespace yaze::zelda3 {
15 return {{{0x141438, 0x141578, 0x1416B8}}, {{160, 160, 160}}, 0x4C881};
16 }
17 return {{{0x4C881, 0x4C901, 0x4CA21}},
18 {{64, 144, 144}},
20}
21
23 const auto layout = GetOverworldSpriteLayout(rom);
24 if (rom.size() < kOverworldSpriteDataEnd)
26 const auto& data = rom.vector();
27 const int pointer = data[kRoomSpritePointerTableOperand] |
28 (data[kRoomSpritePointerTableOperand + 1] << 8);
29 const int table_pc = 0x40000 + pointer;
30 // A table at data_start leaves no overworld sprite region: return that
31 // empty end so every read and write fails closed, instead of falling back
32 // to kOverworldSpriteDataEnd and letting sprite writes overwrite the table.
33 if (pointer >= 0x8000 && table_pc >= layout.data_start &&
34 table_pc < kOverworldSpriteDataEnd) {
35 return table_pc;
36 }
38}
39
40namespace {
41struct ListSpan {
42 int start = 0;
43 int size = 0; // includes the 0xFF terminator
44};
45
46absl::StatusOr<ListSpan> ReadListSpan(const std::vector<uint8_t>& data,
47 int pointer_address, int data_start,
48 int region_end) {
49 if (pointer_address < 0 ||
50 pointer_address > static_cast<int>(data.size()) - 2 ||
51 static_cast<int>(data.size()) < kOverworldSpriteDataEnd) {
52 return absl::FailedPreconditionError(
53 "Overworld sprite region is truncated");
54 }
55 const int pointer = data[pointer_address] | (data[pointer_address + 1] << 8);
56 const int start = 0x40000 + pointer; // bank $09, ROM half $8000..$FFFF
57 if (pointer < 0x8000 || start < data_start || start >= region_end) {
58 return absl::FailedPreconditionError(
59 "Overworld sprite pointer outside reserved region");
60 }
61 for (int pos = start; pos < region_end;) {
62 if (data[pos] == 0xFF)
63 return ListSpan{start, pos - start + 1};
64 if (pos + 3 > region_end)
65 break;
66 pos += 3;
67 }
68 return absl::FailedPreconditionError(
69 "Overworld sprite list has no terminator within reserved region");
70}
71} // namespace
72
73absl::StatusOr<OverworldSpriteBytes> ReadOverworldSpriteList(
74 const Rom& rom, int pointer_address) {
75 const auto layout = GetOverworldSpriteLayout(rom);
77 auto span, ReadListSpan(rom.vector(), pointer_address, layout.data_start,
79 const auto& data = rom.vector();
80 return OverworldSpriteBytes(data.begin() + span.start,
81 data.begin() + span.start + span.size);
82}
83
85 switch (s) {
87 return "no-change";
89 return "in-place";
91 return "grow-in-place";
93 return "relocate";
94 }
95 return "unknown";
96}
97
98absl::StatusOr<OverworldSpriteListEditPlan> PlanOverworldSpriteListEdit(
99 const Rom& rom, int state, int map,
100 const OverworldSpriteBytes& replacement) {
101 const auto layout = GetOverworldSpriteLayout(rom);
102 if (state < 0 || state >= 3 || map < 0 || map >= layout.counts[state]) {
103 return absl::InvalidArgumentError(
104 "Sprite map is not supported in this game state");
105 }
106 if (replacement.empty() || replacement.back() != 0xFF ||
107 replacement.size() % 3 != 1) {
108 return absl::InvalidArgumentError("Invalid overworld sprite stream");
109 }
110 for (size_t i = 0; i + 1 < replacement.size(); i += 3) {
111 if (replacement[i] == 0xFF)
112 return absl::InvalidArgumentError(
113 "Sprite Y byte collides with terminator");
114 }
115 const int region_start = layout.data_start;
116 const int region_end = GetOverworldSpriteRegionEnd(rom);
117 const auto& data = rom.vector();
118
119 // Inventory every pointer slot; any malformed list fails closed.
120 std::vector<uint8_t> referenced(region_end - region_start, 0);
121 std::vector<std::pair<std::pair<int, int>, ListSpan>> spans;
122 for (int s = 0; s < 3; ++s) {
123 for (int m = 0; m < layout.counts[s]; ++m) {
124 ASSIGN_OR_RETURN(auto span, ReadListSpan(data, layout.tables[s] + m * 2,
125 region_start, region_end));
126 for (int i = span.start; i < span.start + span.size; ++i)
127 referenced[i - region_start] = 1;
128 spans.push_back({{s, m}, span});
129 }
130 }
131
133 plan.pointer_pc = layout.tables[state] + map * 2;
134 plan.region_start = region_start;
135 plan.region_end = region_end;
136 plan.unreferenced_bytes = static_cast<int>(
137 std::count(referenced.begin(), referenced.end(), uint8_t{0}));
138 ListSpan target;
139 for (const auto& [slot, span] : spans) {
140 if (slot == std::make_pair(state, map))
141 target = span;
142 }
143 plan.old_list_pc = target.start;
144 plan.old_bytes.assign(data.begin() + target.start,
145 data.begin() + target.start + target.size);
146 plan.new_bytes = replacement;
147 plan.new_list_pc = target.start;
148 for (const auto& [slot, span] : spans) {
149 if (slot == std::make_pair(state, map))
150 continue;
151 if (span.start < target.start + target.size &&
152 target.start < span.start + span.size) {
153 plan.sharers.push_back(slot);
154 }
155 }
156 if (replacement == plan.old_bytes)
157 return plan;
158
159 const int new_size = static_cast<int>(replacement.size());
160 auto is_free = [&](int pc) {
161 return pc >= region_start && pc < region_end &&
162 !referenced[pc - region_start];
163 };
164 if (plan.sharers.empty()) {
165 if (new_size <= target.size) {
167 } else {
168 bool fits = true;
169 for (int pc = target.start + target.size; pc < target.start + new_size;
170 ++pc) {
171 fits &= is_free(pc);
172 }
173 if (fits)
175 }
176 }
178 // Copy-on-write into the smallest unreferenced run that fits. The old
179 // list's own bytes count as free only when no other slot uses them.
180 auto usable = [&](int pc) {
181 if (is_free(pc))
182 return true;
183 return plan.sharers.empty() && pc >= target.start &&
184 pc < target.start + target.size;
185 };
186 int best = -1;
187 int best_len = 0;
188 for (int pc = region_start; pc < region_end;) {
189 if (!usable(pc)) {
190 ++pc;
191 continue;
192 }
193 int end = pc;
194 while (end < region_end && usable(end))
195 ++end;
196 const int len = end - pc;
197 if (len >= new_size && (best < 0 || len < best_len)) {
198 best = pc;
199 best_len = len;
200 }
201 pc = end;
202 }
203 if (best < 0) {
204 return absl::ResourceExhaustedError(absl::StrFormat(
205 "Overworld sprite list for state %d map $%02X cannot grow in place "
206 "and no unreferenced run of %d bytes exists in PC $%06X-$%06X "
207 "(%d unreferenced bytes total); no bytes written",
208 state, map, new_size, region_start, region_end,
209 plan.unreferenced_bytes));
210 }
212 plan.new_list_pc = best;
213 }
214
216 plan.writes.push_back({plan.new_list_pc, replacement});
217 const uint16_t pointer = static_cast<uint16_t>(plan.new_list_pc - 0x40000);
218 plan.writes.push_back({plan.pointer_pc,
219 {static_cast<uint8_t>(pointer & 0xFF),
220 static_cast<uint8_t>(pointer >> 8)}});
221 } else {
222 plan.writes.push_back({target.start, replacement});
223 }
224
225 // Simulate on a copy: the target must read back as the replacement and
226 // every other slot must read back unchanged.
227 std::vector<uint8_t> sim = data;
228 for (const auto& write : plan.writes)
229 std::copy(write.bytes.begin(), write.bytes.end(),
230 sim.begin() + write.address);
231 for (const auto& [slot, span] : spans) {
233 auto after,
234 ReadListSpan(sim, layout.tables[slot.first] + slot.second * 2,
235 region_start, region_end));
236 const OverworldSpriteBytes got(sim.begin() + after.start,
237 sim.begin() + after.start + after.size);
238 const OverworldSpriteBytes want =
239 slot == std::make_pair(state, map)
240 ? replacement
241 : OverworldSpriteBytes(data.begin() + span.start,
242 data.begin() + span.start + span.size);
243 if (got != want) {
244 return absl::InternalError(absl::StrFormat(
245 "Sprite edit plan would change state %d map $%02X; no bytes written",
246 slot.first, slot.second));
247 }
248 }
249 return plan;
250}
251
252absl::StatusOr<OverworldSpriteSavePlan> PlanOverworldSpriteSave(
253 const Rom& rom, const OverworldSpriteEdits& edits) {
254 const auto layout = GetOverworldSpriteLayout(rom);
255 const int region_end = GetOverworldSpriteRegionEnd(rom);
256 std::vector<OverworldSpriteBytes> lists;
257 bool changed = false;
258 for (int state = 0; state < 3; ++state) {
259 for (int map = 0; map < 160; ++map) {
260 if (map >= layout.counts[state]) {
261 if (edits[state][map])
262 return absl::InvalidArgumentError(
263 "Sprite map is not supported in this game state");
264 continue;
265 }
267 rom, layout.tables[state] + map * 2));
268 if (edits[state][map]) {
269 const auto& replacement = *edits[state][map];
270 if (replacement.empty() || replacement.back() != 0xFF ||
271 replacement.size() % 3 != 1) {
272 return absl::InvalidArgumentError("Invalid overworld sprite stream");
273 }
274 for (size_t i = 0; i + 1 < replacement.size(); i += 3) {
275 if (replacement[i] == 0xFF)
276 return absl::InvalidArgumentError(
277 "Sprite Y byte collides with terminator");
278 }
279 changed |= replacement != bytes;
280 bytes = replacement;
281 }
282 lists.push_back(std::move(bytes));
283 }
284 }
285 if (!changed)
287 std::map<OverworldSpriteBytes, uint16_t> shared;
288 std::vector<uint8_t> payload;
290 size_t list_index = 0;
291 for (int state = 0; state < 3; ++state) {
292 std::vector<uint8_t> pointers;
293 for (int map = 0; map < layout.counts[state]; ++map) {
294 const auto& bytes = lists[list_index++];
295 auto it = shared.find(bytes);
296 if (it == shared.end()) {
297 if (payload.size() + bytes.size() >
298 static_cast<size_t>(region_end - layout.data_start)) {
299 return absl::ResourceExhaustedError(
300 "Overworld sprite data exceeds reserved region; no bytes "
301 "written");
302 }
303 const auto pointer =
304 static_cast<uint16_t>(layout.data_start + payload.size() - 0x40000);
305 it = shared.emplace(bytes, pointer).first;
306 payload.insert(payload.end(), bytes.begin(), bytes.end());
307 }
308 pointers.push_back(it->second & 0xFF);
309 pointers.push_back(it->second >> 8);
310 }
311 plan.push_back({layout.tables[state], std::move(pointers)});
312 }
313 plan.push_back({layout.data_start, std::move(payload)});
314 return plan;
315}
316
318 const OverworldSpriteSavePlan& plan) {
319 if (plan.empty())
320 return absl::OkStatus();
321 const auto layout = GetOverworldSpriteLayout(rom);
322 rom::WriteFence fence;
323 RETURN_IF_ERROR(fence.Allow(layout.data_start,
325 "overworld sprite data"));
326 for (int state = 0; state < 3; ++state) {
327 RETURN_IF_ERROR(fence.Allow(layout.tables[state],
328 layout.tables[state] + layout.counts[state] * 2,
329 "overworld sprite pointers"));
330 }
331 ScopedRomTransaction transaction(rom);
332 rom::ScopedWriteFence scope(&rom, &fence);
333 for (const auto& write : plan) {
334 RETURN_IF_ERROR(rom.WriteVector(write.address, write.bytes));
335 }
336 transaction.Commit();
337 return absl::OkStatus();
338}
339} // 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
const auto & vector() const
Definition rom.h:205
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:928
auto size() const
Definition rom.h:200
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
static bool SupportsAreaEnum(OverworldVersion version)
Check if ROM supports area enum system (v3+ only)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< ListSpan > ReadListSpan(const std::vector< uint8_t > &data, int pointer_address, int data_start, int region_end)
Zelda 3 specific classes and functions.
std::vector< uint8_t > OverworldSpriteBytes
absl::StatusOr< OverworldSpriteListEditPlan > PlanOverworldSpriteListEdit(const Rom &rom, int state, int map, const OverworldSpriteBytes &replacement)
constexpr int kOverworldSpriteDataEnd
constexpr int kRoomSpritePointerTableOperand
absl::StatusOr< OverworldSpriteBytes > ReadOverworldSpriteList(const Rom &rom, int pointer_address)
absl::Status ApplyOverworldSpriteSave(Rom &rom, const OverworldSpriteSavePlan &plan)
constexpr int kOverworldSpriteDataStart
int GetOverworldSpriteRegionEnd(const Rom &rom)
const char * OverworldSpriteEditStrategyName(OverworldSpriteEditStrategy s)
std::array< std::array< std::optional< OverworldSpriteBytes >, 160 >, 3 > OverworldSpriteEdits
absl::StatusOr< OverworldSpriteSavePlan > PlanOverworldSpriteSave(const Rom &rom, const OverworldSpriteEdits &edits)
OverworldSpriteLayout GetOverworldSpriteLayout(const Rom &rom)
std::vector< OverworldSpriteWrite > OverworldSpriteSavePlan
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< std::pair< int, int > > sharers