yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
map_refresh_coordinator.cc
Go to the documentation of this file.
1// Related header
3
4#include <algorithm>
5
6#include "absl/status/status.h"
12#include "rom/rom.h"
13#include "util/log.h"
14#include "util/macro.h"
15#include "zelda3/common.h"
19
20namespace yaze::editor {
21
22namespace {
23
24int WorldForMapIndex(int map_index) {
25 if (map_index >= zelda3::kSpecialWorldMapIdStart) {
26 return 2;
27 }
28 if (map_index >= zelda3::kDarkWorldMapIdStart) {
29 return 1;
30 }
31 return 0;
32}
33
35 int map_index) {
36 return overworld->GetMapTiles(WorldForMapIndex(map_index));
37}
38
39bool IsMapInCurrentWorld(int map_index, int current_world) {
40 return WorldForMapIndex(map_index) == current_world;
41}
42
44 return ctx.game_state ? std::clamp(*ctx.game_state, 0, 2) : 0;
45}
46
49 if (!map) {
50 return;
51 }
53 // Child screens render with the area parent's main palette, animated GFX,
54 // tile GFX groups, and subscreen overlay (the game reads them with $8A).
55 if (ctx.overworld) {
58 }
59}
60
62 const zelda3::OverworldMap& map) {
63 if (!ctx.current_gfx_bmp) {
64 return;
65 }
66
67 const auto& current_graphics = map.current_graphics();
68 if (current_graphics.empty()) {
69 return;
70 }
71
72 constexpr int kCurrentGfxBitmapWidth = 0x80;
73 if (current_graphics.size() % kCurrentGfxBitmapWidth != 0) {
74 LOG_WARN("MapRefreshCoordinator",
75 "Current graphics size %zu is not divisible by bitmap width %d",
76 current_graphics.size(), kCurrentGfxBitmapWidth);
77 return;
78 }
79
80 const int bitmap_height =
81 static_cast<int>(current_graphics.size() / kCurrentGfxBitmapWidth);
82 const bool recreate_bitmap =
83 !ctx.current_gfx_bmp->is_active() ||
84 ctx.current_gfx_bmp->width() != kCurrentGfxBitmapWidth ||
85 ctx.current_gfx_bmp->height() != bitmap_height ||
86 ctx.current_gfx_bmp->depth() != 0x08;
87
88 if (recreate_bitmap) {
89 ctx.current_gfx_bmp->Create(kCurrentGfxBitmapWidth, bitmap_height, 0x08,
90 current_graphics);
91 } else {
92 ctx.current_gfx_bmp->set_data(current_graphics);
93 }
94
95 const auto command = ctx.current_gfx_bmp->texture()
99}
100
101} // namespace
102
104 if (map_id < 0) {
105 // Invalidate all maps - clear both editor cache and Overworld's tileset
106 // cache
107 ctx_.current_graphics_set->clear();
109 } else {
110 // Invalidate specific map and its siblings in the Overworld's tileset cache
111 ctx_.current_graphics_set->erase(map_id);
113 }
114}
115
118 for (int id = 0; id < zelda3::kNumOverworldMaps; ++id) {
119 if (auto* map = ctx_.overworld->mutable_overworld_map(id)) {
120 map->SetNotBuilt();
121 }
122 (*ctx_.maps_bmp)[id].set_modified(true);
123 }
124}
125
127 auto* map = ctx_.overworld->mutable_overworld_map(map_index);
128 if (!map) {
129 return;
130 }
131 PrepareMapForRefresh(ctx_, map);
132 map->LoadAreaGraphics();
133 *ctx_.status = map->BuildTileset();
135 *ctx_.status = map->BuildTiles16Gfx(*ctx_.overworld->mutable_tiles16(),
136 ctx_.overworld->tiles16().size());
138 *ctx_.status =
139 map->BuildBitmap(MapTilesForMapIndex(ctx_.overworld, map_index));
140 (*ctx_.maps_bmp)[map_index].set_data(map->bitmap_data());
141 (*ctx_.maps_bmp)[map_index].set_modified(true);
143}
144
146 // Use the new on-demand refresh system
148}
149
158 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
159 return;
160 }
161
162 // Check if the map is actually visible or being edited
163 bool is_current_map = (map_index == *ctx_.current_map);
164 bool is_current_world = IsMapInCurrentWorld(map_index, *ctx_.current_world);
165
166 // For non-current maps in non-current worlds, defer the refresh
167 if (!is_current_map && !is_current_world) {
168 // Mark for deferred refresh - will be processed when the map becomes
169 // visible
170 (*ctx_.maps_bmp)[map_index].set_modified(true);
171 return;
172 }
173
174 // For visible maps, do immediate refresh
175 RefreshChildMapOnDemand(map_index);
176}
177
182 auto* map = ctx_.overworld->mutable_overworld_map(map_index);
183 if (!map) {
184 return; // Map not loaded yet (e.g. Overworld not fully initialized)
185 }
186
187 // Check what actually needs to be refreshed
188 bool needs_graphics_rebuild = (*ctx_.maps_bmp)[map_index].modified();
189
190 if (needs_graphics_rebuild) {
191 // Only rebuild what's actually changed
192 PrepareMapForRefresh(ctx_, map);
193 map->LoadAreaGraphics();
194
195 // Rebuild tileset only if graphics changed
196 auto status = map->BuildTileset();
197 if (!status.ok()) {
198 LOG_ERROR("MapRefreshCoordinator",
199 "Failed to build tileset for map %d: %s", map_index,
200 status.message().data());
201 return;
202 }
203
204 // Rebuild tiles16 graphics
205 status = map->BuildTiles16Gfx(*ctx_.overworld->mutable_tiles16(),
206 ctx_.overworld->tiles16().size());
207 if (!status.ok()) {
208 LOG_ERROR("MapRefreshCoordinator",
209 "Failed to build tiles16 graphics for map %d: %s", map_index,
210 status.message().data());
211 return;
212 }
213
214 // Rebuild bitmap
215 status = map->BuildBitmap(MapTilesForMapIndex(ctx_.overworld, map_index));
216 if (!status.ok()) {
217 LOG_ERROR("MapRefreshCoordinator",
218 "Failed to build bitmap for map %d: %s", map_index,
219 status.message().data());
220 return;
221 }
222
223 // Update bitmap data
224 (*ctx_.maps_bmp)[map_index].set_data(map->bitmap_data());
225 (*ctx_.maps_bmp)[map_index].set_modified(false);
226
227 // Validate surface synchronization to help debug crashes
228 if (!(*ctx_.maps_bmp)[map_index].ValidateDataSurfaceSync()) {
229 LOG_WARN("MapRefreshCoordinator",
230 "Warning: Surface synchronization issue detected for map %d",
231 map_index);
232 }
233
234 // Queue texture update to ensure changes are visible
235 if ((*ctx_.maps_bmp)[map_index].texture()) {
238 } else {
241 }
242 }
243
244 // Handle multi-area maps (large, wide, tall) with safe coordination
245 // Use centralized version detection
247 bool use_v3_area_sizes =
249
250 if (use_v3_area_sizes) {
251 // Use v3 multi-area coordination
252 RefreshMultiAreaMapsSafely(map_index, map);
253 } else {
254 // Legacy logic: only handle large maps for vanilla/v2
255 if (map->is_large_map()) {
256 RefreshMultiAreaMapsSafely(map_index, map);
257 }
258 }
259}
260
276 int map_index, zelda3::OverworldMap* map) {
278
279 auto area_size = map->area_size();
280 if (area_size == AreaSizeEnum::SmallArea) {
281 return; // No siblings to coordinate
282 }
283
284 // Always work from parent perspective for consistent coordination
285 int parent_id = map->parent();
286
287 // If we're not the parent, get the parent map to work from
288 auto* parent_map = ctx_.overworld->mutable_overworld_map(parent_id);
289 if (!parent_map) {
290 LOG_WARN("MapRefreshCoordinator",
291 "RefreshMultiAreaMapsSafely: Could not get parent map %d for "
292 "map %d",
293 parent_id, map_index);
294 return;
295 }
296
297 LOG_DEBUG("MapRefreshCoordinator",
298 "RefreshMultiAreaMapsSafely: Processing %s area from parent %d "
299 "(trigger: %d)",
300 (area_size == AreaSizeEnum::LargeArea) ? "large"
301 : (area_size == AreaSizeEnum::WideArea) ? "wide"
302 : "tall",
303 parent_id, map_index);
304
305 // Determine all maps that are part of this multi-area structure
306 // based on the parent's position and area size
307 std::vector<int> sibling_maps;
308
309 switch (area_size) {
310 case AreaSizeEnum::LargeArea:
311 // Large Area: 2x2 grid (4 maps total)
312 sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
313 break;
314
315 case AreaSizeEnum::WideArea:
316 // Wide Area: 2x1 grid (2 maps total, horizontally adjacent)
317 sibling_maps = {parent_id, parent_id + 1};
318 break;
319
320 case AreaSizeEnum::TallArea:
321 // Tall Area: 1x2 grid (2 maps total, vertically adjacent)
322 sibling_maps = {parent_id, parent_id + 8};
323 break;
324
325 default:
326 LOG_WARN("MapRefreshCoordinator",
327 "RefreshMultiAreaMapsSafely: Unknown area size %d for map %d",
328 static_cast<int>(area_size), map_index);
329 return;
330 }
331
332 // Refresh all siblings (including self if different from trigger)
333 // The trigger map (map_index) was already processed by the caller,
334 // so we skip it to avoid double-processing
335 for (int sibling : sibling_maps) {
336 // Skip the trigger map - it was already processed by
337 // RefreshChildMapOnDemand
338 if (sibling == map_index) {
339 continue;
340 }
341
342 // Bounds check
343 if (sibling < 0 || sibling >= zelda3::kNumOverworldMaps) {
344 continue;
345 }
346
347 // Check visibility - only immediately refresh visible maps
348 bool is_current_map = (sibling == *ctx_.current_map);
349 bool is_current_world = IsMapInCurrentWorld(sibling, *ctx_.current_world);
350
351 // Always mark sibling as needing refresh to ensure consistency
352 (*ctx_.maps_bmp)[sibling].set_modified(true);
353
354 if (is_current_map || is_current_world) {
355 LOG_DEBUG("MapRefreshCoordinator",
356 "RefreshMultiAreaMapsSafely: Refreshing sibling map %d",
357 sibling);
358
359 // Direct refresh for visible siblings
360 auto* sibling_map = ctx_.overworld->mutable_overworld_map(sibling);
361 if (!sibling_map)
362 continue;
363
364 PrepareMapForRefresh(ctx_, sibling_map);
365 sibling_map->LoadAreaGraphics();
366
367 auto status = sibling_map->BuildTileset();
368 if (!status.ok()) {
369 LOG_ERROR("MapRefreshCoordinator",
370 "Failed to build tileset for sibling %d: %s", sibling,
371 status.message().data());
372 continue;
373 }
374
375 status = sibling_map->BuildTiles16Gfx(*ctx_.overworld->mutable_tiles16(),
376 ctx_.overworld->tiles16().size());
377 if (!status.ok()) {
378 LOG_ERROR("MapRefreshCoordinator",
379 "Failed to build tiles16 for sibling %d: %s", sibling,
380 status.message().data());
381 continue;
382 }
383
384 status = sibling_map->LoadPalette();
385 if (!status.ok()) {
386 LOG_ERROR("MapRefreshCoordinator",
387 "Failed to load palette for sibling %d: %s", sibling,
388 status.message().data());
389 continue;
390 }
391
392 status = sibling_map->BuildBitmap(
393 MapTilesForMapIndex(ctx_.overworld, sibling));
394 if (!status.ok()) {
395 LOG_ERROR("MapRefreshCoordinator",
396 "Failed to build bitmap for sibling %d: %s", sibling,
397 status.message().data());
398 continue;
399 }
400
401 // Update bitmap data
402 (*ctx_.maps_bmp)[sibling].set_data(sibling_map->bitmap_data());
403
404 // Set palette if bitmap has a valid surface
405 if ((*ctx_.maps_bmp)[sibling].is_active() &&
406 (*ctx_.maps_bmp)[sibling].surface()) {
407 (*ctx_.maps_bmp)[sibling].SetPalette(sibling_map->current_palette());
408 }
409 (*ctx_.maps_bmp)[sibling].set_modified(false);
410
411 // Queue texture update/creation
412 if ((*ctx_.maps_bmp)[sibling].texture()) {
415 } else {
417 ctx_.ensure_map_texture(sibling);
418 }
419 }
420 }
421 // Non-visible siblings remain marked as modified for deferred refresh
422 }
423}
424
426 const std::string& group, int palette_index) {
427 if (group != "ow_main" && group != "ow_aux" && group != "ow_animated" &&
428 group != "grass" && group != "hud" && group != "global_sprites" &&
429 group != "sprites_aux1" && group != "sprites_aux2" &&
430 group != "sprites_aux3") {
431 return absl::OkStatus();
432 }
433 bool refreshed = false;
434 for (int id = 0; id < zelda3::kNumOverworldMaps; ++id) {
435 auto* map = ctx_.overworld->mutable_overworld_map(id);
436 if (!map || map->current_palette().empty()) {
437 continue;
438 }
439 if (group == "ow_main" && palette_index >= 0 &&
440 map->render_main_palette() != palette_index) {
441 continue;
442 }
443 RETURN_IF_ERROR(map->LoadPalette());
444 auto& bitmap = (*ctx_.maps_bmp)[id];
445 if (bitmap.is_active() && bitmap.surface()) {
446 bitmap.SetPalette(map->current_palette());
450 &bitmap);
451 }
452 refreshed = true;
453 }
454 // This path updates display colors only. Do not invalidate/reload sheets or
455 // repack Tile16/map data just because a palette word changed.
456 return refreshed ? RefreshTile16Blockset() : absl::OkStatus();
457}
458
460 const int target_map = map_id < 0 ? *ctx_.current_map : map_id;
461 auto* current_map = ctx_.overworld->mutable_overworld_map(target_map);
462 if (!current_map) {
463 return absl::FailedPreconditionError("Current overworld map not loaded");
464 }
465 PrepareMapForRefresh(ctx_, current_map);
466 RETURN_IF_ERROR(current_map->LoadPalette());
467 const auto current_map_palette = current_map->current_palette();
468 // Use centralized version detection
470 bool use_v3_area_sizes =
472
473 if (use_v3_area_sizes) {
474 // Use v3 area size system
476 auto area_size = current_map->area_size();
477
478 if (area_size != AreaSizeEnum::SmallArea) {
479 // Get all sibling maps that need palette updates
480 std::vector<int> sibling_maps;
481 int parent_id = current_map->parent();
482
483 switch (area_size) {
484 case AreaSizeEnum::LargeArea:
485 // 2x2 grid: parent, parent+1, parent+8, parent+9
486 sibling_maps = {parent_id, parent_id + 1, parent_id + 8,
487 parent_id + 9};
488 break;
489 case AreaSizeEnum::WideArea:
490 // 2x1 grid: parent, parent+1
491 sibling_maps = {parent_id, parent_id + 1};
492 break;
493 case AreaSizeEnum::TallArea:
494 // 1x2 grid: parent, parent+8
495 sibling_maps = {parent_id, parent_id + 8};
496 break;
497 default:
498 break;
499 }
500
501 // Update palette for all siblings - each uses its own loaded palette
502 for (int sibling_index : sibling_maps) {
503 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
504 continue;
505 }
506 auto* sibling_map =
507 ctx_.overworld->mutable_overworld_map(sibling_index);
508 if (!sibling_map) {
509 continue;
510 }
511 PrepareMapForRefresh(ctx_, sibling_map);
512 RETURN_IF_ERROR(sibling_map->LoadPalette());
513 (*ctx_.maps_bmp)[sibling_index].SetPalette(
514 sibling_map->current_palette());
515 }
516 } else {
517 // Small area - only update current map
518 (*ctx_.maps_bmp)[target_map].SetPalette(current_map_palette);
519 }
520 } else {
521 // Legacy logic for vanilla and v2 ROMs
522 if (current_map->is_large_map()) {
523 // We need to update the map and its siblings if it's a large map
524 for (int i = 1; i < 4; i++) {
525 int sibling_index = current_map->parent() + i;
526 if (i >= 2)
527 sibling_index += 6;
528 auto* sibling_map =
529 ctx_.overworld->mutable_overworld_map(sibling_index);
530 if (!sibling_map) {
531 continue;
532 }
533 PrepareMapForRefresh(ctx_, sibling_map);
534 RETURN_IF_ERROR(sibling_map->LoadPalette());
535
536 // SAFETY: Only set palette if bitmap has a valid surface
537 // Use sibling map's own loaded palette
538 if ((*ctx_.maps_bmp)[sibling_index].is_active() &&
539 (*ctx_.maps_bmp)[sibling_index].surface()) {
540 (*ctx_.maps_bmp)[sibling_index].SetPalette(
541 sibling_map->current_palette());
542 }
543 }
544 }
545
546 // SAFETY: Only set palette if bitmap has a valid surface
547 if ((*ctx_.maps_bmp)[target_map].is_active() &&
548 (*ctx_.maps_bmp)[target_map].surface()) {
549 (*ctx_.maps_bmp)[target_map].SetPalette(current_map_palette);
550 }
551 }
552
553 // The area above is the edit target; the selector and Tile16 preview still
554 // belong to the current selection, which may have moved since the edit.
555 return RefreshTile16Blockset();
556}
557
559 // Mark the bitmap as modified to force refresh on next update
560 if (map_index >= 0 && map_index < zelda3::kNumOverworldMaps) {
561 (*ctx_.maps_bmp)[map_index].set_modified(true);
562
563 // Clear blockset cache
564 *ctx_.current_blockset = 0xFF;
565
566 // Invalidate Overworld's tileset cache for this map and siblings
567 // This ensures stale cached tilesets aren't reused after property changes
569
570 LOG_DEBUG("MapRefreshCoordinator",
571 "ForceRefreshGraphics: Map %d marked for refresh", map_index);
572 }
573}
574
576 bool include_self) {
577 if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
578 return;
579 }
580
581 auto* map = ctx_.overworld->mutable_overworld_map(map_index);
582 if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
583 return; // No siblings for small areas
584 }
585
586 int parent_id = map->parent();
587 std::vector<int> siblings;
588
589 switch (map->area_size()) {
591 siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
592 break;
594 siblings = {parent_id, parent_id + 1};
595 break;
597 siblings = {parent_id, parent_id + 8};
598 break;
599 default:
600 return;
601 }
602
603 for (int sibling : siblings) {
604 if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) {
605 // Skip self unless include_self is true
606 if (sibling == map_index && !include_self) {
607 continue;
608 }
609
610 // Mark as modified FIRST before loading
611 (*ctx_.maps_bmp)[sibling].set_modified(true);
612
613 // Load graphics from ROM
614 auto* sibling_map = ctx_.overworld->mutable_overworld_map(sibling);
615 if (!sibling_map) {
616 continue;
617 }
618 PrepareMapForRefresh(ctx_, sibling_map);
619 sibling_map->LoadAreaGraphics();
620
621 // CRITICAL FIX: Bypass visibility check - force immediate refresh
622 // Call RefreshChildMapOnDemand() directly instead of
623 // RefreshOverworldMapOnDemand()
625
626 LOG_DEBUG("MapRefreshCoordinator",
627 "RefreshSiblingMapGraphics: Refreshed sibling map %d", sibling);
628 }
629 }
630}
631
633 const int target_map = map_id < 0 ? *ctx_.current_map : map_id;
634 const auto* selected = ctx_.overworld->overworld_map(target_map);
635 if (!selected) {
636 return;
637 }
638 const auto* parent = ctx_.overworld->overworld_map(selected->parent());
639 // Property edits target the parent even while cursor-follow selects a child.
640 // Never overwrite siblings with the child's stale pre-edit values.
641 const auto& current_ow_map = parent ? *parent : *selected;
642 // Message editing and lookup use that parent directly. Child message slots
643 // remain independent persistent data, even when graphics are refreshed.
644
645 // Use centralized version detection
647 bool use_v3_area_sizes =
649
650 if (use_v3_area_sizes) {
651 // Use v3 area size system
653 auto area_size = current_ow_map.area_size();
654
655 if (area_size != AreaSizeEnum::SmallArea) {
656 // Get all sibling maps that need property updates
657 std::vector<int> sibling_maps;
658 int parent_id = current_ow_map.parent();
659
660 switch (area_size) {
661 case AreaSizeEnum::LargeArea:
662 // 2x2 grid: parent+1, parent+8, parent+9 (skip parent itself)
663 sibling_maps = {parent_id + 1, parent_id + 8, parent_id + 9};
664 break;
665 case AreaSizeEnum::WideArea:
666 // 2x1 grid: parent+1 (skip parent itself)
667 sibling_maps = {parent_id + 1};
668 break;
669 case AreaSizeEnum::TallArea:
670 // 1x2 grid: parent+8 (skip parent itself)
671 sibling_maps = {parent_id + 8};
672 break;
673 default:
674 break;
675 }
676
677 // Copy properties from parent map to all siblings
678 const int game_state = CurrentGameState(ctx_);
679 for (int sibling_index : sibling_maps) {
680 if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
681 continue;
682 }
683 auto& map = *ctx_.overworld->mutable_overworld_map(sibling_index);
684 map.set_area_graphics(current_ow_map.area_graphics());
685 map.set_area_palette(current_ow_map.area_palette());
686 map.set_sprite_graphics(game_state,
687 current_ow_map.sprite_graphics(game_state));
688 map.set_sprite_palette(game_state,
689 current_ow_map.sprite_palette(game_state));
690
691 // CRITICAL FIX: Reload graphics after changing properties
692 PrepareMapForRefresh(ctx_, &map);
693 map.LoadAreaGraphics();
694 }
695 }
696 } else {
697 // Legacy logic for vanilla and v2 ROMs
698 if (current_ow_map.is_large_map()) {
699 // We need to copy the properties from the parent map to the children
700 const int game_state = CurrentGameState(ctx_);
701 for (int i = 1; i < 4; i++) {
702 int sibling_index = current_ow_map.parent() + i;
703 if (i >= 2) {
704 sibling_index += 6;
705 }
706 auto& map = *ctx_.overworld->mutable_overworld_map(sibling_index);
707 map.set_area_graphics(current_ow_map.area_graphics());
708 map.set_area_palette(current_ow_map.area_palette());
709 map.set_sprite_graphics(game_state,
710 current_ow_map.sprite_graphics(game_state));
711 map.set_sprite_palette(game_state,
712 current_ow_map.sprite_palette(game_state));
713
714 // CRITICAL FIX: Reload graphics after changing properties
715 PrepareMapForRefresh(ctx_, &map);
716 map.LoadAreaGraphics();
717 }
718 }
719 }
720}
721
723 LOG_DEBUG("MapRefreshCoordinator", "RefreshTile16Blockset called");
725 return absl::FailedPreconditionError("Current overworld map not loaded");
726 }
728 auto* current_map = ctx_.overworld->mutable_overworld_map(*ctx_.current_map);
729 if (!current_map) {
730 return absl::FailedPreconditionError("Current overworld map not loaded");
731 }
732 // Area graphics ids are reused across worlds; refresh even when the id
733 // matches so DW/SW base sheets and palettes do not inherit LW state.
734 *ctx_.current_blockset = current_map->area_graphics();
735
736 *ctx_.palette = current_map->current_palette();
737 // The Tile16 editor rebuilds selected-tile previews from current_gfx_bmp.
738 // Keep that source data aligned with the current map before set_palette()
739 // reloads Tile8s and remaps the sheet to the selected brush row.
740 SyncCurrentGraphicsBitmapForTile16Editor(ctx_, *current_map);
741 // Tile16Editor::set_palette refreshes the Tile8 source bitmap with the
742 // selected brush palette. Keeping that remap intact makes the source sheet,
743 // held Tile8 preview, and painted Tile16 pixels agree after blockset refresh.
745
746 const auto& tile16_data = current_map->current_tile16_blockset();
747
750
751 // Queue texture update for the atlas
756 } else if (!ctx_.tile16_blockset->atlas.texture() &&
758 // Create texture if it doesn't exist yet
761 }
762
763 return absl::OkStatus();
764}
765
767 // Skip if blockset not loaded or no pending changes
769 return;
770 }
771
773 return;
774 }
775
776 // Validate the atlas bitmap before modifying
778 ctx_.tile16_blockset->atlas.vector().empty() ||
781 return;
782 }
783
784 // Calculate tile positions in the atlas (8 tiles per row, each 16x16)
785 constexpr int kTilesPerRow = 8;
786 constexpr int kTileSize = 16;
787 int atlas_width = ctx_.tile16_blockset->atlas.width();
788 int atlas_height = ctx_.tile16_blockset->atlas.height();
789
790 bool atlas_modified = false;
791
792 // Iterate through all possible tile IDs to check for modifications
793 // Note: This is a brute-force approach; a more efficient method would
794 // maintain a list of modified tile IDs
795 for (int tile_id = 0; tile_id < zelda3::kNumTile16Individual; ++tile_id) {
796 if (!ctx_.tile16_editor->is_tile_modified(tile_id)) {
797 continue;
798 }
799
800 // Get the pending bitmap for this tile
801 const gfx::Bitmap* pending_bmp =
803 if (!pending_bmp || !pending_bmp->is_active() ||
804 pending_bmp->vector().empty()) {
805 continue;
806 }
807
808 // Calculate position in the atlas
809 int tile_x = (tile_id % kTilesPerRow) * kTileSize;
810 int tile_y = (tile_id / kTilesPerRow) * kTileSize;
811
812 // Validate tile position is within atlas bounds
813 if (tile_x + kTileSize > atlas_width || tile_y + kTileSize > atlas_height) {
814 continue;
815 }
816
817 // Copy pending bitmap data into the atlas at the correct position
818 auto& atlas_data = ctx_.tile16_blockset->atlas.mutable_data();
819 const auto& pending_data = pending_bmp->vector();
820
821 for (int y = 0; y < kTileSize && y < pending_bmp->height(); ++y) {
822 for (int x = 0; x < kTileSize && x < pending_bmp->width(); ++x) {
823 int atlas_idx = (tile_y + y) * atlas_width + (tile_x + x);
824 int pending_idx = y * pending_bmp->width() + x;
825
826 if (atlas_idx >= 0 && atlas_idx < static_cast<int>(atlas_data.size()) &&
827 pending_idx >= 0 &&
828 pending_idx < static_cast<int>(pending_data.size())) {
829 atlas_data[atlas_idx] = pending_data[pending_idx];
830 atlas_modified = true;
831 }
832 }
833 }
834 }
835
836 // Only queue texture update if we actually modified something
837 if (atlas_modified && ctx_.tile16_blockset->atlas.texture()) {
841 }
842}
843
844} // namespace yaze::editor
void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap *map)
Safely refresh multi-area maps without recursion.
void RefreshChildMapOnDemand(int map_index)
On-demand child map refresh with selective updates.
void RefreshChildMap(int map_index)
Refresh a child map's graphics pipeline (legacy full rebuild)
absl::Status RefreshPaletteUsers(const std::string &group, int palette_index)
void InvalidateTile16Definitions()
Shared Tile16 definitions affect maps in every world, including hidden maps.
void InvalidateGraphicsCache(int map_id=-1)
Invalidate cached graphics for a specific map or all maps.
void RefreshOverworldMap()
Refresh the current overworld map.
absl::Status RefreshTile16Blockset()
Refresh the tile16 blockset after graphics/palette changes.
absl::Status RefreshMapPalette(int map_id=-1)
Refresh map palette after palette property changes.
void RefreshMapProperties(int map_id=-1)
Refresh map properties (copy parent properties to siblings)
void RefreshSiblingMapGraphics(int map_index, bool include_self=false)
Refresh sibling map graphics for multi-area maps.
void UpdateBlocksetWithPendingTileChanges()
Update blockset atlas with pending tile16 editor changes.
void RefreshOverworldMapOnDemand(int map_index)
On-demand map refresh that only updates what's actually needed.
void ForceRefreshGraphics(int map_index)
Force refresh graphics for a specific map.
const gfx::Bitmap * GetPendingTileBitmap(int tile_id) const
bool is_tile_modified(int tile_id) const
void set_palette(const gfx::SnesPalette &palette)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
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
TextureHandle texture() const
Definition bitmap.h:410
const std::vector< uint8_t > & vector() const
Definition bitmap.h:411
bool is_active() const
Definition bitmap.h:414
void set_modified(bool modified)
Definition bitmap.h:418
int height() const
Definition bitmap.h:399
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:901
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
int depth() const
Definition bitmap.h:400
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:408
Represents a single Overworld map screen.
const std::vector< uint8_t > & current_graphics() const
void set_game_state(int state)
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)
void SyncAreaProperties(int map_index)
Copy the area parent's render settings (main palette, animated GFX, tile GFX groups,...
void ClearGraphicsConfigCache()
Drop every cached tileset/blockset. Call after graphics buffer or tile16 edits that are not tied to o...
Definition overworld.h:502
void InvalidateSiblingMapCaches(int map_index)
Invalidate cached tilesets for a map and all its siblings.
const std::vector< gfx::Tile16 > & tiles16() const
Definition overworld.h:751
auto overworld_map(int i) const
Definition overworld.h:737
auto mutable_overworld_map(int i)
Definition overworld.h:743
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
void set_game_state(int state)
Definition overworld.cc:132
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:721
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
void SyncCurrentGraphicsBitmapForTile16Editor(const MapRefreshContext &ctx, const zelda3::OverworldMap &map)
zelda3::OverworldBlockset & MapTilesForMapIndex(zelda3::Overworld *overworld, int map_index)
void PrepareMapForRefresh(const MapRefreshContext &ctx, zelda3::OverworldMap *map)
Editors are the view controllers for the application.
void UpdateTilemap(IRenderer *renderer, Tilemap &tilemap, const std::vector< uint8_t > &data)
Definition tilemap.cc:34
constexpr int kNumTile16Individual
Definition overworld.h:243
constexpr int kSpecialWorldMapIdStart
constexpr int kNumOverworldMaps
Definition common.h:85
std::vector< std::vector< uint16_t > > OverworldBlockset
Represents tile32 data for the overworld.
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int kDarkWorldMapIdStart
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Context struct holding all data dependencies for map refresh operations. All pointers/references must...
std::array< gfx::Bitmap, zelda3::kExpandedMapCount > * maps_bmp
std::function< void(int map_index)> ensure_map_texture
Callback to ensure a map texture is created (stays in OverworldEditor)
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119