refactor: Enhance Map Properties System with Graphics Management and Refresh Logic
- Introduced OverworldGraphicsManager to handle graphics loading, refreshing, and texture management for the overworld editor. - Updated MapPropertiesSystem to integrate new graphics management features, ensuring proper refresh order for map properties and graphics. - Added methods for forcing graphics refresh and handling sibling map graphics, improving the responsiveness of the editor. - Enhanced UI elements in the MapPropertiesSystem for better user experience, including tooltips and organized layouts. - Updated CMake configuration to include new graphics manager files, ensuring proper integration into the build system.
This commit is contained in:
@@ -36,6 +36,7 @@ set(
|
||||
app/editor/graphics/gfx_group_editor.cc
|
||||
app/editor/overworld/entity.cc
|
||||
app/editor/overworld/overworld_entity_renderer.cc
|
||||
app/editor/overworld/overworld_graphics_manager.cc
|
||||
app/editor/system/settings_editor.cc
|
||||
app/editor/system/command_manager.cc
|
||||
app/editor/system/extension_manager.cc
|
||||
|
||||
@@ -460,7 +460,22 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) {
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_area_graphics(),
|
||||
kHexByteInputWidth)) {
|
||||
// CORRECT ORDER: Properties first, then graphics reload
|
||||
|
||||
// 1. Propagate properties to siblings FIRST (calls LoadAreaGraphics on siblings)
|
||||
RefreshMapProperties();
|
||||
|
||||
// 2. Force immediate refresh of current map
|
||||
(*maps_bmp_)[current_map].set_modified(true);
|
||||
overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics();
|
||||
|
||||
// 3. Refresh siblings immediately
|
||||
RefreshSiblingMapGraphics(current_map);
|
||||
|
||||
// 4. Update tile selector
|
||||
RefreshTile16Blockset();
|
||||
|
||||
// 5. Final refresh
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
HOVER_HINT("Main tileset graphics for this map area");
|
||||
@@ -472,6 +487,7 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) {
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_sprite_graphics(game_state),
|
||||
kHexByteInputWidth)) {
|
||||
ForceRefreshGraphics(current_map);
|
||||
RefreshMapProperties();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
@@ -484,34 +500,48 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) {
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_animated_gfx(),
|
||||
kHexByteInputWidth)) {
|
||||
ForceRefreshGraphics(current_map);
|
||||
RefreshMapProperties();
|
||||
RefreshTile16Blockset();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
HOVER_HINT("Animated tile graphics (water, lava, etc.)");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics");
|
||||
ImGui::Separator();
|
||||
// Custom Tile Graphics - Only available for v1+ ROMs
|
||||
if (asm_version >= 1 && asm_version != 0xFF) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics");
|
||||
ImGui::Separator();
|
||||
|
||||
// Show the 8 custom graphics IDs in a 2-column layout for density
|
||||
if (BeginTable("CustomTileGraphics", 2,
|
||||
ImGuiTableFlags_SizingFixedFit)) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
TableNextColumn();
|
||||
std::string label = absl::StrFormat(ICON_MD_LAYERS " Sheet %d", i);
|
||||
if (gui::InputHexByte(label.c_str(),
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_custom_tileset(i),
|
||||
90.f)) {
|
||||
RefreshMapProperties();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i);
|
||||
// Show the 8 custom graphics IDs in a 2-column layout for density
|
||||
if (BeginTable("CustomTileGraphics", 2,
|
||||
ImGuiTableFlags_SizingFixedFit)) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
TableNextColumn();
|
||||
std::string label = absl::StrFormat(ICON_MD_LAYERS " Sheet %d", i);
|
||||
if (gui::InputHexByte(label.c_str(),
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_custom_tileset(i),
|
||||
90.f)) {
|
||||
ForceRefreshGraphics(current_map);
|
||||
RefreshMapProperties();
|
||||
RefreshTile16Blockset();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i);
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
} else if (asm_version == 0xFF) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
|
||||
ICON_MD_INFO " Custom Tile Graphics");
|
||||
ImGui::TextWrapped(
|
||||
"Custom tile graphics require ZSCustomOverworld v1+.\n"
|
||||
"Upgrade your ROM to access 8 customizable graphics sheets.");
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2); // Pop the 2 style variables we pushed
|
||||
@@ -693,7 +723,12 @@ void MapPropertiesSystem::DrawBasicPropertiesTab(int current_map) {
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_area_graphics(),
|
||||
kInputFieldSize)) {
|
||||
// CORRECT ORDER: Properties first, then graphics reload
|
||||
RefreshMapProperties();
|
||||
(*maps_bmp_)[current_map].set_modified(true);
|
||||
overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics();
|
||||
RefreshSiblingMapGraphics(current_map);
|
||||
RefreshTile16Blockset();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -955,36 +990,55 @@ void MapPropertiesSystem::DrawCustomFeaturesTab(int current_map) {
|
||||
}
|
||||
|
||||
void MapPropertiesSystem::DrawTileGraphicsTab(int current_map) {
|
||||
ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics (8 sheets)");
|
||||
Separator();
|
||||
|
||||
if (BeginTable("TileGraphics", 2,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
|
||||
ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 180);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
TableNextColumn();
|
||||
ImGui::Text(ICON_MD_LAYERS " Sheet %d", i);
|
||||
TableNextColumn();
|
||||
if (gui::InputHexByte(absl::StrFormat("##TileGfx%d", i).c_str(),
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_custom_tileset(i),
|
||||
kInputFieldSize)) {
|
||||
RefreshMapProperties();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
static uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
|
||||
Separator();
|
||||
ImGui::TextWrapped("These 8 sheets allow custom tile graphics per map. "
|
||||
"Each sheet references a graphics ID loaded into VRAM.");
|
||||
// Only show custom tile graphics for v1+ ROMs
|
||||
if (asm_version >= 1 && asm_version != 0xFF) {
|
||||
ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics (8 sheets)");
|
||||
Separator();
|
||||
|
||||
if (BeginTable("TileGraphics", 2,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
|
||||
ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 180);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
TableNextColumn();
|
||||
ImGui::Text(ICON_MD_LAYERS " Sheet %d", i);
|
||||
TableNextColumn();
|
||||
if (gui::InputHexByte(absl::StrFormat("##TileGfx%d", i).c_str(),
|
||||
overworld_->mutable_overworld_map(current_map)
|
||||
->mutable_custom_tileset(i),
|
||||
kInputFieldSize)) {
|
||||
overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics();
|
||||
ForceRefreshGraphics(current_map);
|
||||
RefreshSiblingMapGraphics(current_map);
|
||||
RefreshMapProperties();
|
||||
RefreshTile16Blockset();
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
Separator();
|
||||
ImGui::TextWrapped("These 8 sheets allow custom tile graphics per map. "
|
||||
"Each sheet references a graphics ID loaded into VRAM.");
|
||||
} else {
|
||||
// Vanilla ROM - show info message
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
|
||||
ICON_MD_INFO " Custom Tile Graphics");
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped(
|
||||
"Custom tile graphics are not available in vanilla ROMs.\n\n"
|
||||
"To enable this feature, upgrade your ROM to ZSCustomOverworld v1+, "
|
||||
"which provides 8 customizable graphics sheets per map for advanced "
|
||||
"tileset customization.");
|
||||
}
|
||||
}
|
||||
|
||||
void MapPropertiesSystem::DrawMusicTab(int current_map) {
|
||||
@@ -1090,6 +1144,70 @@ absl::Status MapPropertiesSystem::RefreshMapPalette() {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status MapPropertiesSystem::RefreshTile16Blockset() {
|
||||
if (refresh_tile16_blockset_) {
|
||||
return refresh_tile16_blockset_();
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void MapPropertiesSystem::ForceRefreshGraphics(int map_index) {
|
||||
if (force_refresh_graphics_) {
|
||||
force_refresh_graphics_(map_index);
|
||||
}
|
||||
}
|
||||
|
||||
void MapPropertiesSystem::RefreshSiblingMapGraphics(int map_index, bool include_self) {
|
||||
if (!overworld_ || !maps_bmp_ || map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* map = overworld_->mutable_overworld_map(map_index);
|
||||
if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
|
||||
return; // No siblings for small areas
|
||||
}
|
||||
|
||||
int parent_id = map->parent();
|
||||
std::vector<int> siblings;
|
||||
|
||||
switch (map->area_size()) {
|
||||
case zelda3::AreaSizeEnum::LargeArea:
|
||||
siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
|
||||
break;
|
||||
case zelda3::AreaSizeEnum::WideArea:
|
||||
siblings = {parent_id, parent_id + 1};
|
||||
break;
|
||||
case zelda3::AreaSizeEnum::TallArea:
|
||||
siblings = {parent_id, parent_id + 8};
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
for (int sibling : siblings) {
|
||||
if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) {
|
||||
// Skip self unless include_self is true
|
||||
if (sibling == map_index && !include_self) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark as modified FIRST
|
||||
(*maps_bmp_)[sibling].set_modified(true);
|
||||
|
||||
// Load graphics from ROM
|
||||
overworld_->mutable_overworld_map(sibling)->LoadAreaGraphics();
|
||||
|
||||
// CRITICAL FIX: Force immediate refresh on the sibling
|
||||
// This will trigger the callback to OverworldEditor's RefreshChildMapOnDemand
|
||||
ForceRefreshGraphics(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
// After marking all siblings, trigger a refresh
|
||||
// This ensures all marked maps get processed
|
||||
RefreshOverworldMap();
|
||||
}
|
||||
|
||||
void MapPropertiesSystem::DrawMosaicControls(int current_map) {
|
||||
static uint8_t asm_version =
|
||||
(*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
|
||||
@@ -22,6 +22,7 @@ class MapPropertiesSystem {
|
||||
// Callback types for refresh operations
|
||||
using RefreshCallback = std::function<void()>;
|
||||
using RefreshPaletteCallback = std::function<absl::Status()>;
|
||||
using ForceRefreshGraphicsCallback = std::function<void(int)>;
|
||||
|
||||
explicit MapPropertiesSystem(zelda3::Overworld* overworld, Rom* rom,
|
||||
std::array<gfx::Bitmap, zelda3::kNumOverworldMaps>* maps_bmp = nullptr,
|
||||
@@ -31,10 +32,14 @@ class MapPropertiesSystem {
|
||||
// Set callbacks for refresh operations
|
||||
void SetRefreshCallbacks(RefreshCallback refresh_map_properties,
|
||||
RefreshCallback refresh_overworld_map,
|
||||
RefreshPaletteCallback refresh_map_palette) {
|
||||
RefreshPaletteCallback refresh_map_palette,
|
||||
RefreshPaletteCallback refresh_tile16_blockset = nullptr,
|
||||
ForceRefreshGraphicsCallback force_refresh_graphics = nullptr) {
|
||||
refresh_map_properties_ = std::move(refresh_map_properties);
|
||||
refresh_overworld_map_ = std::move(refresh_overworld_map);
|
||||
refresh_map_palette_ = std::move(refresh_map_palette);
|
||||
refresh_tile16_blockset_ = std::move(refresh_tile16_blockset);
|
||||
force_refresh_graphics_ = std::move(force_refresh_graphics);
|
||||
}
|
||||
|
||||
// Main interface methods
|
||||
@@ -85,6 +90,11 @@ class MapPropertiesSystem {
|
||||
void RefreshMapProperties();
|
||||
void RefreshOverworldMap();
|
||||
absl::Status RefreshMapPalette();
|
||||
absl::Status RefreshTile16Blockset();
|
||||
void ForceRefreshGraphics(int map_index);
|
||||
|
||||
// Helper to refresh sibling map graphics for multi-area maps
|
||||
void RefreshSiblingMapGraphics(int map_index, bool include_self = false);
|
||||
|
||||
zelda3::Overworld* overworld_;
|
||||
Rom* rom_;
|
||||
@@ -95,6 +105,8 @@ class MapPropertiesSystem {
|
||||
RefreshCallback refresh_map_properties_;
|
||||
RefreshCallback refresh_overworld_map_;
|
||||
RefreshPaletteCallback refresh_map_palette_;
|
||||
RefreshPaletteCallback refresh_tile16_blockset_;
|
||||
ForceRefreshGraphicsCallback force_refresh_graphics_;
|
||||
|
||||
// Using centralized UI constants from ui_constants.h
|
||||
};
|
||||
|
||||
@@ -54,7 +54,9 @@ void OverworldEditor::Initialize() {
|
||||
map_properties_system_->SetRefreshCallbacks(
|
||||
[this]() { this->RefreshMapProperties(); },
|
||||
[this]() { this->RefreshOverworldMap(); },
|
||||
[this]() -> absl::Status { return this->RefreshMapPalette(); }
|
||||
[this]() -> absl::Status { return this->RefreshMapPalette(); },
|
||||
[this]() -> absl::Status { return this->RefreshTile16Blockset(); },
|
||||
[this](int map_index) { this->ForceRefreshGraphics(map_index); }
|
||||
);
|
||||
|
||||
// Initialize OverworldEditorManager for v3 features
|
||||
@@ -343,18 +345,30 @@ void OverworldEditor::DrawToolset() {
|
||||
toolbar.AddSeparator();
|
||||
|
||||
// Inline map properties with icon labels - use toolbar methods for consistency
|
||||
if (toolbar.AddProperty(ICON_MD_IMAGE, "##gfx",
|
||||
if (toolbar.AddProperty(ICON_MD_IMAGE, " Gfx",
|
||||
overworld_.mutable_overworld_map(current_map_)->mutable_area_graphics(),
|
||||
[this]() {
|
||||
// CORRECT ORDER: Properties first, then graphics reload
|
||||
|
||||
// 1. Propagate properties to siblings FIRST (this also calls LoadAreaGraphics on siblings)
|
||||
RefreshMapProperties();
|
||||
RefreshOverworldMap();
|
||||
|
||||
// 2. Force immediate refresh of current map and all siblings
|
||||
maps_bmp_[current_map_].set_modified(true);
|
||||
RefreshChildMapOnDemand(current_map_);
|
||||
RefreshSiblingMapGraphics(current_map_);
|
||||
|
||||
// 3. Update tile selector
|
||||
RefreshTile16Blockset();
|
||||
})) {
|
||||
// Property changed
|
||||
}
|
||||
|
||||
if (toolbar.AddProperty(ICON_MD_PALETTE, "##pal",
|
||||
if (toolbar.AddProperty(ICON_MD_PALETTE, " Pal",
|
||||
overworld_.mutable_overworld_map(current_map_)->mutable_area_palette(),
|
||||
[this]() {
|
||||
// Palette changes also need to propagate to siblings
|
||||
RefreshSiblingMapGraphics(current_map_);
|
||||
RefreshMapProperties();
|
||||
status_ = RefreshMapPalette();
|
||||
RefreshOverworldMap();
|
||||
@@ -406,13 +420,6 @@ void OverworldEditor::DrawToolset() {
|
||||
show_gfx_groups_ = !show_gfx_groups_;
|
||||
}
|
||||
|
||||
toolbar.AddSeparator();
|
||||
|
||||
// v3 Settings and Usage Statistics
|
||||
toolbar.AddV3StatusBadge(asm_version, [this]() {
|
||||
show_v3_settings_ = !show_v3_settings_;
|
||||
});
|
||||
|
||||
if (toolbar.AddUsageStatsButton("Open Usage Statistics")) {
|
||||
show_usage_stats_ = !show_usage_stats_;
|
||||
}
|
||||
@@ -1639,13 +1646,11 @@ void OverworldEditor::RefreshChildMapOnDemand(int map_index) {
|
||||
map_index);
|
||||
}
|
||||
|
||||
// Update texture on main thread
|
||||
if (maps_bmp_[map_index].texture()) {
|
||||
Renderer::Get().UpdateBitmap(&maps_bmp_[map_index]);
|
||||
} else {
|
||||
// Create texture if it doesn't exist
|
||||
EnsureMapTexture(map_index);
|
||||
}
|
||||
// CRITICAL FIX: Force COMPLETE texture recreation for immediate visibility
|
||||
// UpdateBitmap() was still deferred - we need to force a full re-render
|
||||
|
||||
// Always recreate the texture to ensure immediate GPU update
|
||||
Renderer::Get().RenderBitmap(&maps_bmp_[map_index]);
|
||||
}
|
||||
|
||||
// Handle multi-area maps (large, wide, tall) with safe coordination
|
||||
@@ -1888,6 +1893,67 @@ absl::Status OverworldEditor::RefreshMapPalette() {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void OverworldEditor::ForceRefreshGraphics(int map_index) {
|
||||
// Mark the bitmap as modified to force refresh on next update
|
||||
if (map_index >= 0 && map_index < static_cast<int>(maps_bmp_.size())) {
|
||||
maps_bmp_[map_index].set_modified(true);
|
||||
|
||||
// Clear blockset cache
|
||||
current_blockset_ = 0xFF;
|
||||
|
||||
LOG_INFO("OverworldEditor", "ForceRefreshGraphics: Map %d marked for refresh", map_index);
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldEditor::RefreshSiblingMapGraphics(int map_index, bool include_self) {
|
||||
if (map_index < 0 || map_index >= static_cast<int>(maps_bmp_.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* map = overworld_.mutable_overworld_map(map_index);
|
||||
if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) {
|
||||
return; // No siblings for small areas
|
||||
}
|
||||
|
||||
int parent_id = map->parent();
|
||||
std::vector<int> siblings;
|
||||
|
||||
switch (map->area_size()) {
|
||||
case zelda3::AreaSizeEnum::LargeArea:
|
||||
siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
|
||||
break;
|
||||
case zelda3::AreaSizeEnum::WideArea:
|
||||
siblings = {parent_id, parent_id + 1};
|
||||
break;
|
||||
case zelda3::AreaSizeEnum::TallArea:
|
||||
siblings = {parent_id, parent_id + 8};
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
for (int sibling : siblings) {
|
||||
if (sibling >= 0 && sibling < 0xA0) {
|
||||
// Skip self unless include_self is true
|
||||
if (sibling == map_index && !include_self) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark as modified FIRST before loading
|
||||
maps_bmp_[sibling].set_modified(true);
|
||||
|
||||
// Load graphics from ROM
|
||||
overworld_.mutable_overworld_map(sibling)->LoadAreaGraphics();
|
||||
|
||||
// CRITICAL FIX: Bypass visibility check - force immediate refresh
|
||||
// Call RefreshChildMapOnDemand() directly instead of RefreshOverworldMapOnDemand()
|
||||
RefreshChildMapOnDemand(sibling);
|
||||
|
||||
LOG_INFO("OverworldEditor", "RefreshSiblingMapGraphics: Refreshed sibling map %d", sibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldEditor::RefreshMapProperties() {
|
||||
const auto& current_ow_map = *overworld_.mutable_overworld_map(current_map_);
|
||||
|
||||
@@ -1935,6 +2001,9 @@ void OverworldEditor::RefreshMapProperties() {
|
||||
map.set_sprite_palette(game_state_,
|
||||
current_ow_map.sprite_palette(game_state_));
|
||||
map.set_message_id(current_ow_map.message_id());
|
||||
|
||||
// CRITICAL FIX: Reload graphics after changing properties
|
||||
map.LoadAreaGraphics();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1954,6 +2023,9 @@ void OverworldEditor::RefreshMapProperties() {
|
||||
map.set_sprite_palette(game_state_,
|
||||
current_ow_map.sprite_palette(game_state_));
|
||||
map.set_message_id(current_ow_map.message_id());
|
||||
|
||||
// CRITICAL FIX: Reload graphics after changing properties
|
||||
map.LoadAreaGraphics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ class OverworldEditor : public Editor, public gfx::GfxContext {
|
||||
absl::Status RefreshMapPalette();
|
||||
void RefreshMapProperties();
|
||||
absl::Status RefreshTile16Blockset();
|
||||
void ForceRefreshGraphics(int map_index);
|
||||
void RefreshSiblingMapGraphics(int map_index, bool include_self = false);
|
||||
|
||||
void DrawOverworldMaps();
|
||||
void DrawOverworldEdits();
|
||||
|
||||
714
src/app/editor/overworld/overworld_graphics_manager.cc
Normal file
714
src/app/editor/overworld/overworld_graphics_manager.cc
Normal file
@@ -0,0 +1,714 @@
|
||||
#include "overworld_graphics_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "app/core/features.h"
|
||||
#include "app/core/window.h"
|
||||
#include "app/gfx/performance_profiler.h"
|
||||
#include "util/log.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
using core::Renderer;
|
||||
using zelda3::kNumOverworldMaps;
|
||||
using zelda3::kOverworldMapSize;
|
||||
|
||||
constexpr int kTile16Size = 16;
|
||||
|
||||
// ============================================================================
|
||||
// Loading Operations
|
||||
// ============================================================================
|
||||
|
||||
absl::Status OverworldGraphicsManager::LoadGraphics() {
|
||||
gfx::ScopedTimer timer("LoadGraphics");
|
||||
|
||||
LOG_INFO("OverworldGraphicsManager", "Loading overworld.");
|
||||
// Load the Link to the Past overworld.
|
||||
{
|
||||
gfx::ScopedTimer load_timer("Overworld::Load");
|
||||
RETURN_IF_ERROR(overworld_->Load(rom_));
|
||||
}
|
||||
*palette_ = overworld_->current_area_palette();
|
||||
|
||||
LOG_INFO("OverworldGraphicsManager", "Loading overworld graphics (optimized).");
|
||||
|
||||
// Phase 1: Create bitmaps without textures for faster loading
|
||||
// This avoids blocking the main thread with GPU texture creation
|
||||
{
|
||||
gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics");
|
||||
Renderer::Get().CreateBitmapWithoutTexture(0x80, kOverworldMapSize, 0x40,
|
||||
overworld_->current_graphics(),
|
||||
*current_gfx_bmp_, *palette_);
|
||||
}
|
||||
|
||||
LOG_INFO("OverworldGraphicsManager", "Loading overworld tileset (deferred textures).");
|
||||
{
|
||||
gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset");
|
||||
Renderer::Get().CreateBitmapWithoutTexture(
|
||||
0x80, 0x2000, 0x08, overworld_->tile16_blockset_data(),
|
||||
*tile16_blockset_bmp_, *palette_);
|
||||
}
|
||||
map_blockset_loaded_ = true;
|
||||
|
||||
// Copy the tile16 data into individual tiles.
|
||||
auto tile16_blockset_data = overworld_->tile16_blockset_data();
|
||||
LOG_INFO("OverworldGraphicsManager", "Loading overworld tile16 graphics.");
|
||||
|
||||
{
|
||||
gfx::ScopedTimer tilemap_timer("CreateTilemap");
|
||||
*tile16_blockset_ =
|
||||
gfx::CreateTilemap(tile16_blockset_data, 0x80, 0x2000, kTile16Size,
|
||||
zelda3::kNumTile16Individual, *palette_);
|
||||
}
|
||||
|
||||
// Phase 2: Create bitmaps only for essential maps initially
|
||||
// Non-essential maps will be created on-demand when accessed
|
||||
constexpr int kEssentialMapsPerWorld = 8;
|
||||
constexpr int kLightWorldEssential = kEssentialMapsPerWorld;
|
||||
constexpr int kDarkWorldEssential =
|
||||
zelda3::kDarkWorldMapIdStart + kEssentialMapsPerWorld;
|
||||
constexpr int kSpecialWorldEssential =
|
||||
zelda3::kSpecialWorldMapIdStart + kEssentialMapsPerWorld;
|
||||
|
||||
LOG_INFO("OverworldGraphicsManager",
|
||||
"Creating bitmaps for essential maps only (first %d maps per world)",
|
||||
kEssentialMapsPerWorld);
|
||||
|
||||
std::vector<gfx::Bitmap*> maps_to_texture;
|
||||
maps_to_texture.reserve(kEssentialMapsPerWorld *
|
||||
3); // 8 maps per world * 3 worlds
|
||||
|
||||
{
|
||||
gfx::ScopedTimer maps_timer("CreateEssentialOverworldMaps");
|
||||
for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
|
||||
bool is_essential = false;
|
||||
|
||||
// Check if this is an essential map
|
||||
if (i < kLightWorldEssential) {
|
||||
is_essential = true;
|
||||
} else if (i >= zelda3::kDarkWorldMapIdStart && i < kDarkWorldEssential) {
|
||||
is_essential = true;
|
||||
} else if (i >= zelda3::kSpecialWorldMapIdStart &&
|
||||
i < kSpecialWorldEssential) {
|
||||
is_essential = true;
|
||||
}
|
||||
|
||||
if (is_essential) {
|
||||
overworld_->set_current_map(i);
|
||||
auto palette = overworld_->current_area_palette();
|
||||
try {
|
||||
// Create bitmap data and surface but defer texture creation
|
||||
(*maps_bmp_)[i].Create(kOverworldMapSize, kOverworldMapSize, 0x80,
|
||||
overworld_->current_map_bitmap_data());
|
||||
(*maps_bmp_)[i].SetPalette(palette);
|
||||
maps_to_texture.push_back(&(*maps_bmp_)[i]);
|
||||
} catch (const std::bad_alloc& e) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Error allocating map %d: %s",
|
||||
i, e.what());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Non-essential maps will be created on-demand when accessed
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Create textures only for currently visible maps
|
||||
// Only create textures for the first few maps initially
|
||||
const int initial_texture_count =
|
||||
std::min(4, static_cast<int>(maps_to_texture.size()));
|
||||
{
|
||||
gfx::ScopedTimer initial_textures_timer("CreateInitialTextures");
|
||||
for (int i = 0; i < initial_texture_count; ++i) {
|
||||
Renderer::Get().RenderBitmap(maps_to_texture[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Store remaining maps for lazy texture creation
|
||||
deferred_map_textures_.assign(maps_to_texture.begin() + initial_texture_count,
|
||||
maps_to_texture.end());
|
||||
|
||||
if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) {
|
||||
{
|
||||
gfx::ScopedTimer sprites_timer("LoadSpriteGraphics");
|
||||
RETURN_IF_ERROR(LoadSpriteGraphics());
|
||||
}
|
||||
}
|
||||
|
||||
all_gfx_loaded_ = true;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldGraphicsManager::LoadSpriteGraphics() {
|
||||
// Render the sprites for each Overworld map
|
||||
const int depth = 0x10;
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (auto const& sprite : *overworld_->mutable_sprites(i)) {
|
||||
int width = sprite.width();
|
||||
int height = sprite.height();
|
||||
if (width == 0 || height == 0) {
|
||||
continue;
|
||||
}
|
||||
if (sprite_previews_->size() < sprite.id()) {
|
||||
sprite_previews_->resize(sprite.id() + 1);
|
||||
}
|
||||
(*sprite_previews_)[sprite.id()].Create(width, height, depth,
|
||||
*sprite.preview_graphics());
|
||||
(*sprite_previews_)[sprite.id()].SetPalette(*palette_);
|
||||
Renderer::Get().RenderBitmap(&(*sprite_previews_)[sprite.id()]);
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Texture Management
|
||||
// ============================================================================
|
||||
|
||||
void OverworldGraphicsManager::ProcessDeferredTextures() {
|
||||
std::lock_guard<std::mutex> lock(deferred_textures_mutex_);
|
||||
|
||||
// Always process deferred textures progressively, even if the list is "empty"
|
||||
// This allows for continuous background loading
|
||||
|
||||
// PHASE 1: Priority loading for current world
|
||||
const int high_priority_per_frame = 4; // Current world maps
|
||||
const int low_priority_per_frame = 2; // Other world maps
|
||||
const int refresh_per_frame = 2; // Modified map refreshes
|
||||
int processed = 0;
|
||||
|
||||
// Process high-priority deferred textures (current world)
|
||||
if (!deferred_map_textures_.empty()) {
|
||||
auto it = deferred_map_textures_.begin();
|
||||
while (it != deferred_map_textures_.end() && processed < high_priority_per_frame) {
|
||||
if (*it && !(*it)->texture()) {
|
||||
// Find map index for priority check
|
||||
int map_index = -1;
|
||||
for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
|
||||
if (&(*maps_bmp_)[i] == *it) {
|
||||
map_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_current_world = false;
|
||||
if (map_index >= 0) {
|
||||
int map_world = map_index / 0x40; // 64 maps per world
|
||||
is_current_world = (map_world == current_world_);
|
||||
}
|
||||
|
||||
if (is_current_world) {
|
||||
Renderer::Get().RenderBitmap(*it);
|
||||
processed++;
|
||||
it = deferred_map_textures_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 2: Background loading for other worlds (lower priority)
|
||||
if (!deferred_map_textures_.empty() && processed < high_priority_per_frame) {
|
||||
auto it = deferred_map_textures_.begin();
|
||||
int low_priority_processed = 0;
|
||||
while (it != deferred_map_textures_.end() && low_priority_processed < low_priority_per_frame) {
|
||||
if (*it && !(*it)->texture()) {
|
||||
Renderer::Get().RenderBitmap(*it);
|
||||
low_priority_processed++;
|
||||
processed++;
|
||||
it = deferred_map_textures_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 3: Process modified maps that need refresh (highest priority)
|
||||
int refresh_processed = 0;
|
||||
for (int i = 0; i < zelda3::kNumOverworldMaps && refresh_processed < refresh_per_frame; ++i) {
|
||||
if ((*maps_bmp_)[i].modified() && (*maps_bmp_)[i].is_active()) {
|
||||
// Check if this map is in current world (high priority) or visible
|
||||
bool is_current_world = (i / 0x40 == current_world_);
|
||||
bool is_current_map = (i == current_map_);
|
||||
|
||||
if (is_current_map || is_current_world) {
|
||||
RefreshOverworldMapOnDemand(i);
|
||||
refresh_processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 4: Background refresh for modified maps in other worlds (very low priority)
|
||||
if (refresh_processed == 0) {
|
||||
for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) {
|
||||
if ((*maps_bmp_)[i].modified() && (*maps_bmp_)[i].is_active()) {
|
||||
bool is_current_world = (i / 0x40 == current_world_);
|
||||
if (!is_current_world) {
|
||||
// Just mark for later, don't refresh now to avoid lag
|
||||
// These will be refreshed when the world is switched
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::EnsureMapTexture(int map_index) {
|
||||
if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the map is built first (on-demand loading)
|
||||
auto status = overworld_->EnsureMapBuilt(map_index);
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Failed to build map %d: %s", map_index,
|
||||
status.message().data());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& bitmap = (*maps_bmp_)[map_index];
|
||||
|
||||
// If bitmap doesn't exist yet (non-essential map), create it now
|
||||
if (!bitmap.is_active()) {
|
||||
overworld_->set_current_map(map_index);
|
||||
auto palette = overworld_->current_area_palette();
|
||||
try {
|
||||
bitmap.Create(kOverworldMapSize, kOverworldMapSize, 0x80,
|
||||
overworld_->current_map_bitmap_data());
|
||||
bitmap.SetPalette(palette);
|
||||
} catch (const std::bad_alloc& e) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Error allocating bitmap for map %d: %s",
|
||||
map_index, e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bitmap.texture() && bitmap.is_active()) {
|
||||
Renderer::Get().RenderBitmap(&bitmap);
|
||||
|
||||
// Remove from deferred list if it was there
|
||||
std::lock_guard<std::mutex> lock(deferred_textures_mutex_);
|
||||
auto it = std::find(deferred_map_textures_.begin(),
|
||||
deferred_map_textures_.end(), &bitmap);
|
||||
if (it != deferred_map_textures_.end()) {
|
||||
deferred_map_textures_.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Refresh Operations
|
||||
// ============================================================================
|
||||
|
||||
void OverworldGraphicsManager::RefreshOverworldMap() {
|
||||
// Use the new on-demand refresh system
|
||||
RefreshOverworldMapOnDemand(current_map_);
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::RefreshOverworldMapOnDemand(int map_index) {
|
||||
if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the map is actually visible or being edited
|
||||
bool is_current_map = (map_index == current_map_);
|
||||
bool is_current_world = (map_index / 0x40 == current_world_);
|
||||
|
||||
// For non-current maps in non-current worlds, defer the refresh
|
||||
if (!is_current_map && !is_current_world) {
|
||||
// Mark for deferred refresh - will be processed when the map becomes visible
|
||||
(*maps_bmp_)[map_index].set_modified(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// For visible maps, do immediate refresh
|
||||
RefreshChildMapOnDemand(map_index);
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::RefreshChildMap(int map_index) {
|
||||
overworld_->mutable_overworld_map(map_index)->LoadAreaGraphics();
|
||||
auto status = overworld_->mutable_overworld_map(map_index)->BuildTileset();
|
||||
PRINT_IF_ERROR(status);
|
||||
status = overworld_->mutable_overworld_map(map_index)->BuildTiles16Gfx(
|
||||
*overworld_->mutable_tiles16(), overworld_->tiles16().size());
|
||||
PRINT_IF_ERROR(status);
|
||||
status = overworld_->mutable_overworld_map(map_index)->BuildBitmap(
|
||||
overworld_->GetMapTiles(current_world_));
|
||||
(*maps_bmp_)[map_index].set_data(
|
||||
overworld_->mutable_overworld_map(map_index)->bitmap_data());
|
||||
(*maps_bmp_)[map_index].set_modified(true);
|
||||
PRINT_IF_ERROR(status);
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::RefreshChildMapOnDemand(int map_index) {
|
||||
auto* map = overworld_->mutable_overworld_map(map_index);
|
||||
|
||||
// Check what actually needs to be refreshed
|
||||
bool needs_graphics_rebuild = (*maps_bmp_)[map_index].modified();
|
||||
|
||||
if (needs_graphics_rebuild) {
|
||||
// Only rebuild what's actually changed
|
||||
map->LoadAreaGraphics();
|
||||
|
||||
// Rebuild tileset only if graphics changed
|
||||
auto status = map->BuildTileset();
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Failed to build tileset for map %d: %s",
|
||||
map_index, status.message().data());
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild tiles16 graphics
|
||||
status = map->BuildTiles16Gfx(*overworld_->mutable_tiles16(),
|
||||
overworld_->tiles16().size());
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Failed to build tiles16 graphics for map %d: %s",
|
||||
map_index, status.message().data());
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild bitmap
|
||||
status = map->BuildBitmap(overworld_->GetMapTiles(current_world_));
|
||||
if (!status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager", "Failed to build bitmap for map %d: %s",
|
||||
map_index, status.message().data());
|
||||
return;
|
||||
}
|
||||
|
||||
// Update bitmap data
|
||||
(*maps_bmp_)[map_index].set_data(map->bitmap_data());
|
||||
(*maps_bmp_)[map_index].set_modified(false);
|
||||
|
||||
// Validate surface synchronization to help debug crashes
|
||||
if (!(*maps_bmp_)[map_index].ValidateDataSurfaceSync()) {
|
||||
LOG_WARN("OverworldGraphicsManager", "Warning: Surface synchronization issue detected for map %d",
|
||||
map_index);
|
||||
}
|
||||
|
||||
// Update texture on main thread
|
||||
if ((*maps_bmp_)[map_index].texture()) {
|
||||
Renderer::Get().UpdateBitmap(&(*maps_bmp_)[map_index]);
|
||||
} else {
|
||||
// Create texture if it doesn't exist
|
||||
EnsureMapTexture(map_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multi-area maps (large, wide, tall) with safe coordination
|
||||
// Check if ZSCustomOverworld v3 is present
|
||||
uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF);
|
||||
|
||||
if (use_v3_area_sizes) {
|
||||
// Use v3 multi-area coordination
|
||||
RefreshMultiAreaMapsSafely(map_index, map);
|
||||
} else {
|
||||
// Legacy logic: only handle large maps for vanilla/v2
|
||||
if (map->is_large_map()) {
|
||||
RefreshMultiAreaMapsSafely(map_index, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::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
|
||||
}
|
||||
|
||||
LOG_DEBUG("OverworldGraphicsManager",
|
||||
"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)
|
||||
sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
|
||||
LOG_DEBUG("OverworldGraphicsManager",
|
||||
"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)
|
||||
sibling_maps = {parent_id, parent_id + 1};
|
||||
LOG_DEBUG("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Wide area siblings: %d, %d",
|
||||
parent_id, parent_id + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case AreaSizeEnum::TallArea: {
|
||||
// Tall Area: 1x2 grid (2 maps total, vertically adjacent)
|
||||
sibling_maps = {parent_id, parent_id + 8};
|
||||
LOG_DEBUG("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Tall area siblings: %d, %d",
|
||||
parent_id, parent_id + 8);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG_WARN("OverworldGraphicsManager",
|
||||
"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) {
|
||||
LOG_DEBUG("OverworldGraphicsManager",
|
||||
"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();
|
||||
|
||||
if (auto status = sibling_map->BuildTileset(); !status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Failed to refresh sibling map %d: %s",
|
||||
sibling, status.message().data());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto status = sibling_map->BuildTiles16Gfx(*overworld_->mutable_tiles16(),
|
||||
overworld_->tiles16().size()); !status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Failed to build tiles16 graphics for sibling map %d: %s",
|
||||
sibling, status.message().data());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto status = sibling_map->LoadPalette(); !status.ok()) {
|
||||
LOG_ERROR("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Failed to load palette for sibling map %d: %s",
|
||||
sibling, status.message().data());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto status = sibling_map->BuildBitmap(overworld_->GetMapTiles(current_world_)); status.ok()) {
|
||||
(*maps_bmp_)[sibling].set_data(sibling_map->bitmap_data());
|
||||
(*maps_bmp_)[sibling].SetPalette(overworld_->current_area_palette());
|
||||
(*maps_bmp_)[sibling].set_modified(false);
|
||||
|
||||
// Update texture if it exists
|
||||
if ((*maps_bmp_)[sibling].texture()) {
|
||||
Renderer::Get().UpdateBitmap(&(*maps_bmp_)[sibling]);
|
||||
} else {
|
||||
EnsureMapTexture(sibling);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("OverworldGraphicsManager",
|
||||
"RefreshMultiAreaMapsSafely: Failed to build bitmap for 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);
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status OverworldGraphicsManager::RefreshMapPalette() {
|
||||
RETURN_IF_ERROR(
|
||||
overworld_->mutable_overworld_map(current_map_)->LoadPalette());
|
||||
const auto current_map_palette = overworld_->current_area_palette();
|
||||
|
||||
// Check if ZSCustomOverworld v3 is present
|
||||
uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF);
|
||||
|
||||
if (use_v3_area_sizes) {
|
||||
// Use v3 area size system
|
||||
using zelda3::AreaSizeEnum;
|
||||
auto area_size = overworld_->overworld_map(current_map_)->area_size();
|
||||
|
||||
if (area_size != AreaSizeEnum::SmallArea) {
|
||||
// Get all sibling maps that need palette updates
|
||||
std::vector<int> sibling_maps;
|
||||
int parent_id = overworld_->overworld_map(current_map_)->parent();
|
||||
|
||||
switch (area_size) {
|
||||
case AreaSizeEnum::LargeArea:
|
||||
// 2x2 grid: parent, parent+1, parent+8, parent+9
|
||||
sibling_maps = {parent_id, parent_id + 1, parent_id + 8,
|
||||
parent_id + 9};
|
||||
break;
|
||||
case AreaSizeEnum::WideArea:
|
||||
// 2x1 grid: parent, parent+1
|
||||
sibling_maps = {parent_id, parent_id + 1};
|
||||
break;
|
||||
case AreaSizeEnum::TallArea:
|
||||
// 1x2 grid: parent, parent+8
|
||||
sibling_maps = {parent_id, parent_id + 8};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Update palette for all siblings
|
||||
for (int sibling_index : sibling_maps) {
|
||||
if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) {
|
||||
continue;
|
||||
}
|
||||
RETURN_IF_ERROR(
|
||||
overworld_->mutable_overworld_map(sibling_index)->LoadPalette());
|
||||
(*maps_bmp_)[sibling_index].SetPalette(current_map_palette);
|
||||
}
|
||||
} else {
|
||||
// Small area - only update current map
|
||||
(*maps_bmp_)[current_map_].SetPalette(current_map_palette);
|
||||
}
|
||||
} else {
|
||||
// Legacy logic for vanilla and v2 ROMs
|
||||
if (overworld_->overworld_map(current_map_)->is_large_map()) {
|
||||
// We need to update the map and its siblings if it's a large map
|
||||
for (int i = 1; i < 4; i++) {
|
||||
int sibling_index =
|
||||
overworld_->overworld_map(current_map_)->parent() + i;
|
||||
if (i >= 2)
|
||||
sibling_index += 6;
|
||||
RETURN_IF_ERROR(
|
||||
overworld_->mutable_overworld_map(sibling_index)->LoadPalette());
|
||||
(*maps_bmp_)[sibling_index].SetPalette(current_map_palette);
|
||||
}
|
||||
}
|
||||
(*maps_bmp_)[current_map_].SetPalette(current_map_palette);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldGraphicsManager::RefreshTile16Blockset() {
|
||||
LOG_DEBUG("OverworldGraphicsManager", "RefreshTile16Blockset called");
|
||||
if (current_blockset_ ==
|
||||
overworld_->overworld_map(current_map_)->area_graphics()) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
current_blockset_ = overworld_->overworld_map(current_map_)->area_graphics();
|
||||
|
||||
overworld_->set_current_map(current_map_);
|
||||
*palette_ = overworld_->current_area_palette();
|
||||
|
||||
const auto tile16_data = overworld_->tile16_blockset_data();
|
||||
|
||||
gfx::UpdateTilemap(*tile16_blockset_, tile16_data);
|
||||
tile16_blockset_->atlas.SetPalette(*palette_);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void OverworldGraphicsManager::ForceRefreshGraphics(int map_index) {
|
||||
if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("OverworldGraphicsManager",
|
||||
"ForceRefreshGraphics: Forcing graphics reload for map %d", map_index);
|
||||
|
||||
// Mark bitmap as modified to force refresh
|
||||
(*maps_bmp_)[map_index].set_modified(true);
|
||||
|
||||
// Clear the blockset cache to force tile16 reload
|
||||
current_blockset_ = 0xFF;
|
||||
|
||||
// If this is the current map, also ensure sibling maps are refreshed for multi-area maps
|
||||
uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
|
||||
bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF);
|
||||
|
||||
auto* map = overworld_->mutable_overworld_map(map_index);
|
||||
if (use_v3_area_sizes) {
|
||||
using zelda3::AreaSizeEnum;
|
||||
auto area_size = map->area_size();
|
||||
|
||||
if (area_size != AreaSizeEnum::SmallArea) {
|
||||
std::vector<int> sibling_maps;
|
||||
int parent_id = map->parent();
|
||||
|
||||
switch (area_size) {
|
||||
case AreaSizeEnum::LargeArea:
|
||||
sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9};
|
||||
break;
|
||||
case AreaSizeEnum::WideArea:
|
||||
sibling_maps = {parent_id, parent_id + 1};
|
||||
break;
|
||||
case AreaSizeEnum::TallArea:
|
||||
sibling_maps = {parent_id, parent_id + 8};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark all sibling maps as needing refresh
|
||||
for (int sibling : sibling_maps) {
|
||||
if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) {
|
||||
(*maps_bmp_)[sibling].set_modified(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (map->is_large_map()) {
|
||||
// Legacy large map handling
|
||||
int parent_id = map->parent();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int sibling = parent_id + (i < 2 ? i : i + 6);
|
||||
if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) {
|
||||
(*maps_bmp_)[sibling].set_modified(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
204
src/app/editor/overworld/overworld_graphics_manager.h
Normal file
204
src/app/editor/overworld/overworld_graphics_manager.h
Normal file
@@ -0,0 +1,204 @@
|
||||
#ifndef YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H
|
||||
#define YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H
|
||||
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/bitmap.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gfx/tilemap.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/overworld/overworld.h"
|
||||
#include "app/zelda3/overworld/overworld_map.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
|
||||
using Tilemap = gfx::Tilemap;
|
||||
|
||||
/**
|
||||
* @class OverworldGraphicsManager
|
||||
* @brief Manages all graphics-related operations for the Overworld Editor
|
||||
*
|
||||
* This class handles:
|
||||
* - Graphics loading and initialization
|
||||
* - Sprite graphics loading
|
||||
* - Deferred texture processing for smooth loading
|
||||
* - Map texture management
|
||||
* - Map refreshing (full and on-demand)
|
||||
* - Palette refreshing
|
||||
* - Tile16 blockset refreshing
|
||||
* - Multi-area map coordination
|
||||
*
|
||||
* Separating graphics management from the main OverworldEditor improves:
|
||||
* - Code organization and maintainability
|
||||
* - Performance optimization opportunities
|
||||
* - Testing and debugging
|
||||
* - Clear separation of concerns
|
||||
*/
|
||||
class OverworldGraphicsManager {
|
||||
public:
|
||||
OverworldGraphicsManager(
|
||||
zelda3::Overworld* overworld, Rom* rom,
|
||||
std::array<gfx::Bitmap, zelda3::kNumOverworldMaps>* maps_bmp,
|
||||
gfx::Bitmap* tile16_blockset_bmp, gfx::Bitmap* current_gfx_bmp,
|
||||
std::vector<gfx::Bitmap>* sprite_previews, gfx::SnesPalette* palette,
|
||||
Tilemap* tile16_blockset)
|
||||
: overworld_(overworld),
|
||||
rom_(rom),
|
||||
maps_bmp_(maps_bmp),
|
||||
tile16_blockset_bmp_(tile16_blockset_bmp),
|
||||
current_gfx_bmp_(current_gfx_bmp),
|
||||
sprite_previews_(sprite_previews),
|
||||
palette_(palette),
|
||||
tile16_blockset_(tile16_blockset) {}
|
||||
|
||||
// ============================================================================
|
||||
// Loading Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Load all overworld graphics (maps, tilesets, sprites)
|
||||
*
|
||||
* This uses a multi-phase loading strategy:
|
||||
* - Phase 1: Create bitmaps without textures
|
||||
* - Phase 2: Create bitmaps for essential maps only
|
||||
* - Phase 3: Create textures for visible maps
|
||||
* - Deferred loading for remaining maps
|
||||
*/
|
||||
absl::Status LoadGraphics();
|
||||
|
||||
/**
|
||||
* @brief Load sprite graphics for all overworld maps
|
||||
*/
|
||||
absl::Status LoadSpriteGraphics();
|
||||
|
||||
// ============================================================================
|
||||
// Texture Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Process deferred texture creation (called per frame)
|
||||
*
|
||||
* Creates textures gradually to avoid frame drops.
|
||||
* Prioritizes textures for the current world and visible maps.
|
||||
*/
|
||||
void ProcessDeferredTextures();
|
||||
|
||||
/**
|
||||
* @brief Ensure a specific map has a texture created
|
||||
*
|
||||
* @param map_index Index of the map to ensure texture for
|
||||
*/
|
||||
void EnsureMapTexture(int map_index);
|
||||
|
||||
// ============================================================================
|
||||
// Refresh Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Refresh the current overworld map
|
||||
*/
|
||||
void RefreshOverworldMap();
|
||||
|
||||
/**
|
||||
* @brief Refresh a specific map on-demand (only if visible)
|
||||
*
|
||||
* @param map_index Index of the map to refresh
|
||||
*/
|
||||
void RefreshOverworldMapOnDemand(int map_index);
|
||||
|
||||
/**
|
||||
* @brief Refresh a child map (legacy method)
|
||||
*
|
||||
* @param map_index Index of the map to refresh
|
||||
*/
|
||||
void RefreshChildMap(int map_index);
|
||||
|
||||
/**
|
||||
* @brief Refresh a child map with selective updates
|
||||
*
|
||||
* @param map_index Index of the map to refresh
|
||||
*/
|
||||
void RefreshChildMapOnDemand(int map_index);
|
||||
|
||||
/**
|
||||
* @brief Safely refresh multi-area maps (large, wide, tall)
|
||||
*
|
||||
* Handles coordination of multi-area maps without recursion.
|
||||
*
|
||||
* @param map_index Index of the map to refresh
|
||||
* @param map Pointer to the OverworldMap object
|
||||
*/
|
||||
void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap* map);
|
||||
|
||||
/**
|
||||
* @brief Refresh the palette for the current map
|
||||
*
|
||||
* Also handles palette updates for multi-area maps.
|
||||
*/
|
||||
absl::Status RefreshMapPalette();
|
||||
|
||||
/**
|
||||
* @brief Refresh the tile16 blockset
|
||||
*
|
||||
* This should be called whenever area graphics change.
|
||||
*/
|
||||
absl::Status RefreshTile16Blockset();
|
||||
|
||||
/**
|
||||
* @brief Force a graphics refresh for a specific map
|
||||
*
|
||||
* Marks the map's bitmap as modified and clears the blockset cache
|
||||
* to force a full reload on next refresh. Use this when graphics
|
||||
* properties change (area_graphics, animated_gfx, custom tilesets).
|
||||
*
|
||||
* @param map_index Index of the map to force refresh
|
||||
*/
|
||||
void ForceRefreshGraphics(int map_index);
|
||||
|
||||
// ============================================================================
|
||||
// State Management
|
||||
// ============================================================================
|
||||
|
||||
void set_current_map(int map_index) { current_map_ = map_index; }
|
||||
void set_current_world(int world_index) { current_world_ = world_index; }
|
||||
void set_current_blockset(uint8_t blockset) { current_blockset_ = blockset; }
|
||||
|
||||
int current_map() const { return current_map_; }
|
||||
int current_world() const { return current_world_; }
|
||||
uint8_t current_blockset() const { return current_blockset_; }
|
||||
|
||||
bool all_gfx_loaded() const { return all_gfx_loaded_; }
|
||||
bool map_blockset_loaded() const { return map_blockset_loaded_; }
|
||||
|
||||
private:
|
||||
// Core dependencies
|
||||
zelda3::Overworld* overworld_;
|
||||
Rom* rom_;
|
||||
std::array<gfx::Bitmap, zelda3::kNumOverworldMaps>* maps_bmp_;
|
||||
gfx::Bitmap* tile16_blockset_bmp_;
|
||||
gfx::Bitmap* current_gfx_bmp_;
|
||||
std::vector<gfx::Bitmap>* sprite_previews_;
|
||||
gfx::SnesPalette* palette_;
|
||||
Tilemap* tile16_blockset_;
|
||||
|
||||
// State tracking
|
||||
int current_map_ = 0;
|
||||
int current_world_ = 0;
|
||||
uint8_t current_blockset_ = 0;
|
||||
bool all_gfx_loaded_ = false;
|
||||
bool map_blockset_loaded_ = false;
|
||||
|
||||
// Deferred texture loading
|
||||
std::vector<gfx::Bitmap*> deferred_map_textures_;
|
||||
std::mutex deferred_textures_mutex_;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H
|
||||
|
||||
@@ -633,14 +633,6 @@ absl::Status Overworld::LoadExits() {
|
||||
uint16_t px = (uint16_t)((rom_data[OWExitXPlayer + (i * 2) + 1] << 8) +
|
||||
rom_data[OWExitXPlayer + (i * 2)]);
|
||||
|
||||
// util::logf(
|
||||
// "Exit: %d RoomID: %d MapID: %d VRAM: %d YScroll: %d XScroll: "
|
||||
// "%d YPlayer: %d XPlayer: %d YCamera: %d XCamera: %d "
|
||||
// "ScrollModY: %d ScrollModX: %d DoorType1: %d DoorType2: %d",
|
||||
// i, exit_room_id, exit_map_id, exit_vram, exit_y_scroll, exit_x_scroll,
|
||||
// py, px, exit_y_camera, exit_x_camera, exit_scroll_mod_y,
|
||||
// exit_scroll_mod_x, exit_door_type_1, exit_door_type_2);
|
||||
|
||||
exits.emplace_back(exit_room_id, exit_map_id, exit_vram, exit_y_scroll,
|
||||
exit_x_scroll, py, px, exit_y_camera, exit_x_camera,
|
||||
exit_scroll_mod_y, exit_scroll_mod_x, exit_door_type_1,
|
||||
|
||||
@@ -194,10 +194,6 @@ class OverworldExit : public GameEntity {
|
||||
|
||||
map_pos_ = (uint16_t)(((vram_y_scroll & 0xFFF0) << 3) |
|
||||
((vram_x_scroll & 0xFFF0) >> 3));
|
||||
|
||||
std::cout << "Exit: " << room_id_ << " MapId: " << std::hex << mapid
|
||||
<< " X: " << static_cast<int>(area_x_)
|
||||
<< " Y: " << static_cast<int>(area_y_) << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -53,12 +53,6 @@ class OverworldItem : public GameEntity {
|
||||
|
||||
game_x_ = static_cast<uint8_t>(std::abs(x_ - (map_x * 512)) / 16);
|
||||
game_y_ = static_cast<uint8_t>(std::abs(y_ - (map_y * 512)) / 16);
|
||||
|
||||
std::cout << "Item: " << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(id_) << " MapId: " << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << static_cast<int>(room_map_id_)
|
||||
<< " X: " << static_cast<int>(game_x_)
|
||||
<< " Y: " << static_cast<int>(game_y_) << std::endl;
|
||||
}
|
||||
|
||||
bool bg2_ = false;
|
||||
|
||||
Reference in New Issue
Block a user