From 3fb8be385bf9d84343b0f7bd8ad707edce991441 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 20 Apr 2025 04:49:39 +0200 Subject: [PATCH] Add support for MPQ packed mods and turn Hellfire into one --- CMake/Assets.cmake | 1 + Source/CMakeLists.txt | 1 + Source/DiabloUI/selstart.cpp | 1 + Source/DiabloUI/settingsmenu.cpp | 2 -- Source/diablo.cpp | 18 +++++++++++----- Source/discord/discord.cpp | 6 +++--- Source/engine/assets.cpp | 35 ++++++++++++++++++++++--------- Source/engine/assets.hpp | 3 +++ Source/game_mode.cpp | 16 -------------- Source/game_mode.hpp | 5 ----- Source/lua/lua.cpp | 21 ++++++++++++++++++- Source/lua/lua.hpp | 4 ++++ Source/lua/modules/audio.cpp | 2 +- Source/lua/modules/hellfire.cpp | 18 ++++++++++++++++ Source/lua/modules/hellfire.hpp | 9 ++++++++ Source/options.cpp | 18 ++++++++++++++-- Source/options.h | 5 ++--- assets/lua/mods/Hellfire/init.lua | 4 ++++ 18 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 Source/lua/modules/hellfire.cpp create mode 100644 Source/lua/modules/hellfire.hpp create mode 100644 assets/lua/mods/Hellfire/init.lua diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index d2e1b0e72..43919f68c 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -151,6 +151,7 @@ set(devilutionx_assets lua/devilutionx/events.lua lua/inspect.lua lua/mods/clock/init.lua + lua/mods/Hellfire/init.lua lua/repl_prelude.lua nlevels/cutl5w.clx nlevels/cutl6w.clx diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index f0aa29904..4d4620972 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -107,6 +107,7 @@ set(libdevilutionx_SRCS lua/autocomplete.cpp lua/lua.cpp lua/modules/audio.cpp + lua/modules/hellfire.cpp lua/modules/dev.cpp lua/modules/dev/display.cpp lua/modules/dev/items.cpp diff --git a/Source/DiabloUI/selstart.cpp b/Source/DiabloUI/selstart.cpp index 2d468f6be..0e755a1df 100644 --- a/Source/DiabloUI/selstart.cpp +++ b/Source/DiabloUI/selstart.cpp @@ -19,6 +19,7 @@ void ItemSelected(size_t value) { auto option = static_cast(vecDialogItems[value]->m_value); GetOptions().GameMode.gameMode.SetValue(option); + GetOptions().Mods.SetHellfireEnabled(option == StartUpGameMode::Hellfire); SaveOptions(); endMenu = true; } diff --git a/Source/DiabloUI/settingsmenu.cpp b/Source/DiabloUI/settingsmenu.cpp index a21d8900d..ef1c33f28 100644 --- a/Source/DiabloUI/settingsmenu.cpp +++ b/Source/DiabloUI/settingsmenu.cpp @@ -68,8 +68,6 @@ bool IsValidEntry(OptionEntryBase *pOptionEntry) auto flags = pOptionEntry->GetFlags(); if (HasAnyOf(flags, OptionEntryFlags::NeedDiabloMpq) && !HaveIntro()) return false; - if (HasAnyOf(flags, OptionEntryFlags::NeedHellfireMpq) && !HaveHellfire()) - return false; return HasNoneOf(flags, OptionEntryFlags::Invisible | (gbIsHellfire ? OptionEntryFlags::OnlyDiablo : OptionEntryFlags::OnlyHellfire)); } diff --git a/Source/diablo.cpp b/Source/diablo.cpp index afe7e4d31..0f4f4776e 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1192,10 +1192,18 @@ void DiabloInit() { if (forceSpawn || *GetOptions().GameMode.shareware) gbIsSpawn = true; - if (forceDiablo || *GetOptions().GameMode.gameMode == StartUpGameMode::Diablo) - gbIsHellfire = false; - if (forceHellfire) - gbIsHellfire = true; + + bool wasHellfireDiscovered = false; + if (!forceDiablo && !forceHellfire) + wasHellfireDiscovered = (HaveHellfire() && *GetOptions().GameMode.gameMode == StartUpGameMode::Ask); + bool enableHellfire = forceHellfire || wasHellfireDiscovered; + if (!forceDiablo && *GetOptions().GameMode.gameMode == StartUpGameMode::Hellfire) { // Migrate legacy options + GetOptions().GameMode.gameMode.SetValue(StartUpGameMode::Diablo); + enableHellfire = true; + } + if (forceDiablo || enableHellfire) { + GetOptions().Mods.SetHellfireEnabled(enableHellfire); + } gbIsHellfireSaveGame = gbIsHellfire; @@ -1213,7 +1221,7 @@ void DiabloInit() UiInitialize(); was_ui_init = true; - if (gbIsHellfire && !forceHellfire && *GetOptions().GameMode.gameMode == StartUpGameMode::Ask) { + if (wasHellfireDiscovered) { UiSelStartUpGameOption(); if (!gbIsHellfire) { // Reinitialize the UI Elements because we changed the game diff --git a/Source/discord/discord.cpp b/Source/discord/discord.cpp index 7ec110b1c..c4d3f5467 100644 --- a/Source/discord/discord.cpp +++ b/Source/discord/discord.cpp @@ -17,9 +17,9 @@ #include #include "config.h" -#include "game_mode.hpp" #include "levels/gendung.h" #include "levels/setmaps.h" +#include "lua/lua.hpp" #include "multi.h" #include "panels/charpanel.hpp" #include "player.h" @@ -29,11 +29,11 @@ namespace devilution { namespace { -void IsHellfireChanged() +void ModChanged() { discord_manager::UpdateMenu(true); } -const auto IsHellfireChangedHandler = (AddIsHellfireChangeHandler(IsHellfireChanged), true); +const auto ModChangedHandler = (AddModsChangedHandler(ModChanged), true); } // namespace namespace discord_manager { diff --git a/Source/engine/assets.cpp b/Source/engine/assets.cpp index f3c5b5238..ab89442f7 100644 --- a/Source/engine/assets.cpp +++ b/Source/engine/assets.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include "appfat.h" #include "game_mode.hpp" @@ -76,9 +75,6 @@ bool FindMpqFile(std::string_view filename, MpqArchive **archive, uint32_t *file // Iterate over archives in reverse order to prefer files from high priority archives for (auto rit = MpqArchives.rbegin(); rit != MpqArchives.rend(); ++rit) { - int priority = rit->first; - if (!gbIsHellfire && priority >= 8000 && priority < 9000) - continue; if (rit->second->GetFileNumber(fileHash, *fileNumber)) { *archive = rit->second.get(); return true; @@ -423,8 +419,6 @@ void LoadGameArchives() } } hellfire_data_path = FindUnpackedMpqData(paths, "hellfire"); - if (hellfire_data_path) - gbIsHellfire = true; if (forceHellfire && !hellfire_data_path) InsertCDDlg("hellfire"); #else // !UNPACKED_MPQS @@ -442,15 +436,11 @@ void LoadGameArchives() } } - if (HasHellfireMpq) - gbIsHellfire = true; if (forceHellfire && !HasHellfireMpq) InsertCDDlg("hellfire.mpq"); LoadMPQ(paths, "hfbard.mpq", 8110); LoadMPQ(paths, "hfbarb.mpq", 8120); #endif - if (gbIsHellfire) - LoadHellfireArchives(); } void LoadHellfireArchives() @@ -473,4 +463,29 @@ void LoadHellfireArchives() DisplayFatalErrorAndExit(_("Some Hellfire MPQs are missing"), _("Not all Hellfire MPQs were found.\nPlease copy all the hf*.mpq files.")); } +void UnloadModArchives() +{ +#ifndef UNPACKED_MPQS + for (auto it = MpqArchives.begin(); it != MpqArchives.end();) { + if ((it->first >= 8000 && it->first < 9000) || it->first >= 10000) { + it = MpqArchives.erase(it); // erase returns the next valid iterator + } else { + ++it; + } + } +#endif +} + +void LoadModArchives(std::span modnames) +{ +#ifndef UNPACKED_MPQS + int priority = 10000; + const std::string modsPath = StrCat(paths::PrefPath(), "mods/"); + for (std::string_view modname : modnames) { + LoadMPQ({ modsPath }, StrCat(modname, ".mpq"), priority); + priority++; + } +#endif +} + } // namespace devilution diff --git a/Source/engine/assets.hpp b/Source/engine/assets.hpp index bba39b86e..f466ce770 100644 --- a/Source/engine/assets.hpp +++ b/Source/engine/assets.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -297,6 +298,8 @@ void LoadCoreArchives(); void LoadLanguageArchive(); void LoadGameArchives(); void LoadHellfireArchives(); +void UnloadModArchives(); +void LoadModArchives(std::span modnames); #ifdef UNPACKED_MPQS #ifdef BUILD_TESTING diff --git a/Source/game_mode.cpp b/Source/game_mode.cpp index 7830961b1..53bd4be96 100644 --- a/Source/game_mode.cpp +++ b/Source/game_mode.cpp @@ -6,17 +6,6 @@ namespace devilution { namespace { -std::vector> IsHellfireChangeHandlers; - -void OptionGameModeChanged() -{ - gbIsHellfire = *GetOptions().GameMode.gameMode == StartUpGameMode::Hellfire; - for (tl::function_ref handler : IsHellfireChangeHandlers) { - handler(); - } -} -const auto OptionChangeHandlerGameMode = (GetOptions().GameMode.gameMode.SetValueChangedCallback(OptionGameModeChanged), true); - void OptionSharewareChanged() { gbIsSpawn = *GetOptions().GameMode.shareware; @@ -29,9 +18,4 @@ bool gbIsHellfire; bool gbVanilla; bool forceHellfire; -void AddIsHellfireChangeHandler(tl::function_ref callback) -{ - IsHellfireChangeHandlers.push_back(callback); -} - } // namespace devilution diff --git a/Source/game_mode.hpp b/Source/game_mode.hpp index 0be30d72e..60ac5e8f0 100644 --- a/Source/game_mode.hpp +++ b/Source/game_mode.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include "utils/attributes.h" namespace devilution { @@ -15,7 +13,4 @@ extern DVL_API_FOR_TEST bool gbVanilla; /** Whether the Hellfire mode is required (forced). */ extern bool forceHellfire; -/** Adds a handler to be called then `gbIsHellfire` changes after the initial startup. */ -void AddIsHellfireChangeHandler(tl::function_ref callback); - } // namespace devilution diff --git a/Source/lua/lua.cpp b/Source/lua/lua.cpp index abfa386df..f4d6958ac 100644 --- a/Source/lua/lua.cpp +++ b/Source/lua/lua.cpp @@ -11,6 +11,7 @@ #include "appfat.h" #include "engine/assets.hpp" #include "lua/modules/audio.hpp" +#include "lua/modules/hellfire.hpp" #include "lua/modules/i18n.hpp" #include "lua/modules/items.hpp" #include "lua/modules/log.hpp" @@ -42,6 +43,8 @@ struct LuaState { std::optional CurrentLuaState; +std::vector> IsModChangeHandlers; + // A Lua function that we use to generate a `require` implementation. constexpr std::string_view RequireGenSrc = R"lua( function requireGen(env, loaded, loadFn) @@ -198,17 +201,32 @@ sol::environment CreateLuaSandbox() return sandbox; } +void AddModsChangedHandler(tl::function_ref callback) +{ + IsModChangeHandlers.push_back(callback); +} + void LuaReloadActiveMods() { // Loaded without a sandbox. CurrentLuaState->events = RunScript(/*env=*/std::nullopt, "devilutionx.events", /*optional=*/false); CurrentLuaState->commonPackages["devilutionx.events"] = CurrentLuaState->events; - for (std::string_view modname : GetOptions().Mods.GetActiveModList()) { + gbIsHellfire = false; + UnloadModArchives(); + + std::vector modnames = GetOptions().Mods.GetActiveModList(); + LoadModArchives(modnames); + + for (std::string_view modname : modnames) { std::string packageName = StrCat("mods.", modname, ".init"); RunScript(CreateLuaSandbox(), packageName, /*optional=*/true); } + for (tl::function_ref handler : IsModChangeHandlers) { + handler(); + } + LuaEvent("LoadModsComplete"); } @@ -243,6 +261,7 @@ void LuaInitialize() "devilutionx.player", LuaPlayerModule(lua), "devilutionx.render", LuaRenderModule(lua), "devilutionx.towners", LuaTownersModule(lua), + "devilutionx.hellfire", LuaHellfireModule(lua), "devilutionx.message", [](std::string_view text) { EventPlrMsg(text, UiFlags::ColorRed); }, // This package is loaded without a sandbox: "inspect", RunScript(/*env=*/std::nullopt, "inspect", /*optional=*/false)); diff --git a/Source/lua/lua.hpp b/Source/lua/lua.hpp index afe360f31..80fbb1211 100644 --- a/Source/lua/lua.hpp +++ b/Source/lua/lua.hpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace devilution { @@ -15,4 +16,7 @@ sol::state &GetLuaState(); sol::environment CreateLuaSandbox(); sol::object SafeCallResult(sol::protected_function_result result, bool optional); +/** Adds a handler to be called when mods status changes after the initial startup. */ +void AddModsChangedHandler(tl::function_ref callback); + } // namespace devilution diff --git a/Source/lua/modules/audio.cpp b/Source/lua/modules/audio.cpp index e090ded05..8e486fcd5 100644 --- a/Source/lua/modules/audio.cpp +++ b/Source/lua/modules/audio.cpp @@ -1,4 +1,4 @@ -#include "lua/modules/render.hpp" +#include "lua/modules/audio.hpp" #include diff --git a/Source/lua/modules/hellfire.cpp b/Source/lua/modules/hellfire.cpp new file mode 100644 index 000000000..20c64f5db --- /dev/null +++ b/Source/lua/modules/hellfire.cpp @@ -0,0 +1,18 @@ +#include "lua/modules/hellfire.hpp" + +#include + +#include "engine/assets.hpp" +#include "lua/metadoc.hpp" + +namespace devilution { + +sol::table LuaHellfireModule(sol::state_view &lua) +{ + sol::table table = lua.create_table(); + SetWithSignature(table, "loadData", "()", []() { LoadHellfireArchives(); }); + SetWithSignature(table, "enable", "()", []() { gbIsHellfire = true; }); + return table; +} + +} // namespace devilution diff --git a/Source/lua/modules/hellfire.hpp b/Source/lua/modules/hellfire.hpp new file mode 100644 index 000000000..b2a62a580 --- /dev/null +++ b/Source/lua/modules/hellfire.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace devilution { + +sol::table LuaHellfireModule(sol::state_view &lua); + +} // namespace devilution diff --git a/Source/options.cpp b/Source/options.cpp index 8c6f44198..edbd11d69 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -65,6 +65,10 @@ void DiscoverMods() // Add mods available by default: std::unordered_set modNames = { "clock" }; + if (HaveHellfire()) { + modNames.insert("Hellfire"); + } + // Check if the mods directory exists. const std::string modsPath = StrCat(paths::PrefPath(), "mods"); if (DirectoryExists(modsPath.c_str())) { @@ -414,7 +418,7 @@ std::string_view OptionCategoryBase::GetDescription() const GameModeOptions::GameModeOptions() : OptionCategoryBase("GameMode", N_("Game Mode"), N_("Game Mode Settings")) - , gameMode("Game", OptionEntryFlags::NeedHellfireMpq | OptionEntryFlags::RecreateUI, N_("Game Mode"), N_("Play Diablo or Hellfire."), StartUpGameMode::Ask, + , gameMode("Game", OptionEntryFlags::Invisible, N_("Game Mode"), N_("Play Diablo or Hellfire."), StartUpGameMode::Ask, { { StartUpGameMode::Diablo, N_("Diablo") }, // Ask is missing, because we want to hide it from UI-Settings. @@ -1542,6 +1546,16 @@ void ModOptions::RemoveModEntry(const std::string &modName) }); } +void ModOptions::SetHellfireEnabled(bool enableHellfire) +{ + for (auto &modEntry : GetModEntries()) { + if (modEntry.name == "Hellfire") { + modEntry.enabled.SetValue(enableHellfire); + break; + } + } +} + std::forward_list &ModOptions::GetModEntries() { if (modEntries) @@ -1559,7 +1573,7 @@ std::forward_list &ModOptions::GetModEntries() ModOptions::ModEntry::ModEntry(std::string_view name) : name(name) - , enabled(this->name, OptionEntryFlags::None, this->name.c_str(), "", false) + , enabled(this->name, OptionEntryFlags::RecreateUI, this->name.c_str(), "", false) { } diff --git a/Source/options.h b/Source/options.h index e022d97e4..62c969809 100644 --- a/Source/options.h +++ b/Source/options.h @@ -116,8 +116,6 @@ enum class OptionEntryFlags : uint8_t { RecreateUI = 1 << 5, /** @brief diablo.mpq must be present. */ NeedDiabloMpq = 1 << 6, - /** @brief hellfire.mpq must be present. */ - NeedHellfireMpq = 1 << 7, }; use_enum_as_flags(OptionEntryFlags); @@ -832,6 +830,7 @@ struct ModOptions : OptionCategoryBase { std::vector GetEntries() override; void AddModEntry(const std::string &modName); void RemoveModEntry(const std::string &modName); + void SetHellfireEnabled(bool enableHellfire); private: struct ModEntry { @@ -868,6 +867,7 @@ struct Options { { return { &Language, + &Mods, &GameMode, &StartUp, &Graphics, @@ -880,7 +880,6 @@ struct Options { &Chat, &Keymapper, &Padmapper, - &Mods, }; } }; diff --git a/assets/lua/mods/Hellfire/init.lua b/assets/lua/mods/Hellfire/init.lua new file mode 100644 index 000000000..0b2250b30 --- /dev/null +++ b/assets/lua/mods/Hellfire/init.lua @@ -0,0 +1,4 @@ +local hellfire = require("devilutionx.hellfire") + +hellfire.loadData() +hellfire.enable()