Implement safe multi-area map refresh and critical tile update fixes
- Added RefreshMultiAreaMapsSafely method to handle large, wide, and tall area maps without recursion, preventing segmentation faults. - Updated tile application logic to directly use the currently selected tile, enhancing stability during rectangle selections. - Improved logging for tile updates and error handling, ensuring better debugging and user feedback. - Cleared selection state after applying changes to maintain a clean editor state.
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <set>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -925,17 +926,11 @@ void OverworldEditor::CheckForOverworldEdits() {
|
|||||||
// Number of tiles per local map (since each tile is 16x16)
|
// Number of tiles per local map (since each tile is 16x16)
|
||||||
constexpr int tiles_per_local_map = local_map_size / kTile16Size;
|
constexpr int tiles_per_local_map = local_map_size / kTile16Size;
|
||||||
|
|
||||||
util::logf("CheckForOverworldEdits: About to process %zu selected tiles", ow_map_canvas_.selected_tiles().size());
|
util::logf("CheckForOverworldEdits: About to fill rectangle with current_tile16_=%d", current_tile16_);
|
||||||
|
|
||||||
for (int y = start_y, i = 0; y <= end_y; y += kTile16Size) {
|
// Apply the current selected tile to each position in the rectangle
|
||||||
for (int x = start_x; x <= end_x; x += kTile16Size, ++i) {
|
for (int y = start_y; y <= end_y; y += kTile16Size) {
|
||||||
// Bounds check to prevent crashes
|
for (int x = start_x; x <= end_x; x += kTile16Size) {
|
||||||
if (i >= static_cast<int>(ow_map_canvas_.selected_tiles().size())) {
|
|
||||||
util::logf("ERROR: Rectangle selection index %d out of bounds (size: %zu)",
|
|
||||||
i, ow_map_canvas_.selected_tiles().size());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which local map (512x512) the tile is in
|
// Determine which local map (512x512) the tile is in
|
||||||
int local_map_x = x / local_map_size;
|
int local_map_x = x / local_map_size;
|
||||||
int local_map_y = y / local_map_size;
|
int local_map_y = y / local_map_size;
|
||||||
@@ -947,17 +942,32 @@ void OverworldEditor::CheckForOverworldEdits() {
|
|||||||
// Calculate the index within the overall map structure
|
// Calculate the index within the overall map structure
|
||||||
int index_x = local_map_x * tiles_per_local_map + tile16_x;
|
int index_x = local_map_x * tiles_per_local_map + tile16_x;
|
||||||
int index_y = local_map_y * tiles_per_local_map + tile16_y;
|
int index_y = local_map_y * tiles_per_local_map + tile16_y;
|
||||||
overworld_.set_current_world(current_world_);
|
|
||||||
overworld_.set_current_map(current_map_);
|
// Bounds check for the selected world array
|
||||||
int tile16_id = overworld_.GetTileFromPosition(
|
if (index_x >= 0 && index_x < 0x200 && index_y >= 0 && index_y < 0x200) {
|
||||||
ow_map_canvas_.selected_tiles()[i]);
|
// CRITICAL FIX: Set the tile to the currently selected tile16, not read from canvas
|
||||||
selected_world[index_x][index_y] = tile16_id;
|
selected_world[index_x][index_y] = current_tile16_;
|
||||||
|
|
||||||
|
// CRITICAL FIX: Also update the bitmap directly like single tile drawing
|
||||||
|
ImVec2 tile_position(x, y);
|
||||||
|
auto tile_data = gfx::GetTilemapData(tile16_blockset_, current_tile16_);
|
||||||
|
if (!tile_data.empty()) {
|
||||||
|
RenderUpdatedMapBitmap(tile_position, tile_data);
|
||||||
|
util::logf("CheckForOverworldEdits: Updated bitmap at position (%d,%d) with tile16_id=%d",
|
||||||
|
x, y, current_tile16_);
|
||||||
|
} else {
|
||||||
|
util::logf("ERROR: Failed to get tile data for tile16_id=%d", current_tile16_);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
util::logf("ERROR: Rectangle selection position [%d,%d] out of bounds", index_x, index_y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
util::logf("CheckForOverworldEdits: About to call RefreshOverworldMap");
|
// Clear the rectangle selection after applying
|
||||||
RefreshOverworldMap();
|
ow_map_canvas_.mutable_selected_tiles()->clear();
|
||||||
util::logf("CheckForOverworldEdits: RefreshOverworldMap completed");
|
ow_map_canvas_.mutable_points()->clear();
|
||||||
|
util::logf("CheckForOverworldEdits: Rectangle selection applied and cleared");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1991,13 +2001,141 @@ void OverworldEditor::RefreshChildMapOnDemand(int map_index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Disable large map sibling refresh to prevent infinite recursion
|
// Handle multi-area maps (large, wide, tall) with safe coordination
|
||||||
// The large map sibling refresh logic was causing infinite recursion when
|
RefreshMultiAreaMapsSafely(map_index, map);
|
||||||
// editing map 00 (and other large maps), leading to segmentation faults.
|
}
|
||||||
// For now, we'll handle large maps individually to prevent crashes.
|
|
||||||
// TODO: Implement safer large map coordination without recursion
|
/**
|
||||||
if (map->is_large_map()) {
|
* @brief Safely refresh multi-area maps without recursion
|
||||||
util::logf("RefreshChildMapOnDemand: Large map %d detected, skipping sibling refresh to prevent recursion", map_index);
|
*
|
||||||
|
* This function handles the coordination of large, wide, and tall area maps
|
||||||
|
* by using a non-recursive approach with explicit map list processing.
|
||||||
|
* It respects the ZScream area size logic and prevents infinite recursion.
|
||||||
|
*/
|
||||||
|
void OverworldEditor::RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap* map) {
|
||||||
|
using zelda3::AreaSizeEnum;
|
||||||
|
|
||||||
|
// Skip if this is already a processed sibling to avoid double-processing
|
||||||
|
static std::set<int> currently_processing;
|
||||||
|
if (currently_processing.count(map_index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto area_size = map->area_size();
|
||||||
|
if (area_size == AreaSizeEnum::SmallArea) {
|
||||||
|
return; // No siblings to coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Processing %s area map %d (parent: %d)",
|
||||||
|
(area_size == AreaSizeEnum::LargeArea) ? "large" :
|
||||||
|
(area_size == AreaSizeEnum::WideArea) ? "wide" : "tall",
|
||||||
|
map_index, map->parent());
|
||||||
|
|
||||||
|
// Determine all maps that are part of this multi-area structure
|
||||||
|
std::vector<int> sibling_maps;
|
||||||
|
int parent_id = map->parent();
|
||||||
|
|
||||||
|
// Use the same logic as ZScream for area coordination
|
||||||
|
switch (area_size) {
|
||||||
|
case AreaSizeEnum::LargeArea: {
|
||||||
|
// Large Area: 2x2 grid (4 maps total)
|
||||||
|
// Parent is top-left (quadrant 0), siblings are:
|
||||||
|
// +1 (top-right, quadrant 1), +8 (bottom-left, quadrant 2), +9 (bottom-right, quadrant 3)
|
||||||
|
sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Large area siblings: %d, %d, %d, %d",
|
||||||
|
parent_id, parent_id + 1, parent_id + 8, parent_id + 9);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case AreaSizeEnum::WideArea: {
|
||||||
|
// Wide Area: 2x1 grid (2 maps total, horizontally adjacent)
|
||||||
|
// Parent is left, sibling is +1 (right)
|
||||||
|
sibling_maps = {parent_id, parent_id + 1};
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Wide area siblings: %d, %d",
|
||||||
|
parent_id, parent_id + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case AreaSizeEnum::TallArea: {
|
||||||
|
// Tall Area: 1x2 grid (2 maps total, vertically adjacent)
|
||||||
|
// Parent is top, sibling is +8 (bottom)
|
||||||
|
sibling_maps = {parent_id, parent_id + 8};
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Tall area siblings: %d, %d",
|
||||||
|
parent_id, parent_id + 8);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Unknown area size %d for map %d",
|
||||||
|
static_cast<int>(area_size), map_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all siblings as being processed to prevent recursion
|
||||||
|
for (int sibling : sibling_maps) {
|
||||||
|
currently_processing.insert(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only refresh siblings that are visible/current and need updating
|
||||||
|
for (int sibling : sibling_maps) {
|
||||||
|
if (sibling == map_index) {
|
||||||
|
continue; // Skip self (already processed above)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check
|
||||||
|
if (sibling < 0 || sibling >= zelda3::kNumOverworldMaps) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only refresh if it's visible or current
|
||||||
|
bool is_current_map = (sibling == current_map_);
|
||||||
|
bool is_current_world = (sibling / 0x40 == current_world_);
|
||||||
|
bool needs_refresh = maps_bmp_[sibling].modified();
|
||||||
|
|
||||||
|
if ((is_current_map || is_current_world) && needs_refresh) {
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Refreshing %s area sibling map %d (parent: %d)",
|
||||||
|
(area_size == AreaSizeEnum::LargeArea) ? "large" :
|
||||||
|
(area_size == AreaSizeEnum::WideArea) ? "wide" : "tall",
|
||||||
|
sibling, parent_id);
|
||||||
|
|
||||||
|
// Direct refresh without calling RefreshChildMapOnDemand to avoid recursion
|
||||||
|
auto* sibling_map = overworld_.mutable_overworld_map(sibling);
|
||||||
|
if (sibling_map && maps_bmp_[sibling].modified()) {
|
||||||
|
sibling_map->LoadAreaGraphics();
|
||||||
|
|
||||||
|
auto status = sibling_map->BuildTileset();
|
||||||
|
if (status.ok()) {
|
||||||
|
status = sibling_map->BuildTiles16Gfx(*overworld_.mutable_tiles16(), overworld_.tiles16().size());
|
||||||
|
if (status.ok()) {
|
||||||
|
status = sibling_map->BuildBitmap(overworld_.GetMapTiles(current_world_));
|
||||||
|
if (status.ok()) {
|
||||||
|
maps_bmp_[sibling].set_data(sibling_map->bitmap_data());
|
||||||
|
maps_bmp_[sibling].set_modified(false);
|
||||||
|
|
||||||
|
// Update texture if it exists
|
||||||
|
if (maps_bmp_[sibling].texture()) {
|
||||||
|
core::Renderer::Get().UpdateBitmap(&maps_bmp_[sibling]);
|
||||||
|
} else {
|
||||||
|
EnsureMapTexture(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.ok()) {
|
||||||
|
util::logf("RefreshMultiAreaMapsSafely: Failed to refresh sibling map %d: %s",
|
||||||
|
sibling, status.message().data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!is_current_map && !is_current_world) {
|
||||||
|
// Mark non-visible siblings for deferred refresh
|
||||||
|
maps_bmp_[sibling].set_modified(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear processing set after completion
|
||||||
|
for (int sibling : sibling_maps) {
|
||||||
|
currently_processing.erase(sibling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
|||||||
void RefreshOverworldMap();
|
void RefreshOverworldMap();
|
||||||
void RefreshOverworldMapOnDemand(int map_index);
|
void RefreshOverworldMapOnDemand(int map_index);
|
||||||
void RefreshChildMapOnDemand(int map_index);
|
void RefreshChildMapOnDemand(int map_index);
|
||||||
|
void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap* map);
|
||||||
absl::Status RefreshMapPalette();
|
absl::Status RefreshMapPalette();
|
||||||
void RefreshMapProperties();
|
void RefreshMapProperties();
|
||||||
absl::Status RefreshTile16Blockset();
|
absl::Status RefreshTile16Blockset();
|
||||||
|
|||||||
Reference in New Issue
Block a user