diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml
index a3a425d42..8f34d13a2 100644
--- a/.github/workflows/continuous_integration.yml
+++ b/.github/workflows/continuous_integration.yml
@@ -13,7 +13,7 @@ jobs:
- {
name: "macOS",
os: macos-12,
- deps_cmdline: "brew install fluidsynth freeimage ftgl glm lua mpg123 sfml wxwidgets"
+ deps_cmdline: "brew install fluidsynth freeimage ftgl glm lua mpg123 sfml sqlite wxwidgets"
}
- {
name: "Linux GCC",
@@ -22,7 +22,7 @@ jobs:
sudo apt-add-repository 'deb https://repos.codelite.org/wx3.2.0/ubuntu/ focal universe' && \
sudo apt update && sudo apt install \
libfluidsynth-dev libfreeimage-dev libftgl-dev libglm-dev libgtk-3-dev \
- liblua5.3-dev libmpg123-dev libsfml-dev libwxgtk3.2unofficial-dev"
+ liblua5.3-dev libmpg123-dev libsfml-dev libsqlite3-dev libwxgtk3.2unofficial-dev"
}
- {
name: "Linux Clang",
@@ -32,7 +32,7 @@ jobs:
sudo apt-add-repository 'deb https://repos.codelite.org/wx3.2.0/ubuntu/ focal universe' && \
sudo apt update && sudo apt install \
libfluidsynth-dev libfreeimage-dev libftgl-dev libglm-dev libgtk-3-dev \
- liblua5.3-dev libmpg123-dev libsfml-dev libwxgtk3.2unofficial-dev"
+ liblua5.3-dev libmpg123-dev libsfml-dev libsqlite3-dev libwxgtk3.2unofficial-dev"
}
steps:
diff --git a/.gitignore b/.gitignore
index 9f00cb402..c318b3e63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@
!/dist/res
!/dist/CMakeLists.txt
!/dist/makebuild.ps1
+!/dist/7z_command.cmake
/msvc/*
!/msvc/resource.h
diff --git a/cmake/unix.cmake b/cmake/unix.cmake
index d889a849c..1ee1ec86b 100644
--- a/cmake/unix.cmake
+++ b/cmake/unix.cmake
@@ -111,6 +111,7 @@ if (NOT NO_LUA)
endif()
find_package(MPG123 REQUIRED)
find_package(glm REQUIRED)
+find_package(SQLite3 REQUIRED)
include_directories(
${FREEIMAGE_INCLUDE_DIR}
${SFML_INCLUDE_DIR}
@@ -180,6 +181,7 @@ target_link_libraries(slade
${LUA_LIBRARIES}
${MPG123_LIBRARIES}
glm::glm
+ sqlite3
)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION LESS 9)
diff --git a/cmake/win_msvc.cmake b/cmake/win_msvc.cmake
index 7bdd0625c..660c10e67 100644
--- a/cmake/win_msvc.cmake
+++ b/cmake/win_msvc.cmake
@@ -37,6 +37,8 @@ find_package(MPG123 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(SFML COMPONENTS system audio window network CONFIG REQUIRED)
find_package(glm REQUIRED)
+find_package(unofficial-sqlite3 CONFIG REQUIRED)
+#find_package(xxHash CONFIG REQUIRED)
# Include Search Paths ---------------------------------------------------------
@@ -46,6 +48,7 @@ include_directories(
.
..
../thirdparty/glad/include
+ ../thirdparty/SQLiteCpp/include
./Application
)
@@ -97,6 +100,8 @@ target_link_libraries(slade
sfml-network
sfml-window
glm::glm
+ unofficial::sqlite3::sqlite3
+# xxHash::xxhash
)
if (NOT NO_LUA)
diff --git a/dist/res/actions.cfg b/dist/res/actions.cfg
index 883e322a8..bb9d79d6d 100644
--- a/dist/res/actions.cfg
+++ b/dist/res/actions.cfg
@@ -12,6 +12,7 @@
#include "actions/ptxt.cfg" // TextEntryPanel
#include "actions/mapw.cfg" // MapEditorWindow
#include "actions/scrm.cfg" // ScriptManagerWindow
+#include "actions/alib.cfg" // LibraryPanel
// MapEntryPanel
action pmap_open_text
diff --git a/dist/res/actions/alib.cfg b/dist/res/actions/alib.cfg
new file mode 100644
index 000000000..f8b3e13d6
--- /dev/null
+++ b/dist/res/actions/alib.cfg
@@ -0,0 +1,21 @@
+
+action alib_open
+{
+ text = "Open";
+ icon = "open";
+ help_text = "Open the seleted archive(s)";
+}
+
+action alib_remove
+{
+ text = "Remove";
+ icon = "delete";
+ help_text = "Remove the selected archive(s) from the library";
+}
+
+action alib_run
+{
+ text = "Run";
+ icon = "run";
+ help_text = "Run the selected archive";
+}
diff --git a/dist/res/actions/main.cfg b/dist/res/actions/main.cfg
index cb6052495..d94d00932 100644
--- a/dist/res/actions/main.cfg
+++ b/dist/res/actions/main.cfg
@@ -32,7 +32,7 @@ action main_setbra
action main_preferences
{
- text = "&Preferences...";
+ text = "&Preferences";
icon = "settings";
help_text = "Setup SLADE options and preferences";
custom_wx_id = 5022; // wxID_PREFERENCES
@@ -100,7 +100,7 @@ action main_about
action main_updatecheck
{
- text = "Check for Updates...";
+ text = "Check for Updates";
help_text = "Check online for updates";
icon = "up";
}
@@ -111,3 +111,10 @@ action main_runscript
icon = "script";
help_text = "Open the Script Manager to write/run a SLADE script";
}
+
+action main_showlibrary
+{
+ text = "Archive Library";
+ icon = "library";
+ help_text = "Open the SLADE Archive Library";
+}
diff --git a/dist/res/config/archive_formats.cfg b/dist/res/config/archive_formats.cfg
index b763d8253..ca0bfbd48 100644
--- a/dist/res/config/archive_formats.cfg
+++ b/dist/res/config/archive_formats.cfg
@@ -13,8 +13,8 @@ archive_formats
extensions
{
- wad = "WAD";
- iwad = "IWAD";
+ wad = "Doom WAD";
+ iwad = "Doom IWAD";
dta = "SRB2 DTA";
gwa = "GL-Nodes WAD";
hwa = "EDGE HWA";
@@ -56,7 +56,7 @@ archive_formats
{
name = "Quake BSP";
entry_format = "archive_bsp";
- extensions { bsp = "BSP"; }
+ extensions { bsp = "Quake BSP"; }
}
bz2
@@ -209,7 +209,7 @@ archive_formats
max_name_length = 16;
names_extensions = false;
entry_format = "archive_wad2";
- extensions { wad = "WAD2"; }
+ extensions { wad = "Quake WAD2"; }
}
wadj
@@ -220,7 +220,7 @@ archive_formats
max_name_length = 8;
entry_format = "archive_wadj";
prefer_uppercase = true;
- extensions { wad = "WAD"; }
+ extensions { wad = "Jaguar Doom WAD"; }
}
wolf
@@ -249,6 +249,6 @@ archive_formats
max_name_length = 120;
supports_dirs = true;
entry_format = "archive_sin";
- extensions { sin = "SiN"; }
+ extensions { sin = "SiN File"; }
}
}
diff --git a/dist/res/database/tables/archive_bookmark.sql b/dist/res/database/tables/archive_bookmark.sql
new file mode 100644
index 000000000..22896b635
--- /dev/null
+++ b/dist/res/database/tables/archive_bookmark.sql
@@ -0,0 +1,12 @@
+BEGIN;
+
+CREATE TABLE archive_bookmark (
+ archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE,
+ entry_id INTEGER,
+ UNIQUE (
+ archive_id,
+ entry_id
+ )
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_entry.sql b/dist/res/database/tables/archive_entry.sql
new file mode 100644
index 000000000..846ad5079
--- /dev/null
+++ b/dist/res/database/tables/archive_entry.sql
@@ -0,0 +1,24 @@
+BEGIN;
+
+CREATE TABLE archive_entry (
+ archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE,
+ id INTEGER,
+ path TEXT,
+ [index] INTEGER,
+ name TEXT,
+ size INTEGER,
+ hash TEXT,
+ type_id TEXT,
+ CONSTRAINT primary_key UNIQUE (
+ archive_id,
+ id
+ )
+);
+
+CREATE INDEX entry_match ON archive_entry (
+ name,
+ path,
+ hash
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_entry_property.sql b/dist/res/database/tables/archive_entry_property.sql
new file mode 100644
index 000000000..23a3a2045
--- /dev/null
+++ b/dist/res/database/tables/archive_entry_property.sql
@@ -0,0 +1,16 @@
+BEGIN;
+
+CREATE TABLE archive_entry_property (
+ archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE,
+ entry_id INTEGER,
+ [key] TEXT,
+ value_type INTEGER,
+ value NONE,
+ UNIQUE (
+ archive_id,
+ entry_id,
+ [key]
+ )
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_file.sql b/dist/res/database/tables/archive_file.sql
new file mode 100644
index 000000000..1a3aef3bd
--- /dev/null
+++ b/dist/res/database/tables/archive_file.sql
@@ -0,0 +1,14 @@
+BEGIN;
+
+CREATE TABLE archive_file (
+ id INTEGER PRIMARY KEY,
+ path TEXT UNIQUE, -- Path to the archive file on disk (or directory), must be unique in the table
+ size INTEGER, -- Size of the file on disk (in bytes)
+ hash TEXT, -- Hash (xxHash 128-bit) of the file data on disk
+ format_id TEXT, -- Archive format id (eg. 'wad')
+ last_opened DATETIME, -- Time the archive was last opened in SLADE
+ last_modified DATETIME, -- Modified time of the file on disk
+ parent_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE -- For archives within archives, the id of this archive's parent
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_map.sql b/dist/res/database/tables/archive_map.sql
new file mode 100644
index 000000000..b88b0d95d
--- /dev/null
+++ b/dist/res/database/tables/archive_map.sql
@@ -0,0 +1,14 @@
+BEGIN;
+
+CREATE TABLE archive_map (
+ archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE,
+ header_entry_id INTEGER,
+ name TEXT,
+ format INTEGER,
+ UNIQUE (
+ archive_id,
+ header_entry_id
+ )
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_map_config.sql b/dist/res/database/tables/archive_map_config.sql
new file mode 100644
index 000000000..ba200f8fa
--- /dev/null
+++ b/dist/res/database/tables/archive_map_config.sql
@@ -0,0 +1,9 @@
+BEGIN;
+
+CREATE TABLE archive_map_config (
+ archive_id INTEGER REFERENCES archive_file (id) ON DELETE CASCADE,
+ game TEXT,
+ port TEXT
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_run_config.sql b/dist/res/database/tables/archive_run_config.sql
new file mode 100644
index 000000000..db52578fa
--- /dev/null
+++ b/dist/res/database/tables/archive_run_config.sql
@@ -0,0 +1,12 @@
+BEGIN;
+
+CREATE TABLE archive_run_config (
+ archive_id INTEGER PRIMARY KEY
+ REFERENCES archive_file (id) ON DELETE CASCADE,
+ executable_id TEXT,
+ run_config INTEGER,
+ run_extra TEXT,
+ iwad_path TEXT
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/archive_ui_config.sql b/dist/res/database/tables/archive_ui_config.sql
new file mode 100644
index 000000000..c7d1bf001
--- /dev/null
+++ b/dist/res/database/tables/archive_ui_config.sql
@@ -0,0 +1,18 @@
+BEGIN;
+
+CREATE TABLE archive_ui_config (
+ archive_id INTEGER PRIMARY KEY
+ REFERENCES archive_file (id) ON DELETE CASCADE,
+ elist_index_visible BOOLEAN,
+ elist_index_width INTEGER,
+ elist_name_width INTEGER,
+ elist_size_visible BOOLEAN,
+ elist_size_width INTEGER,
+ elist_type_visible BOOLEAN,
+ elist_type_width INTEGER,
+ elist_sort_column TEXT,
+ elist_sort_descending BOOLEAN,
+ splitter_position INTEGER
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/db_info.sql b/dist/res/database/tables/db_info.sql
new file mode 100644
index 000000000..5bec9d072
--- /dev/null
+++ b/dist/res/database/tables/db_info.sql
@@ -0,0 +1,7 @@
+BEGIN;
+
+CREATE TABLE db_info (
+ version INTEGER
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/session.sql b/dist/res/database/tables/session.sql
new file mode 100644
index 000000000..e382284ec
--- /dev/null
+++ b/dist/res/database/tables/session.sql
@@ -0,0 +1,13 @@
+BEGIN;
+
+CREATE TABLE session (
+ id INTEGER PRIMARY KEY,
+ opened_time DATETIME,
+ closed_time DATETIME,
+ version_major INTEGER,
+ version_minor INTEGER,
+ version_revision INTEGER,
+ version_beta INTEGER
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/ui_state.sql b/dist/res/database/tables/ui_state.sql
new file mode 100644
index 000000000..b5a470774
--- /dev/null
+++ b/dist/res/database/tables/ui_state.sql
@@ -0,0 +1,8 @@
+BEGIN;
+
+CREATE TABLE ui_state (
+ name TEXT PRIMARY KEY,
+ value NONE
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/window_info.sql b/dist/res/database/tables/window_info.sql
new file mode 100644
index 000000000..e812063d2
--- /dev/null
+++ b/dist/res/database/tables/window_info.sql
@@ -0,0 +1,12 @@
+BEGIN;
+
+CREATE TABLE window_info (
+ window_id TEXT PRIMARY KEY
+ NOT NULL,
+ left INTEGER,
+ top INTEGER,
+ width INTEGER,
+ height INTEGER
+);
+
+COMMIT;
diff --git a/dist/res/database/tables/window_layout.sql b/dist/res/database/tables/window_layout.sql
new file mode 100644
index 000000000..dd66b44a0
--- /dev/null
+++ b/dist/res/database/tables/window_layout.sql
@@ -0,0 +1,13 @@
+BEGIN;
+
+CREATE TABLE window_layout (
+ window_id TEXT,
+ component TEXT,
+ layout TEXT,
+ UNIQUE (
+ window_id,
+ component
+ )
+);
+
+COMMIT;
diff --git a/dist/res/database/views/archive_library_list.sql b/dist/res/database/views/archive_library_list.sql
new file mode 100644
index 000000000..7416ae3a1
--- /dev/null
+++ b/dist/res/database/views/archive_library_list.sql
@@ -0,0 +1,26 @@
+BEGIN;
+
+CREATE VIEW archive_library_list AS
+ SELECT archive_file.id,
+ archive_file.path,
+ archive_file.size,
+ archive_file.format_id,
+ archive_file.last_opened,
+ archive_file.last_modified,
+ archive_file.parent_id,
+ (
+ SELECT COUNT( * )
+ FROM archive_entry
+ WHERE archive_entry.archive_id = archive_file.id
+ )
+ AS entry_count,
+ (
+ SELECT COUNT( * )
+ FROM archive_map
+ WHERE archive_map.archive_id = archive_file.id
+ )
+ AS map_count
+ FROM archive_file
+ ORDER BY archive_file.path COLLATE NOCASE ASC;
+
+COMMIT;
diff --git a/dist/res/icons.cfg b/dist/res/icons.cfg
index e369ab1ab..a6adadcd4 100644
--- a/dist/res/icons.cfg
+++ b/dist/res/icons.cfg
@@ -41,6 +41,7 @@ general
icon_svg importfiles = "icons/general/importfiles.svg";
icon_svg importdir = "icons/general/importdir.svg";
icon_svg left = "icons/general/left.svg";
+ icon_svg library = "icons/general/library.svg";
icon_svg lightbulb = "icons/general/lightbulb.svg";
icon_svg linedraw = "icons/general/linedraw.svg";
icon_svg lines = "icons/general/lines.svg";
diff --git a/dist/res/icons/general/library.svg b/dist/res/icons/general/library.svg
new file mode 100644
index 000000000..cf011110e
--- /dev/null
+++ b/dist/res/icons/general/library.svg
@@ -0,0 +1,11 @@
+
diff --git a/msvc/ThirdPartyLib.vcxproj b/msvc/ThirdPartyLib.vcxproj
index 4ea6325f6..4e16244e1 100644
--- a/msvc/ThirdPartyLib.vcxproj
+++ b/msvc/ThirdPartyLib.vcxproj
@@ -110,6 +110,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -702,6 +715,14 @@
+
+
+
+
+
+
+
+
@@ -764,25 +785,25 @@
false
- $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include
+ $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include
..\build\thirdparty\$(Platform)\$(Configuration)\
..\build\lib\$(Platform)\$(Configuration)\
true
- $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include
+ $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include
..\build\thirdparty\$(Configuration)\
..\build\lib\$(Configuration)\
true
- $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include
+ $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include
..\build\thirdparty\$(Platform)\$(Configuration)\
..\build\lib\$(Platform)\$(Configuration)\
false
- $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include
+ $(ProjectDir)..\thirdparty\dumb;$(ProjectDir)..\src;$(ProjectDir)..\src\Application;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\software;$(ProjectDir)..\thirdparty\lunasvg\3rdparty\plutovg;$(ProjectDir)..\thirdparty\lunasvg\include;$(ProjectDir)..\thirdparty\fmt\include;$(ProjectDir)..\thirdparty\glad\include;$(ProjectDir)..\thirdparty\sqlitecpp\sqlite3;$(ProjectDir)..\thirdparty\sqlitecpp\include
..\build\thirdparty\$(Configuration)\
..\build\lib\$(Configuration)\
diff --git a/src/Application/App.cpp b/src/Application/App.cpp
index 39dc10355..52e68cc6f 100644
--- a/src/Application/App.cpp
+++ b/src/Application/App.cpp
@@ -36,6 +36,7 @@
#include "Archive/ArchiveManager.h"
#include "Archive/EntryType/EntryDataFormat.h"
#include "Archive/EntryType/EntryType.h"
+#include "Database/Database.h"
#include "Game/Game.h"
#include "Game/SpecialPreset.h"
#include "General/Clipboard.h"
@@ -43,13 +44,13 @@
#include "General/Console.h"
#include "General/Executables.h"
#include "General/KeyBind.h"
-#include "General/Misc.h"
#include "General/ResourceManager.h"
#include "General/SAction.h"
#include "General/UI.h"
#include "Graphics/Icons.h"
#include "Graphics/Palette/PaletteManager.h"
#include "Graphics/SImage/SIFormat.h"
+#include "Library/Library.h"
#include "MainEditor/MainEditor.h"
#include "MapEditor/NodeBuilders.h"
#include "OpenGL/GLTexture.h"
@@ -60,6 +61,7 @@
#include "TextEditor/TextStyle.h"
#include "UI/Dialogs/SetupWizard/SetupWizardDialog.h"
#include "UI/SBrush.h"
+#include "UI/State.h"
#include "UI/WxUtils.h"
#include "Utility/StringUtils.h"
#include "Utility/Tokenizer.h"
@@ -115,7 +117,6 @@ ResourceManager resource_manager;
CVAR(Int, temp_location, 0, CVar::Flag::Save)
CVAR(String, temp_location_custom, "", CVar::Flag::Save)
-CVAR(Bool, setup_wizard_run, false, CVar::Flag::Save)
// ----------------------------------------------------------------------------
@@ -214,7 +215,7 @@ bool initDirectories()
{
if (!wxMkdir(dir_user))
{
- wxMessageBox(wxString::Format("Unable to create user directory \"%s\"", dir_user), "Error", wxICON_ERROR);
+ global::error = fmt::format("Unable to create user directory \"{}\"", dir_user);
return false;
}
}
@@ -225,7 +226,7 @@ bool initDirectories()
{
if (!wxMkdir(dir_temp))
{
- wxMessageBox(wxString::Format("Unable to create temp directory \"%s\"", dir_temp), "Error", wxICON_ERROR);
+ global::error = fmt::format("Unable to create temp directory \"{}\"", dir_temp);
return false;
}
}
@@ -288,18 +289,9 @@ void readConfigFile()
tz.adv(); // Skip ending }
}
- // Read recent files list
+ // Read (pre-3.3.0) recent files list
if (tz.advIf("recent_files", 2))
- {
- while (!tz.checkOrEnd("}"))
- {
- auto path = wxString::FromUTF8(tz.current().text.c_str());
- archive_manager.addRecentFile(wxutil::strToView(path));
- tz.adv();
- }
-
- tz.adv(); // Skip ending }
- }
+ library::readPre330RecentFiles(tz);
// Read keybinds
if (tz.advIf("keys", 2))
@@ -331,10 +323,6 @@ void readConfigFile()
tz.adv(); // Skip ending }
}
- // Read window size/position info
- if (tz.advIf("window_info", 2))
- misc::readWindowInfo(tz);
-
// Next token
tz.adv();
}
@@ -422,6 +410,14 @@ ResourceManager& app::resources()
return resource_manager;
}
+// -----------------------------------------------------------------------------
+// Returns the program resource archive (ie. slade.pk3)
+// -----------------------------------------------------------------------------
+Archive* app::programResource()
+{
+ return archive_manager.programResourceArchive();
+}
+
// -----------------------------------------------------------------------------
// Returns the number of ms elapsed since the application was started
// -----------------------------------------------------------------------------
@@ -479,14 +475,17 @@ bool app::init(const vector& args, double ui_scale)
archive_manager.init();
if (!archive_manager.resArchiveOK())
{
- wxMessageBox(
- "Unable to find slade.pk3, make sure it exists in the same directory as the "
- "SLADE executable",
- "Error",
- wxICON_ERROR);
+ global::error = "Unable to find slade.pk3, make sure it exists in the same directory as the SLADE executable";
return false;
}
+ // Init database
+ if (!database::init())
+ return false;
+
+ // Init library
+ library::init();
+
// Init SActions
SAction::setBaseWxId(26000);
SAction::initActions();
@@ -575,11 +574,11 @@ bool app::init(const vector& args, double ui_scale)
log::info("SLADE Initialisation OK");
// Show Setup Wizard if needed
- if (!setup_wizard_run)
+ if (!ui::getStateBool("SetupWizardRun"))
{
SetupWizardDialog dlg(maineditor::windowWx());
dlg.ShowModal();
- setup_wizard_run = true;
+ ui::saveStateBool("SetupWizardRun", true);
maineditor::windowWx()->Update();
maineditor::windowWx()->Refresh();
}
@@ -615,10 +614,10 @@ void app::saveConfigFile()
return;
// Write cfg header
- file.Write("/*****************************************************\n");
- file.Write(" * SLADE Configuration File\n");
- file.Write(" * Don't edit this unless you know what you're doing\n");
- file.Write(" *****************************************************/\n\n");
+ file.Write("// ----------------------------------------------------\n");
+ file.Write(wxString::Format("// SLADE v%s Configuration File\n", version_num.toString()));
+ file.Write("// Don't edit this unless you know what you're doing\n");
+ file.Write("// ----------------------------------------------------\n\n");
// Write cvars
file.Write(CVar::writeAll(), wxConvUTF8);
@@ -633,11 +632,14 @@ void app::saveConfigFile()
}
file.Write("}\n");
- // Write recent files list (in reverse to keep proper order when reading back)
- file.Write("\nrecent_files\n{\n");
- for (int a = archive_manager.numRecentFiles() - 1; a >= 0; a--)
+ // Write recent files
+ // This isn't used in 3.3.0+, but we'll write them anyway for backwards-compatibility with previous versions
+ // (will be removed eventually, perhaps in 3.4.0)
+ auto recent_files = library::recentFiles();
+ file.Write("\n// Recent Files (for backwards compatibility with pre-3.3.0 SLADE)\nrecent_files\n{\n");
+ for (int i = recent_files.size() - 1; i >= 0; --i)
{
- auto path = archive_manager.recentFile(a);
+ auto path = recent_files[i];
std::replace(path.begin(), path.end(), '\\', '/');
file.Write(wxString::Format("\t\"%s\"\n", path), wxConvUTF8);
}
@@ -657,13 +659,10 @@ void app::saveConfigFile()
file.Write(executables::writePaths());
file.Write("}\n");
- // Write window info
- file.Write("\nwindow_info\n{\n");
- misc::writeWindowInfo(file);
- file.Write("}\n");
-
// Close configuration file
- file.Write("\n// End Configuration File\n\n");
+ file.Write("\n// ----------------------------------------------------\n");
+ file.Write("// End Configuration File\n");
+ file.Write("// ----------------------------------------------------\n");
}
// -----------------------------------------------------------------------------
@@ -727,6 +726,9 @@ void app::exit(bool save_config)
// Close DUMB
dumb_exit();
+ // Close program database
+ database::close();
+
// Exit wx Application
wxGetApp().Exit();
}
diff --git a/src/Application/App.h b/src/Application/App.h
index 1055c11d8..baff38b15 100644
--- a/src/Application/App.h
+++ b/src/Application/App.h
@@ -4,6 +4,7 @@
namespace slade
{
+class Archive;
class ArchiveManager;
class Console;
class PaletteManager;
@@ -20,6 +21,7 @@ namespace app
ArchiveManager& archiveManager();
Clipboard& clipboard();
ResourceManager& resources();
+ Archive* programResource();
bool init(const vector& args, double ui_scale = 1.);
void saveConfigFile();
diff --git a/src/Application/SLADEWxApp.cpp b/src/Application/SLADEWxApp.cpp
index dbd9aa0b4..597bc86b3 100644
--- a/src/Application/SLADEWxApp.cpp
+++ b/src/Application/SLADEWxApp.cpp
@@ -83,7 +83,6 @@ int win_version_minor = 0;
string current_action;
bool update_check_message_box = false;
-CVAR(String, dir_last, "", CVar::Flag::Save)
CVAR(Bool, update_check, true, CVar::Flag::Save)
CVAR(Bool, update_check_beta, false, CVar::Flag::Save)
@@ -485,7 +484,10 @@ bool SLADEWxApp::OnInit()
try
{
if (!app::init(args, ui_scale))
+ {
+ wxMessageBox(global::error, "SLADE Initialization Error", wxICON_ERROR);
return false;
+ }
}
catch (const std::exception& ex)
{
diff --git a/src/Archive/Archive.cpp b/src/Archive/Archive.cpp
index 0910a5ec4..0c195bfa4 100644
--- a/src/Archive/Archive.cpp
+++ b/src/Archive/Archive.cpp
@@ -220,7 +220,7 @@ string Archive::filename(bool full) const
if (parentArchive())
parent_archive = parentArchive()->filename(false) + "/";
- return parent_archive.append(strutil::Path::fileNameOf(parent->name(), false));
+ return parent_archive.append(strutil::Path::fileNameOf(parent->name()));
}
return full ? filename_ : string{ strutil::Path::fileNameOf(filename_) };
@@ -254,7 +254,7 @@ string Archive::formatId() const
// Reads an archive from disk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool Archive::open(string_view filename)
+bool Archive::open(string_view filename, bool detect_types)
{
// Update filename before opening
const auto backupname = filename_;
@@ -262,7 +262,7 @@ bool Archive::open(string_view filename)
// Open via format handler
const sf::Clock timer;
- if (format_handler_->open(*this, filename))
+ if (format_handler_->open(*this, filename, detect_types))
{
log::info(2, "Archive::open took {}ms", timer.getElapsedTime().asMilliseconds());
file_modified_ = fileutil::fileModifiedTime(filename);
@@ -281,14 +281,14 @@ bool Archive::open(string_view filename)
// Reads an archive from an ArchiveEntry
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool Archive::open(ArchiveEntry* entry)
+bool Archive::open(ArchiveEntry* entry, bool detect_types)
{
- return format_handler_->open(*this, entry);
+ return format_handler_->open(*this, entry, detect_types);
}
-bool Archive::open(const MemChunk& mc)
+bool Archive::open(const MemChunk& mc, bool detect_types)
{
- return format_handler_->open(*this, mc);
+ return format_handler_->open(*this, mc, detect_types);
}
bool Archive::write(MemChunk& mc)
@@ -930,7 +930,7 @@ MapDesc Archive::mapDesc(ArchiveEntry* maphead)
// Returns the MapDesc information about all maps in the Archive.
// To be implemented in Archive sub-classes.
// -----------------------------------------------------------------------------
-vector Archive::detectMaps()
+vector Archive::detectMaps() const
{
return format_handler_->detectMaps(*this);
}
@@ -938,7 +938,7 @@ vector Archive::detectMaps()
// -----------------------------------------------------------------------------
// Returns the namespace of the entry at [index] within [dir]
// -----------------------------------------------------------------------------
-string Archive::detectNamespace(unsigned index, ArchiveDir* dir)
+string Archive::detectNamespace(unsigned index, ArchiveDir* dir) const
{
return format_handler_->detectNamespace(*this, index, dir);
}
@@ -946,7 +946,7 @@ string Archive::detectNamespace(unsigned index, ArchiveDir* dir)
// -----------------------------------------------------------------------------
// Returns the namespace that [entry] is within
// -----------------------------------------------------------------------------
-string Archive::detectNamespace(ArchiveEntry* entry)
+string Archive::detectNamespace(ArchiveEntry* entry) const
{
return format_handler_->detectNamespace(*this, entry);
}
@@ -1039,14 +1039,16 @@ void Archive::blockModificationSignals(bool block)
// -----------------------------------------------------------------------------
// Detects the type of all entries in the archive
// -----------------------------------------------------------------------------
-void Archive::detectAllEntryTypes() const
+void Archive::detectAllEntryTypes(bool show_in_splash_window) const
{
auto entries = dir_root_->allEntries();
auto n_entries = entries.size();
- ui::setSplashProgressMessage("Detecting entry types");
+ if (show_in_splash_window)
+ ui::setSplashProgressMessage("Detecting entry types");
for (size_t i = 0; i < n_entries; i++)
{
- ui::setSplashProgress(i, n_entries);
+ if (show_in_splash_window)
+ ui::setSplashProgress(i, n_entries);
EntryType::detectEntryType(*entries[i]);
entries[i]->setState(EntryState::Unmodified);
}
diff --git a/src/Archive/Archive.h b/src/Archive/Archive.h
index 8ae901e85..81ada68d5 100644
--- a/src/Archive/Archive.h
+++ b/src/Archive/Archive.h
@@ -48,9 +48,11 @@ class Archive
bool isReadOnly() const { return read_only_; }
bool isWritable() const;
time_t fileModifiedTime() const { return file_modified_; }
+ int64_t libraryId() const { return library_id_; }
void setModified(bool modified);
void setFilename(string_view filename) { filename_ = filename; }
+ void setLibraryId(int64_t library_id) const { library_id_ = library_id; }
// Entry retrieval/info
bool checkEntry(const ArchiveEntry* entry) const;
@@ -71,9 +73,9 @@ class Archive
string formatId() const;
// Opening
- bool open(string_view filename); // Open from File
- bool open(ArchiveEntry* entry); // Open from ArchiveEntry
- bool open(const MemChunk& mc); // Open from MemChunk
+ bool open(string_view filename, bool detect_types); // Open from File
+ bool open(ArchiveEntry* entry, bool detect_types); // Open from ArchiveEntry
+ bool open(const MemChunk& mc, bool detect_types); // Open from MemChunk
// Writing/Saving
bool write(MemChunk& mc); // Write to MemChunk
@@ -122,9 +124,10 @@ class Archive
// Detection
MapDesc mapDesc(ArchiveEntry* maphead);
- vector detectMaps();
- string detectNamespace(ArchiveEntry* entry);
- string detectNamespace(unsigned index, ArchiveDir* dir = nullptr);
+ vector detectMaps() const;
+ string detectNamespace(ArchiveEntry* entry) const;
+ string detectNamespace(unsigned index, ArchiveDir* dir = nullptr) const;
+ void detectAllEntryTypes(bool show_in_splash_window = true) const;
// Search
ArchiveEntry* findFirst(ArchiveSearchOptions& options);
@@ -156,14 +159,12 @@ class Archive
bool read_only_ = false; // If true, the archive cannot be modified
time_t file_modified_ = 0;
- // Helpers
- void detectAllEntryTypes() const;
-
private:
bool modified_ = true;
shared_ptr dir_root_;
Signals signals_;
unique_ptr format_handler_;
+ mutable int64_t library_id_ = -1;
};
// Simple class that will block and unblock modification signals for an archive via RAII
@@ -178,5 +179,4 @@ class ArchiveModSignalBlocker
private:
Archive* archive_;
};
-
} // namespace slade
diff --git a/src/Archive/ArchiveDir.cpp b/src/Archive/ArchiveDir.cpp
index a6c2bcd97..b92bbbab8 100644
--- a/src/Archive/ArchiveDir.cpp
+++ b/src/Archive/ArchiveDir.cpp
@@ -134,7 +134,7 @@ void ArchiveDir::setArchive(Archive* archive)
// Returns the index of [entry] within this directory, or -1 if the entry
// doesn't exist
// -----------------------------------------------------------------------------
-int ArchiveDir::entryIndex(ArchiveEntry* entry, size_t startfrom) const
+int ArchiveDir::entryIndex(const ArchiveEntry* entry, size_t startfrom) const
{
// Check entry was given
if (!entry)
diff --git a/src/Archive/ArchiveDir.h b/src/Archive/ArchiveDir.h
index 678294a22..85126f003 100644
--- a/src/Archive/ArchiveDir.h
+++ b/src/Archive/ArchiveDir.h
@@ -34,7 +34,7 @@ class ArchiveDir
shared_ptr sharedEntry(string_view name, bool cut_ext = false) const;
shared_ptr sharedEntry(const ArchiveEntry* entry) const;
unsigned numEntries(bool inc_subdirs = false) const;
- int entryIndex(ArchiveEntry* entry, size_t startfrom = 0) const;
+ int entryIndex(const ArchiveEntry* entry, size_t startfrom = 0) const;
vector> allEntries() const;
vector> allDirectories() const;
diff --git a/src/Archive/ArchiveEntry.cpp b/src/Archive/ArchiveEntry.cpp
index c03124dc6..42af26b2e 100644
--- a/src/Archive/ArchiveEntry.cpp
+++ b/src/Archive/ArchiveEntry.cpp
@@ -96,6 +96,7 @@ ArchiveEntry::ArchiveEntry(const ArchiveEntry& copy) :
data_{ copy.data_ },
type_{ copy.type_ },
ex_props_{ copy.ex_props_ },
+ data_hash_{ copy.data_hash_ },
encrypted_{ copy.encrypted_ },
reliability_{ copy.reliability_ }
{
@@ -166,7 +167,7 @@ string ArchiveEntry::path(bool include_name) const
// Returns the 'next' entry from this (ie. index + 1) in its parent ArchiveDir,
// or nullptr if it is the last entry or has no parent dir
// -----------------------------------------------------------------------------
-ArchiveEntry* ArchiveEntry::nextEntry()
+ArchiveEntry* ArchiveEntry::nextEntry() const
{
return parent_ ? parent_->entryAt(parent_->entryIndex(this) + 1) : nullptr;
}
@@ -175,7 +176,7 @@ ArchiveEntry* ArchiveEntry::nextEntry()
// Returns the 'previous' entry from this (ie. index - 1) in its parent
// ArchiveDir, or nullptr if it is the first entry or has no parent dir
// -----------------------------------------------------------------------------
-ArchiveEntry* ArchiveEntry::prevEntry()
+ArchiveEntry* ArchiveEntry::prevEntry() const
{
return parent_ ? parent_->entryAt(parent_->entryIndex(this) - 1) : nullptr;
}
@@ -193,11 +194,22 @@ shared_ptr ArchiveEntry::getShared() const
// Returns the entry's index within its parent ArchiveDir, or -1 if it isn't
// in a dir
// -----------------------------------------------------------------------------
-int ArchiveEntry::index()
+int ArchiveEntry::index() const
{
return parent_ ? parent_->entryIndex(this) : -1;
}
+// -----------------------------------------------------------------------------
+// Returns the hash (XXH128) of the entry's data, calculating it if needed
+// -----------------------------------------------------------------------------
+const string& ArchiveEntry::hash() const
+{
+ if (data_hash_.empty())
+ data_hash_ = data_.hash();
+
+ return data_hash_;
+}
+
// -----------------------------------------------------------------------------
// Sets the entry's name (but doesn't change state to modified)
// -----------------------------------------------------------------------------
@@ -321,7 +333,14 @@ bool ArchiveEntry::resize(uint32_t new_size, bool preserve_data)
// Update attributes
setState(EntryState::Modified);
- return data_.reSize(new_size, preserve_data);
+ // Resize the data
+ if (data_.reSize(new_size, preserve_data))
+ {
+ data_hash_ = data_.hash();
+ return true;
+ }
+
+ return false;
}
// -----------------------------------------------------------------------------
@@ -338,6 +357,7 @@ bool ArchiveEntry::clearData()
// Delete the data
data_.clear();
+ data_hash_.clear();
return true;
}
@@ -374,6 +394,7 @@ bool ArchiveEntry::importMem(const void* data, uint32_t size)
data_.importMem(static_cast(data), size);
// Update attributes
+ data_hash_ = data_.hash();
setType(EntryType::unknownType());
setState(EntryState::Modified);
@@ -489,6 +510,7 @@ bool ArchiveEntry::importFileStream(wxFile& file, uint32_t len)
if (data_.importFileStreamWx(file, len))
{
// Update attributes
+ data_hash_ = data_.hash();
setType(EntryType::unknownType());
setState(EntryState::Modified);
@@ -561,6 +583,7 @@ bool ArchiveEntry::write(const void* data, uint32_t size)
if (data_.write(data, size))
{
// Update attributes
+ data_hash_.clear();
setState(EntryState::Modified);
return true;
@@ -657,17 +680,17 @@ bool ArchiveEntry::isInNamespace(string_view ns)
// Returns the entry at [path] relative to [base], or failing that, the entry
// at absolute [path] in the archive (if [allow_absolute_path] is true)
// -----------------------------------------------------------------------------
-ArchiveEntry* ArchiveEntry::relativeEntry(string_view at_path, bool allow_absolute_path) const
+ArchiveEntry* ArchiveEntry::relativeEntry(string_view path, bool allow_absolute_path) const
{
if (!parent_)
return nullptr;
// Try relative to this entry
- auto include = parent_->archive()->entryAtPath(path().append(at_path));
+ auto include = parent_->archive()->entryAtPath(this->path().append(path));
// Try absolute path
if (!include && allow_absolute_path)
- include = parent_->archive()->entryAtPath(at_path);
+ include = parent_->archive()->entryAtPath(path);
return include;
}
diff --git a/src/Archive/ArchiveEntry.h b/src/Archive/ArchiveEntry.h
index d0206954a..40f8a2f2c 100644
--- a/src/Archive/ArchiveEntry.h
+++ b/src/Archive/ArchiveEntry.h
@@ -49,10 +49,12 @@ class ArchiveEntry
EntryState state() const { return state_; }
bool isLocked() const { return locked_; }
EntryEncryption encryption() const { return encrypted_; }
- ArchiveEntry* nextEntry();
- ArchiveEntry* prevEntry();
+ ArchiveEntry* nextEntry() const;
+ ArchiveEntry* prevEntry() const;
shared_ptr getShared() const;
- int index();
+ int index() const;
+ const string& hash() const;
+ int64_t libraryId() const { return library_id_; }
// Modifiers (won't change entry state, except setState of course :P)
void setName(string_view name);
@@ -63,6 +65,7 @@ class ArchiveEntry
}
void setState(EntryState state, bool silent = false);
void setEncryption(EntryEncryption enc) { encrypted_ = enc; }
+ void setLibraryId(int64_t id) const { library_id_ = id; }
void lock();
void unlock();
void lockState() { state_locked_ = true; }
@@ -111,12 +114,13 @@ class ArchiveEntry
private:
// Entry Info
- string name_;
- string upper_name_;
- MemChunk data_;
- EntryType* type_ = nullptr;
- ArchiveDir* parent_ = nullptr;
- PropertyList ex_props_;
+ string name_;
+ string upper_name_;
+ MemChunk data_;
+ EntryType* type_ = nullptr;
+ ArchiveDir* parent_ = nullptr;
+ PropertyList ex_props_;
+ mutable string data_hash_;
// Entry status
EntryState state_ = EntryState::New;
@@ -125,8 +129,9 @@ class ArchiveEntry
EntryEncryption encrypted_ = EntryEncryption::None; // Is there some encrypting on the archive?
// Misc stuff
- int reliability_ = 0; // The reliability of the entry's identification
- size_t index_guess_ = 0; // for speed
+ int reliability_ = 0; // The reliability of the entry's identification
+ mutable size_t index_guess_ = 0; // for speed
+ mutable int64_t library_id_ = -1; // The id of this entry in the library
};
template T ArchiveEntry::exProp(const string& key)
diff --git a/src/Archive/ArchiveFormat.cpp b/src/Archive/ArchiveFormat.cpp
index d0c696a76..59a3ed283 100644
--- a/src/Archive/ArchiveFormat.cpp
+++ b/src/Archive/ArchiveFormat.cpp
@@ -34,6 +34,7 @@
#include "ArchiveFormat.h"
#include "Utility/Named.h"
#include "Utility/Parser.h"
+#include "Utility/PropertyList.h"
using namespace slade;
using namespace archive;
@@ -227,3 +228,16 @@ ArchiveFormat archive::formatFromId(string_view format_id_string)
return ArchiveFormat::Unknown;
}
+
+// -----------------------------------------------------------------------------
+// Returns true if [file_ext] is a known archive file extension
+// -----------------------------------------------------------------------------
+bool archive::isKnownExtension(string_view file_ext)
+{
+ for (auto& [format, info] : format_info)
+ for (const auto& ext : info.extensions)
+ if (strutil::equalCI(ext.first, file_ext))
+ return true;
+
+ return false;
+}
diff --git a/src/Archive/ArchiveFormat.h b/src/Archive/ArchiveFormat.h
index 821243083..31372b802 100644
--- a/src/Archive/ArchiveFormat.h
+++ b/src/Archive/ArchiveFormat.h
@@ -59,5 +59,6 @@ namespace archive
const ArchiveFormatInfo& formatInfoFromId(string_view id);
string formatId(ArchiveFormat format);
ArchiveFormat formatFromId(string_view format_id_string);
+ bool isKnownExtension(string_view file_ext);
} // namespace archive
} // namespace slade
diff --git a/src/Archive/ArchiveFormatHandler.cpp b/src/Archive/ArchiveFormatHandler.cpp
index a1c979bb2..931851f02 100644
--- a/src/Archive/ArchiveFormatHandler.cpp
+++ b/src/Archive/ArchiveFormatHandler.cpp
@@ -66,7 +66,7 @@ vector> all_handlers;
class EntryRenameUS : public UndoStep
{
public:
- EntryRenameUS(ArchiveEntry* entry, string_view new_name) :
+ EntryRenameUS(const ArchiveEntry* entry, string_view new_name) :
archive_{ entry->parent() },
entry_path_{ entry->path() },
entry_index_{ entry->index() },
@@ -183,7 +183,7 @@ class EntrySwapUS : public UndoStep
class EntryCreateDeleteUS : public UndoStep
{
public:
- EntryCreateDeleteUS(bool created, ArchiveEntry* entry) :
+ EntryCreateDeleteUS(bool created, const ArchiveEntry* entry) :
created_{ created },
archive_{ entry->parent() },
entry_copy_{ new ArchiveEntry(*entry) },
@@ -326,7 +326,7 @@ ArchiveFormatHandler::ArchiveFormatHandler(ArchiveFormat format, bool treeless)
// Reads an archive from disk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ArchiveFormatHandler::open(Archive& archive, string_view filename)
+bool ArchiveFormatHandler::open(Archive& archive, string_view filename, bool detect_types)
{
// Read the file into a MemChunk
MemChunk mc;
@@ -337,18 +337,18 @@ bool ArchiveFormatHandler::open(Archive& archive, string_view filename)
}
// Load from MemChunk
- return open(archive, mc);
+ return open(archive, mc, detect_types);
}
// -----------------------------------------------------------------------------
// Reads an archive from an ArchiveEntry
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry)
+bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry, bool detect_types)
{
// Load from entry's data
auto sp_entry = entry->getShared();
- if (sp_entry && open(archive, sp_entry->data()))
+ if (sp_entry && open(archive, sp_entry->data(), detect_types))
{
// Update variables and return success
archive.parent_ = sp_entry;
@@ -362,7 +362,7 @@ bool ArchiveFormatHandler::open(Archive& archive, ArchiveEntry* entry)
// Reads an archive from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ArchiveFormatHandler::open(Archive& archive, const MemChunk& mc)
+bool ArchiveFormatHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Invalid
return false;
@@ -808,7 +808,7 @@ bool ArchiveFormatHandler::renameEntry(Archive& archive, ArchiveEntry* entry, st
// Returns the MapDesc information about the map beginning at [maphead].
// To be implemented in Archive sub-classes.
// -----------------------------------------------------------------------------
-MapDesc ArchiveFormatHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
+MapDesc ArchiveFormatHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead)
{
return {};
}
@@ -817,7 +817,7 @@ MapDesc ArchiveFormatHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
// Returns the MapDesc information about all maps in the Archive.
// To be implemented in Archive sub-classes.
// -----------------------------------------------------------------------------
-vector ArchiveFormatHandler::detectMaps(Archive& archive)
+vector ArchiveFormatHandler::detectMaps(const Archive& archive)
{
return {};
}
@@ -825,7 +825,7 @@ vector ArchiveFormatHandler::detectMaps(Archive& archive)
// -----------------------------------------------------------------------------
// Returns the namespace of the entry at [index] within [dir]
// -----------------------------------------------------------------------------
-string ArchiveFormatHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir)
+string ArchiveFormatHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir)
{
if (dir && index < dir->numEntries())
return detectNamespace(archive, dir->entryAt(index));
@@ -836,7 +836,7 @@ string ArchiveFormatHandler::detectNamespace(Archive& archive, unsigned index, A
// -----------------------------------------------------------------------------
// Returns the namespace that [entry] is within
// -----------------------------------------------------------------------------
-string ArchiveFormatHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
+string ArchiveFormatHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry)
{
// Check entry
if (!archive.checkEntry(entry))
@@ -862,7 +862,7 @@ string ArchiveFormatHandler::detectNamespace(Archive& archive, ArchiveEntry* ent
// Returns the first entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* ArchiveFormatHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* ArchiveFormatHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = options.dir;
@@ -931,7 +931,7 @@ ArchiveEntry* ArchiveFormatHandler::findFirst(Archive& archive, ArchiveSearchOpt
// Returns the last entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* ArchiveFormatHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* ArchiveFormatHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = options.dir;
@@ -999,7 +999,7 @@ ArchiveEntry* ArchiveFormatHandler::findLast(Archive& archive, ArchiveSearchOpti
// -----------------------------------------------------------------------------
// Returns a list of entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector ArchiveFormatHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector ArchiveFormatHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = options.dir;
@@ -1065,16 +1065,6 @@ vector ArchiveFormatHandler::findAll(Archive& archive, ArchiveSea
return ret;
}
-// -----------------------------------------------------------------------------
-// Detects type for all entries in [archive]
-// (just here because it's a protected function in Archive, so subclasses can't
-// access it directly)
-// -----------------------------------------------------------------------------
-void ArchiveFormatHandler::detectAllEntryTypes(const Archive& archive)
-{
- archive.detectAllEntryTypes();
-}
-
// -----------------------------------------------------------------------------
// Returns true if data in [mc] is a file
// -----------------------------------------------------------------------------
diff --git a/src/Archive/ArchiveFormatHandler.h b/src/Archive/ArchiveFormatHandler.h
index c8b4f6c79..579704056 100644
--- a/src/Archive/ArchiveFormatHandler.h
+++ b/src/Archive/ArchiveFormatHandler.h
@@ -22,9 +22,9 @@ class ArchiveFormatHandler
ArchiveFormat format() const { return format_; }
// Opening
- virtual bool open(Archive& archive, string_view filename); // Open from File
- virtual bool open(Archive& archive, ArchiveEntry* entry); // Open from ArchiveEntry
- virtual bool open(Archive& archive, const MemChunk& mc); // Open from MemChunk
+ virtual bool open(Archive& archive, string_view filename, bool detect_types); // Open from File
+ virtual bool open(Archive& archive, ArchiveEntry* entry, bool detect_types); // Open from ArchiveEntry
+ virtual bool open(Archive& archive, const MemChunk& mc, bool detect_types); // Open from MemChunk
// Writing/Saving
virtual bool write(Archive& archive, MemChunk& mc); // Write to MemChunk
@@ -68,15 +68,15 @@ class ArchiveFormatHandler
virtual bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false);
// Detection
- virtual MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead);
- virtual vector detectMaps(Archive& archive);
- virtual string detectNamespace(Archive& archive, ArchiveEntry* entry);
- virtual string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr);
+ virtual MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead);
+ virtual vector detectMaps(const Archive& archive);
+ virtual string detectNamespace(const Archive& archive, ArchiveEntry* entry);
+ virtual string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr);
// Search
- virtual ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options);
- virtual ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options);
- virtual vector findAll(Archive& archive, ArchiveSearchOptions& options);
+ virtual ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options);
+ virtual ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options);
+ virtual vector findAll(const Archive& archive, ArchiveSearchOptions& options);
// Format detection
virtual bool isThisFormat(const MemChunk& mc);
@@ -85,9 +85,6 @@ class ArchiveFormatHandler
protected:
ArchiveFormat format_;
bool treeless_ = false;
-
- // Temp shortcuts
- void detectAllEntryTypes(const Archive& archive);
};
namespace archive
diff --git a/src/Archive/ArchiveManager.cpp b/src/Archive/ArchiveManager.cpp
index edfc60611..4ab4356f1 100644
--- a/src/Archive/ArchiveManager.cpp
+++ b/src/Archive/ArchiveManager.cpp
@@ -41,6 +41,8 @@
#include "General/Console.h"
#include "General/ResourceManager.h"
#include "General/UI.h"
+#include "Library/Library.h"
+#include "Utility/DateTime.h"
#include "Utility/FileUtils.h"
#include "Utility/StringUtils.h"
@@ -57,6 +59,67 @@ CVAR(Int, max_recent_files, 25, CVar::Flag::Save)
CVAR(Bool, auto_open_wads_root, false, CVar::Flag::Save)
+// -----------------------------------------------------------------------------
+//
+// Functions
+//
+// -----------------------------------------------------------------------------
+namespace
+{
+// -----------------------------------------------------------------------------
+// Updates/Adds [archive] in/to the library and updates last opened time if
+// requested.
+// Returns true if the archive was added to the library (and thus entry types
+// were already detected)
+// -----------------------------------------------------------------------------
+bool updateArchiveInLibrary(const Archive& archive, bool update_last_opened)
+{
+ ui::setSplashProgressMessage("Updating Library");
+ ui::setSplashProgress(1.0f);
+
+ // Read info from library into archive
+ auto lib_id = library::readArchiveInfo(archive);
+
+ // If it wasn't in the library, add it
+ auto added = false;
+ if (lib_id < 0)
+ {
+ // Need to detect all entry types before adding
+ archive.detectAllEntryTypes(false);
+
+ // Add to library
+ ui::setSplashProgressMessage("Updating Library");
+ lib_id = library::writeArchiveInfo(archive);
+
+ added = true;
+ }
+
+ // Update last opened time if needed
+ if (update_last_opened)
+ library::setArchiveLastOpenedTime(lib_id, datetime::now());
+
+ ui::setSplashProgressMessage("");
+
+ return added;
+}
+
+// -----------------------------------------------------------------------------
+// Writes all bookmarked entries for [archive] to the library
+// -----------------------------------------------------------------------------
+void writeArchiveBookmarksToLibrary(const Archive* archive, const vector>& bookmarks)
+{
+ // Remove existing bookmarks for [archive] in library
+ library::removeArchiveBookmarks(archive->libraryId());
+
+ // Add any bookmarks that are in [archive] to the library
+ for (auto& bookmark : bookmarks)
+ if (auto entry = bookmark.lock().get())
+ if (entry->parent() == archive)
+ library::addBookmark(archive->libraryId(), entry->libraryId());
+}
+} // namespace
+
+
// -----------------------------------------------------------------------------
//
// ArchiveManager Class Functions
@@ -118,7 +181,8 @@ bool ArchiveManager::init()
program_resource_archive_ = std::make_unique(ArchiveFormat::Zip);
#ifdef __WXOSX__
- auto resdir = app::path("../Resources", app::Dir::Executable); // Use Resources dir within bundle on mac
+ // Use Resources dir within bundle on mac
+ auto resdir = app::path("../Resources", app::Dir::Executable);
#else
auto resdir = app::path("res", app::Dir::Executable);
#endif
@@ -146,7 +210,7 @@ bool ArchiveManager::init()
dir_slade_pk3 = "slade.pk3";
// Open slade.pk3
- if (!program_resource_archive_->open(dir_slade_pk3))
+ if (!program_resource_archive_->open(dir_slade_pk3, true))
{
log::error("Unable to find slade.pk3!");
res_archive_open_ = false;
@@ -173,7 +237,7 @@ bool ArchiveManager::initArchiveFormats() const
// -----------------------------------------------------------------------------
bool ArchiveManager::initBaseResource()
{
- return openBaseResource((int)base_resource);
+ return openBaseResource(base_resource);
}
// -----------------------------------------------------------------------------
@@ -193,12 +257,27 @@ bool ArchiveManager::addArchive(shared_ptr archive)
// Emit archive changed/saved signal when received from the archive
archive->signals().modified.connect([this](Archive& archive, bool modified)
{ signals_.archive_modified(archiveIndex(&archive), modified); });
- archive->signals().saved.connect([this](Archive& archive) { signals_.archive_saved(archiveIndex(&archive)); });
+ archive->signals().saved.connect(
+ [this](Archive& archive)
+ {
+ // Update in library
+ if (archive.isOnDisk())
+ {
+ archive.setLibraryId(library::writeArchiveInfo(archive));
+
+ // (Re)Write bookmarks since entry ids have likely changed
+ // and/or been added
+ writeArchiveBookmarksToLibrary(&archive, bookmarks_);
+ }
+
+ signals_.archive_saved(archiveIndex(&archive));
+ });
// Announce the addition
signals_.archive_added(open_archives_.size() - 1);
// Add to resource manager
+ ui::setSplashProgressMessage("Loading Resources");
app::resources().addArchive(archive.get());
// ZDoom also loads any WADs found in the root of a PK3 or directory
@@ -258,10 +337,10 @@ shared_ptr ArchiveManager::getArchive(string_view filename)
// ----------------------------------------------------------------------------
shared_ptr ArchiveManager::getArchive(const ArchiveEntry* parent)
{
- for (unsigned a = 0; a < open_archives_.size(); ++a)
+ for (auto& open_archive : open_archives_)
{
- if (open_archives_[a].archive->parentEntry() == parent)
- return open_archives_[a].archive;
+ if (open_archive.archive->parentEntry() == parent)
+ return open_archive.archive;
}
return nullptr;
@@ -304,32 +383,36 @@ shared_ptr ArchiveManager::openArchive(string_view filename, bool manag
// Create archive
new_archive = std::make_shared(format_id);
- // If it opened successfully, add it to the list if needed & return it,
- // Otherwise, delete it and return nullptr
- if (new_archive->open(filename))
+ // Attempt to open archive
+ if (!new_archive->open(filename, false))
{
- if (manage)
- {
- // Add the archive
- auto index = open_archives_.size();
- addArchive(new_archive);
+ log::error(global::error);
+ return nullptr;
+ }
- // Announce open
- if (!silent)
- signals_.archive_opened(index);
+ // Opened ok, add to manager if requested
+ if (manage)
+ {
+ // Add/update in library
+ auto added = updateArchiveInLibrary(*new_archive, true);
- // Add to recent files
- addRecentFile(filename);
- }
+ // Detect entry types if needed (use hints from library)
+ if (!added)
+ new_archive->detectAllEntryTypes();
- // Return the opened archive
- return new_archive;
- }
- else
- {
- log::error(global::error);
- return nullptr;
+ // Restore bookmarks
+ addBookmarksFromLibrary(*new_archive);
+
+ // Add the archive
+ auto index = open_archives_.size();
+ addArchive(new_archive);
+
+ // Announce open
+ if (!silent)
+ signals_.archive_opened(index);
}
+
+ return new_archive;
}
// -----------------------------------------------------------------------------
@@ -366,37 +449,42 @@ shared_ptr ArchiveManager::openArchive(ArchiveEntry* entry, bool manage
// Create archive
auto new_archive = std::make_shared(format_id);
- // If it opened successfully, add it to the list & return it,
- // Otherwise, delete it and return nullptr
- if (new_archive->open(entry))
+ // Attempt to open archive
+ if (!new_archive->open(entry, false))
{
- if (manage)
- {
- // Add to parent's child list if parent is open in the manager (it should be)
- int index_parent = -1;
- if (entry->parent())
- index_parent = archiveIndex(entry->parent());
- if (index_parent >= 0)
- open_archives_[index_parent].open_children.emplace_back(new_archive);
+ log::error(global::error);
+ return nullptr;
+ }
- // Add the new archive
- auto index = open_archives_.size();
- addArchive(new_archive);
+ // Opened ok, add to manager if requested
+ if (manage)
+ {
+ // Add/update in library
+ auto added = updateArchiveInLibrary(*new_archive, true);
- // Announce open
- if (!silent)
- signals_.archive_opened(index);
+ // Detect entry types if needed (use hints from library)
+ if (!added)
+ new_archive->detectAllEntryTypes();
- entry->lock();
- }
+ // Add to parent's child list if parent is open in the manager (it should be)
+ int index_parent = -1;
+ if (entry->parent())
+ index_parent = archiveIndex(entry->parent());
+ if (index_parent >= 0)
+ open_archives_[index_parent].open_children.emplace_back(new_archive);
- return new_archive;
- }
- else
- {
- log::error(global::error);
- return nullptr;
+ // Add the archive
+ auto index = open_archives_.size();
+ addArchive(new_archive);
+
+ // Announce open
+ if (!silent)
+ signals_.archive_opened(index);
+
+ entry->lock();
}
+
+ return new_archive;
}
// -----------------------------------------------------------------------------
@@ -421,32 +509,36 @@ shared_ptr ArchiveManager::openDirArchive(string_view dir, bool manage,
new_archive = std::make_shared(ArchiveFormat::Dir);
- // If it opened successfully, add it to the list if needed & return it,
- // Otherwise, delete it and return nullptr
- if (new_archive->open(dir))
+ // Attempt to open archive
+ if (!new_archive->open(dir, false))
{
- if (manage)
- {
- // Add the archive
- auto index = open_archives_.size();
- addArchive(new_archive);
+ log::error(global::error);
+ return nullptr;
+ }
- // Announce open
- if (!silent)
- signals_.archive_opened(index);
+ // Opened ok, add to manager if requested
+ if (manage)
+ {
+ // Add/update in library
+ auto added = updateArchiveInLibrary(*new_archive, true);
- // Add to recent files
- addRecentFile(dir);
- }
+ // Detect entry types if needed (use hints from library)
+ if (!added)
+ new_archive->detectAllEntryTypes();
- // Return the opened archive
- return new_archive;
- }
- else
- {
- log::error(global::error);
- return nullptr;
+ // Restore bookmarks
+ addBookmarksFromLibrary(*new_archive);
+
+ // Add the archive
+ auto index = open_archives_.size();
+ addArchive(new_archive);
+
+ // Announce open
+ if (!silent)
+ signals_.archive_opened(index);
}
+
+ return new_archive;
}
// -----------------------------------------------------------------------------
@@ -494,13 +586,14 @@ bool ArchiveManager::closeArchive(int index)
signals_.archive_closing(index);
// Delete any bookmarked entries contained in the archive
- deleteBookmarksInArchive(open_archives_[index].archive.get());
+ deleteBookmarksInArchive(open_archives_[index].archive.get(), false);
// Remove from resource manager
app::resources().removeArchive(open_archives_[index].archive.get());
// Close any open child archives
- // Clear out the open_children vector first, lest the children try to remove themselves from it
+ // Clear out the open_children vector first, lest the children try to remove
+ // themselves from it
auto open_children = open_archives_[index].open_children;
open_archives_[index].open_children.clear();
for (auto& archive : open_children)
@@ -533,6 +626,14 @@ bool ArchiveManager::closeArchive(int index)
parent->unlock();
}
+ // Update library
+ if (open_archives_[index].archive->isOnDisk())
+ {
+ const auto& archive = *open_archives_[index].archive;
+ library::writeArchiveEntryInfo(archive);
+ library::writeArchiveMapInfo(archive);
+ }
+
// Close the archive
open_archives_[index].archive->close();
@@ -789,6 +890,14 @@ string ArchiveManager::getBaseResourcePath(unsigned index)
return base_resource_paths_[index];
}
+// -----------------------------------------------------------------------------
+// Returns the currently selected base resource path
+// -----------------------------------------------------------------------------
+string ArchiveManager::currentBaseResourcePath()
+{
+ return getBaseResourcePath(base_resource);
+}
+
// -----------------------------------------------------------------------------
// Opens the base resource archive [index]
// -----------------------------------------------------------------------------
@@ -824,10 +933,11 @@ bool ArchiveManager::openBaseResource(int index)
// Attempt to open the file
ui::showSplash(fmt::format("Opening {}...", filename), true);
- if (base_resource_archive_->open(filename))
+ if (base_resource_archive_->open(filename, true))
{
base_resource = index;
ui::hideSplash();
+ updateArchiveInLibrary(*base_resource_archive_, false);
app::resources().addArchive(base_resource_archive_.get());
signals_.base_res_current_changed(index);
return true;
@@ -928,94 +1038,6 @@ vector ArchiveManager::findAllResourceEntries(ArchiveSearchOption
return ret;
}
-// -----------------------------------------------------------------------------
-// Returns the recent file path at [index]
-// -----------------------------------------------------------------------------
-string ArchiveManager::recentFile(unsigned index)
-{
- // Check index
- if (index >= recent_files_.size())
- return "";
-
- return recent_files_[index];
-}
-
-// -----------------------------------------------------------------------------
-// Adds a recent file to the list, if it doesn't exist already
-// -----------------------------------------------------------------------------
-void ArchiveManager::addRecentFile(string_view path)
-{
- // Check the path is valid
- if (!(fileutil::fileExists(path) || fileutil::dirExists(path)))
- return;
-
- // Replace \ with /
- auto file_path = string{ path };
- std::replace(file_path.begin(), file_path.end(), '\\', '/');
-
- // Check if the file is already in the list
- for (unsigned a = 0; a < recent_files_.size(); a++)
- {
- if (recent_files_[a] == file_path)
- {
- // Move this file to the top of the list
- recent_files_.erase(recent_files_.begin() + a);
- recent_files_.insert(recent_files_.begin(), file_path);
-
- // Announce
- signals_.recent_files_changed();
-
- return;
- }
- }
-
- // Add the file to the top of the list
- recent_files_.insert(recent_files_.begin(), file_path);
-
- // Keep it trimmed
- while (recent_files_.size() > static_cast(max_recent_files))
- recent_files_.pop_back();
-
- // Announce
- signals_.recent_files_changed();
-}
-
-// -----------------------------------------------------------------------------
-// Adds a list of recent file paths to the recent file list
-// -----------------------------------------------------------------------------
-void ArchiveManager::addRecentFiles(const vector& paths)
-{
- // Mute annoucements
- signals_.recent_files_changed.block();
-
- // Clear existing list
- recent_files_.clear();
-
- // Add the files
- for (const auto& path : paths)
- addRecentFile(path);
-
- // Announce
- signals_.recent_files_changed.unblock();
- signals_.recent_files_changed();
-}
-
-// -----------------------------------------------------------------------------
-// Removes the recent file matching [path]
-// -----------------------------------------------------------------------------
-void ArchiveManager::removeRecentFile(string_view path)
-{
- for (unsigned a = 0; a < recent_files_.size(); a++)
- {
- if (recent_files_[a] == path)
- {
- recent_files_.erase(recent_files_.begin() + a);
- signals_.recent_files_changed();
- return;
- }
- }
-}
-
// -----------------------------------------------------------------------------
// Adds [entry] to the bookmark list
// -----------------------------------------------------------------------------
@@ -1033,6 +1055,7 @@ void ArchiveManager::addBookmark(const shared_ptr& entry)
// Add bookmark
bookmarks_.push_back(entry);
+ library::addBookmark(entry->parent()->libraryId(), entry->libraryId());
// Announce
signals_.bookmark_added(entry.get());
@@ -1049,10 +1072,10 @@ bool ArchiveManager::deleteBookmark(ArchiveEntry* entry)
if (bookmarks_[a].lock().get() == entry)
{
// Remove it
- bookmarks_.erase(bookmarks_.begin() + a);
+ removeBookmark(a);
// Announce
- signals_.bookmarks_removed(vector(1, entry));
+ signals_.bookmarks_removed({ 1, entry });
return true;
}
@@ -1073,10 +1096,10 @@ bool ArchiveManager::deleteBookmark(unsigned index)
// Remove bookmark
auto* entry = bookmarks_[index].lock().get();
- bookmarks_.erase(bookmarks_.begin() + index);
+ removeBookmark(index);
// Announce
- signals_.bookmarks_removed(vector(1, entry));
+ signals_.bookmarks_removed({ 1, entry });
return true;
}
@@ -1084,7 +1107,7 @@ bool ArchiveManager::deleteBookmark(unsigned index)
// -----------------------------------------------------------------------------
// Removes any bookmarked entries in [archive] from the list
// -----------------------------------------------------------------------------
-bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive)
+bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive, bool remove_from_library)
{
// Go through bookmarks
bool removed = false;
@@ -1096,7 +1119,10 @@ bool ArchiveManager::deleteBookmarksInArchive(const Archive* archive)
if (!bookmark || bookmark->parent() == archive)
{
removed_entries.push_back(bookmark.get());
- bookmarks_.erase(bookmarks_.begin() + a);
+ if (remove_from_library)
+ removeBookmark(a);
+ else
+ bookmarks_.erase(bookmarks_.begin() + a);
a--;
removed = true;
}
@@ -1151,7 +1177,7 @@ bool ArchiveManager::deleteBookmarksInDir(const ArchiveDir* node)
if (remove)
{
removed_entries.push_back(bookmark.get());
- bookmarks_.erase(bookmarks_.begin() + a);
+ removeBookmark(a);
--a;
removed = true;
}
@@ -1177,7 +1203,11 @@ void ArchiveManager::deleteAllBookmarks()
{
vector removed;
for (const auto& entry : bookmarks_)
- removed.push_back(entry.lock().get());
+ {
+ auto sp = entry.lock();
+ removed.push_back(sp.get());
+ library::removeBookmark(sp->parent()->libraryId(), sp->libraryId());
+ }
bookmarks_.clear();
signals_.bookmarks_removed(removed);
@@ -1208,6 +1238,34 @@ bool slade::ArchiveManager::isBookmarked(const ArchiveEntry* entry) const
return false;
}
+// -----------------------------------------------------------------------------
+// Helper function that removes the bookmark at [index] and handles library
+// updates etc.
+// -----------------------------------------------------------------------------
+void ArchiveManager::removeBookmark(unsigned index)
+{
+ auto* entry = bookmarks_[index].lock().get();
+
+ bookmarks_.erase(bookmarks_.begin() + index);
+
+ if (entry)
+ library::removeBookmark(entry->parent()->libraryId(), entry->libraryId());
+}
+
+// -----------------------------------------------------------------------------
+// Adds all bookmarks from the library for [archive]
+// -----------------------------------------------------------------------------
+void ArchiveManager::addBookmarksFromLibrary(const Archive& archive)
+{
+ auto bookmark_ids = library::bookmarkedEntries(archive.libraryId());
+
+ vector entries;
+ archive.putEntryTreeAsList(entries);
+ for (auto entry : entries)
+ if (VECTOR_EXISTS(bookmark_ids, entry->libraryId()))
+ addBookmark(entry->getShared());
+}
+
// -----------------------------------------------------------------------------
//
diff --git a/src/Archive/ArchiveManager.h b/src/Archive/ArchiveManager.h
index 7132f6040..dfe360dce 100644
--- a/src/Archive/ArchiveManager.h
+++ b/src/Archive/ArchiveManager.h
@@ -39,7 +39,6 @@ class ArchiveManager
shared_ptr shareArchive(const Archive* archive);
// General access
- const vector& recentFiles() const { return recent_files_; }
const vector& baseResourcePaths() const { return base_resource_paths_; }
const vector>& bookmarks() const { return bookmarks_; }
@@ -49,6 +48,7 @@ class ArchiveManager
void removeBaseResourcePath(unsigned index);
unsigned numBaseResourcePaths() const { return base_resource_paths_.size(); }
string getBaseResourcePath(unsigned index);
+ string currentBaseResourcePath();
bool openBaseResource(int index);
// Resource entry get/search
@@ -56,18 +56,11 @@ class ArchiveManager
ArchiveEntry* findResourceEntry(ArchiveSearchOptions& options, const Archive* ignore = nullptr) const;
vector findAllResourceEntries(ArchiveSearchOptions& options, const Archive* ignore = nullptr) const;
- // Recent files
- string recentFile(unsigned index);
- unsigned numRecentFiles() const { return recent_files_.size(); }
- void addRecentFile(string_view path);
- void addRecentFiles(const vector& paths);
- void removeRecentFile(string_view path);
-
// Bookmarks
void addBookmark(const shared_ptr& entry);
bool deleteBookmark(ArchiveEntry* entry);
bool deleteBookmark(unsigned index);
- bool deleteBookmarksInArchive(const Archive* archive);
+ bool deleteBookmarksInArchive(const Archive* archive, bool remove_from_library = true);
bool deleteBookmarksInDir(const ArchiveDir* node);
void deleteAllBookmarks();
ArchiveEntry* getBookmark(unsigned index) const;
@@ -87,7 +80,6 @@ class ArchiveManager
sigslot::signal base_res_path_removed;
sigslot::signal base_res_current_changed;
sigslot::signal<> base_res_current_cleared;
- sigslot::signal<> recent_files_changed;
sigslot::signal bookmark_added;
sigslot::signal&> bookmarks_removed;
};
@@ -98,7 +90,7 @@ class ArchiveManager
{
shared_ptr archive;
vector> open_children; // A list of currently open archives that are within this archive
- bool resource;
+ bool resource = true;
};
vector open_archives_;
@@ -106,13 +98,14 @@ class ArchiveManager
shared_ptr base_resource_archive_;
bool res_archive_open_ = false;
vector base_resource_paths_;
- vector recent_files_;
vector> bookmarks_;
// Signals
Signals signals_;
- bool initArchiveFormats() const;
- void getDependentArchivesInternal(const Archive* archive, vector>& vec);
+ bool initArchiveFormats() const;
+ void getDependentArchivesInternal(const Archive* archive, vector>& vec);
+ inline void removeBookmark(unsigned index);
+ void addBookmarksFromLibrary(const Archive& archive);
};
} // namespace slade
diff --git a/src/Archive/EntryType/EntryType.cpp b/src/Archive/EntryType/EntryType.cpp
index 1f1bd6144..b30f60e3f 100644
--- a/src/Archive/EntryType/EntryType.cpp
+++ b/src/Archive/EntryType/EntryType.cpp
@@ -622,6 +622,20 @@ bool EntryType::detectEntryType(ArchiveEntry& entry)
// Reset entry type
entry.setType(etype_unknown);
+ // Check for hinted type first
+ if (entry.exProps().contains("TypeHint"))
+ {
+ auto hint_type = fromId(std::get(entry.exProp("TypeHint")));
+ if (auto r = hint_type->isThisType(entry); r > 0)
+ {
+ entry.setType(hint_type, r);
+ return true;
+ }
+
+ // Wasn't the hinted type, remove hint
+ entry.exProps().remove("TypeHint");
+ }
+
// Go through all registered types
const size_t entry_types_size = entry_types.size();
for (size_t a = 0; a < entry_types_size; a++)
@@ -676,6 +690,14 @@ EntryType* EntryType::folderType()
return etype_folder;
}
+// -----------------------------------------------------------------------------
+// Returns the global 'marker' entry type
+// -----------------------------------------------------------------------------
+EntryType* EntryType::markerType()
+{
+ return etype_marker;
+}
+
// -----------------------------------------------------------------------------
// Returns the global 'map marker' entry type
// -----------------------------------------------------------------------------
diff --git a/src/Archive/EntryType/EntryType.h b/src/Archive/EntryType/EntryType.h
index 064dbcabc..79889bed9 100644
--- a/src/Archive/EntryType/EntryType.h
+++ b/src/Archive/EntryType/EntryType.h
@@ -41,6 +41,7 @@ class EntryType
static EntryType* fromId(string_view id);
static EntryType* unknownType();
static EntryType* folderType();
+ static EntryType* markerType();
static EntryType* mapMarkerType();
static vector iconList();
static vector allTypes();
diff --git a/src/Archive/Formats/ADatArchiveHandler.cpp b/src/Archive/Formats/ADatArchiveHandler.cpp
index a587798ba..fb76c88ea 100644
--- a/src/Archive/Formats/ADatArchiveHandler.cpp
+++ b/src/Archive/Formats/ADatArchiveHandler.cpp
@@ -52,7 +52,7 @@ using namespace slade;
// Reads dat format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check given data is valid
if (mc.size() < 16)
@@ -144,7 +144,8 @@ bool ADatArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/ADatArchiveHandler.h b/src/Archive/Formats/ADatArchiveHandler.h
index 50dc8a829..b9ed93abe 100644
--- a/src/Archive/Formats/ADatArchiveHandler.h
+++ b/src/Archive/Formats/ADatArchiveHandler.h
@@ -11,7 +11,7 @@ class ADatArchiveHandler : public ArchiveFormatHandler
~ADatArchiveHandler() override = default;
// Opening
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
diff --git a/src/Archive/Formats/BSPArchiveHandler.cpp b/src/Archive/Formats/BSPArchiveHandler.cpp
index c1307df99..066533f98 100644
--- a/src/Archive/Formats/BSPArchiveHandler.cpp
+++ b/src/Archive/Formats/BSPArchiveHandler.cpp
@@ -57,7 +57,7 @@ using namespace slade;
// Reads BSP format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// If size is less than 64, there's not even enough room for a full header
size_t size = mc.size();
@@ -218,7 +218,8 @@ bool BSPArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/BSPArchiveHandler.h b/src/Archive/Formats/BSPArchiveHandler.h
index cddf3dba2..f2c2a8fd7 100644
--- a/src/Archive/Formats/BSPArchiveHandler.h
+++ b/src/Archive/Formats/BSPArchiveHandler.h
@@ -11,8 +11,8 @@ class BSPArchiveHandler : public ArchiveFormatHandler
~BSPArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/BZip2ArchiveHandler.cpp b/src/Archive/Formats/BZip2ArchiveHandler.cpp
index eb01cc1d4..f1f912ff1 100644
--- a/src/Archive/Formats/BZip2ArchiveHandler.cpp
+++ b/src/Archive/Formats/BZip2ArchiveHandler.cpp
@@ -52,7 +52,7 @@ using namespace slade;
// Reads bzip2 format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool BZip2ArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool BZip2ArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
size_t size = mc.size();
if (size < 14)
@@ -151,7 +151,7 @@ bool BZip2ArchiveHandler::loadEntryData(Archive& archive, const ArchiveEntry* en
// Returns the entry if it matches the search criteria in [options],
// or null otherwise
// -----------------------------------------------------------------------------
-ArchiveEntry* BZip2ArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* BZip2ArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
strutil::upperIP(options.match_name);
@@ -191,7 +191,7 @@ ArchiveEntry* BZip2ArchiveHandler::findFirst(Archive& archive, ArchiveSearchOpti
// -----------------------------------------------------------------------------
// Same as findFirst since there's just one entry
// -----------------------------------------------------------------------------
-ArchiveEntry* BZip2ArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* BZip2ArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
return findFirst(archive, options);
}
@@ -199,7 +199,7 @@ ArchiveEntry* BZip2ArchiveHandler::findLast(Archive& archive, ArchiveSearchOptio
// -----------------------------------------------------------------------------
// Returns all entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector BZip2ArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector BZip2ArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
vector ret;
diff --git a/src/Archive/Formats/BZip2ArchiveHandler.h b/src/Archive/Formats/BZip2ArchiveHandler.h
index a68280244..0bd87e710 100644
--- a/src/Archive/Formats/BZip2ArchiveHandler.h
+++ b/src/Archive/Formats/BZip2ArchiveHandler.h
@@ -11,7 +11,7 @@ class BZip2ArchiveHandler : public ArchiveFormatHandler
~BZip2ArchiveHandler() override = default;
// Opening
- bool open(Archive& archive, const MemChunk& mc) override;
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override;
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override;
@@ -47,9 +47,9 @@ class BZip2ArchiveHandler : public ArchiveFormatHandler
}
// Search
- ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override;
- ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override;
- vector findAll(Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override;
+ vector findAll(const Archive& archive, ArchiveSearchOptions& options) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/ChasmBinArchiveHandler.cpp b/src/Archive/Formats/ChasmBinArchiveHandler.cpp
index 14de0f3a3..45ad47f92 100644
--- a/src/Archive/Formats/ChasmBinArchiveHandler.cpp
+++ b/src/Archive/Formats/ChasmBinArchiveHandler.cpp
@@ -77,7 +77,7 @@ void fixBrokenWave(const ArchiveEntry* entry)
// Reads Chasm bin format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ChasmBinArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool ChasmBinArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check given data is valid
if (mc.size() < HEADER_SIZE)
diff --git a/src/Archive/Formats/ChasmBinArchiveHandler.h b/src/Archive/Formats/ChasmBinArchiveHandler.h
index c0160251d..0a0535ec3 100644
--- a/src/Archive/Formats/ChasmBinArchiveHandler.h
+++ b/src/Archive/Formats/ChasmBinArchiveHandler.h
@@ -10,7 +10,7 @@ class ChasmBinArchiveHandler : public ArchiveFormatHandler
ChasmBinArchiveHandler() : ArchiveFormatHandler(ArchiveFormat::ChasmBin) {}
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override;
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override;
bool write(Archive& archive, MemChunk& mc) override;
// Format detection
diff --git a/src/Archive/Formats/DatArchiveHandler.cpp b/src/Archive/Formats/DatArchiveHandler.cpp
index 9a6bef868..0f8a4e461 100644
--- a/src/Archive/Formats/DatArchiveHandler.cpp
+++ b/src/Archive/Formats/DatArchiveHandler.cpp
@@ -68,7 +68,7 @@ bool isNamespaceEntry(const ArchiveEntry* entry)
// Reads wad format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -177,7 +177,8 @@ bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
@@ -191,7 +192,7 @@ bool DatArchiveHandler::open(Archive& archive, const MemChunk& mc)
// -----------------------------------------------------------------------------
// Returns the namespace that [entry] is within
// -----------------------------------------------------------------------------
-string DatArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
+string DatArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry)
{
return detectNamespace(archive, archive.entryIndex(entry));
}
@@ -199,7 +200,7 @@ string DatArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
// -----------------------------------------------------------------------------
// Returns the namespace that the entry at [index] in [dir] is within
// -----------------------------------------------------------------------------
-string DatArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir)
+string DatArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir)
{
// Textures
if (index > static_cast(walls_[0]) && index < static_cast(walls_[1]))
diff --git a/src/Archive/Formats/DatArchiveHandler.h b/src/Archive/Formats/DatArchiveHandler.h
index b98cc32a1..9141cc926 100644
--- a/src/Archive/Formats/DatArchiveHandler.h
+++ b/src/Archive/Formats/DatArchiveHandler.h
@@ -14,8 +14,8 @@ class DatArchiveHandler : public ArchiveFormatHandler
void updateNamespaces(Archive& archive);
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Entry addition/removal
shared_ptr addEntry(
@@ -36,8 +36,8 @@ class DatArchiveHandler : public ArchiveFormatHandler
bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false) override;
// Detection
- string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
- string detectNamespace(Archive& archive, ArchiveEntry* entry) override;
+ string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
+ string detectNamespace(const Archive& archive, ArchiveEntry* entry) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/DirArchiveHandler.cpp b/src/Archive/Formats/DirArchiveHandler.cpp
index 051714b71..291a737b5 100644
--- a/src/Archive/Formats/DirArchiveHandler.cpp
+++ b/src/Archive/Formats/DirArchiveHandler.cpp
@@ -88,7 +88,7 @@ DirArchiveHandler::DirArchiveHandler() :
// Reads files from the directory [filename] into the archive
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool DirArchiveHandler::open(Archive& archive, string_view filename)
+bool DirArchiveHandler::open(Archive& archive, string_view filename, bool detect_types)
{
ui::setSplashProgressMessage("Reading directory structure");
ui::setSplashProgress(0);
@@ -131,9 +131,6 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename)
return false;
file_modification_times_[new_entry.get()] = fileutil::fileModifiedTime(files[a]);
-
- // Detect entry type
- EntryType::detectEntryType(*new_entry);
}
// Add empty directories
@@ -154,6 +151,10 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename)
for (auto& entry : entry_list)
entry->setState(EntryState::Unmodified);
+ // Detect all entry types
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
// Enable announcements
sig_blocker.unblock();
@@ -167,7 +168,7 @@ bool DirArchiveHandler::open(Archive& archive, string_view filename)
// -----------------------------------------------------------------------------
// Reads an archive from an ArchiveEntry (not implemented)
// -----------------------------------------------------------------------------
-bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry)
+bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry, bool detect_types)
{
global::error = "Cannot open Folder Archive from entry";
return false;
@@ -176,7 +177,7 @@ bool DirArchiveHandler::open(Archive& archive, ArchiveEntry* entry)
// -----------------------------------------------------------------------------
// Reads data from a MemChunk (not implemented)
// -----------------------------------------------------------------------------
-bool DirArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool DirArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
global::error = "Cannot open Folder Archive from memory";
return false;
@@ -450,7 +451,7 @@ bool DirArchiveHandler::renameEntry(Archive& archive, ArchiveEntry* entry, strin
// Returns the mapdesc_t information about the map at [entry], if [entry] is
// actually a valid map (ie. a wad archive in the maps folder)
// -----------------------------------------------------------------------------
-MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry)
+MapDesc DirArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* entry)
{
MapDesc map;
@@ -466,11 +467,20 @@ MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry)
if (entry->parentDir()->parent() != archive.rootDir() || entry->parentDir()->name() != "maps")
return map;
+ // Detect map format
+ auto format = MapFormat::Unknown;
+ Archive tempwad(ArchiveFormat::Wad);
+ tempwad.open(entry->data(), true);
+ auto emaps = tempwad.detectMaps();
+ if (!emaps.empty())
+ format = emaps[0].format;
+
// Setup map info
map.archive = true;
map.head = entry->getShared();
map.end = entry->getShared();
map.name = entry->upperNameNoExt();
+ map.format = format;
return map;
}
@@ -479,7 +489,7 @@ MapDesc DirArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* entry)
// Detects all the maps in the archive and returns a vector of information about
// them.
// -----------------------------------------------------------------------------
-vector DirArchiveHandler::detectMaps(Archive& archive)
+vector DirArchiveHandler::detectMaps(const Archive& archive)
{
vector ret;
@@ -500,7 +510,7 @@ vector DirArchiveHandler::detectMaps(Archive& archive)
// Detect map format (probably kinda slow but whatever, no better way to do it really)
auto format = MapFormat::Unknown;
Archive tempwad(ArchiveFormat::Wad);
- tempwad.open(entry->data());
+ tempwad.open(entry->data(), true);
auto emaps = tempwad.detectMaps();
if (!emaps.empty())
format = emaps[0].format;
@@ -522,7 +532,7 @@ vector DirArchiveHandler::detectMaps(Archive& archive)
// Returns the first entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* DirArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* DirArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
@@ -555,7 +565,7 @@ ArchiveEntry* DirArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption
// Returns the last entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* DirArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* DirArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
@@ -587,7 +597,7 @@ ArchiveEntry* DirArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions
// -----------------------------------------------------------------------------
// Returns all entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector DirArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector DirArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
diff --git a/src/Archive/Formats/DirArchiveHandler.h b/src/Archive/Formats/DirArchiveHandler.h
index aa0a96cc1..65f94453d 100644
--- a/src/Archive/Formats/DirArchiveHandler.h
+++ b/src/Archive/Formats/DirArchiveHandler.h
@@ -46,9 +46,9 @@ class DirArchiveHandler : public ArchiveFormatHandler
bool saveErrorsOccurred() const { return save_errors_; }
// Opening
- bool open(Archive& archive, string_view filename) override; // Open from File
- bool open(Archive& archive, ArchiveEntry* entry) override; // Open from ArchiveEntry
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
+ bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File
+ bool open(Archive& archive, ArchiveEntry* entry, bool detect_types) override; // Open from ArchiveEntry
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
@@ -69,13 +69,13 @@ class DirArchiveHandler : public ArchiveFormatHandler
bool renameEntry(Archive& archive, ArchiveEntry* entry, string_view name, bool force = false) override;
// Detection
- MapDesc mapDesc(Archive& archive, ArchiveEntry* entry) override;
- vector detectMaps(Archive& archive) override;
+ MapDesc mapDesc(const Archive& archive, ArchiveEntry* entry) override;
+ vector detectMaps(const Archive& archive) override;
// Search
- ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override;
- ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override;
- vector findAll(Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override;
+ vector findAll(const Archive& archive, ArchiveSearchOptions& options) override;
// DirArchiveHandler-specific
void ignoreChangedEntries(const vector& changes);
diff --git a/src/Archive/Formats/DiskArchiveHandler.cpp b/src/Archive/Formats/DiskArchiveHandler.cpp
index b1bcf820a..bb83bdc66 100644
--- a/src/Archive/Formats/DiskArchiveHandler.cpp
+++ b/src/Archive/Formats/DiskArchiveHandler.cpp
@@ -54,7 +54,7 @@ using namespace slade;
// Reads disk format data from a MemChunk.
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
size_t mcsize = mc.size();
@@ -127,7 +127,8 @@ bool DiskArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/DiskArchiveHandler.h b/src/Archive/Formats/DiskArchiveHandler.h
index 0d18b6b5f..3c23fb889 100644
--- a/src/Archive/Formats/DiskArchiveHandler.h
+++ b/src/Archive/Formats/DiskArchiveHandler.h
@@ -18,8 +18,8 @@ class DiskArchiveHandler : public ArchiveFormatHandler
~DiskArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/GZipArchiveHandler.cpp b/src/Archive/Formats/GZipArchiveHandler.cpp
index 33e3c84a6..1872e4113 100644
--- a/src/Archive/Formats/GZipArchiveHandler.cpp
+++ b/src/Archive/Formats/GZipArchiveHandler.cpp
@@ -53,7 +53,7 @@ using namespace slade;
// Reads gzip format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool GZipArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool GZipArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Minimal metadata size is 18: 10 for header, 8 for footer
size_t mds = 18;
@@ -284,7 +284,7 @@ bool GZipArchiveHandler::loadEntryData(Archive& archive, const ArchiveEntry* ent
// Returns the entry if it matches the search criteria in [options], or null
// otherwise
// -----------------------------------------------------------------------------
-ArchiveEntry* GZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* GZipArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
strutil::upperIP(options.match_name);
@@ -325,7 +325,7 @@ ArchiveEntry* GZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptio
// Returns the last entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* GZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* GZipArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
return findFirst(archive, options);
}
@@ -333,7 +333,7 @@ ArchiveEntry* GZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOption
// -----------------------------------------------------------------------------
// Returns all entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector GZipArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector GZipArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
vector ret;
diff --git a/src/Archive/Formats/GZipArchiveHandler.h b/src/Archive/Formats/GZipArchiveHandler.h
index fc2bd5335..6ac5fb7cd 100644
--- a/src/Archive/Formats/GZipArchiveHandler.h
+++ b/src/Archive/Formats/GZipArchiveHandler.h
@@ -11,7 +11,7 @@ class GZipArchiveHandler : public ArchiveFormatHandler
~GZipArchiveHandler() override = default;
// Opening
- bool open(Archive& archive, const MemChunk& mc) override;
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override;
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override;
@@ -47,9 +47,9 @@ class GZipArchiveHandler : public ArchiveFormatHandler
}
// Search
- ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override;
- ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override;
- vector findAll(Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override;
+ vector findAll(const Archive& archive, ArchiveSearchOptions& options) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/GobArchiveHandler.cpp b/src/Archive/Formats/GobArchiveHandler.cpp
index 4dde7fdcb..37d7bee0c 100644
--- a/src/Archive/Formats/GobArchiveHandler.cpp
+++ b/src/Archive/Formats/GobArchiveHandler.cpp
@@ -49,7 +49,7 @@ using namespace slade;
// Reads gob format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -132,7 +132,8 @@ bool GobArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/GobArchiveHandler.h b/src/Archive/Formats/GobArchiveHandler.h
index 7a27c27e3..401b810eb 100644
--- a/src/Archive/Formats/GobArchiveHandler.h
+++ b/src/Archive/Formats/GobArchiveHandler.h
@@ -11,8 +11,8 @@ class GobArchiveHandler : public ArchiveFormatHandler
~GobArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/GrpArchiveHandler.cpp b/src/Archive/Formats/GrpArchiveHandler.cpp
index d64b24513..1e5376c54 100644
--- a/src/Archive/Formats/GrpArchiveHandler.cpp
+++ b/src/Archive/Formats/GrpArchiveHandler.cpp
@@ -49,7 +49,7 @@ using namespace slade;
// Reads grp format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -69,7 +69,7 @@ bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc)
ken_magic[12] = 0;
// Check the header
- if (string_view{ ken_magic } != "KenSilverman")
+ if (string{ ken_magic } != "KenSilverman")
{
log::error("GrpArchiveHandler::openFile: File {} has invalid header", archive.filename());
global::error = "Invalid grp header";
@@ -129,7 +129,8 @@ bool GrpArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
@@ -211,7 +212,7 @@ bool GrpArchiveHandler::isThisFormat(const MemChunk& mc)
ken_magic[12] = 0;
// Check the header
- if (string_view{ ken_magic } != "KenSilverman")
+ if (string{ ken_magic } != "KenSilverman")
return false;
// Compute total size
@@ -262,7 +263,7 @@ bool GrpArchiveHandler::isThisFormat(const string& filename)
ken_magic[12] = 0;
// Check the header
- if (string_view{ ken_magic } != "KenSilverman")
+ if (string{ ken_magic } != "KenSilverman")
return false;
// Compute total size
diff --git a/src/Archive/Formats/GrpArchiveHandler.h b/src/Archive/Formats/GrpArchiveHandler.h
index bf502ed1c..ab3f3b63b 100644
--- a/src/Archive/Formats/GrpArchiveHandler.h
+++ b/src/Archive/Formats/GrpArchiveHandler.h
@@ -11,8 +11,8 @@ class GrpArchiveHandler : public ArchiveFormatHandler
~GrpArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/HogArchiveHandler.cpp b/src/Archive/Formats/HogArchiveHandler.cpp
index 5ed77fee5..df8002185 100644
--- a/src/Archive/Formats/HogArchiveHandler.cpp
+++ b/src/Archive/Formats/HogArchiveHandler.cpp
@@ -116,7 +116,7 @@ bool shouldEncodeTxb(string_view name)
// Reads hog format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -196,7 +196,8 @@ bool HogArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/HogArchiveHandler.h b/src/Archive/Formats/HogArchiveHandler.h
index c6db82a2c..3eba1af74 100644
--- a/src/Archive/Formats/HogArchiveHandler.h
+++ b/src/Archive/Formats/HogArchiveHandler.h
@@ -11,8 +11,8 @@ class HogArchiveHandler : public ArchiveFormatHandler
~HogArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Entry addition/removal
shared_ptr addEntry(
diff --git a/src/Archive/Formats/LfdArchiveHandler.cpp b/src/Archive/Formats/LfdArchiveHandler.cpp
index 56b46aaf2..b2b219663 100644
--- a/src/Archive/Formats/LfdArchiveHandler.cpp
+++ b/src/Archive/Formats/LfdArchiveHandler.cpp
@@ -50,7 +50,7 @@ using namespace slade;
// Reads lfd format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -140,7 +140,8 @@ bool LfdArchiveHandler::open(Archive& archive, const MemChunk& mc)
log::warning("Computed {} lumps, but actually {} entries", num_lumps, archive.numEntries());
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/LfdArchiveHandler.h b/src/Archive/Formats/LfdArchiveHandler.h
index 33fec39ef..c433d81f7 100644
--- a/src/Archive/Formats/LfdArchiveHandler.h
+++ b/src/Archive/Formats/LfdArchiveHandler.h
@@ -11,8 +11,8 @@ class LfdArchiveHandler : public ArchiveFormatHandler
~LfdArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/LibArchiveHandler.cpp b/src/Archive/Formats/LibArchiveHandler.cpp
index c00c72a89..3daaef609 100644
--- a/src/Archive/Formats/LibArchiveHandler.cpp
+++ b/src/Archive/Formats/LibArchiveHandler.cpp
@@ -49,7 +49,7 @@ using namespace slade;
// Reads wad format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -112,7 +112,8 @@ bool LibArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/LibArchiveHandler.h b/src/Archive/Formats/LibArchiveHandler.h
index 50a2d80f7..ac3b82b7f 100644
--- a/src/Archive/Formats/LibArchiveHandler.h
+++ b/src/Archive/Formats/LibArchiveHandler.h
@@ -11,8 +11,8 @@ class LibArchiveHandler : public ArchiveFormatHandler
~LibArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/PakArchiveHandler.cpp b/src/Archive/Formats/PakArchiveHandler.cpp
index a8565f102..c09d7695c 100644
--- a/src/Archive/Formats/PakArchiveHandler.cpp
+++ b/src/Archive/Formats/PakArchiveHandler.cpp
@@ -50,7 +50,7 @@ using namespace slade;
// Reads pak format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check given data is valid
if (mc.size() < 12)
@@ -124,7 +124,8 @@ bool PakArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/PakArchiveHandler.h b/src/Archive/Formats/PakArchiveHandler.h
index 151191f8c..d88f0d390 100644
--- a/src/Archive/Formats/PakArchiveHandler.h
+++ b/src/Archive/Formats/PakArchiveHandler.h
@@ -11,8 +11,8 @@ class PakArchiveHandler : public ArchiveFormatHandler
~PakArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/PodArchiveHandler.cpp b/src/Archive/Formats/PodArchiveHandler.cpp
index 7feba4b5c..53de93ab9 100644
--- a/src/Archive/Formats/PodArchiveHandler.cpp
+++ b/src/Archive/Formats/PodArchiveHandler.cpp
@@ -70,7 +70,7 @@ void PodArchiveHandler::setId(string_view id)
// Reads pod format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -113,7 +113,8 @@ bool PodArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/PodArchiveHandler.h b/src/Archive/Formats/PodArchiveHandler.h
index 871b8750d..9c7d7b693 100644
--- a/src/Archive/Formats/PodArchiveHandler.h
+++ b/src/Archive/Formats/PodArchiveHandler.h
@@ -14,7 +14,7 @@ class PodArchiveHandler : public ArchiveFormatHandler
void setId(string_view id);
// Opening
- bool open(Archive& archive, const MemChunk& mc) override;
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override;
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override;
diff --git a/src/Archive/Formats/ResArchiveHandler.cpp b/src/Archive/Formats/ResArchiveHandler.cpp
index c13581dd3..09a0187fe 100644
--- a/src/Archive/Formats/ResArchiveHandler.cpp
+++ b/src/Archive/Formats/ResArchiveHandler.cpp
@@ -154,9 +154,6 @@ bool ResArchiveHandler::readDirectory(
{
parent->addEntry(nlump);
- // Detect entry type
- EntryType::detectEntryType(*nlump);
-
// Set entry to unchanged
nlump->setState(EntryState::Unmodified);
}
@@ -168,7 +165,7 @@ bool ResArchiveHandler::readDirectory(
// Reads res format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -212,6 +209,14 @@ bool ResArchiveHandler::open(Archive& archive, const MemChunk& mc)
if (!readDirectory(archive, mc, dir_offset, num_lumps, archive.rootDir()))
return false;
+ // Detect maps (will detect map entry types)
+ ui::setSplashProgressMessage("Detecting maps");
+ detectMaps(archive);
+
+ // Detect all entry types
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
// Setup variables
sig_blocker.unblock();
archive.setModified(false);
diff --git a/src/Archive/Formats/ResArchiveHandler.h b/src/Archive/Formats/ResArchiveHandler.h
index ac3df1851..a64656e9c 100644
--- a/src/Archive/Formats/ResArchiveHandler.h
+++ b/src/Archive/Formats/ResArchiveHandler.h
@@ -17,8 +17,8 @@ class ResArchiveHandler : public ArchiveFormatHandler
size_t dir_offset,
size_t num_lumps,
shared_ptr parent);
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/RffArchiveHandler.cpp b/src/Archive/Formats/RffArchiveHandler.cpp
index ac36af87f..0219a2f10 100644
--- a/src/Archive/Formats/RffArchiveHandler.cpp
+++ b/src/Archive/Formats/RffArchiveHandler.cpp
@@ -120,7 +120,7 @@ void bloodCrypt(void* data, int key, int len)
// Reads rff format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -226,7 +226,8 @@ bool RffArchiveHandler::open(Archive& archive, const MemChunk& mc)
delete[] lumps;
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/RffArchiveHandler.h b/src/Archive/Formats/RffArchiveHandler.h
index 26a65eecc..1b64c37f3 100644
--- a/src/Archive/Formats/RffArchiveHandler.h
+++ b/src/Archive/Formats/RffArchiveHandler.h
@@ -11,8 +11,8 @@ class RffArchiveHandler : public ArchiveFormatHandler
~RffArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/SiNArchiveHandler.cpp b/src/Archive/Formats/SiNArchiveHandler.cpp
index b381df708..587f2d448 100644
--- a/src/Archive/Formats/SiNArchiveHandler.cpp
+++ b/src/Archive/Formats/SiNArchiveHandler.cpp
@@ -51,7 +51,7 @@ using namespace slade;
// Reads SiN format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check given data is valid
if (mc.size() < 12)
@@ -125,7 +125,8 @@ bool SiNArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/SiNArchiveHandler.h b/src/Archive/Formats/SiNArchiveHandler.h
index a02f38d9d..51b05b1ee 100644
--- a/src/Archive/Formats/SiNArchiveHandler.h
+++ b/src/Archive/Formats/SiNArchiveHandler.h
@@ -11,8 +11,8 @@ class SiNArchiveHandler : public ArchiveFormatHandler
~SiNArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/TarArchiveHandler.cpp b/src/Archive/Formats/TarArchiveHandler.cpp
index f0a11ce2b..72e0644d4 100644
--- a/src/Archive/Formats/TarArchiveHandler.cpp
+++ b/src/Archive/Formats/TarArchiveHandler.cpp
@@ -259,7 +259,7 @@ void tarDefaultHeader(TarHeader* header)
// Reads tar format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check given data is valid
if (mc.size() < 1024)
@@ -356,7 +356,8 @@ bool TarArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/TarArchiveHandler.h b/src/Archive/Formats/TarArchiveHandler.h
index 8dc986447..aa2edfcdb 100644
--- a/src/Archive/Formats/TarArchiveHandler.h
+++ b/src/Archive/Formats/TarArchiveHandler.h
@@ -11,8 +11,8 @@ class TarArchiveHandler : public ArchiveFormatHandler
~TarArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/Wad2ArchiveHandler.cpp b/src/Archive/Formats/Wad2ArchiveHandler.cpp
index c80441772..0e3339364 100644
--- a/src/Archive/Formats/Wad2ArchiveHandler.cpp
+++ b/src/Archive/Formats/Wad2ArchiveHandler.cpp
@@ -51,7 +51,7 @@ using namespace slade;
// Reads wad format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -128,7 +128,12 @@ bool Wad2ArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
+ // Detect maps (will detect map entry types)
+ ui::setSplashProgressMessage("Detecting maps");
+ detectMaps(archive);
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/Wad2ArchiveHandler.h b/src/Archive/Formats/Wad2ArchiveHandler.h
index 98abfd387..cec3c5c8c 100644
--- a/src/Archive/Formats/Wad2ArchiveHandler.h
+++ b/src/Archive/Formats/Wad2ArchiveHandler.h
@@ -23,7 +23,7 @@ class Wad2ArchiveHandler : public ArchiveFormatHandler
~Wad2ArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
// Format detection
diff --git a/src/Archive/Formats/WadArchiveHandler.cpp b/src/Archive/Formats/WadArchiveHandler.cpp
index a2cb36c95..8c91ef9bb 100644
--- a/src/Archive/Formats/WadArchiveHandler.cpp
+++ b/src/Archive/Formats/WadArchiveHandler.cpp
@@ -151,8 +151,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive)
// Check for namespace begin
if (strutil::endsWith(entry->upperName(), "_START"))
{
- log::debug("Found namespace start marker {} at index {}", entry->name(), archive.entryIndex(entry));
-
// Create new namespace
NSPair ns(entry, nullptr);
string_view name = entry->name();
@@ -170,8 +168,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive)
if (ns.name == "tt")
ns.name = "t";
- log::debug("Added namespace {}", ns.name);
-
// Add to namespace list
namespaces_.push_back(ns);
}
@@ -179,8 +175,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive)
// else if (strutil::matches(entry->upperName(), "?_END") || strutil::matches(entry->upperName(), "??_END"))
else if (strutil::endsWith(entry->upperName(), "_END"))
{
- log::debug("Found namespace end marker {} at index {}", entry->name(), archive.entryIndex(entry));
-
// Get namespace 'name'
auto ns_name = strutil::lower(entry->name());
strutil::removeLastIP(ns_name, 4);
@@ -195,8 +189,6 @@ void WadArchiveHandler::updateNamespaces(Archive& archive)
if (ns_name == "tt")
ns_name = "t";
- log::debug("Namespace name {}", ns_name);
-
// Check if it's the end of an existing namespace
// Remember entry is getEntry(a)? index is 'a'
// size_t index = entryIndex(entry);
@@ -290,7 +282,7 @@ bool WadArchiveHandler::hasFlatHack()
// Reads wad format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -459,7 +451,8 @@ bool WadArchiveHandler::open(Archive& archive, const MemChunk& mc)
updateNamespaces(archive);
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Identify #included lumps (DECORATE, GLDEFS, etc.)
detectIncludes(archive);
@@ -766,7 +759,7 @@ bool WadArchiveHandler::moveEntry(Archive& archive, ArchiveEntry* entry, unsigne
// If [maphead] is not really a map header entry, an invalid MapDesc will be
// returned (MapDesc::head == nullptr)
// -----------------------------------------------------------------------------
-MapDesc WadArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
+MapDesc WadArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead)
{
MapDesc map;
@@ -910,7 +903,7 @@ MapDesc WadArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
// -----------------------------------------------------------------------------
// Searches for any maps in the wad and adds them to the map list
// -----------------------------------------------------------------------------
-vector WadArchiveHandler::detectMaps(Archive& archive)
+vector WadArchiveHandler::detectMaps(const Archive& archive)
{
vector maps;
@@ -1043,8 +1036,8 @@ vector WadArchiveHandler::detectMaps(Archive& archive)
if (entry->type()->formatId() == "archive_wad")
{
// Detect map format (probably kinda slow but whatever, no better way to do it really)
- Archive tempwad("wad");
- tempwad.open(entry->data());
+ Archive tempwad(ArchiveFormat::Wad);
+ tempwad.open(entry->data(), true);
auto emaps = tempwad.detectMaps();
if (!emaps.empty())
{
@@ -1078,7 +1071,7 @@ vector WadArchiveHandler::detectMaps(Archive& archive)
// -----------------------------------------------------------------------------
// Returns the namespace that [entry] is within
// -----------------------------------------------------------------------------
-string WadArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
+string WadArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry)
{
return detectNamespace(archive, archive.entryIndex(entry));
}
@@ -1086,7 +1079,7 @@ string WadArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
// -----------------------------------------------------------------------------
// Returns the namespace that the entry at [index] in [dir] is within
// -----------------------------------------------------------------------------
-string WadArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir)
+string WadArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir)
{
// Go through namespaces
for (auto& ns : namespaces_)
@@ -1162,7 +1155,7 @@ void WadArchiveHandler::detectIncludes(Archive& archive)
// Returns the first entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* WadArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* WadArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
unsigned index = 0;
@@ -1231,7 +1224,7 @@ ArchiveEntry* WadArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption
// Returns the last entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* WadArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* WadArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
int index = archive.numEntries() - 1;
@@ -1303,7 +1296,7 @@ ArchiveEntry* WadArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions
// -----------------------------------------------------------------------------
// Returns all entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector WadArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector WadArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
unsigned index = 0;
diff --git a/src/Archive/Formats/WadArchiveHandler.h b/src/Archive/Formats/WadArchiveHandler.h
index 1af23f849..2ed53c0b5 100644
--- a/src/Archive/Formats/WadArchiveHandler.h
+++ b/src/Archive/Formats/WadArchiveHandler.h
@@ -16,7 +16,7 @@ class WadArchiveHandler : public ArchiveFormatHandler
void updateNamespaces(Archive& archive);
// Opening
- bool open(Archive& archive, const MemChunk& mc) override;
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override;
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
@@ -41,17 +41,17 @@ class WadArchiveHandler : public ArchiveFormatHandler
override;
// Detection
- MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead) override;
- vector detectMaps(Archive& archive) override;
- string detectNamespace(Archive& archive, ArchiveEntry* entry) override;
- string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
+ MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead) override;
+ vector detectMaps(const Archive& archive) override;
+ string detectNamespace(const Archive& archive, ArchiveEntry* entry) override;
+ string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
void detectIncludes(Archive& archive);
bool hasFlatHack() override;
// Search
- ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override;
- ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override;
- vector findAll(Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override;
+ vector findAll(const Archive& archive, ArchiveSearchOptions& options) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/WadJArchiveHandler.cpp b/src/Archive/Formats/WadJArchiveHandler.cpp
index 28b8dbb95..92d20e7f0 100644
--- a/src/Archive/Formats/WadJArchiveHandler.cpp
+++ b/src/Archive/Formats/WadJArchiveHandler.cpp
@@ -65,7 +65,7 @@ WadJArchiveHandler::WadJArchiveHandler() : WadArchiveHandler(ArchiveFormat::WadJ
// Reads wad format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -204,7 +204,8 @@ bool WadJArchiveHandler::open(Archive& archive, const MemChunk& mc)
updateNamespaces(archive);
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Detect maps (will detect map entry types)
ui::setSplashProgressMessage("Detecting maps");
@@ -284,14 +285,14 @@ bool WadJArchiveHandler::write(Archive& archive, MemChunk& mc)
// -----------------------------------------------------------------------------
// Hack to account for Jaguar Doom's silly sprite scheme
// -----------------------------------------------------------------------------
-string WadJArchiveHandler::detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir)
+string WadJArchiveHandler::detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir)
{
auto nextentry = archive.entryAt(index + 1);
if (nextentry && strutil::equalCI(nextentry->name(), "."))
return "sprites";
return WadArchiveHandler::detectNamespace(archive, index);
}
-string WadJArchiveHandler::detectNamespace(Archive& archive, ArchiveEntry* entry)
+string WadJArchiveHandler::detectNamespace(const Archive& archive, ArchiveEntry* entry)
{
size_t index = archive.entryIndex(entry);
auto nextentry = archive.entryAt(index + 1);
diff --git a/src/Archive/Formats/WadJArchiveHandler.h b/src/Archive/Formats/WadJArchiveHandler.h
index 60892a58d..e1a0f2b1b 100644
--- a/src/Archive/Formats/WadJArchiveHandler.h
+++ b/src/Archive/Formats/WadJArchiveHandler.h
@@ -11,11 +11,11 @@ class WadJArchiveHandler : public WadArchiveHandler
~WadJArchiveHandler() override = default;
// Opening/writing
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
- bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
+ bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
- string detectNamespace(Archive& archive, ArchiveEntry* entry) override;
- string detectNamespace(Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
+ string detectNamespace(const Archive& archive, ArchiveEntry* entry) override;
+ string detectNamespace(const Archive& archive, unsigned index, ArchiveDir* dir = nullptr) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/Archive/Formats/WolfArchiveHandler.cpp b/src/Archive/Formats/WolfArchiveHandler.cpp
index 0a1919621..3ed90b06c 100644
--- a/src/Archive/Formats/WolfArchiveHandler.cpp
+++ b/src/Archive/Formats/WolfArchiveHandler.cpp
@@ -34,7 +34,6 @@
#include "Archive/Archive.h"
#include "Archive/ArchiveDir.h"
#include "Archive/ArchiveEntry.h"
-#include "Archive/EntryType/EntryType.h"
#include "General/UI.h"
#include "Utility/FileUtils.h"
#include "Utility/StringUtils.h"
@@ -372,7 +371,7 @@ void expandWolfGraphLump(ArchiveEntry* entry, size_t lumpnum, size_t numlumps, H
// Reads a Wolf format file from disk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WolfArchiveHandler::open(Archive& archive, string_view filename)
+bool WolfArchiveHandler::open(Archive& archive, string_view filename, bool detect_types)
{
// Find wolf archive type
strutil::Path fn1(filename);
@@ -395,7 +394,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename)
MemChunk data, head;
head.importFile(findFileCasing(fn1));
data.importFile(findFileCasing(fn2));
- opened = openMaps(archive, head, data);
+ opened = openMaps(archive, head, data, detect_types);
}
else if (fn1_name == "AUDIOHED" || fn1_name == "AUDIOT")
{
@@ -405,7 +404,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename)
MemChunk data, head;
head.importFile(findFileCasing(fn1));
data.importFile(findFileCasing(fn2));
- opened = openAudio(archive, head, data);
+ opened = openAudio(archive, head, data, detect_types);
}
else if (fn1_name == "VGAHEAD" || fn1_name == "VGAGRAPH" || fn1_name == "VGADICT")
{
@@ -418,7 +417,7 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename)
head.importFile(findFileCasing(fn1));
data.importFile(findFileCasing(fn2));
dict.importFile(findFileCasing(fn3));
- opened = openGraph(archive, head, data, dict);
+ opened = openGraph(archive, head, data, dict, detect_types);
}
else
{
@@ -430,17 +429,26 @@ bool WolfArchiveHandler::open(Archive& archive, string_view filename)
return false;
}
// Load from MemChunk
- opened = open(archive, mc);
+ opened = open(archive, mc, detect_types);
}
- return opened;
+ if (opened)
+ {
+ // Detect all entry types
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
+ return true;
+ }
+ else
+ return false;
}
// -----------------------------------------------------------------------------
// Reads VSWAP Wolf format data from a MemChunk.
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Check data was given
if (!mc.hasData())
@@ -552,7 +560,8 @@ bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc)
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
@@ -567,7 +576,7 @@ bool WolfArchiveHandler::open(Archive& archive, const MemChunk& mc)
// Reads Wolf AUDIOT/AUDIOHEAD format data from a MemChunk.
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemChunk& data)
+bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types)
{
// Check data was given
if (!head.hasData() || !data.hasData())
@@ -689,16 +698,19 @@ bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemCh
nlump->setOffsetOnDisk(offset);
nlump->setSizeOnDisk();
- // Detect entry type
+ // Load data
if (size > 0)
nlump->importMemChunk(edata);
- EntryType::detectEntryType(*nlump);
// Add to entry list
nlump->setState(EntryState::Unmodified);
archive.rootDir()->addEntry(nlump);
}
+ // Detect all entry types
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
// Setup variables
sig_blocker.unblock();
archive.setModified(false);
@@ -712,7 +724,7 @@ bool WolfArchiveHandler::openAudio(Archive& archive, MemChunk& head, const MemCh
// Reads Wolf GAMEMAPS/MAPHEAD format data from a MemChunk.
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChunk& data)
+bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types)
{
// Check data was given
if (!head.hasData() || !data.hasData())
@@ -790,7 +802,8 @@ bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChu
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
@@ -806,7 +819,12 @@ bool WolfArchiveHandler::openMaps(Archive& archive, MemChunk& head, const MemChu
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
#define WC(a) WolfConstant(a, num_lumps)
-bool WolfArchiveHandler::openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict)
+bool WolfArchiveHandler::openGraph(
+ Archive& archive,
+ const MemChunk& head,
+ const MemChunk& data,
+ MemChunk& dict,
+ bool detect_types)
{
// Check data was given
if (!head.hasData() || !data.hasData() || !dict.hasData())
@@ -905,7 +923,8 @@ bool WolfArchiveHandler::openGraph(Archive& archive, const MemChunk& head, const
}
// Detect all entry types
- detectAllEntryTypes(archive);
+ if (detect_types)
+ archive.detectAllEntryTypes();
// Setup variables
sig_blocker.unblock();
diff --git a/src/Archive/Formats/WolfArchiveHandler.h b/src/Archive/Formats/WolfArchiveHandler.h
index 720add501..164cc3ad1 100644
--- a/src/Archive/Formats/WolfArchiveHandler.h
+++ b/src/Archive/Formats/WolfArchiveHandler.h
@@ -11,12 +11,12 @@ class WolfArchiveHandler : public ArchiveFormatHandler
~WolfArchiveHandler() override = default;
// Opening
- bool open(Archive& archive, string_view filename) override; // Open from File
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
+ bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
- bool openAudio(Archive& archive, MemChunk& head, const MemChunk& data);
- bool openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict);
- bool openMaps(Archive& archive, MemChunk& head, const MemChunk& data);
+ bool openAudio(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types);
+ bool openGraph(Archive& archive, const MemChunk& head, const MemChunk& data, MemChunk& dict, bool detect_types);
+ bool openMaps(Archive& archive, MemChunk& head, const MemChunk& data, bool detect_types);
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
diff --git a/src/Archive/Formats/ZipArchiveHandler.cpp b/src/Archive/Formats/ZipArchiveHandler.cpp
index d05cf3aa3..1d9e0d746 100644
--- a/src/Archive/Formats/ZipArchiveHandler.cpp
+++ b/src/Archive/Formats/ZipArchiveHandler.cpp
@@ -93,7 +93,7 @@ void ZipArchiveHandler::init(Archive& archive)
// Reads zip data from a file
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ZipArchiveHandler::open(Archive& archive, string_view filename)
+bool ZipArchiveHandler::open(Archive& archive, string_view filename, bool detect_types)
{
// Check the file exists
if (!fileutil::fileExists(filename))
@@ -162,9 +162,6 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename)
zip.Read(data.data(), ze_size); // Note: this is where exceedingly large files cause an exception.
new_entry->importMem(data.data(), static_cast(ze_size));
}
-
- // Determine its type
- EntryType::detectEntryType(*new_entry);
}
else
{
@@ -192,6 +189,10 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename)
for (auto& entry : entry_list)
entry->setState(EntryState::Unmodified);
+ // Detect all entry types
+ if (detect_types)
+ archive.detectAllEntryTypes();
+
// Enable announcements
sig_blocker.unblock();
@@ -206,14 +207,14 @@ bool ZipArchiveHandler::open(Archive& archive, string_view filename)
// Reads zip format data from a MemChunk
// Returns true if successful, false otherwise
// -----------------------------------------------------------------------------
-bool ZipArchiveHandler::open(Archive& archive, const MemChunk& mc)
+bool ZipArchiveHandler::open(Archive& archive, const MemChunk& mc, bool detect_types)
{
// Write the MemChunk to a temp file
const auto tempfile = app::path("slade-temp-open.zip", app::Dir::Temp);
mc.exportFile(tempfile);
// Load the file
- const bool success = open(archive, tempfile);
+ const bool success = open(archive, tempfile, true);
// Clean up
fileutil::removeFile(tempfile);
@@ -468,7 +469,7 @@ shared_ptr ZipArchiveHandler::addEntry(
// Returns the mapdesc_t information about the map at [entry], if [entry] is
// actually a valid map (ie. a wad archive in the maps folder)
// -----------------------------------------------------------------------------
-MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
+MapDesc ZipArchiveHandler::mapDesc(const Archive& archive, ArchiveEntry* maphead)
{
MapDesc map;
@@ -484,11 +485,20 @@ MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
if (maphead->parentDir()->parent() != archive.rootDir() || maphead->parentDir()->name() != "maps")
return map;
+ // Detect map format
+ auto format = MapFormat::Unknown;
+ Archive tempwad(ArchiveFormat::Wad);
+ tempwad.open(maphead->data(), true);
+ auto emaps = tempwad.detectMaps();
+ if (!emaps.empty())
+ format = emaps[0].format;
+
// Setup map info
map.archive = true;
map.head = maphead->getShared();
map.end = maphead->getShared();
map.name = maphead->upperNameNoExt();
+ map.format = format;
return map;
}
@@ -497,7 +507,7 @@ MapDesc ZipArchiveHandler::mapDesc(Archive& archive, ArchiveEntry* maphead)
// Detects all the maps in the archive and returns a vector of information about
// them.
// -----------------------------------------------------------------------------
-vector ZipArchiveHandler::detectMaps(Archive& archive)
+vector ZipArchiveHandler::detectMaps(const Archive& archive)
{
vector ret;
@@ -518,7 +528,7 @@ vector ZipArchiveHandler::detectMaps(Archive& archive)
// Detect map format (probably kinda slow but whatever, no better way to do it really)
auto format = MapFormat::Unknown;
Archive tempwad(ArchiveFormat::Wad);
- tempwad.open(entry->data());
+ tempwad.open(entry->data(), true);
auto emaps = tempwad.detectMaps();
if (!emaps.empty())
format = emaps[0].format;
@@ -540,7 +550,7 @@ vector ZipArchiveHandler::detectMaps(Archive& archive)
// Returns the first entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* ZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* ZipArchiveHandler::findFirst(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
@@ -573,7 +583,7 @@ ArchiveEntry* ZipArchiveHandler::findFirst(Archive& archive, ArchiveSearchOption
// Returns the last entry matching the search criteria in [options], or null if
// no matching entry was found
// -----------------------------------------------------------------------------
-ArchiveEntry* ZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions& options)
+ArchiveEntry* ZipArchiveHandler::findLast(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
@@ -605,7 +615,7 @@ ArchiveEntry* ZipArchiveHandler::findLast(Archive& archive, ArchiveSearchOptions
// -----------------------------------------------------------------------------
// Returns all entries matching the search criteria in [options]
// -----------------------------------------------------------------------------
-vector ZipArchiveHandler::findAll(Archive& archive, ArchiveSearchOptions& options)
+vector ZipArchiveHandler::findAll(const Archive& archive, ArchiveSearchOptions& options)
{
// Init search variables
auto dir = archive.rootDir().get();
diff --git a/src/Archive/Formats/ZipArchiveHandler.h b/src/Archive/Formats/ZipArchiveHandler.h
index 5ea8170bd..3d987a5d9 100644
--- a/src/Archive/Formats/ZipArchiveHandler.h
+++ b/src/Archive/Formats/ZipArchiveHandler.h
@@ -13,8 +13,8 @@ class ZipArchiveHandler : public ArchiveFormatHandler
void init(Archive& archive) override;
// Opening
- bool open(Archive& archive, string_view filename) override; // Open from File
- bool open(Archive& archive, const MemChunk& mc) override; // Open from MemChunk
+ bool open(Archive& archive, string_view filename, bool detect_types) override; // Open from File
+ bool open(Archive& archive, const MemChunk& mc, bool detect_types) override; // Open from MemChunk
// Writing/Saving
bool write(Archive& archive, MemChunk& mc) override; // Write to MemChunk
@@ -28,13 +28,13 @@ class ZipArchiveHandler : public ArchiveFormatHandler
override;
// Detection
- MapDesc mapDesc(Archive& archive, ArchiveEntry* maphead) override;
- vector detectMaps(Archive& archive) override;
+ MapDesc mapDesc(const Archive& archive, ArchiveEntry* maphead) override;
+ vector detectMaps(const Archive& archive) override;
// Search
- ArchiveEntry* findFirst(Archive& archive, ArchiveSearchOptions& options) override;
- ArchiveEntry* findLast(Archive& archive, ArchiveSearchOptions& options) override;
- vector findAll(Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findFirst(const Archive& archive, ArchiveSearchOptions& options) override;
+ ArchiveEntry* findLast(const Archive& archive, ArchiveSearchOptions& options) override;
+ vector findAll(const Archive& archive, ArchiveSearchOptions& options) override;
// Format detection
bool isThisFormat(const MemChunk& mc) override;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6c7527d3e..0edc8fc6e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -9,10 +9,12 @@ file(GLOB_RECURSE SLADE_SOURCES CONFIGURE_DEPENDS
Application/*.cpp
Archive/*.cpp
Audio/*.cpp
+ Database/*.cpp
Game/*.cpp
General/*.cpp
Geometry/*.cpp
Graphics/*.cpp
+ Library/*.cpp
MainEditor/*.cpp
MapEditor/*.cpp
OpenGL/*.cpp
diff --git a/src/Database/Context.cpp b/src/Database/Context.cpp
new file mode 100644
index 000000000..546b8ccb7
--- /dev/null
+++ b/src/Database/Context.cpp
@@ -0,0 +1,274 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: Context.cpp
+// Description: Database Context class - keeps connections open to a database,
+// since opening a new connection is expensive. It can also keep
+// cached sql queries (for frequent reuse)
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "Context.h"
+#include "App.h"
+#include "Transaction.h"
+#include
+#include
+
+using namespace slade;
+using namespace database;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::database
+{
+Context db_global;
+vector thread_contexts;
+std::shared_mutex mutex_thread_contexts;
+} // namespace slade::database
+
+
+// -----------------------------------------------------------------------------
+//
+// Context Class Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Context class constructor
+// -----------------------------------------------------------------------------
+Context::Context(string_view file_path)
+{
+ thread_id_ = std::this_thread::get_id();
+
+ if (!file_path.empty())
+ open(file_path);
+}
+
+// -----------------------------------------------------------------------------
+// Context class destructor
+// -----------------------------------------------------------------------------
+Context::~Context()
+{
+ close();
+
+ for (int i = static_cast(thread_contexts.size()) - 1; i >= 0; i--)
+ if (thread_contexts[i] == this)
+ thread_contexts.erase(thread_contexts.begin() + i);
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if the context was created on the current thread
+// -----------------------------------------------------------------------------
+bool Context::isForThisThread() const
+{
+ return thread_id_ == std::this_thread::get_id();
+}
+
+// -----------------------------------------------------------------------------
+// Opens connections to the database file at [file_path].
+// Returns false if any existing connections couldn't be closed, true otherwise
+// -----------------------------------------------------------------------------
+bool Context::open(string_view file_path)
+{
+ if (!close())
+ return false;
+
+ file_path_ = file_path;
+ connection_ro_ = std::make_unique(file_path_, SQLite::OPEN_READONLY, 100);
+ connection_rw_ = std::make_unique(file_path_, SQLite::OPEN_READWRITE, 100);
+
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Closes the context's connections to its database
+// -----------------------------------------------------------------------------
+bool Context::close()
+{
+ if (!connection_ro_)
+ return true;
+
+ try
+ {
+ cached_queries_.clear();
+ connection_ro_ = nullptr;
+ connection_rw_ = nullptr;
+ }
+ catch (std::exception& ex)
+ {
+ log::error("Error closing connections for database {}: {}", file_path_, ex.what());
+ return false;
+ }
+
+ file_path_.clear();
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the cached query [id] or nullptr if not found
+// -----------------------------------------------------------------------------
+Statement* Context::cachedQuery(string_view id)
+{
+ auto i = cached_queries_.find(id);
+ if (i != cached_queries_.end())
+ {
+ i->second->tryReset();
+ return i->second.get();
+ }
+
+ return nullptr;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the cached query [id] if it exists, otherwise creates a new cached
+// query from the given [sql] string and returns it.
+// If [writes] is true, the created query will use the read+write connection.
+// -----------------------------------------------------------------------------
+Statement* Context::cacheQuery(string_view id, string_view sql, bool writes)
+{
+ // Check for existing cached query [id]
+ auto i = cached_queries_.find(id);
+ if (i != cached_queries_.end())
+ {
+ i->second->tryReset();
+ return i->second.get();
+ }
+
+ // Check connection
+ if (!connection_ro_)
+ return nullptr;
+
+ // Create & add cached query
+ auto& db = writes ? *connection_rw_ : *connection_ro_;
+ auto statement = std::make_unique(db, sql);
+ auto ptr = statement.get();
+ cached_queries_.emplace(id, std::move(statement));
+
+ return ptr;
+}
+
+// -----------------------------------------------------------------------------
+// Executes an sql [query] on the database.
+// Returns the number of rows modified/created by the query, or 0 if the context
+// is not connected
+// -----------------------------------------------------------------------------
+int Context::exec(const string& query) const
+{
+ return connection_rw_ ? connection_rw_->exec(query) : 0;
+}
+int Context::exec(const char* query) const
+{
+ return connection_rw_ ? connection_rw_->exec(query) : 0;
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if a row exists in [table_name] where [id_col] = [id].
+// The column must be an integer column for this to work correctly
+// -----------------------------------------------------------------------------
+bool Context::rowIdExists(string_view table_name, int64_t id, string_view id_col) const
+{
+ auto query = fmt::format("SELECT EXISTS(SELECT 1 FROM {} WHERE {} = {})", table_name, id_col, id);
+ return connection_ro_->execAndGet(query).getInt() > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Begins a transaction and returns a Transaction object to encapsulate it
+// -----------------------------------------------------------------------------
+Transaction Context::beginTransaction(bool write) const
+{
+ return { write ? connection_rw_.get() : connection_ro_.get(), true };
+}
+
+// -----------------------------------------------------------------------------
+// Cleans up the database file to reduce size on disk
+// -----------------------------------------------------------------------------
+void Context::vacuum() const
+{
+ exec("VACUUM;");
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// Database Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Returns the 'global' database connection context for this thread.
+//
+// If this isn't being called from the main thread, it will first look for a
+// context that has previously been registered for the current thread via
+// registerThreadContext. If no context has been registered for the thread, the
+// main thread's context will be returned and a warning logged
+// -----------------------------------------------------------------------------
+Context& database::global()
+{
+ // Check if we are not on the main thread
+ if (std::this_thread::get_id() != app::mainThreadId())
+ {
+ std::shared_lock lock(mutex_thread_contexts);
+
+ // Find context for this thread
+ for (auto* thread_context : thread_contexts)
+ if (thread_context->isForThisThread())
+ return *thread_context;
+
+ // No context available for this thread, warn and use main thread context
+ // (should this throw an exception?)
+ log::warning("A non-main thread is requesting the global database connection context");
+ }
+
+ return db_global;
+}
+
+// -----------------------------------------------------------------------------
+// Sets [context] as the database connection context to use for the current
+// thread when calling database::global()
+// -----------------------------------------------------------------------------
+void database::registerThreadContext(Context& context)
+{
+ std::unique_lock lock(mutex_thread_contexts);
+
+ thread_contexts.push_back(&context);
+}
+
+// -----------------------------------------------------------------------------
+// Clears all contexts registered for the current thread
+// -----------------------------------------------------------------------------
+void database::deregisterThreadContexts()
+{
+ std::unique_lock lock(mutex_thread_contexts);
+
+ for (int i = static_cast(thread_contexts.size()) - 1; i >= 0; i--)
+ if (thread_contexts[i]->isForThisThread())
+ thread_contexts.erase(thread_contexts.begin() + i);
+}
diff --git a/src/Database/Context.h b/src/Database/Context.h
new file mode 100644
index 000000000..9b355f4a0
--- /dev/null
+++ b/src/Database/Context.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "Statement.h"
+#include
+
+namespace SQLite
+{
+class Database;
+}
+
+namespace slade::database
+{
+class Transaction;
+
+class Context
+{
+public:
+ Context(string_view file_path = {});
+ ~Context();
+
+ const string& filePath() const { return file_path_; }
+ SQLite::Database* connectionRO() const { return connection_ro_.get(); }
+ SQLite::Database* connectionRW() const { return connection_rw_.get(); }
+
+ bool isOpen() const { return connection_ro_.get(); }
+ bool isForThisThread() const;
+
+ bool open(string_view file_path);
+ bool close();
+
+ Statement* cachedQuery(string_view id);
+ Statement* cacheQuery(string_view id, string_view sql, bool writes = false);
+
+ int exec(const string& query) const;
+ int exec(const char* query) const;
+
+ bool rowIdExists(string_view table_name, int64_t id, string_view id_col = "id") const;
+
+ Transaction beginTransaction(bool write = false) const;
+
+ void vacuum() const;
+
+private:
+ string file_path_;
+ std::thread::id thread_id_;
+
+ unique_ptr connection_ro_;
+ unique_ptr connection_rw_;
+
+ std::map, std::less<>> cached_queries_;
+};
+
+// Global (config) database context
+Context& global();
+
+// Contexts for threads
+void registerThreadContext(Context& context);
+void deregisterThreadContexts();
+} // namespace slade::database
+
+// Shortcut alias for database namespace
+namespace db = slade::database;
diff --git a/src/Database/Database.cpp b/src/Database/Database.cpp
new file mode 100644
index 000000000..a39ae5142
--- /dev/null
+++ b/src/Database/Database.cpp
@@ -0,0 +1,594 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: Database.cpp
+// Description: Functions for working with the SLADE program database
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "Database.h"
+#include "App.h"
+#include "Archive/Archive.h"
+#include "Archive/ArchiveDir.h"
+#include "Archive/ArchiveEntry.h"
+#include "Context.h"
+#include "General/Console.h"
+#include "General/UI.h"
+#include "UI/State.h"
+#include "Utility/DateTime.h"
+#include "Utility/FileUtils.h"
+#include "Utility/StringUtils.h"
+#include "Utility/Tokenizer.h"
+#include
+#include
+
+using namespace slade;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::database
+{
+int db_version = 1;
+int64_t session_id = -1;
+} // namespace slade::database
+
+
+// -----------------------------------------------------------------------------
+//
+// Database Namespace Functions
+//
+// -----------------------------------------------------------------------------
+namespace slade::database
+{
+void migrateWindowLayout(string_view filename, const char* window_id)
+{
+ // Open layout file
+ Tokenizer tz;
+ if (!tz.openFile(app::path(filename, app::Dir::User)))
+ return;
+
+ // Parse layout
+ vector layouts;
+ while (true)
+ {
+ // Read component+layout pair
+ auto component = tz.getToken();
+ auto layout = tz.getToken();
+ layouts.emplace_back(component, layout);
+
+ // Check if we're done
+ if (tz.peekToken().empty())
+ break;
+ }
+
+ ui::setWindowLayout(window_id, layouts);
+}
+
+// -----------------------------------------------------------------------------
+// Creates any missing tables/views in the SLADE database [db]
+// -----------------------------------------------------------------------------
+bool createMissingTables(SQLite::Database& db)
+{
+ // Get slade.pk3 dir with table definition scripts
+ auto tables_dir = app::programResource()->dirAtPath("database/tables");
+ if (!tables_dir)
+ {
+ global::error = "Unable to initialize SLADE database: no table definitions in slade.pk3";
+ return false;
+ }
+
+ for (const auto& entry : tables_dir->entries())
+ {
+ // Check table exists
+ string table_name{ strutil::Path::fileNameOf(entry->name(), false) };
+ if (db.tableExists(table_name))
+ continue;
+
+ // Doesn't exist, create table
+ string sql{ reinterpret_cast(entry->data().data()), entry->data().size() };
+ try
+ {
+ db.exec(sql);
+ log::info("Created database table {}", table_name);
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ global::error = fmt::format("Failed to create database table {}: {}", table_name, ex.what());
+ return false;
+ }
+ }
+
+ // Get slade.pk3 dir with view definition scripts
+ auto views_dir = app::programResource()->dirAtPath("database/views");
+ if (views_dir)
+ {
+ for (const auto& entry : views_dir->entries())
+ {
+ // Check view exists
+ string view_name{ strutil::Path::fileNameOf(entry->name(), false) };
+ if (viewExists(view_name, db))
+ continue;
+
+ // Doesn't exist, create view
+ string sql{ reinterpret_cast(entry->data().data()), entry->data().size() };
+ try
+ {
+ db.exec(sql);
+ log::info("Created database view {}", view_name);
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ global::error = fmt::format("Failed to create database view {}: {}", view_name, ex.what());
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Creates and initializes a new program database file at [file_path]
+// -----------------------------------------------------------------------------
+bool createDatabase(const string& file_path)
+{
+ SQLite::Database db(file_path, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
+
+ // Create tables
+ if (!createMissingTables(db))
+ return false;
+
+ // Init db_info table
+ try
+ {
+ Statement sql{ db, "INSERT INTO db_info (version) VALUES (?)" };
+ sql.bind(1, db_version);
+ sql.exec();
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ global::error = fmt::format("Failed to initialize database: {}", ex.what());
+ log::error(global::error);
+ return false;
+ }
+
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Updates the program database tables
+// -----------------------------------------------------------------------------
+bool updateDatabase(int prev_version)
+{
+ log::info("Updating database from v{} to v{}...", prev_version, db_version);
+
+ // Create missing tables
+ if (!createMissingTables(*connectionRW()))
+ return false;
+
+ // Update db_info.version
+ exec(fmt::format("UPDATE db_info SET version = {}", db_version));
+
+ // Done
+ log::info("Database updated to v{} successfully", db_version);
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Writes a new session row to the database
+// -----------------------------------------------------------------------------
+bool beginSession()
+{
+ try
+ {
+ Statement sql_new_session{ *connectionRW(),
+ "INSERT INTO session (opened_time, closed_time, version_major, version_minor, "
+ "version_revision, version_beta) "
+ "VALUES (?,?,?,?,?,?)" };
+
+ sql_new_session.bindDateTime(1, datetime::now());
+ sql_new_session.bind(2); // NULL closed time
+ sql_new_session.bind(3, static_cast(app::version().major));
+ sql_new_session.bind(4, static_cast(app::version().minor));
+ sql_new_session.bind(5, static_cast(app::version().revision));
+ sql_new_session.bind(6, static_cast(app::version().beta));
+
+ if (sql_new_session.exec() == 0)
+ {
+ global::error = "Failed to initialize database: Session row creation failed";
+ return false;
+ }
+
+ session_id = connectionRO()->execAndGet("SELECT MAX(id) FROM session").getInt64();
+
+ return true;
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ global::error = fmt::format("Failed to initialize database: {}", ex.what());
+ log::error(global::error);
+ return false;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Sets the closed time in the current session row
+// -----------------------------------------------------------------------------
+bool endSession()
+{
+ try
+ {
+ Statement sql_close_session{ *connectionRW(), "UPDATE session SET closed_time = ? WHERE id = ?" };
+ sql_close_session.bindDateTime(1, datetime::now());
+ sql_close_session.bind(2, session_id);
+ sql_close_session.exec();
+
+ session_id = -1;
+
+ return true;
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error(ex.what());
+ return false;
+ }
+}
+} // namespace slade::database
+
+// -----------------------------------------------------------------------------
+// Executes an sql [query] on the database using the given [connection].
+// If [connection] is null, the global read+write connection is used.
+// Returns the number of rows modified/created by the query, or 0 if the global
+// connection context is not connected
+// -----------------------------------------------------------------------------
+int database::exec(const string& query, SQLite::Database* connection)
+{
+ if (!connection)
+ connection = connectionRW();
+ if (!connection)
+ return 0;
+
+ return connection->exec(query);
+}
+int database::exec(const char* query, SQLite::Database* connection)
+{
+ if (!connection)
+ connection = connectionRW();
+ if (!connection)
+ return 0;
+
+ return connection->exec(query);
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if a view with [view_name] exists in the database [connection]
+// -----------------------------------------------------------------------------
+bool database::viewExists(string_view view_name, const SQLite::Database& connection)
+{
+ Statement query(connection, "SELECT count(*) FROM sqlite_master WHERE type='view' AND name=?");
+ query.bind(1, view_name);
+ (void)query.executeStep(); // Cannot return false, as the above query always return a result
+ return (1 == query.getColumn(0).getInt());
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if a row exists in [table_name] where [id_col] = [id].
+// The column must be an integer column for this to work correctly
+// -----------------------------------------------------------------------------
+bool database::rowIdExists(string_view table_name, int64_t id, string_view id_col)
+{
+ return global().rowIdExists(table_name, id, id_col);
+}
+
+// -----------------------------------------------------------------------------
+// Returns the cached query [id] if it exists, otherwise creates a new cached
+// query from the given [sql] string and returns it.
+// If [writes] is true, the created query will use the read+write connection.
+// -----------------------------------------------------------------------------
+database::Statement* database::cacheQuery(string_view id, string_view sql, bool writes)
+{
+ return global().cacheQuery(id, sql, writes);
+}
+
+// -----------------------------------------------------------------------------
+// Returns the read-only connection to the database (for the calling thread)
+// -----------------------------------------------------------------------------
+SQLite::Database* database::connectionRO()
+{
+ return global().connectionRO();
+}
+
+// -----------------------------------------------------------------------------
+// Returns the read+write connection to the database (for the calling thread)
+// -----------------------------------------------------------------------------
+SQLite::Database* database::connectionRW()
+{
+ return global().connectionRW();
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if the program database file exists
+// -----------------------------------------------------------------------------
+bool database::fileExists()
+{
+ return fileutil::fileExists(app::path("slade.sqlite", app::Dir::User));
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if a transaction (BEGIN -> COMMIT/ROLLBACK) is currently active
+// on [connection]
+// -----------------------------------------------------------------------------
+bool database::isTransactionActive(const SQLite::Database* connection)
+{
+ return sqlite3_get_autocommit(connection->getHandle()) == 0;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the path to the program database file
+// -----------------------------------------------------------------------------
+string database::programDatabasePath()
+{
+ return app::path("slade.sqlite", app::Dir::User);
+}
+
+// -----------------------------------------------------------------------------
+// Returns the current database session id
+// -----------------------------------------------------------------------------
+int64_t database::sessionId()
+{
+ return session_id;
+}
+
+// -----------------------------------------------------------------------------
+// Initialises the program database, creating it if it doesn't exist and opening
+// the 'global' connection context.
+// Returns false if the database couldn't be created or the global context
+// failed to open, true otherwise
+// -----------------------------------------------------------------------------
+bool database::init()
+{
+ auto db_path = app::path("slade.sqlite", app::Dir::User);
+
+ // Create database if needed
+ bool created = false;
+ if (!fileutil::fileExists(db_path))
+ {
+ if (!createDatabase(db_path))
+ return false;
+
+ created = true;
+ }
+
+ // Open global connections to database (for main thread usage only)
+ if (!global().open(db_path))
+ {
+ global::error = "Unable to open global database connections";
+ return false;
+ }
+
+ // Migrate pre-3.3.0 config stuff to database
+ if (created)
+ migrateConfigs();
+
+ // Update the database if needed
+ auto existing_version = connectionRO()->execAndGet("SELECT version FROM db_info").getInt();
+ if (existing_version < db_version)
+ return updateDatabase(existing_version);
+
+ // Write new session
+ if (!beginSession())
+ return false;
+
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Closes the global connection context to the database
+// -----------------------------------------------------------------------------
+void database::close()
+{
+ endSession();
+ global().vacuum(); // Shrink size on disk
+ global().close();
+}
+
+// -----------------------------------------------------------------------------
+// Migrates various configurations from text/cfg files (pre-3.3.0) to the
+// SLADE program database
+// -----------------------------------------------------------------------------
+void database::migrateConfigs()
+{
+#define MIGRATE_CVAR_BOOL(cvar, state) else if (tz.check(#cvar)) ui::saveStateBool(#state, tz.peek().asBool())
+#define MIGRATE_CVAR_INT(cvar, state) else if (tz.check(#cvar)) ui::saveStateInt(#state, tz.peek().asInt())
+#define MIGRATE_CVAR_STRING(cvar, state) else if (tz.check(#cvar)) ui::saveStateString(#state, tz.peek().text)
+
+ // Migrate window layouts from .layout files
+ migrateWindowLayout("mainwindow.layout", "main");
+ migrateWindowLayout("mapwindow.layout", "map");
+ migrateWindowLayout("scriptmanager.layout", "scriptmanager");
+
+ // Migrate various things from SLADE.cfg
+ Tokenizer tz;
+ if (!tz.openFile(app::path("slade3.cfg", app::Dir::User)))
+ return;
+ while (!tz.atEnd())
+ {
+ // Migrate old CVars to UI state table
+ if (tz.advIf("cvars", 2))
+ {
+ while (!tz.checkOrEnd("}"))
+ {
+ // Last archive format
+ if (tz.check("archive_last_created_format"))
+ ui::saveStateString("ArchiveLastCreatedFormat", tz.peek().text);
+
+ // Window maximized flags
+ MIGRATE_CVAR_BOOL(browser_maximised, BrowserWindowMaximized);
+ MIGRATE_CVAR_BOOL(mw_maximized, MainWindowMaximized);
+ MIGRATE_CVAR_BOOL(mew_maximized, MapEditorWindowMaximized);
+ MIGRATE_CVAR_BOOL(sm_maximized, ScriptManagerWindowMaximized);
+
+ // Entry list column widths
+ MIGRATE_CVAR_INT(elist_colsize_index, EntryListIndexWidth);
+ MIGRATE_CVAR_INT(elist_colsize_size, EntryListSizeWidth);
+ MIGRATE_CVAR_INT(elist_colsize_type, EntryListTypeWidth);
+ MIGRATE_CVAR_INT(elist_colsize_name_list, EntryListNameWidthList);
+ MIGRATE_CVAR_INT(elist_colsize_name_tree, EntryListNameWidthTree);
+
+ // Entry list column visibility
+ MIGRATE_CVAR_BOOL(elist_colindex_show, EntryListIndexVisible);
+ MIGRATE_CVAR_BOOL(elist_colsize_show, EntryListSizeVisible);
+ MIGRATE_CVAR_BOOL(elist_coltype_show, EntryListTypeVisible);
+
+ // Splitter position
+ MIGRATE_CVAR_INT(ap_splitter_position_list, ArchivePanelSplitPosList);
+ MIGRATE_CVAR_INT(ap_splitter_position_tree, ArchivePanelSplitPosTree);
+
+ // Colourize/Tint Dialogs
+ MIGRATE_CVAR_STRING(last_colour, ColouriseDialogLastColour);
+ MIGRATE_CVAR_STRING(last_tint_colour, TintDialogLastColour);
+ MIGRATE_CVAR_INT(last_tint_amount, TintDialogLastAmount);
+
+ // Zoom sliders
+ MIGRATE_CVAR_INT(zoom_gfx, ZoomGfxCanvas);
+ MIGRATE_CVAR_INT(zoom_ctex, ZoomCTextureCanvas);
+
+ // Misc.
+ MIGRATE_CVAR_BOOL(setup_wizard_run, SetupWizardRun);
+
+ tz.adv(2);
+ }
+
+ tz.adv(); // Skip ending }
+ }
+
+ // Migrate window size/position info
+ if (tz.advIf("window_info", 2))
+ {
+ tz.advIf("{");
+ while (!tz.check("}") && !tz.atEnd())
+ {
+ auto id = tz.current().text;
+ int width = tz.next().asInt();
+ int height = tz.next().asInt();
+ int left = tz.next().asInt();
+ int top = tz.next().asInt();
+ ui::setWindowInfo(id.c_str(), width, height, left, top);
+ tz.adv();
+ }
+ }
+
+ // Next token
+ tz.adv();
+ }
+#undef MIGRATE_CVAR_BOOL
+#undef MIGRATE_CVAR_INT
+#undef MIGRATE_CVAR_STRING
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// Console Commands
+//
+// -----------------------------------------------------------------------------
+
+
+void c_db(const vector& args);
+ConsoleCommand db_cmd("db", &c_db, 1, false);
+void c_db(const vector& args)
+{
+ const auto& command = args[0];
+
+ try
+ {
+ // List tables
+ if (command == "tables")
+ {
+ if (auto db = database::connectionRO())
+ {
+ SQLite::Statement sql(*db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name");
+ while (sql.executeStep())
+ log::console(sql.getColumn(0).getString());
+ }
+ }
+
+ // Row count of table
+ else if (command == "rowcount")
+ {
+ if (args.size() < 2)
+ {
+ log::console("No table name given. Usage: db rowcount ");
+ return;
+ }
+
+ if (auto db = database::connectionRO())
+ {
+ SQLite::Statement sql(*db, fmt::format("SELECT COUNT(*) FROM {}", args[1]));
+ if (sql.executeStep())
+ log::console("{} rows", sql.getColumn(0).getInt());
+ else
+ log::console("No such table");
+ }
+ }
+
+ // Reset table from template
+ else if (command == "reset")
+ {
+ if (args.size() < 2)
+ {
+ log::console("No table name given. Usage: db reset ");
+ return;
+ }
+
+ const auto& table = args[1];
+
+ if (auto db = database::connectionRW())
+ {
+ auto sql_entry = app::programResource()->entryAtPath(fmt::format("database/tables/{}.sql", table));
+ if (!sql_entry)
+ {
+ log::console("Can't find table sql script for {}", table);
+ return;
+ }
+
+ string sql{ reinterpret_cast(sql_entry->data().data()), sql_entry->data().size() };
+ db->exec(fmt::format("DROP TABLE IF EXISTS {}", table));
+ db->exec(sql);
+ log::console("Table {} recreated and reset to default", table);
+ }
+ }
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error(ex.what());
+ }
+}
diff --git a/src/Database/Database.h b/src/Database/Database.h
new file mode 100644
index 000000000..9ccca2502
--- /dev/null
+++ b/src/Database/Database.h
@@ -0,0 +1,33 @@
+#pragma once
+
+namespace SQLite
+{
+class Database;
+}
+
+namespace slade::database
+{
+class Statement;
+class Transaction;
+
+// Helpers
+bool isTransactionActive(const SQLite::Database* connection);
+int exec(const string& query, SQLite::Database* connection = nullptr);
+int exec(const char* query, SQLite::Database* connection = nullptr);
+bool viewExists(string_view view_name, const SQLite::Database& connection);
+bool rowIdExists(string_view table_name, int64_t id, string_view id_col = "id");
+Statement* cacheQuery(string_view id, string_view sql, bool writes = false);
+SQLite::Database* connectionRO();
+SQLite::Database* connectionRW();
+
+// General
+bool fileExists();
+string programDatabasePath();
+int64_t sessionId();
+bool init();
+void close();
+void migrateConfigs();
+} // namespace slade::database
+
+// Shortcut alias for database namespace
+namespace db = slade::database;
diff --git a/src/Database/DbUtils.h b/src/Database/DbUtils.h
new file mode 100644
index 000000000..a33282ddf
--- /dev/null
+++ b/src/Database/DbUtils.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "Statement.h"
+
+namespace slade::database
+{
+template bool rowExists(SQLite::Database& connection, string_view table_name, string_view col_name, T value)
+{
+ Statement sql(connection, fmt::format("SELECT * FROM {} WHERE {} = ?", table_name, col_name));
+ sql.bind(1, value);
+ return sql.executeStep();
+}
+} // namespace slade::database
diff --git a/src/Database/Statement.h b/src/Database/Statement.h
new file mode 100644
index 000000000..2cbf546fa
--- /dev/null
+++ b/src/Database/Statement.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+
+namespace slade::database
+{
+// Wrapper of SQLiteCpp Statement to handle string_view
+class Statement : public SQLite::Statement
+{
+public:
+ Statement(const SQLite::Database& db, string_view query) :
+ SQLite::Statement(db, string{ query.data(), query.size() })
+ {
+ }
+
+ void bind(int index, int32_t value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, uint32_t value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, int64_t value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, double value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, const string& value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, const char* value) { SQLite::Statement::bind(index, value); }
+ void bind(int index, string_view value) { SQLite::Statement::bind(index, string{ value.data(), value.size() }); }
+ void bind(int index, const void* value, int size) { SQLite::Statement::bind(index, value, size); }
+ void bind(int index) { SQLite::Statement::bind(index); }
+
+ // Needed to avoid ambiguous call error on some systems for time_t
+ void bindDateTime(int index, time_t value) { SQLite::Statement::bind(index, static_cast(value)); }
+
+ void bindNoCopy(int index, const string& value) { SQLite::Statement::bindNoCopy(index, value); }
+ void bindNoCopy(int index, const char* value) { SQLite::Statement::bindNoCopy(index, value); }
+ void bindNoCopy(int index, string_view value)
+ {
+ SQLite::Statement::bindNoCopy(index, string{ value.data(), value.size() });
+ }
+ void bindNoCopy(int index, const void* value, int size) { SQLite::Statement::bindNoCopy(index, value, size); }
+};
+} // namespace slade::database
diff --git a/src/Database/Transaction.cpp b/src/Database/Transaction.cpp
new file mode 100644
index 000000000..f2cd8e13e
--- /dev/null
+++ b/src/Database/Transaction.cpp
@@ -0,0 +1,123 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: Transaction.cpp
+// Description: Encapsulates a single SQL transaction, ensuring it's closed off
+// properly etc. via RAII
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "Transaction.h"
+#include "Database.h"
+#include
+
+using namespace slade;
+using namespace database;
+
+
+// -----------------------------------------------------------------------------
+//
+// Transaction Class Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Transaction class constructor
+// -----------------------------------------------------------------------------
+Transaction::Transaction(SQLite::Database* connection, bool begin) : connection_{ connection }, has_begun_{ begin }
+{
+ if (begin)
+ connection->exec("BEGIN");
+}
+
+// -----------------------------------------------------------------------------
+// Transaction class move constructor
+// -----------------------------------------------------------------------------
+Transaction::Transaction(Transaction&& other) noexcept :
+ connection_{ other.connection_ },
+ has_begun_{ other.has_begun_ },
+ has_ended_{ other.has_ended_ }
+{
+ other.connection_ = nullptr;
+ other.has_begun_ = true;
+ other.has_ended_ = true;
+}
+
+// -----------------------------------------------------------------------------
+// Transaction class destructor
+// -----------------------------------------------------------------------------
+Transaction::~Transaction()
+{
+ if (has_begun_ && !has_ended_)
+ connection_->exec("ROLLBACK");
+}
+
+// -----------------------------------------------------------------------------
+// Begins the transaction if it isn't active already
+// -----------------------------------------------------------------------------
+void Transaction::begin()
+{
+ if (has_begun_)
+ return;
+
+ connection_->exec("BEGIN");
+ has_begun_ = true;
+}
+
+// -----------------------------------------------------------------------------
+// Begins the transaction if there are no currently active transactions on the
+// connection
+// -----------------------------------------------------------------------------
+void Transaction::beginIfNoActiveTransaction()
+{
+ if (!isTransactionActive(connection_))
+ begin();
+}
+
+// -----------------------------------------------------------------------------
+// Commits the transaction
+// -----------------------------------------------------------------------------
+void Transaction::commit()
+{
+ if (!has_begun_ || has_ended_)
+ return;
+
+ connection_->exec("COMMIT");
+ has_ended_ = true;
+}
+
+// -----------------------------------------------------------------------------
+// Rolls the transaction back
+// -----------------------------------------------------------------------------
+void Transaction::rollback()
+{
+ if (!has_begun_ || has_ended_)
+ return;
+
+ connection_->exec("ROLLBACK");
+ has_ended_ = true;
+}
diff --git a/src/Database/Transaction.h b/src/Database/Transaction.h
new file mode 100644
index 000000000..e65a63e6c
--- /dev/null
+++ b/src/Database/Transaction.h
@@ -0,0 +1,30 @@
+#pragma once
+
+namespace SQLite
+{
+class Database;
+}
+
+namespace slade::database
+{
+class Transaction
+{
+public:
+ Transaction(SQLite::Database* connection, bool begin = true);
+ Transaction(Transaction&& other) noexcept;
+ ~Transaction();
+
+ Transaction(const Transaction&) = delete;
+ Transaction& operator=(const Transaction&) = delete;
+
+ void begin();
+ void beginIfNoActiveTransaction();
+ void commit();
+ void rollback();
+
+private:
+ SQLite::Database* connection_ = nullptr;
+ bool has_begun_ = false;
+ bool has_ended_ = false;
+};
+} // namespace slade::database
diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp
index b40d92fdc..b40fb87c1 100644
--- a/src/Game/Game.cpp
+++ b/src/Game/Game.cpp
@@ -419,7 +419,7 @@ void game::init()
[=]()
{
Archive zdoom_pk3(ArchiveFormat::Zip);
- if (!zdoom_pk3.open(zdoom_pk3_path))
+ if (!zdoom_pk3.open(zdoom_pk3_path, true))
return;
// ZScript
diff --git a/src/General/CVar.cpp b/src/General/CVar.cpp
index 3f00dcb34..04b52cda4 100644
--- a/src/General/CVar.cpp
+++ b/src/General/CVar.cpp
@@ -199,7 +199,7 @@ string CVar::writeAll()
}
}
- format_to(buf, "}}\n\n");
+ format_to(buf, "}}\n");
return to_string(mem_buf);
}
diff --git a/src/General/Log.h b/src/General/Log.h
index 83eb4a159..97b60b933 100644
--- a/src/General/Log.h
+++ b/src/General/Log.h
@@ -112,6 +112,15 @@ template void error(string_view text, const Args&... args)
message(MessageType::Error, text, fmt::make_format_args(args...));
}
+template void console(int level, string_view text, const Args&... args)
+{
+ message(MessageType::Console, level, text, fmt::make_format_args(args...));
+}
+template void console(string_view text, const Args&... args)
+{
+ message(MessageType::Console, text, fmt::make_format_args(args...));
+}
+
template void debug(int level, string_view text, const Args&... args)
{
if (global::debug)
diff --git a/src/General/MapPreviewData.cpp b/src/General/MapPreviewData.cpp
index 6a4fbbb6c..70fe8d97e 100644
--- a/src/General/MapPreviewData.cpp
+++ b/src/General/MapPreviewData.cpp
@@ -344,7 +344,7 @@ bool MapPreviewData::openMap(MapDesc map)
// Attempt to open entry as wad archive
temp_archive = std::make_unique(ArchiveFormat::Wad);
- if (!temp_archive->open(m_head->data()))
+ if (!temp_archive->open(m_head->data(), true))
{
return false;
}
diff --git a/src/General/Misc.cpp b/src/General/Misc.cpp
index 1c2980127..a5ac46546 100644
--- a/src/General/Misc.cpp
+++ b/src/General/Misc.cpp
@@ -40,7 +40,6 @@
#include "Graphics/SImage/SImage.h"
#include "Utility/PropertyList.h"
#include "Utility/StringUtils.h"
-#include "Utility/Tokenizer.h"
using namespace slade;
@@ -54,10 +53,6 @@ CVAR(Bool, size_as_string, true, CVar::Flag::Save)
CVAR(Bool, percent_encoding, false, CVar::Flag::Save)
EXTERN_CVAR(Float, col_cie_tristim_x)
EXTERN_CVAR(Float, col_cie_tristim_z)
-namespace slade::misc
-{
-vector window_info;
-}
// -----------------------------------------------------------------------------
@@ -552,72 +547,3 @@ Vec2i misc::findJaguarTextureDimensions(const ArchiveEntry* entry, string_view n
// We didn't find the texture
return dimensions;
}
-
-// -----------------------------------------------------------------------------
-// Gets the saved window info for [id]
-// -----------------------------------------------------------------------------
-misc::WindowInfo misc::getWindowInfo(string_view id)
-{
- for (auto& a : window_info)
- {
- if (a.id == id)
- return a;
- }
-
- return WindowInfo("", -1, -1, -1, -1);
-}
-
-// -----------------------------------------------------------------------------
-// Sets the saved window info for [id]
-// -----------------------------------------------------------------------------
-void misc::setWindowInfo(string_view id, int width, int height, int left, int top)
-{
- if (id.empty())
- return;
-
- for (auto& a : window_info)
- {
- if (a.id == id)
- {
- if (width >= -1)
- a.width = width;
- if (height >= -1)
- a.height = height;
- if (left >= -1)
- a.left = left;
- if (top >= -1)
- a.top = top;
- return;
- }
- }
-
- window_info.emplace_back(id, width, height, left, top);
-}
-
-// -----------------------------------------------------------------------------
-// Reads saved window info from tokenizer [tz]
-// -----------------------------------------------------------------------------
-void misc::readWindowInfo(Tokenizer& tz)
-{
- // Read definitions
- tz.advIf("{");
- while (!tz.check("}") && !tz.atEnd())
- {
- auto id = tz.current().text;
- int width = tz.next().asInt();
- int height = tz.next().asInt();
- int left = tz.next().asInt();
- int top = tz.next().asInt();
- setWindowInfo(id, width, height, left, top);
- tz.adv();
- }
-}
-
-// -----------------------------------------------------------------------------
-// Writes all saved window info to [file]
-// -----------------------------------------------------------------------------
-void misc::writeWindowInfo(wxFile& file)
-{
- for (auto& a : window_info)
- file.Write(wxString::Format("\t%s %d %d %d %d\n", a.id, a.width, a.height, a.left, a.top));
-}
diff --git a/src/General/Misc.h b/src/General/Misc.h
index 9b260eba9..6d2aaea0f 100644
--- a/src/General/Misc.h
+++ b/src/General/Misc.h
@@ -36,19 +36,5 @@ namespace misc
// Mass Rename
string massRenameFilter(const vector& names);
void doMassRename(vector& names, string_view name_filter);
-
- // Dialog/Window sizes
- struct WindowInfo
- {
- string id;
- int width, height, left, top;
- WindowInfo(string_view id, int w, int h, int l, int t) : id{ id }, width{ w }, height{ h }, left{ l }, top{ t }
- {
- }
- };
- WindowInfo getWindowInfo(string_view id);
- void setWindowInfo(string_view id, int width, int height, int left, int top);
- void readWindowInfo(Tokenizer& tz);
- void writeWindowInfo(wxFile& file);
} // namespace misc
} // namespace slade
diff --git a/src/General/ResourceManager.cpp b/src/General/ResourceManager.cpp
index 021a689a8..b12d71a7b 100644
--- a/src/General/ResourceManager.cpp
+++ b/src/General/ResourceManager.cpp
@@ -356,7 +356,7 @@ void ResourceManager::addEntry(shared_ptr& entry)
strutil::upperIP(path);
path.erase(0, 1);
- log::debug("Adding entry {} to resource manager", path);
+ //log::debug("Adding entry {} to resource manager", path);
// Check for palette entry
if (type->id() == "palette")
@@ -483,7 +483,7 @@ void ResourceManager::removeEntry(ArchiveEntry* entry, string_view entry_name, b
strutil::upperIP(path);
path.erase(0, 1);
- log::debug("Removing entry {} from resource manager", path);
+ //log::debug("Removing entry {} from resource manager", path);
// Remove from palettes
removeEntryFromMap(palettes_, name, entry, full_check);
diff --git a/src/General/UI.cpp b/src/General/UI.cpp
index 2f9c25d99..d394abe9b 100644
--- a/src/General/UI.cpp
+++ b/src/General/UI.cpp
@@ -32,9 +32,14 @@
#include "Main.h"
#include "UI.h"
#include "App.h"
+#include "Database/Context.h"
+#include "Database/Transaction.h"
#include "General/Console.h"
#include "UI/SplashWindow.h"
+#include "UI/State.h"
#include "Utility/StringUtils.h"
+#include
+#include
using namespace slade;
@@ -77,7 +82,8 @@ bool isMainThread()
} // namespace slade::ui
// -----------------------------------------------------------------------------
-// Initialises UI metric values based on [scale]
+// Initialises UI metric values based on [scale] and other various UI related
+// things
// -----------------------------------------------------------------------------
void ui::init(double scale)
{
@@ -87,6 +93,7 @@ void ui::init(double scale)
scale = splash_window->GetDPIScaleFactor();
#endif
+ // Set scale + metrics
ui::scale = scale;
px_pad_small = 8 * scale;
px_pad = 12 * scale;
@@ -98,6 +105,9 @@ void ui::init(double scale)
px_spin_width = 64 * scale;
SplashWindow::init();
+
+ // Init saved state props
+ initStateProps();
}
// -----------------------------------------------------------------------------
@@ -277,6 +287,126 @@ int ui::padMin()
return px_pad_min;
}
+// -----------------------------------------------------------------------------
+// Returns the saved window info for window/dialog [id]
+// -----------------------------------------------------------------------------
+ui::WindowInfo ui::getWindowInfo(const char* id)
+{
+ WindowInfo inf{ {}, 0, 0, 0, 0 };
+
+ try
+ {
+ if (auto* sql = database::global().cacheQuery(
+ "get_window_info", "SELECT left, top, width, height FROM window_info WHERE window_id = ?"))
+ {
+ sql->bind(1, id);
+ if (sql->executeStep())
+ {
+ inf.id = id;
+ inf.left = sql->getColumn(0).getInt();
+ inf.top = sql->getColumn(1).getInt();
+ inf.width = sql->getColumn(2).getInt();
+ inf.height = sql->getColumn(3).getInt();
+ }
+
+ sql->reset();
+ }
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error("Error getting window info for \"{}\": {}", id, ex.what());
+ }
+
+ return inf;
+}
+
+// -----------------------------------------------------------------------------
+// Saves the window info for window/dialog [id]
+// -----------------------------------------------------------------------------
+void ui::setWindowInfo(const char* id, int width, int height, int left, int top)
+{
+ try
+ {
+ if (auto sql = database::global().cacheQuery(
+ "set_window_info",
+ "REPLACE INTO window_info (window_id, left, top, width, height) "
+ "VALUES (?,?,?,?,?)",
+ true))
+ {
+ sql->clearBindings();
+ sql->bind(1, id);
+ sql->bind(2, left);
+ sql->bind(3, top);
+ sql->bind(4, width);
+ sql->bind(5, height);
+
+ sql->exec();
+ sql->reset();
+ }
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error("Error writing window info for \"{}\": {}", id, ex.what());
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Returns the saved window AUI layout for window [id]
+// -----------------------------------------------------------------------------
+vector ui::getWindowLayout(const char* id)
+{
+ vector layout;
+
+ try
+ {
+ if (auto sql = database::global().cacheQuery(
+ "get_window_layout", "SELECT component, layout FROM window_layout WHERE window_id = ?"))
+ {
+ sql->bind(1, id);
+ while (sql->executeStep())
+ layout.emplace_back(sql->getColumn(0).getString(), sql->getColumn(1).getString());
+ sql->reset();
+ }
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error("Error getting window layout for \"{}\": {}", id, ex.what());
+ }
+
+ return layout;
+}
+
+// -----------------------------------------------------------------------------
+// Saves the AUI layout for window [id]
+// -----------------------------------------------------------------------------
+void ui::setWindowLayout(const char* id, const vector& layout)
+{
+ try
+ {
+ auto transaction = database::global().beginTransaction(true);
+
+ if (auto sql = database::global().cacheQuery(
+ "set_window_layout", "REPLACE INTO window_layout VALUES (?, ?, ?)", true))
+ {
+ sql->clearBindings();
+ sql->bind(1, id);
+ for (const auto& row : layout)
+ {
+ sql->bind(2, row.first);
+ sql->bind(3, row.second);
+ sql->exec();
+ sql->reset();
+ }
+ }
+
+ transaction.commit();
+ }
+ catch (const SQLite::Exception& ex)
+ {
+ log::error("Error writing window layout for \"{}\": {}", id, ex.what());
+ }
+}
+
// -----------------------------------------------------------------------------
//
diff --git a/src/General/UI.h b/src/General/UI.h
index a1ddd7a5c..bf3192ae7 100644
--- a/src/General/UI.h
+++ b/src/General/UI.h
@@ -1,5 +1,7 @@
#pragma once
+#include "Utility/StringPair.h"
+
namespace slade::ui
{
// General
@@ -47,4 +49,15 @@ int pad(); // Shortcut for ui::px(UI::Size::Pad)
int padLarge(); // Shortcut for ui::px(UI::Size::PadLarge)
int padMin(); // Shortcut for ui::px(ui::Size::PadMinimum)
+// Window size, position & layout persistence
+struct WindowInfo
+{
+ string id;
+ int left, top, width, height;
+};
+WindowInfo getWindowInfo(const char* id);
+void setWindowInfo(const char* id, int width, int height, int left, int top);
+vector getWindowLayout(const char* id);
+void setWindowLayout(const char* id, const vector& layout);
+
} // namespace slade::ui
diff --git a/src/Graphics/Icons.cpp b/src/Graphics/Icons.cpp
index 88f6b74f4..384a40d87 100644
--- a/src/Graphics/Icons.cpp
+++ b/src/Graphics/Icons.cpp
@@ -576,7 +576,7 @@ wxBitmap icons::getInterfaceIcon(string_view name, int size, InterfaceTheme them
}
// Get icon definition
- auto& icon_set = dark ? iconset_ui_dark : iconset_ui_light;
+ auto& icon_set = dark ? iconset_ui_dark : iconset_ui_light;
IconDef* icon_def = nullptr;
if (auto i = icon_set.icons.find(name); i != icon_set.icons.end())
icon_def = &i->second;
@@ -623,3 +623,26 @@ bool icons::iconExists(Type type, string_view name)
{
return iconDef(type, name) != nullptr;
}
+
+
+// -----------------------------------------------------------------------------
+//
+// IconCache Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Adds an icon to the cache
+// -----------------------------------------------------------------------------
+void icons::IconCache::cacheIcon(Type type, const string& name, int size, Point2i padding)
+{
+#if wxCHECK_VERSION(3, 1, 6)
+ const auto bundle = getIcon(type, name, size, padding);
+ icons[name] = bundle;
+#else
+ const auto bmp = icons::getIcon(type, name, ui::scalePx(size), padding);
+ wxIcon icon;
+ icon.CopyFromBitmap(bmp);
+ icons[name] = icon;
+#endif
+}
diff --git a/src/Graphics/Icons.h b/src/Graphics/Icons.h
index a99e35a19..487410653 100644
--- a/src/Graphics/Icons.h
+++ b/src/Graphics/Icons.h
@@ -1,5 +1,7 @@
#pragma once
+#include
+
namespace slade
{
namespace icons
@@ -31,5 +33,17 @@ namespace icons
vector iconSets(Type type);
bool iconExists(Type type, string_view name);
+
+ struct IconCache
+ {
+#if wxCHECK_VERSION(3, 1, 6)
+ std::unordered_map icons;
+#else
+ std::unordered_map icons;
+#endif
+
+ bool isCached(const string& name) { return icons.find(name) != icons.end(); }
+ void cacheIcon(Type type, const string& name, int size, Point2i padding = {});
+ };
} // namespace icons
} // namespace slade
diff --git a/src/Library/ArchiveEntry.cpp b/src/Library/ArchiveEntry.cpp
new file mode 100644
index 000000000..e3400859e
--- /dev/null
+++ b/src/Library/ArchiveEntry.cpp
@@ -0,0 +1,681 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveEntry.cpp
+// Description: ArchiveEntryRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "Archive/ArchiveEntry.h"
+#include "App.h"
+#include "Archive/EntryType/EntryType.h"
+#include "ArchiveEntry.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include "Database/Transaction.h"
+#include
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+string update_archive_entry =
+ "UPDATE archive_entry "
+ "SET path = ?, [index] = ?, name = ?, size = ?, hash = ?, type_id = ? "
+ "WHERE archive_id = ? AND id = ?";
+string insert_archive_entry =
+ "INSERT INTO archive_entry (archive_id, id, path, [index], name, size, hash, type_id) "
+ "VALUES (?,?,?,?,?,?,?,?)";
+string copy_archive_entries =
+ "INSERT INTO archive_entry (archive_id, id, path, [index], name, size, hash, type_id) "
+ " SELECT ?, id, path, [index], name, size, hash, type_id "
+ " FROM archive_entry WHERE archive_id = ?";
+string insert_archive_entry_property = "INSERT INTO archive_entry_property VALUES (?,?,?,?,?)";
+string get_archive_entry_properties =
+ "SELECT key, value_type, value FROM archive_entry_property WHERE archive_id = ? AND entry_id = ?";
+
+vector saved_ex_props = { "TextPosition", "TextLanguage" };
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveEntryRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveEntryRow constructor
+// Initializes the row for [archive_id]+[id] with info from [entry]
+// -----------------------------------------------------------------------------
+ArchiveEntryRow::ArchiveEntryRow(int64_t archive_id, int64_t id, const ArchiveEntry& entry) :
+ archive_id{ archive_id },
+ id{ id },
+ path{ entry.path() },
+ index{ entry.index() },
+ name{ entry.name() },
+ size{ entry.size() },
+ hash{ entry.hash() },
+ type_id{ entry.type()->id() }
+{
+}
+
+// -----------------------------------------------------------------------------
+// ArchiveEntryRow constructor
+// Reads existing data from the database. If row [archive_id]+[id] doesn't exist
+// in the database, the row id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveEntryRow::ArchiveEntryRow(db::Context& db, int64_t archive_id, int64_t id) : archive_id{ archive_id }, id{ id }
+{
+ if (auto sql = db.cacheQuery("get_archive_entry", "SELECT * FROM archive_entry WHERE archive_id = ? AND id = ?"))
+ {
+ sql->clearBindings();
+ sql->bind(1, archive_id);
+ sql->bind(2, id);
+
+ if (sql->executeStep())
+ {
+ path = sql->getColumn(2).getString();
+ index = sql->getColumn(3).getInt();
+ name = sql->getColumn(4).getString();
+ size = sql->getColumn(5).getUInt();
+ hash = sql->getColumn(6).getString();
+ type_id = sql->getColumn(7).getString();
+ }
+ else
+ {
+ log::warning("archive_entry row with archive_id {}, id {} does not exist in the database", archive_id, id);
+ this->id = -1;
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, the inserted row id is returned, otherwise returns -1.
+// (note the returned row id isn't the 'id' column since that is not a primary
+// key, it returns the sqlite rowid of the row)
+// -----------------------------------------------------------------------------
+int64_t ArchiveEntryRow::insert() const
+{
+ if (id >= 0)
+ {
+ log::warning("Trying to insert archive_entry id {} that already exists", id);
+ return id;
+ }
+
+ int64_t row_id = -1;
+ if (auto sql = db::cacheQuery("insert_archive_entry", insert_archive_entry, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, archive_id);
+ sql->bind(2, id);
+ sql->bind(3, path);
+ sql->bind(4, index);
+ sql->bind(5, name);
+ sql->bind(6, size);
+ sql->bind(7, hash);
+ sql->bind(8, type_id);
+
+ if (sql->exec() > 0)
+ row_id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ return row_id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveEntryRow::update() const
+{
+ // Ignore invalid id
+ if (id < 0)
+ {
+ log::warning("Trying to update archive_entry row with no id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("update_archive_entry", update_archive_entry, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, path);
+ sql->bind(2, index);
+ sql->bind(3, name);
+ sql->bind(4, size);
+ sql->bind(5, hash);
+ sql->bind(6, type_id);
+ sql->bind(7, archive_id);
+ sql->bind(8, id);
+
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ return rows > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveEntryRow::remove()
+{
+ // Ignore invalid id
+ if (id < 0)
+ {
+ log::warning("Trying to remove archive_entry row with no id");
+ return false;
+ }
+
+ auto rows = 0;
+
+ if (auto sql = db::cacheQuery("delete_archive_entry", "DELETE FROM archive_entry WHERE archive_id = ? AND id = ?"))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+// -----------------------------------------------------------------------------
+// Returns a list of all ArchiveEntryRows for [archive_id]
+// -----------------------------------------------------------------------------
+vector getArchiveEntryRows(int64_t archive_id)
+{
+ vector rows;
+
+ if (auto sql = db::cacheQuery(
+ "get_archive_entries_for_archive", "SELECT * FROM archive_entry WHERE archive_id = ?"))
+ {
+ sql->clearBindings();
+ sql->bind(1, archive_id);
+
+ while (sql->executeStep())
+ {
+ rows.emplace_back(
+ archive_id,
+ sql->getColumn(1).getInt64(),
+ sql->getColumn(2).getString(),
+ sql->getColumn(3).getInt(),
+ sql->getColumn(4).getString(),
+ sql->getColumn(5).getUInt(),
+ sql->getColumn(6).getString(),
+ sql->getColumn(7).getString());
+ }
+
+ sql->reset();
+ }
+
+ return rows;
+}
+
+// -----------------------------------------------------------------------------
+// Mass updates the given [rows] in the database
+// -----------------------------------------------------------------------------
+void updateArchiveEntryRows(const vector& rows)
+{
+ if (auto sql = db::cacheQuery("update_archive_entry", update_archive_entry, true))
+ {
+ // Begin transaction if none currently active
+ db::Transaction transaction(db::connectionRW(), false);
+ transaction.beginIfNoActiveTransaction();
+
+ for (const auto& row : rows)
+ {
+ sql->clearBindings();
+
+ sql->bind(1, row.path);
+ sql->bind(2, row.index);
+ sql->bind(3, row.name);
+ sql->bind(4, row.size);
+ sql->bind(5, row.hash);
+ sql->bind(6, row.type_id);
+ sql->bind(7, row.archive_id);
+ sql->bind(8, row.id);
+
+ sql->exec();
+
+ sql->reset();
+ }
+
+ transaction.commit();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Mass inserts the given [rows] in the database
+// -----------------------------------------------------------------------------
+void insertArchiveEntryRows(const vector& rows)
+{
+ if (auto sql = db::cacheQuery("insert_archive_entry", insert_archive_entry, true))
+ {
+ // Begin transaction if none currently active
+ db::Transaction transaction(db::connectionRW(), false);
+ transaction.beginIfNoActiveTransaction();
+
+ for (auto& row : rows)
+ {
+ sql->clearBindings();
+
+ sql->bind(1, row.archive_id);
+ sql->bind(2, row.id);
+ sql->bind(3, row.path);
+ sql->bind(4, row.index);
+ sql->bind(5, row.name);
+ sql->bind(6, row.size);
+ sql->bind(7, row.hash);
+ sql->bind(8, row.type_id);
+
+ sql->exec();
+
+ sql->reset();
+ }
+
+ transaction.commit();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Mass deletes all archive_entry rows for [archive_id] in the database
+// -----------------------------------------------------------------------------
+int deleteArchiveEntryRowsByArchiveId(int64_t archive_id)
+{
+ int rows = 0;
+
+ if (auto sql = db::cacheQuery(
+ "delete_archive_entry_by_archive", "DELETE FROM archive_entry WHERE archive_id = ?", true))
+ {
+ sql->clearBindings();
+ sql->bind(1, archive_id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ return rows;
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if [row] exactly matches the given entry details.
+//
+// Note this currently ignores index so that if an entry has been moved up/down
+// externally it will still be able to be matched (it's very likely to be the
+// same entry)
+// -----------------------------------------------------------------------------
+inline bool entryRowExactMatch(
+ const ArchiveEntryRow& row,
+ string_view entry_path,
+ string_view entry_name,
+ unsigned entry_size,
+ string_view entry_hash)
+{
+ return row.path == entry_path && row.name == entry_name && row.size == entry_size && row.hash == entry_hash;
+}
+
+void writeEntryProperties(SQLite::Statement* sql, int64_t archive_id, const ArchiveEntry* entry)
+{
+ for (const auto& prop : entry->exProps().properties())
+ {
+ if (VECTOR_EXISTS(saved_ex_props, prop.name))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, entry->libraryId());
+ sql->bind(3, prop.name);
+ sql->bind(4, static_cast(prop.value.index()));
+ switch (prop.value.index())
+ {
+ case 0: sql->bind(5, std::get(prop.value)); break;
+ case 1: sql->bind(5, std::get(prop.value)); break;
+ case 2: sql->bind(5, std::get(prop.value)); break;
+ case 3: sql->bind(5, std::get(prop.value)); break;
+ case 4: sql->bind(5, std::get(prop.value)); break;
+ default: sql->bind(5, 0); break; // Shouldn't happen
+ }
+ sql->exec();
+ sql->reset();
+ }
+ }
+}
+
+void readEntryProperties(int64_t archive_id, ArchiveEntry* entry)
+{
+ if (auto sql = db::cacheQuery("get_archive_entry_properties", get_archive_entry_properties))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, entry->libraryId());
+
+ while (sql->executeStep())
+ {
+ auto key = sql->getColumn(0).getString();
+ switch (sql->getColumn(1).getInt())
+ {
+ case 0: entry->exProp(key) = sql->getColumn(2).getInt() > 0; break;
+ case 1: entry->exProp(key) = sql->getColumn(2).getInt(); break;
+ case 2: entry->exProp(key) = sql->getColumn(2).getUInt(); break;
+ case 3: entry->exProp(key) = sql->getColumn(2).getDouble(); break;
+ case 4: entry->exProp(key) = sql->getColumn(2).getString(); break;
+ default: break; // Shouldn't happen
+ }
+ }
+
+ sql->reset();
+ }
+}
+} // namespace slade::library
+
+// -----------------------------------------------------------------------------
+// Copies all archive_entry rows from [from_archive_id] and inserts them for
+// [to_archive_id]
+// -----------------------------------------------------------------------------
+int library::copyArchiveEntries(int64_t from_archive_id, int64_t to_archive_id)
+{
+ // Check args
+ if (from_archive_id < 0 || to_archive_id < 0)
+ return 0;
+
+ int n_copied = 0;
+
+ if (auto sql = db::cacheQuery("copy_archive_entries", copy_archive_entries, true))
+ {
+ sql->bind(1, to_archive_id);
+ sql->bind(2, from_archive_id);
+
+ n_copied = sql->exec();
+
+ sql->reset();
+ }
+
+ return n_copied;
+}
+
+// -----------------------------------------------------------------------------
+// Rebuilds all archive_entry rows for [archive_id] using the given [entries]
+// -----------------------------------------------------------------------------
+void library::rebuildEntries(int64_t archive_id, const vector& entries)
+{
+ auto start_time = app::runTimer();
+
+ // Delete all existing archive_entry rows for archive_id
+ deleteArchiveEntryRowsByArchiveId(archive_id);
+
+ // Build list of entry rows to add
+ vector rows;
+ for (auto i = 0; i < entries.size(); ++i)
+ {
+ rows.emplace_back(archive_id, i, *entries[i]);
+ entries[i]->setLibraryId(i);
+ }
+
+ // Add rows to database
+ insertArchiveEntryRows(rows);
+
+ // Write entry properties to database
+ saveAllEntryProperties(archive_id, entries);
+
+ log::debug("library::rebuildEntries took {}ms", app::runTimer() - start_time);
+}
+
+void library::saveEntryProperties(int64_t archive_id, const ArchiveEntry& entry)
+{
+ if (entry.libraryId() < 0)
+ return;
+
+ // Delete existing rows
+ db::connectionRW()->exec(fmt::format(
+ "DELETE FROM archive_entry_property WHERE archive_id = {} AND entry_id = {}", archive_id, entry.libraryId()));
+
+ // Insert prop rows
+ if (auto sql = db::cacheQuery("insert_archive_entry_property", insert_archive_entry_property, true))
+ writeEntryProperties(sql, archive_id, &entry);
+}
+
+void library::saveAllEntryProperties(int64_t archive_id, const vector& entries)
+{
+ // Delete existing rows
+ db::connectionRW()->exec(fmt::format("DELETE FROM archive_entry_property WHERE archive_id = {}", archive_id));
+
+ // Insert prop rows for all entries
+ if (auto sql = db::cacheQuery("insert_archive_entry_property", insert_archive_entry_property, true))
+ {
+ db::Transaction transaction(db::connectionRW(), false);
+ transaction.beginIfNoActiveTransaction();
+
+ for (auto entry : entries)
+ writeEntryProperties(sql, archive_id, entry);
+
+ transaction.commit();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Reads all entry info from the library for [archive_id] into the given
+// [entries].
+//
+// Will attempt to match any archive_entry rows that don't match exactly to an
+// entry but are close enough (eg. if the entry was renamed or moved since the
+// archive was last recorded in the library)
+// -----------------------------------------------------------------------------
+void library::readEntryInfo(int64_t archive_id, const vector& entries)
+{
+ // Get existing archive_entry rows for the archive
+ auto existing_rows = getArchiveEntryRows(archive_id);
+ if (existing_rows.empty())
+ return; // No rows, so no existing info to read
+
+ auto start_time = app::runTimer();
+
+ // Find all exact matches (path+name+size+hash is close enough to exact)
+ // This is likely to account a majority of the entries unless the archive
+ // has changed a *lot* since it was last saved to the library
+ unsigned n_unmatched_entries = 0;
+ unsigned row_start_index = 0;
+ for (auto entry : entries)
+ {
+ entry->setLibraryId(-1);
+
+ for (unsigned i = row_start_index; i < existing_rows.size(); ++i)
+ {
+ // Ignore rows already matched to an entry (id < 0)
+ if (existing_rows[i].id < 0)
+ {
+ // If this is the first row being checked, don't check it again
+ if (row_start_index == i)
+ ++row_start_index;
+
+ continue;
+ }
+
+ if (entryRowExactMatch(existing_rows[i], entry->path(), entry->name(), entry->size(), entry->hash()))
+ {
+ entry->setLibraryId(existing_rows[i].id);
+ entry->exProp("TypeHint") = existing_rows[i].type_id;
+ existing_rows[i].id = -1;
+ break;
+ }
+ }
+
+ if (entry->libraryId() < 0)
+ ++n_unmatched_entries;
+ }
+
+ log::debug("archive_entry matching (exact) took {}ms", app::runTimer() - start_time);
+
+
+ // For any remaining unmatched entries, try to determine what archive_entry
+ // row most closely matches the entry (and can reasonably be considered the
+ // 'same' entry - ideally we should have no match over matching to the wrong
+ // entry)
+ if (n_unmatched_entries > 0)
+ {
+ start_time = app::runTimer();
+
+ log::debug(
+ "Found {} entries in archive {} with no exact-matching archive_entry row", n_unmatched_entries, archive_id);
+
+ // Get list of unmatched entries
+ vector unmatched_entries;
+ unmatched_entries.reserve(n_unmatched_entries);
+ for (auto entry : entries)
+ if (entry->libraryId() < 0)
+ unmatched_entries.push_back(entry);
+
+ // Get list of unmatched rows
+ vector unmatched_rows;
+ for (auto& row : existing_rows)
+ if (row.id >= 0)
+ unmatched_rows.push_back(&row);
+
+ string entry_path, entry_type;
+ ArchiveEntryRow* current_match;
+ uint8_t match_score;
+ bool match_path, match_name, match_data, match_type;
+ for (auto entry : unmatched_entries)
+ {
+ entry_path = entry->path();
+ entry_type = entry->type()->id();
+ current_match = nullptr;
+ match_score = 0;
+
+ // Find best match in unmatched rows (if any)
+ for (auto row : unmatched_rows)
+ {
+ if (row->id < 0)
+ continue;
+
+ // Check what matches the entry
+ // (zero-sized entries can't be matched by data)
+ match_path = row->path == entry_path;
+ match_name = row->name == entry->name();
+ match_data = entry->size() > 0 && row->size > 0 && row->hash == entry->hash();
+ match_type = row->type_id == entry_type;
+
+ // Renamed entry
+ if (match_path && match_data)
+ {
+ current_match = row;
+ match_score = 10;
+ }
+
+ // Moved entry
+ if (match_score < 9 && match_name && match_data)
+ {
+ current_match = row;
+ match_score = 9;
+ }
+
+ // Modified entry (same type)
+ if (match_score < 8 && match_path && match_name && match_type)
+ {
+ current_match = row;
+ match_score = 8;
+ }
+
+ // Moved+Renamed entry
+ if (match_score < 7 && match_data)
+ {
+ current_match = row;
+ match_score = 7;
+ }
+ }
+
+ if (current_match)
+ {
+#ifdef _DEBUG
+ static string match_desc;
+ switch (match_score)
+ {
+ case 10: match_desc = "entry renamed"; break;
+ case 9: match_desc = "entry moved"; break;
+ case 8: match_desc = "entry modified (same type)"; break;
+ case 7: match_desc = "entry moved & renamed"; break;
+ default: match_desc = "unknown match"; break;
+ }
+
+ log::debug(
+ "Matched entry {} to row {} ({}) - {}",
+ entry->path(true),
+ current_match->id,
+ current_match->name,
+ match_desc);
+#endif
+
+ entry->setLibraryId(current_match->id);
+ current_match->id = -1;
+ }
+#ifdef _DEBUG
+ else
+ log::debug("No matching row found for entry {}", entry->path(true));
+#endif
+ }
+
+ log::debug("archive_entry matching (remaining unmatched) took {}ms", app::runTimer() - start_time);
+ }
+ else
+ log::debug("All archive_entry rows in archive {} matched", archive_id);
+
+
+ // Load entry properties
+ for (auto entry : entries)
+ readEntryProperties(archive_id, entry);
+
+
+ // Rebuild entry rows if there were any mismatches
+ if (n_unmatched_entries > 0)
+ rebuildEntries(archive_id, entries);
+}
diff --git a/src/Library/ArchiveEntry.h b/src/Library/ArchiveEntry.h
new file mode 100644
index 000000000..b232cefac
--- /dev/null
+++ b/src/Library/ArchiveEntry.h
@@ -0,0 +1,60 @@
+#pragma once
+
+namespace slade
+{
+class ArchiveEntry;
+
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ struct ArchiveEntryRow
+ {
+ int64_t archive_id = -1;
+ int64_t id = -1;
+ string path;
+ int index = -1;
+ string name;
+ unsigned size = 0;
+ string hash;
+ string type_id;
+
+ ArchiveEntryRow() = default;
+ ArchiveEntryRow(
+ int64_t archive_id,
+ int64_t id,
+ string_view path,
+ int index,
+ string_view name,
+ unsigned size,
+ string_view hash,
+ string_view type_id) :
+ archive_id{ archive_id },
+ id{ id },
+ path{ path },
+ index{ index },
+ name{ name },
+ size{ size },
+ hash{ hash },
+ type_id{ type_id }
+ {
+ }
+ ArchiveEntryRow(int64_t archive_id, int64_t id, const ArchiveEntry& entry);
+ ArchiveEntryRow(database::Context& db, int64_t archive_id, int64_t id);
+
+ int64_t insert() const;
+ bool update() const;
+ bool remove();
+ };
+
+ int copyArchiveEntries(int64_t from_archive_id, int64_t to_archive_id);
+ void rebuildEntries(int64_t archive_id, const vector& entries);
+ void saveEntryProperties(int64_t archive_id, const ArchiveEntry& entry);
+ void saveAllEntryProperties(int64_t archive_id, const vector& entries);
+ void readEntryInfo(int64_t archive_id, const vector& entries);
+
+} // namespace library
+} // namespace slade
diff --git a/src/Library/ArchiveFile.cpp b/src/Library/ArchiveFile.cpp
new file mode 100644
index 000000000..eea1a188a
--- /dev/null
+++ b/src/Library/ArchiveFile.cpp
@@ -0,0 +1,409 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveFile.cpp
+// Description: ArchiveFileRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "ArchiveFile.h"
+#include "ArchiveEntry.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include "Library.h"
+#include "Utility/FileUtils.h"
+#include "Utility/StringUtils.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+// SQL query strings
+string update_archive_file =
+ "UPDATE archive_file "
+ "SET path = ?, size = ?, hash = ?, format_id = ?, last_opened = ?, last_modified = ?, parent_id = ? "
+ "WHERE id = ?";
+string insert_archive_file =
+ "REPLACE INTO archive_file (path, size, hash, format_id, last_opened, last_modified, parent_id) "
+ "VALUES (?,?,?,?,?,?,?)";
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveFileRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveFileRow constructor
+// Gets info from the file at [file_path] if it exists
+// -----------------------------------------------------------------------------
+ArchiveFileRow::ArchiveFileRow(string_view file_path, string_view format_id) : path{ file_path }, format_id{ format_id }
+{
+ // Sanitize path
+ strutil::replaceIP(path, "\\", "/");
+
+ // Get file info
+ if (fileutil::fileExists(file_path))
+ {
+ SFile file(file_path);
+ size = file.size();
+ hash = file.calculateHash();
+ last_modified = fileutil::fileModifiedTime(file_path);
+ }
+}
+
+// -----------------------------------------------------------------------------
+// ArchiveFileRow constructor
+// Reads existing data from the database. If row [id] doesn't exist in the
+// database, the row id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveFileRow::ArchiveFileRow(database::Context& db, int64_t id) : id{ id }
+{
+ // Load from database
+ if (auto sql = db.cacheQuery("get_archive_file", "SELECT * FROM archive_file WHERE id = ?"))
+ {
+ sql->clearBindings();
+ sql->bind(1, id);
+
+ if (sql->executeStep())
+ {
+ path = sql->getColumn(1).getString();
+ size = sql->getColumn(2).getUInt();
+ hash = sql->getColumn(3).getString();
+ format_id = sql->getColumn(4).getString();
+ last_opened = sql->getColumn(5).getInt64();
+ last_modified = sql->getColumn(6).getInt64();
+ parent_id = sql->getColumn(7).getInt64();
+ }
+ else
+ {
+ log::warning("archive_file row with id {} does not exist in the database", id);
+ this->id = -1;
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// ArchiveFileRow constructor
+// Reads data from result columns in the given SQLite statement [sql]
+// -----------------------------------------------------------------------------
+ArchiveFileRow::ArchiveFileRow(const SQLite::Statement* sql)
+{
+ if (!sql)
+ return;
+
+ id = sql->getColumn(0).getInt64();
+ path = sql->getColumn(1).getString();
+ size = sql->getColumn(2).getUInt();
+ hash = sql->getColumn(3).getString();
+ format_id = sql->getColumn(4).getString();
+ last_opened = sql->getColumn(5).getInt64();
+ last_modified = sql->getColumn(6).getInt64();
+ parent_id = sql->getColumn(7).getInt64();
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, id will be updated and returned, otherwise returns -1
+// -----------------------------------------------------------------------------
+int64_t ArchiveFileRow::insert()
+{
+ if (id >= 0)
+ {
+ log::warning("Trying to insert archive_file row id {} that already exists", id);
+ return id;
+ }
+
+ if (auto sql = db::cacheQuery("insert_archive_file", insert_archive_file, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, path);
+ sql->bind(2, size);
+ sql->bind(3, hash);
+ sql->bind(4, format_id);
+ sql->bindDateTime(5, last_opened);
+ sql->bindDateTime(6, last_modified);
+ sql->bind(7, parent_id);
+
+ if (sql->exec() > 0)
+ id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ if (id >= 0)
+ library::signals().archive_file_inserted(id);
+
+ return id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveFileRow::update() const
+{
+ // Ignore invalid id
+ if (id < 0)
+ {
+ log::warning("Trying to update archive_file row with no id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("update_archive_file", update_archive_file, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, path);
+ sql->bind(2, size);
+ sql->bind(3, hash);
+ sql->bind(4, format_id);
+ sql->bindDateTime(5, last_opened);
+ sql->bindDateTime(6, last_modified);
+ sql->bind(7, parent_id);
+ sql->bind(8, id);
+
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ library::signals().archive_file_updated(id);
+ return true;
+ }
+
+ return false;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveFileRow::remove()
+{
+ // Ignore invalid id
+ if (id < 0)
+ {
+ log::warning("Trying to remove archive_file row with no id");
+ return false;
+ }
+
+ auto rows = 0;
+
+ if (auto sql = db::cacheQuery("delete_archive_file", "DELETE FROM archive_file WHERE id = ?"))
+ {
+ sql->bind(1, id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ library::signals().archive_file_deleted(id);
+ id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Returns the archive_file row id for [filename] (in [parent_id] if given),
+// or -1 if it does not exist in the database
+// -----------------------------------------------------------------------------
+int64_t library::archiveFileId(string_view filename, int64_t parent_id)
+{
+ int64_t archive_id = -1;
+
+ if (auto sql = db::cacheQuery("lib_get_archive_id", "SELECT id FROM archive_file WHERE path = ? AND parent_id = ?"))
+ {
+ sql->bind(1, filename);
+ sql->bind(2, parent_id);
+
+ if (sql->executeStep())
+ archive_id = sql->getColumn(0);
+
+ sql->reset();
+ }
+
+ return archive_id;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the first archive_file row id found that has a matching [size] and
+// [hash], or -1 if none found
+// -----------------------------------------------------------------------------
+int64_t library::findArchiveFileIdFromData(unsigned size, string_view hash, int64_t parent_id)
+{
+ int64_t archive_id = -1;
+
+ if (auto sql = db::cacheQuery(
+ "lib_find_archive_id_data", "SELECT id FROM archive_file WHERE size = ? AND hash = ? AND parent_id = ?"))
+ {
+ sql->bind(1, size);
+ sql->bind(2, hash);
+ sql->bind(3, parent_id);
+
+ if (sql->executeStep())
+ archive_id = sql->getColumn(0);
+
+ sql->reset();
+ }
+
+ return archive_id;
+}
+
+// -----------------------------------------------------------------------------
+// Saves [row] to the database, either inserts or updates depending on the id
+// -----------------------------------------------------------------------------
+bool library::saveArchiveFile(ArchiveFileRow& row)
+{
+ if (row.id < 0)
+ return row.insert() >= 0;
+ else
+ return row.update();
+}
+
+// -----------------------------------------------------------------------------
+// Creates a new archive_file row in the database for [file_path], copying data
+// from an existing row [copy_from_id] including any related data eg.
+// archive_entry rows.
+// Returns the id of the created row or -1 if copy_from_id was invalid
+// -----------------------------------------------------------------------------
+int64_t library::copyArchiveFile(string_view file_path, int64_t copy_from_id)
+{
+ // Get row to copy
+ ArchiveFileRow archive_file{ db::global(), copy_from_id };
+ if (archive_file.id < 0)
+ return -1;
+
+ // Set path
+ archive_file.id = -1;
+ archive_file.path = file_path;
+
+ // Reset last opened time
+ archive_file.last_opened = 0;
+
+ // Add new archive_file row
+ auto archive_id = archive_file.insert();
+
+ // Copy entries
+ copyArchiveEntries(copy_from_id, archive_id);
+
+ return archive_id;
+}
+
+// -----------------------------------------------------------------------------
+// Removes the archive_file row [id] from the database including all related
+// data eg. archive_entry etc.
+// -----------------------------------------------------------------------------
+void library::removeArchiveFile(int64_t id)
+{
+ // Delete row from archive_file
+ // (all related data will also be removed via cascading foreign keys)
+ if (db::connectionRW()->exec(fmt::format("DELETE FROM archive_file WHERE id = {}", id)) > 0)
+ library::signals().archive_file_deleted(id);
+}
+
+// -----------------------------------------------------------------------------
+// Returns the time archive [id] in the library was last opened (as time_t)
+// -----------------------------------------------------------------------------
+time_t library::archiveFileLastOpened(int64_t id)
+{
+ time_t last_opened = 0;
+
+ if (auto sql = db::cacheQuery("get_archive_file_last_opened", "SELECT last_opened FROM archive_file WHERE id = ?"))
+ {
+ sql->bind(1, id);
+
+ if (sql->executeStep())
+ last_opened = sql->getColumn(0).getInt64();
+
+ sql->reset();
+ }
+
+ return last_opened;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the time archive [id] in the library was last modified on disk
+// (as time_t)
+// -----------------------------------------------------------------------------
+time_t library::archiveFileLastModified(int64_t id)
+{
+ time_t last_modified = 0;
+
+ if (auto sql = db::cacheQuery(
+ "get_archive_file_last_modified", "SELECT last_modified FROM archive_file WHERE id = ?"))
+ {
+ sql->bind(1, id);
+
+ if (sql->executeStep())
+ last_modified = sql->getColumn(0).getInt64();
+
+ sql->reset();
+ }
+
+ return last_modified;
+}
+
+// -----------------------------------------------------------------------------
+// Returns models for all rows in the archive_file table
+// -----------------------------------------------------------------------------
+vector library::allArchiveFileRows()
+{
+ vector rows;
+
+ if (auto sql = db::cacheQuery("all_archive_file_rows", "SELECT * FROM archive_file"))
+ {
+ while (sql->executeStep())
+ rows.emplace_back(sql);
+ }
+
+ return rows;
+}
diff --git a/src/Library/ArchiveFile.h b/src/Library/ArchiveFile.h
new file mode 100644
index 000000000..a6822d70d
--- /dev/null
+++ b/src/Library/ArchiveFile.h
@@ -0,0 +1,66 @@
+#pragma once
+
+namespace SQLite
+{
+class Statement;
+}
+
+namespace slade
+{
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ // Database model for rows in the archive_file table in the database
+ struct ArchiveFileRow
+ {
+ int64_t id = -1;
+ string path;
+ unsigned size = 0;
+ string hash;
+ string format_id;
+ time_t last_opened = 0;
+ time_t last_modified = 0;
+ int64_t parent_id = -1;
+
+ ArchiveFileRow() = default;
+ ArchiveFileRow(
+ string_view path,
+ unsigned size,
+ string_view hash,
+ string_view format_id,
+ time_t last_opened,
+ time_t last_modified,
+ int64_t parent_id) :
+ path{ path },
+ size{ size },
+ hash{ hash },
+ format_id{ format_id },
+ last_opened{ last_opened },
+ last_modified{ last_modified },
+ parent_id{ parent_id }
+ {
+ }
+ ArchiveFileRow(string_view file_path, string_view format_id);
+ ArchiveFileRow(database::Context& db, int64_t id);
+ ArchiveFileRow(const SQLite::Statement* sql);
+
+ int64_t insert();
+ bool update() const;
+ bool remove();
+ };
+
+ int64_t archiveFileId(string_view filename, int64_t parent_id = -1);
+ int64_t findArchiveFileIdFromData(unsigned size, string_view hash, int64_t parent_id = -1);
+ bool saveArchiveFile(ArchiveFileRow& row);
+ int64_t copyArchiveFile(string_view file_path, int64_t copy_from_id);
+ void removeArchiveFile(int64_t id);
+ time_t archiveFileLastOpened(int64_t id);
+ time_t archiveFileLastModified(int64_t id);
+
+ vector allArchiveFileRows();
+} // namespace library
+} // namespace slade
diff --git a/src/Library/ArchiveMap.cpp b/src/Library/ArchiveMap.cpp
new file mode 100644
index 000000000..87e7027bf
--- /dev/null
+++ b/src/Library/ArchiveMap.cpp
@@ -0,0 +1,220 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveMap.cpp
+// Description: ArchiveMapRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "ArchiveMap.h"
+#include "Archive/ArchiveEntry.h"
+#include "Archive/MapDesc.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+string update_archive_map = "UPDATE archive_map SET name = ?, format = ? WHERE archive_id = ? AND header_entry_id = ?";
+string insert_archive_map = "INSERT INTO archive_map (archive_id, header_entry_id, name, format) VALUES (?,?,?,?)";
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveMapRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveMapRow constructor
+// Reads existing data from the database. If row [archive_id]+[header_entry_id]
+// doesn't exist in the database, the row id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveMapRow::ArchiveMapRow(database::Context& db, int64_t archive_id, int64_t header_entry_id)
+{
+ if (auto sql = db.cacheQuery(
+ "get_archive_map", "SELECT * FROM archive_map WHERE archive_id = ? AND header_entry_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, header_entry_id);
+
+ if (sql->executeStep())
+ {
+ this->archive_id = archive_id;
+ this->header_entry_id = header_entry_id;
+ name = sql->getColumn(2).getString();
+ auto format_int = sql->getColumn(3).getInt();
+
+ if (format_int <= static_cast(MapFormat::Unknown))
+ format = static_cast(format_int);
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// ArchiveMapRow constructor
+// Initializes the row with info from [map_desc]
+// -----------------------------------------------------------------------------
+ArchiveMapRow::ArchiveMapRow(int64_t archive_id, const MapDesc& map_desc) : archive_id{ archive_id }
+{
+ auto* head_entry = map_desc.head.lock().get();
+ if (!head_entry)
+ {
+ this->archive_id = -1;
+ return;
+ }
+
+ header_entry_id = head_entry->libraryId();
+ name = map_desc.name;
+ format = map_desc.format;
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, the inserted row id is returned, otherwise returns -1.
+// (note the returned row id isn't the 'id' column since that is not a primary
+// key, it returns the sqlite rowid of the row)
+// -----------------------------------------------------------------------------
+int64_t ArchiveMapRow::insert()
+{
+ int64_t row_id = -1;
+
+ if (auto sql = db::cacheQuery("insert_archive_map", insert_archive_map, true))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, header_entry_id);
+ sql->bind(3, name);
+ sql->bind(4, static_cast(format));
+
+ if (sql->exec() > 0)
+ row_id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ return row_id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveMapRow::update() const
+{
+ // Ignore invalid id
+ if (archive_id < 0 || header_entry_id < 0)
+ {
+ log::warning("Trying to update archive_map row with no archive+entry id");
+ return false;
+ }
+
+ auto rows = 0;
+
+ if (auto sql = db::cacheQuery("update_archive_map", update_archive_map, true))
+ {
+ sql->bind(1, name);
+ sql->bind(2, static_cast(format));
+ sql->bind(3, archive_id);
+ sql->bind(4, header_entry_id);
+
+ rows = sql->exec();
+
+ sql->reset();
+ }
+
+ return rows > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, archive_id and header_entry_id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveMapRow::remove()
+{
+ // Ignore invalid id
+ if (archive_id < 0 || header_entry_id < 0)
+ {
+ log::warning("Trying to remove archive_map row with no archive+entry id");
+ return false;
+ }
+
+ auto rows = 0;
+
+ if (auto sql = db::cacheQuery(
+ "delete_archive_map", "DELETE FROM archive_map WHERE archive_id = ? AND header_entry_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, header_entry_id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ archive_id = -1;
+ header_entry_id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Rebuilds all archive_map rows for [archive_id] from [archive]
+// -----------------------------------------------------------------------------
+void library::updateArchiveMaps(int64_t archive_id, const Archive& archive)
+{
+ auto maps = archive.detectMaps();
+
+ // Delete existing map rows
+ db::exec(fmt::format("DELETE FROM archive_map WHERE archive_id = {}", archive_id));
+
+ // Add detected maps to database
+ for (const auto& map : maps)
+ {
+ ArchiveMapRow row{ archive_id, map };
+ row.insert();
+ }
+}
diff --git a/src/Library/ArchiveMap.h b/src/Library/ArchiveMap.h
new file mode 100644
index 000000000..388df4ab2
--- /dev/null
+++ b/src/Library/ArchiveMap.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "Archive/Archive.h"
+#include "General/Defs.h"
+
+namespace slade
+{
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ struct ArchiveMapRow
+ {
+ int64_t archive_id = -1;
+ int64_t header_entry_id = -1;
+ string name;
+ MapFormat format = MapFormat::Unknown;
+
+ ArchiveMapRow(database::Context& db, int64_t archive_id, int64_t header_entry_id);
+ ArchiveMapRow(int64_t archive_id, const MapDesc& map_desc);
+
+ int64_t insert();
+ bool update() const;
+ bool remove();
+ };
+
+ void updateArchiveMaps(int64_t archive_id, const Archive& archive);
+} // namespace library
+} // namespace slade
diff --git a/src/Library/ArchiveMapConfig.cpp b/src/Library/ArchiveMapConfig.cpp
new file mode 100644
index 000000000..6bf8ae963
--- /dev/null
+++ b/src/Library/ArchiveMapConfig.cpp
@@ -0,0 +1,200 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveMapConfig.cpp
+// Description: ArchiveMapConfigRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "ArchiveMapConfig.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+string insert_archive_map_config = "INSERT INTO archive_map_config VALUES (?,?,?)";
+string update_archive_map_config =
+ "UPDATE archive_map_config "
+ "SET game = ?, port = ? "
+ "WHERE archive_id = ?";
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveMapConfigRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveMapConfigRow constructor
+// Reads existing data from the database. If a row with [archive_id] doesn't
+// exist in the database, the row archive_id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveMapConfigRow::ArchiveMapConfigRow(database::Context& db, int64_t archive_id)
+{
+ if (auto sql = db.cacheQuery("get_archive_map_config", "SELECT * FROM archive_map_config WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+
+ if (sql->executeStep())
+ {
+ this->archive_id = archive_id;
+ game = sql->getColumn(1).getString();
+ port = sql->getColumn(2).getString();
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, the inserted row id is returned, otherwise returns -1
+// -----------------------------------------------------------------------------
+int64_t ArchiveMapConfigRow::insert() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to insert archive_map_config row with no archive_id");
+ return false;
+ }
+
+ int64_t row_id = -1;
+
+ if (auto sql = db::cacheQuery("insert_archive_map_config", insert_archive_map_config, true))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, game);
+ sql->bind(3, port);
+
+ if (sql->exec() > 0)
+ row_id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ return row_id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveMapConfigRow::update() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to update archive_map_config row with no archive_id");
+ return false;
+ }
+
+ int rows = 0;
+
+ if (auto sql = db::cacheQuery("update_archive_map_config", update_archive_map_config, true))
+ {
+ sql->bind(1, game);
+ sql->bind(2, port);
+ sql->bind(3, archive_id);
+
+ rows = sql->exec();
+
+ sql->reset();
+ }
+
+ return rows > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, archive_id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveMapConfigRow::remove()
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to delete archive_map_config row with no archive_id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("delete_archive_map_config", "DELETE FROM archive_map_config WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ archive_id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Returns the archive_map_config row for [archive_id].
+// If it doesn't exist in the database, the row's archive_id will be -1
+// -----------------------------------------------------------------------------
+ArchiveMapConfigRow library::getArchiveMapConfig(int64_t archive_id)
+{
+ return { db::global(), archive_id };
+}
+
+// -----------------------------------------------------------------------------
+// Saves [row] to the database, either inserts or updates if the row for
+// archive_id already exists
+// -----------------------------------------------------------------------------
+bool library::saveArchiveMapConfig(const ArchiveMapConfigRow& row)
+{
+ if (row.archive_id < 0)
+ return false;
+
+ // Update/Insert
+ return db::rowIdExists("archive_map_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0;
+}
diff --git a/src/Library/ArchiveMapConfig.h b/src/Library/ArchiveMapConfig.h
new file mode 100644
index 000000000..c29defaeb
--- /dev/null
+++ b/src/Library/ArchiveMapConfig.h
@@ -0,0 +1,30 @@
+#pragma once
+
+namespace slade
+{
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ struct ArchiveMapConfigRow
+ {
+ int64_t archive_id = -1;
+ string game;
+ string port;
+
+ ArchiveMapConfigRow() = default;
+ ArchiveMapConfigRow(database::Context& db, int64_t archive_id);
+ ArchiveMapConfigRow(int64_t archive_id) : archive_id{ archive_id } {}
+
+ int64_t insert() const;
+ bool update() const;
+ bool remove();
+ };
+
+ ArchiveMapConfigRow getArchiveMapConfig(int64_t archive_id);
+ bool saveArchiveMapConfig(const ArchiveMapConfigRow& row);
+} // namespace library
+} // namespace slade
diff --git a/src/Library/ArchiveRunConfig.cpp b/src/Library/ArchiveRunConfig.cpp
new file mode 100644
index 000000000..30e8cf7a3
--- /dev/null
+++ b/src/Library/ArchiveRunConfig.cpp
@@ -0,0 +1,206 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveRunConfig.cpp
+// Description: ArchiveRunConfigRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "ArchiveRunConfig.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+string insert_archive_run_config = "INSERT INTO archive_run_config VALUES (?,?,?,?,?)";
+string update_archive_run_config =
+ "UPDATE archive_run_config "
+ "SET executable_id = ?, run_config = ?, run_extra = ?, iwad_path = ? "
+ "WHERE archive_id = ?";
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveRunConfigRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveRunConfigRow constructor
+// Reads existing data from the database. If a row with [archive_id] doesn't
+// exist in the database, the row archive_id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveRunConfigRow::ArchiveRunConfigRow(database::Context& db, int64_t archive_id)
+{
+ if (auto sql = db.cacheQuery("get_archive_run_config", "SELECT * FROM archive_run_config WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+
+ if (sql->executeStep())
+ {
+ this->archive_id = archive_id;
+ executable_id = sql->getColumn(1).getString();
+ run_config = sql->getColumn(2).getInt();
+ run_extra = sql->getColumn(3).getString();
+ iwad_path = sql->getColumn(4).getString();
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, the inserted row id is returned, otherwise returns -1
+// -----------------------------------------------------------------------------
+int64_t ArchiveRunConfigRow::insert() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to insert archive_run_config row with no archive_id");
+ return false;
+ }
+
+ int64_t row_id = -1;
+
+ if (auto sql = db::cacheQuery("insert_archive_run_config", insert_archive_run_config, true))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, executable_id);
+ sql->bind(3, run_config);
+ sql->bind(4, run_extra);
+ sql->bind(5, iwad_path);
+
+ if (sql->exec() > 0)
+ row_id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ return row_id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveRunConfigRow::update() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to update archive_run_config row with no archive_id");
+ return false;
+ }
+
+ int rows = 0;
+
+ if (auto sql = db::cacheQuery("update_archive_run_config", update_archive_run_config, true))
+ {
+ sql->bind(1, executable_id);
+ sql->bind(2, run_config);
+ sql->bind(3, run_extra);
+ sql->bind(4, iwad_path);
+ sql->bind(5, archive_id);
+
+ rows = sql->exec();
+
+ sql->reset();
+ }
+
+ return rows > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, archive_id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveRunConfigRow::remove()
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to delete archive_run_config row with no archive_id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("delete_archive_run_config", "DELETE FROM archive_run_config WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ archive_id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Returns the archive_run_config row for [archive_id].
+// If it doesn't exist in the database, the row's archive_id will be -1
+// -----------------------------------------------------------------------------
+ArchiveRunConfigRow library::getArchiveRunConfig(int64_t archive_id)
+{
+ return { db::global(), archive_id };
+}
+
+// -----------------------------------------------------------------------------
+// Saves [row] to the database, either inserts or updates if the row for
+// archive_id already exists
+// -----------------------------------------------------------------------------
+bool library::saveArchiveRunConfig(const ArchiveRunConfigRow& row)
+{
+ if (row.archive_id < 0)
+ return false;
+
+ // Update/Insert
+ return db::rowIdExists("archive_run_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0;
+}
diff --git a/src/Library/ArchiveRunConfig.h b/src/Library/ArchiveRunConfig.h
new file mode 100644
index 000000000..b8a4e7ba2
--- /dev/null
+++ b/src/Library/ArchiveRunConfig.h
@@ -0,0 +1,32 @@
+#pragma once
+
+namespace slade
+{
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ struct ArchiveRunConfigRow
+ {
+ int64_t archive_id = -1;
+ string executable_id;
+ int run_config = 0;
+ string run_extra;
+ string iwad_path;
+
+ ArchiveRunConfigRow() = default;
+ ArchiveRunConfigRow(database::Context& db, int64_t archive_id);
+ ArchiveRunConfigRow(int64_t archive_id) : archive_id{ archive_id } {}
+
+ int64_t insert() const;
+ bool update() const;
+ bool remove();
+ };
+
+ ArchiveRunConfigRow getArchiveRunConfig(int64_t archive_id);
+ bool saveArchiveRunConfig(const ArchiveRunConfigRow& row);
+} // namespace library
+} // namespace slade
diff --git a/src/Library/ArchiveUIConfig.cpp b/src/Library/ArchiveUIConfig.cpp
new file mode 100644
index 000000000..7edfc7297
--- /dev/null
+++ b/src/Library/ArchiveUIConfig.cpp
@@ -0,0 +1,303 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: ArchiveUIConfig.cpp
+// Description: ArchiveUIConfigRow struct and related functions
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "ArchiveUIConfig.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include "UI/State.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+string update_archive_ui_config =
+ "UPDATE archive_ui_config "
+ "SET elist_index_visible = ?, elist_index_width = ?, elist_name_width = ?, elist_size_visible = ?, "
+ " elist_size_width = ?, elist_type_visible = ?, elist_type_width = ?, elist_sort_column = ?, "
+ " elist_sort_descending = ?, splitter_position = ? "
+ "WHERE archive_id = ?";
+string insert_archive_ui_config =
+ "INSERT INTO archive_ui_config (archive_id, elist_index_visible, elist_index_width, elist_name_width, "
+ " elist_size_visible, elist_size_width, elist_type_visible, elist_type_width, "
+ " elist_sort_column, elist_sort_descending, splitter_position) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)";
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// ArchiveUIConfigRow Struct Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// ArchiveUIConfigRow constructor
+// Reads existing data from the database. If a row with [archive_id] doesn't
+// exist in the database, the row archive_id will be set to -1
+// -----------------------------------------------------------------------------
+ArchiveUIConfigRow::ArchiveUIConfigRow(db::Context& db, int64_t archive_id) : archive_id{ archive_id }
+{
+ if (auto sql = db.cacheQuery("get_archive_ui_config", "SELECT * FROM archive_ui_config WHERE archive_id = ?"))
+ {
+ sql->clearBindings();
+ sql->bind(1, archive_id);
+
+ if (sql->executeStep())
+ {
+ elist_index_visible = sql->getColumn(1).getInt() > 0;
+ elist_index_width = sql->getColumn(2).getInt();
+ elist_name_width = sql->getColumn(3).getInt();
+ elist_size_visible = sql->getColumn(4).getInt() > 0;
+ elist_size_width = sql->getColumn(5).getInt();
+ elist_type_visible = sql->getColumn(6).getInt() > 0;
+ elist_type_width = sql->getColumn(7).getInt();
+ elist_sort_column = sql->getColumn(8).getString();
+ elist_sort_descending = sql->getColumn(9).getInt() > 0;
+ splitter_position = sql->getColumn(10).getInt();
+ }
+ else
+ {
+ log::warning("archive_ui_config row with archive_id {} does not exist in the database", archive_id);
+ this->archive_id = -1;
+ }
+
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// ArchiveUIConfigRow constructor
+// Initializes the row for [archive_id] with 'default' values taken from cvars
+// depending on [tree_view]
+// -----------------------------------------------------------------------------
+ArchiveUIConfigRow::ArchiveUIConfigRow(int64_t archive_id, bool tree_view) : archive_id{ archive_id }
+{
+ elist_index_visible = ui::getStateBool("EntryListIndexVisible");
+ elist_index_width = ui::getStateInt("EntryListIndexWidth");
+ elist_name_width = ui::getStateInt(tree_view ? "EntryListNameWidthTree" : "EntryListNameWidthList");
+ elist_size_visible = ui::getStateBool("EntryListSizeVisible");
+ elist_size_width = ui::getStateInt("EntryListSizeWidth");
+ elist_type_visible = ui::getStateBool("EntryListTypeVisible");
+ elist_type_width = ui::getStateInt("EntryListTypeWidth");
+ splitter_position = ui::getStateInt(tree_view ? "ArchivePanelSplitPosTree" : "ArchivePanelSplitPosList");
+
+ log::debug("Created default entry list config for archive {}", archive_id);
+}
+
+// -----------------------------------------------------------------------------
+// Inserts this row into the database.
+// If successful, the inserted row id is returned, otherwise returns -1
+// -----------------------------------------------------------------------------
+int64_t ArchiveUIConfigRow::insert() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to insert archive_ui_config row with no archive_id");
+ return false;
+ }
+
+ int64_t row_id = -1;
+
+ if (auto sql = db::cacheQuery("insert_archive_ui_config", insert_archive_ui_config, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, archive_id);
+ sql->bind(2, elist_index_visible);
+ sql->bind(3, elist_index_width);
+ sql->bind(4, elist_name_width);
+ sql->bind(5, elist_size_visible);
+ sql->bind(6, elist_size_width);
+ sql->bind(7, elist_type_visible);
+ sql->bind(8, elist_type_width);
+ sql->bind(9, elist_sort_column);
+ sql->bind(10, elist_sort_descending);
+ sql->bind(11, splitter_position);
+
+ if (sql->exec() > 0)
+ row_id = db::connectionRW()->getLastInsertRowid();
+
+ sql->reset();
+ }
+
+ return row_id;
+}
+
+// -----------------------------------------------------------------------------
+// Updates this row in the database
+// -----------------------------------------------------------------------------
+bool ArchiveUIConfigRow::update() const
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to update archive_ui_config row with no archive_id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("update_archive_ui_config", update_archive_ui_config, true))
+ {
+ sql->clearBindings();
+
+ sql->bind(1, elist_index_visible);
+ sql->bind(2, elist_index_width);
+ sql->bind(3, elist_name_width);
+ sql->bind(4, elist_size_visible);
+ sql->bind(5, elist_size_width);
+ sql->bind(6, elist_type_visible);
+ sql->bind(7, elist_type_width);
+ sql->bind(8, elist_sort_column);
+ sql->bind(9, elist_sort_descending);
+ sql->bind(10, splitter_position);
+ sql->bind(11, archive_id);
+
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ return rows > 0;
+}
+
+// -----------------------------------------------------------------------------
+// Removes this row from the database.
+// If successful, archive_id will be set to -1
+// -----------------------------------------------------------------------------
+bool ArchiveUIConfigRow::remove()
+{
+ // Ignore invalid id
+ if (archive_id < 0)
+ {
+ log::warning("Trying to delete archive_ui_config row with no archive_id");
+ return false;
+ }
+
+ auto rows = 0;
+ if (auto sql = db::cacheQuery("delete_archive_ui_config", "DELETE FROM archive_ui_config WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ rows = sql->exec();
+ sql->reset();
+ }
+
+ if (rows > 0)
+ {
+ archive_id = -1;
+ return true;
+ }
+
+ return false;
+}
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Returns the archive_ui_config row for [archive_id].
+// If it doesn't exist in the database, the row's archive_id will be -1
+// -----------------------------------------------------------------------------
+ArchiveUIConfigRow library::getArchiveUIConfig(int64_t archive_id)
+{
+ return { db::global(), archive_id };
+}
+
+// -----------------------------------------------------------------------------
+// Saves [row] to the database, either inserts or updates if the row for
+// archive_id already exists
+// -----------------------------------------------------------------------------
+bool library::saveArchiveUIConfig(const ArchiveUIConfigRow& row)
+{
+ if (row.archive_id < 0)
+ return false;
+
+ // Update/Insert
+ return db::rowIdExists("archive_ui_config", row.archive_id, "archive_id") ? row.update() : row.insert() >= 0;
+}
+
+// -----------------------------------------------------------------------------
+// Returns the splitter position for [archive_id], or -1 if no config exists
+// -----------------------------------------------------------------------------
+int library::archiveUIConfigSplitterPos(int64_t archive_id)
+{
+ int splitter_pos = -1;
+
+ if (auto sql = db::cacheQuery(
+ "archive_ui_config_splitter_pos", "SELECT splitter_position FROM archive_ui_config WHERE archive_id = ?"))
+ {
+ sql->clearBindings();
+ sql->bind(1, archive_id);
+
+ if (sql->executeStep())
+ splitter_pos = sql->getColumn(0).getInt();
+
+ sql->reset();
+ }
+
+ return splitter_pos;
+}
+
+// -----------------------------------------------------------------------------
+// Saves the splitter position for [archive_id]
+// -----------------------------------------------------------------------------
+bool library::saveArchiveUIConfigSplitterPos(int64_t archive_id, int splitter_pos)
+{
+ bool updated = false;
+
+ if (auto sql = db::cacheQuery(
+ "update_archive_ui_config_splitter_position",
+ "UPDATE archive_ui_config SET splitter_position = ? WHERE archive_id = ?",
+ true))
+ {
+ sql->clearBindings();
+ sql->bind(1, splitter_pos);
+ sql->bind(2, archive_id);
+
+ updated = sql->exec() > 0;
+
+ sql->reset();
+ }
+
+ return updated;
+}
diff --git a/src/Library/ArchiveUIConfig.h b/src/Library/ArchiveUIConfig.h
new file mode 100644
index 000000000..fa4f97ebe
--- /dev/null
+++ b/src/Library/ArchiveUIConfig.h
@@ -0,0 +1,68 @@
+#pragma once
+
+namespace slade
+{
+namespace database
+{
+ class Context;
+}
+
+namespace library
+{
+ // Database model for rows in the archive_ui_config table in the database
+ struct ArchiveUIConfigRow
+ {
+ int64_t archive_id = -1;
+ bool elist_index_visible = false;
+ int elist_index_width = -1;
+ int elist_name_width = -1;
+ bool elist_size_visible = true;
+ int elist_size_width = -1;
+ bool elist_type_visible = true;
+ int elist_type_width = -1;
+ string elist_sort_column;
+ bool elist_sort_descending = false;
+ int splitter_position = -1;
+
+ ArchiveUIConfigRow() = default;
+ ArchiveUIConfigRow(int64_t archive_id) : archive_id{ archive_id } {}
+ ArchiveUIConfigRow(
+ int64_t archive_id,
+ bool elist_index_visible,
+ int elist_index_width,
+ int elist_name_width,
+ bool elist_size_visible,
+ int elist_size_width,
+ bool elist_type_visible,
+ int elist_type_width,
+ string_view elist_sort_column,
+ bool elist_sort_descending,
+ int splitter_position) :
+ archive_id{ archive_id },
+ elist_index_visible{ elist_index_visible },
+ elist_index_width{ elist_index_width },
+ elist_name_width{ elist_name_width },
+ elist_size_visible{ elist_size_visible },
+ elist_size_width{ elist_size_width },
+ elist_type_visible{ elist_type_visible },
+ elist_type_width{ elist_type_width },
+ elist_sort_column{ elist_sort_column },
+ elist_sort_descending{ elist_sort_descending },
+ splitter_position{ splitter_position }
+ {
+ }
+ ArchiveUIConfigRow(database::Context& db, int64_t archive_id);
+ ArchiveUIConfigRow(int64_t archive_id, bool tree_view);
+
+ int64_t insert() const;
+ bool update() const;
+ bool remove();
+ };
+
+ ArchiveUIConfigRow getArchiveUIConfig(int64_t archive_id);
+ bool saveArchiveUIConfig(const ArchiveUIConfigRow& row);
+ int archiveUIConfigSplitterPos(int64_t archive_id);
+ bool saveArchiveUIConfigSplitterPos(int64_t archive_id, int splitter_pos);
+
+} // namespace library
+} // namespace slade
diff --git a/src/Library/Library.cpp b/src/Library/Library.cpp
new file mode 100644
index 000000000..ff4078679
--- /dev/null
+++ b/src/Library/Library.cpp
@@ -0,0 +1,691 @@
+
+// -----------------------------------------------------------------------------
+// SLADE - It's a Doom Editor
+// Copyright(C) 2008 - 2024 Simon Judd
+//
+// Email: sirjuddington@gmail.com
+// Web: http://slade.mancubus.net
+// Filename: Library.cpp
+// Description: Functions dealing with the program's archive 'library', which is
+// essentially a bunch of info about archives that have been opened
+// in SLADE.
+//
+// This program 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 2 of the License, or (at your option)
+// any later version.
+//
+// This program 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, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA.
+// -----------------------------------------------------------------------------
+
+
+// -----------------------------------------------------------------------------
+//
+// Includes
+//
+// -----------------------------------------------------------------------------
+#include "Main.h"
+#include "Library.h"
+#include "App.h"
+#include "Archive/Archive.h"
+#include "Archive/ArchiveEntry.h"
+#include "Archive/ArchiveFormat.h"
+#include "Archive/ArchiveFormatHandler.h"
+#include "ArchiveEntry.h"
+#include "ArchiveFile.h"
+#include "ArchiveMap.h"
+#include "ArchiveUIConfig.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include "Database/Transaction.h"
+#include "Utility/FileUtils.h"
+#include "Utility/StringUtils.h"
+#include "Utility/Tokenizer.h"
+#include
+
+using namespace slade;
+using namespace library;
+
+
+// -----------------------------------------------------------------------------
+//
+// Variables
+//
+// -----------------------------------------------------------------------------
+namespace slade::library
+{
+Signals lib_signals;
+std::atomic lib_scan_running{ false }; // Whether a library scan is currently running, set to false to request stop
+
+string insert_archive_bookmark = "INSERT OR REPLACE INTO archive_bookmark VALUES (?,?)";
+
+vector recent_files;
+} // namespace slade::library
+
+
+// -----------------------------------------------------------------------------
+//
+// library Namespace Functions
+//
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// Initializes the library
+// -----------------------------------------------------------------------------
+void library::init()
+{
+ // Remove pre-3.3.0 recent files that exist in the library
+ // (and have been opened)
+ vector rf_to_remove;
+ for (const auto& recent_file : recent_files)
+ {
+ // Find archive_file row id from path
+ auto archive_id = archiveFileId(recent_file);
+ if (archive_id < 0)
+ continue;
+
+ // Load row details
+ ArchiveFileRow archive_file{ db::global(), archive_id };
+
+ // If the last_opened time exists, we can remove it from the pre-3.3.0
+ // recent files list
+ if (archive_file.last_opened > 0)
+ rf_to_remove.push_back(recent_file);
+ }
+ for (const auto& path : rf_to_remove)
+ VECTOR_REMOVE(recent_files, path);
+}
+
+// -----------------------------------------------------------------------------
+// Reads all info from the library about the given [archive].
+// Returns the library id of the archive
+// -----------------------------------------------------------------------------
+int64_t library::readArchiveInfo(const Archive& archive)
+{
+ try
+ {
+ // Find archive_file row for [archive] ---------------------------------
+
+ // Create row from file path to use for comparison
+ auto archive_file = ArchiveFileRow{ archive.filename(), archive.formatId() };
+
+ // Check for parent archive
+ if (auto* parent = archive.parentArchive())
+ {
+ auto* entry = archive.parentEntry();
+ archive_file.parent_id = parent->libraryId();
+ archive_file.path = parent->filename() + "/" + entry->name();
+ }
+
+ // Find existing archive_file row id for the archive's filename
+ auto archive_id = archiveFileId(archive_file.path, archive_file.parent_id);
+ if (archive_id < 0)
+ {
+ // Not found - look for match
+
+ // Can't match folder archives by data (yet)
+ // TODO: Figure out a good way to store size/hash for folder archive
+ if (archive_file.format_id == "folder")
+ return -1;
+
+ // Find archive_file row with matching data
+ // If no data match found this archive doesn't exist in the library
+ auto match_id = findArchiveFileIdFromData(archive_file.size, archive_file.hash, archive_file.parent_id);
+ if (match_id < 0)
+ return -1;
+
+ // Check if the matched file exists on disk (or in the parent archive)
+ // - If it exists the archive has likely been copied so copy its
+ // data in the library
+ // - If it doesn't exist the archive has likely been moved so just
+ // use and update the existing (matched) row in the library
+ auto match_row = ArchiveFileRow{ db::global(), match_id };
+ if (archive_file.parent_id < 0 && fileutil::fileExists(match_row.path))
+ archive_id = copyArchiveFile(archive_file.path, match_id);
+ else if (archive_file.parent_id >= 0 && archive.parentArchive()->entryAtPath(archive_file.path))
+ archive_id = copyArchiveFile(archive_file.path, match_id);
+ else
+ {
+ archive_id = match_id;
+
+ // Update existing row with new file details
+ auto existing_row = ArchiveFileRow{ db::global(), archive_id };
+ existing_row.path = archive_file.path;
+ existing_row.last_modified = archive_file.last_modified;
+ existing_row.format_id = archive_file.format_id;
+ existing_row.update();
+ }
+ }
+
+ // Read archive_entry rows for archive ---------------------------------
+
+ vector all_entries;
+ archive.putEntryTreeAsList(all_entries);
+ readEntryInfo(archive_id, all_entries);
+
+
+ // Finish up
+ archive.setLibraryId(archive_id);
+ return archive_id;
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error reading archive info from the library: {}", ex.what());
+ return -1;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Sets the [last_opened] time for [archive_id] in the library
+// -----------------------------------------------------------------------------
+void library::setArchiveLastOpenedTime(int64_t archive_id, time_t last_opened)
+{
+ try
+ {
+ if (auto sql = db::cacheQuery(
+ "lib_set_archive_last_opened", "UPDATE archive_file SET last_opened = ? WHERE id = ?", true))
+ {
+ sql->bindDateTime(1, last_opened);
+ sql->bind(2, archive_id);
+ sql->exec();
+ sql->reset();
+ }
+
+ lib_signals.archive_file_updated(archive_id);
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error setting archive last opened time in library: {}", ex.what());
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Writes all info for [archive] into the library.
+// Returns the library id of the archive
+// -----------------------------------------------------------------------------
+int64_t library::writeArchiveInfo(const Archive& archive)
+{
+ try
+ {
+ // Create row from archive path + format
+ auto archive_file = ArchiveFileRow{ archive.filename(), archive.formatId() };
+
+ // Check for parent archive
+ if (auto* parent = archive.parentArchive())
+ {
+ auto* entry = archive.parentEntry();
+ archive_file.parent_id = parent->libraryId();
+ archive_file.path = parent->filename() + "/" + entry->name();
+ archive_file.size = entry->size();
+ archive_file.hash = entry->hash();
+ }
+
+ // Get id of row in database if it exists
+ archive_file.id = archiveFileId(archive_file.path, archive_file.parent_id);
+ auto new_archive_row = archive_file.id < 0;
+
+ // Keep last opened time if the row exists
+ if (!new_archive_row)
+ archive_file.last_opened = archiveFileLastOpened(archive_file.id);
+
+ // Write row to database
+ saveArchiveFile(archive_file);
+
+ // Create archive_ui_config row if needed
+ if (new_archive_row)
+ {
+ ArchiveUIConfigRow ui_config{ archive_file.id, archive.formatInfo().supports_dirs };
+ ui_config.insert();
+ }
+
+ // Write entries to database
+ vector all_entries;
+ archive.putEntryTreeAsList(all_entries);
+ rebuildEntries(archive_file.id, all_entries);
+
+ // Write maps to database
+ updateArchiveMaps(archive_file.id, archive);
+
+ // Remove from pre-3.3.0 recent files list if it's there
+ if (auto rf = find(recent_files.begin(), recent_files.end(), archive_file.path); rf != recent_files.end())
+ recent_files.erase(rf);
+
+ // Finish up
+ archive.setLibraryId(archive_file.id);
+ return archive_file.id;
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error writing archive info to the library: {}", ex.what());
+ return -1;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// (Re)Writes all info for [archive]'s entries into the library
+// -----------------------------------------------------------------------------
+void library::writeArchiveEntryInfo(const Archive& archive)
+{
+ // If it doesn't exist in the library need to add it
+ if (archive.libraryId() < 0)
+ {
+ writeArchiveInfo(archive);
+ return;
+ }
+
+ try
+ {
+ // Write entries to database
+ vector all_entries;
+ archive.putEntryTreeAsList(all_entries);
+ rebuildEntries(archive.libraryId(), all_entries);
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error writing archive entry info to the library: {}", ex.what());
+ }
+}
+
+// -----------------------------------------------------------------------------
+// (Re)Writes all info for [archive]'s maps into the library
+// -----------------------------------------------------------------------------
+void library::writeArchiveMapInfo(const Archive& archive)
+{
+ // If it doesn't exist in the library need to add it
+ if (archive.libraryId() < 0)
+ {
+ writeArchiveInfo(archive);
+ return;
+ }
+
+ try
+ {
+ // Write maps to database
+ updateArchiveMaps(archive.libraryId(), archive);
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error writing archive map info to the library: {}", ex.what());
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Removes all archives in the library that no longer exist on disk
+// -----------------------------------------------------------------------------
+void library::removeMissingArchives()
+{
+ try
+ {
+ vector to_remove;
+
+ if (auto sql = db::cacheQuery("lib_all_archive_paths", "SELECT id, path FROM archive_file WHERE parent_id < 0"))
+ {
+ while (sql->executeStep())
+ {
+ if (!fileutil::fileExists(sql->getColumn(1).getString()))
+ to_remove.push_back(sql->getColumn(0).getInt64());
+ }
+
+ sql->reset();
+ }
+
+ for (auto id : to_remove)
+ {
+ log::info("Removing archive {} from library (no longer exists)", id);
+ removeArchiveFile(id);
+ }
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error removing missing archives from the library: {}", ex.what());
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Returns the filenames of the [count] most recently opened archives
+// -----------------------------------------------------------------------------
+vector library::recentFiles(unsigned count)
+{
+ vector paths;
+
+ try
+ {
+ // Get or create cached query to select base resource paths
+ if (auto sql = db::cacheQuery(
+ "lib_recent_files",
+ "SELECT path FROM archive_file "
+ "WHERE last_opened > 0 AND parent_id < 0 "
+ "ORDER BY last_opened DESC LIMIT ?"))
+ {
+ sql->bind(1, count);
+
+ // Execute query and add results to list
+ while (sql->executeStep())
+ paths.push_back(sql->getColumn(0).getString());
+
+ sql->reset();
+ }
+ }
+ catch (SQLite::Exception& ex)
+ {
+ log::error("Error getting recent files from the library: {}", ex.what());
+ }
+
+ // Append pre-3.3.0 recent files list to make up count if required
+ if (!recent_files.empty() && paths.size() < count)
+ {
+ for (const auto& recent_file : recent_files)
+ {
+ if (paths.size() == count)
+ break;
+
+ paths.push_back(recent_file);
+ }
+ }
+
+ return paths;
+}
+
+// -----------------------------------------------------------------------------
+// Finds and scans all archives in [path] (recursively), adding or updating them
+// in the library. Files with extensions in [ignore_ext] will be ignored.
+//
+// This is safe to run in a background thread, and only one scan can be running
+// at any time
+// -----------------------------------------------------------------------------
+void library::scanArchivesInDir(string_view path, const vector& ignore_ext, bool rebuild)
+{
+ if (lib_scan_running)
+ {
+ // Abort if a scan is already running (eg. in another thread)
+ log::warning("Library scan already running, can only have one running at once");
+ return;
+ }
+
+ auto files = fileutil::allFilesInDir(path, true);
+
+ lib_scan_running = true;
+
+ for (auto& filename : files)
+ {
+ // Sanitize path
+ strutil::replaceIP(filename, "\\", "/");
+
+ // Check extension
+ auto ext = strutil::Path::extensionOf(filename);
+ if (VECTOR_EXISTS(ignore_ext, ext))
+ {
+ log::debug("File {} has ignored extension, skipping", filename);
+ continue;
+ }
+ if (!archive::isKnownExtension(ext))
+ {
+ log::debug("File {} has unknown archive extension, skipping", filename);
+ continue;
+ }
+
+ // Check if the file exists in the library
+ auto lib_id = archiveFileId(filename);
+ if (lib_id >= 0 && !rebuild)
+ {
+ // Check if the file on disk hasn't been modified since it was last updated in the library
+ auto lib_file_modified = archiveFileLastModified(lib_id);
+ if (lib_file_modified == fileutil::fileModifiedTime(filename))
+ {
+ log::info(
+ "Library Scan: File {} is already in library and has not been modified since last scanned",
+ filename);
+ continue;
+ }
+ }
+
+ // Check if file is a known archive format
+ auto format = archive::detectArchiveFormat(filename);
+ if (format != ArchiveFormat::Unknown)
+ {
+ auto archive = std::make_unique(format);
+
+ log::info("Library Scan: Scanning file \"{}\" (detected as {})", filename, archive->formatInfo().name);
+
+ if (!archive->open(filename, true))
+ {
+ log::info("Library Scan: Failed to open archive file {}: {}", filename, global::error);
+ continue;
+ }
+
+ auto id = readArchiveInfo(*archive);
+ if (id < 0 || rebuild)
+ {
+ if (!rebuild)
+ log::info("Library Scan: Archive file doesn't exist in library, adding");
+
+ writeArchiveInfo(*archive);
+ }
+ else
+ log::info("Library Scan: Archive already exists in library");
+ }
+ else
+ log::debug("File {} is not a known/valid archive format, skipping", filename);
+
+ // Check if stop scan was requested
+ if (!lib_scan_running)
+ {
+ log::info("Library Scan: Stop scan requested, ending scan");
+ return;
+ }
+ }
+
+ lib_scan_running = false;
+}
+
+// -----------------------------------------------------------------------------
+// Stops the currently running library scan (if any)
+// -----------------------------------------------------------------------------
+void library::stopArchiveDirScan()
+{
+ if (lib_scan_running)
+ lib_scan_running = false;
+}
+
+// -----------------------------------------------------------------------------
+// Returns true if a library scan is currently running
+// -----------------------------------------------------------------------------
+bool library::archiveDirScanRunning()
+{
+ return lib_scan_running;
+}
+
+// -----------------------------------------------------------------------------
+// Returns all bookmarked entry ids for [archive_id]
+// -----------------------------------------------------------------------------
+vector library::bookmarkedEntries(int64_t archive_id)
+{
+ vector entry_ids;
+
+ if (auto sql = db::cacheQuery(
+ "archive_all_bookmarks", "SELECT entry_id FROM archive_bookmark WHERE archive_id = ?"))
+ {
+ sql->bind(1, archive_id);
+ while (sql->executeStep())
+ entry_ids.push_back(sql->getColumn(0).getInt64());
+ sql->reset();
+ }
+
+ return entry_ids;
+}
+
+// -----------------------------------------------------------------------------
+// Adds a bookmarked entry to the library
+// -----------------------------------------------------------------------------
+void library::addBookmark(int64_t archive_id, int64_t entry_id)
+{
+ if (archive_id < 0 || entry_id < 0)
+ return;
+
+ if (auto sql = db::cacheQuery("insert_archive_bookmark", insert_archive_bookmark, true))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, entry_id);
+ sql->exec();
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Removes a bookmarked entry from the library
+// -----------------------------------------------------------------------------
+void library::removeBookmark(int64_t archive_id, int64_t entry_id)
+{
+ if (archive_id < 0 || entry_id < 0)
+ return;
+
+ if (auto sql = db::cacheQuery(
+ "delete_archive_bookmark", "DELETE FROM archive_bookmark WHERE archive_id = ? AND entry_id = ?", true))
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, entry_id);
+ sql->exec();
+ sql->reset();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Removes all bookmarked entries for [archive_id] in the library
+// -----------------------------------------------------------------------------
+void library::removeArchiveBookmarks(int64_t archive_id)
+{
+ db::connectionRW()->exec(fmt::format("DELETE FROM archive_bookmark WHERE archive_id = {}", archive_id));
+}
+
+// -----------------------------------------------------------------------------
+// Writes multiple bookmarked entries to the library
+// -----------------------------------------------------------------------------
+void library::writeArchiveBookmarks(int64_t archive_id, const vector& entry_ids)
+{
+ auto connection = db::connectionRW();
+
+ // Delete existing bookmarks in library first
+ removeArchiveBookmarks(archive_id);
+
+ // Insert bookmark rows
+ if (auto sql = db::cacheQuery("insert_archive_bookmark", insert_archive_bookmark, true))
+ {
+ db::Transaction transaction{ connection, false };
+ transaction.beginIfNoActiveTransaction();
+
+ for (const auto& entry_id : entry_ids)
+ {
+ sql->bind(1, archive_id);
+ sql->bind(2, entry_id);
+ sql->exec();
+ sql->reset();
+ }
+
+ transaction.commit();
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Attempts to find the EntryType id of the given [entry] by finding an entry
+// in the library with the exact same name and data
+// -----------------------------------------------------------------------------
+string library::findEntryTypeId(const ArchiveEntry& entry)
+{
+ if (entry.size() == 0)
+ return "marker";
+
+ string format_id;
+
+ if (auto sql = db::cacheQuery(
+ "find_entry_type_id", "SELECT type_id FROM archive_entry WHERE name = ? AND hash = ?"))
+ {
+ sql->bind(1, entry.name());
+ sql->bind(2, entry.hash());
+
+ if (sql->executeStep())
+ format_id = sql->getColumn(0).getString();
+
+ sql->reset();
+ }
+
+ return format_id;
+}
+
+// -----------------------------------------------------------------------------
+// Reads the pre-3.3.0 recent_files section in slade3.cfg
+// -----------------------------------------------------------------------------
+void library::readPre330RecentFiles(Tokenizer& tz)
+{
+ while (!tz.checkOrEnd("}"))
+ {
+ // Read recent file path
+ auto path = wxString::FromUTF8(tz.current().text.c_str()).ToStdString();
+ tz.adv();
+
+ // Check the path is valid
+ if (!(fileutil::fileExists(path) || fileutil::dirExists(path)))
+ continue;
+
+ recent_files.insert(recent_files.begin(), path);
+ }
+
+ tz.adv(); // Skip ending }
+}
+
+// -----------------------------------------------------------------------------
+// Returns the library signals struct
+// -----------------------------------------------------------------------------
+Signals& library::signals()
+{
+ return lib_signals;
+}
+
+
+
+#include "General/Console.h"
+
+CONSOLE_COMMAND(lib_cleanup, 0, true)
+{
+ log::console("Removing missing archives...");
+ library::removeMissingArchives();
+
+ log::console("Library cleanup complete");
+}
+
+CONSOLE_COMMAND(lib_scan, 1, true)
+{
+ if (args[0] == "stop")
+ {
+ // Stop scan requested
+ lib_scan_running = false;
+ log::console("Library scan stop requested, will stop after the current archive is finished scanning");
+ return;
+ }
+
+ vector ignore_ext; //{ "zip" };
+
+ // Start scan in background thread
+ std::thread scan_thread(
+ [ignore_ext, args]
+ {
+ // Create+Register database connection context for thread
+ db::Context ctx{ db::programDatabasePath() };
+ db::registerThreadContext(ctx);
+
+ bool rebuild = false;
+ if (args.size() >= 2 && args[1] == "rebuild")
+ rebuild = true;
+
+ library::scanArchivesInDir(args[0], ignore_ext, rebuild);
+
+ db::deregisterThreadContexts();
+ });
+ scan_thread.detach();
+}
diff --git a/src/Library/Library.h b/src/Library/Library.h
new file mode 100644
index 000000000..1cc9d3398
--- /dev/null
+++ b/src/Library/Library.h
@@ -0,0 +1,49 @@
+#pragma once
+
+namespace slade
+{
+class Archive;
+class ArchiveEntry;
+class Tokenizer;
+
+namespace library
+{
+ struct Signals
+ {
+ sigslot::signal archive_file_updated;
+ sigslot::signal archive_file_inserted;
+ sigslot::signal archive_file_deleted;
+ };
+
+ // General
+ void init();
+ Signals& signals();
+
+ // Archives
+ int64_t readArchiveInfo(const Archive& archive);
+ void setArchiveLastOpenedTime(int64_t archive_id, time_t last_opened);
+ int64_t writeArchiveInfo(const Archive& archive);
+ void writeArchiveEntryInfo(const Archive& archive);
+ void writeArchiveMapInfo(const Archive& archive);
+ void removeMissingArchives();
+ vector recentFiles(unsigned count = 20);
+
+ // Archive Dir Scan
+ void scanArchivesInDir(string_view path, const vector& ignore_ext, bool rebuild = false);
+ void stopArchiveDirScan();
+ bool archiveDirScanRunning();
+
+ // Bookmarks
+ vector bookmarkedEntries(int64_t archive_id);
+ void addBookmark(int64_t archive_id, int64_t entry_id);
+ void removeBookmark(int64_t archive_id, int64_t entry_id);
+ void removeArchiveBookmarks(int64_t archive_id);
+ void writeArchiveBookmarks(int64_t archive_id, const vector& entry_ids);
+
+ // Entries
+ string findEntryTypeId(const slade::ArchiveEntry& entry);
+
+ // Recent Files (for pre-3.3.0 compatibility, remove in 3.4.0)
+ void readPre330RecentFiles(Tokenizer& tz);
+} // namespace library
+} // namespace slade
diff --git a/src/Library/UI/LibraryPanel.cpp b/src/Library/UI/LibraryPanel.cpp
new file mode 100644
index 000000000..44dc26794
--- /dev/null
+++ b/src/Library/UI/LibraryPanel.cpp
@@ -0,0 +1,595 @@
+
+#include "Main.h"
+#include "LibraryPanel.h"
+#include "App.h"
+#include "Archive/Archive.h"
+#include "Archive/ArchiveFormat.h"
+#include "Archive/ArchiveManager.h"
+#include "Database/Context.h"
+#include "Database/Database.h"
+#include "General/Misc.h"
+#include "General/SAction.h"
+#include "General/UI.h"
+#include "Graphics/Icons.h"
+#include "Library/ArchiveFile.h"
+#include "Library/Library.h"
+#include "MainEditor/MainEditor.h"
+#include "UI/Dialogs/RunDialog.h"
+#include "UI/SToolBar/SToolBar.h"
+#include "UI/State.h"
+#include "UI/WxUtils.h"
+#include "Utility/DateTime.h"
+#include "Utility/StringUtils.h"
+#include
+
+using namespace slade;
+using namespace ui;
+using namespace library;
+
+
+namespace
+{
+icons::IconCache icon_cache;
+}
+
+
+namespace
+{
+template int compare(const T& left, const T& right)
+{
+ if (left == right)
+ return 0;
+
+ return left < right ? -1 : 1;
+}
+
+void openArchive(const LibraryViewModel::LibraryListRow* row)
+{
+ // Check if it's an archive within another archive
+ if (row->parent_id >= 0)
+ {
+ // Open parent archive
+ ArchiveFileRow parent_row{ db::global(), row->parent_id };
+ maineditor::openArchiveFile(parent_row.path);
+
+ // Now open the archive's entry within the parent
+ auto parent_archive = app::archiveManager().getArchive(parent_row.path);
+ if (auto entry = parent_archive->entryAtPath(strutil::replace(row->path, parent_row.path, {})))
+ maineditor::openEntry(entry);
+
+ return;
+ }
+
+ maineditor::openArchiveFile(row->path);
+}
+} // namespace
+
+LibraryViewModel::LibraryViewModel()
+{
+ loadRows();
+ Cleared();
+
+ // Connect to library signals ----------------------------------------------
+ auto& signals = library::signals();
+
+ // Archive updated
+ signal_connections_ += signals.archive_file_updated.connect(
+ [this](int64_t id)
+ {
+ bool changed = false;
+ for (auto& row : rows_)
+ if (row.id == id)
+ {
+ row = LibraryListRow{ db::global(), id };
+ ItemChanged(wxDataViewItem{ &row });
+ changed = true;
+ }
+
+ if (changed)
+ Resort();
+ });
+
+ // Archive added
+ signal_connections_ += signals.archive_file_inserted.connect(
+ [this](int64_t id)
+ {
+ rows_.emplace_back(database::global(), id);
+ ItemAdded({}, wxDataViewItem{ &rows_.back() });
+ Resort();
+ });
+
+ // Archive deleted
+ signal_connections_ += signals.archive_file_deleted.connect(
+ [this](int64_t id)
+ {
+ for (auto i = 0; i < rows_.size(); ++i)
+ if (rows_[i].id == id)
+ {
+ ItemDeleted({}, wxDataViewItem{ &rows_[i] });
+ rows_.erase(rows_.begin() + i);
+ Resort();
+ break;
+ }
+ });
+}
+
+LibraryViewModel::LibraryListRow::LibraryListRow(database::Context& db, int64_t id)
+{
+ if (auto sql = db.cacheQuery("get_library_list", "SELECT * FROM archive_library_list WHERE id = ?"))
+ {
+ sql->bind(1, id);
+
+ if (sql->executeStep())
+ {
+ this->id = id;
+ path = sql->getColumn(1).getString();
+ size = sql->getColumn(2).getUInt();
+ format_id = sql->getColumn(3).getString();
+ last_opened = sql->getColumn(4).getInt64();
+ last_modified = sql->getColumn(5).getInt64();
+ parent_id = sql->getColumn(6).getInt64();
+ entry_count = sql->getColumn(7).getUInt();
+ map_count = sql->getColumn(8).getUInt();
+ }
+
+ sql->reset();
+ }
+}
+
+wxDataViewItem LibraryViewModel::itemForArchiveId(int64_t id) const
+{
+ for (auto& row : rows_)
+ if (row.id == id)
+ return wxDataViewItem{ &row };
+
+ return {};
+}
+
+void LibraryViewModel::setFilter(string_view filter)
+{
+ filter_ = filter;
+
+ Cleared();
+}
+
+wxString LibraryViewModel::GetColumnType(unsigned col) const
+{
+ switch (static_cast(col))
+ {
+ case Column::Name: return "wxDataViewIconText";
+ default: return "string";
+ }
+}
+
+void LibraryViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned col) const
+{
+ auto* row = static_cast(item.GetID());
+ if (!row)
+ return;
+
+ switch (static_cast(col))
+ {
+ case Column::Name:
+ {
+ // Determine icon
+ string icon = "archive";
+ if (row->format_id == "wad")
+ icon = "wad";
+ else if (row->format_id == "zip")
+ icon = "zip";
+ else if (row->format_id == "folder")
+ icon = "folder";
+
+ // Find icon in cache
+ if (!icon_cache.isCached(icon))
+ {
+ // Not found, add to cache
+ constexpr auto pad = Point2i{ 1, 1 };
+ icon_cache.cacheIcon(icons::Type::Entry, icon, 16, pad);
+ }
+
+ auto fn = wxutil::strFromView(strutil::Path::fileNameOf(row->path));
+
+ variant << wxDataViewIconText(fn, icon_cache.icons[icon]);
+
+ break;
+ }
+ case Column::Path: variant = wxutil::strFromView(strutil::Path::pathOf(row->path, false)); break;
+ case Column::Size: variant = misc::sizeAsString(row->size); break;
+ case Column::Type:
+ {
+ auto fn_ext = strutil::Path::extensionOf(row->path);
+ auto desc = archive::formatInfoFromId(row->format_id);
+
+ variant = desc.name;
+
+ for (const auto& ext : desc.extensions)
+ if (strutil::equalCI(fn_ext, ext.first))
+ variant = ext.second;
+
+ break;
+ }
+ case Column::LastOpened:
+ if (row->last_opened == 0)
+ variant = "Never";
+ else
+ variant = datetime::toString(row->last_opened, datetime::Format::Local);
+ break;
+ case Column::FileModified:
+ if (row->last_modified == 0)
+ variant = "Unknown";
+ else
+ variant = datetime::toString(row->last_modified, datetime::Format::Local);
+ break;
+ case Column::EntryCount: variant = wxString::Format("%d", row->entry_count); break;
+ case Column::MapCount: variant = row->map_count > 0 ? wxString ::Format("%d", row->map_count) : ""; break;
+ default: break;
+ }
+}
+
+bool LibraryViewModel::GetAttr(const wxDataViewItem& item, unsigned col, wxDataViewItemAttr& attr) const
+{
+ return wxDataViewModel::GetAttr(item, col, attr);
+}
+
+bool LibraryViewModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned col)
+{
+ return false;
+}
+
+wxDataViewItem LibraryViewModel::GetParent(const wxDataViewItem& item) const
+{
+ return {};
+}
+
+bool LibraryViewModel::IsContainer(const wxDataViewItem& item) const
+{
+ return !item.IsOk();
+}
+
+unsigned LibraryViewModel::GetChildren(const wxDataViewItem& item, wxDataViewItemArray& children) const
+{
+ if (!item.IsOk())
+ {
+ // Root item
+ for (auto& row : rows_)
+ if (matchesFilter(row))
+ children.Add(wxDataViewItem{ &row });
+
+ return children.size();
+ }
+
+ return 0;
+}
+
+int LibraryViewModel::Compare(const wxDataViewItem& item1, const wxDataViewItem& item2, unsigned column, bool ascending)
+ const
+{
+ auto row1 = static_cast(item1.GetID());
+ auto row2 = static_cast(item2.GetID());
+ if (!row1 || !row2)
+ return 0;
+
+ auto col = static_cast(column);
+
+ // Size column
+ if (col == Column::Size)
+ return ascending ? compare(row1->size, row2->size) : compare(row2->size, row1->size);
+
+ // Last Opened column
+ if (col == Column::LastOpened)
+ return ascending ? compare(row1->last_opened, row2->last_opened) :
+ compare(row2->last_opened, row1->last_opened);
+
+ // File Modified column
+ if (col == Column::FileModified)
+ return ascending ? compare(row1->last_modified, row2->last_modified) :
+ compare(row2->last_modified, row1->last_modified);
+
+ // Entry Count column
+ if (col == Column::EntryCount)
+ return ascending ? compare(row1->entry_count, row2->entry_count) :
+ compare(row2->entry_count, row1->entry_count);
+
+ // Map Count column
+ if (col == Column::MapCount)
+ return ascending ? compare(row1->map_count, row2->map_count) : compare(row2->map_count, row1->map_count);
+
+ // Default compare
+ return wxDataViewModel::Compare(item1, item2, column, ascending);
+}
+
+void LibraryViewModel::loadRows() const
+{
+ rows_.clear();
+
+ if (auto sql = db::cacheQuery("library_list", "SELECT * FROM archive_library_list"))
+ {
+ LibraryListRow row;
+ while (sql->executeStep())
+ {
+ row.id = sql->getColumn(0).getInt64();
+ row.path = sql->getColumn(1).getString();
+ row.size = sql->getColumn(2).getUInt();
+ row.format_id = sql->getColumn(3).getString();
+ row.last_opened = sql->getColumn(4).getInt64();
+ row.last_modified = sql->getColumn(5).getInt64();
+ row.parent_id = sql->getColumn(6).getInt64();
+ row.entry_count = sql->getColumn(7).getUInt();
+ row.map_count = sql->getColumn(8).getUInt();
+
+ rows_.push_back(row);
+ }
+
+ sql->reset();
+ }
+}
+
+bool LibraryViewModel::matchesFilter(const LibraryListRow& row) const
+{
+ // Check for filename match if needed
+ if (!filter_.empty())
+ return strutil::matchesCI(strutil::Path::fileNameOf(row.path), fmt::format("*{}*", filter_));
+
+ return true;
+}
+
+LibraryPanel::LibraryPanel(wxWindow* parent) : wxPanel{ parent }
+{
+ setup();
+}
+
+void LibraryPanel::setup()
+{
+ auto sizer = new wxBoxSizer(wxVERTICAL);
+ SetSizer(sizer);
+
+ // Toolbar
+ setupToolbar();
+ sizer->Add(toolbar_, wxutil::sfWithBorder(0, wxLEFT | wxRIGHT | wxTOP).Expand());
+ sizer->AddSpacer(px(Size::Pad));
+
+ // Archive list
+ list_archives_ = new SDataViewCtrl{ this, wxDV_MULTIPLE };
+ sizer->Add(list_archives_, wxutil::sfWithBorder(1, wxLEFT | wxRIGHT | wxBOTTOM).Expand());
+
+ // Init archive list
+ model_library_ = new LibraryViewModel();
+ list_archives_->AssociateModel(model_library_);
+ model_library_->DecRef();
+ setupListColumns();
+
+ bindEvents();
+}
+
+void LibraryPanel::setupToolbar()
+{
+ toolbar_ = new SToolBar(this);
+
+ // Actions
+ toolbar_->addActionGroup("_Library", { "alib_open", "alib_run", "alib_remove" });
+
+ // Filter
+ auto tbg_filter = new SToolBarGroup(toolbar_, "Filter");
+ text_filter_ = new wxTextCtrl(tbg_filter, -1);
+ tbg_filter->addCustomControl(new wxStaticText(tbg_filter, -1, "Filter:"));
+ tbg_filter->addCustomControl(text_filter_);
+ toolbar_->addGroup(tbg_filter, true);
+}
+
+bool LibraryPanel::handleAction(string_view id)
+{
+ // Open
+ if (id == "alib_open")
+ {
+ wxDataViewItemArray selection;
+ list_archives_->GetSelections(selection);
+
+ for (const auto& item : selection)
+ if (auto row = model_library_->rowForItem(item))
+ openArchive(row);
+
+ return true;
+ }
+
+ // Remove
+ if (id == "alib_remove")
+ {
+ wxDataViewItemArray selection;
+ list_archives_->GetSelections(selection);
+
+ vector to_remove;
+ for (const auto& item : selection)
+ if (auto row = model_library_->rowForItem(item))
+ to_remove.push_back(row->id);
+
+ for (auto archive_id : to_remove)
+ library::removeArchiveFile(archive_id);
+
+ return true;
+ }
+
+ // Run
+ if (id == "alib_run")
+ {
+ wxDataViewItemArray selection;
+ list_archives_->GetSelections(selection);
+
+ string path;
+ int64_t id = -1;
+ for (const auto& item : selection)
+ if (auto row = model_library_->rowForItem(item))
+ {
+ path = row->path;
+ id = row->id;
+ break;
+ }
+
+ RunDialog dlg{ this, id };
+ if (dlg.ShowModal() == wxID_OK)
+ dlg.run(RunDialog::Config{ path }, id);
+
+ return true;
+ }
+
+ return false;
+}
+
+void LibraryPanel::bindEvents()
+{
+ // Open archive if activated
+ list_archives_->Bind(
+ wxEVT_DATAVIEW_ITEM_ACTIVATED,
+ [this](wxDataViewEvent& e)
+ {
+ if (auto row = model_library_->rowForItem(e.GetItem()))
+ openArchive(row);
+ });
+
+ // Context menu
+ list_archives_->Bind(
+ wxEVT_DATAVIEW_ITEM_CONTEXT_MENU,
+ [this](wxDataViewEvent& e)
+ {
+ wxMenu context;
+ SAction::fromId("alib_open")->addToMenu(&context);
+ SAction::fromId("alib_run")->addToMenu(&context);
+ SAction::fromId("alib_remove")->addToMenu(&context);
+ list_archives_->PopupMenu(&context);
+ });
+
+ // Header right click
+ list_archives_->Bind(
+ wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK,
+ [this](wxDataViewEvent& e)
+ {
+ using Column = LibraryViewModel::Column;
+
+ // Popup context menu
+ wxMenu context;
+ context.Append(static_cast(Column::__Count), "Reset Sorting");
+ context.AppendSeparator();
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::Path));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::Size));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::Type));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::LastOpened));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::FileModified));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::EntryCount));
+ list_archives_->appendColumnToggleItem(context, static_cast(Column::MapCount));
+ list_archives_->PopupMenu(&context);
+ e.Skip();
+ });
+
+ // Header context menu
+ list_archives_->Bind(
+ wxEVT_MENU,
+ [this](wxCommandEvent& e)
+ {
+ using Column = LibraryViewModel::Column;
+
+ string toggle_column;
+ switch (static_cast(e.GetId()))
+ {
+ case Column::Name: break;
+ case Column::Path: toggle_column = "LibraryPanelPathVisible"; break;
+ case Column::Size: toggle_column = "LibraryPanelSizeVisible"; break;
+ case Column::Type: toggle_column = "LibraryPanelTypeVisible"; break;
+ case Column::LastOpened: toggle_column = "LibraryPanelLastOpenedVisible"; break;
+ case Column::FileModified: toggle_column = "LibraryPanelFileModifiedVisible"; break;
+ case Column::EntryCount: toggle_column = "LibraryPanelEntryCountVisible"; break;
+ case Column::MapCount: toggle_column = "LibraryPanelMapCountVisible"; break;
+ case Column::__Count: list_archives_->resetSorting(); break;
+ default: e.Skip(); break;
+ }
+
+ if (!toggle_column.empty())
+ {
+ list_archives_->toggleColumnVisibility(e.GetId(), toggle_column);
+ updateColumnWidths();
+ }
+ });
+
+ // List column resized
+ list_archives_->Bind(
+ EVT_SDVC_COLUMN_RESIZED,
+ [this](wxDataViewEvent& e)
+ {
+ using Column = LibraryViewModel::Column;
+ auto col = static_cast(e.GetColumn());
+ auto width = e.GetDataViewColumn()->GetWidth();
+
+ switch (col)
+ {
+ case Column::Name: saveStateInt("LibraryPanelFilenameWidth", width); break;
+ case Column::Path: saveStateInt("LibraryPanelPathWidth", width); break;
+ case Column::Size: saveStateInt("LibraryPanelSizeWidth", width); break;
+ case Column::Type: saveStateInt("LibraryPanelTypeWidth", width); break;
+ case Column::LastOpened: saveStateInt("LibraryPanelLastOpenedWidth", width); break;
+ case Column::FileModified: saveStateInt("LibraryPanelFileModifiedWidth", width); break;
+ case Column::EntryCount: saveStateInt("LibraryPanelEntryCountWidth", width); break;
+ case Column::MapCount: saveStateInt("LibraryPanelMapCountWidth", width); break;
+ default: break;
+ }
+ });
+
+ // Filter changed
+ text_filter_->Bind(
+ wxEVT_TEXT,
+ [this](wxCommandEvent& e) { model_library_->setFilter(wxutil::strToView(text_filter_->GetValue())); });
+}
+
+void LibraryPanel::setupListColumns() const
+{
+ using Column = LibraryViewModel::Column;
+
+ // Search by filename column
+ list_archives_->setSearchColumn(static_cast(Column::Name));
+
+ // Filename column is fixed
+ list_archives_->AppendIconTextColumn(
+ "Filename",
+ static_cast(Column::Name),
+ wxDATAVIEW_CELL_INERT,
+ getStateInt("LibraryPanelFilenameWidth"),
+ wxALIGN_NOT,
+ wxDATAVIEW_COL_SORTABLE | wxDATAVIEW_COL_RESIZABLE);
+
+ // Add other columns
+ appendTextColumn(Column::Path, "Path", "Path");
+ appendTextColumn(Column::Size, "Size", "Size");
+ appendTextColumn(Column::Type, "Type", "Type");
+ appendTextColumn(Column::LastOpened, "Last Opened", "LastOpened");
+ appendTextColumn(Column::FileModified, "File Modified", "FileModified");
+ appendTextColumn(Column::EntryCount, "# Entries", "EntryCount");
+ appendTextColumn(Column::MapCount, "# Maps", "MapCount");
+}
+
+void LibraryPanel::updateColumnWidths()
+{
+ using Column = LibraryViewModel::Column;
+
+ Freeze();
+ list_archives_->setColumnWidth(modelColumn(Column::Name), getStateInt("LibraryPanelFilenameWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::Path), getStateInt("LibraryPanelPathWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::Size), getStateInt("LibraryPanelSizeWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::Type), getStateInt("LibraryPanelTypeWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::LastOpened), getStateInt("LibraryPanelLastOpenedWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::FileModified), getStateInt("LibraryPanelFileModifiedWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::EntryCount), getStateInt("LibraryPanelEntryCountWidth"));
+ list_archives_->setColumnWidth(modelColumn(Column::MapCount), getStateInt("LibraryPanelMapCountWidth"));
+ Thaw();
+}
+
+void LibraryPanel::appendTextColumn(LibraryViewModel::Column column, string_view title, string_view id) const
+{
+ static auto colstyle_visible = wxDATAVIEW_COL_SORTABLE | wxDATAVIEW_COL_RESIZABLE;
+ static auto colstyle_hidden = colstyle_visible | wxDATAVIEW_COL_HIDDEN;
+
+ list_archives_->AppendTextColumn(
+ wxutil::strFromView(title),
+ static_cast(column),
+ wxDATAVIEW_CELL_INERT,
+ getStateInt(fmt::format("LibraryPanel{}Width", id)),
+ wxALIGN_NOT,
+ getStateBool(fmt::format("LibraryPanel{}Visible", id)) ? colstyle_visible : colstyle_hidden);
+}
diff --git a/src/Library/UI/LibraryPanel.h b/src/Library/UI/LibraryPanel.h
new file mode 100644
index 000000000..8ccf768cb
--- /dev/null
+++ b/src/Library/UI/LibraryPanel.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include "General/SActionHandler.h"
+#include "General/Sigslot.h"
+#include "Library/ArchiveFile.h"
+#include "UI/Lists/SDataViewCtrl.h"
+
+namespace slade
+{
+class SToolBar;
+
+namespace ui
+{
+ class LibraryViewModel : public wxDataViewModel
+ {
+ public:
+ LibraryViewModel();
+ ~LibraryViewModel() override = default;
+
+ enum class Column
+ {
+ Name = 0,
+ Path,
+ Size,
+ Type,
+ LastOpened,
+ FileModified,
+ EntryCount,
+ MapCount,
+
+ __Count
+ };
+
+ struct LibraryListRow
+ {
+ int64_t id = -1;
+ string path;
+ unsigned size = 0;
+ string format_id;
+ time_t last_opened = 0;
+ time_t last_modified = 0;
+ int64_t parent_id = -1;
+ unsigned entry_count = 0;
+ unsigned map_count = 0;
+
+ LibraryListRow() = default;
+ LibraryListRow(database::Context& db, int64_t id);
+ };
+
+ wxDataViewItem itemForArchiveId(int64_t id) const;
+ LibraryListRow* rowForItem(const wxDataViewItem& item) const
+ {
+ return static_cast(item.GetID());
+ }
+
+ void setFilter(string_view filter);
+
+ private:
+ mutable vector rows_;
+ ScopedConnectionList signal_connections_;
+ string filter_;
+
+ // wxDataViewModel
+ unsigned int GetColumnCount() const override { return static_cast