yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
resolved_graphics_context.cc
Go to the documentation of this file.
2
3#include <string_view>
4
5#include "absl/strings/str_format.h"
9#include "nlohmann/json.hpp"
10#include "zelda3/dungeon/room.h"
11#include "zelda3/game_data.h"
16
17namespace yaze::zelda3 {
18namespace {
19
23using gfx::SheetRole;
24
25std::string RoleName(SheetRole role) {
26 switch (role) {
27 case SheetRole::kOverworldMain:
28 return "overworld_main";
29 case SheetRole::kOverworldGfx:
30 return "overworld_gfx";
31 case SheetRole::kOverworldAreaAux1:
32 return "overworld_area_aux1";
33 case SheetRole::kOverworldAreaAux2:
34 return "overworld_area_aux2";
35 case SheetRole::kOverworldAnimated:
36 return "overworld_animated";
37 case SheetRole::kSpriteAux1:
38 return "sprite_aux1";
39 case SheetRole::kHud:
40 return "hud";
41 case SheetRole::kDungeonMain:
42 return "dungeon_main";
43 case SheetRole::kUnclassified:
44 return "unclassified";
45 }
46 return "unclassified";
47}
48
49std::string PaletteGroupFor(SheetRole role) {
50 return std::string(DefaultBindingFor(role).palette_group_name);
51}
52
53uint64_t MixRevision(uint64_t revision, int slot, uint64_t sheet_revision) {
54 revision ^=
55 sheet_revision + 0x9e3779b97f4a7c15ULL * static_cast<uint64_t>(slot + 1);
56 return revision;
57}
58
59uint64_t SheetRevision(const GameData* game_data, int sheet_id) {
60 if (game_data == nullptr || sheet_id < 0 ||
62 return 0;
63 }
64 return game_data->sheet_store.Revision(static_cast<uint16_t>(sheet_id));
65}
66
68 slot->width = static_cast<int>(GraphicsSheetStore::kSheetWidth);
69 slot->height = static_cast<int>(GraphicsSheetStore::kSheetHeight);
70}
71
72std::string RomSha256(const Rom& rom) {
73 const auto& bytes = rom.vector();
74 return core::ComputeSourceArtifactSha256(std::string_view(
75 reinterpret_cast<const char*>(bytes.data()), bytes.size()));
76}
77
78void AttachIdentity(ResolvedGraphicsContext* context, const Rom* rom,
79 const GraphicsContextIdentity& identity,
80 std::string_view surface_bytes) {
81 context->project_name = identity.project_name;
82 if (rom != nullptr && rom->is_loaded()) {
83 context->rom_sha256 = RomSha256(*rom);
84 }
85 context->graphics_fingerprint = core::TerrainFingerprint(surface_bytes);
86 if (identity.terrain_catalog == nullptr) {
87 context->terrain_catalog = "not_attached";
88 return;
89 }
90 context->terrain_catalog = "attached";
91 const auto& document = identity.terrain_catalog->document();
92 if (!document.contains("entries") || !document["entries"].is_array()) {
93 return;
94 }
95 for (const auto& entry : document["entries"]) {
96 if (!entry.contains("bindings") || !entry["bindings"].is_array()) {
97 continue;
98 }
99 for (const auto& binding : entry["bindings"]) {
100 if (binding.contains("adapter") &&
101 binding["adapter"] == "alttp.overworld.tile16.v1") {
102 ++context->terrain_adapter_entries;
103 break;
104 }
105 }
106 }
107}
108
109nlohmann::json SlotJson(const ResolvedGraphicsSlot& slot) {
110 nlohmann::json json = {
111 {"slot", slot.slot},
112 {"role", slot.role},
113 {"palette_group", slot.palette_group},
114 {"sheet_id", slot.sheet_id},
115 {"source", slot.source},
116 {"inherited_sheet", slot.inherited_sheet},
117 {"loader_agrees", slot.loader_agrees},
118 {"store_revision", std::to_string(slot.store_revision)},
119 {"width", slot.width},
120 {"height", slot.height},
121 {"composite", slot.composite},
122 };
123 if (slot.composite) {
124 json["composite_upper_sheet"] = slot.composite_upper_sheet;
125 json["composite_upper_source"] = slot.composite_upper_source;
126 json["composite_upper_revision"] =
127 std::to_string(slot.composite_upper_revision);
128 }
129 return json;
130}
131
132bool DeathMountain(int area) {
133 return area == 0x03 || area == 0x05 || area == 0x07 || area == 0x43 ||
134 area == 0x45 || area == 0x47;
135}
136
137} // namespace
138
140 switch (group) {
142 return "dungeon_main";
144 return "ow_main";
146 return "ow_aux";
147 }
148 return "";
149}
150
151absl::StatusOr<gfx::SnesColor> ReadPaletteColor(
152 const GameData& owner, const PaletteColorAddress& address) {
153 const auto* group =
155 if (!group || address.palette_index < 0 ||
156 address.palette_index >= static_cast<int>(group->size())) {
157 return absl::OutOfRangeError("Palette address group/index is unavailable");
158 }
159 const auto& palette = group->palette_ref(address.palette_index);
160 if (address.color_index < 0 ||
161 address.color_index >= static_cast<int>(palette.size())) {
162 return absl::OutOfRangeError("Palette address color is unavailable");
163 }
164 return palette[address.color_index];
165}
166
168 nlohmann::json document = {
169 {"schema", schema},
170 {"surface", surface},
171 {"graphics_loaded", graphics_loaded},
172 {"id", id},
173 {"parent_id", parent_id},
174 {"project_name", project_name},
175 {"rom_sha256", rom_sha256},
176 {"graphics_revision", std::to_string(graphics_revision)},
177 {"graphics_fingerprint", graphics_fingerprint},
178 {"terrain_catalog", terrain_catalog},
179 {"terrain_adapter_entries", terrain_adapter_entries},
180 {"palette_colors_resolved", palette_colors_resolved},
181 {"loader_agrees", loader_agrees},
182 {"slots", nlohmann::json::array()},
183 {"palettes", nlohmann::json::array()},
184 {"alternates", nlohmann::json::array()},
185 };
186 for (const auto& slot : slots) {
187 document["slots"].push_back(SlotJson(slot));
188 }
189 for (const auto& palette : palettes) {
190 document["palettes"].push_back({
191 {"name", palette.name},
192 {"source", palette.source},
193 {"selector", palette.selector},
194 {"inherited", palette.inherited},
195 });
196 }
197 for (const auto& alternate : alternates) {
198 document["alternates"].push_back({
199 {"name", alternate.name},
200 {"state", alternate.state},
201 {"detail", alternate.detail},
202 });
203 }
204 return document.dump();
205}
206
208 const OverworldMap& map, const GraphicsContextIdentity& identity) {
210 context.surface = "overworld_map";
211 context.id = map.index();
212 context.parent_id = map.parent();
213 context.graphics_loaded = map.main_gfx_id() != 0;
214 if (map.rom_ == nullptr) {
215 AttachIdentity(&context, nullptr, identity, "");
216 return context;
217 }
218 if (!context.graphics_loaded) {
219 AttachIdentity(&context, map.rom_, identity, "");
220 return context;
221 }
222
223 Rom& rom = *map.rom_;
224 const auto version = OverworldVersionHelper::GetVersion(rom);
225 const auto constants = map.version_constants();
226 const auto props = map.area_render_properties();
227 const bool custom_tiles =
230 const int world_offset = (map.parent() & 0xC0) >> 3;
231 const auto main_sheets = map.ResolveMainGraphicsGroup();
232 const auto area_sheets = map.ResolveAreaGraphicsGroup();
233 const auto sprite_sheets = map.ResolveSpriteGraphicsGroup();
234 const auto record_group_source = [&](const char* name, bool shared) {
235 context.alternates.push_back(
236 {name, shared ? "session" : "rom",
237 shared ? "Includes unsaved shared group assignments"
238 : "Session table is unloaded or uses a different ROM address"});
239 };
240 record_group_source("main_group_storage", map.UsesSessionMainGraphicsGroup());
241 record_group_source("area_group_storage", map.UsesSessionAreaGraphicsGroup());
242 record_group_source("sprite_group_storage",
244
245 std::string fingerprint_bytes;
246 context.loader_agrees = true;
247 context.slots.reserve(16);
248 for (int slot = 0; slot < 16; ++slot) {
249 ResolvedGraphicsSlot resolved;
250 resolved.slot = slot;
251 resolved.sheet_id = map.static_graphics(slot);
252 FillSheetShape(&resolved);
253 resolved.store_revision = SheetRevision(map.game_data_, resolved.sheet_id);
254 context.graphics_revision =
255 MixRevision(context.graphics_revision, slot, resolved.store_revision);
256 fingerprint_bytes.push_back(static_cast<char>(resolved.sheet_id));
257
258 if (slot < 8) {
259 resolved.role = RoleName(RoleForBlocksetSlot(slot));
260 resolved.palette_group = PaletteGroupFor(RoleForBlocksetSlot(slot));
261 resolved.inherited_sheet = main_sheets[slot];
262 uint8_t predicted = main_sheets[slot];
263 resolved.source = "main_group";
264 if (!custom_tiles && slot >= 3 && slot <= 6 &&
265 area_sheets[slot - 3] != 0) {
266 predicted = area_sheets[slot - 3];
267 resolved.source = "area_override";
268 }
269 if (custom_tiles) {
270 uint8_t custom_sheet = props.custom_gfx_ids[slot];
271 if (custom_sheet == 0xFF) {
272 predicted =
273 rom[OverworldCustomDefaultGFXGroups + world_offset + slot];
274 resolved.source = "world_default";
275 } else {
276 predicted = custom_sheet;
277 resolved.source = "custom_tile_gfx";
278 }
279 }
280 resolved.loader_agrees = predicted == resolved.sheet_id;
281 } else if (slot < 12) {
282 static constexpr uint8_t kStaticSpriteSheets[] = {0x73, 0x74, 0x79, 0x7A};
283 resolved.role = "static_sprite";
284 resolved.palette_group = PaletteGroupFor(SheetRole::kSpriteAux1);
285 resolved.source = "static_sprite";
286 resolved.inherited_sheet = kStaticSpriteSheets[slot - 8];
287 resolved.loader_agrees = resolved.sheet_id == resolved.inherited_sheet;
288 } else {
289 resolved.role = "spriteset";
290 resolved.palette_group = PaletteGroupFor(SheetRole::kSpriteAux1);
291 resolved.source = "spriteset";
292 const int set = map.sprite_graphics(map.game_state());
293 const int predicted =
294 static_cast<uint8_t>(sprite_sheets[slot - 12] + 0x73);
295 resolved.inherited_sheet = set;
296 resolved.loader_agrees = predicted == resolved.sheet_id;
297 }
298 context.loader_agrees = context.loader_agrees && resolved.loader_agrees;
299 context.slots.push_back(resolved);
300 }
301
302 auto& slot7 = context.slots[7];
303 slot7.composite = true;
304 const uint8_t raw_animated = props.animated_gfx;
305 const bool custom_animated = version != OverworldVersion::kVanilla &&
307 uint8_t predicted_upper = raw_animated;
308 if (custom_animated) {
309 if (raw_animated == 0x00 || raw_animated == 0xFF) {
310 predicted_upper = rom[OverworldCustomDefaultGFXGroups + world_offset + 7];
311 slot7.composite_upper_source = "world_default";
312 } else {
313 slot7.composite_upper_source = "animated_table";
314 }
315 } else if (raw_animated == 0x00 || raw_animated == 0xFF) {
316 predicted_upper = DeathMountain(map.parent()) ? 0x59 : 0x5B;
317 slot7.composite_upper_source = "vanilla_default";
318 } else {
319 slot7.composite_upper_source = "animated_table";
320 }
321 slot7.composite_upper_sheet = map.animated_sheet();
322 slot7.composite_upper_revision =
323 SheetRevision(map.game_data_, slot7.composite_upper_sheet);
324 if (slot7.composite_upper_sheet != slot7.sheet_id) {
325 context.graphics_revision = MixRevision(context.graphics_revision, 16,
326 slot7.composite_upper_revision);
327 }
328 const bool upper_agrees = predicted_upper == slot7.composite_upper_sheet;
329 slot7.loader_agrees = slot7.loader_agrees && upper_agrees;
330 context.loader_agrees = context.loader_agrees && upper_agrees;
331 fingerprint_bytes.push_back(static_cast<char>(slot7.composite_upper_sheet));
332
333 context.palettes.push_back({
334 "area",
336 ? "expanded_screen_palette"
337 : "vanilla_screen_palette",
338 map.area_palette(),
339 false,
340 });
341 context.palettes.push_back({
342 "main",
343 map.has_inherited_area_properties() ? "parent_area" : "area_main_palette",
344 props.main_palette,
346 });
347 const int area_palette = map.area_palette();
348 const uint8_t aux1 =
349 rom[constants.kOverworldMapPaletteGroup + (area_palette * 4)];
350 const uint8_t aux2 =
351 rom[constants.kOverworldMapPaletteGroup + (area_palette * 4) + 1];
352 const uint8_t animated =
353 rom[constants.kOverworldMapPaletteGroup + (area_palette * 4) + 2];
354 context.palettes.push_back({"aux1", "overworld_aux", aux1, aux1 == 0xFF});
355 context.palettes.push_back({"aux2", "overworld_aux", aux2, aux2 == 0xFF});
356 context.palettes.push_back(
357 {"animated", "overworld_animated", animated, animated == 0xFF});
358 const int sprite_palette = map.sprite_palette(map.game_state());
359 const uint8_t sprite0 =
360 rom[kOverworldSpritePaletteGroup + (sprite_palette * 2)];
361 const uint8_t sprite1 =
362 rom[kOverworldSpritePaletteGroup + (sprite_palette * 2) + 1];
363 context.palettes.push_back(
364 {"sprite0", "sprite_palette_group", sprite0, sprite0 == 0xFF});
365 context.palettes.push_back(
366 {"sprite1", "sprite_palette_group", sprite1, sprite1 == 0xFF});
367 const bool custom_bg =
370 context.palettes.push_back({
371 "backdrop",
372 custom_bg ? "area_specific_bg" : "world_default",
373 custom_bg ? props.area_specific_bg_color
374 : map.parent() < 0x40 ? 0
375 : map.parent() < 0x80 ? 1
376 : 2,
377 custom_bg && map.has_inherited_area_properties(),
378 });
380
381 for (int state = 0; state < 3; ++state) {
382 context.alternates.push_back({
383 absl::StrFormat("sprite_state_%d", state),
384 state == map.game_state() ? "active" : "recorded",
385 absl::StrFormat("graphics=0x%02X palette=0x%02X",
386 map.sprite_graphics(state), map.sprite_palette(state)),
387 });
388 }
389 context.alternates.push_back(
390 {"day_night_tint", "unsupported", "not resolved by this loader"});
391 context.alternates.push_back({"runtime_palette_transfer", "unsupported",
392 "not resolved by this loader"});
393
394 AttachIdentity(&context, &rom, identity, fingerprint_bytes);
395 return context;
396}
397
399 const Room& room, const GraphicsContextIdentity& identity) {
401 context.surface = "dungeon_room";
402 context.id = room.id();
403 context.parent_id = room.id();
404 context.graphics_loaded = room.resolved_main_blockset_ != 0xFF;
405 if (!context.graphics_loaded || room.rom_ == nullptr ||
406 room.game_data_ == nullptr) {
407 AttachIdentity(&context, room.rom_, identity, "");
408 context.graphics_loaded = false;
409 return context;
410 }
411
412 const GameData& data = *room.game_data_;
413 const auto blocks = room.blocks();
414 const uint8_t main_index = room.resolved_main_blockset_;
415 const bool main_ok = main_index < data.main_blockset_ids.size();
416 const bool room_ok = room.blockset() < data.room_blockset_ids.size();
417 const size_t sprite_index = static_cast<size_t>(room.spriteset()) + 64;
418 const bool sprites_ok = sprite_index < data.spriteset_ids.size();
419 static constexpr uint8_t kStaticSpriteSheets[] = {115, 125, 121, 122};
420
421 std::string fingerprint_bytes;
422 context.loader_agrees = main_ok;
423 context.slots.reserve(16);
424 for (int slot = 0; slot < 16; ++slot) {
425 ResolvedGraphicsSlot resolved;
426 resolved.slot = slot;
427 resolved.sheet_id = blocks[slot];
428 FillSheetShape(&resolved);
429 resolved.store_revision = SheetRevision(&data, resolved.sheet_id);
430 context.graphics_revision =
431 MixRevision(context.graphics_revision, slot, resolved.store_revision);
432 fingerprint_bytes.push_back(static_cast<char>(resolved.sheet_id));
433
434 if (slot < 8) {
435 if (slot == 7) {
436 resolved.role = "main_blockset";
437 resolved.palette_group = PaletteGroupFor(SheetRole::kDungeonMain);
438 } else if (slot >= 3) {
439 // The shared roomset table also serves the overworld, but dungeon
440 // sheets use the room's dungeon palette, not overworld aux palettes.
441 resolved.role = "room_blockset";
442 resolved.palette_group = PaletteGroupFor(SheetRole::kDungeonMain);
443 } else {
444 resolved.role = "main_blockset";
445 resolved.palette_group = PaletteGroupFor(SheetRole::kDungeonMain);
446 }
447 resolved.source = "main_blockset";
448 uint8_t predicted = 0;
449 if (main_ok) {
450 predicted = data.main_blockset_ids[main_index][slot];
451 resolved.inherited_sheet = predicted;
452 }
453 if (slot >= 3 && slot <= 6 && room_ok) {
454 const uint8_t room_sheet =
455 data.room_blockset_ids[room.blockset()][slot - 3];
456 if (room_sheet != 0) {
457 predicted = room_sheet;
458 resolved.source = "room_blockset";
459 }
460 }
461 resolved.loader_agrees = main_ok && predicted == resolved.sheet_id;
462 } else if (slot < 12) {
463 resolved.role = "static_sprite";
464 resolved.palette_group = PaletteGroupFor(SheetRole::kSpriteAux1);
465 resolved.source = "static_sprite";
466 resolved.inherited_sheet = kStaticSpriteSheets[slot - 8];
467 resolved.loader_agrees = resolved.sheet_id == resolved.inherited_sheet;
468 } else {
469 resolved.role = "spriteset";
470 resolved.palette_group = PaletteGroupFor(SheetRole::kSpriteAux1);
471 resolved.source = sprites_ok ? "spriteset" : "spriteset_missing";
472 resolved.inherited_sheet = room.spriteset();
473 const int predicted =
474 sprites_ok ? static_cast<uint8_t>(
475 data.spriteset_ids[sprite_index][slot - 12] + 115)
476 : 0;
477 resolved.loader_agrees = predicted == resolved.sheet_id;
478 }
479 context.loader_agrees = context.loader_agrees && resolved.loader_agrees;
480 context.slots.push_back(resolved);
481 }
482
483 context.palettes.push_back({"header", "room_header", room.palette(), false});
484 if (room.palette() < data.paletteset_ids.size()) {
485 static constexpr const char* kSetNames[] = {
486 "dungeon_main_offset", "sprite_aux1", "sprite_aux3", "sprite_aux3_b"};
487 for (int i = 0; i < 4; ++i) {
488 context.palettes.push_back({kSetNames[i], "dungeon_palette_set",
489 data.paletteset_ids[room.palette()][i],
490 false});
491 }
492 }
493 context.palettes.push_back({"dungeon_main", "palette_pointer",
494 room.ResolveDungeonPaletteId(), false});
496 context.alternates.push_back(
497 {"entrance_world", "unresolved",
498 "the room does not retain the entrance overworld"});
499 context.alternates.push_back(
500 {"sprite_environment", "light_world_fallback",
501 "preview uses the outdoor light-world sprite environment"});
502
503 AttachIdentity(&context, room.rom_, identity, fingerprint_bytes);
504 return context;
505}
506
507} // 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
bool is_loaded() const
Definition rom.h:187
const nlohmann::json & document() const
uint64_t Revision(uint16_t sheet) const
static constexpr uint16_t kSheetCount
Represents a single Overworld map screen.
const gfx::SnesPalette & current_palette() const
zelda3_version_pointers version_constants() const
bool UsesSessionAreaGraphicsGroup() const
bool has_inherited_area_properties() const
std::array< uint8_t, 4 > ResolveSpriteGraphicsGroup() const
auto sprite_graphics(int i) const
bool UsesSessionSpriteGraphicsGroup() const
bool UsesSessionMainGraphicsGroup() const
auto static_graphics(int i) const
uint8_t animated_sheet() const
std::array< uint8_t, 4 > ResolveAreaGraphicsGroup() const
auto sprite_palette(int i) const
std::array< uint8_t, 8 > ResolveMainGraphicsGroup() const
AreaRenderProperties area_render_properties() const
static bool SupportsCustomBGColors(OverworldVersion version)
Check if ROM supports custom background colors per area (v2+)
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)
static bool SupportsCustomTileGFX(OverworldVersion version)
Check if ROM supports custom tile GFX groups (v3+)
uint8_t resolved_main_blockset_
Definition room.h:1272
GameData * game_data_
Definition room.h:1184
uint8_t blockset() const
Definition room.h:1015
uint8_t palette() const
Definition room.h:1019
const gfx::SnesPalette & rendered_dungeon_palette() const
Definition room.h:1132
uint8_t spriteset() const
Definition room.h:1018
auto blocks() const
Definition room.h:1080
int ResolveDungeonPaletteId() const
Definition room.cc:921
int id() const
Definition room.h:1012
One ROM session's graphics sheet pixels, with a revision per sheet.
std::string ComputeSourceArtifactSha256(std::string_view content)
std::string TerrainFingerprint(std::string_view bytes)
SheetRolePaletteBinding DefaultBindingFor(SheetRole role)
SheetRole RoleForRoomsetSlot(int slot_index)
SheetRole RoleForBlocksetSlot(int slot_index)
uint64_t MixRevision(uint64_t revision, int slot, uint64_t sheet_revision)
void AttachIdentity(ResolvedGraphicsContext *context, const Rom *rom, const GraphicsContextIdentity &identity, std::string_view surface_bytes)
uint64_t SheetRevision(const GameData *game_data, int sheet_id)
Zelda 3 specific classes and functions.
constexpr int OverworldCustomTileGFXGroupEnabled
constexpr int OverworldCustomAreaSpecificBGEnabled
constexpr int kOverworldSpritePaletteGroup
Definition overworld.h:116
nlohmann::json json
constexpr int OverworldCustomDefaultGFXGroups
constexpr int OverworldCustomAnimatedGFXEnabled
ResolvedGraphicsContext DescribeDungeonGraphicsContext(const Room &room, const GraphicsContextIdentity &identity)
const char * PaletteAddressGroupName(PaletteAddressGroup group)
@ kVanilla
0xFF in ROM, no ZScream ASM applied
ResolvedGraphicsContext DescribeOverworldGraphicsContext(const OverworldMap &map, const GraphicsContextIdentity &identity)
absl::StatusOr< gfx::SnesColor > ReadPaletteColor(const GameData &owner, const PaletteColorAddress &address)
PaletteGroup * get_group(const std::string &group_name)
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:99
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition game_data.h:108
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97
std::vector< ResolvedPaletteSource > palettes
std::vector< ResolvedAlternateContext > alternates
std::vector< ResolvedGraphicsSlot > slots