tibia-rme/source/gui.cpp
Eduardo Dantas d78a2546c7
feat: add Cyclopedia protobuf export pipeline (#160)
Add editor support for exporting Cyclopedia and CipSoft protobuf assets from the currently loaded map.

Main changes:
- Add File > Client Assets actions for Static House Data export, Cyclopedia minimap and satellite export, and restoring timestamped client asset backups.
- Keep generic minimap and tileset exports under File > Export.
- Export staticdata, staticmapdata, and mapdata protobuf files with content-hashed filenames.
- Update catalog-content.json so generated assets are picked up by the client.
- Merge compatible CipSoft templates when they match the loaded map.
- Fall back to generated data for custom maps instead of failing the export.
- Support filtered house exports.
- Report warnings for missing or failed static map entries.
- Emit aligned MINIMAP and sprite-based SATELLITE assets for the documented scales, including the 1/16 layer.
- Keep Cyclopedia export progress in the status bar instead of using a blocking modal progress dialog.

Performance:
- Precompute floor and chunk plans for global map exports.
- Write BMP output directly.
- Reuse minimap renders for matching satellite assets.
- Parallelize asset hash and LZMA encoding through a bounded worker queue.
- Remove modal progress overhead from the export path.
- Revert the tile-grid satellite optimization after profiling showed it shifted cost without improving total export time.

Documentation:
- Document the export contract in docs/static-data.md.
- Add docs/cyclopedia-export-roadmap.md with deferred optimization candidates and follow-up notes.

Scope:
- Focus this change on the Cyclopedia/staticdata protobuf export path and the export-specific performance work needed for practical global map exports.
- Move unrelated runtime, performance, and UI experiments out of this branch.

Validation:
- Ran static inspection and git diff checks during the export changes.
- Reviewed Visual Studio CPU profiles after each global map export performance pass.
- Verified hash and LZMA work moved into bounded async workers.
- Verified the restore action is labeled as restore and grouped under File > Client Assets, not File > Export.

This adds the editor-side pipeline needed to generate client Cyclopedia assets from a loaded map while keeping the export flow recoverable, documented, and practical for large maps.
2026-05-28 08:35:21 -03:00

2158 lines
52 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "gui.h"
#include "application.h"
#include "client_assets.h"
#include "main_menubar.h"
#include "editor.h"
#include "brush.h"
#include "map.h"
#include "sprites.h"
#include "materials.h"
#include "doodad_brush.h"
#include "spawn_monster_brush.h"
#include "object_pool.h"
#include "common_windows.h"
#include "result_window.h"
#include "minimap_window.h"
#include "palette_window.h"
#include "map_display.h"
#include "application.h"
#include "welcome_dialog.h"
#include "spawn_npc_brush.h"
#include "actions_history_window.h"
#include "lua/lua_scripts_window.h"
#include "sprite_appearances.h"
#include "preferences.h"
#include "live_client.h"
#include "live_tab.h"
#include "live_server.h"
#ifdef __WXOSX__
#include <AGL/agl.h>
#endif
#include <appearances.pb.h>
namespace InternalGUI {
void logErrorAndSetMessage(const std::string &message, wxString &error) {
spdlog::error(message);
error = message + ". Error:" + error;
g_gui.DestroyLoadBar();
g_gui.unloadMapWindow();
}
} // namespace (internal use only)
const wxEventType EVT_UPDATE_MENUS = wxNewEventType();
const wxEventType EVT_UPDATE_ACTIONS = wxNewEventType();
// Global GUI instance
GUI g_gui;
// GUI class implementation
GUI::GUI() :
aui_manager(nullptr),
root(nullptr),
minimap(nullptr),
gem(nullptr),
search_result_window(nullptr),
actions_history_window(nullptr),
script_manager_window(nullptr),
secondary_map(nullptr),
doodad_buffer_map(nullptr),
house_brush(nullptr),
house_exit_brush(nullptr),
waypoint_brush(nullptr),
optional_brush(nullptr),
eraser(nullptr),
normal_door_brush(nullptr),
locked_door_brush(nullptr),
magic_door_brush(nullptr),
quest_door_brush(nullptr),
hatch_door_brush(nullptr),
window_door_brush(nullptr),
OGLContext(nullptr),
mode(SELECTION_MODE),
pasting(false),
hotkeys_enabled(true),
current_brush(nullptr),
previous_brush(nullptr),
brush_shape(BRUSHSHAPE_SQUARE),
brush_size(0),
brush_variation(0),
monster_spawntime(0),
npc_spawntime(0),
use_custom_thickness(false),
custom_thickness_mod(0.0),
progressBar(nullptr),
disabled_counter(0) {
doodad_buffer_map = newd BaseMap();
}
GUI::~GUI() {
JoinAsyncSqliteBootstrapThread();
delete doodad_buffer_map;
delete g_gui.aui_manager;
delete OGLContext;
}
wxGLContext* GUI::GetGLContext(wxGLCanvas* win) {
if (OGLContext == nullptr) {
#ifdef __WXOSX__
/*
wxGLContext(AGLPixelFormat fmt, wxGLCanvas *win,
const wxPalette& WXUNUSED(palette),
const wxGLContext *other
);
*/
OGLContext = new wxGLContext(win, nullptr);
#else
OGLContext = newd wxGLContext(win);
#endif
}
return OGLContext;
}
wxString GUI::GetDataDirectory() {
std::string cfg_str = g_settings.getString(Config::DATA_DIRECTORY);
if (!cfg_str.empty()) {
FileName dir;
dir.Assign(wxstr(cfg_str));
wxString path;
if (dir.DirExists()) {
path = dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
return path;
}
}
// Silently reset directory
FileName exec_directory;
try {
exec_directory = dynamic_cast<wxStandardPaths &>(wxStandardPaths::Get()).GetExecutablePath();
} catch (const std::bad_cast &) {
throw; // Crash application (this should never happend anyways...)
}
exec_directory.AppendDir("data");
return exec_directory.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
}
wxString GUI::GetExecDirectory() {
// Silently reset directory
FileName exec_directory;
try {
exec_directory = dynamic_cast<wxStandardPaths &>(wxStandardPaths::Get()).GetExecutablePath();
} catch (const std::bad_cast &) {
wxLogError("Could not fetch executable directory.");
}
return exec_directory.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
}
wxString GUI::GetLocalDataDirectory() {
if (g_settings.getInteger(Config::INDIRECTORY_INSTALLATION)) {
FileName dir = GetDataDirectory();
dir.AppendDir("user");
dir.AppendDir("data");
dir.Mkdir(0755, wxPATH_MKDIR_FULL);
return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
;
} else {
FileName dir = dynamic_cast<wxStandardPaths &>(wxStandardPaths::Get()).GetUserDataDir();
#ifdef __WINDOWS__
dir.AppendDir("Remere's Map Editor");
#else
dir.AppendDir(".rme");
#endif
dir.AppendDir("data");
dir.Mkdir(0755, wxPATH_MKDIR_FULL);
return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
}
}
wxString GUI::GetLocalDirectory() {
if (g_settings.getInteger(Config::INDIRECTORY_INSTALLATION)) {
FileName dir = GetDataDirectory();
dir.AppendDir("user");
dir.Mkdir(0755, wxPATH_MKDIR_FULL);
return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
;
} else {
FileName dir = dynamic_cast<wxStandardPaths &>(wxStandardPaths::Get()).GetUserDataDir();
#ifdef __WINDOWS__
dir.AppendDir("Remere's Map Editor");
#else
dir.AppendDir(".rme");
#endif
dir.Mkdir(0755, wxPATH_MKDIR_FULL);
return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
}
}
void GUI::discoverDataDirectory(const wxString &existentFile) {
wxString currentDir = wxGetCwd();
wxString execDir = GetExecDirectory();
wxString possiblePaths[] = {
execDir,
currentDir + "/",
// these are used usually when running from build directories
execDir + "/../",
execDir + "/../../",
execDir + "/../../../",
currentDir + "/../",
};
bool found = false;
for (const wxString &path : possiblePaths) {
if (wxFileName(path + "data/" + existentFile).FileExists()) {
m_dataDirectory = path + "data/";
found = true;
break;
}
}
if (!found) {
wxLogError(wxString() + "Could not find data directory.\n");
}
}
bool GUI::loadMapWindow(wxString &error, wxArrayString &warnings, bool force /* = false*/) {
if (!force && ClientAssets::isLoaded()) {
return true;
}
// There is another version loaded right now, save window layout
g_gui.SavePerspective();
// Disable all rendering so the data is not accessed while reloading
UnnamedRenderingLock();
DestroyPalettes();
DestroyMinimap();
g_spriteAppearances.terminate();
// Destroy the previous window
unloadMapWindow();
bool ret = LoadDataFiles(error, warnings);
if (ret) {
g_gui.LoadPerspective();
}
return ret;
}
void GUI::EnableHotkeys() {
hotkeys_enabled = true;
}
void GUI::DisableHotkeys() {
hotkeys_enabled = false;
}
bool GUI::AreHotkeysEnabled() const {
return hotkeys_enabled;
}
void GUI::CycleTab(bool forward) {
tabbook->CycleTab(forward);
}
bool GUI::LoadDataFiles(wxString &error, wxArrayString &warnings) {
FileName data_path = GetDataDirectory();
FileName exec_directory;
try {
exec_directory = dynamic_cast<wxStandardPaths &>(wxStandardPaths::Get()).GetExecutablePath();
} catch (std::bad_cast &) {
error = "Couldn't establish working directory...";
return false;
}
g_spriteAppearances.init();
g_gui.CreateLoadBar("Loading assets files");
g_gui.SetLoadDone(0, "Loading assets file");
spdlog::info("Loading assets");
g_gui.SetLoadDone(20, "Loading client assets...");
spdlog::info("Loading appearances");
if (!ClientAssets::loadAppearanceProtobuf(error, warnings)) {
InternalGUI::logErrorAndSetMessage("Couldn't load catalog-content.json", error);
return false;
}
g_gui.SetLoadDone(30, "Loading items.xml ...");
spdlog::info("Loading items");
FileName itemsPath(exec_directory);
itemsPath.AppendDir("data");
itemsPath.AppendDir("items");
itemsPath.SetFullName("items.xml");
if (!g_items.loadFromGameXml(itemsPath, error, warnings)) {
warnings.push_back("Couldn't load items.xml: " + error);
spdlog::warn("[GUI::LoadDataFiles] {}: {}", itemsPath.GetFullPath().ToStdString(), error.ToStdString());
}
{
std::string monstersLuaDir = g_settings.getString(Config::MONSTERS_LUA_DIRECTORY);
if (monstersLuaDir.empty()) {
warnings.push_back("Monsters Lua Directory is not configured. Set it in Edit > Preferences.");
spdlog::warn("[GUI::LoadDataFiles] Monsters Lua Directory is not configured.");
} else {
g_gui.SetLoadDone(47, "Loading Canary monster Lua files...");
wxString luaErr;
wxArrayString luaWarn;
if (!g_monsters.loadFromLuaDir(wxString(monstersLuaDir), luaErr, luaWarn)) {
warnings.push_back("Error loading Canary monster Lua files: " + luaErr);
}
for (const auto &w : luaWarn) {
warnings.push_back(w);
}
}
}
{
std::string npcsLuaDir = g_settings.getString(Config::NPCS_LUA_DIRECTORY);
if (npcsLuaDir.empty()) {
warnings.push_back("NPCs Lua Directory is not configured. Set it in Edit > Preferences.");
spdlog::warn("[GUI::LoadDataFiles] NPCs Lua Directory is not configured.");
} else {
g_gui.SetLoadDone(48, "Loading Canary NPC Lua files...");
wxString luaErr;
wxArrayString luaWarn;
if (!g_npcs.loadFromLuaDir(wxString(npcsLuaDir), luaErr, luaWarn)) {
warnings.push_back("Error loading Canary NPC Lua files: " + luaErr);
}
for (const auto &w : luaWarn) {
warnings.push_back(w);
}
}
}
g_gui.SetLoadDone(50, "Loading materials.xml ...");
spdlog::info("Loading materials");
g_materials.initializeBrushDatabase(warnings);
auto materialsPath = wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "materials/materials.xml");
bool startAsyncSqliteBootstrap = false;
if (g_settings.getBoolean(Config::USE_SQLITE_MATERIALS)) {
bool skipSqliteImport = false;
wxString sqliteImportStatus;
if (!g_materials.shouldSkipSqliteBootstrapImports(skipSqliteImport, sqliteImportStatus)) {
warnings.push_back("SQLite import bootstrap check failed: " + sqliteImportStatus);
spdlog::warn("[GUI::LoadDataFiles] SQLite import bootstrap check failed: {}", sqliteImportStatus.ToStdString());
} else if (skipSqliteImport) {
spdlog::info("{}", sqliteImportStatus.ToStdString());
} else {
spdlog::info("{}", sqliteImportStatus.ToStdString());
startAsyncSqliteBootstrap = true;
}
}
if (!g_materials.loadMaterials(materialsPath, error, warnings)) {
warnings.push_back("Couldn't load materials.xml: " + error);
spdlog::warn("[GUI::LoadDataFiles] {}: {}", materialsPath.ToStdString(), error.ToStdString());
}
g_gui.SetLoadDone(70, "Finishing...");
spdlog::info("Finishing load map...");
g_brushes.init();
g_materials.createOtherTileset();
g_materials.createNpcTileset();
g_gui.DestroyLoadBar();
spdlog::info("Assets loaded");
if (startAsyncSqliteBootstrap) {
StartAsyncSqliteBootstrapImport();
}
return true;
}
void GUI::unloadMapWindow() {
UnnamedRenderingLock();
gfx.clear();
current_brush = nullptr;
previous_brush = nullptr;
house_brush = nullptr;
house_exit_brush = nullptr;
waypoint_brush = nullptr;
optional_brush = nullptr;
eraser = nullptr;
normal_door_brush = nullptr;
locked_door_brush = nullptr;
magic_door_brush = nullptr;
quest_door_brush = nullptr;
hatch_door_brush = nullptr;
window_door_brush = nullptr;
// Reload/unload tears down the in-memory materials graph, so wait for the
// background SQLite bootstrap to stop reading shared brush/items state first.
JoinAsyncSqliteBootstrapThread();
g_materials.clear();
g_brushes.clear();
g_items.clear();
gfx.clear();
FileName cdb = ClientAssets::getLocalPath();
cdb.SetFullName("monsters.xml");
g_monsters.saveToXML(cdb);
cdb.SetFullName("npcs.xml");
g_monsters.saveToXML(cdb);
g_monsters.clear();
g_npcs.clear();
}
void GUI::SaveCurrentMap(FileName filename, bool showdialog) {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
Editor* editor = mapTab->GetEditor();
if (editor) {
editor->saveMap(filename, showdialog);
const std::string &filename = editor->getMap().getFilename();
const Position &position = mapTab->GetScreenCenterPosition();
std::ostringstream stream;
stream << position;
g_settings.setString(Config::RECENT_EDITED_MAP_PATH, filename);
g_settings.setString(Config::RECENT_EDITED_MAP_POSITION, stream.str());
}
}
UpdateTitle();
root->UpdateMenubar();
root->Refresh();
}
bool GUI::IsEditorOpen() const {
return tabbook != nullptr && GetCurrentMapTab();
}
double GUI::GetCurrentZoom() {
MapTab* tab = GetCurrentMapTab();
if (tab) {
return tab->GetCanvas()->GetZoom();
}
return 1.0;
}
void GUI::SetCurrentZoom(double zoom) {
MapTab* tab = GetCurrentMapTab();
if (tab) {
tab->GetCanvas()->SetZoom(zoom);
}
}
void GUI::FitViewToMap() {
for (int index = 0; index < tabbook->GetTabCount(); ++index) {
if (auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(index))) {
tab->GetView()->FitToMap();
}
}
}
void GUI::FitViewToMap(MapTab* mt) {
for (int index = 0; index < tabbook->GetTabCount(); ++index) {
if (auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(index))) {
if (tab->HasSameReference(mt)) {
tab->GetView()->FitToMap();
}
}
}
}
bool GUI::NewMap() {
FinishWelcomeDialog();
Editor* editor;
try {
editor = newd Editor(copybuffer);
} catch (std::runtime_error &e) {
PopupDialog(root, "Error!", wxString(e.what(), wxConvUTF8), wxOK);
return false;
}
auto* mapTab = newd MapTab(tabbook, editor);
mapTab->OnSwitchEditorMode(mode);
editor->clearChanges();
SetStatusText("Created new map");
UpdateTitle();
RefreshPalettes();
root->UpdateMenubar();
root->Refresh();
return true;
}
void GUI::OpenMap() {
wxString wildcard = MAP_LOAD_FILE_WILDCARD;
wxFileDialog dialog(root, "Open map file", wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
if (dialog.ShowModal() == wxID_OK) {
wxArrayString paths;
dialog.GetPaths(paths);
for (uint32_t i = 0; i < paths.GetCount(); ++i) {
LoadMap(FileName(paths[i]));
}
}
}
void GUI::SaveMap() {
if (!IsEditorOpen()) {
return;
}
if (GetCurrentMap().hasFile()) {
SaveCurrentMap(true);
} else {
wxString wildcard = MAP_SAVE_FILE_WILDCARD;
wxFileDialog dialog(root, "Save...", wxEmptyString, wxEmptyString, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dialog.ShowModal() == wxID_OK) {
SaveCurrentMap(dialog.GetPath(), true);
}
}
}
void GUI::SaveMapAs() {
if (!IsEditorOpen()) {
return;
}
wxString wildcard = MAP_SAVE_FILE_WILDCARD;
wxFileDialog dialog(root, "Save As...", "", "", wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dialog.ShowModal() == wxID_OK) {
SaveCurrentMap(dialog.GetPath(), true);
UpdateTitle();
root->menu_bar->AddRecentFile(dialog.GetPath());
root->UpdateMenubar();
}
}
bool GUI::LoadMap(const FileName &fileName) {
rme::bindPooledObjectOwnerThread();
FinishWelcomeDialog();
if (GetCurrentEditor() && !GetCurrentMap().hasChanged() && !GetCurrentMap().hasFile()) {
g_gui.CloseCurrentEditor();
}
rme::resetPooledObjectStats();
Editor* editor;
try {
editor = newd Editor(copybuffer, fileName);
} catch (std::runtime_error &e) {
rme::dumpPooledObjectStats();
PopupDialog(root, "Error!", wxString(e.what(), wxConvUTF8), wxOK);
return false;
}
rme::dumpPooledObjectStats();
auto* mapTab = newd MapTab(tabbook, editor);
mapTab->OnSwitchEditorMode(mode);
root->AddRecentFile(fileName);
mapTab->GetView()->FitToMap();
UpdateTitle();
ListDialog("Map loader errors", mapTab->GetMap()->getWarnings());
// Npc and monsters
root->DoQueryImportCreatures();
FitViewToMap(mapTab);
root->UpdateMenubar();
std::string path = g_settings.getString(Config::RECENT_EDITED_MAP_PATH);
if (!path.empty()) {
FileName file(path);
if (file == fileName) {
std::istringstream stream(g_settings.getString(Config::RECENT_EDITED_MAP_POSITION));
Position position;
stream >> position;
mapTab->SetScreenCenterPosition(position);
}
}
for (const auto &palette : palettes) {
palette->OnUpdate(mapTab->GetMap());
}
return true;
}
Editor* GUI::GetCurrentEditor() {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
return mapTab->GetEditor();
}
return nullptr;
}
EditorTab* GUI::GetTab(int idx) {
return tabbook->GetTab(idx);
}
int GUI::GetTabCount() const {
return tabbook->GetTabCount();
}
EditorTab* GUI::GetCurrentTab() {
return tabbook->GetCurrentTab();
}
MapTab* GUI::GetCurrentMapTab() const {
if (tabbook && tabbook->GetTabCount() > 0) {
EditorTab* editorTab = tabbook->GetCurrentTab();
auto* mapTab = dynamic_cast<MapTab*>(editorTab);
return mapTab;
}
return nullptr;
}
Map &GUI::GetCurrentMap() {
Editor* editor = GetCurrentEditor();
ASSERT(editor);
return editor->getMap();
}
int GUI::GetOpenMapCount() {
std::set<Map*> open_maps;
for (int i = 0; i < tabbook->GetTabCount(); ++i) {
auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
if (tab) {
open_maps.insert(open_maps.begin(), tab->GetMap());
}
}
return static_cast<int>(open_maps.size());
}
bool GUI::ShouldSave() {
Editor* editor = GetCurrentEditor();
ASSERT(editor);
return editor->hasChanges();
}
void GUI::AddPendingCanvasEvent(wxEvent &event) {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
mapTab->GetCanvas()->GetEventHandler()->AddPendingEvent(event);
}
}
void GUI::CloseCurrentEditor() {
RefreshPalettes();
tabbook->DeleteTab(tabbook->GetSelection());
root->UpdateMenubar();
}
bool GUI::CloseLiveEditors(LiveSocket* sock) {
for (int i = 0; i < tabbook->GetTabCount(); ++i) {
auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
if (mapTab) {
Editor* editor = mapTab->GetEditor();
if (editor->GetLiveClient() == sock) {
tabbook->DeleteTab(i--);
}
}
auto* liveLogTab = dynamic_cast<LiveLogTab*>(tabbook->GetTab(i));
if (liveLogTab) {
if (liveLogTab->GetSocket() == sock) {
liveLogTab->Disconnect();
tabbook->DeleteTab(i--);
}
}
}
root->UpdateMenubar();
return true;
}
bool GUI::CloseAllEditors() {
for (int i = 0; i < tabbook->GetTabCount(); ++i) {
auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
if (mapTab) {
if (mapTab->IsUniqueReference() && mapTab->GetMap() && mapTab->GetMap()->hasChanged()) {
tabbook->SetFocusedTab(i);
if (!root->DoQuerySave(false)) {
return false;
} else {
RefreshPalettes();
tabbook->DeleteTab(i--);
}
} else {
tabbook->DeleteTab(i--);
}
}
}
if (root) {
root->UpdateMenubar();
}
return true;
}
void GUI::NewMapView() {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
auto* newMapTab = newd MapTab(mapTab);
newMapTab->OnSwitchEditorMode(mode);
SetStatusText("Created new view");
UpdateTitle();
RefreshPalettes();
root->UpdateMenubar();
root->Refresh();
}
}
void GUI::LoadPerspective() {
if (!ClientAssets::isLoaded()) {
if (g_settings.getInteger(Config::WINDOW_MAXIMIZED)) {
root->Maximize();
} else {
root->SetSize(wxSize(
g_settings.getInteger(Config::WINDOW_WIDTH),
g_settings.getInteger(Config::WINDOW_HEIGHT)
));
}
} else {
std::string tmp;
std::string layout = g_settings.getString(Config::PALETTE_LAYOUT);
std::vector<std::string> palette_list;
for (char c : layout) {
if (c == '|') {
palette_list.push_back(tmp);
tmp.clear();
} else {
tmp.push_back(c);
}
}
if (!tmp.empty()) {
palette_list.push_back(tmp);
}
for (const std::string &name : palette_list) {
PaletteWindow* palette = CreatePalette();
wxAuiPaneInfo &info = aui_manager->GetPane(palette);
aui_manager->LoadPaneInfo(wxstr(name), info);
if (info.IsFloatable()) {
bool offscreen = true;
for (uint32_t index = 0; index < wxDisplay::GetCount(); ++index) {
wxDisplay display(index);
wxRect rect = display.GetClientArea();
if (rect.Contains(info.floating_pos)) {
offscreen = false;
break;
}
}
if (offscreen) {
info.Dock();
}
}
}
if (g_settings.getInteger(Config::MINIMAP_VISIBLE)) {
if (!minimap) {
wxAuiPaneInfo info;
const wxString &data = wxstr(g_settings.getString(Config::MINIMAP_LAYOUT));
aui_manager->LoadPaneInfo(data, info);
minimap = newd MinimapWindow(root);
aui_manager->AddPane(minimap, info);
} else {
wxAuiPaneInfo &info = aui_manager->GetPane(minimap);
const wxString &data = wxstr(g_settings.getString(Config::MINIMAP_LAYOUT));
aui_manager->LoadPaneInfo(data, info);
}
wxAuiPaneInfo &info = aui_manager->GetPane(minimap);
if (info.IsFloatable()) {
bool offscreen = true;
for (uint32_t index = 0; index < wxDisplay::GetCount(); ++index) {
wxDisplay display(index);
wxRect rect = display.GetClientArea();
if (rect.Contains(info.floating_pos)) {
offscreen = false;
break;
}
}
if (offscreen) {
info.Dock();
}
}
}
if (g_settings.getInteger(Config::ACTIONS_HISTORY_VISIBLE)) {
if (!actions_history_window) {
wxAuiPaneInfo info;
const wxString &data = wxstr(g_settings.getString(Config::ACTIONS_HISTORY_LAYOUT));
aui_manager->LoadPaneInfo(data, info);
actions_history_window = new ActionsHistoryWindow(root);
aui_manager->AddPane(actions_history_window, info);
} else {
wxAuiPaneInfo &info = aui_manager->GetPane(actions_history_window);
const wxString &data = wxstr(g_settings.getString(Config::ACTIONS_HISTORY_LAYOUT));
aui_manager->LoadPaneInfo(data, info);
}
wxAuiPaneInfo &info = aui_manager->GetPane(actions_history_window);
if (info.IsFloatable()) {
bool offscreen = true;
for (uint32_t index = 0; index < wxDisplay::GetCount(); ++index) {
wxDisplay display(index);
wxRect rect = display.GetClientArea();
if (rect.Contains(info.floating_pos)) {
offscreen = false;
break;
}
}
if (offscreen) {
info.Dock();
}
}
}
aui_manager->Update();
root->UpdateMenubar();
}
root->GetAuiToolBar()->LoadPerspective();
}
void GUI::SavePerspective() {
g_settings.setInteger(Config::WINDOW_MAXIMIZED, root->IsMaximized());
g_settings.setInteger(Config::WINDOW_WIDTH, root->GetSize().GetWidth());
g_settings.setInteger(Config::WINDOW_HEIGHT, root->GetSize().GetHeight());
g_settings.setInteger(Config::MINIMAP_VISIBLE, minimap ? 1 : 0);
g_settings.setInteger(Config::ACTIONS_HISTORY_VISIBLE, actions_history_window ? 1 : 0);
wxString pinfo;
for (auto &palette : palettes) {
if (aui_manager->GetPane(palette).IsShown()) {
pinfo << aui_manager->SavePaneInfo(aui_manager->GetPane(palette)) << "|";
}
}
g_settings.setString(Config::PALETTE_LAYOUT, nstr(pinfo));
if (minimap) {
wxString s = aui_manager->SavePaneInfo(aui_manager->GetPane(minimap));
g_settings.setString(Config::MINIMAP_LAYOUT, nstr(s));
}
if (actions_history_window) {
wxString info = aui_manager->SavePaneInfo(aui_manager->GetPane(actions_history_window));
g_settings.setString(Config::ACTIONS_HISTORY_LAYOUT, nstr(info));
}
root->GetAuiToolBar()->SavePerspective();
}
void GUI::HideSearchWindow() {
if (search_result_window) {
aui_manager->GetPane(search_result_window).Show(false);
aui_manager->Update();
}
}
SearchResultWindow* GUI::ShowSearchWindow() {
if (search_result_window == nullptr) {
search_result_window = newd SearchResultWindow(root);
aui_manager->AddPane(search_result_window, wxAuiPaneInfo().Caption("Search Results"));
} else {
aui_manager->GetPane(search_result_window).Show();
}
aui_manager->Update();
return search_result_window;
}
ActionsHistoryWindow* GUI::ShowActionsWindow() {
if (!actions_history_window) {
actions_history_window = new ActionsHistoryWindow(root);
aui_manager->AddPane(actions_history_window, wxAuiPaneInfo().Caption("Actions History"));
} else {
aui_manager->GetPane(actions_history_window).Show();
}
aui_manager->Update();
actions_history_window->RefreshActions();
return actions_history_window;
}
void GUI::HideActionsWindow() {
if (actions_history_window) {
aui_manager->GetPane(actions_history_window).Show(false);
aui_manager->Update();
}
}
LuaScriptsWindow* GUI::ShowScriptManagerWindow() {
if (!script_manager_window) {
script_manager_window = new LuaScriptsWindow(root);
LuaScriptsWindow::SetInstance(script_manager_window);
aui_manager->AddPane(script_manager_window, wxAuiPaneInfo().Caption("Script Manager").Right().Layer(1).CloseButton(true).MinSize(300, 200).BestSize(400, 300));
} else {
aui_manager->GetPane(script_manager_window).Show();
}
aui_manager->Update();
script_manager_window->RefreshScriptList();
return script_manager_window;
}
//=============================================================================
// Palette Window Interface implementation
PaletteWindow* GUI::GetPalette() {
if (palettes.empty()) {
return nullptr;
}
return palettes.front();
}
PaletteWindow* GUI::NewPalette() {
return CreatePalette();
}
void GUI::RefreshPalettes(Map* m, bool usedefault) {
for (auto &palette : palettes) {
const auto currentMap = IsEditorOpen() ? &GetCurrentMap() : nullptr;
const auto defaultMap = usedefault ? currentMap : nullptr;
palette->OnUpdate(m ? m : defaultMap);
}
SelectBrush();
RefreshActions();
}
void GUI::RefreshOtherPalettes(PaletteWindow* p) {
for (auto &palette : palettes) {
if (palette != p) {
palette->OnUpdate(IsEditorOpen() ? &GetCurrentMap() : nullptr);
}
}
SelectBrush();
}
PaletteWindow* GUI::CreatePalette() {
if (!ClientAssets::isLoaded()) {
return nullptr;
}
auto* palette = newd PaletteWindow(root, g_materials.tilesets);
aui_manager->AddPane(palette, wxAuiPaneInfo().Caption("Palette").TopDockable(false).BottomDockable(false));
palette->OnUpdate(GetCurrentMapTab()->GetMap());
aui_manager->Update();
// Make us the active palette
palettes.push_front(palette);
// Select brush from this palette
SelectBrushInternal(palette->GetSelectedBrush());
RefreshPalettes();
return palette;
}
void GUI::ActivatePalette(PaletteWindow* p) {
palettes.erase(std::find(palettes.begin(), palettes.end(), p));
palettes.push_front(p);
}
void GUI::DestroyPalettes() {
for (auto palette : palettes) {
aui_manager->DetachPane(palette);
palette->Destroy();
palette = nullptr;
}
palettes.clear();
aui_manager->Update();
}
void GUI::RebuildPalettes() {
// Palette lits might be modified due to active palette changes
// Use a temporary list for iterating
PaletteList tmp = palettes;
for (auto &piter : tmp) {
piter->ReloadSettings(IsEditorOpen() ? &GetCurrentMap() : nullptr);
}
aui_manager->Update();
}
void GUI::ShowPalette() {
if (palettes.empty()) {
return;
}
for (auto &palette : palettes) {
if (aui_manager->GetPane(palette).IsShown()) {
return;
}
}
aui_manager->GetPane(palettes.front()).Show(true);
aui_manager->Update();
}
void GUI::SelectPalettePage(PaletteType pt) {
if (palettes.empty()) {
CreatePalette();
}
PaletteWindow* p = GetPalette();
if (!p) {
return;
}
ShowPalette();
p->SelectPage(pt);
aui_manager->Update();
SelectBrushInternal(p->GetSelectedBrush());
}
//=============================================================================
// Minimap Window Interface Implementation
void GUI::CreateMinimap() {
if (!ClientAssets::isLoaded()) {
return;
}
if (minimap) {
aui_manager->GetPane(minimap).Show(true);
} else {
minimap = newd MinimapWindow(root);
minimap->Show(true);
aui_manager->AddPane(minimap, wxAuiPaneInfo().Caption("Minimap"));
}
aui_manager->Update();
}
void GUI::HideMinimap() {
if (minimap) {
aui_manager->GetPane(minimap).Show(false);
aui_manager->Update();
}
}
void GUI::DestroyMinimap() {
if (minimap) {
aui_manager->DetachPane(minimap);
aui_manager->Update();
minimap->Destroy();
minimap = nullptr;
}
}
void GUI::UpdateMinimap(bool immediate) {
if (IsMinimapVisible()) {
if (immediate) {
minimap->Refresh();
} else {
minimap->DelayedUpdate();
}
}
}
bool GUI::IsMinimapVisible() const {
if (minimap) {
const wxAuiPaneInfo &pi = aui_manager->GetPane(minimap);
if (pi.IsShown()) {
return true;
}
}
return false;
}
//=============================================================================
void GUI::RefreshView() {
EditorTab* editorTab = GetCurrentTab();
if (!editorTab) {
return;
}
if (!dynamic_cast<MapTab*>(editorTab)) {
editorTab->GetWindow()->Refresh();
return;
}
std::vector<EditorTab*> editorTabs;
for (int32_t index = 0; index < tabbook->GetTabCount(); ++index) {
auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(index));
if (mapTab) {
editorTabs.push_back(mapTab);
}
}
for (EditorTab* editorTab : editorTabs) {
auto* mapTab = static_cast<MapTab*>(editorTab);
mapTab->GetCanvas()->Refresh(); // MapCanvas::Refresh() → markDirty() + wxGLCanvas::Refresh()
editorTab->GetWindow()->Update();
}
}
void GUI::CreateLoadBar(wxString message, bool canCancel /* = false */) {
CreateLoadBar(message, canCancel, true);
}
void GUI::CreateLoadBar(wxString message, bool canCancel, bool appModal) {
progressText = message;
progressFrom = 0;
progressTo = 100;
currentProgress = -1;
const long style = (appModal ? wxPD_APP_MODAL : 0) | wxPD_SMOOTH | (canCancel ? wxPD_CAN_ABORT : 0);
progressBar = newd wxGenericProgressDialog("Loading", progressText + " (0%)", 100, root, style);
progressBar->SetSize(280, -1);
progressBar->Show(true);
for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
auto* mt = dynamic_cast<MapTab*>(tabbook->GetTab(idx));
if (mt && mt->GetEditor()->IsLiveServer()) {
mt->GetEditor()->GetLiveServer()->startOperation(progressText);
}
}
progressBar->Update(0);
}
void GUI::SetLoadScale(int32_t from, int32_t to) {
progressFrom = from;
progressTo = to;
}
bool GUI::SetLoadDone(int32_t done, const wxString &newMessage) {
if (done == 100) {
DestroyLoadBar();
return true;
}
int32_t newProgress = progressFrom + static_cast<int32_t>((done / 100.f) * (progressTo - progressFrom));
newProgress = std::max<int32_t>(0, std::min<int32_t>(100, newProgress));
bool messageChanged = !newMessage.empty() && newMessage != progressText;
if (newProgress == currentProgress && !messageChanged) {
return true;
}
if (!newMessage.empty()) {
progressText = newMessage;
}
bool skip = false;
bool continueProcessing = true;
if (progressBar) {
continueProcessing = progressBar->Update(
newProgress,
wxString::Format("%s (%d%%)", progressText.c_str(), newProgress),
&skip
);
currentProgress = newProgress;
}
for (int32_t index = 0; index < tabbook->GetTabCount(); ++index) {
auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(index));
if (mapTab && mapTab->GetEditor()) {
LiveServer* server = mapTab->GetEditor()->GetLiveServer();
if (server) {
server->updateOperation(newProgress);
}
}
}
return continueProcessing && !skip;
}
void GUI::DestroyLoadBar() {
if (progressBar) {
progressBar->Show(false);
currentProgress = -1;
progressBar->Destroy();
progressBar = nullptr;
if (root->IsActive()) {
root->Raise();
} else {
root->RequestUserAttention();
}
}
}
void GUI::ShowWelcomeDialog(const wxBitmap &icon) {
std::vector<wxString> recent_files = root->GetRecentFiles();
welcomeDialog = newd WelcomeDialog(__W_RME_APPLICATION_NAME__, "Version " + __W_RME_VERSION__, FROM_DIP(root, wxSize(800, 480)), icon, recent_files);
welcomeDialog->Bind(wxEVT_CLOSE_WINDOW, &GUI::OnWelcomeDialogClosed, this);
welcomeDialog->Bind(WELCOME_DIALOG_ACTION, &GUI::OnWelcomeDialogAction, this);
welcomeDialog->Show();
UpdateMenubar();
}
void GUI::FinishWelcomeDialog() {
if (welcomeDialog != nullptr) {
welcomeDialog->Hide();
root->Show();
welcomeDialog->Destroy();
welcomeDialog = nullptr;
}
}
bool GUI::IsWelcomeDialogShown() {
return welcomeDialog != nullptr && welcomeDialog->IsShown();
}
void GUI::OnWelcomeDialogClosed(wxCloseEvent &event) {
welcomeDialog->Destroy();
root->Close();
}
void GUI::OnWelcomeDialogAction(wxCommandEvent &event) {
if (event.GetId() == wxID_NEW) {
NewMap();
} else if (event.GetId() == wxID_OPEN) {
LoadMap(FileName(event.GetString()));
}
}
void GUI::UpdateMenubar() {
if (root == nullptr) {
return;
}
root->UpdateMenubar();
}
void GUI::SetScreenCenterPosition(const Position &position, bool showIndicator) {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
mapTab->SetScreenCenterPosition(position, showIndicator);
}
}
void GUI::DoCut() {
if (!IsSelectionMode()) {
return;
}
Editor* editor = GetCurrentEditor();
if (!editor) {
return;
}
editor->copybuffer.cut(*editor, GetCurrentFloor());
RefreshView();
root->UpdateMenubar();
}
void GUI::DoCopy() {
if (!IsSelectionMode()) {
return;
}
Editor* editor = GetCurrentEditor();
if (!editor) {
return;
}
editor->copybuffer.copy(*editor, GetCurrentFloor());
RefreshView();
root->UpdateMenubar();
}
void GUI::DoPaste() {
MapTab* mapTab = GetCurrentMapTab();
if (mapTab) {
copybuffer.paste(*mapTab->GetEditor(), mapTab->GetCanvas()->GetCursorPosition());
}
}
void GUI::PreparePaste() {
Editor* editor = GetCurrentEditor();
if (editor) {
SetSelectionMode();
Selection &selection = editor->getSelection();
selection.start();
selection.clear();
selection.finish();
StartPasting();
RefreshView();
}
}
void GUI::StartPasting() {
if (GetCurrentEditor()) {
pasting = true;
secondary_map = &copybuffer.getBufferMap();
}
}
void GUI::EndPasting() {
if (pasting) {
pasting = false;
secondary_map = nullptr;
}
}
bool GUI::CanUndo() {
Editor* editor = GetCurrentEditor();
return (editor && editor->canUndo());
}
bool GUI::CanRedo() {
Editor* editor = GetCurrentEditor();
return (editor && editor->canRedo());
}
bool GUI::DoUndo() {
Editor* editor = GetCurrentEditor();
if (editor && editor->canUndo()) {
editor->undo();
if (editor->hasSelection()) {
SetSelectionMode();
}
SetStatusText("Undo action");
UpdateMinimap();
root->UpdateMenubar();
RefreshView();
return true;
}
return false;
}
bool GUI::DoRedo() {
Editor* editor = GetCurrentEditor();
if (editor && editor->canRedo()) {
editor->redo();
if (editor->hasSelection()) {
SetSelectionMode();
}
SetStatusText("Redo action");
UpdateMinimap();
root->UpdateMenubar();
RefreshView();
return true;
}
return false;
}
int GUI::GetCurrentFloor() {
MapTab* tab = GetCurrentMapTab();
ASSERT(tab);
return tab->GetCanvas()->GetFloor();
}
void GUI::ChangeFloor(int new_floor) {
MapTab* tab = GetCurrentMapTab();
if (tab) {
int old_floor = GetCurrentFloor();
if (new_floor < rme::MapMinLayer || new_floor > rme::MapMaxLayer) {
return;
}
if (old_floor != new_floor) {
tab->GetCanvas()->ChangeFloor(new_floor);
}
}
}
void GUI::SetStatusText(wxString text) {
if (g_gui.root == nullptr) {
return;
}
g_gui.root->SetStatusText(text, 0);
}
bool GUI::IsAsyncSqliteBootstrapRunning() const {
return sqlite_bootstrap_running_.load();
}
void GUI::JoinAsyncSqliteBootstrapThread() {
if (!sqlite_bootstrap_thread_.joinable()) {
return;
}
sqlite_bootstrap_thread_.join();
sqlite_bootstrap_running_.store(false);
}
void GUI::RunAsyncSqliteBootstrapImport() {
wxString sqliteImportError;
wxArrayString sqliteWarnings;
const bool success = g_materials.bootstrapSqliteDatabase(sqliteImportError, sqliteWarnings);
for (const wxString &warning : sqliteWarnings) {
spdlog::warn("[SQLiteBootstrap] {}", warning.ToStdString());
}
if (!success && !sqliteImportError.IsEmpty()) {
spdlog::error("[SQLiteBootstrap] {}", sqliteImportError.ToStdString());
}
sqlite_bootstrap_running_.store(false);
if (wxTheApp) {
wxTheApp->CallAfter([this, success, sqliteImportError, sqliteWarnings]() {
HandleAsyncSqliteBootstrapResult(success, sqliteImportError, sqliteWarnings);
});
}
}
void GUI::HandleAsyncSqliteBootstrapResult(bool success, const wxString &sqliteImportError, const wxArrayString &sqliteWarnings) {
g_gui.UpdateMenubar();
if (success) {
if (sqliteWarnings.IsEmpty()) {
g_gui.SetStatusText("SQLite materials database built in background.");
} else {
g_gui.SetStatusText("SQLite materials database built in background with warnings.");
}
return;
}
g_gui.SetStatusText("SQLite materials background build failed.");
if (g_gui.root) {
g_gui.PopupDialog(g_gui.root, "SQLite Materials Import Failed", sqliteImportError, wxOK | wxICON_WARNING);
}
}
void GUI::StartAsyncSqliteBootstrapImport() {
if (sqlite_bootstrap_running_.exchange(true)) {
return;
}
JoinAsyncSqliteBootstrapThread();
SetStatusText("Building SQLite materials database in background...");
UpdateMenubar();
sqlite_bootstrap_thread_ = std::jthread(&GUI::RunAsyncSqliteBootstrapImport, this);
}
void GUI::SetTitle(wxString title) {
if (g_gui.root == nullptr) {
return;
}
#ifdef NIGHTLY_BUILD
#ifdef SVN_BUILD
#define TITLE_APPEND (wxString(" (Nightly Build #") << i2ws(SVN_BUILD) << ")")
#else
#define TITLE_APPEND (wxString(" (Nightly Build)"))
#endif
#else
#ifdef SVN_BUILD
#define TITLE_APPEND (wxString(" (Build #") << i2ws(SVN_BUILD) << ")")
#else
#define TITLE_APPEND (wxString(""))
#endif
#endif
#ifdef __EXPERIMENTAL__
if (title != "") {
g_gui.root->SetTitle(title << " - Canary's Map Editor BETA" << TITLE_APPEND);
} else {
g_gui.root->SetTitle(wxString("Canary's Map Editor BETA") << TITLE_APPEND);
}
#elif __SNAPSHOT__
if (title != "") {
g_gui.root->SetTitle(title << " - Canary's Map Editor - SNAPSHOT" << TITLE_APPEND);
} else {
g_gui.root->SetTitle(wxString("Canary's Map Editor - SNAPSHOT") << TITLE_APPEND);
}
#else
if (!title.empty()) {
g_gui.root->SetTitle(title << " - Canary's Map Editor" << TITLE_APPEND);
} else {
g_gui.root->SetTitle(wxString("Canary's Map Editor") << TITLE_APPEND);
}
#endif
}
void GUI::UpdateTitle() {
if (tabbook->GetTabCount() > 0) {
SetTitle(tabbook->GetCurrentTab()->GetTitle());
for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
if (tabbook->GetTab(idx)) {
tabbook->SetTabLabel(idx, tabbook->GetTab(idx)->GetTitle());
}
}
} else {
SetTitle("");
}
}
void GUI::UpdateMenus() {
wxCommandEvent evt(EVT_UPDATE_MENUS);
g_gui.root->AddPendingEvent(evt);
}
void GUI::UpdateActions() {
wxCommandEvent evt(EVT_UPDATE_ACTIONS);
g_gui.root->AddPendingEvent(evt);
}
void GUI::RefreshActions() {
if (actions_history_window) {
actions_history_window->RefreshActions();
}
}
void GUI::ShowToolbar(ToolBarID id, bool show) {
if (root && root->GetAuiToolBar()) {
root->GetAuiToolBar()->Show(id, show);
}
}
void GUI::SwitchMode() {
if (mode == DRAWING_MODE) {
SetSelectionMode();
} else {
SetDrawingMode();
}
}
void GUI::SetSelectionMode() {
if (mode == SELECTION_MODE) {
return;
}
if (current_brush && current_brush->isDoodad()) {
secondary_map = nullptr;
}
tabbook->OnSwitchEditorMode(SELECTION_MODE);
mode = SELECTION_MODE;
}
void GUI::SetDrawingMode() {
if (mode == DRAWING_MODE) {
return;
}
std::set<MapTab*> al;
for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
EditorTab* editorTab = tabbook->GetTab(idx);
if (MapTab* mapTab = dynamic_cast<MapTab*>(editorTab)) {
if (al.find(mapTab) != al.end()) {
continue;
}
Editor* editor = mapTab->GetEditor();
Selection &selection = editor->getSelection();
selection.start(Selection::NONE, ACTION_UNSELECT);
selection.clear();
selection.finish();
selection.updateSelectionCount();
al.insert(mapTab);
}
}
if (current_brush && current_brush->isDoodad()) {
secondary_map = doodad_buffer_map;
} else {
secondary_map = nullptr;
}
tabbook->OnSwitchEditorMode(DRAWING_MODE);
mode = DRAWING_MODE;
}
void GUI::SetBrushSizeInternal(int nz) {
if (nz != brush_size && current_brush && current_brush->isDoodad() && !current_brush->oneSizeFitsAll()) {
brush_size = nz;
FillDoodadPreviewBuffer();
secondary_map = doodad_buffer_map;
} else {
brush_size = nz;
}
}
void GUI::SetBrushSize(int nz) {
SetBrushSizeInternal(nz);
for (auto &palette : palettes) {
palette->OnUpdateBrushSize(brush_shape, brush_size);
}
root->GetAuiToolBar()->UpdateBrushSize(brush_shape, brush_size);
}
void GUI::SetBrushVariation(int nz) {
if (nz != brush_variation && current_brush && current_brush->isDoodad()) {
// Monkey!
brush_variation = nz;
FillDoodadPreviewBuffer();
secondary_map = doodad_buffer_map;
}
}
void GUI::SetBrushShape(BrushShape bs) {
if (bs != brush_shape && current_brush && current_brush->isDoodad() && !current_brush->oneSizeFitsAll()) {
// Donkey!
brush_shape = bs;
FillDoodadPreviewBuffer();
secondary_map = doodad_buffer_map;
}
brush_shape = bs;
for (auto &palette : palettes) {
palette->OnUpdateBrushSize(brush_shape, brush_size);
}
root->GetAuiToolBar()->UpdateBrushSize(brush_shape, brush_size);
}
void GUI::SetBrushThickness(bool on, int x, int y) {
use_custom_thickness = on;
if (x != -1 || y != -1) {
custom_thickness_mod = std::max<float>(x, 1.f) / std::max<float>(y, 1.f);
}
if (current_brush && current_brush->isDoodad()) {
FillDoodadPreviewBuffer();
}
RefreshView();
}
void GUI::SetBrushThickness(int low, int ceil) {
custom_thickness_mod = std::max<float>(low, 1.f) / std::max<float>(ceil, 1.f);
if (use_custom_thickness && current_brush && current_brush->isDoodad()) {
FillDoodadPreviewBuffer();
}
RefreshView();
}
void GUI::DecreaseBrushSize(bool wrap) {
switch (brush_size) {
case 0: {
if (wrap) {
SetBrushSize(11);
}
break;
}
case 1: {
SetBrushSize(0);
break;
}
case 2:
case 3: {
SetBrushSize(1);
break;
}
case 4:
case 5: {
SetBrushSize(2);
break;
}
case 6:
case 7: {
SetBrushSize(4);
break;
}
case 8:
case 9:
case 10: {
SetBrushSize(6);
break;
}
case 11:
default: {
SetBrushSize(8);
break;
}
}
}
void GUI::IncreaseBrushSize(bool wrap) {
switch (brush_size) {
case 0: {
SetBrushSize(1);
break;
}
case 1: {
SetBrushSize(2);
break;
}
case 2:
case 3: {
SetBrushSize(4);
break;
}
case 4:
case 5: {
SetBrushSize(6);
break;
}
case 6:
case 7: {
SetBrushSize(8);
break;
}
case 8:
case 9:
case 10: {
SetBrushSize(11);
break;
}
case 11:
default: {
if (wrap) {
SetBrushSize(0);
}
break;
}
}
}
Brush* GUI::GetCurrentBrush() const {
return current_brush;
}
BrushShape GUI::GetBrushShape() const {
if (current_brush == spawn_brush) {
return BRUSHSHAPE_SQUARE;
}
if (current_brush == spawn_npc_brush) {
return BRUSHSHAPE_SQUARE;
}
return brush_shape;
}
int GUI::GetBrushSize() const {
return brush_size;
}
int GUI::GetBrushVariation() const {
return brush_variation;
}
int GUI::GetSpawnMonsterTime() const {
return monster_spawntime;
}
int GUI::GetSpawnNpcTime() const {
return npc_spawntime;
}
void GUI::SelectBrush() {
if (palettes.empty()) {
return;
}
SelectBrushInternal(palettes.front()->GetSelectedBrush());
RefreshView();
}
bool GUI::SelectBrush(const Brush* whatbrush, PaletteType primary) {
if (palettes.empty()) {
if (!CreatePalette()) {
return false;
}
}
if (!palettes.front()->OnSelectBrush(whatbrush, primary)) {
return false;
}
SelectBrushInternal(const_cast<Brush*>(whatbrush));
root->GetAuiToolBar()->UpdateBrushButtons();
return true;
}
void GUI::SelectBrushInternal(Brush* brush) {
// Fear no evil don't you say no evil
if (current_brush != brush && brush) {
previous_brush = current_brush;
}
current_brush = brush;
if (!current_brush) {
return;
}
brush_variation = std::min(brush_variation, brush->getMaxVariation());
FillDoodadPreviewBuffer();
if (brush->isDoodad()) {
secondary_map = doodad_buffer_map;
}
SetDrawingMode();
RefreshView();
}
void GUI::SelectPreviousBrush() {
if (previous_brush) {
SelectBrush(previous_brush);
}
}
void GUI::FillDoodadPreviewBuffer() {
if (!current_brush || !current_brush->isDoodad()) {
return;
}
doodad_buffer_map->clear();
DoodadBrush* brush = current_brush->asDoodad();
if (brush->isEmpty(GetBrushVariation())) {
return;
}
int object_count = 0;
int area;
if (GetBrushShape() == BRUSHSHAPE_SQUARE) {
area = 2 * GetBrushSize();
area = area * area + 1;
} else {
if (GetBrushSize() == 1) {
// There is a huge deviation here with the other formula.
area = 5;
} else {
area = int(0.5 + GetBrushSize() * GetBrushSize() * rme::PI);
}
}
const int object_range = (use_custom_thickness ? int(area * custom_thickness_mod) : brush->getThickness() * area / std::max(1, brush->getThicknessCeiling()));
const int final_object_count = std::max(1, object_range + random(object_range));
Position center_pos(0x8000, 0x8000, 0x8);
if (brush_size > 0 && !brush->oneSizeFitsAll()) {
while (object_count < final_object_count) {
int retries = 0;
bool exit = false;
// Try to place objects 5 times
while (retries < 5 && !exit) {
int pos_retries = 0;
int xpos = 0, ypos = 0;
bool found_pos = false;
if (GetBrushShape() == BRUSHSHAPE_CIRCLE) {
while (pos_retries < 5 && !found_pos) {
xpos = random(-brush_size, brush_size);
ypos = random(-brush_size, brush_size);
float distance = sqrt(float(xpos * xpos) + float(ypos * ypos));
if (distance < g_gui.GetBrushSize() + 0.005) {
found_pos = true;
} else {
++pos_retries;
}
}
} else {
found_pos = true;
xpos = random(-brush_size, brush_size);
ypos = random(-brush_size, brush_size);
}
if (!found_pos) {
++retries;
continue;
}
// Decide whether the zone should have a composite or several single objects.
bool fail = false;
if (random(brush->getTotalChance(GetBrushVariation())) <= brush->getCompositeChance(GetBrushVariation())) {
// Composite
const CompositeTileList &composites = brush->getComposite(GetBrushVariation());
// Figure out if the placement is valid
for (const auto &composite : composites) {
Position pos = center_pos + composite.first + Position(xpos, ypos, 0);
if (Tile* tile = doodad_buffer_map->getTile(pos)) {
if (!tile->empty()) {
fail = true;
break;
}
}
}
if (fail) {
++retries;
break;
}
// Transfer items to the stack
for (const auto &composite : composites) {
Position pos = center_pos + composite.first + Position(xpos, ypos, 0);
const ItemVector &items = composite.second;
Tile* tile = doodad_buffer_map->getTile(pos);
if (!tile) {
tile = doodad_buffer_map->allocator(doodad_buffer_map->createTileL(pos));
}
for (auto item : items) {
tile->addItem(item->deepCopy());
}
doodad_buffer_map->setTile(tile->getPosition(), tile);
}
exit = true;
} else if (brush->hasSingleObjects(GetBrushVariation())) {
Position pos = center_pos + Position(xpos, ypos, 0);
Tile* tile = doodad_buffer_map->getTile(pos);
if (tile) {
if (!tile->empty()) {
fail = true;
break;
}
} else {
tile = doodad_buffer_map->allocator(doodad_buffer_map->createTileL(pos));
}
int variation = GetBrushVariation();
brush->draw(doodad_buffer_map, tile, &variation);
// std::cout << "\tpos: " << tile->getPosition() << std::endl;
doodad_buffer_map->setTile(tile->getPosition(), tile);
exit = true;
}
if (fail) {
++retries;
break;
}
}
++object_count;
}
} else {
if (brush->hasCompositeObjects(GetBrushVariation()) && random(brush->getTotalChance(GetBrushVariation())) <= brush->getCompositeChance(GetBrushVariation())) {
// Composite
const CompositeTileList &composites = brush->getComposite(GetBrushVariation());
// All placement is valid...
// Transfer items to the buffer
for (const auto &composite : composites) {
Position pos = center_pos + composite.first;
const ItemVector &items = composite.second;
Tile* tile = doodad_buffer_map->allocator(doodad_buffer_map->createTileL(pos));
// std::cout << pos << " = " << center_pos << " + " << buffer_tile->getPosition() << std::endl;
for (auto item : items) {
tile->addItem(item->deepCopy());
}
doodad_buffer_map->setTile(tile->getPosition(), tile);
}
} else if (brush->hasSingleObjects(GetBrushVariation())) {
Tile* tile = doodad_buffer_map->allocator(doodad_buffer_map->createTileL(center_pos));
int variation = GetBrushVariation();
brush->draw(doodad_buffer_map, tile, &variation);
doodad_buffer_map->setTile(center_pos, tile);
}
}
}
long GUI::PopupDialog(wxWindow* parent, wxString title, wxString text, long style, wxString confisavename, uint32_t configsavevalue) {
if (text.empty()) {
return wxID_ANY;
}
wxMessageDialog dlg(parent, text, title, style);
return dlg.ShowModal();
}
long GUI::PopupDialog(wxString title, wxString text, long style, wxString configsavename, uint32_t configsavevalue) {
return g_gui.PopupDialog(g_gui.root, title, text, style, configsavename, configsavevalue);
}
void GUI::ListDialog(wxWindow* parent, wxString title, const wxArrayString &param_items) {
if (param_items.empty()) {
return;
}
wxArrayString list_items(param_items);
// Create the window
wxDialog* dlg = newd wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
wxSizer* sizer = newd wxBoxSizer(wxVERTICAL);
wxListBox* item_list = newd wxListBox(dlg, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
item_list->SetMinSize(wxSize(500, 300));
for (size_t i = 0; i != list_items.GetCount();) {
wxString str = list_items[i];
size_t pos = str.find("\n");
if (pos != wxString::npos) {
// Split string!
item_list->Append(str.substr(0, pos));
list_items[i] = str.substr(pos + 1);
continue;
}
item_list->Append(list_items[i]);
++i;
}
sizer->Add(item_list, 1, wxEXPAND);
wxSizer* stdsizer = newd wxBoxSizer(wxHORIZONTAL);
stdsizer->Add(newd wxButton(dlg, wxID_OK, "OK"), wxSizerFlags(1).Center());
sizer->Add(stdsizer, wxSizerFlags(0).Center());
dlg->SetSizerAndFit(sizer);
// Show the window
dlg->ShowModal();
delete dlg;
}
void GUI::ShowTextBox(wxWindow* parent, wxString title, wxString content) {
wxDialog* dlg = newd wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL);
wxTextCtrl* text_field = newd wxTextCtrl(dlg, wxID_ANY, content, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
text_field->SetMinSize(wxSize(400, 550));
topsizer->Add(text_field, wxSizerFlags(5).Expand());
wxSizer* choicesizer = newd wxBoxSizer(wxHORIZONTAL);
choicesizer->Add(newd wxButton(dlg, wxID_CANCEL, "OK"), wxSizerFlags(1).Center());
topsizer->Add(choicesizer, wxSizerFlags(0).Center());
dlg->SetSizerAndFit(topsizer);
dlg->ShowModal();
}
void GUI::SetHotkey(int index, Hotkey &hotkey) {
ASSERT(index >= 0 && index <= 9);
hotkeys[index] = hotkey;
SetStatusText("Set hotkey " + i2ws(index) + ".");
}
const Hotkey &GUI::GetHotkey(int index) const {
ASSERT(index >= 0 && index <= 9);
return hotkeys[index];
}
void GUI::SaveHotkeys() const {
std::ostringstream os;
for (const auto &hotkey : hotkeys) {
os << hotkey << '\n';
}
g_settings.setString(Config::NUMERICAL_HOTKEYS, os.str());
}
void GUI::LoadHotkeys() {
std::istringstream is;
is.str(g_settings.getString(Config::NUMERICAL_HOTKEYS));
std::string line;
int index = 0;
while (getline(is, line)) {
std::istringstream line_is;
line_is.str(line);
line_is >> hotkeys[index];
++index;
}
}
Hotkey::Hotkey() :
type(NONE) {
////
}
Hotkey::Hotkey(Position _pos) :
type(POSITION), pos(_pos) {
////
}
Hotkey::Hotkey(Brush* brush) :
type(BRUSH), brushname(brush->getName()) {
////
}
Hotkey::Hotkey(std::string _name) :
type(BRUSH), brushname(_name) {
////
}
Hotkey::~Hotkey() {
////
}
std::ostream &operator<<(std::ostream &os, const Hotkey &hotkey) {
switch (hotkey.type) {
case Hotkey::POSITION: {
os << "pos:{" << hotkey.pos << "}";
} break;
case Hotkey::BRUSH: {
if (hotkey.brushname.find('{') != std::string::npos || hotkey.brushname.find('}') != std::string::npos) {
break;
}
os << "brush:{" << hotkey.brushname << "}";
} break;
default: {
os << "none:{}";
} break;
}
return os;
}
std::istream &operator>>(std::istream &is, Hotkey &hotkey) {
std::string type;
getline(is, type, ':');
if (type == "none") {
is.ignore(2); // ignore "{}"
} else if (type == "pos") {
is.ignore(1); // ignore "{"
Position pos;
is >> pos;
hotkey = Hotkey(pos);
is.ignore(1); // ignore "}"
} else if (type == "brush") {
is.ignore(1); // ignore "{"
std::string brushname;
getline(is, brushname, '}');
hotkey = Hotkey(brushname);
} else {
// Do nothing...
}
return is;
}
void SetWindowToolTip(wxWindow* a, const wxString &tip) {
a->SetToolTip(tip);
}
void SetWindowToolTip(wxWindow* a, wxWindow* b, const wxString &tip) {
a->SetToolTip(tip);
b->SetToolTip(tip);
}