28 return absl::InvalidArgumentError(
"Lyra profile output is required");
30 return absl::FailedPreconditionError(
31 "Open the main project ROM to draw Lyra terrain");
34 return absl::FailedPreconditionError(
"Loom map $3A is unavailable");
35 const auto* game_data = donor->game_data();
41 return absl::FailedPreconditionError(
42 "Load native graphics and palettes before drawing Lyra terrain");
44 if ((
static_cast<size_t>(sheet) + 1) * 0x1000 >
46 return absl::FailedPreconditionError(
47 "Loom native graphics sheets are incomplete");
51 return absl::FailedPreconditionError(
52 "Loom native animation sheet is incomplete");
56 source.set_game_state(0);
57 source.LoadAreaGraphics();
60 return absl::FailedPreconditionError(absl::StrFormat(
61 "Loom graphics mismatch at slot %d: expected $%02X, found $%02X", i,
64 if (source.render_main_palette() != profile->
main_palette ||
67 return absl::FailedPreconditionError(
68 "Loom palette or animation profile mismatch");
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");
77 std::string definitions;
78 for (
const auto& tile : overworld.
tiles16())
79 for (
const auto& quadrant : tile.tiles_info)
83 const auto& pixels = source.current_graphics();
85 {
reinterpret_cast<const char*
>(pixels.data()), pixels.size()});
87 for (
size_t i = 0; i < source.current_palette().size(); ++i)
88 AddWord(colors, source.current_palette()[i].snes());
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),
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);
113 return absl::OkStatus();
162 return absl::FailedPreconditionError(
"Lyra native source is unavailable");
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)
183 for (
int i = 0; i < 3; ++i) {
185 if (p.
sprite_graphics[i] >=
static_cast<int>(data.spriteset_ids.size()))
186 return absl::OutOfRangeError(
"Lyra sprite set is unavailable");
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");
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");
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");
214 for (
int slot = 0; slot < 8; ++slot) {
217 auto* pixels = source.mutable_current_graphics();
218 for (
int offset = slot == 7 ? 0x800 : 0; offset < 0x1000; ++offset) {
220 (slot == 0 || slot == 3 || slot == 4 || slot == 5) ? 0x88 : 0;
221 (*pixels)[slot * 0x1000 + offset] = data.graphics_buffer[offset] + bias;
225 if (source.current_palette().size() != 256)
226 return absl::FailedPreconditionError(
"Lyra resolved palette is incomplete");
229 p, *source.mutable_current_palette()));
230 const auto& pixels = source.current_graphics();
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());
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");
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) {
270 :
static_cast<int>(source.static_graphics(slot));
272 const auto empty_slot = [&](
int slot) {
273 return source.static_graphics(slot) == 0 &&
280 for (
int slot = 0; slot < 16; ++slot) {
281 const size_t sheet = effective_sheet(slot);
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) {
288 data->graphics_buffer[
static_cast<size_t>(source.animated_sheet()) *
294 expected =
static_cast<uint8_t
>(
295 data->graphics_buffer[sheet * 0x1000 + offset] + bias);
297 if (pixels[slot * 0x1000 + offset] != expected)
298 return absl::FailedPreconditionError(
299 "Lyra inspection pixels do not match native source sheets");
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());
310 binding.append(
reinterpret_cast<const char*
>(pixels.data()), pixels.size());
311 binding.append(colors);
313 return absl::FailedPreconditionError(
314 "Lyra inspection native binding does not match resolved assets");
316 const auto pixel_hash = [&](
size_t offset,
size_t count) {
318 {
reinterpret_cast<const char*
>(pixels.data() + offset), count});
320 const auto sheet_hash = [&](
int sheet) {
322 {
reinterpret_cast<const char*
>(data->graphics_buffer.data() +
323 static_cast<size_t>(sheet) * 0x1000),
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 = {
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},
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;
355 slots.push_back(std::move(entry));
357 nlohmann::json saved = {
365 nlohmann::json bg_words =
nullptr;
366 nlohmann::json bg_hash =
nullptr;
368 std::string bg_bytes;
370 AddWord(bg_bytes, word);
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"},
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)},
394 {{
"format",
"indexed8"},
395 {
"byte_count", pixels.size()},
396 {
"sha256", pixel_hash(0, pixels.size())}}},
398 {{
"words", std::move(words)},
399 {
"encoding",
"256 uint16 little-endian SNES words"},
400 {
"byte_count", colors.size()},
402 {
"background_override_words", std::move(bg_words)},
403 {
"background_override_uint16le_sha256", std::move(bg_hash)},
408 {{
"sheet_id", source.animated_sheet()},
409 {
"decoded_sheet_sha256", sheet_hash(source.animated_sheet())},
410 {
"decoded_byte_count", 0x1000},
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"},
430 return document.dump();