//////////////////////////////////////////////////////////////////////
// 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 .
//////////////////////////////////////////////////////////////////////
#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"
#include
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) {
OGLContext = newd wxGLContext(win);
}
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::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::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::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::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::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(tabbook->GetTab(index))) {
tab->GetView()->FitToMap();
}
}
}
void GUI::FitViewToMap(MapTab* mt) {
for (int index = 0; index < tabbook->GetTabCount(); ++index) {
if (auto* tab = dynamic_cast(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(editorTab);
return mapTab;
}
return nullptr;
}
Map &GUI::GetCurrentMap() {
Editor* editor = GetCurrentEditor();
ASSERT(editor);
return editor->getMap();
}
int GUI::GetOpenMapCount() {
std::set