yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
lyra_terrain_source.cc
Go to the documentation of this file.
2
3#include <string>
4#include <utility>
5#include <vector>
6
7#include "absl/strings/str_format.h"
10#include "nlohmann/json.hpp"
11#include "util/macro.h"
12#include "zelda3/game_data.h"
15
16namespace yaze::zelda3 {
17namespace {
18void AddWord(std::string& bytes, uint16_t word) {
19 bytes.push_back(static_cast<char>(word));
20 bytes.push_back(static_cast<char>(word >> 8));
21}
22} // namespace
23
24absl::Status BindLyraTerrainSource(Overworld& overworld,
26 gfx::Bitmap* atlas) {
27 if (!profile)
28 return absl::InvalidArgumentError("Lyra profile output is required");
29 if (!overworld.is_loaded() || overworld.is_lyra_preview())
30 return absl::FailedPreconditionError(
31 "Open the main project ROM to draw Lyra terrain");
32 const auto* donor = overworld.mutable_overworld_map(0x3A);
33 if (!donor)
34 return absl::FailedPreconditionError("Loom map $3A is unavailable");
35 const auto* game_data = donor->game_data();
36 if (!game_data || !game_data->gfx_groups_loaded ||
37 game_data->palette_groups.grass.empty() ||
38 game_data->palette_groups.grass[0].size() < 3 ||
39 game_data->palette_groups.hud.empty() ||
40 game_data->palette_groups.hud[0].size() < 32)
41 return absl::FailedPreconditionError(
42 "Load native graphics and palettes before drawing Lyra terrain");
43 for (const auto sheet : profile->graphics_sheets) {
44 if ((static_cast<size_t>(sheet) + 1) * 0x1000 >
45 game_data->graphics_buffer.size())
46 return absl::FailedPreconditionError(
47 "Loom native graphics sheets are incomplete");
48 }
49 if ((static_cast<size_t>(profile->animated_sheet) + 1) * 0x1000 >
50 game_data->graphics_buffer.size())
51 return absl::FailedPreconditionError(
52 "Loom native animation sheet is incomplete");
53 // Work with a copy: building this atlas cannot alter normal map selection,
54 // properties, dirty flags, or renderer caches.
55 auto source = *donor;
56 source.set_game_state(0);
57 source.LoadAreaGraphics();
58 for (size_t i = 0; i < profile->graphics_sheets.size(); ++i) {
59 if (source.static_graphics(i) != profile->graphics_sheets[i])
60 return absl::FailedPreconditionError(absl::StrFormat(
61 "Loom graphics mismatch at slot %d: expected $%02X, found $%02X", i,
62 profile->graphics_sheets[i], source.static_graphics(i)));
63 }
64 if (source.render_main_palette() != profile->main_palette ||
65 source.area_palette() != profile->palette_set ||
66 source.animated_sheet() != profile->animated_sheet)
67 return absl::FailedPreconditionError(
68 "Loom palette or animation profile mismatch");
69 RETURN_IF_ERROR(source.BuildTileset());
70 RETURN_IF_ERROR(source.LoadPalette());
71 if (source.current_palette().size() != 256)
72 return absl::FailedPreconditionError("Loom resolved palette is incomplete");
73 if (overworld.tiles16().empty() || overworld.tiles16().size() > 4096)
74 return absl::FailedPreconditionError(
75 "Loom Tile16 definitions exceed native atlas capacity");
76 profile->tile_count = overworld.tiles16().size();
77 std::string definitions;
78 for (const auto& tile : overworld.tiles16())
79 for (const auto& quadrant : tile.tiles_info)
80 AddWord(definitions, gfx::TileInfoToWord(quadrant));
83 const auto& pixels = source.current_graphics();
85 {reinterpret_cast<const char*>(pixels.data()), pixels.size()});
86 std::string colors;
87 for (size_t i = 0; i < source.current_palette().size(); ++i)
88 AddWord(colors, source.current_palette()[i].snes());
90 // Include the animation selector and resolved animated pixels (slot 7).
91 std::string animation(1, static_cast<char>(source.animated_gfx()));
92 if (pixels.size() < 0x8000)
93 return absl::FailedPreconditionError(
94 "Loom decoded graphics are incomplete");
95 animation.append(reinterpret_cast<const char*>(pixels.data() + 0x7000),
96 0x1000);
98 RETURN_IF_ERROR(profile->Validate());
99 if (atlas) {
100 RETURN_IF_ERROR(source.BuildTiles16Gfx(*overworld.mutable_tiles16(),
101 profile->tile_count));
102 const int height = ((profile->tile_count + 7) / 8) * 16;
103 const auto& blockset = source.current_tile16_blockset();
104 const size_t atlas_bytes = static_cast<size_t>(128) * height;
105 if (blockset.size() < atlas_bytes)
106 return absl::FailedPreconditionError(
107 "Loom Tile16 atlas pixels are incomplete");
108 std::vector<uint8_t> pixels(blockset.begin(),
109 blockset.begin() + atlas_bytes);
110 atlas->Create(128, height, 8, pixels);
111 atlas->SetPalette(source.current_palette());
112 }
113 return absl::OkStatus();
114}
115
116std::string LyraMapAssetKey(const core::LyraMapProperties& p, int state) {
117 std::string key;
118 for (auto id : p.graphics_sheets)
119 key.push_back(id);
120 key.push_back(p.main_palette);
121 key.push_back(p.palette_set);
122 key.push_back(p.animated_sheet);
123 for (auto id : p.sprite_graphics)
124 AddWord(key, id + 1);
125 for (auto id : p.sprite_palettes)
126 AddWord(key, id + 1);
127 key.push_back(state);
128 // Preserve legacy keys exactly when no project override is present.
129 if (p.background_palette) {
130 key.append("BG");
131 for (const auto color : *p.background_palette)
132 AddWord(key, color);
133 }
134 return key;
135}
136
138 const core::LyraMapProperties& properties, gfx::SnesPalette& palette) {
139 if (!properties.background_palette)
140 return absl::OkStatus();
141 if (palette.size() != 256)
142 return absl::FailedPreconditionError("Lyra resolved palette is incomplete");
143 // Validate the complete override before changing even the palette copy.
144 for (const auto color : *properties.background_palette) {
145 if (color > 0x7FFF)
146 return absl::InvalidArgumentError(
147 "Lyra background palette colors require BGR555 values 0..32767");
148 }
149 for (size_t i = 0; i < properties.background_palette->size(); ++i) {
150 gfx::SnesColor color((*properties.background_palette)[i]);
151 // These are display colors; retain native transparency and edit metadata.
152 color.set_transparent(palette[i].is_transparent());
153 color.set_modified(palette[i].is_modified());
154 palette[i] = color;
155 }
156 return absl::OkStatus();
157}
158
159absl::StatusOr<LyraNativeMapAssets> ResolveLyraMapAssets(
160 Overworld& overworld, const core::LyraMapProperties& p, int state) {
161 if (!overworld.is_loaded() || overworld.is_lyra_preview())
162 return absl::FailedPreconditionError("Lyra native source is unavailable");
163 const auto* donor = overworld.overworld_map(0x3A);
164 if (!donor || state < 0 || state > 2 || !donor->game_data())
165 return absl::FailedPreconditionError("Lyra native source is unavailable");
166 const auto& data = *donor->game_data();
167 if (p.main_palette >= data.palette_groups.overworld_main.size() ||
168 p.palette_set > 0xA3 || data.palette_groups.overworld_aux.size() < 20 ||
169 data.palette_groups.overworld_animated.size() < 14 ||
170 data.palette_groups.sprites_aux3.size() < 24 ||
171 data.palette_groups.armors.empty() ||
172 data.palette_groups.armors[0].size() < 15)
173 return absl::OutOfRangeError(
174 "Lyra palette selector or palette source is unavailable");
175 auto source = *donor;
176 source.ClearInheritedAreaProperties();
177 source.set_game_state(state);
178 for (int i = 0; i < 8; ++i)
179 source.set_custom_tileset(i, p.graphics_sheets[i]);
180 source.set_main_palette(p.main_palette);
181 source.set_area_palette(p.palette_set);
182 source.set_animated_gfx(p.animated_sheet);
183 for (int i = 0; i < 3; ++i) {
184 if (p.sprite_graphics[i] >= 0) {
185 if (p.sprite_graphics[i] >= static_cast<int>(data.spriteset_ids.size()))
186 return absl::OutOfRangeError("Lyra sprite set is unavailable");
187 source.set_sprite_graphics(i, p.sprite_graphics[i]);
188 }
189 if (p.sprite_palettes[i] >= 0)
190 source.set_sprite_palette(i, p.sprite_palettes[i]);
191 }
192 const auto sprite_set = source.sprite_graphics(state);
193 if (sprite_set >= data.spriteset_ids.size())
194 return absl::OutOfRangeError("Inherited Lyra sprite set is unavailable");
195 const auto sprites = data.spriteset_ids[sprite_set];
196 for (auto id : sprites) {
197 if (static_cast<int>(id) + 0x73 > 0xFF)
198 return absl::OutOfRangeError(
199 "Lyra sprite set overflows the native graphics sheet range");
200 }
201 source.LoadAreaGraphics();
202 for (int i = 0; i < 16; ++i) {
203 if ((static_cast<size_t>(source.static_graphics(i)) + 1) * 0x1000 >
204 data.graphics_buffer.size())
205 return absl::OutOfRangeError(
206 "Lyra resolved graphics sheet is unavailable");
207 }
208 if ((static_cast<size_t>(source.animated_sheet()) + 1) * 0x1000 >
209 data.graphics_buffer.size())
210 return absl::OutOfRangeError("Lyra animated sheet is unavailable");
211 RETURN_IF_ERROR(source.BuildTileset());
212 // BuildTileset's legacy zero sentinel predates explicit v3 sheet selectors.
213 // Project $00 selects real sheet zero; preserve slot 7's animated top half.
214 for (int slot = 0; slot < 8; ++slot) {
215 if (p.graphics_sheets[slot] != 0)
216 continue;
217 auto* pixels = source.mutable_current_graphics();
218 for (int offset = slot == 7 ? 0x800 : 0; offset < 0x1000; ++offset) {
219 const int bias =
220 (slot == 0 || slot == 3 || slot == 4 || slot == 5) ? 0x88 : 0;
221 (*pixels)[slot * 0x1000 + offset] = data.graphics_buffer[offset] + bias;
222 }
223 }
224 RETURN_IF_ERROR(source.LoadPalette());
225 if (source.current_palette().size() != 256)
226 return absl::FailedPreconditionError("Lyra resolved palette is incomplete");
227 if (p.background_palette)
229 p, *source.mutable_current_palette()));
230 const auto& pixels = source.current_graphics();
231 std::string binding = LyraMapAssetKey(p, state);
232 binding.append(reinterpret_cast<const char*>(pixels.data()), pixels.size());
233 for (size_t i = 0; i < source.current_palette().size(); ++i)
234 AddWord(binding, source.current_palette()[i].snes());
235 return LyraNativeMapAssets{std::move(source),
237}
238
239absl::StatusOr<std::string> DescribeLyraNativeMapAssets(
240 const core::LyraMapProperties& p, int state,
241 const LyraNativeMapAssets& assets) {
242 const auto& source = assets.source;
243 if (state < 0 || state > 2 || source.game_state() != state)
244 return absl::InvalidArgumentError(
245 "Lyra inspection state does not match assets");
246 const auto* data = source.game_data();
247 const auto& pixels = source.current_graphics();
248 const auto& palette = source.current_palette();
249 if (!data || source.HasGraphicsPreview() || pixels.size() != 0x10000 ||
250 palette.size() != 256)
251 return absl::FailedPreconditionError(
252 "Lyra inspection requires complete native resolved assets");
253 for (int slot = 0; slot < 16; ++slot) {
254 if ((static_cast<size_t>(source.static_graphics(slot)) + 1) * 0x1000 >
255 data->graphics_buffer.size())
256 return absl::OutOfRangeError(
257 "Lyra inspection source sheet is unavailable");
258 }
259 if ((static_cast<size_t>(source.animated_sheet()) + 1) * 0x1000 >
260 data->graphics_buffer.size())
261 return absl::OutOfRangeError(
262 "Lyra inspection animation sheet is unavailable");
263 if (source.animated_sheet() == 0)
264 return absl::FailedPreconditionError(
265 "Lyra inspection has no resolved animation sheet");
266 const auto effective_sheet = [&](int slot) {
267 // The resolver's explicit-zero repair wins over the legacy loader's slot.
268 return slot < 8 && p.graphics_sheets[slot] == 0
269 ? 0
270 : static_cast<int>(source.static_graphics(slot));
271 };
272 const auto empty_slot = [&](int slot) {
273 return source.static_graphics(slot) == 0 &&
274 !(slot < 8 && p.graphics_sheets[slot] == 0);
275 };
276
277 // A copied map can retain sheet overrides independently of GraphicsPreview.
278 // Require the actual rendered bytes to agree with the native source whose
279 // identity we export. Do not rebuild or mutate the supplied resolver result.
280 for (int slot = 0; slot < 16; ++slot) {
281 const size_t sheet = effective_sheet(slot);
282 const int bias =
283 slot == 0 || slot == 3 || slot == 4 || slot == 5 ? 0x88 : 0;
284 for (size_t offset = 0; offset < 0x1000; ++offset) {
285 uint8_t expected = 0;
286 if (slot == 7 && offset < 0x800) {
287 expected =
288 data->graphics_buffer[static_cast<size_t>(source.animated_sheet()) *
289 0x1000 +
290 offset];
291 } else if (sheet != 0 || (slot < 8 && p.graphics_sheets[slot] == 0)) {
292 // Explicit saved BG sheet zero is real, unlike BuildTileset's legacy
293 // empty-slot sentinel; ResolveLyraMapAssets repairs it after the build.
294 expected = static_cast<uint8_t>(
295 data->graphics_buffer[sheet * 0x1000 + offset] + bias);
296 }
297 if (pixels[slot * 0x1000 + offset] != expected)
298 return absl::FailedPreconditionError(
299 "Lyra inspection pixels do not match native source sheets");
300 }
301 }
302
303 std::string colors;
304 auto words = nlohmann::json::array();
305 for (size_t i = 0; i < palette.size(); ++i) {
306 words.push_back(palette[i].snes());
307 AddWord(colors, palette[i].snes());
308 }
309 std::string binding = LyraMapAssetKey(p, state);
310 binding.append(reinterpret_cast<const char*>(pixels.data()), pixels.size());
311 binding.append(colors);
312 if (core::ComputeSourceArtifactSha256(binding) != assets.binding)
313 return absl::FailedPreconditionError(
314 "Lyra inspection native binding does not match resolved assets");
315
316 const auto pixel_hash = [&](size_t offset, size_t count) {
318 {reinterpret_cast<const char*>(pixels.data() + offset), count});
319 };
320 const auto sheet_hash = [&](int sheet) {
322 {reinterpret_cast<const char*>(data->graphics_buffer.data() +
323 static_cast<size_t>(sheet) * 0x1000),
324 0x1000});
325 };
326 const auto context = DescribeOverworldGraphicsContext(source);
327 auto slots = nlohmann::json::array();
328 for (int slot = 0; slot < 16; ++slot) {
329 const bool empty = empty_slot(slot);
330 nlohmann::json entry = {
331 {"slot", slot},
332 {"sheet_id", empty ? nlohmann::json(nullptr)
333 : nlohmann::json(effective_sheet(slot))},
334 {"loader_sheet_id", source.static_graphics(slot)},
335 {"source", empty ? "native_empty_slot" : "native_rom_decoded_sheet"},
336 {"role", slot < 8 ? "background" : "sprite"},
337 {"decoded_byte_count", empty ? 0 : 0x1000},
338 {"decoded_format", empty ? "none" : "indexed8"},
339 {"decoded_sheet_sha256",
340 empty ? nlohmann::json(nullptr)
341 : nlohmann::json(sheet_hash(effective_sheet(slot)))},
342 {"resolved_offset", slot * 0x1000},
343 {"resolved_byte_count", 0x1000},
344 {"resolved_slot_sha256", pixel_hash(slot * 0x1000, 0x1000)},
345 {"palette_index_bias",
346 slot == 0 || slot == 3 || slot == 4 || slot == 5 ? 0x88 : 0},
347 {"composite", slot == 7},
348 };
349 if (context.slots.size() == 16) {
350 entry["loader_role"] = context.slots[slot].role;
351 entry["loader_source"] = context.slots[slot].source;
352 entry["inherited_sheet"] = context.slots[slot].inherited_sheet;
353 entry["loader_agrees"] = context.slots[slot].loader_agrees;
354 }
355 slots.push_back(std::move(entry));
356 }
357 nlohmann::json saved = {
358 {"graphics_sheets", p.graphics_sheets},
359 {"main_palette", p.main_palette},
360 {"palette_set", p.palette_set},
361 {"animated_sheet", p.animated_sheet},
362 {"sprite_graphics", p.sprite_graphics},
363 {"sprite_palettes", p.sprite_palettes},
364 };
365 nlohmann::json bg_words = nullptr;
366 nlohmann::json bg_hash = nullptr;
367 if (p.background_palette) {
368 std::string bg_bytes;
369 for (auto word : *p.background_palette)
370 AddWord(bg_bytes, word);
371 saved["background_palette"] = *p.background_palette;
372 bg_words = *p.background_palette;
373 bg_hash = core::ComputeSourceArtifactSha256(bg_bytes);
374 }
375 nlohmann::json document = {
376 {"schema", "yaze.lyra-native-resolution.v1"},
377 {"resolution_mode", "native-only"},
378 {"inspection_only", true},
379 {"resolver_seed_map", 0x3A},
380 {"seed_semantics", "resolver seed; not gameplay parent"},
381 {"state", state},
382 {"saved_properties", std::move(saved)},
383 {"native_base_binding", assets.binding},
384 {"binding_algorithm", "yaze.lyra-native-binding.legacy-v1"},
385 {"runtime_assignment", "unassigned"},
386 {"runtime_assignment_scope",
387 "inspection does not assign catalog or areas"},
388 {"custom_catalog_binding", nullptr},
389 {"custom_transfers", nlohmann::json::array()},
390 {"effective_sprite_graphics", source.sprite_graphics(state)},
391 {"effective_sprite_palette", source.sprite_palette(state)},
392 {"slots", std::move(slots)},
393 {"graphics",
394 {{"format", "indexed8"},
395 {"byte_count", pixels.size()},
396 {"sha256", pixel_hash(0, pixels.size())}}},
397 {"palette",
398 {{"words", std::move(words)},
399 {"encoding", "256 uint16 little-endian SNES words"},
400 {"byte_count", colors.size()},
401 {"uint16le_sha256", core::ComputeSourceArtifactSha256(colors)},
402 {"background_override_words", std::move(bg_words)},
403 {"background_override_uint16le_sha256", std::move(bg_hash)},
404 {"background_source", p.background_palette ? "saved_bg128" : "native"},
405 {"runtime_consumer",
406 p.background_palette ? "unsupported" : "unassigned"}}},
407 {"animation",
408 {{"sheet_id", source.animated_sheet()},
409 {"decoded_sheet_sha256", sheet_hash(source.animated_sheet())},
410 {"decoded_byte_count", 0x1000},
411 {"frame", 0},
412 {"source_offset", 0},
413 {"destination_offset", 0x7000},
414 {"byte_count", 0x800},
415 {"frame0_sha256", pixel_hash(0x7000, 0x800)},
416 {"composite_slot7_sha256", pixel_hash(0x7000, 0x1000)},
417 {"static_lower_sheet_id", empty_slot(7)
418 ? nlohmann::json(nullptr)
419 : nlohmann::json(effective_sheet(7))},
420 {"static_lower_source",
421 empty_slot(7) ? "native_empty_slot" : "native_rom_decoded_sheet"},
422 {"static_lower_source_offset",
423 empty_slot(7) ? nlohmann::json(nullptr) : nlohmann::json(0x800)},
424 {"static_lower_destination_offset", 0x7800},
425 {"static_lower_byte_count", 0x800},
426 {"sequence_status", "frame0_only; runtime sequencing not resolved"}}},
427 {"context", nlohmann::json::parse(context.ToJson())},
428 {"context_scope", "native resolver seed; not authored map or area"},
429 };
430 return document.dump();
431}
432
433} // namespace yaze::zelda3
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
SNES Color container.
Definition snes_color.h:110
constexpr void set_modified(bool m)
Definition snes_color.h:198
constexpr void set_transparent(bool t)
Definition snes_color.h:197
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
const std::vector< gfx::Tile16 > & tiles16() const
Definition overworld.h:784
bool is_lyra_preview() const
Definition overworld.h:444
auto is_loaded() const
Definition overworld.h:836
auto overworld_map(int i) const
Definition overworld.h:770
auto mutable_overworld_map(int i)
Definition overworld.h:776
std::string ComputeSourceArtifactSha256(std::string_view content)
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
Zelda 3 specific classes and functions.
absl::StatusOr< std::string > DescribeLyraNativeMapAssets(const core::LyraMapProperties &p, int state, const LyraNativeMapAssets &assets)
std::string LyraMapAssetKey(const core::LyraMapProperties &p, int state)
absl::StatusOr< LyraNativeMapAssets > ResolveLyraMapAssets(Overworld &overworld, const core::LyraMapProperties &p, int state)
absl::Status ApplyLyraBackgroundPaletteOverride(const core::LyraMapProperties &properties, gfx::SnesPalette &palette)
absl::Status BindLyraTerrainSource(Overworld &overworld, core::LyraTerrainProfile *profile, gfx::Bitmap *atlas)
ResolvedGraphicsContext DescribeOverworldGraphicsContext(const OverworldMap &map, const GraphicsContextIdentity &identity)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::optional< std::array< uint16_t, 128 > > background_palette
std::array< uint8_t, 8 > graphics_sheets
std::array< uint8_t, 8 > graphics_sheets
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
std::vector< uint8_t > & graphics_buffer
Legacy name for sheet_store.mutable_pixels().