yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
menu_tilemap_sources.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/ascii.h"
9#include "absl/strings/match.h"
10#include "absl/strings/numbers.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
16#include "rom/rom.h"
17#include "rom/snes.h"
18#include "util/hex.h"
19#include "util/macro.h"
20#include "zelda3/game_data.h"
21
22namespace yaze::zelda3 {
23namespace {
24
25// Both the ROM-symbol source and the raw-file source ultimately provide a
26// flat sequence of SNES color words that starts at "CGRAM color 1" (the
27// menu upload's +1 offset -- see menu_tilemap_sources.h). Given that
28// sequence, sub-palette p / color c (c=1..3) is table[p*4 + c - 1] and
29// color 0 of every sub-palette is always transparent (an SNES BG
30// convention, independent of whatever happens to be stored at that CGRAM
31// slot), so it doesn't matter what we put there.
32std::array<gfx::SnesColor, 32> BuildSubpaletteArray(
33 const std::vector<uint16_t>& table_words) {
34 std::array<gfx::SnesColor, 32> out{};
35 for (int i = 0; i < 32; ++i) {
36 if (i % 4 == 0 || i - 1 >= static_cast<int>(table_words.size())) {
37 out[i] = gfx::SnesColor(); // transparent slot; value is irrelevant
38 continue;
39 }
40 out[i] = gfx::SnesColor(table_words[i - 1]);
41 }
42 return out;
43}
44
45uint16_t ReadWordLe(const uint8_t* p) {
46 return static_cast<uint16_t>(p[0] | (p[1] << 8));
47}
48
49// menu_palette.asm defines exactly 31 entries (8 sub-palettes x 3 colors +
50// the unused final slot); accept up to 32 so a 32-word table also parses.
51constexpr size_t kMenuPaletteMinEntries = 31;
52constexpr size_t kMenuPaletteMaxEntries = 32;
53
54// asar-style number: $HEX, 0xHEX or decimal. nullopt if it isn't one.
55// absl::string_view and util::ParseHexString: Ubuntu 22.04's system Abseil
56// (20210324) has its own string_view and predates SimpleHexAtoi.
57std::optional<uint32_t> ParseAsmNumber(absl::string_view s) {
58 s = absl::StripAsciiWhitespace(s);
59 if (s.empty())
60 return std::nullopt;
61 uint64_t value = 0;
62 if (s.front() == '$' || absl::StartsWithIgnoreCase(s, "0x")) {
63 if (!util::ParseHexString(s, &value))
64 return std::nullopt;
65 } else if (!absl::SimpleAtoi(s, &value)) {
66 return std::nullopt;
67 }
68 if (value > 0xFFFFFFFFull)
69 return std::nullopt;
70 return static_cast<uint32_t>(value);
71}
72
73// One operand of a `dw`: hexto555($RRGGBB) or a raw 16-bit word.
74absl::StatusOr<uint16_t> ParseDwOperand(absl::string_view op) {
75 op = absl::StripAsciiWhitespace(op);
76 if (op.empty()) {
77 return absl::InvalidArgumentError("empty dw operand (stray comma?)");
78 }
79 constexpr absl::string_view kMacro = "hexto555";
80 if (absl::StartsWithIgnoreCase(op, kMacro)) {
81 absl::string_view rest =
82 absl::StripAsciiWhitespace(op.substr(kMacro.size()));
83 if (rest.size() < 2 || rest.front() != '(' || rest.back() != ')') {
84 return absl::InvalidArgumentError(absl::StrFormat(
85 "expected hexto555($RRGGBB), got '%s'", std::string(op)));
86 }
87 absl::string_view inner = rest.substr(1, rest.size() - 2);
88 std::optional<uint32_t> rgb = ParseAsmNumber(inner);
89 if (!rgb.has_value() || *rgb > 0xFFFFFF) {
90 return absl::InvalidArgumentError(
91 absl::StrFormat("hexto555 argument '%s' is not a $RRGGBB value",
92 std::string(absl::StripAsciiWhitespace(inner))));
93 }
94 return Hexto555(*rgb);
95 }
96 std::optional<uint32_t> word = ParseAsmNumber(op);
97 if (!word.has_value()) {
98 return absl::InvalidArgumentError(absl::StrFormat(
99 "unsupported dw operand '%s' (expected hexto555($RRGGBB) or a "
100 "$XXXX word)",
101 std::string(op)));
102 }
103 if (*word > 0xFFFF) {
104 return absl::InvalidArgumentError(absl::StrFormat(
105 "dw operand '%s' does not fit in 16 bits", std::string(op)));
106 }
107 return static_cast<uint16_t>(*word);
108}
109
110bool IsDwDirective(absl::string_view line, absl::string_view* operands) {
111 size_t end = line.find_first_of(" \t");
112 absl::string_view token = line.substr(0, end);
113 if (!absl::EqualsIgnoreCase(token, "dw"))
114 return false;
115 *operands =
116 end == absl::string_view::npos ? absl::string_view() : line.substr(end);
117 return true;
118}
119
120} // namespace
121
122uint16_t Hexto555(uint32_t rgb) {
123 const uint32_t r = (rgb >> 16) & 0xFF;
124 const uint32_t g = (rgb >> 8) & 0xFF;
125 const uint32_t b = rgb & 0xFF;
126 return static_cast<uint16_t>(((b / 8) << 10) | ((g / 8) << 5) | (r / 8));
127}
128
129absl::StatusOr<std::vector<uint16_t>> ParseMenuPaletteAsmText(
130 std::string_view text_in, std::string_view label_in) {
131 const absl::string_view text(text_in.data(), text_in.size());
132 const absl::string_view label(label_in.data(), label_in.size());
133 std::vector<uint16_t> words;
134 bool in_table = false;
135 int label_line = 0;
136 int last_table_line = 0;
137 int line_no = 0;
138 bool done = false;
139
140 for (absl::string_view raw : absl::StrSplit(text, '\n')) {
141 ++line_no;
142 // Strip the `;` comment, then whitespace (this also drops a CRLF's \r).
143 absl::string_view line =
144 absl::StripAsciiWhitespace(raw.substr(0, raw.find(';')));
145 if (line.empty())
146 continue;
147
148 if (!in_table) {
149 size_t end = line.find_first_of(": \t");
150 if (line.substr(0, end) != label)
151 continue;
152 in_table = true;
153 label_line = line_no;
154 absl::string_view rest = end == absl::string_view::npos
155 ? absl::string_view()
156 : line.substr(end);
157 if (!rest.empty() && rest.front() == ':')
158 rest.remove_prefix(1);
159 line = absl::StripAsciiWhitespace(rest);
160 if (line.empty())
161 continue; // `Menu_Palette:` alone on its line
162 }
163
164 absl::string_view operands;
165 if (!IsDwDirective(line, &operands)) {
166 break; // first non-`dw` line ends the table
167 }
168 last_table_line = line_no;
169 for (absl::string_view op : absl::StrSplit(operands, ',')) {
170 absl::StatusOr<uint16_t> word = ParseDwOperand(op);
171 if (!word.ok()) {
172 return absl::InvalidArgumentError(
173 absl::StrFormat("line %d: %s", line_no, word.status().message()));
174 }
175 words.push_back(*word);
176 if (words.size() >= kMenuPaletteMaxEntries) {
177 done = true;
178 break;
179 }
180 }
181 if (done)
182 break;
183 }
184
185 if (!in_table) {
186 return absl::NotFoundError(
187 absl::StrFormat("label '%s' not found", std::string(label)));
188 }
189 if (words.size() < kMenuPaletteMinEntries) {
190 return absl::InvalidArgumentError(absl::StrFormat(
191 "line %d: '%s' has only %zu dw entries; at least %zu are needed "
192 "(8 sub-palettes x 3 colors + the unused final slot)",
193 last_table_line > 0 ? last_table_line : label_line, std::string(label),
194 words.size(), kMenuPaletteMinEntries));
195 }
196 return words;
197}
198
199absl::StatusOr<std::array<gfx::SnesColor, 32>> ResolveMenuPaletteFromAsm(
200 const std::string& path, const std::string& label) {
201 std::ifstream in(path, std::ios::binary);
202 if (!in) {
203 return absl::NotFoundError(
204 absl::StrFormat("could not open '%s' for reading", path));
205 }
206 std::ostringstream buffer;
207 buffer << in.rdbuf();
208 const std::string text = buffer.str();
209
210 absl::StatusOr<std::vector<uint16_t>> words =
211 ParseMenuPaletteAsmText(text, label);
212 constexpr absl::string_view kSymbolPrefix = "Oracle_";
213 if (!words.ok() && absl::IsNotFound(words.status()) &&
214 absl::StartsWith(label, kSymbolPrefix)) {
215 words = ParseMenuPaletteAsmText(text, label.substr(kSymbolPrefix.size()));
216 }
217 if (!words.ok()) {
218 return absl::Status(words.status().code(),
219 absl::StrCat(path, ": ", words.status().message()));
220 }
221 return BuildSubpaletteArray(*words);
222}
223
224std::optional<std::string> FindMenuPaletteAsm(const std::string& project_dir,
225 const std::string& code_folder) {
226 namespace fs = std::filesystem;
227 std::vector<fs::path> candidates;
228 if (!project_dir.empty()) {
229 candidates.push_back(fs::path(project_dir) / "Menu" / "menu_palette.asm");
230 }
231 if (!code_folder.empty()) {
232 fs::path code(code_folder);
233 if (code.is_absolute()) {
234 candidates.push_back(code / "Menu" / "menu_palette.asm");
235 } else if (!project_dir.empty()) {
236 candidates.push_back(fs::path(project_dir) / code / "Menu" /
237 "menu_palette.asm");
238 }
239 }
240 for (const fs::path& candidate : candidates) {
241 std::error_code ec;
242 if (fs::is_regular_file(candidate, ec) && !ec) {
243 return candidate.string();
244 }
245 }
246 return std::nullopt;
247}
248
249absl::StatusOr<std::array<gfx::SnesColor, 32>> ResolveMenuPaletteFromSymbol(
250 const Rom& rom, const emu::debug::SymbolProvider& symbols,
251 const std::string& label) {
252 std::optional<emu::debug::Symbol> symbol = symbols.FindSymbol(label);
253 if (!symbol.has_value()) {
254 return absl::NotFoundError(absl::StrFormat(
255 "symbol '%s' not found in the loaded symbol table", label));
256 }
257 uint32_t pc = SnesToPc(symbol->address);
258 // Table is 32 words = 64 bytes, matching Oracle's Menu_Palette.
259 ASSIGN_OR_RETURN(std::vector<uint8_t> raw, rom.ReadByteVector(pc, 64));
260 if (raw.size() < 64) {
261 return absl::OutOfRangeError(absl::StrFormat(
262 "symbol '%s' resolved to PC offset 0x%06X, which is too close to "
263 "the end of the ROM (%zu bytes) to hold a 32-word palette table",
264 label, pc, rom.size()));
265 }
266 std::vector<uint16_t> table_words(32);
267 bool all_zero = true;
268 for (int i = 0; i < 32; ++i) {
269 table_words[i] = ReadWordLe(&raw[i * 2]);
270 if (table_words[i] != 0)
271 all_zero = false;
272 }
273 if (all_zero) {
274 return absl::FailedPreconditionError(absl::StrFormat(
275 "symbol '%s' resolved to PC offset 0x%06X, but every byte there is "
276 "0. For Oracle of Secrets, labels like this are produced by "
277 "assembling the ASM sources and only exist in the *patched* ROM "
278 "(oos168x.sfc) -- the 'clean' base ROM a yaze project normally "
279 "points at (oos168.sfc) does not contain this data. Point this "
280 "source at the patched ROM, or use the HUD-palette or CGRAM-dump "
281 "source instead.",
282 label, pc));
283 }
284 return BuildSubpaletteArray(table_words);
285}
286
287absl::StatusOr<std::array<gfx::SnesColor, 32>> ResolveMenuPaletteFromHud(
288 const GameData& game_data) {
289 if (game_data.palette_groups.hud.empty()) {
290 return absl::FailedPreconditionError(
291 "GameData::palette_groups.hud is empty; load a ROM/GameData first");
292 }
293 std::vector<uint16_t> table_words(32);
294 for (int i = 0; i < 32; ++i) {
295 // hud[0] has up to 32 colors already (see snes_palette.cc); colors
296 // past the group's actual size come back as SnesColor() == 0, which
297 // is harmless (transparent slots don't care, and a short HUD group
298 // simply yields black for palettes it doesn't define).
299 gfx::SnesColor color = game_data.palette_groups.hud.GetColor(0, i);
300 table_words[i] = color.snes();
301 }
302 return BuildSubpaletteArray(table_words);
303}
304
305absl::StatusOr<std::array<gfx::SnesColor, 32>> ResolveMenuPaletteFromFile(
306 const std::string& path, size_t byte_offset) {
307 std::ifstream in(path, std::ios::binary | std::ios::ate);
308 if (!in) {
309 return absl::NotFoundError(
310 absl::StrFormat("could not open '%s' for reading", path));
311 }
312 std::streamsize size = in.tellg();
313 if (size < 0 ||
314 static_cast<size_t>(size) < byte_offset + 62 /* 31 words min */) {
315 return absl::InvalidArgumentError(absl::StrFormat(
316 "'%s' is too small (%lld bytes) to hold 31 colors starting at byte "
317 "offset %zu",
318 path, static_cast<long long>(size), byte_offset));
319 }
320 in.seekg(static_cast<std::streamoff>(byte_offset), std::ios::beg);
321 std::vector<uint8_t> raw(64);
322 size_t available = static_cast<size_t>(size) - byte_offset;
323 size_t to_read = std::min<size_t>(64, available);
324 if (!in.read(reinterpret_cast<char*>(raw.data()),
325 static_cast<std::streamsize>(to_read))) {
326 return absl::InternalError(absl::StrFormat("short read on '%s'", path));
327 }
328 std::vector<uint16_t> table_words(31, 0);
329 for (size_t i = 0; i < table_words.size() && (i * 2 + 1) < to_read; ++i) {
330 table_words[i] = ReadWordLe(&raw[i * 2]);
331 }
332 return BuildSubpaletteArray(table_words);
333}
334
335absl::StatusOr<std::vector<uint8_t>> ResolveMenuChrFromRom(const Rom& rom) {
336 return Load2BppGraphics(rom);
337}
338
339absl::StatusOr<std::vector<uint8_t>> ResolveMenuChrFromFile(
340 const std::string& path) {
341 std::ifstream in(path, std::ios::binary | std::ios::ate);
342 if (!in) {
343 return absl::NotFoundError(
344 absl::StrFormat("could not open '%s' for reading", path));
345 }
346 std::streamsize size = in.tellg();
347 if (size <= 0) {
348 return absl::InvalidArgumentError(absl::StrFormat("'%s' is empty", path));
349 }
350 in.seekg(0, std::ios::beg);
351 std::vector<uint8_t> raw(static_cast<size_t>(size));
352 if (!in.read(reinterpret_cast<char*>(raw.data()), size)) {
353 return absl::InternalError(absl::StrFormat("short read on '%s'", path));
354 }
355 if (raw.size() % 16 != 0) {
356 return absl::InvalidArgumentError(absl::StrFormat(
357 "'%s' is %zu bytes, not a multiple of 16 (one 2bpp 8x8 tile is 16 "
358 "bytes)",
359 path, raw.size()));
360 }
361 // Decode in 0x800-byte (128-tile) chunks, the same chunking
362 // Load2BppGraphics() uses, so MakeChrPixelFn() works uniformly over
363 // either source. A short final chunk is padded with zero tiles.
364 constexpr size_t kChunkBytes = 0x800;
365 size_t num_chunks = (raw.size() + kChunkBytes - 1) / kChunkBytes;
366 std::vector<uint8_t> out;
367 out.reserve(num_chunks * 8192);
368 for (size_t c = 0; c < num_chunks; ++c) {
369 size_t begin = c * kChunkBytes;
370 size_t end = std::min(raw.size(), begin + kChunkBytes);
371 std::vector<uint8_t> chunk(raw.begin() + begin, raw.begin() + end);
372 chunk.resize(kChunkBytes, 0);
373 auto converted = gfx::SnesTo8bppSheet(chunk, 2);
374 out.insert(out.end(), converted.begin(), converted.end());
375 }
376 return out;
377}
378
379} // 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
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:808
auto size() const
Definition rom.h:200
Provider for symbol (label) resolution in disassembly.
std::optional< Symbol > FindSymbol(const std::string &name) const
Find symbol by name.
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
#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
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
std::optional< uint32_t > ParseAsmNumber(absl::string_view s)
bool IsDwDirective(absl::string_view line, absl::string_view *operands)
std::array< gfx::SnesColor, 32 > BuildSubpaletteArray(const std::vector< uint16_t > &table_words)
absl::StatusOr< uint16_t > ParseDwOperand(absl::string_view op)
Zelda 3 specific classes and functions.
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromHud(const GameData &game_data)
absl::StatusOr< std::vector< uint8_t > > Load2BppGraphics(const Rom &rom)
Loads 2BPP graphics sheets from ROM.
Definition game_data.cc:638
uint16_t Hexto555(uint32_t rgb)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromFile(const std::string &path, size_t byte_offset)
absl::StatusOr< std::vector< uint8_t > > ResolveMenuChrFromRom(const Rom &rom)
std::optional< std::string > FindMenuPaletteAsm(const std::string &project_dir, const std::string &code_folder)
absl::StatusOr< std::vector< uint16_t > > ParseMenuPaletteAsmText(std::string_view text_in, std::string_view label_in)
absl::StatusOr< std::vector< uint8_t > > ResolveMenuChrFromFile(const std::string &path)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromAsm(const std::string &path, const std::string &label)
absl::StatusOr< std::array< gfx::SnesColor, 32 > > ResolveMenuPaletteFromSymbol(const Rom &rom, const emu::debug::SymbolProvider &symbols, const std::string &label)
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
SnesColor GetColor(int palette_index, int color_index) const
Get a specific color from a palette.
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95