From f4ac7853282b4e82082a2ae3640a09367ad7762f Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 01/13] Add town registry data and wire CreateTown to TownConfig --- Source/CMakeLists.txt | 2 + Source/levels/gendung.cpp | 2 + Source/levels/gendung_defs.hpp | 1 + Source/levels/town.cpp | 105 ++++++++++++++------------------- Source/levels/town_data.cpp | 105 +++++++++++++++++++++++++++++++++ Source/levels/town_data.h | 82 +++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 61 deletions(-) create mode 100644 Source/levels/town_data.cpp create mode 100644 Source/levels/town_data.h diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index b1770d39c78..f1779f92716 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -97,6 +97,7 @@ set(libdevilutionx_SRCS levels/themes.cpp levels/tile_properties.cpp levels/town.cpp + levels/town_data.cpp levels/trigs.cpp lua/autocomplete.cpp @@ -127,6 +128,7 @@ set(libdevilutionx_SRCS lua/modules/render.cpp lua/modules/system.cpp lua/modules/towners.cpp + lua/modules/towns.cpp lua/repl.cpp monsters/validation.cpp diff --git a/Source/levels/gendung.cpp b/Source/levels/gendung.cpp index 8499bcceabc..39ebc3e2410 100644 --- a/Source/levels/gendung.cpp +++ b/Source/levels/gendung.cpp @@ -24,6 +24,7 @@ #include "levels/drlg_l4.h" #include "levels/reencode_dun_cels.hpp" #include "levels/town.h" +#include "levels/town_data.h" #include "lighting.h" #include "monster.h" #include "objects.h" @@ -812,6 +813,7 @@ bool IsNearThemeRoom(WorldTilePosition testPosition) void InitLevels() { + InitializeTristram(); currlevel = 0; leveltype = DTYPE_TOWN; setlevel = false; diff --git a/Source/levels/gendung_defs.hpp b/Source/levels/gendung_defs.hpp index 2b24c4c797e..1821b03e422 100644 --- a/Source/levels/gendung_defs.hpp +++ b/Source/levels/gendung_defs.hpp @@ -32,6 +32,7 @@ enum lvl_entry : uint8_t { ENTRY_WARPLVL, ENTRY_TWARPDN, ENTRY_TWARPUP, + ENTRY_TOWNSWITCH, }; } // namespace devilution diff --git a/Source/levels/town.cpp b/Source/levels/town.cpp index ad2090cfee7..e602e77842c 100644 --- a/Source/levels/town.cpp +++ b/Source/levels/town.cpp @@ -7,6 +7,7 @@ #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "levels/drlg_l1.h" +#include "levels/town_data.h" #include "levels/trigs.h" #include "multi.h" #include "player.h" @@ -199,48 +200,50 @@ void DrlgTPass3() } } - FillSector("levels\\towndata\\sector1s.dun", 46, 46); - FillSector("levels\\towndata\\sector2s.dun", 46, 0); - FillSector("levels\\towndata\\sector3s.dun", 0, 46); - FillSector("levels\\towndata\\sector4s.dun", 0, 0); + const TownConfig &config = GetTownRegistry().GetTown(GetTownRegistry().GetCurrentTown()); + for (const auto §or : config.sectors) { + FillSector(sector.filePath.c_str(), sector.x, sector.y); + } - auto dunData = LoadFileInMem("levels\\towndata\\automap.dun"); - PlaceDunTiles(dunData.get(), { 0, 0 }); + if (GetTownRegistry().GetCurrentTown() == "tristram") { + auto dunData = LoadFileInMem("levels\\towndata\\automap.dun"); + PlaceDunTiles(dunData.get(), { 0, 0 }); - if (!IsWarpOpen(DTYPE_CATACOMBS)) { - dungeon[20][7] = 10; - dungeon[20][6] = 8; - FillTile(48, 20, 320); - } - if (!IsWarpOpen(DTYPE_CAVES)) { - dungeon[4][30] = 8; - FillTile(16, 68, 332); - FillTile(16, 70, 331); - } - if (!IsWarpOpen(DTYPE_HELL)) { - dungeon[15][35] = 7; - dungeon[16][35] = 7; - dungeon[17][35] = 7; - for (int x = 36; x < 46; x++) { - FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); + if (!IsWarpOpen(DTYPE_CATACOMBS)) { + dungeon[20][7] = 10; + dungeon[20][6] = 8; + FillTile(48, 20, 320); } - } - if (gbIsHellfire) { - if (IsWarpOpen(DTYPE_NEST)) { - TownOpenHive(); - } else { - TownCloseHive(); + if (!IsWarpOpen(DTYPE_CAVES)) { + dungeon[4][30] = 8; + FillTile(16, 68, 332); + FillTile(16, 70, 331); + } + if (!IsWarpOpen(DTYPE_HELL)) { + dungeon[15][35] = 7; + dungeon[16][35] = 7; + dungeon[17][35] = 7; + for (int x = 36; x < 46; x++) { + FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); + } + } + if (gbIsHellfire) { + if (IsWarpOpen(DTYPE_NEST)) { + TownOpenHive(); + } else { + TownCloseHive(); + } + if (IsWarpOpen(DTYPE_CRYPT)) + TownOpenGrave(); + else + TownCloseGrave(); } - if (IsWarpOpen(DTYPE_CRYPT)) - TownOpenGrave(); - else - TownCloseGrave(); - } - if (Quests[Q_PWATER]._qactive != QUEST_DONE && Quests[Q_PWATER]._qactive != QUEST_NOTAVAIL) { - FillTile(60, 70, 342); - } else { - FillTile(60, 70, 71); + if (Quests[Q_PWATER]._qactive != QUEST_DONE && Quests[Q_PWATER]._qactive != QUEST_NOTAVAIL) { + FillTile(60, 70, 342); + } else { + FillTile(60, 70, 71); + } } InitTownPieces(); @@ -358,30 +361,10 @@ void CleanTownFountain() void CreateTown(lvl_entry entry) { - dminPosition = { 10, 10 }; - dmaxPosition = { 84, 84 }; - - if (entry == ENTRY_MAIN) { // New game - ViewPosition = { 75, 68 }; - } else if (entry == ENTRY_PREV) { // Cathedral - ViewPosition = { 25, 31 }; - } else if (entry == ENTRY_TWARPUP) { - if (TWarpFrom == 5) { - ViewPosition = { 49, 22 }; - } - if (TWarpFrom == 9) { - ViewPosition = { 18, 69 }; - } - if (TWarpFrom == 13) { - ViewPosition = { 41, 81 }; - } - if (TWarpFrom == 21) { - ViewPosition = { 36, 25 }; - } - if (TWarpFrom == 17) { - ViewPosition = { 79, 62 }; - } - } + const TownConfig &config = GetTownRegistry().GetTown(GetTownRegistry().GetCurrentTown()); + dminPosition = config.dminPosition; + dmaxPosition = config.dmaxPosition; + ViewPosition = config.GetEntryPoint(entry, TWarpFrom); DrlgTPass3(); } diff --git a/Source/levels/town_data.cpp b/Source/levels/town_data.cpp new file mode 100644 index 00000000000..d4950dc58e2 --- /dev/null +++ b/Source/levels/town_data.cpp @@ -0,0 +1,105 @@ +#include "levels/town_data.h" + +#include "utils/log.hpp" + +namespace devilution { + +namespace { + +TownRegistry g_townRegistry; + +} // namespace + +std::string DestinationTownID; + +TownRegistry &GetTownRegistry() +{ + return g_townRegistry; +} + +void TownRegistry::RegisterTown(const std::string &id, const TownConfig &config) +{ + towns[id] = config; + LogInfo("Registered town: {}", id); +} + +TownConfig &TownRegistry::GetTown(const std::string &id) +{ + return towns.at(id); +} + +bool TownRegistry::HasTown(const std::string &id) const +{ + return towns.count(id) > 0; +} + +void TownRegistry::SetCurrentTown(const std::string &id) +{ + currentTownID = id; +} + +std::string TownRegistry::GetCurrentTown() const +{ + return currentTownID; +} + +std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const +{ + for (const auto &[id, config] : towns) { + if (config.saveId == saveId) { + return id; + } + } + return "tristram"; +} + +Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const +{ + // For ENTRY_TWARPUP, match both entry type and warpFromLevel + if (entry == ENTRY_TWARPUP) { + for (const auto &ep : entries) { + if (ep.entryType == entry && ep.warpFromLevel == warpFrom) { + return ep.viewPosition; + } + } + } + + // For other entry types, just match the type + for (const auto &ep : entries) { + if (ep.entryType == entry) { + return ep.viewPosition; + } + } + + // Default fallback + return { 75, 68 }; +} + +void InitializeTristram() +{ + TownConfig tristram; + tristram.name = "Tristram"; + tristram.saveId = 0; + tristram.dminPosition = { 10, 10 }; + tristram.dmaxPosition = { 84, 84 }; + tristram.sectors = { + { "levels\\towndata\\sector1s.dun", 46, 46 }, + { "levels\\towndata\\sector2s.dun", 46, 0 }, + { "levels\\towndata\\sector3s.dun", 0, 46 }, + { "levels\\towndata\\sector4s.dun", 0, 0 }, + }; + tristram.entries = { + { ENTRY_MAIN, { 75, 68 }, -1 }, + { ENTRY_PREV, { 25, 31 }, -1 }, + { ENTRY_TWARPUP, { 49, 22 }, 5 }, + { ENTRY_TWARPUP, { 18, 69 }, 9 }, + { ENTRY_TWARPUP, { 41, 81 }, 13 }, + { ENTRY_TWARPUP, { 36, 25 }, 21 }, + { ENTRY_TWARPUP, { 79, 62 }, 17 }, + { ENTRY_TOWNSWITCH, { 75, 68 }, -1 }, + }; + GetTownRegistry().RegisterTown("tristram", tristram); + GetTownRegistry().SetCurrentTown("tristram"); +} + +} // namespace devilution diff --git a/Source/levels/town_data.h b/Source/levels/town_data.h new file mode 100644 index 00000000000..47ca05eb231 --- /dev/null +++ b/Source/levels/town_data.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include "engine/point.hpp" +#include "levels/gendung_defs.hpp" + +namespace devilution { + +/** + * @brief Represents a town sector (map piece) + */ +struct TownSector { + std::string filePath; + int x, y; +}; + +/** + * @brief Represents a town entry/spawn point + */ +struct TownEntryPoint { + lvl_entry entryType; + Point viewPosition; + int warpFromLevel; // Source level for ENTRY_TWARPUP (-1 for any) +}; + +/** + * @brief Position override for a towner NPC + */ +struct TownerPositionOverride { + std::string shortName; // e.g. "griswold", "farnham" + Point position; +}; + +/** + * @brief Complete configuration for a town + */ +struct TownConfig { + std::string name; + uint8_t saveId = 0; + Point dminPosition = { 10, 10 }; + Point dmaxPosition = { 84, 84 }; + std::vector sectors; + std::string solFile; + std::vector entries; + std::vector townerOverrides; + + /** + * @brief Gets the spawn point for a given entry type and warp source + */ + Point GetEntryPoint(lvl_entry entry, int warpFrom = -1) const; +}; + +/** + * @brief Registry for managing multiple town configurations + */ +class TownRegistry { +private: + std::unordered_map towns; + std::string currentTownID; + +public: + void RegisterTown(const std::string &id, const TownConfig &config); + TownConfig &GetTown(const std::string &id); + bool HasTown(const std::string &id) const; + void SetCurrentTown(const std::string &id); + std::string GetCurrentTown() const; + const std::unordered_map &GetTowns() const { return towns; } + + /** @brief Finds town string ID by its saveId. Returns "tristram" if not found. */ + std::string GetTownBySaveId(uint8_t saveId) const; +}; + +TownRegistry &GetTownRegistry(); + +extern std::string DestinationTownID; + +void InitializeTristram(); + +} // namespace devilution From d112b051eda24e64ea8f9c4ea4356862cd790829 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 02/13] Add Lua towns module for registration and travel --- Source/diablo.cpp | 7026 ++++++++++++++++---------------- Source/diablo.h | 254 +- Source/gamemenu.cpp | 782 ++-- Source/inv.cpp | 4598 ++++++++++----------- Source/levels/trigs.cpp | 1894 ++++----- Source/lua/lua_global.cpp | 3 + Source/lua/lua_global.hpp | 46 +- Source/lua/modules/towns.cpp | 151 + Source/lua/modules/towns.hpp | 17 + Source/utils/screen_reader.cpp | 110 +- Source/utils/screen_reader.hpp | 50 +- 11 files changed, 7551 insertions(+), 7380 deletions(-) create mode 100644 Source/lua/modules/towns.cpp create mode 100644 Source/lua/modules/towns.hpp diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 36a679cce3e..ac8a1ff178c 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1,3513 +1,3513 @@ -/** - * @file diablo.cpp - * - * Implementation of the main game initialization functions. - */ -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include -#include -#else -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif -#endif - -#include - -#include - -#include "DiabloUI/selstart.h" -#include "appfat.h" -#include "automap.h" -#include "capture.h" -#include "control/control.hpp" -#include "cursor.h" -#include "dead.h" -#ifdef _DEBUG -#include "debug.h" -#endif -#include "DiabloUI/diabloui.h" -#include "controls/control_mode.hpp" -#include "controls/keymapper.hpp" -#include "controls/plrctrls.h" -#include "controls/remap_keyboard.h" -#include "diablo.h" -#include "diablo_msg.hpp" -#include "discord/discord.h" -#include "doom.h" -#include "encrypt.h" -#include "engine/backbuffer_state.hpp" -#include "engine/clx_sprite.hpp" -#include "engine/demomode.h" -#include "engine/dx.h" -#include "engine/events.hpp" -#include "engine/load_cel.hpp" -#include "engine/load_file.hpp" -#include "engine/random.hpp" -#include "engine/render/clx_render.hpp" -#include "engine/sound.h" -#include "game_mode.hpp" -#include "gamemenu.h" -#include "gmenu.h" -#include "headless_mode.hpp" -#include "help.h" -#include "hwcursor.hpp" -#include "init.hpp" -#include "inv.h" -#include "levels/drlg_l1.h" -#include "levels/drlg_l2.h" -#include "levels/drlg_l3.h" -#include "levels/drlg_l4.h" -#include "levels/gendung.h" -#include "levels/setmaps.h" -#include "levels/themes.h" -#include "levels/town.h" -#include "levels/trigs.h" -#include "lighting.h" -#include "loadsave.h" -#include "lua/lua_event.hpp" -#include "lua/lua_global.hpp" -#include "menu.h" -#include "minitext.h" -#include "missiles.h" -#include "movie.h" -#include "multi.h" -#include "nthread.h" -#include "objects.h" -#include "options.h" -#include "panels/console.hpp" -#include "panels/info_box.hpp" -#include "panels/partypanel.hpp" -#include "panels/spell_book.hpp" -#include "panels/spell_list.hpp" -#include "pfile.h" -#include "plrmsg.h" -#include "qol/chatlog.h" -#include "qol/floatingnumbers.h" -#include "qol/itemlabels.h" -#include "qol/monhealthbar.h" -#include "qol/stash.h" -#include "qol/visual_store.h" -#include "qol/xpbar.h" -#include "quick_messages.hpp" -#include "restrict.h" -#include "stores.h" -#include "storm/storm_net.hpp" -#include "storm/storm_svid.h" -#include "tables/monstdat.h" -#include "tables/playerdat.hpp" -#include "towners.h" -#include "track.h" -#include "utils/console.h" -#include "utils/display.h" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/parse_int.hpp" -#include "utils/paths.h" -#include "utils/screen_reader.hpp" -#include "utils/sdl_compat.h" -#include "utils/sdl_thread.h" -#include "utils/status_macros.hpp" -#include "utils/str_cat.hpp" -#include "utils/utf8.hpp" - -#ifndef USE_SDL1 -#include "controls/touch/gamepad.h" -#include "controls/touch/renderers.h" -#endif - -#ifdef __vita__ -#include "platform/vita/touch.h" -#endif - -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION -#include -#endif - -namespace devilution { - -uint32_t DungeonSeeds[NUMLEVELS]; -std::optional LevelSeeds[NUMLEVELS]; -Point MousePosition; -bool gbRunGameResult; -bool ReturnToMainMenu; -/** Enable updating of player character, set to false once Diablo dies */ -bool gbProcessPlayers; -bool gbLoadGame; -bool cineflag; -int PauseMode; -clicktype sgbMouseDown; -uint16_t gnTickDelay = 50; -char gszProductName[64] = "DevilutionX vUnknown"; - -#ifdef _DEBUG -bool DebugDisableNetworkTimeout = false; -std::vector DebugCmdsFromCommandLine; -#endif -GameLogicStep gGameLogicStep = GameLogicStep::None; - -/** This and the following mouse variables are for handling in-game click-and-hold actions */ -PlayerActionType LastPlayerAction = PlayerActionType::None; - -// Controller support: Actions to run after updating the cursor state. -// Defined in SourceX/controls/plctrls.cpp. -extern void plrctrls_after_check_curs_move(); -extern void plrctrls_every_frame(); -extern void plrctrls_after_game_logic(); - -namespace { - -char gszVersionNumber[64] = "internal version unknown"; - -bool gbGameLoopStartup; -bool forceSpawn; -bool forceDiablo; -int sgnTimeoutCurs; -bool gbShowIntro = true; -/** To know if these things have been done when we get to the diablo_deinit() function */ -bool was_archives_init = false; -/** To know if surfaces have been initialized or not */ -bool was_window_init = false; -bool was_ui_init = false; - -void StartGame(interface_mode uMsg) -{ - CalcViewportGeometry(); - cineflag = false; - InitCursor(); -#ifdef _DEBUG - LoadDebugGFX(); -#endif - assert(HeadlessMode || ghMainWnd); - music_stop(); - InitMonsterHealthBar(); - InitXPBar(); - ShowProgress(uMsg); - gmenu_init_menu(); - InitLevelCursor(); - sgnTimeoutCurs = CURSOR_NONE; - sgbMouseDown = CLICK_NONE; - LastPlayerAction = PlayerActionType::None; -} - -void FreeGame() -{ - FreeMonsterHealthBar(); - FreeXPBar(); - FreeControlPan(); - FreeInvGFX(); - FreeGMenu(); - FreeQuestText(); - FreeInfoBoxGfx(); - FreeStoreMem(); - - for (Player &player : Players) - ResetPlayerGFX(player); - - FreeCursor(); -#ifdef _DEBUG - FreeDebugGFX(); -#endif - FreeGameMem(); - stream_stop(); - music_stop(); -} - -bool ProcessInput() -{ - if (PauseMode == 2) { - return false; - } - - plrctrls_every_frame(); - - if (!gbIsMultiplayer && gmenu_is_active()) { - RedrawViewport(); - return false; - } - - if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { -#ifdef __vita__ - FinishSimulatedMouseClicks(MousePosition); -#endif - CheckCursMove(); - plrctrls_after_check_curs_move(); - RepeatPlayerAction(); - } - - return true; -} - -void LeftMouseCmd(bool bShift) -{ - bool bNear; - - assert(!GetMainPanel().contains(MousePosition)); - - if (leveltype == DTYPE_TOWN) { - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); - if (pcursitem != -1 && pcurs == CURSOR_HAND) - NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); - if (pcursmonst != -1) - NetSendCmdLocParam1(true, CMD_TALKXY, cursPosition, pcursmonst); - if (pcursitem == -1 && pcursmonst == -1 && PlayerUnderCursor == nullptr) { - LastPlayerAction = PlayerActionType::Walk; - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); - } - return; - } - - const Player &myPlayer = *MyPlayer; - bNear = myPlayer.position.tile.WalkingDistance(cursPosition) < 2; - if (pcursitem != -1 && pcurs == CURSOR_HAND && !bShift) { - NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); - } else if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled() && (!bShift || (bNear && ObjectUnderCursor->_oBreak == 1))) { - LastPlayerAction = PlayerActionType::OperateObject; - NetSendCmdLoc(MyPlayerId, true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, cursPosition); - } else if (myPlayer.UsesRangedWeapon()) { - if (bShift) { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_RATTACKXY, cursPosition); - } else if (pcursmonst != -1) { - if (CanTalkToMonst(Monsters[pcursmonst])) { - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else { - LastPlayerAction = PlayerActionType::AttackMonsterTarget; - NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); - } - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - LastPlayerAction = PlayerActionType::AttackPlayerTarget; - NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId()); - } - } else { - if (bShift) { - if (pcursmonst != -1) { - if (CanTalkToMonst(Monsters[pcursmonst])) { - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); - } - } else { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); - } - } else if (pcursmonst != -1) { - LastPlayerAction = PlayerActionType::AttackMonsterTarget; - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - LastPlayerAction = PlayerActionType::AttackPlayerTarget; - NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId()); - } - } - if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { - LastPlayerAction = PlayerActionType::Walk; - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); - } -} - -bool TryOpenDungeonWithMouse() -{ - if (leveltype != DTYPE_TOWN) - return false; - - const Item &holdItem = MyPlayer->HoldItem; - if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) - OpenHive(); - else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) - OpenGrave(); - else - return false; - - NewCursor(CURSOR_HAND); - return true; -} - -void LeftMouseDown(uint16_t modState) -{ - LastPlayerAction = PlayerActionType::None; - - if (gmenu_left_mouse(true)) - return; - - if (CheckMuteButton()) - return; - - if (sgnTimeoutCurs != CURSOR_NONE) - return; - - if (MyPlayerIsDead) { - CheckMainPanelButtonDead(); - return; - } - - if (PauseMode == 2) { - return; - } - if (DoomFlag) { - doom_close(); - return; - } - - if (SpellSelectFlag) { - SetSpell(); - return; - } - - if (IsPlayerInStore()) { - CheckStoreBtn(); - return; - } - - const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; - const bool isCtrlHeld = (modState & SDL_KMOD_CTRL) != 0; - - if (!GetMainPanel().contains(MousePosition)) { - if (!gmenu_is_active() && !TryIconCurs()) { - if (QuestLogIsOpen && GetLeftPanel().contains(MousePosition)) { - QuestlogESC(); - } else if (qtextflag) { - qtextflag = false; - stream_stop(); - } else if (CharFlag && GetLeftPanel().contains(MousePosition)) { - CheckChrBtns(); - } else if (invflag && GetRightPanel().contains(MousePosition)) { - if (!DropGoldFlag) - CheckInvItem(isShiftHeld, isCtrlHeld); - } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { - if (!IsWithdrawGoldOpen) - CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); - CheckStashButtonPress(MousePosition); - } else if (IsVisualStoreOpen && GetLeftPanel().contains(MousePosition)) { - if (!MyPlayer->HoldItem.isEmpty()) { - CheckVisualStorePaste(MousePosition); - } else { - CheckVisualStoreItem(MousePosition, isCtrlHeld, isShiftHeld); - } - CheckVisualStoreButtonPress(MousePosition); - } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { - CheckSBook(); - } else if (!MyPlayer->HoldItem.isEmpty()) { - if (!TryOpenDungeonWithMouse()) { - const Point currentPosition = MyPlayer->position.tile; - std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, cursPosition)); - if (itemTile) { - NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, MyPlayer->HoldItem); - NewCursor(CURSOR_HAND); - } - } - } else { - CheckLevelButton(); - if (!LevelButtonDown) - LeftMouseCmd(isShiftHeld); - } - } - } else { - if (!ChatFlag && !DropGoldFlag && !IsWithdrawGoldOpen && !gmenu_is_active()) - CheckInvScrn(isShiftHeld, isCtrlHeld); - CheckMainPanelButton(); - CheckStashButtonPress(MousePosition); - if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) - NewCursor(CURSOR_HAND); - } -} - -void LeftMouseUp(uint16_t modState) -{ - gmenu_left_mouse(false); - CheckMuteButtonUp(); - if (MainPanelButtonDown) - CheckMainPanelButtonUp(); - CheckStashButtonRelease(MousePosition); - CheckVisualStoreButtonRelease(MousePosition); - if (CharPanelButtonActive) { - const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; - ReleaseChrBtns(isShiftHeld); - } - if (LevelButtonDown) - CheckLevelButtonUp(); - if (IsPlayerInStore()) - ReleaseStoreBtn(); -} - -void RightMouseDown(bool isShiftHeld) -{ - LastPlayerAction = PlayerActionType::None; - - if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { - return; - } - - if (qtextflag) { - qtextflag = false; - stream_stop(); - return; - } - - if (DoomFlag) { - doom_close(); - return; - } - if (IsPlayerInStore()) - return; - if (SpellSelectFlag) { - SetSpell(); - return; - } - if (SpellbookFlag && GetRightPanel().contains(MousePosition)) - return; - if (TryIconCurs()) - return; - if (pcursinvitem != -1 && UseInvItem(pcursinvitem)) - return; - if (pcursstashitem != StashStruct::EmptyCell && UseStashItem(pcursstashitem)) - return; - if (DidRightClickPartyPortrait()) - return; - if (pcurs == CURSOR_HAND) { - CheckPlrSpell(isShiftHeld); - } else if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { - NewCursor(CURSOR_HAND); - } -} - -void ReleaseKey(SDL_Keycode vkey) -{ - remap_keyboard_key(&vkey); - if (sgnTimeoutCurs != CURSOR_NONE) - return; - KeymapperRelease(vkey); -} - -void ClosePanels() -{ - if (CanPanelsCoverView()) { - if (!IsLeftPanelOpen() && IsRightPanelOpen() && MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } else if (!IsRightPanelOpen() && IsLeftPanelOpen() && MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - CloseInventory(); - CloseCharPanel(); - SpellbookFlag = false; - QuestLogIsOpen = false; -} - -void PressKey(SDL_Keycode vkey, uint16_t modState) -{ - Options &options = GetOptions(); - remap_keyboard_key(&vkey); - - if (vkey == SDLK_UNKNOWN) - return; - - if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { - return; - } - - if (MyPlayerIsDead) { - if (vkey == SDLK_ESCAPE) { - if (!gbIsMultiplayer) { - if (gbValidSaveFile) - gamemenu_load_game(false); - else - gamemenu_exit_game(false); - } else { - NetSendCmd(true, CMD_RETOWN); - } - return; - } - if (sgnTimeoutCurs != CURSOR_NONE) { - return; - } - KeymapperPress(vkey); - if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { - if ((modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } else { - TypeChatMessage(); - } - } - if (vkey != SDLK_ESCAPE) { - return; - } - } - // Disallow player from accessing escape menu during the frames before the death message appears - if (vkey == SDLK_ESCAPE && MyPlayer->_pHitPoints > 0) { - if (!PressEscKey()) { - LastPlayerAction = PlayerActionType::None; - gamemenu_on(); - } - return; - } - - if (DropGoldFlag) { - control_drop_gold(vkey); - return; - } - if (IsWithdrawGoldOpen) { - WithdrawGoldKeyPress(vkey); - return; - } - - if (sgnTimeoutCurs != CURSOR_NONE) { - return; - } - - KeymapperPress(vkey); - - if (PauseMode == 2) { - if ((vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) && (modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } - return; - } - - if (DoomFlag) { - doom_close(); - return; - } - - switch (vkey) { - case SDLK_PLUS: - case SDLK_KP_PLUS: - case SDLK_EQUALS: - case SDLK_KP_EQUALS: - if (AutomapActive) { - AutomapZoomIn(); - } - return; - case SDLK_MINUS: - case SDLK_KP_MINUS: - case SDLK_UNDERSCORE: - if (AutomapActive) { - AutomapZoomOut(); - } - return; -#ifdef _DEBUG - case SDLK_V: - if ((modState & SDL_KMOD_SHIFT) != 0) - NextDebugMonster(); - else - GetDebugMonster(); - return; -#endif - case SDLK_RETURN: - case SDLK_KP_ENTER: - if ((modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } else if (IsPlayerInStore()) { - StoreEnter(); - } else if (QuestLogIsOpen) { - QuestlogEnter(); - } else { - TypeChatMessage(); - } - return; - case SDLK_UP: - if (IsPlayerInStore()) { - StoreUp(); - } else if (QuestLogIsOpen) { - QuestlogUp(); - } else if (HelpFlag) { - HelpScrollUp(); - } else if (ChatLogFlag) { - ChatLogScrollUp(); - } else if (AutomapActive) { - AutomapUp(); - } else if (IsStashOpen) { - Stash.PreviousPage(); - } - return; - case SDLK_DOWN: - if (IsPlayerInStore()) { - StoreDown(); - } else if (QuestLogIsOpen) { - QuestlogDown(); - } else if (HelpFlag) { - HelpScrollDown(); - } else if (ChatLogFlag) { - ChatLogScrollDown(); - } else if (AutomapActive) { - AutomapDown(); - } else if (IsStashOpen) { - Stash.NextPage(); - } - return; - case SDLK_PAGEUP: - if (IsPlayerInStore()) { - StorePrior(); - } else if (ChatLogFlag) { - ChatLogScrollTop(); - } - return; - case SDLK_PAGEDOWN: - if (IsPlayerInStore()) { - StoreNext(); - } else if (ChatLogFlag) { - ChatLogScrollBottom(); - } - return; - case SDLK_LEFT: - if (AutomapActive && !ChatFlag) - AutomapLeft(); - return; - case SDLK_RIGHT: - if (AutomapActive && !ChatFlag) - AutomapRight(); - return; - default: - break; - } -} - -void HandleMouseButtonDown(Uint8 button, uint16_t modState) -{ - if (IsPlayerInStore() && (button == SDL_BUTTON_X1 -#if !SDL_VERSION_ATLEAST(2, 0, 0) - || button == 8 -#endif - )) { - StoreESC(); - return; - } - - switch (button) { - case SDL_BUTTON_LEFT: - if (sgbMouseDown == CLICK_NONE) { - sgbMouseDown = CLICK_LEFT; - LeftMouseDown(modState); - } - break; - case SDL_BUTTON_RIGHT: - if (sgbMouseDown == CLICK_NONE) { - sgbMouseDown = CLICK_RIGHT; - RightMouseDown((modState & SDL_KMOD_SHIFT) != 0); - } - break; - default: - KeymapperPress(static_cast(button | KeymapperMouseButtonMask)); - break; - } -} - -void HandleMouseButtonUp(Uint8 button, uint16_t modState) -{ - if (sgbMouseDown == CLICK_LEFT && button == SDL_BUTTON_LEFT) { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - LeftMouseUp(modState); - } else if (sgbMouseDown == CLICK_RIGHT && button == SDL_BUTTON_RIGHT) { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - } else { - KeymapperRelease(static_cast(button | KeymapperMouseButtonMask)); - } -} - -[[maybe_unused]] void LogUnhandledEvent(const char *name, int value) -{ - LogVerbose("Unhandled SDL event: {} {}", name, value); -} - -void PrepareForFadeIn() -{ - if (HeadlessMode) return; - BlackPalette(); - - // Render the game to the buffer(s) with a fully black palette. - // Palette fade-in will gradually make it visible. - RedrawEverything(); - while (IsRedrawEverything()) { - DrawAndBlit(); - } -} - -void GameEventHandler(const SDL_Event &event, uint16_t modState) -{ - [[maybe_unused]] const Options &options = GetOptions(); - StaticVector ctrlEvents = ToControllerButtonEvents(event); - for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { - GameAction action; - if (HandleControllerButtonEvent(event, ctrlEvent, action) && action.type == GameActionType_SEND_KEY) { - if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { - const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; - if (!action.send_key.up) - HandleMouseButtonDown(static_cast(button), modState); - else - HandleMouseButtonUp(static_cast(button), modState); - } else { - if (!action.send_key.up) - PressKey(static_cast(action.send_key.vk_code), modState); - else - ReleaseKey(static_cast(action.send_key.vk_code)); - } - } - } - if (ctrlEvents.size() > 0 && ctrlEvents[0].button != ControllerButton_NONE) { - return; - } - -#ifdef _DEBUG - if (ConsoleHandleEvent(event)) { - return; - } -#endif - - if (IsChatActive() && HandleTalkTextInputEvent(event)) { - return; - } - if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { - return; - } - if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { - return; - } - - switch (event.type) { - case SDL_EVENT_KEY_DOWN: - PressKey(SDLC_EventKey(event), modState); - return; - case SDL_EVENT_KEY_UP: - ReleaseKey(SDLC_EventKey(event)); - return; - case SDL_EVENT_MOUSE_MOTION: - if (ControlMode == ControlTypes::KeyboardAndMouse && invflag) - InvalidateInventorySlot(); - MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; - gmenu_on_mouse_move(); - return; - case SDL_EVENT_MOUSE_BUTTON_DOWN: - MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; - HandleMouseButtonDown(event.button.button, modState); - return; - case SDL_EVENT_MOUSE_BUTTON_UP: - MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; - HandleMouseButtonUp(event.button.button, modState); - return; -#if SDL_VERSION_ATLEAST(2, 0, 0) - case SDL_EVENT_MOUSE_WHEEL: - if (SDLC_EventWheelIntY(event) > 0) { // Up - if (IsPlayerInStore()) { - StoreUp(); - } else if (QuestLogIsOpen) { - QuestlogUp(); - } else if (HelpFlag) { - HelpScrollUp(); - } else if (ChatLogFlag) { - ChatLogScrollUp(); - } else if (IsStashOpen) { - Stash.PreviousPage(); - } else if (SDL_GetModState() & SDL_KMOD_CTRL) { - if (AutomapActive) { - AutomapZoomIn(); - } - } else { - KeymapperPress(MouseScrollUpButton); - } - } else if (SDLC_EventWheelIntY(event) < 0) { // down - if (IsPlayerInStore()) { - StoreDown(); - } else if (QuestLogIsOpen) { - QuestlogDown(); - } else if (HelpFlag) { - HelpScrollDown(); - } else if (ChatLogFlag) { - ChatLogScrollDown(); - } else if (IsStashOpen) { - Stash.NextPage(); - } else if (SDL_GetModState() & SDL_KMOD_CTRL) { - if (AutomapActive) { - AutomapZoomOut(); - } - } else { - KeymapperPress(MouseScrollDownButton); - } - } else if (SDLC_EventWheelIntX(event) > 0) { // left - KeymapperPress(MouseScrollLeftButton); - } else if (SDLC_EventWheelIntX(event) < 0) { // right - KeymapperPress(MouseScrollRightButton); - } - break; -#endif - default: - if (IsCustomEvent(event.type)) { - if (gbIsMultiplayer) - pfile_write_hero(); - nthread_ignore_mutex(true); - PaletteFadeOut(8); - sound_stop(); - ShowProgress(GetCustomEvent(event)); - - PrepareForFadeIn(); - LoadPWaterPalette(); - if (gbRunGame) - PaletteFadeIn(8); - nthread_ignore_mutex(false); - gbGameLoopStartup = true; - return; - } - MainWndProc(event); - break; - } -} - -void RunGameLoop(interface_mode uMsg) -{ - demo::NotifyGameLoopStart(); - - nthread_ignore_mutex(true); - StartGame(uMsg); - assert(HeadlessMode || ghMainWnd); - EventHandler previousHandler = SetEventHandler(GameEventHandler); - run_delta_info(); - gbRunGame = true; - gbProcessPlayers = IsDiabloAlive(true); - gbRunGameResult = true; - - PrepareForFadeIn(); - LoadPWaterPalette(); - PaletteFadeIn(8); - InitBackbufferState(); - RedrawEverything(); - gbGameLoopStartup = true; - nthread_ignore_mutex(false); - - discord_manager::StartGame(); - lua::GameStart(); -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION - unsigned run_game_iteration = 0; -#endif - - while (gbRunGame) { - -#ifdef _DEBUG - if (!gbGameLoopStartup && !DebugCmdsFromCommandLine.empty()) { - InitConsole(); - for (const std::string &cmd : DebugCmdsFromCommandLine) { - RunInConsole(cmd); - } - DebugCmdsFromCommandLine.clear(); - } -#endif - - SDL_Event event; - uint16_t modState; - while (FetchMessage(&event, &modState)) { - if (event.type == SDL_EVENT_QUIT) { - gbRunGameResult = false; - gbRunGame = false; - break; - } - HandleMessage(event, modState); - } - if (!gbRunGame) - break; - - bool drawGame = true; - bool processInput = true; - const bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed(&drawGame); - if (demo::IsRecording()) - demo::RecordGameLoopResult(runGameLoop); - - discord_manager::UpdateGame(); - - if (!runGameLoop) { - if (processInput) - ProcessInput(); - DvlNet_ProcessNetworkPackets(); - if (!drawGame) - continue; - RedrawViewport(); - DrawAndBlit(); - continue; - } - - ProcessGameMessagePackets(); - if (game_loop(gbGameLoopStartup)) - diablo_color_cyc_logic(); - gbGameLoopStartup = false; - if (drawGame) - DrawAndBlit(); -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION - if (run_game_iteration++ == 0) - HeapProfilerDump("first_game_iteration"); -#endif - } - - demo::NotifyGameLoopEnd(); - - if (gbIsMultiplayer) { - pfile_write_hero(/*writeGameData=*/false); - sfile_write_stash(); - } - - PaletteFadeOut(8); - NewCursor(CURSOR_NONE); - ClearScreenBuffer(); - RedrawEverything(); - scrollrt_draw_game_screen(); - previousHandler = SetEventHandler(previousHandler); - assert(HeadlessMode || previousHandler == GameEventHandler); - FreeGame(); - - if (cineflag) { - cineflag = false; - DoEnding(); - } -} - -void PrintWithRightPadding(std::string_view str, size_t width) -{ - printInConsole(str); - if (str.size() >= width) - return; - printInConsole(std::string(width - str.size(), ' ')); -} - -void PrintHelpOption(std::string_view flags, std::string_view description) -{ - printInConsole(" "); - PrintWithRightPadding(flags, 20); - printInConsole(" "); - PrintWithRightPadding(description, 30); - printNewlineInConsole(); -} - -#if SDL_VERSION_ATLEAST(2, 0, 0) -FILE *SdlLogFile = nullptr; - -extern "C" void SdlLogToFile(void *userdata, int /*category*/, SDL_LogPriority priority, const char *message) -{ - FILE *file = reinterpret_cast(userdata); - static const char *const LogPriorityPrefixes[SDL_LOG_PRIORITY_COUNT] = { - "", - "VERBOSE", - "DEBUG", - "INFO", - "WARN", - "ERROR", - "CRITICAL" - }; - std::fprintf(file, "%s: %s\n", LogPriorityPrefixes[priority], message); - std::fflush(file); -} -#endif - -[[noreturn]] void PrintHelpAndExit() -{ - printInConsole((/* TRANSLATORS: Commandline Option */ "Options:")); - printNewlineInConsole(); - PrintHelpOption("-h, --help", _(/* TRANSLATORS: Commandline Option */ "Print this message and exit")); - PrintHelpOption("--version", _(/* TRANSLATORS: Commandline Option */ "Print the version and exit")); - PrintHelpOption("--data-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of diabdat.mpq")); - PrintHelpOption("--save-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of save files")); - PrintHelpOption("--config-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the location of diablo.ini")); - PrintHelpOption("--lang", _(/* TRANSLATORS: Commandline Option */ "Specify the language code (e.g. en or pt_BR)")); - PrintHelpOption("-n", _(/* TRANSLATORS: Commandline Option */ "Skip startup videos")); - PrintHelpOption("-f", _(/* TRANSLATORS: Commandline Option */ "Display frames per second")); - PrintHelpOption("--verbose", _(/* TRANSLATORS: Commandline Option */ "Enable verbose logging")); -#if SDL_VERSION_ATLEAST(2, 0, 0) - PrintHelpOption("--log-to-file ", _(/* TRANSLATORS: Commandline Option */ "Log to a file instead of stderr")); -#endif -#ifndef DISABLE_DEMOMODE - PrintHelpOption("--record <#>", _(/* TRANSLATORS: Commandline Option */ "Record a demo file")); - PrintHelpOption("--demo <#>", _(/* TRANSLATORS: Commandline Option */ "Play a demo file")); - PrintHelpOption("--timedemo", _(/* TRANSLATORS: Commandline Option */ "Disable all frame limiting during demo playback")); -#endif - printNewlineInConsole(); - printInConsole(_(/* TRANSLATORS: Commandline Option */ "Game selection:")); - printNewlineInConsole(); - PrintHelpOption("--spawn", _(/* TRANSLATORS: Commandline Option */ "Force Shareware mode")); - PrintHelpOption("--diablo", _(/* TRANSLATORS: Commandline Option */ "Force Diablo mode")); - PrintHelpOption("--hellfire", _(/* TRANSLATORS: Commandline Option */ "Force Hellfire mode")); - printInConsole(_(/* TRANSLATORS: Commandline Option */ "Hellfire options:")); - printNewlineInConsole(); -#ifdef _DEBUG - printNewlineInConsole(); - printInConsole("Debug options:"); - printNewlineInConsole(); - PrintHelpOption("-i", "Ignore network timeout"); - PrintHelpOption("+", "Pass commands to the engine"); -#endif - printNewlineInConsole(); - printInConsole(_("Report bugs at https://github.com/diasurgical/devilutionX/")); - printNewlineInConsole(); - diablo_quit(0); -} - -void PrintFlagMessage(std::string_view flag, std::string_view message) -{ - printInConsole(flag); - printInConsole(message); - printNewlineInConsole(); -} - -void PrintFlagRequiresArgument(std::string_view flag) -{ - PrintFlagMessage(flag, " requires an argument"); -} - -void DiabloParseFlags(int argc, char **argv) -{ -#ifdef _DEBUG - int argumentIndexOfLastCommandPart = -1; - std::string currentCommand; -#endif -#ifndef DISABLE_DEMOMODE - bool timedemo = false; - int demoNumber = -1; - int recordNumber = -1; - bool createDemoReference = false; -#endif - for (int i = 1; i < argc; i++) { - const std::string_view arg = argv[i]; - if (arg == "-h" || arg == "--help") { - PrintHelpAndExit(); - } else if (arg == "--version") { - printInConsole(PROJECT_NAME); - printInConsole(" v"); - printInConsole(PROJECT_VERSION); - printNewlineInConsole(); - diablo_quit(0); - } else if (arg == "--data-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--data-dir"); - diablo_quit(64); - } - paths::SetBasePath(argv[++i]); - } else if (arg == "--save-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--save-dir"); - diablo_quit(64); - } - paths::SetPrefPath(argv[++i]); - } else if (arg == "--config-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--config-dir"); - diablo_quit(64); - } - paths::SetConfigPath(argv[++i]); - } else if (arg == "--lang") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--lang"); - diablo_quit(64); - } - forceLocale = argv[++i]; -#ifndef DISABLE_DEMOMODE - } else if (arg == "--demo") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--demo"); - diablo_quit(64); - } - ParseIntResult parsedParam = ParseInt(argv[++i]); - if (!parsedParam.has_value()) { - PrintFlagMessage("--demo", " must be a number"); - diablo_quit(64); - } - demoNumber = parsedParam.value(); - gbShowIntro = false; - } else if (arg == "--timedemo") { - timedemo = true; - } else if (arg == "--record") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--record"); - diablo_quit(64); - } - ParseIntResult parsedParam = ParseInt(argv[++i]); - if (!parsedParam.has_value()) { - PrintFlagMessage("--record", " must be a number"); - diablo_quit(64); - } - recordNumber = parsedParam.value(); - } else if (arg == "--create-reference") { - createDemoReference = true; -#else - } else if (arg == "--demo" || arg == "--timedemo" || arg == "--record" || arg == "--create-reference") { - printInConsole("Binary compiled without demo mode support."); - printNewlineInConsole(); - diablo_quit(1); -#endif - } else if (arg == "-n") { - gbShowIntro = false; - } else if (arg == "-f") { - EnableFrameCount(); - } else if (arg == "--spawn") { - forceSpawn = true; - } else if (arg == "--diablo") { - forceDiablo = true; - } else if (arg == "--hellfire") { - forceHellfire = true; - } else if (arg == "--vanilla") { - gbVanilla = true; - } else if (arg == "--verbose") { - SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); -#if SDL_VERSION_ATLEAST(2, 0, 0) - } else if (arg == "--log-to-file") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--log-to-file"); - diablo_quit(64); - } - SdlLogFile = OpenFile(argv[++i], "wb"); - if (SdlLogFile == nullptr) { - printInConsole("Failed to open log file for writing"); - diablo_quit(64); - } - SDL_SetLogOutputFunction(&SdlLogToFile, /*userdata=*/SdlLogFile); -#endif -#ifdef _DEBUG - } else if (arg == "-i") { - DebugDisableNetworkTimeout = true; - } else if (arg[0] == '+') { - if (!currentCommand.empty()) - DebugCmdsFromCommandLine.push_back(currentCommand); - argumentIndexOfLastCommandPart = i; - currentCommand = arg.substr(1); - } else if (arg[0] != '-' && (argumentIndexOfLastCommandPart + 1) == i) { - currentCommand.append(" "); - currentCommand.append(arg); - argumentIndexOfLastCommandPart = i; -#endif - } else { - printInConsole("unrecognized option '"); - printInConsole(argv[i]); - printInConsole("'"); - printNewlineInConsole(); - PrintHelpAndExit(); - } - } - -#ifdef _DEBUG - if (!currentCommand.empty()) - DebugCmdsFromCommandLine.push_back(currentCommand); -#endif - -#ifndef DISABLE_DEMOMODE - if (demoNumber != -1) - demo::InitPlayBack(demoNumber, timedemo); - if (recordNumber != -1) - demo::InitRecording(recordNumber, createDemoReference); -#endif -} - -void DiabloInitScreen() -{ - MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; - if (ControlMode == ControlTypes::KeyboardAndMouse) - SetCursorPos(MousePosition); - - ClrDiabloMsg(); -} - -void SetApplicationVersions() -{ - *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; - *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; -} - -void CheckArchivesUpToDate() -{ - const bool devilutionxMpqOutOfDate = IsDevilutionXMpqOutOfDate(); - const bool fontsMpqOutOfDate = AreExtraFontsOutOfDate(); - - if (devilutionxMpqOutOfDate && fontsMpqOutOfDate) { - app_fatal(_("Please update devilutionx.mpq and fonts.mpq to the latest version")); - } else if (devilutionxMpqOutOfDate) { - app_fatal(_("Failed to load UI resources.\n" - "\n" - "Make sure devilutionx.mpq is in the game folder and that it is up to date.")); - } else if (fontsMpqOutOfDate) { - app_fatal(_("Please update fonts.mpq to the latest version")); - } -} - -void ApplicationInit() -{ - if (*GetOptions().Graphics.showFPS) - EnableFrameCount(); - - init_create_window(); - was_window_init = true; - - InitializeScreenReader(); - LanguageInitialize(); - - SetApplicationVersions(); - - ReadOnlyTest(); -} - -void DiabloInit() -{ - if (forceSpawn || *GetOptions().GameMode.shareware) - gbIsSpawn = 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; - - for (size_t i = 0; i < QuickMessages.size(); i++) { - auto &messages = GetOptions().Chat.szHotKeyMsgs[i]; - if (messages.empty()) { - messages.emplace_back(_(QuickMessages[i].message)); - } - } - -#ifndef USE_SDL1 - InitializeVirtualGamepad(); -#endif - - UiInitialize(); - was_ui_init = true; - - if (wasHellfireDiscovered) { - UiSelStartUpGameOption(); - if (!gbIsHellfire) { - // Reinitialize the UI Elements because we changed the game - UnloadUiGFX(); - UiInitialize(); - if (IsHardwareCursor()) - SetHardwareCursor(CursorInfo::UnknownCursor()); - } - } - - DiabloInitScreen(); - - snd_init(); - - ui_sound_init(); - - // Item graphics are loaded early, they already get touched during hero selection. - InitItemGFX(); - - // Always available. - LoadSmallSelectionSpinner(); - - CheckArchivesUpToDate(); -} - -void DiabloSplash() -{ - if (!gbShowIntro) - return; - - if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) - play_movie("gendata\\logo.smk", true); - - auto &intro = gbIsHellfire ? GetOptions().StartUp.hellfireIntro : GetOptions().StartUp.diabloIntro; - - if (*intro != StartUpIntro::Off) { - if (gbIsHellfire) - play_movie("gendata\\Hellfire.smk", true); - else - play_movie("gendata\\diablo1.smk", true); - if (*intro == StartUpIntro::Once) { - intro.SetValue(StartUpIntro::Off); - if (!demo::IsRunning()) SaveOptions(); - } - } - - if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) - UiTitleDialog(); -} - -void DiabloDeinit() -{ - FreeItemGFX(); - - LuaShutdown(); - ShutDownScreenReader(); - - if (gbSndInited) - effects_cleanup_sfx(); - snd_deinit(); - if (was_ui_init) - UiDestroy(); - if (was_archives_init) - init_cleanup(); - if (was_window_init) - dx_cleanup(); // Cleanup SDL surfaces stuff, so we have to do it before SDL_Quit(). - UnloadFonts(); - if (SDL_WasInit((~0U) & ~SDL_INIT_HAPTIC) != 0) - SDL_Quit(); -} - -tl::expected LoadLvlGFX() -{ - assert(pDungeonCels == nullptr); - constexpr int SpecialCelWidth = 64; - - const auto loadAll = [](const char *cel, const char *til, const char *special) -> tl::expected { - ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus(cel)); - ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus(til)); - ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus(special, SpecialCelWidth)); - return {}; - }; - - switch (leveltype) { - case DTYPE_TOWN: { - auto cel = LoadFileInMemWithStatus("nlevels\\towndata\\town.cel"); - if (!cel.has_value()) { - ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus("levels\\towndata\\town.cel")); - } else { - pDungeonCels = std::move(*cel); - } - auto til = LoadFileInMemWithStatus("nlevels\\towndata\\town.til"); - if (!til.has_value()) { - ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus("levels\\towndata\\town.til")); - } else { - pMegaTiles = std::move(*til); - } - ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus("levels\\towndata\\towns", SpecialCelWidth)); - return {}; - } - case DTYPE_CATHEDRAL: - return loadAll( - "levels\\l1data\\l1.cel", - "levels\\l1data\\l1.til", - "levels\\l1data\\l1s"); - case DTYPE_CATACOMBS: - return loadAll( - "levels\\l2data\\l2.cel", - "levels\\l2data\\l2.til", - "levels\\l2data\\l2s"); - case DTYPE_CAVES: - return loadAll( - "levels\\l3data\\l3.cel", - "levels\\l3data\\l3.til", - "levels\\l1data\\l1s"); - case DTYPE_HELL: - return loadAll( - "levels\\l4data\\l4.cel", - "levels\\l4data\\l4.til", - "levels\\l2data\\l2s"); - case DTYPE_NEST: - return loadAll( - "nlevels\\l6data\\l6.cel", - "nlevels\\l6data\\l6.til", - "levels\\l1data\\l1s"); - case DTYPE_CRYPT: - return loadAll( - "nlevels\\l5data\\l5.cel", - "nlevels\\l5data\\l5.til", - "nlevels\\l5data\\l5s"); - default: - return tl::make_unexpected("LoadLvlGFX"); - } -} - -tl::expected LoadAllGFX() -{ - IncProgress(); -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - IncProgress(); - RETURN_IF_ERROR(InitObjectGFX()); - IncProgress(); - RETURN_IF_ERROR(InitMissileGFX()); - IncProgress(); - return {}; -} - -/** - * @param entry Where is the player entering from - */ -void CreateLevel(lvl_entry entry) -{ - CreateDungeon(DungeonSeeds[currlevel], entry); - - switch (leveltype) { - case DTYPE_TOWN: - InitTownTriggers(); - break; - case DTYPE_CATHEDRAL: - InitL1Triggers(); - break; - case DTYPE_CATACOMBS: - InitL2Triggers(); - break; - case DTYPE_CAVES: - InitL3Triggers(); - break; - case DTYPE_HELL: - InitL4Triggers(); - break; - case DTYPE_NEST: - InitHiveTriggers(); - break; - case DTYPE_CRYPT: - InitCryptTriggers(); - break; - default: - app_fatal("CreateLevel"); - } - - if (leveltype != DTYPE_TOWN) { - Freeupstairs(); - } - LoadRndLvlPal(leveltype); -} - -void UnstuckChargers() -{ - if (gbIsMultiplayer) { - for (Player &player : Players) { - if (!player.plractive) - continue; - if (player._pLvlChanging) - continue; - if (!player.isOnActiveLevel()) - continue; - if (&player == MyPlayer) - continue; - return; - } - } - for (size_t i = 0; i < ActiveMonsterCount; i++) { - Monster &monster = Monsters[ActiveMonsters[i]]; - if (monster.mode == MonsterMode::Charge) - monster.mode = MonsterMode::Stand; - } -} - -void UpdateMonsterLights() -{ - for (size_t i = 0; i < ActiveMonsterCount; i++) { - Monster &monster = Monsters[ActiveMonsters[i]]; - - if ((monster.flags & MFLAG_BERSERK) != 0) { - const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; - monster.lightId = AddLight(monster.position.tile, lightRadius); - } - - if (monster.lightId != NO_LIGHT) { - if (monster.lightId == MyPlayer->lightId) { // Fix old saves where some monsters had 0 instead of NO_LIGHT - monster.lightId = NO_LIGHT; - continue; - } - - const Light &light = Lights[monster.lightId]; - if (monster.position.tile != light.position.tile) { - ChangeLightXY(monster.lightId, monster.position.tile); - } - } - } -} - -void GameLogic() -{ - if (!ProcessInput()) { - return; - } - if (gbProcessPlayers) { - gGameLogicStep = GameLogicStep::ProcessPlayers; - ProcessPlayers(); - } - if (leveltype != DTYPE_TOWN) { - gGameLogicStep = GameLogicStep::ProcessMonsters; -#ifdef _DEBUG - if (!DebugInvisible) -#endif - ProcessMonsters(); - gGameLogicStep = GameLogicStep::ProcessObjects; - ProcessObjects(); - gGameLogicStep = GameLogicStep::ProcessMissiles; - ProcessMissiles(); - gGameLogicStep = GameLogicStep::ProcessItems; - ProcessItems(); - ProcessLightList(); - ProcessVisionList(); - } else { - gGameLogicStep = GameLogicStep::ProcessTowners; - ProcessTowners(); - gGameLogicStep = GameLogicStep::ProcessItemsTown; - ProcessItems(); - gGameLogicStep = GameLogicStep::ProcessMissilesTown; - ProcessMissiles(); - } - gGameLogicStep = GameLogicStep::None; - -#ifdef _DEBUG - if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { - ScrollView(); - } -#endif - - sound_update(); - CheckTriggers(); - CheckQuests(); - RedrawViewport(); - pfile_update(false); - - plrctrls_after_game_logic(); -} - -void TimeoutCursor(bool bTimeout) -{ - if (bTimeout) { - if (sgnTimeoutCurs == CURSOR_NONE && sgbMouseDown == CLICK_NONE) { - sgnTimeoutCurs = pcurs; - multi_net_ping(); - InfoString = StringOrView {}; - AddInfoBoxString(_("-- Network timeout --")); - AddInfoBoxString(_("-- Waiting for players --")); - for (uint8_t i = 0; i < Players.size(); i++) { - bool isConnected = (player_state[i] & PS_CONNECTED) != 0; - bool isActive = (player_state[i] & PS_ACTIVE) != 0; - if (!isConnected || isActive) continue; - - DvlNetLatencies latencies = DvlNet_GetLatencies(i); - - std::string ping = fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Player {:s} is timing out!")), - Players[i].name()); - - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); - - if (latencies.providerLatency) { - if (latencies.isRelayed && *latencies.isRelayed) { - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); - } else { - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); - } - } - EventPlrMsg(ping); - } - NewCursor(CURSOR_HOURGLASS); - RedrawEverything(); - } - scrollrt_draw_game_screen(); - } else if (sgnTimeoutCurs != CURSOR_NONE) { - // Timeout is gone, we should restore the previous cursor. - // But the timeout cursor could already be changed by the now processed messages (for example item cursor from CMD_GETITEM). - // Changing the item cursor back to the previous (hand) cursor could result in deleted items, because this resets Player.HoldItem (see NewCursor). - if (pcurs == CURSOR_HOURGLASS) - NewCursor(sgnTimeoutCurs); - sgnTimeoutCurs = CURSOR_NONE; - InfoString = StringOrView {}; - RedrawEverything(); - } -} - -void HelpKeyPressed() -{ - if (HelpFlag) { - HelpFlag = false; - } else if (IsPlayerInStore()) { - InfoString = StringOrView {}; - AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed - AddInfoBoxString(_("while in stores")); - LastPlayerAction = PlayerActionType::None; - } else { - CloseInventory(); - CloseCharPanel(); - SpellbookFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - QuestLogIsOpen = false; - CancelCurrentDiabloMsg(); - gamemenu_off(); - DisplayHelp(); - doom_close(); - } -} - -void InventoryKeyPressed() -{ - if (IsPlayerInStore()) - return; - invflag = !invflag; - if (!IsLeftPanelOpen() && CanPanelsCoverView()) { - if (!invflag) { // We closed the inventory - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } else if (!SpellbookFlag) { // We opened the inventory - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - } - SpellbookFlag = false; - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); -} - -void CharacterSheetKeyPressed() -{ - if (IsPlayerInStore()) - return; - if (!IsRightPanelOpen() && CanPanelsCoverView()) { - if (CharFlag) { // We are closing the character sheet - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } else if (!QuestLogIsOpen) { // We opened the character sheet - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } - } - ToggleCharPanel(); -} - -void PartyPanelSideToggleKeyPressed() -{ - PartySidePanelOpen = !PartySidePanelOpen; -} - -void QuestLogKeyPressed() -{ - if (IsPlayerInStore()) - return; - if (!QuestLogIsOpen) { - StartQuestlog(); - } else { - QuestLogIsOpen = false; - } - if (!IsRightPanelOpen() && CanPanelsCoverView()) { - if (!QuestLogIsOpen) { // We closed the quest log - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } else if (!CharFlag) { // We opened the character quest log - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } - } - CloseCharPanel(); - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); -} - -void DisplaySpellsKeyPressed() -{ - if (IsPlayerInStore()) - return; - CloseCharPanel(); - QuestLogIsOpen = false; - CloseInventory(); - SpellbookFlag = false; - if (!SpellSelectFlag) { - DoSpeedBook(); - } else { - SpellSelectFlag = false; - } - LastPlayerAction = PlayerActionType::None; -} - -void SpellBookKeyPressed() -{ - if (IsPlayerInStore()) - return; - SpellbookFlag = !SpellbookFlag; - if (!IsLeftPanelOpen() && CanPanelsCoverView()) { - if (!SpellbookFlag) { // We closed the inventory - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } else if (!invflag) { // We opened the inventory - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - } - CloseInventory(); - CloseVisualStore(); -} - -void CycleSpellHotkeys(bool next) -{ - StaticVector validHotKeyIndexes; - std::optional currentIndex; - for (size_t slot = 0; slot < NumHotkeys; slot++) { - if (!IsValidSpeedSpell(slot)) - continue; - if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) { - // found current - currentIndex = validHotKeyIndexes.size(); - } - validHotKeyIndexes.emplace_back(slot); - } - if (validHotKeyIndexes.size() == 0) - return; - - size_t newIndex; - if (!currentIndex) { - newIndex = next ? 0 : (validHotKeyIndexes.size() - 1); - } else if (next) { - newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1); - } else { - newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1); - } - ToggleSpell(validHotKeyIndexes[newIndex]); -} - -bool IsPlayerDead() -{ - return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; -} - -bool IsGameRunning() -{ - return PauseMode != 2; -} - -bool CanPlayerTakeAction() -{ - return !IsPlayerDead() && IsGameRunning(); -} - -bool CanAutomapBeToggledOff() -{ - // check if every window is closed - if yes, automap can be toggled off - return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !IsVisualStoreOpen && !CharFlag - && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag - && !ChatLogFlag && !HelpFlag; -} - -void OptionLanguageCodeChanged() -{ - UnloadFonts(); - LanguageInitialize(); - LoadLanguageArchive(); - effects_cleanup_sfx(false); - if (gbRunGame) - sound_init(); - else - ui_sound_init(); -} - -const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); - -} // namespace - -void InitKeymapActions() -{ - Options &options = GetOptions(); - for (uint32_t i = 0; i < 8; ++i) { - options.Keymapper.AddAction( - "BeltItem{}", - N_("Belt item {}"), - N_("Use Belt item."), - '1' + i, - [i] { - const Player &myPlayer = *MyPlayer; - if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { - UseInvItem(INVITEM_BELT_FIRST + i); - } - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - for (uint32_t i = 0; i < NumHotkeys; ++i) { - options.Keymapper.AddAction( - "QuickSpell{}", - N_("Quick spell {}"), - N_("Hotkey for skill or spell."), - i < 4 ? static_cast(SDLK_F5) + i : static_cast(SDLK_UNKNOWN), - [i]() { - if (SpellSelectFlag) { - SetSpeedSpell(i); - return; - } - if (!*GetOptions().Gameplay.quickCast) - ToggleSpell(i); - else - QuickCast(i); - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - options.Keymapper.AddAction( - "QuickSpellPrevious", - N_("Previous quick spell"), - N_("Selects the previous quick spell (cycles)."), - MouseScrollUpButton, - [] { CycleSpellHotkeys(false); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuickSpellNext", - N_("Next quick spell"), - N_("Selects the next quick spell (cycles)."), - MouseScrollDownButton, - [] { CycleSpellHotkeys(true); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "UseHealthPotion", - N_("Use health potion"), - N_("Use health potions from belt."), - SDLK_UNKNOWN, - [] { UseBeltItem(BeltItemType::Healing); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "UseManaPotion", - N_("Use mana potion"), - N_("Use mana potions from belt."), - SDLK_UNKNOWN, - [] { UseBeltItem(BeltItemType::Mana); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "DisplaySpells", - N_("Speedbook"), - N_("Open Speedbook."), - 'S', - DisplaySpellsKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuickSave", - N_("Quick save"), - N_("Saves the game."), - SDLK_F2, - [] { gamemenu_save_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); - options.Keymapper.AddAction( - "QuickLoad", - N_("Quick load"), - N_("Loads the game."), - SDLK_F3, - [] { gamemenu_load_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); -#ifndef NOEXIT - options.Keymapper.AddAction( - "QuitGame", - N_("Quit game"), - N_("Closes the game."), - SDLK_UNKNOWN, - [] { gamemenu_quit_game(false); }); -#endif - options.Keymapper.AddAction( - "StopHero", - N_("Stop hero"), - N_("Stops walking and cancel pending actions."), - SDLK_UNKNOWN, - [] { MyPlayer->Stop(); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "ItemHighlighting", - N_("Item highlighting"), - N_("Show/hide items on ground."), - SDLK_LALT, - [] { HighlightKeyPressed(true); }, - [] { HighlightKeyPressed(false); }); - options.Keymapper.AddAction( - "ToggleItemHighlighting", - N_("Toggle item highlighting"), - N_("Permanent show/hide items on ground."), - SDLK_RCTRL, - nullptr, - [] { ToggleItemLabelHighlight(); }); - options.Keymapper.AddAction( - "ToggleAutomap", - N_("Toggle automap"), - N_("Toggles if automap is displayed."), - SDLK_TAB, - DoAutoMap, - nullptr, - IsGameRunning); - options.Keymapper.AddAction( - "CycleAutomapType", - N_("Cycle map type"), - N_("Opaque -> Transparent -> Minimap -> None"), - SDLK_M, - CycleAutomapType, - nullptr, - IsGameRunning); - - options.Keymapper.AddAction( - "Inventory", - N_("Inventory"), - N_("Open Inventory screen."), - 'I', - InventoryKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Character", - N_("Character"), - N_("Open Character screen."), - 'C', - CharacterSheetKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Party", - N_("Party"), - N_("Open side Party panel."), - 'Y', - PartyPanelSideToggleKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuestLog", - N_("Quest log"), - N_("Open Quest log."), - 'Q', - QuestLogKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "SpellBook", - N_("Spellbook"), - N_("Open Spellbook."), - 'B', - SpellBookKeyPressed, - nullptr, - CanPlayerTakeAction); - for (uint32_t i = 0; i < QuickMessages.size(); ++i) { - options.Keymapper.AddAction( - "QuickMessage{}", - N_("Quick Message {}"), - N_("Use Quick Message in chat."), - (i < 4) ? static_cast(SDLK_F9) + i : static_cast(SDLK_UNKNOWN), - [i]() { DiabloHotkeyMsg(i); }, - nullptr, - nullptr, - i + 1); - } - options.Keymapper.AddAction( - "HideInfoScreens", - N_("Hide Info Screens"), - N_("Hide all info screens."), - SDLK_SPACE, - [] { - if (CanAutomapBeToggledOff()) - AutomapActive = false; - - ClosePanels(); - HelpFlag = false; - ChatLogFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - - CancelCurrentDiabloMsg(); - gamemenu_off(); - doom_close(); - }, - nullptr, - IsGameRunning); - options.Keymapper.AddAction( - "Zoom", - N_("Zoom"), - N_("Zoom Game Screen."), - 'Z', - [] { - GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); - CalcViewportGeometry(); - }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "PauseGame", - N_("Pause Game"), - N_("Pauses the game."), - 'P', - diablo_pause_game); - options.Keymapper.AddAction( - "PauseGameAlternate", - N_("Pause Game (Alternate)"), - N_("Pauses the game."), - SDLK_PAUSE, - diablo_pause_game); - options.Keymapper.AddAction( - "DecreaseBrightness", - N_("Decrease Brightness"), - N_("Reduce screen brightness."), - 'F', - DecreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "IncreaseBrightness", - N_("Increase Brightness"), - N_("Increase screen brightness."), - 'G', - IncreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Help", - N_("Help"), - N_("Open Help Screen."), - SDLK_F1, - HelpKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Screenshot", - N_("Screenshot"), - N_("Takes a screenshot."), - SDLK_PRINTSCREEN, - nullptr, - CaptureScreen); - options.Keymapper.AddAction( - "GameInfo", - N_("Game info"), - N_("Displays game infos."), - 'V', - [] { - EventPlrMsg(fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), - PROJECT_NAME, - PROJECT_VERSION), - UiFlags::ColorWhite); - }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "ChatLog", - N_("Chat Log"), - N_("Displays chat log."), - 'L', - [] { - ToggleChatLog(); - }); - options.Keymapper.AddAction( - "SortInv", - N_("Sort Inventory"), - N_("Sorts the inventory."), - 'R', - [] { - ReorganizeInventory(*MyPlayer); - }); -#ifdef _DEBUG - options.Keymapper.AddAction( - "OpenConsole", - N_("Console"), - N_("Opens Lua console."), - SDLK_GRAVE, - OpenConsole); - options.Keymapper.AddAction( - "DebugToggle", - "Debug toggle", - "Programming is like magic.", - 'X', - [] { - DebugToggle = !DebugToggle; - }); -#endif - options.Keymapper.CommitActions(); -} - -void InitPadmapActions() -{ - Options &options = GetOptions(); - for (int i = 0; i < 8; ++i) { - options.Padmapper.AddAction( - "BeltItem{}", - N_("Belt item {}"), - N_("Use Belt item."), - ControllerButton_NONE, - [i] { - const Player &myPlayer = *MyPlayer; - if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { - UseInvItem(INVITEM_BELT_FIRST + i); - } - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - for (uint32_t i = 0; i < NumHotkeys; ++i) { - options.Padmapper.AddAction( - "QuickSpell{}", - N_("Quick spell {}"), - N_("Hotkey for skill or spell."), - ControllerButton_NONE, - [i]() { - if (SpellSelectFlag) { - SetSpeedSpell(i); - return; - } - if (!*GetOptions().Gameplay.quickCast) - ToggleSpell(i); - else - QuickCast(i); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }, - i + 1); - } - options.Padmapper.AddAction( - "PrimaryAction", - N_("Primary action"), - N_("Attack monsters, talk to towners, lift and place inventory items."), - ControllerButton_BUTTON_B, - [] { - ControllerActionHeld = GameActionType_PRIMARY_ACTION; - LastPlayerAction = PlayerActionType::None; - PerformPrimaryAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SecondaryAction", - N_("Secondary action"), - N_("Open chests, interact with doors, pick up items."), - ControllerButton_BUTTON_Y, - [] { - ControllerActionHeld = GameActionType_SECONDARY_ACTION; - LastPlayerAction = PlayerActionType::None; - PerformSecondaryAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SpellAction", - N_("Spell action"), - N_("Cast the active spell."), - ControllerButton_BUTTON_X, - [] { - ControllerActionHeld = GameActionType_CAST_SPELL; - LastPlayerAction = PlayerActionType::None; - PerformSpellAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "CancelAction", - N_("Cancel action"), - N_("Close menus."), - ControllerButton_BUTTON_A, - [] { - if (DoomFlag) { - doom_close(); - return; - } - - GameAction action; - if (SpellSelectFlag) - action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); - else if (invflag) - action = GameAction(GameActionType_TOGGLE_INVENTORY); - else if (SpellbookFlag) - action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); - else if (QuestLogIsOpen) - action = GameAction(GameActionType_TOGGLE_QUEST_LOG); - else if (CharFlag) - action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - ProcessGameAction(action); - }, - nullptr, - [] { return DoomFlag || SpellSelectFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag; }); - options.Padmapper.AddAction( - "MoveUp", - N_("Move up"), - N_("Moves the player character up."), - ControllerButton_BUTTON_DPAD_UP, - [] {}); - options.Padmapper.AddAction( - "MoveDown", - N_("Move down"), - N_("Moves the player character down."), - ControllerButton_BUTTON_DPAD_DOWN, - [] {}); - options.Padmapper.AddAction( - "MoveLeft", - N_("Move left"), - N_("Moves the player character left."), - ControllerButton_BUTTON_DPAD_LEFT, - [] {}); - options.Padmapper.AddAction( - "MoveRight", - N_("Move right"), - N_("Moves the player character right."), - ControllerButton_BUTTON_DPAD_RIGHT, - [] {}); - options.Padmapper.AddAction( - "StandGround", - N_("Stand ground"), - N_("Hold to prevent the player from moving."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "ToggleStandGround", - N_("Toggle stand ground"), - N_("Toggle whether the player moves."), - ControllerButton_NONE, - [] { StandToggle = !StandToggle; }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "UseHealthPotion", - N_("Use health potion"), - N_("Use health potions from belt."), - ControllerButton_BUTTON_LEFTSHOULDER, - [] { UseBeltItem(BeltItemType::Healing); }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "UseManaPotion", - N_("Use mana potion"), - N_("Use mana potions from belt."), - ControllerButton_BUTTON_RIGHTSHOULDER, - [] { UseBeltItem(BeltItemType::Mana); }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Character", - N_("Character"), - N_("Open Character screen."), - ControllerButton_AXIS_TRIGGERLEFT, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "Inventory", - N_("Inventory"), - N_("Open Inventory screen."), - ControllerButton_AXIS_TRIGGERRIGHT, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "QuestLog", - N_("Quest log"), - N_("Open Quest log."), - { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "SpellBook", - N_("Spellbook"), - N_("Open Spellbook."), - { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "DisplaySpells", - N_("Speedbook"), - N_("Open Speedbook."), - ControllerButton_BUTTON_A, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "ToggleAutomap", - N_("Toggle automap"), - N_("Toggles if automap is displayed."), - ControllerButton_BUTTON_LEFTSTICK, - DoAutoMap); - options.Padmapper.AddAction( - "AutomapMoveUp", - N_("Automap Move Up"), - N_("Moves the automap up when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveDown", - N_("Automap Move Down"), - N_("Moves the automap down when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveLeft", - N_("Automap Move Left"), - N_("Moves the automap left when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveRight", - N_("Automap Move Right"), - N_("Moves the automap right when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "MouseUp", - N_("Move mouse up"), - N_("Simulates upward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, - [] {}); - options.Padmapper.AddAction( - "MouseDown", - N_("Move mouse down"), - N_("Simulates downward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, - [] {}); - options.Padmapper.AddAction( - "MouseLeft", - N_("Move mouse left"), - N_("Simulates leftward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, - [] {}); - options.Padmapper.AddAction( - "MouseRight", - N_("Move mouse right"), - N_("Simulates rightward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, - [] {}); - auto leftMouseDown = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - sgbMouseDown = CLICK_LEFT; - LeftMouseDown(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); - }; - auto leftMouseUp = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - LeftMouseUp(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); - }; - options.Padmapper.AddAction( - "LeftMouseClick1", - N_("Left mouse click"), - N_("Simulates the left mouse button."), - ControllerButton_BUTTON_RIGHTSTICK, - leftMouseDown, - leftMouseUp); - options.Padmapper.AddAction( - "LeftMouseClick2", - N_("Left mouse click"), - N_("Simulates the left mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, - leftMouseDown, - leftMouseUp); - auto rightMouseDown = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_RIGHT; - RightMouseDown(standGround); - }; - auto rightMouseUp = [] { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - }; - options.Padmapper.AddAction( - "RightMouseClick1", - N_("Right mouse click"), - N_("Simulates the right mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, - rightMouseDown, - rightMouseUp); - options.Padmapper.AddAction( - "RightMouseClick2", - N_("Right mouse click"), - N_("Simulates the right mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, - rightMouseDown, - rightMouseUp); - options.Padmapper.AddAction( - "PadHotspellMenu", - N_("Gamepad hotspell menu"), - N_("Hold to set or use spell hotkeys."), - ControllerButton_BUTTON_BACK, - [] { PadHotspellMenuActive = true; }, - [] { PadHotspellMenuActive = false; }); - options.Padmapper.AddAction( - "PadMenuNavigator", - N_("Gamepad menu navigator"), - N_("Hold to access gamepad menu navigation."), - ControllerButton_BUTTON_START, - [] { PadMenuNavigatorActive = true; }, - [] { PadMenuNavigatorActive = false; }); - auto toggleGameMenu = [] { - const bool inMenu = gmenu_is_active(); - PressEscKey(); - LastPlayerAction = PlayerActionType::None; - PadHotspellMenuActive = false; - PadMenuNavigatorActive = false; - if (!inMenu) - gamemenu_on(); - }; - options.Padmapper.AddAction( - "ToggleGameMenu1", - N_("Toggle game menu"), - N_("Opens the game menu."), - { - ControllerButton_BUTTON_BACK, - ControllerButton_BUTTON_START, - }, - toggleGameMenu); - options.Padmapper.AddAction( - "ToggleGameMenu2", - N_("Toggle game menu"), - N_("Opens the game menu."), - { - ControllerButton_BUTTON_START, - ControllerButton_BUTTON_BACK, - }, - toggleGameMenu); - options.Padmapper.AddAction( - "QuickSave", - N_("Quick save"), - N_("Saves the game."), - ControllerButton_NONE, - [] { gamemenu_save_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); - options.Padmapper.AddAction( - "QuickLoad", - N_("Quick load"), - N_("Loads the game."), - ControllerButton_NONE, - [] { gamemenu_load_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); - options.Padmapper.AddAction( - "ItemHighlighting", - N_("Item highlighting"), - N_("Show/hide items on ground."), - ControllerButton_NONE, - [] { HighlightKeyPressed(true); }, - [] { HighlightKeyPressed(false); }); - options.Padmapper.AddAction( - "ToggleItemHighlighting", - N_("Toggle item highlighting"), - N_("Permanent show/hide items on ground."), - ControllerButton_NONE, - nullptr, - [] { ToggleItemLabelHighlight(); }); - options.Padmapper.AddAction( - "HideInfoScreens", - N_("Hide Info Screens"), - N_("Hide all info screens."), - ControllerButton_NONE, - [] { - if (CanAutomapBeToggledOff()) - AutomapActive = false; - - ClosePanels(); - HelpFlag = false; - ChatLogFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - - CancelCurrentDiabloMsg(); - gamemenu_off(); - doom_close(); - }, - nullptr, - IsGameRunning); - options.Padmapper.AddAction( - "Zoom", - N_("Zoom"), - N_("Zoom Game Screen."), - ControllerButton_NONE, - [] { - GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); - CalcViewportGeometry(); - }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "PauseGame", - N_("Pause Game"), - N_("Pauses the game."), - ControllerButton_NONE, - diablo_pause_game); - options.Padmapper.AddAction( - "DecreaseBrightness", - N_("Decrease Brightness"), - N_("Reduce screen brightness."), - ControllerButton_NONE, - DecreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "IncreaseBrightness", - N_("Increase Brightness"), - N_("Increase screen brightness."), - ControllerButton_NONE, - IncreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Help", - N_("Help"), - N_("Open Help Screen."), - ControllerButton_NONE, - HelpKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Screenshot", - N_("Screenshot"), - N_("Takes a screenshot."), - ControllerButton_NONE, - nullptr, - CaptureScreen); - options.Padmapper.AddAction( - "GameInfo", - N_("Game info"), - N_("Displays game infos."), - ControllerButton_NONE, - [] { - EventPlrMsg(fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), - PROJECT_NAME, - PROJECT_VERSION), - UiFlags::ColorWhite); - }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SortInv", - N_("Sort Inventory"), - N_("Sorts the inventory."), - ControllerButton_NONE, - [] { - ReorganizeInventory(*MyPlayer); - }); - options.Padmapper.AddAction( - "ChatLog", - N_("Chat Log"), - N_("Displays chat log."), - ControllerButton_NONE, - [] { - ToggleChatLog(); - }); - options.Padmapper.CommitActions(); -} - -void SetCursorPos(Point position) -{ - if (ControlDevice != ControlTypes::KeyboardAndMouse) { - MousePosition = position; - return; - } - - LogicalToOutput(&position.x, &position.y); - if (!demo::IsRunning()) - SDL_WarpMouseInWindow(ghMainWnd, position.x, position.y); -} - -void FreeGameMem() -{ - pDungeonCels = nullptr; - pMegaTiles = nullptr; - pSpecialCels = std::nullopt; - - FreeMonsters(); - FreeMissileGFX(); - FreeObjectGFX(); - FreeTownerGFX(); - FreeStashGFX(); - FreeVisualStoreGFX(); -#ifndef USE_SDL1 - DeactivateVirtualGamepad(); - FreeVirtualGamepadGFX(); -#endif -} - -bool StartGame(bool bNewGame, bool bSinglePlayer) -{ - gbSelectProvider = true; - ReturnToMainMenu = false; - - do { - gbLoadGame = false; - - if (!NetInit(bSinglePlayer)) { - gbRunGameResult = true; - break; - } - - // Save 2.8 MiB of RAM by freeing all main menu resources - // before starting the game. - UiDestroy(); - - gbSelectProvider = false; - - if (bNewGame || !gbValidSaveFile) { - InitLevels(); - InitQuests(); - InitPortals(); - InitDungMsgs(*MyPlayer); - DeltaSyncJunk(); - } - giNumberOfLevels = gbIsHellfire ? 25 : 17; - interface_mode uMsg = WM_DIABNEWGAME; - if (gbValidSaveFile && gbLoadGame) { - uMsg = WM_DIABLOADGAME; - } - RunGameLoop(uMsg); - NetClose(); - UnloadFonts(); - - // If the player left the game into the main menu, - // initialize main menu resources. - if (gbRunGameResult) - UiInitialize(); - if (ReturnToMainMenu) - return true; - } while (gbRunGameResult); - - SNetDestroy(); - return gbRunGameResult; -} - -void diablo_quit(int exitStatus) -{ - FreeGameMem(); - music_stop(); - DiabloDeinit(); - -#if SDL_VERSION_ATLEAST(2, 0, 0) - if (SdlLogFile != nullptr) std::fclose(SdlLogFile); -#endif - - exit(exitStatus); -} - -#ifdef __UWP__ -void (*onInitialized)() = NULL; - -void setOnInitialized(void (*callback)()) -{ - onInitialized = callback; -} -#endif - -int DiabloMain(int argc, char **argv) -{ -#ifdef _DEBUG - SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); -#endif - - DiabloParseFlags(argc, argv); - InitKeymapActions(); - InitPadmapActions(); - - // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings - LoadCoreArchives(); - was_archives_init = true; - - // Read settings including translation next. This will use the presence of fonts.mpq and look for assets in devilutionx.mpq - LoadOptions(); - if (demo::IsRunning()) demo::OverrideOptions(); - - // Then look for a voice pack file based on the selected translation - LoadLanguageArchive(); - - ApplicationInit(); - LuaInitialize(); - if (!demo::IsRunning()) SaveOptions(); - - // Finally load game data - LoadGameArchives(); - - LoadTextData(); - - // Load dynamic data before we go into the menu as we need to initialise player characters in memory pretty early. - LoadPlayerDataFiles(); - - // TODO: We can probably load this much later (when the game is starting). - LoadSpellData(); - LoadMissileData(); - LoadMonsterData(); - LoadItemData(); - LoadObjectData(); - LoadQuestData(); - - DiabloInit(); -#ifdef __UWP__ - onInitialized(); -#endif - if (!demo::IsRunning()) SaveOptions(); - - DiabloSplash(); - mainmenu_loop(); - DiabloDeinit(); - - return 0; -} - -bool TryIconCurs() -{ - if (pcurs == CURSOR_RESURRECT) { - if (PlayerUnderCursor != nullptr) { - NetSendCmdParam1(true, CMD_RESURRECT, PlayerUnderCursor->getId()); - NewCursor(CURSOR_HAND); - return true; - } - - return false; - } - - if (pcurs == CURSOR_HEALOTHER) { - if (PlayerUnderCursor != nullptr) { - NetSendCmdParam1(true, CMD_HEALOTHER, PlayerUnderCursor->getId()); - NewCursor(CURSOR_HAND); - return true; - } - - return false; - } - - if (pcurs == CURSOR_TELEKINESIS) { - DoTelekinesis(); - return true; - } - - Player &myPlayer = *MyPlayer; - - if (pcurs == CURSOR_IDENTIFY) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) - CheckIdentify(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - item._iIdentified = true; - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_REPAIR) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) { - if (IsVisualStoreOpen) - VisualStoreRepairItem(pcursinvitem); - else - DoRepair(myPlayer, pcursinvitem); - } else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - RepairItem(item, myPlayer.getCharacterLevel()); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_RECHARGE) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) - DoRecharge(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - RechargeItem(item, myPlayer); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_OIL) { - bool changeCursor = true; - if (pcursinvitem != -1 && !IsInspectingPlayer()) - changeCursor = DoOil(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - changeCursor = ApplyOilToItem(item, myPlayer); - } - if (changeCursor) - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_TELEPORT) { - const SpellID spellID = myPlayer.inventorySpell; - const SpellType spellType = SpellType::Scroll; - const int spellFrom = myPlayer.spellFrom; - if (IsWallSpell(spellID)) { - const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); - NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); - } else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) { - NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); - } else { - NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast(spellID), static_cast(spellType), spellFrom); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_DISARM && ObjectUnderCursor == nullptr) { - NewCursor(CURSOR_HAND); - return true; - } - - return false; -} - -void diablo_pause_game() -{ - if (!gbIsMultiplayer) { - if (PauseMode != 0) { - PauseMode = 0; - } else { - PauseMode = 2; - sound_stop(); - qtextflag = false; - LastPlayerAction = PlayerActionType::None; - } - - RedrawEverything(); - } -} - -bool GameWasAlreadyPaused = false; -bool MinimizePaused = false; - -bool diablo_is_focused() -{ -#ifndef USE_SDL1 - return SDL_GetKeyboardFocus() == ghMainWnd; -#else - Uint8 appState = SDL_GetAppState(); - return (appState & SDL_APPINPUTFOCUS) != 0; -#endif -} - -void diablo_focus_pause() -{ - if (!movie_playing && (gbIsMultiplayer || MinimizePaused)) { - return; - } - - GameWasAlreadyPaused = PauseMode != 0; - - if (!GameWasAlreadyPaused) { - PauseMode = 2; - sound_stop(); - LastPlayerAction = PlayerActionType::None; - } - - SVidMute(); - music_mute(); - - MinimizePaused = true; -} - -void diablo_focus_unpause() -{ - if (!GameWasAlreadyPaused) { - PauseMode = 0; - } - - SVidUnmute(); - music_unmute(); - - MinimizePaused = false; -} - -bool PressEscKey() -{ - bool rv = false; - - if (DoomFlag) { - doom_close(); - rv = true; - } - - if (HelpFlag) { - HelpFlag = false; - rv = true; - } - - if (ChatLogFlag) { - ChatLogFlag = false; - rv = true; - } - - if (qtextflag) { - qtextflag = false; - stream_stop(); - rv = true; - } - - if (IsPlayerInStore()) { - StoreESC(); - rv = true; - } - - if (IsDiabloMsgAvailable()) { - CancelCurrentDiabloMsg(); - rv = true; - } - - if (ChatFlag) { - ResetChat(); - rv = true; - } - - if (DropGoldFlag) { - control_drop_gold(SDLK_ESCAPE); - rv = true; - } - - if (IsWithdrawGoldOpen) { - WithdrawGoldKeyPress(SDLK_ESCAPE); - rv = true; - } - - if (SpellSelectFlag) { - SpellSelectFlag = false; - rv = true; - } - - if (IsLeftPanelOpen() || IsRightPanelOpen()) { - ClosePanels(); - rv = true; - } - - return rv; -} - -void DisableInputEventHandler(const SDL_Event &event, uint16_t /*modState*/) -{ - switch (event.type) { - case SDL_EVENT_MOUSE_MOTION: - MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; - return; - case SDL_EVENT_MOUSE_BUTTON_DOWN: - if (sgbMouseDown != CLICK_NONE) - return; - switch (event.button.button) { - case SDL_BUTTON_LEFT: - sgbMouseDown = CLICK_LEFT; - return; - case SDL_BUTTON_RIGHT: - sgbMouseDown = CLICK_RIGHT; - return; - default: - return; - } - case SDL_EVENT_MOUSE_BUTTON_UP: - sgbMouseDown = CLICK_NONE; - return; - } - - MainWndProc(event); -} - -void LoadGameLevelStopMusic(_music_id neededTrack) -{ - if (neededTrack != sgnMusicTrack) - music_stop(); -} - -void LoadGameLevelStartMusic(_music_id neededTrack) -{ - if (sgnMusicTrack != neededTrack) - music_start(neededTrack); - - if (MinimizePaused) { - music_mute(); - } -} - -void LoadGameLevelResetCursor() -{ - if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { - NewCursor(CURSOR_HAND); - } -} - -void SetRndSeedForDungeonLevel() -{ - if (setlevel) { - // Maps are not randomly generated, but the monsters max hitpoints are. - // So we need to ensure that we have a stable seed when generating quest/set-maps. - // For this purpose we reuse the normal dungeon seeds. - SetRndSeed(DungeonSeeds[static_cast(setlvlnum)]); - } else { - SetRndSeed(DungeonSeeds[currlevel]); - } -} - -void LoadGameLevelFirstFlagEntry() -{ - CloseInventory(); - qtextflag = false; - if (!HeadlessMode) { - InitInv(); - ClearUniqueItemFlags(); - InitQuestText(); - InitInfoBoxGfx(); - InitHelp(); - } - InitStores(); - InitAutomapOnce(); -} - -void LoadGameLevelStores() -{ - if (leveltype == DTYPE_TOWN) { - SetupTownStores(); - } else { - FreeStoreMem(); - } -} - -void LoadGameLevelStash() -{ - const bool isHellfireSaveGame = gbIsHellfireSaveGame; - - gbIsHellfireSaveGame = gbIsHellfire; - LoadStash(); - gbIsHellfireSaveGame = isHellfireSaveGame; -} - -tl::expected LoadGameLevelDungeon(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pLvlVisited[currlevel] || gbIsMultiplayer) { - HoldThemeRooms(); - [[maybe_unused]] const uint32_t mid1Seed = GetLCGEngineState(); - InitGolems(); - InitObjects(); - [[maybe_unused]] const uint32_t mid2Seed = GetLCGEngineState(); - - IncProgress(); - - RETURN_IF_ERROR(InitMonsters()); - InitItems(); - CreateThemeRooms(); - - IncProgress(); - - [[maybe_unused]] const uint32_t mid3Seed = GetLCGEngineState(); - InitMissiles(); - InitCorpses(); -#ifdef _DEBUG - SetDebugLevelSeedInfos(mid1Seed, mid2Seed, mid3Seed, GetLCGEngineState()); -#endif - SavePreLighting(); - - IncProgress(); - - if (gbIsMultiplayer) - DeltaLoadLevel(); - } else { - HoldThemeRooms(); - InitGolems(); - RETURN_IF_ERROR(InitMonsters()); - InitMissiles(); - InitCorpses(); - - IncProgress(); - - RETURN_IF_ERROR(LoadLevel()); - - IncProgress(); - } - return {}; -} - -void LoadGameLevelSyncPlayerEntry(lvl_entry lvldir) -{ - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel() && (!player._pLvlChanging || &player == MyPlayer)) { - if (player._pHitPoints > 0) { - if (lvldir != ENTRY_LOAD) - SyncInitPlrPos(player); - } else { - dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; - } - } - } -} - -void LoadGameLevelLightVision() -{ - if (leveltype != DTYPE_TOWN) { - memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light - ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh - ProcessLightList(); - ProcessVisionList(); - } -} - -void LoadGameLevelReturn() -{ - ViewPosition = GetMapReturnPosition(); - if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) - Quests[Q_BETRAYER]._qvar2 = 2; -} - -void LoadGameLevelInitPlayers(bool firstflag, lvl_entry lvldir) -{ - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel()) { - InitPlayerGFX(player); - if (lvldir != ENTRY_LOAD) - InitPlayer(player, firstflag); - } - } -} - -void LoadGameLevelSetVisited() -{ - bool visited = false; - for (const Player &player : Players) { - if (player.plractive) - visited = visited || player._pLvlVisited[currlevel]; - } -} - -tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) - for (int j = 0; j < MAXDUNY; j++) { - dFlags[i][j] |= DungeonFlag::Lit; - } - } - - InitTowners(); - InitStash(); - InitVisualStore(); - InitItems(); - InitMissiles(); - - IncProgress(); - - if (!firstflag && lvldir != ENTRY_LOAD && myPlayer._pLvlVisited[currlevel] && !gbIsMultiplayer) - RETURN_IF_ERROR(LoadLevel()); - if (gbIsMultiplayer) - DeltaLoadLevel(); - - IncProgress(); - - for (int x = 0; x < DMAXX; x++) - for (int y = 0; y < DMAXY; y++) - UpdateAutomapExplorer({ x, y }, MAP_EXP_SELF); - return {}; -} - -tl::expected LoadGameLevelSetLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - LoadSetMap(); - IncProgress(); - RETURN_IF_ERROR(GetLevelMTypes()); - IncProgress(); - InitGolems(); - RETURN_IF_ERROR(InitMonsters()); - IncProgress(); - if (!HeadlessMode) { -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - RETURN_IF_ERROR(InitMissileGFX()); - IncProgress(); - } - InitCorpses(); - IncProgress(); - - if (lvldir == ENTRY_WARPLVL) - GetPortalLvlPos(); - IncProgress(); - - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel()) { - InitPlayerGFX(player); - if (lvldir != ENTRY_LOAD) - InitPlayer(player, firstflag); - } - } - IncProgress(); - InitMultiView(); - IncProgress(); - - if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pSLvlVisited[setlvlnum] || gbIsMultiplayer) { - InitItems(); - SavePreLighting(); - } else { - RETURN_IF_ERROR(LoadLevel()); - } - if (gbIsMultiplayer) { - DeltaLoadLevel(); - if (!UseMultiplayerQuests()) - ResyncQuests(); - } - - PlayDungMsgs(); - InitMissiles(); - IncProgress(); - return {}; -} - -tl::expected LoadGameLevelStandardLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - CreateLevel(lvldir); - - IncProgress(); - - SetRndSeedForDungeonLevel(); - - if (leveltype != DTYPE_TOWN) { - RETURN_IF_ERROR(GetLevelMTypes()); - InitThemes(); - if (!HeadlessMode) - RETURN_IF_ERROR(LoadAllGFX()); - } else if (!HeadlessMode) { - IncProgress(); - -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - - IncProgress(); - - RETURN_IF_ERROR(InitMissileGFX()); - - IncProgress(); - IncProgress(); - } - - IncProgress(); - - if (lvldir == ENTRY_RTNLVL) { - LoadGameLevelReturn(); - } - - if (lvldir == ENTRY_WARPLVL) - GetPortalLvlPos(); - - IncProgress(); - - LoadGameLevelInitPlayers(firstflag, lvldir); - InitMultiView(); - - IncProgress(); - - LoadGameLevelSetVisited(); - - SetRndSeedForDungeonLevel(); - - if (leveltype == DTYPE_TOWN) { - LoadGameLevelTown(firstflag, lvldir, myPlayer); - } else { - LoadGameLevelDungeon(firstflag, lvldir, myPlayer); - } - - PlayDungMsgs(); - - if (UseMultiplayerQuests()) - ResyncMPQuests(); - else - ResyncQuests(); - return {}; -} - -void LoadGameLevelCrypt() -{ - if (CornerStone.isAvailable()) { - CornerstoneLoad(CornerStone.position); - } - if (Quests[Q_NAKRUL]._qactive == QUEST_DONE && currlevel == 24) { - SyncNakrulRoom(); - } -} - -void LoadGameLevelCalculateCursor() -{ - // Recalculate mouse selection of entities after level change/load - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - ResetItemlabelHighlighted(); // level changed => item changed - pcursmonst = -1; // ensure pcurstemp is set to a valid value - CheckCursMove(); -} - -tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) -{ - const _music_id neededTrack = GetLevelMusic(leveltype); - - ClearFloatingNumbers(); - LoadGameLevelStopMusic(neededTrack); - LoadGameLevelResetCursor(); - SetRndSeedForDungeonLevel(); - NaKrulTomeSequence = 0; - - IncProgress(); - - RETURN_IF_ERROR(LoadTrns()); - MakeLightTable(); - RETURN_IF_ERROR(LoadLevelSOLData()); - - IncProgress(); - - RETURN_IF_ERROR(LoadLvlGFX()); - SetDungeonMicros(pDungeonCels, MicroTileLen); - ClearClxDrawCache(); - - IncProgress(); - - if (firstflag) { - LoadGameLevelFirstFlagEntry(); - } - - SetRndSeedForDungeonLevel(); - - LoadGameLevelStores(); - - if (firstflag || lvldir == ENTRY_LOAD) { - LoadGameLevelStash(); - } - - IncProgress(); - - InitAutomap(); - - if (leveltype != DTYPE_TOWN && lvldir != ENTRY_LOAD) { - InitLighting(); - } - - InitLevelMonsters(); - - IncProgress(); - - const Player &myPlayer = *MyPlayer; - - if (setlevel) { - RETURN_IF_ERROR(LoadGameLevelSetLevel(firstflag, lvldir, myPlayer)); - } else { - RETURN_IF_ERROR(LoadGameLevelStandardLevel(firstflag, lvldir, myPlayer)); - } - - SyncPortals(); - LoadGameLevelSyncPlayerEntry(lvldir); - - IncProgress(); - IncProgress(); - - if (firstflag) { - RETURN_IF_ERROR(InitMainPanel()); - } - - IncProgress(); - - UpdateMonsterLights(); - UnstuckChargers(); - - LoadGameLevelLightVision(); - - if (leveltype == DTYPE_CRYPT) { - LoadGameLevelCrypt(); - } - -#ifndef USE_SDL1 - ActivateVirtualGamepad(); -#endif - LoadGameLevelStartMusic(neededTrack); - - CompleteProgress(); - - LoadGameLevelCalculateCursor(); - return {}; -} - -bool game_loop(bool bStartup) -{ - const uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; - - for (unsigned i = 0; i < wait; i++) { - if (!multi_handle_delta()) { - TimeoutCursor(true); - return false; - } - TimeoutCursor(false); - GameLogic(); - ClearLastSentPlayerCmd(); - - if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) - break; - } - return true; -} - -void diablo_color_cyc_logic() -{ - if (!*GetOptions().Graphics.colorCycling) - return; - - if (PauseMode != 0) - return; - - if (leveltype == DTYPE_CAVES) { - if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { - UpdatePWaterPalette(); - } else { - palette_update_caves(); - } - } else if (leveltype == DTYPE_HELL) { - lighting_color_cycling(); - } else if (leveltype == DTYPE_NEST) { - palette_update_hive(); - } else if (leveltype == DTYPE_CRYPT) { - palette_update_crypt(); - } -} - -bool IsDiabloAlive(bool playSFX) -{ - if (Quests[Q_DIABLO]._qactive == QUEST_DONE && !gbIsMultiplayer) { - if (playSFX) - PlaySFX(SfxID::DiabloDeath); - return false; - } - - return true; -} - -void PrintScreen(SDL_Keycode vkey) -{ - ReleaseKey(vkey); -} - -} // namespace devilution +/** + * @file diablo.cpp + * + * Implementation of the main game initialization functions. + */ +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif + +#include + +#include + +#include "DiabloUI/selstart.h" +#include "appfat.h" +#include "automap.h" +#include "capture.h" +#include "control/control.hpp" +#include "cursor.h" +#include "dead.h" +#ifdef _DEBUG +#include "debug.h" +#endif +#include "DiabloUI/diabloui.h" +#include "controls/control_mode.hpp" +#include "controls/keymapper.hpp" +#include "controls/plrctrls.h" +#include "controls/remap_keyboard.h" +#include "diablo.h" +#include "diablo_msg.hpp" +#include "discord/discord.h" +#include "doom.h" +#include "encrypt.h" +#include "engine/backbuffer_state.hpp" +#include "engine/clx_sprite.hpp" +#include "engine/demomode.h" +#include "engine/dx.h" +#include "engine/events.hpp" +#include "engine/load_cel.hpp" +#include "engine/load_file.hpp" +#include "engine/random.hpp" +#include "engine/render/clx_render.hpp" +#include "engine/sound.h" +#include "game_mode.hpp" +#include "gamemenu.h" +#include "gmenu.h" +#include "headless_mode.hpp" +#include "help.h" +#include "hwcursor.hpp" +#include "init.hpp" +#include "inv.h" +#include "levels/drlg_l1.h" +#include "levels/drlg_l2.h" +#include "levels/drlg_l3.h" +#include "levels/drlg_l4.h" +#include "levels/gendung.h" +#include "levels/setmaps.h" +#include "levels/themes.h" +#include "levels/town.h" +#include "levels/trigs.h" +#include "lighting.h" +#include "loadsave.h" +#include "lua/lua_event.hpp" +#include "lua/lua_global.hpp" +#include "menu.h" +#include "minitext.h" +#include "missiles.h" +#include "movie.h" +#include "multi.h" +#include "nthread.h" +#include "objects.h" +#include "options.h" +#include "panels/console.hpp" +#include "panels/info_box.hpp" +#include "panels/partypanel.hpp" +#include "panels/spell_book.hpp" +#include "panels/spell_list.hpp" +#include "pfile.h" +#include "plrmsg.h" +#include "qol/chatlog.h" +#include "qol/floatingnumbers.h" +#include "qol/itemlabels.h" +#include "qol/monhealthbar.h" +#include "qol/stash.h" +#include "qol/visual_store.h" +#include "qol/xpbar.h" +#include "quick_messages.hpp" +#include "restrict.h" +#include "stores.h" +#include "storm/storm_net.hpp" +#include "storm/storm_svid.h" +#include "tables/monstdat.h" +#include "tables/playerdat.hpp" +#include "towners.h" +#include "track.h" +#include "utils/console.h" +#include "utils/display.h" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/parse_int.hpp" +#include "utils/paths.h" +#include "utils/screen_reader.hpp" +#include "utils/sdl_compat.h" +#include "utils/sdl_thread.h" +#include "utils/status_macros.hpp" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +#ifndef USE_SDL1 +#include "controls/touch/gamepad.h" +#include "controls/touch/renderers.h" +#endif + +#ifdef __vita__ +#include "platform/vita/touch.h" +#endif + +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION +#include +#endif + +namespace devilution { + +uint32_t DungeonSeeds[NUMLEVELS]; +std::optional LevelSeeds[NUMLEVELS]; +Point MousePosition; +bool gbRunGameResult; +bool ReturnToMainMenu; +/** Enable updating of player character, set to false once Diablo dies */ +bool gbProcessPlayers; +bool gbLoadGame; +bool cineflag; +int PauseMode; +clicktype sgbMouseDown; +uint16_t gnTickDelay = 50; +char gszProductName[64] = "DevilutionX vUnknown"; + +#ifdef _DEBUG +bool DebugDisableNetworkTimeout = false; +std::vector DebugCmdsFromCommandLine; +#endif +GameLogicStep gGameLogicStep = GameLogicStep::None; + +/** This and the following mouse variables are for handling in-game click-and-hold actions */ +PlayerActionType LastPlayerAction = PlayerActionType::None; + +// Controller support: Actions to run after updating the cursor state. +// Defined in SourceX/controls/plctrls.cpp. +extern void plrctrls_after_check_curs_move(); +extern void plrctrls_every_frame(); +extern void plrctrls_after_game_logic(); + +namespace { + +char gszVersionNumber[64] = "internal version unknown"; + +bool gbGameLoopStartup; +bool forceSpawn; +bool forceDiablo; +int sgnTimeoutCurs; +bool gbShowIntro = true; +/** To know if these things have been done when we get to the diablo_deinit() function */ +bool was_archives_init = false; +/** To know if surfaces have been initialized or not */ +bool was_window_init = false; +bool was_ui_init = false; + +void StartGame(interface_mode uMsg) +{ + CalcViewportGeometry(); + cineflag = false; + InitCursor(); +#ifdef _DEBUG + LoadDebugGFX(); +#endif + assert(HeadlessMode || ghMainWnd); + music_stop(); + InitMonsterHealthBar(); + InitXPBar(); + ShowProgress(uMsg); + gmenu_init_menu(); + InitLevelCursor(); + sgnTimeoutCurs = CURSOR_NONE; + sgbMouseDown = CLICK_NONE; + LastPlayerAction = PlayerActionType::None; +} + +void FreeGame() +{ + FreeMonsterHealthBar(); + FreeXPBar(); + FreeControlPan(); + FreeInvGFX(); + FreeGMenu(); + FreeQuestText(); + FreeInfoBoxGfx(); + FreeStoreMem(); + + for (Player &player : Players) + ResetPlayerGFX(player); + + FreeCursor(); +#ifdef _DEBUG + FreeDebugGFX(); +#endif + FreeGameMem(); + stream_stop(); + music_stop(); +} + +bool ProcessInput() +{ + if (PauseMode == 2) { + return false; + } + + plrctrls_every_frame(); + + if (!gbIsMultiplayer && gmenu_is_active()) { + RedrawViewport(); + return false; + } + + if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { +#ifdef __vita__ + FinishSimulatedMouseClicks(MousePosition); +#endif + CheckCursMove(); + plrctrls_after_check_curs_move(); + RepeatPlayerAction(); + } + + return true; +} + +void LeftMouseCmd(bool bShift) +{ + bool bNear; + + assert(!GetMainPanel().contains(MousePosition)); + + if (leveltype == DTYPE_TOWN) { + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); + if (pcursitem != -1 && pcurs == CURSOR_HAND) + NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); + if (pcursmonst != -1) + NetSendCmdLocParam1(true, CMD_TALKXY, cursPosition, pcursmonst); + if (pcursitem == -1 && pcursmonst == -1 && PlayerUnderCursor == nullptr) { + LastPlayerAction = PlayerActionType::Walk; + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); + } + return; + } + + const Player &myPlayer = *MyPlayer; + bNear = myPlayer.position.tile.WalkingDistance(cursPosition) < 2; + if (pcursitem != -1 && pcurs == CURSOR_HAND && !bShift) { + NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); + } else if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled() && (!bShift || (bNear && ObjectUnderCursor->_oBreak == 1))) { + LastPlayerAction = PlayerActionType::OperateObject; + NetSendCmdLoc(MyPlayerId, true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, cursPosition); + } else if (myPlayer.UsesRangedWeapon()) { + if (bShift) { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_RATTACKXY, cursPosition); + } else if (pcursmonst != -1) { + if (CanTalkToMonst(Monsters[pcursmonst])) { + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else { + LastPlayerAction = PlayerActionType::AttackMonsterTarget; + NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); + } + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + LastPlayerAction = PlayerActionType::AttackPlayerTarget; + NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId()); + } + } else { + if (bShift) { + if (pcursmonst != -1) { + if (CanTalkToMonst(Monsters[pcursmonst])) { + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); + } + } else { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); + } + } else if (pcursmonst != -1) { + LastPlayerAction = PlayerActionType::AttackMonsterTarget; + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + LastPlayerAction = PlayerActionType::AttackPlayerTarget; + NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId()); + } + } + if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { + LastPlayerAction = PlayerActionType::Walk; + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); + } +} + +bool TryOpenDungeonWithMouse() +{ + if (leveltype != DTYPE_TOWN) + return false; + + const Item &holdItem = MyPlayer->HoldItem; + if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) + OpenHive(); + else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) + OpenGrave(); + else + return false; + + NewCursor(CURSOR_HAND); + return true; +} + +void LeftMouseDown(uint16_t modState) +{ + LastPlayerAction = PlayerActionType::None; + + if (gmenu_left_mouse(true)) + return; + + if (CheckMuteButton()) + return; + + if (sgnTimeoutCurs != CURSOR_NONE) + return; + + if (MyPlayerIsDead) { + CheckMainPanelButtonDead(); + return; + } + + if (PauseMode == 2) { + return; + } + if (DoomFlag) { + doom_close(); + return; + } + + if (SpellSelectFlag) { + SetSpell(); + return; + } + + if (IsPlayerInStore()) { + CheckStoreBtn(); + return; + } + + const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; + const bool isCtrlHeld = (modState & SDL_KMOD_CTRL) != 0; + + if (!GetMainPanel().contains(MousePosition)) { + if (!gmenu_is_active() && !TryIconCurs()) { + if (QuestLogIsOpen && GetLeftPanel().contains(MousePosition)) { + QuestlogESC(); + } else if (qtextflag) { + qtextflag = false; + stream_stop(); + } else if (CharFlag && GetLeftPanel().contains(MousePosition)) { + CheckChrBtns(); + } else if (invflag && GetRightPanel().contains(MousePosition)) { + if (!DropGoldFlag) + CheckInvItem(isShiftHeld, isCtrlHeld); + } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { + if (!IsWithdrawGoldOpen) + CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); + CheckStashButtonPress(MousePosition); + } else if (IsVisualStoreOpen && GetLeftPanel().contains(MousePosition)) { + if (!MyPlayer->HoldItem.isEmpty()) { + CheckVisualStorePaste(MousePosition); + } else { + CheckVisualStoreItem(MousePosition, isCtrlHeld, isShiftHeld); + } + CheckVisualStoreButtonPress(MousePosition); + } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { + CheckSBook(); + } else if (!MyPlayer->HoldItem.isEmpty()) { + if (!TryOpenDungeonWithMouse()) { + const Point currentPosition = MyPlayer->position.tile; + std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, cursPosition)); + if (itemTile) { + NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, MyPlayer->HoldItem); + NewCursor(CURSOR_HAND); + } + } + } else { + CheckLevelButton(); + if (!LevelButtonDown) + LeftMouseCmd(isShiftHeld); + } + } + } else { + if (!ChatFlag && !DropGoldFlag && !IsWithdrawGoldOpen && !gmenu_is_active()) + CheckInvScrn(isShiftHeld, isCtrlHeld); + CheckMainPanelButton(); + CheckStashButtonPress(MousePosition); + if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) + NewCursor(CURSOR_HAND); + } +} + +void LeftMouseUp(uint16_t modState) +{ + gmenu_left_mouse(false); + CheckMuteButtonUp(); + if (MainPanelButtonDown) + CheckMainPanelButtonUp(); + CheckStashButtonRelease(MousePosition); + CheckVisualStoreButtonRelease(MousePosition); + if (CharPanelButtonActive) { + const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; + ReleaseChrBtns(isShiftHeld); + } + if (LevelButtonDown) + CheckLevelButtonUp(); + if (IsPlayerInStore()) + ReleaseStoreBtn(); +} + +void RightMouseDown(bool isShiftHeld) +{ + LastPlayerAction = PlayerActionType::None; + + if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { + return; + } + + if (qtextflag) { + qtextflag = false; + stream_stop(); + return; + } + + if (DoomFlag) { + doom_close(); + return; + } + if (IsPlayerInStore()) + return; + if (SpellSelectFlag) { + SetSpell(); + return; + } + if (SpellbookFlag && GetRightPanel().contains(MousePosition)) + return; + if (TryIconCurs()) + return; + if (pcursinvitem != -1 && UseInvItem(pcursinvitem)) + return; + if (pcursstashitem != StashStruct::EmptyCell && UseStashItem(pcursstashitem)) + return; + if (DidRightClickPartyPortrait()) + return; + if (pcurs == CURSOR_HAND) { + CheckPlrSpell(isShiftHeld); + } else if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { + NewCursor(CURSOR_HAND); + } +} + +void ReleaseKey(SDL_Keycode vkey) +{ + remap_keyboard_key(&vkey); + if (sgnTimeoutCurs != CURSOR_NONE) + return; + KeymapperRelease(vkey); +} + +void ClosePanels() +{ + if (CanPanelsCoverView()) { + if (!IsLeftPanelOpen() && IsRightPanelOpen() && MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } else if (!IsRightPanelOpen() && IsLeftPanelOpen() && MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + CloseInventory(); + CloseCharPanel(); + SpellbookFlag = false; + QuestLogIsOpen = false; +} + +void PressKey(SDL_Keycode vkey, uint16_t modState) +{ + Options &options = GetOptions(); + remap_keyboard_key(&vkey); + + if (vkey == SDLK_UNKNOWN) + return; + + if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { + return; + } + + if (MyPlayerIsDead) { + if (vkey == SDLK_ESCAPE) { + if (!gbIsMultiplayer) { + if (gbValidSaveFile) + gamemenu_load_game(false); + else + gamemenu_exit_game(false); + } else { + NetSendCmd(true, CMD_RETOWN); + } + return; + } + if (sgnTimeoutCurs != CURSOR_NONE) { + return; + } + KeymapperPress(vkey); + if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { + if ((modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } else { + TypeChatMessage(); + } + } + if (vkey != SDLK_ESCAPE) { + return; + } + } + // Disallow player from accessing escape menu during the frames before the death message appears + if (vkey == SDLK_ESCAPE && MyPlayer->_pHitPoints > 0) { + if (!PressEscKey()) { + LastPlayerAction = PlayerActionType::None; + gamemenu_on(); + } + return; + } + + if (DropGoldFlag) { + control_drop_gold(vkey); + return; + } + if (IsWithdrawGoldOpen) { + WithdrawGoldKeyPress(vkey); + return; + } + + if (sgnTimeoutCurs != CURSOR_NONE) { + return; + } + + KeymapperPress(vkey); + + if (PauseMode == 2) { + if ((vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) && (modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } + return; + } + + if (DoomFlag) { + doom_close(); + return; + } + + switch (vkey) { + case SDLK_PLUS: + case SDLK_KP_PLUS: + case SDLK_EQUALS: + case SDLK_KP_EQUALS: + if (AutomapActive) { + AutomapZoomIn(); + } + return; + case SDLK_MINUS: + case SDLK_KP_MINUS: + case SDLK_UNDERSCORE: + if (AutomapActive) { + AutomapZoomOut(); + } + return; +#ifdef _DEBUG + case SDLK_V: + if ((modState & SDL_KMOD_SHIFT) != 0) + NextDebugMonster(); + else + GetDebugMonster(); + return; +#endif + case SDLK_RETURN: + case SDLK_KP_ENTER: + if ((modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } else if (IsPlayerInStore()) { + StoreEnter(); + } else if (QuestLogIsOpen) { + QuestlogEnter(); + } else { + TypeChatMessage(); + } + return; + case SDLK_UP: + if (IsPlayerInStore()) { + StoreUp(); + } else if (QuestLogIsOpen) { + QuestlogUp(); + } else if (HelpFlag) { + HelpScrollUp(); + } else if (ChatLogFlag) { + ChatLogScrollUp(); + } else if (AutomapActive) { + AutomapUp(); + } else if (IsStashOpen) { + Stash.PreviousPage(); + } + return; + case SDLK_DOWN: + if (IsPlayerInStore()) { + StoreDown(); + } else if (QuestLogIsOpen) { + QuestlogDown(); + } else if (HelpFlag) { + HelpScrollDown(); + } else if (ChatLogFlag) { + ChatLogScrollDown(); + } else if (AutomapActive) { + AutomapDown(); + } else if (IsStashOpen) { + Stash.NextPage(); + } + return; + case SDLK_PAGEUP: + if (IsPlayerInStore()) { + StorePrior(); + } else if (ChatLogFlag) { + ChatLogScrollTop(); + } + return; + case SDLK_PAGEDOWN: + if (IsPlayerInStore()) { + StoreNext(); + } else if (ChatLogFlag) { + ChatLogScrollBottom(); + } + return; + case SDLK_LEFT: + if (AutomapActive && !ChatFlag) + AutomapLeft(); + return; + case SDLK_RIGHT: + if (AutomapActive && !ChatFlag) + AutomapRight(); + return; + default: + break; + } +} + +void HandleMouseButtonDown(Uint8 button, uint16_t modState) +{ + if (IsPlayerInStore() && (button == SDL_BUTTON_X1 +#if !SDL_VERSION_ATLEAST(2, 0, 0) + || button == 8 +#endif + )) { + StoreESC(); + return; + } + + switch (button) { + case SDL_BUTTON_LEFT: + if (sgbMouseDown == CLICK_NONE) { + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(modState); + } + break; + case SDL_BUTTON_RIGHT: + if (sgbMouseDown == CLICK_NONE) { + sgbMouseDown = CLICK_RIGHT; + RightMouseDown((modState & SDL_KMOD_SHIFT) != 0); + } + break; + default: + KeymapperPress(static_cast(button | KeymapperMouseButtonMask)); + break; + } +} + +void HandleMouseButtonUp(Uint8 button, uint16_t modState) +{ + if (sgbMouseDown == CLICK_LEFT && button == SDL_BUTTON_LEFT) { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(modState); + } else if (sgbMouseDown == CLICK_RIGHT && button == SDL_BUTTON_RIGHT) { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + } else { + KeymapperRelease(static_cast(button | KeymapperMouseButtonMask)); + } +} + +[[maybe_unused]] void LogUnhandledEvent(const char *name, int value) +{ + LogVerbose("Unhandled SDL event: {} {}", name, value); +} + +void PrepareForFadeIn() +{ + if (HeadlessMode) return; + BlackPalette(); + + // Render the game to the buffer(s) with a fully black palette. + // Palette fade-in will gradually make it visible. + RedrawEverything(); + while (IsRedrawEverything()) { + DrawAndBlit(); + } +} + +void GameEventHandler(const SDL_Event &event, uint16_t modState) +{ + [[maybe_unused]] const Options &options = GetOptions(); + StaticVector ctrlEvents = ToControllerButtonEvents(event); + for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { + GameAction action; + if (HandleControllerButtonEvent(event, ctrlEvent, action) && action.type == GameActionType_SEND_KEY) { + if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { + const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; + if (!action.send_key.up) + HandleMouseButtonDown(static_cast(button), modState); + else + HandleMouseButtonUp(static_cast(button), modState); + } else { + if (!action.send_key.up) + PressKey(static_cast(action.send_key.vk_code), modState); + else + ReleaseKey(static_cast(action.send_key.vk_code)); + } + } + } + if (ctrlEvents.size() > 0 && ctrlEvents[0].button != ControllerButton_NONE) { + return; + } + +#ifdef _DEBUG + if (ConsoleHandleEvent(event)) { + return; + } +#endif + + if (IsChatActive() && HandleTalkTextInputEvent(event)) { + return; + } + if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { + return; + } + if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { + return; + } + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + PressKey(SDLC_EventKey(event), modState); + return; + case SDL_EVENT_KEY_UP: + ReleaseKey(SDLC_EventKey(event)); + return; + case SDL_EVENT_MOUSE_MOTION: + if (ControlMode == ControlTypes::KeyboardAndMouse && invflag) + InvalidateInventorySlot(); + MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; + gmenu_on_mouse_move(); + return; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; + HandleMouseButtonDown(event.button.button, modState); + return; + case SDL_EVENT_MOUSE_BUTTON_UP: + MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; + HandleMouseButtonUp(event.button.button, modState); + return; +#if SDL_VERSION_ATLEAST(2, 0, 0) + case SDL_EVENT_MOUSE_WHEEL: + if (SDLC_EventWheelIntY(event) > 0) { // Up + if (IsPlayerInStore()) { + StoreUp(); + } else if (QuestLogIsOpen) { + QuestlogUp(); + } else if (HelpFlag) { + HelpScrollUp(); + } else if (ChatLogFlag) { + ChatLogScrollUp(); + } else if (IsStashOpen) { + Stash.PreviousPage(); + } else if (SDL_GetModState() & SDL_KMOD_CTRL) { + if (AutomapActive) { + AutomapZoomIn(); + } + } else { + KeymapperPress(MouseScrollUpButton); + } + } else if (SDLC_EventWheelIntY(event) < 0) { // down + if (IsPlayerInStore()) { + StoreDown(); + } else if (QuestLogIsOpen) { + QuestlogDown(); + } else if (HelpFlag) { + HelpScrollDown(); + } else if (ChatLogFlag) { + ChatLogScrollDown(); + } else if (IsStashOpen) { + Stash.NextPage(); + } else if (SDL_GetModState() & SDL_KMOD_CTRL) { + if (AutomapActive) { + AutomapZoomOut(); + } + } else { + KeymapperPress(MouseScrollDownButton); + } + } else if (SDLC_EventWheelIntX(event) > 0) { // left + KeymapperPress(MouseScrollLeftButton); + } else if (SDLC_EventWheelIntX(event) < 0) { // right + KeymapperPress(MouseScrollRightButton); + } + break; +#endif + default: + if (IsCustomEvent(event.type)) { + if (gbIsMultiplayer) + pfile_write_hero(); + nthread_ignore_mutex(true); + PaletteFadeOut(8); + sound_stop(); + ShowProgress(GetCustomEvent(event)); + + PrepareForFadeIn(); + LoadPWaterPalette(); + if (gbRunGame) + PaletteFadeIn(8); + nthread_ignore_mutex(false); + gbGameLoopStartup = true; + return; + } + MainWndProc(event); + break; + } +} + +void RunGameLoop(interface_mode uMsg) +{ + demo::NotifyGameLoopStart(); + + nthread_ignore_mutex(true); + StartGame(uMsg); + assert(HeadlessMode || ghMainWnd); + EventHandler previousHandler = SetEventHandler(GameEventHandler); + run_delta_info(); + gbRunGame = true; + gbProcessPlayers = IsDiabloAlive(true); + gbRunGameResult = true; + + PrepareForFadeIn(); + LoadPWaterPalette(); + PaletteFadeIn(8); + InitBackbufferState(); + RedrawEverything(); + gbGameLoopStartup = true; + nthread_ignore_mutex(false); + + discord_manager::StartGame(); + lua::GameStart(); +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION + unsigned run_game_iteration = 0; +#endif + + while (gbRunGame) { + +#ifdef _DEBUG + if (!gbGameLoopStartup && !DebugCmdsFromCommandLine.empty()) { + InitConsole(); + for (const std::string &cmd : DebugCmdsFromCommandLine) { + RunInConsole(cmd); + } + DebugCmdsFromCommandLine.clear(); + } +#endif + + SDL_Event event; + uint16_t modState; + while (FetchMessage(&event, &modState)) { + if (event.type == SDL_EVENT_QUIT) { + gbRunGameResult = false; + gbRunGame = false; + break; + } + HandleMessage(event, modState); + } + if (!gbRunGame) + break; + + bool drawGame = true; + bool processInput = true; + const bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed(&drawGame); + if (demo::IsRecording()) + demo::RecordGameLoopResult(runGameLoop); + + discord_manager::UpdateGame(); + + if (!runGameLoop) { + if (processInput) + ProcessInput(); + DvlNet_ProcessNetworkPackets(); + if (!drawGame) + continue; + RedrawViewport(); + DrawAndBlit(); + continue; + } + + ProcessGameMessagePackets(); + if (game_loop(gbGameLoopStartup)) + diablo_color_cyc_logic(); + gbGameLoopStartup = false; + if (drawGame) + DrawAndBlit(); +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION + if (run_game_iteration++ == 0) + HeapProfilerDump("first_game_iteration"); +#endif + } + + demo::NotifyGameLoopEnd(); + + if (gbIsMultiplayer) { + pfile_write_hero(/*writeGameData=*/false); + sfile_write_stash(); + } + + PaletteFadeOut(8); + NewCursor(CURSOR_NONE); + ClearScreenBuffer(); + RedrawEverything(); + scrollrt_draw_game_screen(); + previousHandler = SetEventHandler(previousHandler); + assert(HeadlessMode || previousHandler == GameEventHandler); + FreeGame(); + + if (cineflag) { + cineflag = false; + DoEnding(); + } +} + +void PrintWithRightPadding(std::string_view str, size_t width) +{ + printInConsole(str); + if (str.size() >= width) + return; + printInConsole(std::string(width - str.size(), ' ')); +} + +void PrintHelpOption(std::string_view flags, std::string_view description) +{ + printInConsole(" "); + PrintWithRightPadding(flags, 20); + printInConsole(" "); + PrintWithRightPadding(description, 30); + printNewlineInConsole(); +} + +#if SDL_VERSION_ATLEAST(2, 0, 0) +FILE *SdlLogFile = nullptr; + +extern "C" void SdlLogToFile(void *userdata, int /*category*/, SDL_LogPriority priority, const char *message) +{ + FILE *file = reinterpret_cast(userdata); + static const char *const LogPriorityPrefixes[SDL_LOG_PRIORITY_COUNT] = { + "", + "VERBOSE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "CRITICAL" + }; + std::fprintf(file, "%s: %s\n", LogPriorityPrefixes[priority], message); + std::fflush(file); +} +#endif + +[[noreturn]] void PrintHelpAndExit() +{ + printInConsole((/* TRANSLATORS: Commandline Option */ "Options:")); + printNewlineInConsole(); + PrintHelpOption("-h, --help", _(/* TRANSLATORS: Commandline Option */ "Print this message and exit")); + PrintHelpOption("--version", _(/* TRANSLATORS: Commandline Option */ "Print the version and exit")); + PrintHelpOption("--data-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of diabdat.mpq")); + PrintHelpOption("--save-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of save files")); + PrintHelpOption("--config-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the location of diablo.ini")); + PrintHelpOption("--lang", _(/* TRANSLATORS: Commandline Option */ "Specify the language code (e.g. en or pt_BR)")); + PrintHelpOption("-n", _(/* TRANSLATORS: Commandline Option */ "Skip startup videos")); + PrintHelpOption("-f", _(/* TRANSLATORS: Commandline Option */ "Display frames per second")); + PrintHelpOption("--verbose", _(/* TRANSLATORS: Commandline Option */ "Enable verbose logging")); +#if SDL_VERSION_ATLEAST(2, 0, 0) + PrintHelpOption("--log-to-file ", _(/* TRANSLATORS: Commandline Option */ "Log to a file instead of stderr")); +#endif +#ifndef DISABLE_DEMOMODE + PrintHelpOption("--record <#>", _(/* TRANSLATORS: Commandline Option */ "Record a demo file")); + PrintHelpOption("--demo <#>", _(/* TRANSLATORS: Commandline Option */ "Play a demo file")); + PrintHelpOption("--timedemo", _(/* TRANSLATORS: Commandline Option */ "Disable all frame limiting during demo playback")); +#endif + printNewlineInConsole(); + printInConsole(_(/* TRANSLATORS: Commandline Option */ "Game selection:")); + printNewlineInConsole(); + PrintHelpOption("--spawn", _(/* TRANSLATORS: Commandline Option */ "Force Shareware mode")); + PrintHelpOption("--diablo", _(/* TRANSLATORS: Commandline Option */ "Force Diablo mode")); + PrintHelpOption("--hellfire", _(/* TRANSLATORS: Commandline Option */ "Force Hellfire mode")); + printInConsole(_(/* TRANSLATORS: Commandline Option */ "Hellfire options:")); + printNewlineInConsole(); +#ifdef _DEBUG + printNewlineInConsole(); + printInConsole("Debug options:"); + printNewlineInConsole(); + PrintHelpOption("-i", "Ignore network timeout"); + PrintHelpOption("+", "Pass commands to the engine"); +#endif + printNewlineInConsole(); + printInConsole(_("Report bugs at https://github.com/diasurgical/devilutionX/")); + printNewlineInConsole(); + diablo_quit(0); +} + +void PrintFlagMessage(std::string_view flag, std::string_view message) +{ + printInConsole(flag); + printInConsole(message); + printNewlineInConsole(); +} + +void PrintFlagRequiresArgument(std::string_view flag) +{ + PrintFlagMessage(flag, " requires an argument"); +} + +void DiabloParseFlags(int argc, char **argv) +{ +#ifdef _DEBUG + int argumentIndexOfLastCommandPart = -1; + std::string currentCommand; +#endif +#ifndef DISABLE_DEMOMODE + bool timedemo = false; + int demoNumber = -1; + int recordNumber = -1; + bool createDemoReference = false; +#endif + for (int i = 1; i < argc; i++) { + const std::string_view arg = argv[i]; + if (arg == "-h" || arg == "--help") { + PrintHelpAndExit(); + } else if (arg == "--version") { + printInConsole(PROJECT_NAME); + printInConsole(" v"); + printInConsole(PROJECT_VERSION); + printNewlineInConsole(); + diablo_quit(0); + } else if (arg == "--data-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--data-dir"); + diablo_quit(64); + } + paths::SetBasePath(argv[++i]); + } else if (arg == "--save-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--save-dir"); + diablo_quit(64); + } + paths::SetPrefPath(argv[++i]); + } else if (arg == "--config-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--config-dir"); + diablo_quit(64); + } + paths::SetConfigPath(argv[++i]); + } else if (arg == "--lang") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--lang"); + diablo_quit(64); + } + forceLocale = argv[++i]; +#ifndef DISABLE_DEMOMODE + } else if (arg == "--demo") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--demo"); + diablo_quit(64); + } + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.has_value()) { + PrintFlagMessage("--demo", " must be a number"); + diablo_quit(64); + } + demoNumber = parsedParam.value(); + gbShowIntro = false; + } else if (arg == "--timedemo") { + timedemo = true; + } else if (arg == "--record") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--record"); + diablo_quit(64); + } + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.has_value()) { + PrintFlagMessage("--record", " must be a number"); + diablo_quit(64); + } + recordNumber = parsedParam.value(); + } else if (arg == "--create-reference") { + createDemoReference = true; +#else + } else if (arg == "--demo" || arg == "--timedemo" || arg == "--record" || arg == "--create-reference") { + printInConsole("Binary compiled without demo mode support."); + printNewlineInConsole(); + diablo_quit(1); +#endif + } else if (arg == "-n") { + gbShowIntro = false; + } else if (arg == "-f") { + EnableFrameCount(); + } else if (arg == "--spawn") { + forceSpawn = true; + } else if (arg == "--diablo") { + forceDiablo = true; + } else if (arg == "--hellfire") { + forceHellfire = true; + } else if (arg == "--vanilla") { + gbVanilla = true; + } else if (arg == "--verbose") { + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); +#if SDL_VERSION_ATLEAST(2, 0, 0) + } else if (arg == "--log-to-file") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--log-to-file"); + diablo_quit(64); + } + SdlLogFile = OpenFile(argv[++i], "wb"); + if (SdlLogFile == nullptr) { + printInConsole("Failed to open log file for writing"); + diablo_quit(64); + } + SDL_SetLogOutputFunction(&SdlLogToFile, /*userdata=*/SdlLogFile); +#endif +#ifdef _DEBUG + } else if (arg == "-i") { + DebugDisableNetworkTimeout = true; + } else if (arg[0] == '+') { + if (!currentCommand.empty()) + DebugCmdsFromCommandLine.push_back(currentCommand); + argumentIndexOfLastCommandPart = i; + currentCommand = arg.substr(1); + } else if (arg[0] != '-' && (argumentIndexOfLastCommandPart + 1) == i) { + currentCommand.append(" "); + currentCommand.append(arg); + argumentIndexOfLastCommandPart = i; +#endif + } else { + printInConsole("unrecognized option '"); + printInConsole(argv[i]); + printInConsole("'"); + printNewlineInConsole(); + PrintHelpAndExit(); + } + } + +#ifdef _DEBUG + if (!currentCommand.empty()) + DebugCmdsFromCommandLine.push_back(currentCommand); +#endif + +#ifndef DISABLE_DEMOMODE + if (demoNumber != -1) + demo::InitPlayBack(demoNumber, timedemo); + if (recordNumber != -1) + demo::InitRecording(recordNumber, createDemoReference); +#endif +} + +void DiabloInitScreen() +{ + MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; + if (ControlMode == ControlTypes::KeyboardAndMouse) + SetCursorPos(MousePosition); + + ClrDiabloMsg(); +} + +void SetApplicationVersions() +{ + *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; + *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; +} + +void CheckArchivesUpToDate() +{ + const bool devilutionxMpqOutOfDate = IsDevilutionXMpqOutOfDate(); + const bool fontsMpqOutOfDate = AreExtraFontsOutOfDate(); + + if (devilutionxMpqOutOfDate && fontsMpqOutOfDate) { + app_fatal(_("Please update devilutionx.mpq and fonts.mpq to the latest version")); + } else if (devilutionxMpqOutOfDate) { + app_fatal(_("Failed to load UI resources.\n" + "\n" + "Make sure devilutionx.mpq is in the game folder and that it is up to date.")); + } else if (fontsMpqOutOfDate) { + app_fatal(_("Please update fonts.mpq to the latest version")); + } +} + +void ApplicationInit() +{ + if (*GetOptions().Graphics.showFPS) + EnableFrameCount(); + + init_create_window(); + was_window_init = true; + + InitializeScreenReader(); + LanguageInitialize(); + + SetApplicationVersions(); + + ReadOnlyTest(); +} + +void DiabloInit() +{ + if (forceSpawn || *GetOptions().GameMode.shareware) + gbIsSpawn = 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; + + for (size_t i = 0; i < QuickMessages.size(); i++) { + auto &messages = GetOptions().Chat.szHotKeyMsgs[i]; + if (messages.empty()) { + messages.emplace_back(_(QuickMessages[i].message)); + } + } + +#ifndef USE_SDL1 + InitializeVirtualGamepad(); +#endif + + UiInitialize(); + was_ui_init = true; + + if (wasHellfireDiscovered) { + UiSelStartUpGameOption(); + if (!gbIsHellfire) { + // Reinitialize the UI Elements because we changed the game + UnloadUiGFX(); + UiInitialize(); + if (IsHardwareCursor()) + SetHardwareCursor(CursorInfo::UnknownCursor()); + } + } + + DiabloInitScreen(); + + snd_init(); + + ui_sound_init(); + + // Item graphics are loaded early, they already get touched during hero selection. + InitItemGFX(); + + // Always available. + LoadSmallSelectionSpinner(); + + CheckArchivesUpToDate(); +} + +void DiabloSplash() +{ + if (!gbShowIntro) + return; + + if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) + play_movie("gendata\\logo.smk", true); + + auto &intro = gbIsHellfire ? GetOptions().StartUp.hellfireIntro : GetOptions().StartUp.diabloIntro; + + if (*intro != StartUpIntro::Off) { + if (gbIsHellfire) + play_movie("gendata\\Hellfire.smk", true); + else + play_movie("gendata\\diablo1.smk", true); + if (*intro == StartUpIntro::Once) { + intro.SetValue(StartUpIntro::Off); + if (!demo::IsRunning()) SaveOptions(); + } + } + + if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) + UiTitleDialog(); +} + +void DiabloDeinit() +{ + FreeItemGFX(); + + LuaShutdown(); + ShutDownScreenReader(); + + if (gbSndInited) + effects_cleanup_sfx(); + snd_deinit(); + if (was_ui_init) + UiDestroy(); + if (was_archives_init) + init_cleanup(); + if (was_window_init) + dx_cleanup(); // Cleanup SDL surfaces stuff, so we have to do it before SDL_Quit(). + UnloadFonts(); + if (SDL_WasInit((~0U) & ~SDL_INIT_HAPTIC) != 0) + SDL_Quit(); +} + +tl::expected LoadLvlGFX() +{ + assert(pDungeonCels == nullptr); + constexpr int SpecialCelWidth = 64; + + const auto loadAll = [](const char *cel, const char *til, const char *special) -> tl::expected { + ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus(cel)); + ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus(til)); + ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus(special, SpecialCelWidth)); + return {}; + }; + + switch (leveltype) { + case DTYPE_TOWN: { + auto cel = LoadFileInMemWithStatus("nlevels\\towndata\\town.cel"); + if (!cel.has_value()) { + ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus("levels\\towndata\\town.cel")); + } else { + pDungeonCels = std::move(*cel); + } + auto til = LoadFileInMemWithStatus("nlevels\\towndata\\town.til"); + if (!til.has_value()) { + ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus("levels\\towndata\\town.til")); + } else { + pMegaTiles = std::move(*til); + } + ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus("levels\\towndata\\towns", SpecialCelWidth)); + return {}; + } + case DTYPE_CATHEDRAL: + return loadAll( + "levels\\l1data\\l1.cel", + "levels\\l1data\\l1.til", + "levels\\l1data\\l1s"); + case DTYPE_CATACOMBS: + return loadAll( + "levels\\l2data\\l2.cel", + "levels\\l2data\\l2.til", + "levels\\l2data\\l2s"); + case DTYPE_CAVES: + return loadAll( + "levels\\l3data\\l3.cel", + "levels\\l3data\\l3.til", + "levels\\l1data\\l1s"); + case DTYPE_HELL: + return loadAll( + "levels\\l4data\\l4.cel", + "levels\\l4data\\l4.til", + "levels\\l2data\\l2s"); + case DTYPE_NEST: + return loadAll( + "nlevels\\l6data\\l6.cel", + "nlevels\\l6data\\l6.til", + "levels\\l1data\\l1s"); + case DTYPE_CRYPT: + return loadAll( + "nlevels\\l5data\\l5.cel", + "nlevels\\l5data\\l5.til", + "nlevels\\l5data\\l5s"); + default: + return tl::make_unexpected("LoadLvlGFX"); + } +} + +tl::expected LoadAllGFX() +{ + IncProgress(); +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + IncProgress(); + RETURN_IF_ERROR(InitObjectGFX()); + IncProgress(); + RETURN_IF_ERROR(InitMissileGFX()); + IncProgress(); + return {}; +} + +/** + * @param entry Where is the player entering from + */ +void CreateLevel(lvl_entry entry) +{ + CreateDungeon(DungeonSeeds[currlevel], entry); + + switch (leveltype) { + case DTYPE_TOWN: + InitTownTriggers(); + break; + case DTYPE_CATHEDRAL: + InitL1Triggers(); + break; + case DTYPE_CATACOMBS: + InitL2Triggers(); + break; + case DTYPE_CAVES: + InitL3Triggers(); + break; + case DTYPE_HELL: + InitL4Triggers(); + break; + case DTYPE_NEST: + InitHiveTriggers(); + break; + case DTYPE_CRYPT: + InitCryptTriggers(); + break; + default: + app_fatal("CreateLevel"); + } + + if (leveltype != DTYPE_TOWN) { + Freeupstairs(); + } + LoadRndLvlPal(leveltype); +} + +void UnstuckChargers() +{ + if (gbIsMultiplayer) { + for (Player &player : Players) { + if (!player.plractive) + continue; + if (player._pLvlChanging) + continue; + if (!player.isOnActiveLevel()) + continue; + if (&player == MyPlayer) + continue; + return; + } + } + for (size_t i = 0; i < ActiveMonsterCount; i++) { + Monster &monster = Monsters[ActiveMonsters[i]]; + if (monster.mode == MonsterMode::Charge) + monster.mode = MonsterMode::Stand; + } +} + +void UpdateMonsterLights() +{ + for (size_t i = 0; i < ActiveMonsterCount; i++) { + Monster &monster = Monsters[ActiveMonsters[i]]; + + if ((monster.flags & MFLAG_BERSERK) != 0) { + const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; + monster.lightId = AddLight(monster.position.tile, lightRadius); + } + + if (monster.lightId != NO_LIGHT) { + if (monster.lightId == MyPlayer->lightId) { // Fix old saves where some monsters had 0 instead of NO_LIGHT + monster.lightId = NO_LIGHT; + continue; + } + + const Light &light = Lights[monster.lightId]; + if (monster.position.tile != light.position.tile) { + ChangeLightXY(monster.lightId, monster.position.tile); + } + } + } +} + +void GameLogic() +{ + if (!ProcessInput()) { + return; + } + if (gbProcessPlayers) { + gGameLogicStep = GameLogicStep::ProcessPlayers; + ProcessPlayers(); + } + if (leveltype != DTYPE_TOWN) { + gGameLogicStep = GameLogicStep::ProcessMonsters; +#ifdef _DEBUG + if (!DebugInvisible) +#endif + ProcessMonsters(); + gGameLogicStep = GameLogicStep::ProcessObjects; + ProcessObjects(); + gGameLogicStep = GameLogicStep::ProcessMissiles; + ProcessMissiles(); + gGameLogicStep = GameLogicStep::ProcessItems; + ProcessItems(); + ProcessLightList(); + ProcessVisionList(); + } else { + gGameLogicStep = GameLogicStep::ProcessTowners; + ProcessTowners(); + gGameLogicStep = GameLogicStep::ProcessItemsTown; + ProcessItems(); + gGameLogicStep = GameLogicStep::ProcessMissilesTown; + ProcessMissiles(); + } + gGameLogicStep = GameLogicStep::None; + +#ifdef _DEBUG + if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { + ScrollView(); + } +#endif + + sound_update(); + CheckTriggers(); + CheckQuests(); + RedrawViewport(); + pfile_update(false); + + plrctrls_after_game_logic(); +} + +void TimeoutCursor(bool bTimeout) +{ + if (bTimeout) { + if (sgnTimeoutCurs == CURSOR_NONE && sgbMouseDown == CLICK_NONE) { + sgnTimeoutCurs = pcurs; + multi_net_ping(); + InfoString = StringOrView {}; + AddInfoBoxString(_("-- Network timeout --")); + AddInfoBoxString(_("-- Waiting for players --")); + for (uint8_t i = 0; i < Players.size(); i++) { + bool isConnected = (player_state[i] & PS_CONNECTED) != 0; + bool isActive = (player_state[i] & PS_ACTIVE) != 0; + if (!isConnected || isActive) continue; + + DvlNetLatencies latencies = DvlNet_GetLatencies(i); + + std::string ping = fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Player {:s} is timing out!")), + Players[i].name()); + + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); + + if (latencies.providerLatency) { + if (latencies.isRelayed && *latencies.isRelayed) { + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); + } else { + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); + } + } + EventPlrMsg(ping); + } + NewCursor(CURSOR_HOURGLASS); + RedrawEverything(); + } + scrollrt_draw_game_screen(); + } else if (sgnTimeoutCurs != CURSOR_NONE) { + // Timeout is gone, we should restore the previous cursor. + // But the timeout cursor could already be changed by the now processed messages (for example item cursor from CMD_GETITEM). + // Changing the item cursor back to the previous (hand) cursor could result in deleted items, because this resets Player.HoldItem (see NewCursor). + if (pcurs == CURSOR_HOURGLASS) + NewCursor(sgnTimeoutCurs); + sgnTimeoutCurs = CURSOR_NONE; + InfoString = StringOrView {}; + RedrawEverything(); + } +} + +void HelpKeyPressed() +{ + if (HelpFlag) { + HelpFlag = false; + } else if (IsPlayerInStore()) { + InfoString = StringOrView {}; + AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed + AddInfoBoxString(_("while in stores")); + LastPlayerAction = PlayerActionType::None; + } else { + CloseInventory(); + CloseCharPanel(); + SpellbookFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + QuestLogIsOpen = false; + CancelCurrentDiabloMsg(); + gamemenu_off(); + DisplayHelp(); + doom_close(); + } +} + +void InventoryKeyPressed() +{ + if (IsPlayerInStore()) + return; + invflag = !invflag; + if (!IsLeftPanelOpen() && CanPanelsCoverView()) { + if (!invflag) { // We closed the inventory + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } else if (!SpellbookFlag) { // We opened the inventory + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + } + SpellbookFlag = false; + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); +} + +void CharacterSheetKeyPressed() +{ + if (IsPlayerInStore()) + return; + if (!IsRightPanelOpen() && CanPanelsCoverView()) { + if (CharFlag) { // We are closing the character sheet + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } else if (!QuestLogIsOpen) { // We opened the character sheet + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } + } + ToggleCharPanel(); +} + +void PartyPanelSideToggleKeyPressed() +{ + PartySidePanelOpen = !PartySidePanelOpen; +} + +void QuestLogKeyPressed() +{ + if (IsPlayerInStore()) + return; + if (!QuestLogIsOpen) { + StartQuestlog(); + } else { + QuestLogIsOpen = false; + } + if (!IsRightPanelOpen() && CanPanelsCoverView()) { + if (!QuestLogIsOpen) { // We closed the quest log + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } else if (!CharFlag) { // We opened the character quest log + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } + } + CloseCharPanel(); + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); +} + +void DisplaySpellsKeyPressed() +{ + if (IsPlayerInStore()) + return; + CloseCharPanel(); + QuestLogIsOpen = false; + CloseInventory(); + SpellbookFlag = false; + if (!SpellSelectFlag) { + DoSpeedBook(); + } else { + SpellSelectFlag = false; + } + LastPlayerAction = PlayerActionType::None; +} + +void SpellBookKeyPressed() +{ + if (IsPlayerInStore()) + return; + SpellbookFlag = !SpellbookFlag; + if (!IsLeftPanelOpen() && CanPanelsCoverView()) { + if (!SpellbookFlag) { // We closed the inventory + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } else if (!invflag) { // We opened the inventory + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + } + CloseInventory(); + CloseVisualStore(); +} + +void CycleSpellHotkeys(bool next) +{ + StaticVector validHotKeyIndexes; + std::optional currentIndex; + for (size_t slot = 0; slot < NumHotkeys; slot++) { + if (!IsValidSpeedSpell(slot)) + continue; + if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) { + // found current + currentIndex = validHotKeyIndexes.size(); + } + validHotKeyIndexes.emplace_back(slot); + } + if (validHotKeyIndexes.size() == 0) + return; + + size_t newIndex; + if (!currentIndex) { + newIndex = next ? 0 : (validHotKeyIndexes.size() - 1); + } else if (next) { + newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1); + } else { + newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1); + } + ToggleSpell(validHotKeyIndexes[newIndex]); +} + +bool IsPlayerDead() +{ + return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; +} + +bool IsGameRunning() +{ + return PauseMode != 2; +} + +bool CanPlayerTakeAction() +{ + return !IsPlayerDead() && IsGameRunning(); +} + +bool CanAutomapBeToggledOff() +{ + // check if every window is closed - if yes, automap can be toggled off + return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !IsVisualStoreOpen && !CharFlag + && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag + && !ChatLogFlag && !HelpFlag; +} + +void OptionLanguageCodeChanged() +{ + UnloadFonts(); + LanguageInitialize(); + LoadLanguageArchive(); + effects_cleanup_sfx(false); + if (gbRunGame) + sound_init(); + else + ui_sound_init(); +} + +const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); + +} // namespace + +void InitKeymapActions() +{ + Options &options = GetOptions(); + for (uint32_t i = 0; i < 8; ++i) { + options.Keymapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), + '1' + i, + [i] { + const Player &myPlayer = *MyPlayer; + if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { + UseInvItem(INVITEM_BELT_FIRST + i); + } + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + for (uint32_t i = 0; i < NumHotkeys; ++i) { + options.Keymapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), + i < 4 ? static_cast(SDLK_F5) + i : static_cast(SDLK_UNKNOWN), + [i]() { + if (SpellSelectFlag) { + SetSpeedSpell(i); + return; + } + if (!*GetOptions().Gameplay.quickCast) + ToggleSpell(i); + else + QuickCast(i); + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + options.Keymapper.AddAction( + "QuickSpellPrevious", + N_("Previous quick spell"), + N_("Selects the previous quick spell (cycles)."), + MouseScrollUpButton, + [] { CycleSpellHotkeys(false); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuickSpellNext", + N_("Next quick spell"), + N_("Selects the next quick spell (cycles)."), + MouseScrollDownButton, + [] { CycleSpellHotkeys(true); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "UseHealthPotion", + N_("Use health potion"), + N_("Use health potions from belt."), + SDLK_UNKNOWN, + [] { UseBeltItem(BeltItemType::Healing); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "UseManaPotion", + N_("Use mana potion"), + N_("Use mana potions from belt."), + SDLK_UNKNOWN, + [] { UseBeltItem(BeltItemType::Mana); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), + 'S', + DisplaySpellsKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuickSave", + N_("Quick save"), + N_("Saves the game."), + SDLK_F2, + [] { gamemenu_save_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); + options.Keymapper.AddAction( + "QuickLoad", + N_("Quick load"), + N_("Loads the game."), + SDLK_F3, + [] { gamemenu_load_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); +#ifndef NOEXIT + options.Keymapper.AddAction( + "QuitGame", + N_("Quit game"), + N_("Closes the game."), + SDLK_UNKNOWN, + [] { gamemenu_quit_game(false); }); +#endif + options.Keymapper.AddAction( + "StopHero", + N_("Stop hero"), + N_("Stops walking and cancel pending actions."), + SDLK_UNKNOWN, + [] { MyPlayer->Stop(); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "ItemHighlighting", + N_("Item highlighting"), + N_("Show/hide items on ground."), + SDLK_LALT, + [] { HighlightKeyPressed(true); }, + [] { HighlightKeyPressed(false); }); + options.Keymapper.AddAction( + "ToggleItemHighlighting", + N_("Toggle item highlighting"), + N_("Permanent show/hide items on ground."), + SDLK_RCTRL, + nullptr, + [] { ToggleItemLabelHighlight(); }); + options.Keymapper.AddAction( + "ToggleAutomap", + N_("Toggle automap"), + N_("Toggles if automap is displayed."), + SDLK_TAB, + DoAutoMap, + nullptr, + IsGameRunning); + options.Keymapper.AddAction( + "CycleAutomapType", + N_("Cycle map type"), + N_("Opaque -> Transparent -> Minimap -> None"), + SDLK_M, + CycleAutomapType, + nullptr, + IsGameRunning); + + options.Keymapper.AddAction( + "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), + 'I', + InventoryKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Character", + N_("Character"), + N_("Open Character screen."), + 'C', + CharacterSheetKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Party", + N_("Party"), + N_("Open side Party panel."), + 'Y', + PartyPanelSideToggleKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuestLog", + N_("Quest log"), + N_("Open Quest log."), + 'Q', + QuestLogKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), + 'B', + SpellBookKeyPressed, + nullptr, + CanPlayerTakeAction); + for (uint32_t i = 0; i < QuickMessages.size(); ++i) { + options.Keymapper.AddAction( + "QuickMessage{}", + N_("Quick Message {}"), + N_("Use Quick Message in chat."), + (i < 4) ? static_cast(SDLK_F9) + i : static_cast(SDLK_UNKNOWN), + [i]() { DiabloHotkeyMsg(i); }, + nullptr, + nullptr, + i + 1); + } + options.Keymapper.AddAction( + "HideInfoScreens", + N_("Hide Info Screens"), + N_("Hide all info screens."), + SDLK_SPACE, + [] { + if (CanAutomapBeToggledOff()) + AutomapActive = false; + + ClosePanels(); + HelpFlag = false; + ChatLogFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + + CancelCurrentDiabloMsg(); + gamemenu_off(); + doom_close(); + }, + nullptr, + IsGameRunning); + options.Keymapper.AddAction( + "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), + 'Z', + [] { + GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "PauseGame", + N_("Pause Game"), + N_("Pauses the game."), + 'P', + diablo_pause_game); + options.Keymapper.AddAction( + "PauseGameAlternate", + N_("Pause Game (Alternate)"), + N_("Pauses the game."), + SDLK_PAUSE, + diablo_pause_game); + options.Keymapper.AddAction( + "DecreaseBrightness", + N_("Decrease Brightness"), + N_("Reduce screen brightness."), + 'F', + DecreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "IncreaseBrightness", + N_("Increase Brightness"), + N_("Increase screen brightness."), + 'G', + IncreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Help", + N_("Help"), + N_("Open Help Screen."), + SDLK_F1, + HelpKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Screenshot", + N_("Screenshot"), + N_("Takes a screenshot."), + SDLK_PRINTSCREEN, + nullptr, + CaptureScreen); + options.Keymapper.AddAction( + "GameInfo", + N_("Game info"), + N_("Displays game infos."), + 'V', + [] { + EventPlrMsg(fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), + PROJECT_NAME, + PROJECT_VERSION), + UiFlags::ColorWhite); + }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "ChatLog", + N_("Chat Log"), + N_("Displays chat log."), + 'L', + [] { + ToggleChatLog(); + }); + options.Keymapper.AddAction( + "SortInv", + N_("Sort Inventory"), + N_("Sorts the inventory."), + 'R', + [] { + ReorganizeInventory(*MyPlayer); + }); +#ifdef _DEBUG + options.Keymapper.AddAction( + "OpenConsole", + N_("Console"), + N_("Opens Lua console."), + SDLK_GRAVE, + OpenConsole); + options.Keymapper.AddAction( + "DebugToggle", + "Debug toggle", + "Programming is like magic.", + 'X', + [] { + DebugToggle = !DebugToggle; + }); +#endif + options.Keymapper.CommitActions(); +} + +void InitPadmapActions() +{ + Options &options = GetOptions(); + for (int i = 0; i < 8; ++i) { + options.Padmapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), + ControllerButton_NONE, + [i] { + const Player &myPlayer = *MyPlayer; + if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { + UseInvItem(INVITEM_BELT_FIRST + i); + } + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + for (uint32_t i = 0; i < NumHotkeys; ++i) { + options.Padmapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), + ControllerButton_NONE, + [i]() { + if (SpellSelectFlag) { + SetSpeedSpell(i); + return; + } + if (!*GetOptions().Gameplay.quickCast) + ToggleSpell(i); + else + QuickCast(i); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }, + i + 1); + } + options.Padmapper.AddAction( + "PrimaryAction", + N_("Primary action"), + N_("Attack monsters, talk to towners, lift and place inventory items."), + ControllerButton_BUTTON_B, + [] { + ControllerActionHeld = GameActionType_PRIMARY_ACTION; + LastPlayerAction = PlayerActionType::None; + PerformPrimaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SecondaryAction", + N_("Secondary action"), + N_("Open chests, interact with doors, pick up items."), + ControllerButton_BUTTON_Y, + [] { + ControllerActionHeld = GameActionType_SECONDARY_ACTION; + LastPlayerAction = PlayerActionType::None; + PerformSecondaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SpellAction", + N_("Spell action"), + N_("Cast the active spell."), + ControllerButton_BUTTON_X, + [] { + ControllerActionHeld = GameActionType_CAST_SPELL; + LastPlayerAction = PlayerActionType::None; + PerformSpellAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "CancelAction", + N_("Cancel action"), + N_("Close menus."), + ControllerButton_BUTTON_A, + [] { + if (DoomFlag) { + doom_close(); + return; + } + + GameAction action; + if (SpellSelectFlag) + action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); + else if (invflag) + action = GameAction(GameActionType_TOGGLE_INVENTORY); + else if (SpellbookFlag) + action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); + else if (QuestLogIsOpen) + action = GameAction(GameActionType_TOGGLE_QUEST_LOG); + else if (CharFlag) + action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); + ProcessGameAction(action); + }, + nullptr, + [] { return DoomFlag || SpellSelectFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag; }); + options.Padmapper.AddAction( + "MoveUp", + N_("Move up"), + N_("Moves the player character up."), + ControllerButton_BUTTON_DPAD_UP, + [] {}); + options.Padmapper.AddAction( + "MoveDown", + N_("Move down"), + N_("Moves the player character down."), + ControllerButton_BUTTON_DPAD_DOWN, + [] {}); + options.Padmapper.AddAction( + "MoveLeft", + N_("Move left"), + N_("Moves the player character left."), + ControllerButton_BUTTON_DPAD_LEFT, + [] {}); + options.Padmapper.AddAction( + "MoveRight", + N_("Move right"), + N_("Moves the player character right."), + ControllerButton_BUTTON_DPAD_RIGHT, + [] {}); + options.Padmapper.AddAction( + "StandGround", + N_("Stand ground"), + N_("Hold to prevent the player from moving."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "ToggleStandGround", + N_("Toggle stand ground"), + N_("Toggle whether the player moves."), + ControllerButton_NONE, + [] { StandToggle = !StandToggle; }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "UseHealthPotion", + N_("Use health potion"), + N_("Use health potions from belt."), + ControllerButton_BUTTON_LEFTSHOULDER, + [] { UseBeltItem(BeltItemType::Healing); }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "UseManaPotion", + N_("Use mana potion"), + N_("Use mana potions from belt."), + ControllerButton_BUTTON_RIGHTSHOULDER, + [] { UseBeltItem(BeltItemType::Mana); }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Character", + N_("Character"), + N_("Open Character screen."), + ControllerButton_AXIS_TRIGGERLEFT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), + ControllerButton_AXIS_TRIGGERRIGHT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "QuestLog", + N_("Quest log"), + N_("Open Quest log."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), + ControllerButton_BUTTON_A, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "ToggleAutomap", + N_("Toggle automap"), + N_("Toggles if automap is displayed."), + ControllerButton_BUTTON_LEFTSTICK, + DoAutoMap); + options.Padmapper.AddAction( + "AutomapMoveUp", + N_("Automap Move Up"), + N_("Moves the automap up when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveDown", + N_("Automap Move Down"), + N_("Moves the automap down when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveLeft", + N_("Automap Move Left"), + N_("Moves the automap left when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveRight", + N_("Automap Move Right"), + N_("Moves the automap right when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "MouseUp", + N_("Move mouse up"), + N_("Simulates upward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, + [] {}); + options.Padmapper.AddAction( + "MouseDown", + N_("Move mouse down"), + N_("Simulates downward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, + [] {}); + options.Padmapper.AddAction( + "MouseLeft", + N_("Move mouse left"), + N_("Simulates leftward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, + [] {}); + options.Padmapper.AddAction( + "MouseRight", + N_("Move mouse right"), + N_("Simulates rightward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, + [] {}); + auto leftMouseDown = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); + }; + auto leftMouseUp = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); + }; + options.Padmapper.AddAction( + "LeftMouseClick1", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + ControllerButton_BUTTON_RIGHTSTICK, + leftMouseDown, + leftMouseUp); + options.Padmapper.AddAction( + "LeftMouseClick2", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, + leftMouseDown, + leftMouseUp); + auto rightMouseDown = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_RIGHT; + RightMouseDown(standGround); + }; + auto rightMouseUp = [] { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + }; + options.Padmapper.AddAction( + "RightMouseClick1", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, + rightMouseDown, + rightMouseUp); + options.Padmapper.AddAction( + "RightMouseClick2", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, + rightMouseDown, + rightMouseUp); + options.Padmapper.AddAction( + "PadHotspellMenu", + N_("Gamepad hotspell menu"), + N_("Hold to set or use spell hotkeys."), + ControllerButton_BUTTON_BACK, + [] { PadHotspellMenuActive = true; }, + [] { PadHotspellMenuActive = false; }); + options.Padmapper.AddAction( + "PadMenuNavigator", + N_("Gamepad menu navigator"), + N_("Hold to access gamepad menu navigation."), + ControllerButton_BUTTON_START, + [] { PadMenuNavigatorActive = true; }, + [] { PadMenuNavigatorActive = false; }); + auto toggleGameMenu = [] { + const bool inMenu = gmenu_is_active(); + PressEscKey(); + LastPlayerAction = PlayerActionType::None; + PadHotspellMenuActive = false; + PadMenuNavigatorActive = false; + if (!inMenu) + gamemenu_on(); + }; + options.Padmapper.AddAction( + "ToggleGameMenu1", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_BACK, + ControllerButton_BUTTON_START, + }, + toggleGameMenu); + options.Padmapper.AddAction( + "ToggleGameMenu2", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_START, + ControllerButton_BUTTON_BACK, + }, + toggleGameMenu); + options.Padmapper.AddAction( + "QuickSave", + N_("Quick save"), + N_("Saves the game."), + ControllerButton_NONE, + [] { gamemenu_save_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); + options.Padmapper.AddAction( + "QuickLoad", + N_("Quick load"), + N_("Loads the game."), + ControllerButton_NONE, + [] { gamemenu_load_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); + options.Padmapper.AddAction( + "ItemHighlighting", + N_("Item highlighting"), + N_("Show/hide items on ground."), + ControllerButton_NONE, + [] { HighlightKeyPressed(true); }, + [] { HighlightKeyPressed(false); }); + options.Padmapper.AddAction( + "ToggleItemHighlighting", + N_("Toggle item highlighting"), + N_("Permanent show/hide items on ground."), + ControllerButton_NONE, + nullptr, + [] { ToggleItemLabelHighlight(); }); + options.Padmapper.AddAction( + "HideInfoScreens", + N_("Hide Info Screens"), + N_("Hide all info screens."), + ControllerButton_NONE, + [] { + if (CanAutomapBeToggledOff()) + AutomapActive = false; + + ClosePanels(); + HelpFlag = false; + ChatLogFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + + CancelCurrentDiabloMsg(); + gamemenu_off(); + doom_close(); + }, + nullptr, + IsGameRunning); + options.Padmapper.AddAction( + "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), + ControllerButton_NONE, + [] { + GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "PauseGame", + N_("Pause Game"), + N_("Pauses the game."), + ControllerButton_NONE, + diablo_pause_game); + options.Padmapper.AddAction( + "DecreaseBrightness", + N_("Decrease Brightness"), + N_("Reduce screen brightness."), + ControllerButton_NONE, + DecreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "IncreaseBrightness", + N_("Increase Brightness"), + N_("Increase screen brightness."), + ControllerButton_NONE, + IncreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Help", + N_("Help"), + N_("Open Help Screen."), + ControllerButton_NONE, + HelpKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Screenshot", + N_("Screenshot"), + N_("Takes a screenshot."), + ControllerButton_NONE, + nullptr, + CaptureScreen); + options.Padmapper.AddAction( + "GameInfo", + N_("Game info"), + N_("Displays game infos."), + ControllerButton_NONE, + [] { + EventPlrMsg(fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), + PROJECT_NAME, + PROJECT_VERSION), + UiFlags::ColorWhite); + }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SortInv", + N_("Sort Inventory"), + N_("Sorts the inventory."), + ControllerButton_NONE, + [] { + ReorganizeInventory(*MyPlayer); + }); + options.Padmapper.AddAction( + "ChatLog", + N_("Chat Log"), + N_("Displays chat log."), + ControllerButton_NONE, + [] { + ToggleChatLog(); + }); + options.Padmapper.CommitActions(); +} + +void SetCursorPos(Point position) +{ + if (ControlDevice != ControlTypes::KeyboardAndMouse) { + MousePosition = position; + return; + } + + LogicalToOutput(&position.x, &position.y); + if (!demo::IsRunning()) + SDL_WarpMouseInWindow(ghMainWnd, position.x, position.y); +} + +void FreeGameMem() +{ + pDungeonCels = nullptr; + pMegaTiles = nullptr; + pSpecialCels = std::nullopt; + + FreeMonsters(); + FreeMissileGFX(); + FreeObjectGFX(); + FreeTownerGFX(); + FreeStashGFX(); + FreeVisualStoreGFX(); +#ifndef USE_SDL1 + DeactivateVirtualGamepad(); + FreeVirtualGamepadGFX(); +#endif +} + +bool StartGame(bool bNewGame, bool bSinglePlayer) +{ + gbSelectProvider = true; + ReturnToMainMenu = false; + + do { + gbLoadGame = false; + + if (!NetInit(bSinglePlayer)) { + gbRunGameResult = true; + break; + } + + // Save 2.8 MiB of RAM by freeing all main menu resources + // before starting the game. + UiDestroy(); + + gbSelectProvider = false; + + if (bNewGame || !gbValidSaveFile) { + InitLevels(); + InitQuests(); + InitPortals(); + InitDungMsgs(*MyPlayer); + DeltaSyncJunk(); + } + giNumberOfLevels = gbIsHellfire ? 25 : 17; + interface_mode uMsg = WM_DIABNEWGAME; + if (gbValidSaveFile && gbLoadGame) { + uMsg = WM_DIABLOADGAME; + } + RunGameLoop(uMsg); + NetClose(); + UnloadFonts(); + + // If the player left the game into the main menu, + // initialize main menu resources. + if (gbRunGameResult) + UiInitialize(); + if (ReturnToMainMenu) + return true; + } while (gbRunGameResult); + + SNetDestroy(); + return gbRunGameResult; +} + +void diablo_quit(int exitStatus) +{ + FreeGameMem(); + music_stop(); + DiabloDeinit(); + +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (SdlLogFile != nullptr) std::fclose(SdlLogFile); +#endif + + exit(exitStatus); +} + +#ifdef __UWP__ +void (*onInitialized)() = NULL; + +void setOnInitialized(void (*callback)()) +{ + onInitialized = callback; +} +#endif + +int DiabloMain(int argc, char **argv) +{ +#ifdef _DEBUG + SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); +#endif + + DiabloParseFlags(argc, argv); + InitKeymapActions(); + InitPadmapActions(); + + // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings + LoadCoreArchives(); + was_archives_init = true; + + // Read settings including translation next. This will use the presence of fonts.mpq and look for assets in devilutionx.mpq + LoadOptions(); + if (demo::IsRunning()) demo::OverrideOptions(); + + // Then look for a voice pack file based on the selected translation + LoadLanguageArchive(); + + ApplicationInit(); + LuaInitialize(); + if (!demo::IsRunning()) SaveOptions(); + + // Finally load game data + LoadGameArchives(); + + LoadTextData(); + + // Load dynamic data before we go into the menu as we need to initialise player characters in memory pretty early. + LoadPlayerDataFiles(); + + // TODO: We can probably load this much later (when the game is starting). + LoadSpellData(); + LoadMissileData(); + LoadMonsterData(); + LoadItemData(); + LoadObjectData(); + LoadQuestData(); + + DiabloInit(); +#ifdef __UWP__ + onInitialized(); +#endif + if (!demo::IsRunning()) SaveOptions(); + + DiabloSplash(); + mainmenu_loop(); + DiabloDeinit(); + + return 0; +} + +bool TryIconCurs() +{ + if (pcurs == CURSOR_RESURRECT) { + if (PlayerUnderCursor != nullptr) { + NetSendCmdParam1(true, CMD_RESURRECT, PlayerUnderCursor->getId()); + NewCursor(CURSOR_HAND); + return true; + } + + return false; + } + + if (pcurs == CURSOR_HEALOTHER) { + if (PlayerUnderCursor != nullptr) { + NetSendCmdParam1(true, CMD_HEALOTHER, PlayerUnderCursor->getId()); + NewCursor(CURSOR_HAND); + return true; + } + + return false; + } + + if (pcurs == CURSOR_TELEKINESIS) { + DoTelekinesis(); + return true; + } + + Player &myPlayer = *MyPlayer; + + if (pcurs == CURSOR_IDENTIFY) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) + CheckIdentify(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + item._iIdentified = true; + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_REPAIR) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) { + if (IsVisualStoreOpen) + VisualStoreRepairItem(pcursinvitem); + else + DoRepair(myPlayer, pcursinvitem); + } else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + RepairItem(item, myPlayer.getCharacterLevel()); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_RECHARGE) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) + DoRecharge(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + RechargeItem(item, myPlayer); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_OIL) { + bool changeCursor = true; + if (pcursinvitem != -1 && !IsInspectingPlayer()) + changeCursor = DoOil(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + changeCursor = ApplyOilToItem(item, myPlayer); + } + if (changeCursor) + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_TELEPORT) { + const SpellID spellID = myPlayer.inventorySpell; + const SpellType spellType = SpellType::Scroll; + const int spellFrom = myPlayer.spellFrom; + if (IsWallSpell(spellID)) { + const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); + NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); + } else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) { + NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); + } else { + NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast(spellID), static_cast(spellType), spellFrom); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_DISARM && ObjectUnderCursor == nullptr) { + NewCursor(CURSOR_HAND); + return true; + } + + return false; +} + +void diablo_pause_game() +{ + if (!gbIsMultiplayer) { + if (PauseMode != 0) { + PauseMode = 0; + } else { + PauseMode = 2; + sound_stop(); + qtextflag = false; + LastPlayerAction = PlayerActionType::None; + } + + RedrawEverything(); + } +} + +bool GameWasAlreadyPaused = false; +bool MinimizePaused = false; + +bool diablo_is_focused() +{ +#ifndef USE_SDL1 + return SDL_GetKeyboardFocus() == ghMainWnd; +#else + Uint8 appState = SDL_GetAppState(); + return (appState & SDL_APPINPUTFOCUS) != 0; +#endif +} + +void diablo_focus_pause() +{ + if (!movie_playing && (gbIsMultiplayer || MinimizePaused)) { + return; + } + + GameWasAlreadyPaused = PauseMode != 0; + + if (!GameWasAlreadyPaused) { + PauseMode = 2; + sound_stop(); + LastPlayerAction = PlayerActionType::None; + } + + SVidMute(); + music_mute(); + + MinimizePaused = true; +} + +void diablo_focus_unpause() +{ + if (!GameWasAlreadyPaused) { + PauseMode = 0; + } + + SVidUnmute(); + music_unmute(); + + MinimizePaused = false; +} + +bool PressEscKey() +{ + bool rv = false; + + if (DoomFlag) { + doom_close(); + rv = true; + } + + if (HelpFlag) { + HelpFlag = false; + rv = true; + } + + if (ChatLogFlag) { + ChatLogFlag = false; + rv = true; + } + + if (qtextflag) { + qtextflag = false; + stream_stop(); + rv = true; + } + + if (IsPlayerInStore()) { + StoreESC(); + rv = true; + } + + if (IsDiabloMsgAvailable()) { + CancelCurrentDiabloMsg(); + rv = true; + } + + if (ChatFlag) { + ResetChat(); + rv = true; + } + + if (DropGoldFlag) { + control_drop_gold(SDLK_ESCAPE); + rv = true; + } + + if (IsWithdrawGoldOpen) { + WithdrawGoldKeyPress(SDLK_ESCAPE); + rv = true; + } + + if (SpellSelectFlag) { + SpellSelectFlag = false; + rv = true; + } + + if (IsLeftPanelOpen() || IsRightPanelOpen()) { + ClosePanels(); + rv = true; + } + + return rv; +} + +void DisableInputEventHandler(const SDL_Event &event, uint16_t /*modState*/) +{ + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; + return; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + if (sgbMouseDown != CLICK_NONE) + return; + switch (event.button.button) { + case SDL_BUTTON_LEFT: + sgbMouseDown = CLICK_LEFT; + return; + case SDL_BUTTON_RIGHT: + sgbMouseDown = CLICK_RIGHT; + return; + default: + return; + } + case SDL_EVENT_MOUSE_BUTTON_UP: + sgbMouseDown = CLICK_NONE; + return; + } + + MainWndProc(event); +} + +void LoadGameLevelStopMusic(_music_id neededTrack) +{ + if (neededTrack != sgnMusicTrack) + music_stop(); +} + +void LoadGameLevelStartMusic(_music_id neededTrack) +{ + if (sgnMusicTrack != neededTrack) + music_start(neededTrack); + + if (MinimizePaused) { + music_mute(); + } +} + +void LoadGameLevelResetCursor() +{ + if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { + NewCursor(CURSOR_HAND); + } +} + +void SetRndSeedForDungeonLevel() +{ + if (setlevel) { + // Maps are not randomly generated, but the monsters max hitpoints are. + // So we need to ensure that we have a stable seed when generating quest/set-maps. + // For this purpose we reuse the normal dungeon seeds. + SetRndSeed(DungeonSeeds[static_cast(setlvlnum)]); + } else { + SetRndSeed(DungeonSeeds[currlevel]); + } +} + +void LoadGameLevelFirstFlagEntry() +{ + CloseInventory(); + qtextflag = false; + if (!HeadlessMode) { + InitInv(); + ClearUniqueItemFlags(); + InitQuestText(); + InitInfoBoxGfx(); + InitHelp(); + } + InitStores(); + InitAutomapOnce(); +} + +void LoadGameLevelStores() +{ + if (leveltype == DTYPE_TOWN) { + SetupTownStores(); + } else { + FreeStoreMem(); + } +} + +void LoadGameLevelStash() +{ + const bool isHellfireSaveGame = gbIsHellfireSaveGame; + + gbIsHellfireSaveGame = gbIsHellfire; + LoadStash(); + gbIsHellfireSaveGame = isHellfireSaveGame; +} + +tl::expected LoadGameLevelDungeon(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pLvlVisited[currlevel] || gbIsMultiplayer) { + HoldThemeRooms(); + [[maybe_unused]] const uint32_t mid1Seed = GetLCGEngineState(); + InitGolems(); + InitObjects(); + [[maybe_unused]] const uint32_t mid2Seed = GetLCGEngineState(); + + IncProgress(); + + RETURN_IF_ERROR(InitMonsters()); + InitItems(); + CreateThemeRooms(); + + IncProgress(); + + [[maybe_unused]] const uint32_t mid3Seed = GetLCGEngineState(); + InitMissiles(); + InitCorpses(); +#ifdef _DEBUG + SetDebugLevelSeedInfos(mid1Seed, mid2Seed, mid3Seed, GetLCGEngineState()); +#endif + SavePreLighting(); + + IncProgress(); + + if (gbIsMultiplayer) + DeltaLoadLevel(); + } else { + HoldThemeRooms(); + InitGolems(); + RETURN_IF_ERROR(InitMonsters()); + InitMissiles(); + InitCorpses(); + + IncProgress(); + + RETURN_IF_ERROR(LoadLevel()); + + IncProgress(); + } + return {}; +} + +void LoadGameLevelSyncPlayerEntry(lvl_entry lvldir) +{ + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel() && (!player._pLvlChanging || &player == MyPlayer)) { + if (player._pHitPoints > 0) { + if (lvldir != ENTRY_LOAD) + SyncInitPlrPos(player); + } else { + dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; + } + } + } +} + +void LoadGameLevelLightVision() +{ + if (leveltype != DTYPE_TOWN) { + memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light + ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh + ProcessLightList(); + ProcessVisionList(); + } +} + +void LoadGameLevelReturn() +{ + ViewPosition = GetMapReturnPosition(); + if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) + Quests[Q_BETRAYER]._qvar2 = 2; +} + +void LoadGameLevelInitPlayers(bool firstflag, lvl_entry lvldir) +{ + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel()) { + InitPlayerGFX(player); + if (lvldir != ENTRY_LOAD) + InitPlayer(player, firstflag); + } + } +} + +void LoadGameLevelSetVisited() +{ + bool visited = false; + for (const Player &player : Players) { + if (player.plractive) + visited = visited || player._pLvlVisited[currlevel]; + } +} + +tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) + for (int j = 0; j < MAXDUNY; j++) { + dFlags[i][j] |= DungeonFlag::Lit; + } + } + + InitTowners(); + InitStash(); + InitVisualStore(); + InitItems(); + InitMissiles(); + + IncProgress(); + + if (!firstflag && lvldir != ENTRY_LOAD && myPlayer._pLvlVisited[currlevel] && !gbIsMultiplayer) + RETURN_IF_ERROR(LoadLevel()); + if (gbIsMultiplayer) + DeltaLoadLevel(); + + IncProgress(); + + for (int x = 0; x < DMAXX; x++) + for (int y = 0; y < DMAXY; y++) + UpdateAutomapExplorer({ x, y }, MAP_EXP_SELF); + return {}; +} + +tl::expected LoadGameLevelSetLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + LoadSetMap(); + IncProgress(); + RETURN_IF_ERROR(GetLevelMTypes()); + IncProgress(); + InitGolems(); + RETURN_IF_ERROR(InitMonsters()); + IncProgress(); + if (!HeadlessMode) { +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + RETURN_IF_ERROR(InitMissileGFX()); + IncProgress(); + } + InitCorpses(); + IncProgress(); + + if (lvldir == ENTRY_WARPLVL) + GetPortalLvlPos(); + IncProgress(); + + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel()) { + InitPlayerGFX(player); + if (lvldir != ENTRY_LOAD) + InitPlayer(player, firstflag); + } + } + IncProgress(); + InitMultiView(); + IncProgress(); + + if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pSLvlVisited[setlvlnum] || gbIsMultiplayer) { + InitItems(); + SavePreLighting(); + } else { + RETURN_IF_ERROR(LoadLevel()); + } + if (gbIsMultiplayer) { + DeltaLoadLevel(); + if (!UseMultiplayerQuests()) + ResyncQuests(); + } + + PlayDungMsgs(); + InitMissiles(); + IncProgress(); + return {}; +} + +tl::expected LoadGameLevelStandardLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + CreateLevel(lvldir); + + IncProgress(); + + SetRndSeedForDungeonLevel(); + + if (leveltype != DTYPE_TOWN) { + RETURN_IF_ERROR(GetLevelMTypes()); + InitThemes(); + if (!HeadlessMode) + RETURN_IF_ERROR(LoadAllGFX()); + } else if (!HeadlessMode) { + IncProgress(); + +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + + IncProgress(); + + RETURN_IF_ERROR(InitMissileGFX()); + + IncProgress(); + IncProgress(); + } + + IncProgress(); + + if (lvldir == ENTRY_RTNLVL) { + LoadGameLevelReturn(); + } + + if (lvldir == ENTRY_WARPLVL) + GetPortalLvlPos(); + + IncProgress(); + + LoadGameLevelInitPlayers(firstflag, lvldir); + InitMultiView(); + + IncProgress(); + + LoadGameLevelSetVisited(); + + SetRndSeedForDungeonLevel(); + + if (leveltype == DTYPE_TOWN) { + LoadGameLevelTown(firstflag, lvldir, myPlayer); + } else { + LoadGameLevelDungeon(firstflag, lvldir, myPlayer); + } + + PlayDungMsgs(); + + if (UseMultiplayerQuests()) + ResyncMPQuests(); + else + ResyncQuests(); + return {}; +} + +void LoadGameLevelCrypt() +{ + if (CornerStone.isAvailable()) { + CornerstoneLoad(CornerStone.position); + } + if (Quests[Q_NAKRUL]._qactive == QUEST_DONE && currlevel == 24) { + SyncNakrulRoom(); + } +} + +void LoadGameLevelCalculateCursor() +{ + // Recalculate mouse selection of entities after level change/load + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + ResetItemlabelHighlighted(); // level changed => item changed + pcursmonst = -1; // ensure pcurstemp is set to a valid value + CheckCursMove(); +} + +tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) +{ + const _music_id neededTrack = GetLevelMusic(leveltype); + + ClearFloatingNumbers(); + LoadGameLevelStopMusic(neededTrack); + LoadGameLevelResetCursor(); + SetRndSeedForDungeonLevel(); + NaKrulTomeSequence = 0; + + IncProgress(); + + RETURN_IF_ERROR(LoadTrns()); + MakeLightTable(); + RETURN_IF_ERROR(LoadLevelSOLData()); + + IncProgress(); + + RETURN_IF_ERROR(LoadLvlGFX()); + SetDungeonMicros(pDungeonCels, MicroTileLen); + ClearClxDrawCache(); + + IncProgress(); + + if (firstflag) { + LoadGameLevelFirstFlagEntry(); + } + + SetRndSeedForDungeonLevel(); + + LoadGameLevelStores(); + + if (firstflag || lvldir == ENTRY_LOAD) { + LoadGameLevelStash(); + } + + IncProgress(); + + InitAutomap(); + + if (leveltype != DTYPE_TOWN && lvldir != ENTRY_LOAD) { + InitLighting(); + } + + InitLevelMonsters(); + + IncProgress(); + + const Player &myPlayer = *MyPlayer; + + if (setlevel) { + RETURN_IF_ERROR(LoadGameLevelSetLevel(firstflag, lvldir, myPlayer)); + } else { + RETURN_IF_ERROR(LoadGameLevelStandardLevel(firstflag, lvldir, myPlayer)); + } + + SyncPortals(); + LoadGameLevelSyncPlayerEntry(lvldir); + + IncProgress(); + IncProgress(); + + if (firstflag) { + RETURN_IF_ERROR(InitMainPanel()); + } + + IncProgress(); + + UpdateMonsterLights(); + UnstuckChargers(); + + LoadGameLevelLightVision(); + + if (leveltype == DTYPE_CRYPT) { + LoadGameLevelCrypt(); + } + +#ifndef USE_SDL1 + ActivateVirtualGamepad(); +#endif + LoadGameLevelStartMusic(neededTrack); + + CompleteProgress(); + + LoadGameLevelCalculateCursor(); + return {}; +} + +bool game_loop(bool bStartup) +{ + const uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; + + for (unsigned i = 0; i < wait; i++) { + if (!multi_handle_delta()) { + TimeoutCursor(true); + return false; + } + TimeoutCursor(false); + GameLogic(); + ClearLastSentPlayerCmd(); + + if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) + break; + } + return true; +} + +void diablo_color_cyc_logic() +{ + if (!*GetOptions().Graphics.colorCycling) + return; + + if (PauseMode != 0) + return; + + if (leveltype == DTYPE_CAVES) { + if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { + UpdatePWaterPalette(); + } else { + palette_update_caves(); + } + } else if (leveltype == DTYPE_HELL) { + lighting_color_cycling(); + } else if (leveltype == DTYPE_NEST) { + palette_update_hive(); + } else if (leveltype == DTYPE_CRYPT) { + palette_update_crypt(); + } +} + +bool IsDiabloAlive(bool playSFX) +{ + if (Quests[Q_DIABLO]._qactive == QUEST_DONE && !gbIsMultiplayer) { + if (playSFX) + PlaySFX(SfxID::DiabloDeath); + return false; + } + + return true; +} + +void PrintScreen(SDL_Keycode vkey) +{ + ReleaseKey(vkey); +} + +} // namespace devilution diff --git a/Source/diablo.h b/Source/diablo.h index 46d2586fd1c..7ac017c0e48 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -1,127 +1,127 @@ -/** - * @file diablo.h - * - * Interface of the main game initialization functions. - */ -#pragma once - -#include - -#ifdef USE_SDL3 -#include -#include -#else -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif -#endif - -#ifdef _DEBUG -#include "tables/monstdat.h" -#endif -#include "levels/gendung.h" -#include "utils/attributes.h" -#include "utils/endian_read.hpp" - -namespace devilution { - -constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); -constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); -constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); -constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); -#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) - -#define NUMLEVELS 25 - -enum clicktype : int8_t { - CLICK_NONE, - CLICK_LEFT, - CLICK_RIGHT, -}; - -/** - * @brief Specifies what game logic step is currently executed - */ -enum class GameLogicStep : uint8_t { - None, - ProcessPlayers, - ProcessMonsters, - ProcessObjects, - ProcessMissiles, - ProcessItems, - ProcessTowners, - ProcessItemsTown, - ProcessMissilesTown, -}; - -enum class PlayerActionType : uint8_t { - None, - Walk, - Spell, - SpellMonsterTarget, - SpellPlayerTarget, - Attack, - AttackMonsterTarget, - AttackPlayerTarget, - OperateObject, -}; - -extern uint32_t DungeonSeeds[NUMLEVELS]; -extern DVL_API_FOR_TEST std::optional LevelSeeds[NUMLEVELS]; -extern DVL_API_FOR_TEST Point MousePosition; - -extern bool gbRunGameResult; -extern bool ReturnToMainMenu; -extern bool gbProcessPlayers; -extern DVL_API_FOR_TEST bool gbLoadGame; -extern bool cineflag; -/* These are defined in fonts.h */ -extern void FontsCleanup(); -extern DVL_API_FOR_TEST int PauseMode; -extern clicktype sgbMouseDown; -extern uint16_t gnTickDelay; -extern char gszProductName[64]; - -extern PlayerActionType LastPlayerAction; - -void InitKeymapActions(); -void SetCursorPos(Point position); -void FreeGameMem(); -bool StartGame(bool bNewGame, bool bSinglePlayer); -[[noreturn]] void diablo_quit(int exitStatus); -int DiabloMain(int argc, char **argv); -bool TryIconCurs(); -void diablo_pause_game(); -bool diablo_is_focused(); -void diablo_focus_pause(); -void diablo_focus_unpause(); -bool PressEscKey(); -void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); -tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); -bool IsDiabloAlive(bool playSFX); -void PrintScreen(SDL_Keycode vkey); - -/** - * @param bStartup Process additional ticks before returning - */ -bool game_loop(bool bStartup); -void diablo_color_cyc_logic(); - -/* rdata */ - -#ifdef _DEBUG -extern bool DebugDisableNetworkTimeout; -#endif - -/** - * @brief Specifies what game logic step is currently executed - */ -extern GameLogicStep gGameLogicStep; - -#ifdef __UWP__ -void setOnInitialized(void (*)()); -#endif - -} // namespace devilution +/** + * @file diablo.h + * + * Interface of the main game initialization functions. + */ +#pragma once + +#include + +#ifdef USE_SDL3 +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif + +#ifdef _DEBUG +#include "tables/monstdat.h" +#endif +#include "levels/gendung.h" +#include "utils/attributes.h" +#include "utils/endian_read.hpp" + +namespace devilution { + +constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); +constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); +constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); +constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); +#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) + +#define NUMLEVELS 25 + +enum clicktype : int8_t { + CLICK_NONE, + CLICK_LEFT, + CLICK_RIGHT, +}; + +/** + * @brief Specifies what game logic step is currently executed + */ +enum class GameLogicStep : uint8_t { + None, + ProcessPlayers, + ProcessMonsters, + ProcessObjects, + ProcessMissiles, + ProcessItems, + ProcessTowners, + ProcessItemsTown, + ProcessMissilesTown, +}; + +enum class PlayerActionType : uint8_t { + None, + Walk, + Spell, + SpellMonsterTarget, + SpellPlayerTarget, + Attack, + AttackMonsterTarget, + AttackPlayerTarget, + OperateObject, +}; + +extern uint32_t DungeonSeeds[NUMLEVELS]; +extern DVL_API_FOR_TEST std::optional LevelSeeds[NUMLEVELS]; +extern DVL_API_FOR_TEST Point MousePosition; + +extern bool gbRunGameResult; +extern bool ReturnToMainMenu; +extern bool gbProcessPlayers; +extern DVL_API_FOR_TEST bool gbLoadGame; +extern bool cineflag; +/* These are defined in fonts.h */ +extern void FontsCleanup(); +extern DVL_API_FOR_TEST int PauseMode; +extern clicktype sgbMouseDown; +extern uint16_t gnTickDelay; +extern char gszProductName[64]; + +extern PlayerActionType LastPlayerAction; + +void InitKeymapActions(); +void SetCursorPos(Point position); +void FreeGameMem(); +bool StartGame(bool bNewGame, bool bSinglePlayer); +[[noreturn]] void diablo_quit(int exitStatus); +int DiabloMain(int argc, char **argv); +bool TryIconCurs(); +void diablo_pause_game(); +bool diablo_is_focused(); +void diablo_focus_pause(); +void diablo_focus_unpause(); +bool PressEscKey(); +void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); +tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); +bool IsDiabloAlive(bool playSFX); +void PrintScreen(SDL_Keycode vkey); + +/** + * @param bStartup Process additional ticks before returning + */ +bool game_loop(bool bStartup); +void diablo_color_cyc_logic(); + +/* rdata */ + +#ifdef _DEBUG +extern bool DebugDisableNetworkTimeout; +#endif + +/** + * @brief Specifies what game logic step is currently executed + */ +extern GameLogicStep gGameLogicStep; + +#ifdef __UWP__ +void setOnInitialized(void (*)()); +#endif + +} // namespace devilution diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index f1f997c7556..cb7c1773bf9 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -1,391 +1,391 @@ -/** - * @file gamemenu.cpp - * - * Implementation of the in-game menu functions. - */ -#include "gamemenu.h" - -#ifdef USE_SDL3 -#include -#endif - -#include "cursor.h" -#include "diablo_msg.hpp" -#include "engine/backbuffer_state.hpp" -#include "engine/demomode.h" -#include "engine/events.hpp" -#include "engine/sound.h" -#include "engine/sound_defs.hpp" -#include "game_mode.hpp" -#include "gmenu.h" -#include "headless_mode.hpp" -#include "loadsave.h" -#include "multi.h" -#include "options.h" -#include "pfile.h" -#include "qol/floatingnumbers.h" -#include "utils/language.h" - -#ifndef USE_SDL1 -#include "controls/touch/renderers.h" -#endif - -namespace devilution { - -bool isGameMenuOpen = false; - -namespace { - -// Forward-declare menu handlers, used by the global menu structs below. -void GamemenuPrevious(bool bActivate); -void GamemenuNewGame(bool bActivate); -void GamemenuOptions(bool bActivate); -void GamemenuMusicVolume(bool bActivate); -void GamemenuSoundVolume(bool bActivate); -void GamemenuBrightness(bool bActivate); -void GamemenuSpeed(bool bActivate); - -/** Contains the game menu items of the single player menu. */ -TMenuItem sgSingleMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, - { GMENU_ENABLED, N_("Save Game"), &gamemenu_save_game }, - { GMENU_ENABLED, N_("Load Game"), &gamemenu_load_game }, - { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, - { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, - { GMENU_ENABLED, nullptr, nullptr }, - // clang-format on -}; -/** Contains the game menu items of the multi player menu. */ -TMenuItem sgMultiMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, - { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, - { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, - { GMENU_ENABLED, nullptr, nullptr }, - // clang-format on -}; -TMenuItem sgOptionsMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, - { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, - { GMENU_ENABLED , nullptr, nullptr }, - // clang-format on -}; -/** Specifies the menu names for music enabled and disabled. */ -const char *const MusicToggleNames[] = { - N_("Music"), - N_("Music Disabled"), -}; -/** Specifies the menu names for sound enabled and disabled. */ -const char *const SoundToggleNames[] = { - N_("Sound"), - N_("Sound Disabled"), -}; - -void GamemenuUpdateSingle() -{ - sgSingleMenu[2].setEnabled(gbValidSaveFile); - - const bool enable = MyPlayer->_pmode != PM_DEATH && !MyPlayerIsDead; - - sgSingleMenu[0].setEnabled(enable); -} - -void GamemenuPrevious(bool /*bActivate*/) -{ - gamemenu_on(); -} - -void GamemenuNewGame(bool /*bActivate*/) -{ - for (Player &player : Players) { - player._pmode = PM_QUIT; - player._pInvincible = true; - } - - MyPlayerIsDead = false; - if (!HeadlessMode) { - RedrawEverything(); - scrollrt_draw_game_screen(); - } - CornerStone.activated = false; - gbRunGame = false; - gamemenu_off(); -} - -void GamemenuSoundMusicToggle(const char *const *names, TMenuItem *menuItem, int volume) -{ - if (gbSndInited) { - menuItem->addFlags(GMENU_ENABLED | GMENU_SLIDER); - menuItem->pszStr = names[0]; - gmenu_slider_steps(menuItem, VOLUME_STEPS); - gmenu_slider_set(menuItem, VOLUME_MIN, VOLUME_MAX, volume); - return; - } - - menuItem->removeFlags(GMENU_ENABLED | GMENU_SLIDER); - menuItem->pszStr = names[1]; -} - -int GamemenuSliderMusicSound(TMenuItem *menuItem) -{ - return gmenu_slider_get(menuItem, VOLUME_MIN, VOLUME_MAX); -} - -void GamemenuGetMusic() -{ - GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); -} - -void GamemenuGetSound() -{ - GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); -} - -void GamemenuGetBrightness() -{ - gmenu_slider_steps(&sgOptionsMenu[2], 21); - gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); -} - -void GamemenuGetSpeed() -{ - if (gbIsMultiplayer) { - sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); - if (sgGameInitInfo.nTickRate >= 50) - sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); - else if (sgGameInitInfo.nTickRate >= 40) - sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); - else if (sgGameInitInfo.nTickRate >= 30) - sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); - else if (sgGameInitInfo.nTickRate == 20) - sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); - return; - } - - sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); - - sgOptionsMenu[3].pszStr = _("Speed").data(); - gmenu_slider_steps(&sgOptionsMenu[3], 46); - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); -} - -int GamemenuSliderBrightness() -{ - return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); -} - -void GamemenuOptions(bool /*bActivate*/) -{ - GamemenuGetMusic(); - GamemenuGetSound(); - GamemenuGetBrightness(); - GamemenuGetSpeed(); - gmenu_set_items(sgOptionsMenu, nullptr); -} - -void GamemenuMusicVolume(bool bActivate) -{ - if (bActivate) { - if (gbMusicOn) { - gbMusicOn = false; - music_stop(); - sound_get_or_set_music_volume(VOLUME_MIN); - } else { - gbMusicOn = true; - sound_get_or_set_music_volume(VOLUME_MAX); - music_start(GetLevelMusic(leveltype)); - } - } else { - const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[0]); - sound_get_or_set_music_volume(volume); - if (volume == VOLUME_MIN) { - if (gbMusicOn) { - gbMusicOn = false; - music_stop(); - } - } else if (!gbMusicOn) { - gbMusicOn = true; - music_start(GetLevelMusic(leveltype)); - } - } - - GamemenuGetMusic(); -} - -void GamemenuSoundVolume(bool bActivate) -{ - if (bActivate) { - if (gbSoundOn) { - gbSoundOn = false; - sound_stop(); - sound_get_or_set_sound_volume(VOLUME_MIN); - } else { - gbSoundOn = true; - sound_get_or_set_sound_volume(VOLUME_MAX); - } - } else { - const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[1]); - sound_get_or_set_sound_volume(volume); - if (volume == VOLUME_MIN) { - if (gbSoundOn) { - gbSoundOn = false; - sound_stop(); - } - } else if (!gbSoundOn) { - gbSoundOn = true; - } - } - PlaySFX(SfxID::MenuMove); - GamemenuGetSound(); -} - -void GamemenuBrightness(bool bActivate) -{ - int brightness; - if (bActivate) { - brightness = UpdateBrightness(-1); - brightness = (brightness == 0) ? 100 : 0; - } else { - brightness = GamemenuSliderBrightness(); - } - - UpdateBrightness(brightness); - GamemenuGetBrightness(); -} - -void GamemenuSpeed(bool bActivate) -{ - if (bActivate) { - if (sgGameInitInfo.nTickRate != 20) - sgGameInitInfo.nTickRate = 20; - else - sgGameInitInfo.nTickRate = 50; - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); - } else { - sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); - } - - GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); - gnTickDelay = 1000 / sgGameInitInfo.nTickRate; -} - -} // namespace - -void gamemenu_exit_game(bool bActivate) -{ - GamemenuNewGame(bActivate); -} - -void gamemenu_quit_game(bool bActivate) -{ - GamemenuNewGame(bActivate); -#ifndef NOEXIT - gbRunGameResult = false; -#else - ReturnToMainMenu = true; -#endif -} - -void gamemenu_load_game(bool /*bActivate*/) -{ - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); - gamemenu_off(); - ClearFloatingNumbers(); - NewCursor(CURSOR_NONE); - InitDiabloMsg(EMSG_LOADING); - RedrawEverything(); - DrawAndBlit(); - - const std::array prevPalette = logical_palette; -#ifndef USE_SDL1 - DeactivateVirtualGamepad(); - FreeVirtualGamepadTextures(); -#endif - if (tl::expected result = LoadGame(false); !result.has_value()) { - app_fatal(result.error()); - } -#if !defined(USE_SDL1) && !defined(__vita__) - if (renderer != nullptr) { - InitVirtualGamepadTextures(*renderer); - } -#endif - ClrDiabloMsg(); - PaletteFadeOut(8, prevPalette); - - LoadPWaterPalette(); - NewCursor(CURSOR_HAND); - CornerStone.activated = false; - MyPlayerIsDead = false; - RedrawEverything(); - DrawAndBlit(); - PaletteFadeIn(8); - NewCursor(CURSOR_HAND); - interface_msg_pump(); - SetEventHandler(saveProc); -} - -void gamemenu_save_game(bool /*bActivate*/) -{ - if (pcurs != CURSOR_HAND) { - return; - } - - if (MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead) { - gamemenu_off(); - return; - } - - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); - NewCursor(CURSOR_NONE); - gamemenu_off(); - InitDiabloMsg(EMSG_SAVING); - RedrawEverything(); - DrawAndBlit(); - const uint32_t currentTime = SDL_GetTicks(); - SaveGame(); - ClrDiabloMsg(); - InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks()); - RedrawEverything(); - NewCursor(CURSOR_HAND); - if (CornerStone.activated) { - CornerstoneSave(); - if (!demo::IsRunning()) SaveOptions(); - } - interface_msg_pump(); - SetEventHandler(saveProc); -} - -void gamemenu_on() -{ - isGameMenuOpen = true; - if (!gbIsMultiplayer) { - gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); - } else { - gmenu_set_items(sgMultiMenu, nullptr); - } - PressEscKey(); -} - -void gamemenu_off() -{ - isGameMenuOpen = false; - gmenu_set_items(nullptr, nullptr); -} - -void gamemenu_handle_previous() -{ - if (gmenu_is_active()) - gamemenu_off(); - else - gamemenu_on(); -} - -} // namespace devilution +/** + * @file gamemenu.cpp + * + * Implementation of the in-game menu functions. + */ +#include "gamemenu.h" + +#ifdef USE_SDL3 +#include +#endif + +#include "cursor.h" +#include "diablo_msg.hpp" +#include "engine/backbuffer_state.hpp" +#include "engine/demomode.h" +#include "engine/events.hpp" +#include "engine/sound.h" +#include "engine/sound_defs.hpp" +#include "game_mode.hpp" +#include "gmenu.h" +#include "headless_mode.hpp" +#include "loadsave.h" +#include "multi.h" +#include "options.h" +#include "pfile.h" +#include "qol/floatingnumbers.h" +#include "utils/language.h" + +#ifndef USE_SDL1 +#include "controls/touch/renderers.h" +#endif + +namespace devilution { + +bool isGameMenuOpen = false; + +namespace { + +// Forward-declare menu handlers, used by the global menu structs below. +void GamemenuPrevious(bool bActivate); +void GamemenuNewGame(bool bActivate); +void GamemenuOptions(bool bActivate); +void GamemenuMusicVolume(bool bActivate); +void GamemenuSoundVolume(bool bActivate); +void GamemenuBrightness(bool bActivate); +void GamemenuSpeed(bool bActivate); + +/** Contains the game menu items of the single player menu. */ +TMenuItem sgSingleMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, + { GMENU_ENABLED, N_("Save Game"), &gamemenu_save_game }, + { GMENU_ENABLED, N_("Load Game"), &gamemenu_load_game }, + { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, + { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, + { GMENU_ENABLED, nullptr, nullptr }, + // clang-format on +}; +/** Contains the game menu items of the multi player menu. */ +TMenuItem sgMultiMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, + { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, + { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, + { GMENU_ENABLED, nullptr, nullptr }, + // clang-format on +}; +TMenuItem sgOptionsMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, + { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, + { GMENU_ENABLED , nullptr, nullptr }, + // clang-format on +}; +/** Specifies the menu names for music enabled and disabled. */ +const char *const MusicToggleNames[] = { + N_("Music"), + N_("Music Disabled"), +}; +/** Specifies the menu names for sound enabled and disabled. */ +const char *const SoundToggleNames[] = { + N_("Sound"), + N_("Sound Disabled"), +}; + +void GamemenuUpdateSingle() +{ + sgSingleMenu[2].setEnabled(gbValidSaveFile); + + const bool enable = MyPlayer->_pmode != PM_DEATH && !MyPlayerIsDead; + + sgSingleMenu[0].setEnabled(enable); +} + +void GamemenuPrevious(bool /*bActivate*/) +{ + gamemenu_on(); +} + +void GamemenuNewGame(bool /*bActivate*/) +{ + for (Player &player : Players) { + player._pmode = PM_QUIT; + player._pInvincible = true; + } + + MyPlayerIsDead = false; + if (!HeadlessMode) { + RedrawEverything(); + scrollrt_draw_game_screen(); + } + CornerStone.activated = false; + gbRunGame = false; + gamemenu_off(); +} + +void GamemenuSoundMusicToggle(const char *const *names, TMenuItem *menuItem, int volume) +{ + if (gbSndInited) { + menuItem->addFlags(GMENU_ENABLED | GMENU_SLIDER); + menuItem->pszStr = names[0]; + gmenu_slider_steps(menuItem, VOLUME_STEPS); + gmenu_slider_set(menuItem, VOLUME_MIN, VOLUME_MAX, volume); + return; + } + + menuItem->removeFlags(GMENU_ENABLED | GMENU_SLIDER); + menuItem->pszStr = names[1]; +} + +int GamemenuSliderMusicSound(TMenuItem *menuItem) +{ + return gmenu_slider_get(menuItem, VOLUME_MIN, VOLUME_MAX); +} + +void GamemenuGetMusic() +{ + GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); +} + +void GamemenuGetSound() +{ + GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); +} + +void GamemenuGetBrightness() +{ + gmenu_slider_steps(&sgOptionsMenu[2], 21); + gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); +} + +void GamemenuGetSpeed() +{ + if (gbIsMultiplayer) { + sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); + if (sgGameInitInfo.nTickRate >= 50) + sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); + else if (sgGameInitInfo.nTickRate >= 40) + sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); + else if (sgGameInitInfo.nTickRate >= 30) + sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); + else if (sgGameInitInfo.nTickRate == 20) + sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); + return; + } + + sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); + + sgOptionsMenu[3].pszStr = _("Speed").data(); + gmenu_slider_steps(&sgOptionsMenu[3], 46); + gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); +} + +int GamemenuSliderBrightness() +{ + return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); +} + +void GamemenuOptions(bool /*bActivate*/) +{ + GamemenuGetMusic(); + GamemenuGetSound(); + GamemenuGetBrightness(); + GamemenuGetSpeed(); + gmenu_set_items(sgOptionsMenu, nullptr); +} + +void GamemenuMusicVolume(bool bActivate) +{ + if (bActivate) { + if (gbMusicOn) { + gbMusicOn = false; + music_stop(); + sound_get_or_set_music_volume(VOLUME_MIN); + } else { + gbMusicOn = true; + sound_get_or_set_music_volume(VOLUME_MAX); + music_start(GetLevelMusic(leveltype)); + } + } else { + const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[0]); + sound_get_or_set_music_volume(volume); + if (volume == VOLUME_MIN) { + if (gbMusicOn) { + gbMusicOn = false; + music_stop(); + } + } else if (!gbMusicOn) { + gbMusicOn = true; + music_start(GetLevelMusic(leveltype)); + } + } + + GamemenuGetMusic(); +} + +void GamemenuSoundVolume(bool bActivate) +{ + if (bActivate) { + if (gbSoundOn) { + gbSoundOn = false; + sound_stop(); + sound_get_or_set_sound_volume(VOLUME_MIN); + } else { + gbSoundOn = true; + sound_get_or_set_sound_volume(VOLUME_MAX); + } + } else { + const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[1]); + sound_get_or_set_sound_volume(volume); + if (volume == VOLUME_MIN) { + if (gbSoundOn) { + gbSoundOn = false; + sound_stop(); + } + } else if (!gbSoundOn) { + gbSoundOn = true; + } + } + PlaySFX(SfxID::MenuMove); + GamemenuGetSound(); +} + +void GamemenuBrightness(bool bActivate) +{ + int brightness; + if (bActivate) { + brightness = UpdateBrightness(-1); + brightness = (brightness == 0) ? 100 : 0; + } else { + brightness = GamemenuSliderBrightness(); + } + + UpdateBrightness(brightness); + GamemenuGetBrightness(); +} + +void GamemenuSpeed(bool bActivate) +{ + if (bActivate) { + if (sgGameInitInfo.nTickRate != 20) + sgGameInitInfo.nTickRate = 20; + else + sgGameInitInfo.nTickRate = 50; + gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); + } else { + sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); + } + + GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); + gnTickDelay = 1000 / sgGameInitInfo.nTickRate; +} + +} // namespace + +void gamemenu_exit_game(bool bActivate) +{ + GamemenuNewGame(bActivate); +} + +void gamemenu_quit_game(bool bActivate) +{ + GamemenuNewGame(bActivate); +#ifndef NOEXIT + gbRunGameResult = false; +#else + ReturnToMainMenu = true; +#endif +} + +void gamemenu_load_game(bool /*bActivate*/) +{ + EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + gamemenu_off(); + ClearFloatingNumbers(); + NewCursor(CURSOR_NONE); + InitDiabloMsg(EMSG_LOADING); + RedrawEverything(); + DrawAndBlit(); + + const std::array prevPalette = logical_palette; +#ifndef USE_SDL1 + DeactivateVirtualGamepad(); + FreeVirtualGamepadTextures(); +#endif + if (tl::expected result = LoadGame(false); !result.has_value()) { + app_fatal(result.error()); + } +#if !defined(USE_SDL1) && !defined(__vita__) + if (renderer != nullptr) { + InitVirtualGamepadTextures(*renderer); + } +#endif + ClrDiabloMsg(); + PaletteFadeOut(8, prevPalette); + + LoadPWaterPalette(); + NewCursor(CURSOR_HAND); + CornerStone.activated = false; + MyPlayerIsDead = false; + RedrawEverything(); + DrawAndBlit(); + PaletteFadeIn(8); + NewCursor(CURSOR_HAND); + interface_msg_pump(); + SetEventHandler(saveProc); +} + +void gamemenu_save_game(bool /*bActivate*/) +{ + if (pcurs != CURSOR_HAND) { + return; + } + + if (MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead) { + gamemenu_off(); + return; + } + + EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + NewCursor(CURSOR_NONE); + gamemenu_off(); + InitDiabloMsg(EMSG_SAVING); + RedrawEverything(); + DrawAndBlit(); + const uint32_t currentTime = SDL_GetTicks(); + SaveGame(); + ClrDiabloMsg(); + InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks()); + RedrawEverything(); + NewCursor(CURSOR_HAND); + if (CornerStone.activated) { + CornerstoneSave(); + if (!demo::IsRunning()) SaveOptions(); + } + interface_msg_pump(); + SetEventHandler(saveProc); +} + +void gamemenu_on() +{ + isGameMenuOpen = true; + if (!gbIsMultiplayer) { + gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); + } else { + gmenu_set_items(sgMultiMenu, nullptr); + } + PressEscKey(); +} + +void gamemenu_off() +{ + isGameMenuOpen = false; + gmenu_set_items(nullptr, nullptr); +} + +void gamemenu_handle_previous() +{ + if (gmenu_is_active()) + gamemenu_off(); + else + gamemenu_on(); +} + +} // namespace devilution diff --git a/Source/inv.cpp b/Source/inv.cpp index 5dae0e53216..7fd3f6e68cb 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -1,2299 +1,2299 @@ -/** - * @file inv.cpp - * - * Implementation of player inventory. - */ -#include -#include -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include -#else -#include -#endif - -#include - -#include "DiabloUI/ui_flags.hpp" -#include "controls/control_mode.hpp" -#include "controls/plrctrls.h" -#include "cursor.h" -#include "engine/backbuffer_state.hpp" -#include "engine/clx_sprite.hpp" -#include "engine/load_cel.hpp" -#include "engine/palette.h" -#include "engine/render/clx_render.hpp" -#include "engine/render/text_render.hpp" -#include "engine/size.hpp" -#include "hwcursor.hpp" -#include "inv_iterators.hpp" -#include "levels/tile_properties.hpp" -#include "levels/town.h" -#include "minitext.h" -#include "options.h" -#include "panels/ui_panels.hpp" -#include "player.h" -#include "plrmsg.h" -#include "qol/stash.h" -#include "qol/visual_store.h" -#include "stores.h" -#include "towners.h" -#include "utils/display.h" -#include "utils/format_int.hpp" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/sdl_geometry.h" -#include "utils/str_cat.hpp" -#include "utils/utf8.hpp" - -namespace devilution { - -bool invflag; - -/** - * Maps from inventory slot to screen position. The inventory slots are - * arranged as follows: - * - * @code{.unparsed} - * 00 00 - * 00 00 03 - * - * 04 04 06 06 05 05 - * 04 04 06 06 05 05 - * 04 04 06 06 05 05 - * - * 01 02 - * - * 07 08 09 10 11 12 13 14 15 16 - * 17 18 19 20 21 22 23 24 25 26 - * 27 28 29 30 31 32 33 34 35 36 - * 37 38 39 40 41 42 43 44 45 46 - * - * 47 48 49 50 51 52 53 54 - * @endcode - */ -const Rectangle InvRect[] = { - // clang-format off - //{ X, Y }, { W, H } - { { 132, 2 }, { 58, 59 } }, // helmet - { { 47, 177 }, { 28, 29 } }, // left ring - { { 248, 177 }, { 28, 29 } }, // right ring - { { 205, 32 }, { 28, 29 } }, // amulet - { { 17, 75 }, { 58, 86 } }, // left hand - { { 248, 75 }, { 58, 87 } }, // right hand - { { 132, 75 }, { 58, 87 } }, // chest - { { 17, 222 }, { 29, 29 } }, // inv row 1 - { { 46, 222 }, { 29, 29 } }, // inv row 1 - { { 75, 222 }, { 29, 29 } }, // inv row 1 - { { 104, 222 }, { 29, 29 } }, // inv row 1 - { { 133, 222 }, { 29, 29 } }, // inv row 1 - { { 162, 222 }, { 29, 29 } }, // inv row 1 - { { 191, 222 }, { 29, 29 } }, // inv row 1 - { { 220, 222 }, { 29, 29 } }, // inv row 1 - { { 249, 222 }, { 29, 29 } }, // inv row 1 - { { 278, 222 }, { 29, 29 } }, // inv row 1 - { { 17, 251 }, { 29, 29 } }, // inv row 2 - { { 46, 251 }, { 29, 29 } }, // inv row 2 - { { 75, 251 }, { 29, 29 } }, // inv row 2 - { { 104, 251 }, { 29, 29 } }, // inv row 2 - { { 133, 251 }, { 29, 29 } }, // inv row 2 - { { 162, 251 }, { 29, 29 } }, // inv row 2 - { { 191, 251 }, { 29, 29 } }, // inv row 2 - { { 220, 251 }, { 29, 29 } }, // inv row 2 - { { 249, 251 }, { 29, 29 } }, // inv row 2 - { { 278, 251 }, { 29, 29 } }, // inv row 2 - { { 17, 280 }, { 29, 29 } }, // inv row 3 - { { 46, 280 }, { 29, 29 } }, // inv row 3 - { { 75, 280 }, { 29, 29 } }, // inv row 3 - { { 104, 280 }, { 29, 29 } }, // inv row 3 - { { 133, 280 }, { 29, 29 } }, // inv row 3 - { { 162, 280 }, { 29, 29 } }, // inv row 3 - { { 191, 280 }, { 29, 29 } }, // inv row 3 - { { 220, 280 }, { 29, 29 } }, // inv row 3 - { { 249, 280 }, { 29, 29 } }, // inv row 3 - { { 278, 280 }, { 29, 29 } }, // inv row 3 - { { 17, 309 }, { 29, 29 } }, // inv row 4 - { { 46, 309 }, { 29, 29 } }, // inv row 4 - { { 75, 309 }, { 29, 29 } }, // inv row 4 - { { 104, 309 }, { 29, 29 } }, // inv row 4 - { { 133, 309 }, { 29, 29 } }, // inv row 4 - { { 162, 309 }, { 29, 29 } }, // inv row 4 - { { 191, 309 }, { 29, 29 } }, // inv row 4 - { { 220, 309 }, { 29, 29 } }, // inv row 4 - { { 249, 309 }, { 29, 29 } }, // inv row 4 - { { 278, 309 }, { 29, 29 } }, // inv row 4 - { { 205, 5 }, { 29, 29 } }, // belt - { { 234, 5 }, { 29, 29 } }, // belt - { { 263, 5 }, { 29, 29 } }, // belt - { { 292, 5 }, { 29, 29 } }, // belt - { { 321, 5 }, { 29, 29 } }, // belt - { { 350, 5 }, { 29, 29 } }, // belt - { { 379, 5 }, { 29, 29 } }, // belt - { { 408, 5 }, { 29, 29 } } // belt - // clang-format on -}; - -namespace { - -OptionalOwnedClxSpriteList pInvCels; - -/** - * @brief Adds an item to a player's InvGrid array - * @param player The player reference - * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) - * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) - * @param itemSize Size of item - */ -void AddItemToInvGrid(Player &player, int invGridIndex, int invListIndex, Size itemSize, bool sendNetworkMessage) -{ - const int pitch = 10; - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - if (x == 0 && y == itemSize.height - 1) - player.InvGrid[rowGridIndex + x] = invListIndex; - else - player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell. - } - } - - if (sendNetworkMessage) { - NetSendCmdChInvItem(false, invGridIndex); - } -} - -/** - * @brief Checks whether the given item can fit in a belt slot (i.e. the item's size in inventory cells is 1x1). - * @param item The item to be checked. - * @return 'True' in case the item can fit a belt slot and 'False' otherwise. - */ -bool FitsInBeltSlot(const Item &item) -{ - return GetInventorySize(item) == Size { 1, 1 }; -} - -/** - * @brief Checks whether the given item can be equipped. Since this overload doesn't take player information, it only considers - * general aspects about the item, like if its requirements are met and if the item's target location is valid for the body. - * @param item The item to check. - * @return 'True' in case the item could be equipped in a player, and 'False' otherwise. - */ -bool CanEquip(const Item &item) -{ - return item.isEquipment() - && item._iStatFlag; -} - -/** - * @brief A specialized version of 'CanEquip(int, Item&, int)' that specifically checks whether the item can be equipped - * in one/both of the player's hands. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to check. - * @return 'True' if the player can currently equip the item in either one of his hands (i.e. the required hands are empty and - * allow the item), and 'False' otherwise. - */ -bool CanWield(Player &player, const Item &item) -{ - if (!CanEquip(item) || IsNoneOf(player.GetItemLocation(item), ILOC_ONEHAND, ILOC_TWOHAND)) - return false; - - const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; - const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; - - if (leftHandItem.isEmpty() && rightHandItem.isEmpty()) { - return true; - } - - if (!leftHandItem.isEmpty() && !rightHandItem.isEmpty()) { - return false; - } - - const Item &occupiedHand = !leftHandItem.isEmpty() ? leftHandItem : rightHandItem; - - // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied - // slot is another one-handed weapon. - const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); - if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { - const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND - && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); - - const bool weaponToEquipIsOneHandedSwordOrMace = player.GetItemLocation(item) == ILOC_ONEHAND - && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace); - - if (occupiedHandIsOneHandedSwordOrMace && weaponToEquipIsOneHandedSwordOrMace) { - return true; - } - } - - return player.GetItemLocation(item) == ILOC_ONEHAND - && player.GetItemLocation(occupiedHand) == ILOC_ONEHAND - && item._iClass != occupiedHand._iClass; -} - -/** - * @brief Checks whether the specified item can be equipped in the desired body location on the player. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to check. - * @param bodyLocation The location in the inventory to be checked against. - * @return 'True' if the player can currently equip the item in the specified body location (i.e. the body location is empty and - * allows the item), and 'False' otherwise. - */ -bool CanEquip(Player &player, const Item &item, inv_body_loc bodyLocation) -{ - if (!CanEquip(item) || player._pmode > PM_WALK_SIDEWAYS || !player.InvBody[bodyLocation].isEmpty()) { - return false; - } - - switch (bodyLocation) { - case INVLOC_AMULET: - return item._iLoc == ILOC_AMULET; - - case INVLOC_CHEST: - return item._iLoc == ILOC_ARMOR; - - case INVLOC_HAND_LEFT: - case INVLOC_HAND_RIGHT: - return CanWield(player, item); - - case INVLOC_HEAD: - return item._iLoc == ILOC_HELM; - - case INVLOC_RING_LEFT: - case INVLOC_RING_RIGHT: - return item._iLoc == ILOC_RING; - - default: - return false; - } -} - -void ChangeEquipment(Player &player, inv_body_loc bodyLocation, const Item &item, bool sendNetworkMessage) -{ - player.InvBody[bodyLocation] = item; - - if (sendNetworkMessage) { - NetSendCmdChItem(false, bodyLocation, true); - } -} - -bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool persistItem, bool sendNetworkMessage) -{ - if (!CanEquip(player, item, bodyLocation)) { - return false; - } - - if (persistItem) { - ChangeEquipment(player, bodyLocation, item, sendNetworkMessage); - - if (sendNetworkMessage && *GetOptions().Audio.autoEquipSound) { - PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); - } - - CalcPlrInv(player, true); - } - - return true; -} - -int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) -{ - Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; - for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) - return r; - } - for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) { - // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. - if (itemSize.height <= 1 && itemSize.width <= 1) { - // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do - return r; - } - // Otherwise work out how far the central cell is from the top-left cell - Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; - // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. - if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { - // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left - hotPixelCellOffset.deltaX++; - } - if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { - // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above - hotPixelCellOffset.deltaY++; - } - // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) - const int hotPixelCell = r - SLOTXY_INV_FIRST; - const int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); - const int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); - return SLOTXY_INV_FIRST + (targetRow * InventorySizeInSlots.width) + targetColumn; - } - } - - panelOffset = Point { 0, 0 } - GetMainPanel().position; - for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) - return r; - } - return NUM_XY_SLOTS; -} - -void ChangeBodyEquipment(Player &player, int slot, item_equip_type location) -{ - const inv_body_loc bodyLocation = [&slot](item_equip_type location) { - switch (location) { - case ILOC_HELM: - return INVLOC_HEAD; - case ILOC_RING: - return (slot == SLOTXY_RING_LEFT ? INVLOC_RING_LEFT : INVLOC_RING_RIGHT); - case ILOC_AMULET: - return INVLOC_AMULET; - case ILOC_ARMOR: - return INVLOC_CHEST; - default: - app_fatal("Unexpected equipment type"); - } - }(location); - const Item previouslyEquippedItem = player.InvBody[slot]; - ChangeEquipment(player, bodyLocation, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } -} - -void ChangeEquippedItem(Player &player, uint8_t slot) -{ - const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; - const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; - - const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); - - const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) - || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); - - const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); - - const inv_body_loc pasteHand = pasteIntoSelectedHand ? selectedHand : otherHand; - const Item previouslyEquippedItem = dequipTwoHandedWeapon ? player.InvBody[otherHand] : player.InvBody[pasteHand]; - if (dequipTwoHandedWeapon) { - RemoveEquipment(player, otherHand, false); - } - ChangeEquipment(player, pasteHand, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } -} - -void ChangeTwoHandItem(Player &player) -{ - if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && !player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - inv_body_loc locationToUnequip = INVLOC_HAND_LEFT; - if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { - locationToUnequip = INVLOC_HAND_RIGHT; - } - if (!AutoPlaceItemInInventory(player, player.InvBody[locationToUnequip])) { - return; - } - - if (locationToUnequip == INVLOC_HAND_RIGHT) { - RemoveEquipment(player, INVLOC_HAND_RIGHT, false); - } else { - player.InvBody[INVLOC_HAND_LEFT].clear(); - } - } - - if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_LEFT]; - ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } - } else { - const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_RIGHT]; - RemoveEquipment(player, INVLOC_HAND_RIGHT, false); - ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem, &player == MyPlayer); - player.HoldItem = previouslyEquippedItem; - } -} - -int8_t CheckOverlappingItems(int slot, const Player &player, Size itemSize) -{ - // check that the item we're pasting only overlaps one other item (or is going into empty space) - const auto originCell = static_cast(slot - SLOTXY_INV_FIRST); - - int8_t overlappingId = 0; - for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { - - for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { - const unsigned testCell = originCell + rowOffset + columnOffset; - // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. - assert(testCell < sizeof(player.InvGrid)); - if (player.InvGrid[testCell] != 0) { - const int8_t iv = std::abs(player.InvGrid[testCell]); - if (overlappingId != 0) { - if (overlappingId != iv) { - // Found two different items that would be displaced by the held item, can't paste the item here. - return -1; - } - } else { - overlappingId = iv; - } - } - } - } - - return overlappingId; -} - -int8_t GetPrevItemId(int slot, const Player &player, const Size &itemSize) -{ - if (player.HoldItem._itype != ItemType::Gold) - return CheckOverlappingItems(slot, player, itemSize); - const int8_t item_cell_begin = player.InvGrid[slot - SLOTXY_INV_FIRST]; - if (item_cell_begin == 0) - return 0; - if (item_cell_begin <= 0) - return -item_cell_begin; - if (player.InvList[item_cell_begin - 1]._itype != ItemType::Gold) - return item_cell_begin; - return 0; -} - -bool ChangeInvItem(Player &player, int slot, Size itemSize) -{ - int8_t prevItemId = GetPrevItemId(slot, player, itemSize); - if (prevItemId < 0) return false; - - if (player.HoldItem._itype == ItemType::Gold && prevItemId == 0) { - const int ii = slot - SLOTXY_INV_FIRST; - if (player.InvGrid[ii] > 0) { - const int invIndex = player.InvGrid[ii] - 1; - const int gt = player.InvList[invIndex]._ivalue; - int ig = player.HoldItem._ivalue + gt; - if (ig <= MaxGold) { - player.InvList[invIndex]._ivalue = ig; - SetPlrHandGoldCurs(player.InvList[invIndex]); - player._pGold += player.HoldItem._ivalue; - player.HoldItem.clear(); - } else { - ig = MaxGold - gt; - player._pGold += ig; - player.HoldItem._ivalue -= ig; - SetPlrHandGoldCurs(player.HoldItem); - player.InvList[invIndex]._ivalue = MaxGold; - player.InvList[invIndex]._iCurs = ICURS_GOLD_LARGE; - } - } else { - const int invIndex = player._pNumInv; - player._pGold += player.HoldItem._ivalue; - player.InvList[invIndex] = player.HoldItem.pop(); - player._pNumInv++; - player.InvGrid[ii] = player._pNumInv; - } - if (&player == MyPlayer) { - NetSendCmdChInvItem(false, ii); - } - } else { - if (prevItemId == 0) { - player.InvList[player._pNumInv] = player.HoldItem.pop(); - player._pNumInv++; - prevItemId = player._pNumInv; - } else { - const int invIndex = prevItemId - 1; - if (player.HoldItem._itype == ItemType::Gold) - player._pGold += player.HoldItem._ivalue; - std::swap(player.InvList[invIndex], player.HoldItem); - if (player.HoldItem._itype == ItemType::Gold) - player._pGold = CalculateGold(player); - for (int8_t &itemIndex : player.InvGrid) { - if (itemIndex == prevItemId) - itemIndex = 0; - if (itemIndex == -prevItemId) - itemIndex = 0; - } - } - - AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); - } - - return true; -} - -void ChangeBeltItem(Player &player, int slot) -{ - const int ii = slot - SLOTXY_BELT_FIRST; - if (player.SpdList[ii].isEmpty()) { - player.SpdList[ii] = player.HoldItem.pop(); - } else { - std::swap(player.SpdList[ii], player.HoldItem); - - if (player.HoldItem._itype == ItemType::Gold) - player._pGold = CalculateGold(player); - } - if (&player == MyPlayer) { - NetSendCmdChBeltItem(false, ii); - } - RedrawComponent(PanelDrawComponent::Belt); -} - -item_equip_type GetItemEquipType(int slot, item_equip_type desiredLocation) -{ - if (slot == SLOTXY_HEAD) - return ILOC_HELM; - if (slot == SLOTXY_RING_LEFT || slot == SLOTXY_RING_RIGHT) - return ILOC_RING; - if (slot == SLOTXY_AMULET) - return ILOC_AMULET; - if (slot == SLOTXY_HAND_LEFT || slot == SLOTXY_HAND_RIGHT) { - if (desiredLocation == ILOC_TWOHAND) - return ILOC_TWOHAND; - return ILOC_ONEHAND; - } - if (slot == SLOTXY_CHEST) - return ILOC_ARMOR; - if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) - return ILOC_BELT; - - return ILOC_UNEQUIPABLE; -} - -void CheckInvPaste(Player &player, Point cursorPosition) -{ - const Size itemSize = GetInventorySize(player.HoldItem); - - const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); - if (slot == NUM_XY_SLOTS) - return; - - const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); - const item_equip_type location = GetItemEquipType(slot, desiredLocation); - - if (location == ILOC_BELT) { - if (!CanBePlacedOnBelt(player, player.HoldItem)) return; - } else if (location != ILOC_UNEQUIPABLE) { - if (desiredLocation != location) return; - } - - if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { - if (!player.CanUseItem(player.HoldItem)) { - player.Say(HeroSpeech::ICantUseThisYet); - return; - } - if (player._pmode > PM_WALK_SIDEWAYS) - return; - } - - if (&player == MyPlayer) { - PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); - } - - // Select the parameters that go into - // ChangeEquipment and add it to post switch - switch (location) { - case ILOC_HELM: - case ILOC_RING: - case ILOC_AMULET: - case ILOC_ARMOR: - ChangeBodyEquipment(player, slot, location); - break; - case ILOC_ONEHAND: - ChangeEquippedItem(player, slot); - break; - case ILOC_TWOHAND: - ChangeTwoHandItem(player); - break; - case ILOC_UNEQUIPABLE: - if (!ChangeInvItem(player, slot, itemSize)) return; - break; - case ILOC_BELT: - ChangeBeltItem(player, slot); - break; - case ILOC_NONE: - case ILOC_INVALID: - break; - } - - CalcPlrInv(player, true); - if (&player == MyPlayer) { - NewCursor(player.HoldItem); - } -} - -inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) -{ - assert(slot <= SLOTXY_CHEST); - return static_cast(slot); -} - -std::optional FindSlotUnderCursor(Point cursorPosition) -{ - - auto testPosition = static_cast(cursorPosition - GetRightPanel().position); - for (std::underlying_type_t r = SLOTXY_EQUIPPED_FIRST; r != SLOTXY_BELT_FIRST; r++) { - // check which body/inventory rectangle the mouse is in, if any - if (InvRect[r].contains(testPosition)) { - return static_cast(r); - } - } - - testPosition = static_cast(cursorPosition - GetMainPanel().position); - for (std::underlying_type_t r = SLOTXY_BELT_FIRST; r != NUM_XY_SLOTS; r++) { - // check which belt rectangle the mouse is in, if any - if (InvRect[r].contains(testPosition)) { - return static_cast(r); - } - } - - return {}; -} - -/** - * @brief Checks whether an item of the given size can be placed on the specified player's inventory slot. - * @param player The player whose inventory will be checked. - * @param slotIndex The 0-based index of the slot to put the item on. - * @param itemSize The size of the item to be checked. - * @param itemIndexToIgnore can be used to check if an item of the given size would fit if the item with the given (positive) ID was removed. - * @return 'True' in case the item can be placed on the specified player's inventory slot and 'False' otherwise. - */ -bool CheckItemFitsInInventorySlot(const Player &player, int slotIndex, const Size &itemSize, int itemIndexToIgnore) -{ - int yy = (slotIndex > 0) ? (10 * (slotIndex / 10)) : 0; - - for (int j = 0; j < itemSize.height; j++) { - if (yy >= InventoryGridCells) { - return false; - } - int xx = (slotIndex > 0) ? (slotIndex % 10) : 0; - for (int i = 0; i < itemSize.width; i++) { - if (xx >= 10 || (player.InvGrid[xx + yy] != 0 && std::abs(player.InvGrid[xx + yy]) - 1 != itemIndexToIgnore)) { - // The item is too wide to fit in the specified column, or one of the cells is occupied (and not by the item we're planning on removing) - return false; - } - xx++; - } - yy += 10; - } - return true; -} - -/** - * @brief Finds the first slot that could fit an item of the given size - * @param player Player whose inventory will be checked. - * @param itemSize Dimensions of the item. - * @param itemIndexToIgnore Can be used if you want to find whether the new item would fit with this item removed, without performing unnecessary actions. - * @return The first slot that could fit the item or an empty optional. - */ -std::optional FindSlotForItem(const Player &player, const Size &itemSize, int itemIndexToIgnore = -1) -{ - if (itemSize.height == 1) { - for (int i = 30; i <= 39; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - for (int x = 9; x >= 0; x--) { - for (int y = 2; y >= 0; y--) { - if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) - return (10 * y) + x; - } - } - return {}; - } - - if (itemSize.height == 2) { - for (int x = 10 - itemSize.width; x >= 0; x--) { - for (int y = 0; y < 3; y++) { - if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) - return (10 * y) + x; - } - } - return {}; - } - - if (itemSize == Size { 1, 3 }) { - for (int i = 0; i < 20; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - return {}; - } - - if (itemSize == Size { 2, 3 }) { - for (int i = 0; i < 9; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - - for (int i = 10; i < 19; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - return {}; - } - - app_fatal(StrCat("Unknown item size: ", itemSize.width, "x", itemSize.height)); -} - -/** - * @brief Checks if the given item could be placed on the specified players inventory if the other item was removed. - * @param player The player whose inventory will be checked. - * @param item The item to be checked. - * @param itemIndexToIgnore The inventory index of the item that we assume will be removed. - * @return 'True' if the item could fit with the other item removed and 'False' otherwise. - */ -bool CouldFitItemInInventory(const Player &player, const Item &item, int itemIndexToIgnore) -{ - return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); -} - -void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) -{ - if (player._pmode > PM_WALK_SIDEWAYS) { - return; - } - - CloseGoldDrop(); - - std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); - - if (!maybeSlot) { - // not on an inventory slot rectangle - return; - } - - const inv_xy_slot r = *maybeSlot; - - Item &holdItem = player.HoldItem; - holdItem.clear(); - - bool attemptedMove = false; - bool automaticallyMoved = false; - SfxID successSound = SfxID::None; - HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else - - if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { - const inv_body_loc invloc = MapSlotToInvBodyLoc(r); - if (!player.InvBody[invloc].isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; - RemoveEquipment(player, invloc, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = player.InvBody[invloc]; - RemoveEquipment(player, invloc, false); - } - } - } - - if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { - const unsigned ig = r - SLOTXY_INV_FIRST; - const int iv = std::abs(player.InvGrid[ig]) - 1; - if (iv >= 0) { - if (automaticMove) { - attemptedMove = true; - if (CanBePlacedOnBelt(player, player.InvList[iv])) { - automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveInvItem(iv, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else if (CanEquip(player.InvList[iv])) { - failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails - - /* - * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be - * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except - * we don't want to leave the item on the hand if the equip attempt failed. We would end up - * generating wasteful network messages if we did the lift first. Instead we work out whatever slot - * needs to be unequipped (if any): - */ - int invloc = NUM_INVLOC; - switch (player.GetItemLocation(player.InvList[iv])) { - case ILOC_ARMOR: - invloc = INVLOC_CHEST; - break; - case ILOC_HELM: - invloc = INVLOC_HEAD; - break; - case ILOC_AMULET: - invloc = INVLOC_AMULET; - break; - case ILOC_ONEHAND: - if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() - && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass - || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { - // The left hand is not empty and we're either trying to equip the same type of item or - // it's holding a two handed weapon, so it must be unequipped - invloc = INVLOC_HAND_LEFT; - } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { - // The right hand is not empty and we're trying to equip the same type of item, so we need - // to unequip that item - invloc = INVLOC_HAND_RIGHT; - } - // otherwise one hand is empty (and we can let the auto-equip code put the target item into - // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip - // a shield (in which case the attempt will fail). - break; - case ILOC_TWOHAND: - // Moving a two-hand item from inventory to InvBody requires emptying both hands. - if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - // If the right hand is empty then we can simply try equipping this item in the left hand, - // we'll let the common code take care of unequipping anything held there. - invloc = INVLOC_HAND_LEFT; - } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { - // We have an item in the right hand but nothing in the left, so let the common code - // take care of unequipping whatever is held in the right hand. The auto-equip code - // picks the most appropriate location for the item type (which in this case will be - // the left hand), invloc isn't used there. - invloc = INVLOC_HAND_RIGHT; - } else { - // Both hands are holding items, we must unequip one of the items and check that there's - // space for the other before trying to auto-equip - inv_body_loc mainHand = INVLOC_HAND_LEFT; - inv_body_loc offHand = INVLOC_HAND_RIGHT; - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - // No space to move right hand item to inventory, can we move the left instead? - std::swap(mainHand, offHand); - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - break; - } - } - if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { - // No space for the main hand item. Move the other item back to the off hand and abort. - player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; - player.RemoveInvItem(player._pNumInv - 1, false); - break; - } - RemoveEquipment(player, offHand, false); - invloc = mainHand; - } - break; - default: - // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. - failedSpeech = HeroSpeech::ICantDoThat; - break; - } - // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip - if (invloc != NUM_INVLOC - && !player.InvBody[invloc].isEmpty() - && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { - holdItem = player.InvBody[invloc].pop(); - } - automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; - player.RemoveInvItem(iv, false); - - // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv - if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { - holdItem.clear(); - } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. - } else if (!holdItem.isEmpty()) { - // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... - player.InvBody[invloc] = holdItem.pop(); - } - } - } else if (IsVisualStoreOpen && CanSellToCurrentVendor(player.InvList[iv]) && dropItem) { - // If visual store is open, ctrl-click sells the item - SellItemToVisualStore(iv); - automaticallyMoved = true; - } else { - holdItem = player.InvList[iv]; - player.RemoveInvItem(iv, false); - } - } - } - - if (r >= SLOTXY_BELT_FIRST) { - const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; - if (!beltItem.isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = beltItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } - } - } - - if (!holdItem.isEmpty()) { - if (holdItem._itype == ItemType::Gold) { - player._pGold = CalculateGold(player); - } - - CalcPlrInv(player, true); - holdItem._iStatFlag = player.CanUseItem(holdItem); - - if (&player == MyPlayer) { - PlaySFX(SfxID::GrabItem); - NewCursor(holdItem); - } - if (dropItem) { - TryDropItem(); - } - } else if (automaticMove) { - if (automaticallyMoved) { - CalcPlrInv(player, true); - } - if (attemptedMove && &player == MyPlayer) { - if (automaticallyMoved) { - PlaySFX(successSound); - } else { - player.SaySpecific(failedSpeech); - } - } - } -} - -void TryCombineNaKrulNotes(Player &player, Item ¬eItem) -{ - const int idx = noteItem.IDidx; - const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; - - if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { - return; - } - - for (const _item_indexes note : notes) { - if (idx != note && !HasInventoryItemWithId(player, note)) { - return; // the player doesn't have all notes - } - } - - MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - - for (const _item_indexes note : notes) { - if (idx != note) { - RemoveInventoryItemById(player, note); - } - } - - const Point position = noteItem.position; // copy the position to restore it after re-initialising the item - noteItem = {}; - GetItemAttrs(noteItem, IDI_FULLNOTE, 16); - SetupItem(noteItem); - noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table -} - -void CheckQuestItem(Player &player, Item &questItem) -{ - const Player &myPlayer = *MyPlayer; - - if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE - && (questItem.IDidx == IDI_OPTAMULET - || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { - Quests[Q_BLIND]._qactive = QUEST_DONE; - NetSendCmdQuest(true, Quests[Q_BLIND]); - } - - if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { - player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP - Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; - NetSendCmdQuest(true, Quests[Q_MUSHROOM]); - } - - if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { - Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ANVIL]); - } - if (Quests[Q_ANVIL]._qlog) { - myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); - } - } - - if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { - myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); - } - - if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ROCK]._qactive == QUEST_INIT) { - Quests[Q_ROCK]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ROCK]); - } - if (Quests[Q_ROCK]._qlog) { - myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); - } - } - - if (Quests[Q_BLOOD]._qactive == QUEST_ACTIVE - && (questItem.IDidx == IDI_ARMOFVAL - || (Quests[Q_BLOOD].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 9, 3 })))) { - Quests[Q_BLOOD]._qactive = QUEST_DONE; - NetSendCmdQuest(true, Quests[Q_BLOOD]); - myPlayer.Say(HeroSpeech::MayTheSpiritOfArkaineProtectMe, 20); - } - - if (questItem.IDidx == IDI_MAPOFDOOM) { - Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; - if (Quests[Q_GRAVE]._qvar1 != 1) { - MyPlayer->Say(HeroSpeech::UhHuh, 10); - Quests[Q_GRAVE]._qvar1 = 1; - } - } - - TryCombineNaKrulNotes(player, questItem); -} - -void CleanupItems(int ii) -{ - const Item &item = Items[ii]; - dItem[item.position.x][item.position.y] = 0; - - if (CornerStone.isAvailable() && item.position == CornerStone.position) { - CornerStone.item.clear(); - CornerStone.item.selectionRegion = SelectionRegion::None; - CornerStone.item.position = { 0, 0 }; - CornerStone.item._iAnimFlag = false; - CornerStone.item._iIdentified = false; - CornerStone.item._iPostDraw = false; - } - - int i = 0; - while (i < ActiveItemCount) { - if (ActiveItems[i] == ii) { - DeleteItem(i); - i = 0; - continue; - } - - i++; - } -} - -bool CanUseStaff(Item &staff, SpellID spell) -{ - return !staff.isEmpty() - && IsAnyOf(staff._iMiscId, IMISC_STAFF, IMISC_UNIQUE) - && staff._iSpell == spell - && staff._iCharges > 0; -} - -void StartGoldDrop() -{ - CloseGoldWithdraw(); - - const int8_t invIndex = pcursinvitem; - - const Player &myPlayer = *MyPlayer; - - const int max = (invIndex <= INVITEM_INV_LAST) - ? myPlayer.InvList[invIndex - INVITEM_INV_FIRST]._ivalue - : myPlayer.SpdList[invIndex - INVITEM_BELT_FIRST]._ivalue; - - if (ChatFlag) - ResetChat(); - - const Point start = GetPanelPosition(UiPanels::Inventory, { 67, 128 }); - SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); - SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); - OpenGoldDrop(invIndex, max); -} - -int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) -{ - if (player.InvGrid[slotIndex] != 0) { - return value; - } - - Item &goldItem = player.InvList[player._pNumInv]; - MakeGoldStack(goldItem, std::min(value, MaxGold)); - player._pNumInv++; - player.InvGrid[slotIndex] = player._pNumInv; - if (&player == MyPlayer) { - NetSendCmdChInvItem(false, slotIndex); - } - - value -= goldItem._ivalue; - - return value; -} - -} // namespace - -void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) -{ - SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); - out.Clip(&srcRect, &targetPosition); - if (size.width <= 0 || size.height <= 0) - return; - - uint8_t colorShift; - switch (itemQuality) { - case ITEM_QUALITY_MAGIC: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BLUE : PAL16_ORANGE) - 1; - break; - case ITEM_QUALITY_UNIQUE: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_YELLOW : PAL16_ORANGE) - 1; - break; - default: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BEIGE : PAL16_ORANGE) - 1; - break; - } - - uint8_t *dst = &out[targetPosition]; - const auto dstPitch = out.pitch(); - for (int y = size.height; y != 0; --y, dst -= dstPitch + size.width) { - for (const uint8_t *end = dst + size.width; dst < end; ++dst) { - uint8_t &pix = *dst; - if (pix >= PAL16_GRAY) { - pix -= colorShift; - } - } - } -} - -bool CanBePlacedOnBelt(const Player &player, const Item &item) -{ - return FitsInBeltSlot(item) - && item._itype != ItemType::Gold - && player.CanUseItem(item) - && item.isUsable(); -} - -void FreeInvGFX() -{ - pInvCels = std::nullopt; -} - -void InitInv() -{ - const PlayerData &playerClassData = GetPlayerDataForClass(MyPlayer->_pClass); - const char *invName = playerClassData.inv.c_str(); - if (gbIsSpawn && (playerClassData.inv == "inv_rog" || playerClassData.inv == "inv_sor")) { - invName = "inv"; - } - pInvCels = LoadCel(StrCat("data\\inv\\", invName).c_str(), static_cast(SidePanelSize.width)); -} - -void DrawInv(const Surface &out) -{ - ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 0, 351 }), (*pInvCels)[0]); - - const Size slotSize[] = { - { 2, 2 }, // head - { 1, 1 }, // left ring - { 1, 1 }, // right ring - { 1, 1 }, // amulet - { 2, 3 }, // left hand - { 2, 3 }, // right hand - { 2, 3 }, // chest - }; - - const Point slotPos[] = { - { 133, 59 }, // head - { 48, 205 }, // left ring - { 249, 205 }, // right ring - { 205, 60 }, // amulet - { 17, 160 }, // left hand - { 248, 160 }, // right hand - { 133, 160 }, // chest - }; - - const Player &myPlayer = *InspectPlayer; - - for (int slot = INVLOC_HEAD; slot < NUM_INVLOC; slot++) { - if (!myPlayer.InvBody[slot].isEmpty()) { - int screenX = slotPos[slot].x; - int screenY = slotPos[slot].y; - InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, { screenX, screenY }), { slotSize[slot].width * InventorySlotSizeInPixels.width, slotSize[slot].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); - - const int cursId = myPlayer.InvBody[slot]._iCurs + CURSOR_FIRSTITEM; - - const Size frameSize = GetInvItemSize(cursId); - - // calc item offsets for weapons/armor smaller than 2x3 slots - if (IsAnyOf(slot, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST)) { - screenX += frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0; - screenY += frameSize.height == (3 * InventorySlotSizeInPixels.height) ? 0 : -INV_SLOT_HALF_SIZE_PX; - } - - const ClxSprite sprite = GetInvItemSprite(cursId); - const Point position = GetPanelPosition(UiPanels::Inventory, { screenX, screenY }); - - if (pcursinvitem == slot) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.InvBody[slot], true), position, sprite); - } - - DrawItem(myPlayer.InvBody[slot], out, position, sprite); - - if (slot == INVLOC_HAND_LEFT) { - if (myPlayer.GetItemLocation(myPlayer.InvBody[slot]) == ILOC_TWOHAND) { - InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, slotPos[INVLOC_HAND_RIGHT]), { slotSize[INVLOC_HAND_RIGHT].width * InventorySlotSizeInPixels.width, slotSize[INVLOC_HAND_RIGHT].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); - const int dstX = GetRightPanel().position.x + slotPos[INVLOC_HAND_RIGHT].x + (frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0) - 1; - const int dstY = GetRightPanel().position.y + slotPos[INVLOC_HAND_RIGHT].y; - ClxDrawBlended(out, { dstX, dstY }, sprite); - } - } - } - } - - for (int i = 0; i < InventoryGridCells; i++) { - if (myPlayer.InvGrid[i] != 0) { - InvDrawSlotBack( - out, - GetPanelPosition(UiPanels::Inventory, InvRect[i + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }, - InventorySlotSizeInPixels, - myPlayer.InvList[std::abs(myPlayer.InvGrid[i]) - 1]._iMagical); - } - } - - for (int j = 0; j < InventoryGridCells; j++) { - if (myPlayer.InvGrid[j] > 0) { // first slot of an item - const int ii = myPlayer.InvGrid[j] - 1; - const int cursId = myPlayer.InvList[ii]._iCurs + CURSOR_FIRSTITEM; - - const ClxSprite sprite = GetInvItemSprite(cursId); - const Point position = GetPanelPosition(UiPanels::Inventory, InvRect[j + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }; - if (pcursinvitem == ii + INVITEM_INV_FIRST) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.InvList[ii], true), position, sprite); - } - - DrawItem(myPlayer.InvList[ii], out, position, sprite); - } - } -} - -void DrawInvBelt(const Surface &out) -{ - if (ChatFlag) { - return; - } - - const Point mainPanelPosition = GetMainPanel().position; - - DrawPanelBox(out, { 205, 21, 232, 28 }, mainPanelPosition + Displacement { 205, 5 }); - - const Player &myPlayer = *InspectPlayer; - - for (int i = 0; i < MaxBeltItems; i++) { - if (myPlayer.SpdList[i].isEmpty()) { - continue; - } - - const Point position { InvRect[i + SLOTXY_BELT_FIRST].position.x + mainPanelPosition.x, InvRect[i + SLOTXY_BELT_FIRST].position.y + mainPanelPosition.y + InventorySlotSizeInPixels.height }; - InvDrawSlotBack(out, position, InventorySlotSizeInPixels, myPlayer.SpdList[i]._iMagical); - const int cursId = myPlayer.SpdList[i]._iCurs + CURSOR_FIRSTITEM; - - const ClxSprite sprite = GetInvItemSprite(cursId); - - if (pcursinvitem == i + INVITEM_BELT_FIRST) { - if (ControlMode == ControlTypes::KeyboardAndMouse || invflag) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.SpdList[i], true), position, sprite); - } - } - - DrawItem(myPlayer.SpdList[i], out, position, sprite); - - if (myPlayer.SpdList[i].isUsable() - && myPlayer.SpdList[i]._itype != ItemType::Gold) { - auto beltKey = StrCat("BeltItem", i + 1); - std::string_view keyName = ControlMode == ControlTypes::Gamepad - ? GetOptions().Padmapper.InputNameForAction(beltKey, true) - : GetOptions().Keymapper.KeyNameForAction(beltKey); - - if (keyName.length() > 2) - keyName = {}; - - DrawString(out, keyName, { position - Displacement { 0, 12 }, InventorySlotSizeInPixels }, - { .flags = UiFlags::ColorWhite | UiFlags::AlignRight }); - } - } -} - -void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri) -{ - if (&player == MyPlayer) { - NetSendCmdDelItem(hiPri, bodyLocation); - } - - player.InvBody[bodyLocation].clear(); -} - -bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) -{ - if (!CanBePlacedOnBelt(player, item)) { - return false; - } - - for (Item &beltItem : player.SpdList) { - if (beltItem.isEmpty()) { - if (persistItem) { - beltItem = item; - player.CalcScrolls(); - RedrawComponent(PanelDrawComponent::Belt); - if (sendNetworkMessage) { - const auto beltIndex = static_cast(std::distance(&player.SpdList[0], &beltItem)); - NetSendCmdChBeltItem(false, beltIndex); - } - } - - return true; - } - } - - return false; -} - -bool AutoEquip(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) -{ - if (!CanEquip(item)) { - return false; - } - - for (int bodyLocation = INVLOC_HEAD; bodyLocation < NUM_INVLOC; bodyLocation++) { - if (AutoEquip(player, item, (inv_body_loc)bodyLocation, persistItem, sendNetworkMessage)) { - return true; - } - } - - return false; -} - -bool AutoEquipEnabled(const Player &player, const Item &item) -{ - if (item.isWeapon()) { - // Monk can use unarmed attack as an encouraged option, thus we do not automatically equip weapons on him so as to not - // annoy players who prefer that playstyle. - return player._pClass != HeroClass::Monk && *GetOptions().Gameplay.autoEquipWeapons; - } - - if (item.isArmor()) { - return *GetOptions().Gameplay.autoEquipArmor; - } - - if (item.isHelm()) { - return *GetOptions().Gameplay.autoEquipHelms; - } - - if (item.isShield()) { - return *GetOptions().Gameplay.autoEquipShields; - } - - if (item.isJewelry()) { - return *GetOptions().Gameplay.autoEquipJewelry; - } - - return true; -} - -bool CanFitItemInInventory(const Player &player, const Item &item) -{ - return static_cast(FindSlotForItem(player, GetInventorySize(item))); -} - -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) -{ - const Size itemSize = GetInventorySize(item); - std::optional targetSlot = FindSlotForItem(player, itemSize); - - if (targetSlot) { - player.InvList[player._pNumInv] = item; - player._pNumInv++; - - AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); - player.CalcScrolls(); - - return true; - } - - return false; -} - -std::vector SortItemsBySize(Player &player) -{ - std::vector> itemSizes; // Pair of item size and its index in InvList - itemSizes.reserve(player._pNumInv); // Reserves space for the number of items in the player's inventory - - for (int i = 0; i < player._pNumInv; i++) { - const Size size = GetInventorySize(player.InvList[i]); - itemSizes.emplace_back(size, i); - } - - // Sort items by height first, then by width - std::sort(itemSizes.begin(), itemSizes.end(), [](const auto &a, const auto &b) { - if (a.first.height == b.first.height) return a.first.width > b.first.width; - return a.first.height > b.first.height; - }); - - // Extract sorted indices - std::vector sortedIndices; - sortedIndices.reserve(itemSizes.size()); // Pre-allocate the necessary capacity - - for (const auto &itemSize : itemSizes) { - sortedIndices.push_back(itemSize.second); - } - - return sortedIndices; -} - -void ReorganizeInventory(Player &player) -{ - // Sort items by size - const std::vector sortedIndices = SortItemsBySize(player); - - // Temporary storage for items and a copy of InvGrid - std::vector tempStorage(player._pNumInv); - std::array originalInvGrid; // Declare an array for InvGrid copy - std::copy(std::begin(player.InvGrid), std::end(player.InvGrid), std::begin(originalInvGrid)); // Copy InvGrid to originalInvGrid - - // Move items to temporary storage and clear inventory slots - for (int i = 0; i < player._pNumInv; ++i) { - tempStorage[i] = player.InvList[i]; - player.InvList[i] = {}; - } - player._pNumInv = 0; // Reset inventory count - std::fill(std::begin(player.InvGrid), std::end(player.InvGrid), 0); // Clear InvGrid - - // Attempt to place items back, now from the temp storage - bool reorganizationFailed = false; - for (const int index : sortedIndices) { - const Item &item = tempStorage[index]; - if (!AutoPlaceItemInInventory(player, item, false)) { - reorganizationFailed = true; - break; - } - } - - // If reorganization failed, restore items and InvGrid from tempStorage and originalInvGrid - if (reorganizationFailed) { - for (const Item &item : tempStorage) { - if (!item.isEmpty()) { - player.InvList[player._pNumInv++] = item; - } - } - std::copy(std::begin(originalInvGrid), std::end(originalInvGrid), std::begin(player.InvGrid)); // Restore InvGrid - } -} - -int RoomForGold() -{ - int amount = 0; - for (const int8_t &itemIndex : MyPlayer->InvGrid) { - if (itemIndex < 0) { - continue; - } - if (itemIndex == 0) { - amount += MaxGold; - continue; - } - - const Item &goldItem = MyPlayer->InvList[itemIndex - 1]; - if (goldItem._itype != ItemType::Gold || goldItem._ivalue == MaxGold) { - continue; - } - - amount += MaxGold - goldItem._ivalue; - } - - return amount; -} - -int AddGoldToInventory(Player &player, int value) -{ - // Top off existing piles - for (int i = 0; i < player._pNumInv && value > 0; i++) { - Item &goldItem = player.InvList[i]; - if (goldItem._itype != ItemType::Gold || goldItem._ivalue >= MaxGold) { - continue; - } - - if (goldItem._ivalue + value > MaxGold) { - value -= MaxGold - goldItem._ivalue; - goldItem._ivalue = MaxGold; - } else { - goldItem._ivalue += value; - value = 0; - } - - NetSyncInvItem(player, i); - SetPlrHandGoldCurs(goldItem); - } - - // Last row right to left - for (int i = 39; i >= 30 && value > 0; i--) { - value = CreateGoldItemInInventorySlot(player, i, value); - } - - // Remaining inventory in columns, bottom to top, right to left - for (int x = 9; x >= 0 && value > 0; x--) { - for (int y = 2; y >= 0 && value > 0; y--) { - value = CreateGoldItemInInventorySlot(player, (10 * y) + x, value); - } - } - - return value; -} - -bool GoldAutoPlace(Player &player, Item &goldStack) -{ - goldStack._ivalue = AddGoldToInventory(player, goldStack._ivalue); - SetPlrHandGoldCurs(goldStack); - - player._pGold = CalculateGold(player); - - return goldStack._ivalue == 0; -} - -void CheckInvSwap(Player &player, inv_body_loc bLoc) -{ - const Item &item = player.InvBody[bLoc]; - - if (bLoc == INVLOC_HAND_LEFT && player.GetItemLocation(item) == ILOC_TWOHAND) { - player.InvBody[INVLOC_HAND_RIGHT].clear(); - } else if (bLoc == INVLOC_HAND_RIGHT && player.GetItemLocation(item) == ILOC_TWOHAND) { - player.InvBody[INVLOC_HAND_LEFT].clear(); - } - - CalcPlrInv(player, true); -} - -void inv_update_rem_item(Player &player, inv_body_loc iv) -{ - player.InvBody[iv].clear(); - - CalcPlrInv(player, player._pmode != PM_DEATH); -} - -void CheckInvSwap(Player &player, const Item &item, int invGridIndex) -{ - Size itemSize = GetInventorySize(item); - - const int pitch = 10; - const int invListIndex = [&]() -> int { - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - const int gridIndex = rowGridIndex + x; - if (player.InvGrid[gridIndex] != 0) - return std::abs(player.InvGrid[gridIndex]); - } - } - player._pNumInv++; - return player._pNumInv; - }(); - - if (invListIndex < player._pNumInv) { - for (int8_t &itemIndex : player.InvGrid) { - if (itemIndex == invListIndex) - itemIndex = 0; - if (itemIndex == -invListIndex) - itemIndex = 0; - } - } - - player.InvList[invListIndex - 1] = item; - - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - if (x == 0 && y == itemSize.height - 1) - player.InvGrid[rowGridIndex + x] = invListIndex; - else - player.InvGrid[rowGridIndex + x] = -invListIndex; - } - } - - CalcPlrInv(player, true); -} - -void CheckInvRemove(Player &player, int invGridIndex) -{ - const int invListIndex = std::abs(player.InvGrid[invGridIndex]) - 1; - - if (invListIndex >= 0) { - player.RemoveInvItem(invListIndex); - } -} - -void TransferItemToStash(Player &player, int location) -{ - if (location == -1) { - return; - } - - const Item &item = GetInventoryItem(player, location); - if (!AutoPlaceItemInStash(item, true)) { - player.SaySpecific(HeroSpeech::WhereWouldIPutThis); - return; - } - - PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); - - if (location < INVITEM_INV_FIRST) { - RemoveEquipment(player, static_cast(location), false); - CalcPlrInv(player, true); - } else if (location <= INVITEM_INV_LAST) - player.RemoveInvItem(location - INVITEM_INV_FIRST); - else - player.RemoveSpdBarItem(location - INVITEM_BELT_FIRST); -} - -void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) -{ - if (IsInspectingPlayer()) - return; - if (!MyPlayer->HoldItem.isEmpty()) { - CheckInvPaste(*MyPlayer, MousePosition); - } else if (IsStashOpen && isCtrlHeld) { - TransferItemToStash(*MyPlayer, pcursinvitem); - } else { - CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); - } -} - -void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld) -{ - const Point mainPanelPosition = GetMainPanel().position; - if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x - && MousePosition.y > mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) { - CheckInvItem(isShiftHeld, isCtrlHeld); - } -} - -void InvGetItem(Player &player, int ii) -{ - Item &item = Items[ii]; - CloseGoldDrop(); - - if (dItem[item.position.x][item.position.y] == 0) - return; - - item._iCreateInfo &= ~CF_PREGEN; - CheckQuestItem(player, item); - item.updateRequiredStatsCacheForPlayer(player); - - if (item._itype == ItemType::Gold && GoldAutoPlace(player, item)) { - if (MyPlayer == &player) { - // Non-gold items (or gold when you have a full inventory) go to the hand then provide audible feedback on - // paste. To give the same feedback for auto-placed gold we play the sound effect now. - PlaySFX(SfxID::ItemGold); - } - } else { - // The item needs to go into the players hand - if (MyPlayer == &player && !player.HoldItem.isEmpty()) { - // drop whatever the player is currently holding - NetSendCmdPItem(true, CMD_SYNCPUTITEM, player.position.tile, player.HoldItem); - } - - // need to copy here instead of move so CleanupItems still has access to the position - player.HoldItem = item; - NewCursor(player.HoldItem); - } - - // This potentially moves items in memory so must be done after we've made a copy - CleanupItems(ii); - pcursitem = -1; -} - -std::optional FindAdjacentPositionForItem(Point origin, Direction facing) -{ - if (ActiveItemCount >= MAXITEMS) - return {}; - - if (CanPut(origin + facing)) - return origin + facing; - - if (CanPut(origin + Left(facing))) - return origin + Left(facing); - - if (CanPut(origin + Right(facing))) - return origin + Right(facing); - - if (CanPut(origin + Left(Left(facing)))) - return origin + Left(Left(facing)); - - if (CanPut(origin + Right(Right(facing)))) - return origin + Right(Right(facing)); - - if (CanPut(origin + Left(Left(Left(facing))))) - return origin + Left(Left(Left(facing))); - - if (CanPut(origin + Right(Right(Right(facing))))) - return origin + Right(Right(Right(facing))); - - if (CanPut(origin + Opposite(facing))) - return origin + Opposite(facing); - - if (CanPut(origin)) - return origin; - - return {}; -} - -void AutoGetItem(Player &player, Item *itemPointer, int ii) -{ - Item &item = *itemPointer; - - CloseGoldDrop(); - - if (dItem[item.position.x][item.position.y] == 0) - return; - - item._iCreateInfo &= ~CF_PREGEN; - CheckQuestItem(player, item); - item.updateRequiredStatsCacheForPlayer(player); - - bool done; - bool autoEquipped = false; - - if (item._itype == ItemType::Gold) { - done = GoldAutoPlace(player, item); - if (!done) { - SetPlrHandGoldCurs(item); - } - } else { - done = AutoEquipEnabled(player, item) && AutoEquip(player, item, true, &player == MyPlayer); - if (done) { - autoEquipped = true; - } - - if (!done) { - done = AutoPlaceItemInBelt(player, item, true, &player == MyPlayer); - } - if (!done) { - done = AutoPlaceItemInInventory(player, item, &player == MyPlayer); - } - } - - if (done) { - if (!autoEquipped && *GetOptions().Audio.itemPickupSound && &player == MyPlayer) { - PlaySFX(SfxID::GrabItem); - } - - CleanupItems(ii); - return; - } - - if (&player == MyPlayer) { - player.Say(HeroSpeech::ICantCarryAnymore); - } - RespawnItem(item, true); - NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); -} - -int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t createInfo) -{ - for (uint8_t i = 0; i < ActiveItemCount; i++) { - const Item &item = Items[ActiveItems[i]]; - if (item.keyAttributesMatch(iseed, idx, createInfo)) { - return i; - } - } - - return -1; -} - -void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci) -{ - // Check what the local client has at the target position - int ii = dItem[position.x][position.y] - 1; - - if (ii >= 0 && ii < MAXITEMS) { - // If there was an item there, check that it's the same item as the remote player has - if (!Items[ii].keyAttributesMatch(iseed, idx, ci)) { - // Key attributes don't match so we must've desynced, ignore this index and try find a matching item via lookup - ii = -1; - } - } - - if (ii == -1) { - // Either there's no item at the expected position or it doesn't match what is being picked up, so look for an item that matches the key attributes - ii = FindGetItem(iseed, idx, ci); - - if (ii != -1) { - // Translate to Items index for CleanupItems, FindGetItem returns an ActiveItems index - ii = ActiveItems[ii]; - } - } - - if (ii == -1) { - // Still can't find the expected item, assume it was collected earlier and this caused the desync - return; - } - - CleanupItems(ii); -} - -bool CanPut(Point position) -{ - if (!InDungeonBounds(position)) { - return false; - } - - if (IsTileSolid(position)) { - return false; - } - - if (dItem[position.x][position.y] != 0) { - return false; - } - - if (leveltype == DTYPE_TOWN) { - if (dMonster[position.x][position.y] != 0) { - return false; - } - if (dMonster[position.x + 1][position.y + 1] != 0) { - return false; - } - } - - if (IsItemBlockingObjectAtPosition(position)) { - return false; - } - - return true; -} - -int ClampDurability(const Item &item, int durability) -{ - if (item._iMaxDur == 0) - return 0; - - return std::clamp(durability, 1, item._iMaxDur); -} - -int16_t ClampToHit(const Item &item, int16_t toHit) -{ - if (toHit < item._iPLToHit || toHit > 51) - return item._iPLToHit; - - return toHit; -} - -uint8_t ClampMaxDam(const Item &item, uint8_t maxDam) -{ - if (maxDam < item._iMaxDam || maxDam - item._iMinDam > 30) - return item._iMaxDam; - - return maxDam; -} - -int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam) -{ - if (ActiveItemCount >= MAXITEMS) - return -1; - - Item item; - - RecreateItem(*MyPlayer, item, idx, icreateinfo, iseed, ivalue, ibuff); - if (id != 0) - item._iIdentified = true; - item._iMaxDur = mdur; - item._iDurability = ClampDurability(item, dur); - item._iMaxCharges = std::clamp(mch, 0, item._iMaxCharges); - item._iCharges = std::clamp(ch, 0, item._iMaxCharges); - if (gbIsHellfire) { - item._iPLToHit = ClampToHit(item, toHit); - item._iMaxDam = ClampMaxDam(item, maxDam); - } - - return PlaceItemInWorld(std::move(item), position); -} - -int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname) -{ - if (ActiveItemCount >= MAXITEMS) - return -1; - - Item item; - RecreateEar(item, icreateinfo, iseed, cursval, heroname); - - return PlaceItemInWorld(std::move(item), position); -} - -int8_t CheckInvHLight() -{ - int8_t r = 0; - for (; r < NUM_XY_SLOTS; r++) { - int xo = GetRightPanel().position.x; - int yo = GetRightPanel().position.y; - if (r >= SLOTXY_BELT_FIRST) { - xo = GetMainPanel().position.x; - yo = GetMainPanel().position.y; - } - - if (InvRect[r].contains(MousePosition - Displacement(xo, yo))) { - break; - } - } - - if (r >= NUM_XY_SLOTS) - return -1; - - int8_t rv = -1; - InfoColor = UiFlags::ColorWhite; - Item *pi = nullptr; - Player &myPlayer = *InspectPlayer; - - if (r == SLOTXY_HEAD) { - rv = INVLOC_HEAD; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_RING_LEFT) { - rv = INVLOC_RING_LEFT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_RING_RIGHT) { - rv = INVLOC_RING_RIGHT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_AMULET) { - rv = INVLOC_AMULET; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_HAND_LEFT) { - rv = INVLOC_HAND_LEFT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_HAND_RIGHT) { - pi = &myPlayer.InvBody[INVLOC_HAND_LEFT]; - if (pi->isEmpty() || myPlayer.GetItemLocation(*pi) != ILOC_TWOHAND) { - rv = INVLOC_HAND_RIGHT; - pi = &myPlayer.InvBody[rv]; - } else { - rv = INVLOC_HAND_LEFT; - } - } else if (r == SLOTXY_CHEST) { - rv = INVLOC_CHEST; - pi = &myPlayer.InvBody[rv]; - } else if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { - const int8_t itemId = std::abs(myPlayer.InvGrid[r - SLOTXY_INV_FIRST]); - if (itemId == 0) - return -1; - const int ii = itemId - 1; - rv = ii + INVITEM_INV_FIRST; - pi = &myPlayer.InvList[ii]; - } else if (r >= SLOTXY_BELT_FIRST) { - r -= SLOTXY_BELT_FIRST; - RedrawComponent(PanelDrawComponent::Belt); - pi = &myPlayer.SpdList[r]; - if (pi->isEmpty()) - return -1; - rv = r + INVITEM_BELT_FIRST; - } - - if (pi->isEmpty()) - return -1; - - if (IsVisualStoreOpen && pcurs == CURSOR_REPAIR) { - InfoColor = pi->getTextColor(); - InfoString = pi->getName(); - FloatingInfoString = pi->getName(); - if (pi->_iIdentified) { - PrintItemDetails(*pi); - } else { - PrintItemDur(*pi); - } - int cost = GetRepairCost(*pi); - if (cost > 0) - AddInfoBoxString(StrCat(FormatInteger(cost), " Gold")); - else - AddInfoBoxString(_("Fully Repaired")); - } else if (pi->_itype == ItemType::Gold) { - const int nGold = pi->_ivalue; - InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); - FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); - } else { - InfoColor = pi->getTextColor(); - InfoString = pi->getName(); - FloatingInfoString = pi->getName(); - if (pi->_iIdentified) { - PrintItemDetails(*pi); - } else { - PrintItemDur(*pi); - } - } - - return rv; -} - -void ConsumeScroll(Player &player) -{ - const SpellID spellId = player.executedSpell.spellId; - - const auto isCurrentSpell = [spellId](const Item &item) -> bool { - return item.isScrollOf(spellId) || item.isRuneOf(spellId); - }; - - // Try to remove the scroll from selected inventory slot - const int8_t itemSlot = player.executedSpell.spellFrom; - if (itemSlot >= INVITEM_INV_FIRST && itemSlot <= INVITEM_INV_LAST) { - const int itemIndex = itemSlot - INVITEM_INV_FIRST; - const Item *item = &player.InvList[itemIndex]; - if (!item->isEmpty() && isCurrentSpell(*item)) { - player.RemoveInvItem(itemIndex); - return; - } - } else if (itemSlot >= INVITEM_BELT_FIRST && itemSlot <= INVITEM_BELT_LAST) { - const int itemIndex = itemSlot - INVITEM_BELT_FIRST; - const Item *item = &player.SpdList[itemIndex]; - if (!item->isEmpty() && isCurrentSpell(*item)) { - player.RemoveSpdBarItem(itemIndex); - return; - } - } else if (itemSlot != 0) { - app_fatal(StrCat("ConsumeScroll: Invalid item index ", itemSlot)); - } - - // Didn't find it at the selected slot, take the first one we find - // This path is always used when the scroll is consumed via spell selection - RemoveInventoryOrBeltItem(player, isCurrentSpell); -} - -bool CanUseScroll(Player &player, SpellID spell) -{ - if (leveltype == DTYPE_TOWN && !GetSpellData(spell).isAllowedInTown()) - return false; - - return HasInventoryOrBeltItem(player, [spell](const Item &item) { - return item.isScrollOf(spell) || item.isRuneOf(spell); - }); -} - -void ConsumeStaffCharge(Player &player) -{ - Item &staff = player.InvBody[INVLOC_HAND_LEFT]; - - if (!CanUseStaff(staff, player.executedSpell.spellId)) - return; - - staff._iCharges--; - CalcPlrInv(player, false); -} - -bool CanUseStaff(Player &player, SpellID spellId) -{ - return CanUseStaff(player.InvBody[INVLOC_HAND_LEFT], spellId); -} - -Item &GetInventoryItem(Player &player, int location) -{ - if (location < INVITEM_INV_FIRST) - return player.InvBody[location]; - - if (location <= INVITEM_INV_LAST) - return player.InvList[location - INVITEM_INV_FIRST]; - - return player.SpdList[location - INVITEM_BELT_FIRST]; -} - -bool UseInvItem(int cii) -{ - if (IsInspectingPlayer()) - return false; - - Player &player = *MyPlayer; - - if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) - return true; - if (pcurs != CURSOR_HAND) - return true; - if (IsPlayerInStore()) - return true; - if (cii < INVITEM_INV_FIRST) - return false; - - bool speedlist = false; - int c; - Item *item; - if (cii <= INVITEM_INV_LAST) { - c = cii - INVITEM_INV_FIRST; - item = &player.InvList[c]; - } else { - if (ChatFlag) - return true; - c = cii - INVITEM_BELT_FIRST; - - item = &player.SpdList[c]; - speedlist = true; - - // If selected speedlist item exists in InvList, use the InvList item. - for (int i = 0; i < player._pNumInv && *GetOptions().Gameplay.autoRefillBelt; i++) { - if (player.InvList[i]._iMiscId == item->_iMiscId && player.InvList[i]._iSpell == item->_iSpell) { - c = i; - item = &player.InvList[c]; - cii = c + INVITEM_INV_FIRST; - speedlist = false; - break; - } - } - - // If speedlist item is not inventory, use same item at the end of the speedlist if exists. - if (speedlist && *GetOptions().Gameplay.autoRefillBelt) { - for (int i = INVITEM_BELT_LAST - INVITEM_BELT_FIRST; i > c; i--) { - Item &candidate = player.SpdList[i]; - - if (!candidate.isEmpty() && candidate._iMiscId == item->_iMiscId && candidate._iSpell == item->_iSpell) { - c = i; - cii = c + INVITEM_BELT_FIRST; - item = &candidate; - break; - } - } - } - } - - constexpr int SpeechDelay = 10; - if (item->IDidx == IDI_MUSHROOM) { - player.Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); - return true; - } - if (item->IDidx == IDI_FUNGALTM) { - - PlaySFX(SfxID::ItemBook); - player.Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); - return true; - } - - if (player.isOnLevel(0)) { - if (UseItemOpensHive(*item, player.position.tile)) { - OpenHive(); - player.RemoveInvItem(c); - return true; - } - if (UseItemOpensGrave(*item, player.position.tile)) { - OpenGrave(); - player.RemoveInvItem(c); - return true; - } - } - - if (!item->isUsable()) - return false; - - if (!player.CanUseItem(*item)) { - player.Say(HeroSpeech::ICantUseThisYet); - return true; - } - - if (item->_iMiscId == IMISC_NONE && item->_itype == ItemType::Gold) { - StartGoldDrop(); - return true; - } - - CloseGoldDrop(); - - if (item->isScroll() && leveltype == DTYPE_TOWN && !GetSpellData(item->_iSpell).isAllowedInTown()) { - return true; - } - - if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { - return true; - } - - if (item->_iMiscId == IMISC_ARENAPOT && !player.isOnArenaLevel()) { - player.Say(HeroSpeech::ThatWontWorkHere); - return true; - } - - const int idata = ItemCAnimTbl[item->_iCurs]; - if (item->_iMiscId == IMISC_BOOK) - PlaySFX(SfxID::ReadBook); - else if (&player == MyPlayer) - PlaySFX(ItemInvSnds[idata]); - - UseItem(player, item->_iMiscId, item->_iSpell, cii); - - if (speedlist) { - if (player.SpdList[c]._iMiscId == IMISC_NOTE) { - InitQTextMsg(TEXT_BOOK9); - CloseInventory(); - return true; - } - if (!item->isScroll() && !item->isRune()) - player.RemoveSpdBarItem(c); - return true; - } - if (player.InvList[c]._iMiscId == IMISC_MAPOFDOOM) - return true; - if (player.InvList[c]._iMiscId == IMISC_NOTE) { - InitQTextMsg(TEXT_BOOK9); - CloseInventory(); - return true; - } - if (!item->isScroll() && !item->isRune()) - player.RemoveInvItem(c); - - return true; -} - -void CloseInventory() -{ - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); - invflag = false; -} - -void CloseStash() -{ - if (!IsStashOpen) - return; - - Player &myPlayer = *MyPlayer; - if (!myPlayer.HoldItem.isEmpty()) { - std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); - if (itemTile) { - NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); - } else { - if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) - && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true) - && !AutoPlaceItemInStash(myPlayer.HoldItem, true)) { - // This can fail for max gold, arena potions and a stash that has been arranged - // to not have room for the item all 3 cases are extremely unlikely - app_fatal(_("No room for item")); - } - PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); - } - myPlayer.HoldItem.clear(); - NewCursor(CURSOR_HAND); - } - - IsStashOpen = false; -} - -void DoTelekinesis() -{ - if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) - NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJT, cursPosition); - if (pcursitem != -1) - NetSendCmdGItem(true, CMD_REQUESTAGITEM, *MyPlayer, pcursitem); - if (pcursmonst != -1) { - const Monster &monter = Monsters[pcursmonst]; - if (!M_Talker(monter) && monter.talkMsg == TEXT_NONE) - NetSendCmdParam1(true, CMD_KNOCKBACK, pcursmonst); - } - NewCursor(CURSOR_HAND); -} - -int CalculateGold(Player &player) -{ - int gold = 0; - - for (int i = 0; i < player._pNumInv; i++) { - if (player.InvList[i]._itype == ItemType::Gold) - gold += player.InvList[i]._ivalue; - } - - return gold; -} - -Size GetInventorySize(const Item &item) -{ - const Size size = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); - - return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; -} - -} // namespace devilution +/** + * @file inv.cpp + * + * Implementation of player inventory. + */ +#include +#include +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include +#else +#include +#endif + +#include + +#include "DiabloUI/ui_flags.hpp" +#include "controls/control_mode.hpp" +#include "controls/plrctrls.h" +#include "cursor.h" +#include "engine/backbuffer_state.hpp" +#include "engine/clx_sprite.hpp" +#include "engine/load_cel.hpp" +#include "engine/palette.h" +#include "engine/render/clx_render.hpp" +#include "engine/render/text_render.hpp" +#include "engine/size.hpp" +#include "hwcursor.hpp" +#include "inv_iterators.hpp" +#include "levels/tile_properties.hpp" +#include "levels/town.h" +#include "minitext.h" +#include "options.h" +#include "panels/ui_panels.hpp" +#include "player.h" +#include "plrmsg.h" +#include "qol/stash.h" +#include "qol/visual_store.h" +#include "stores.h" +#include "towners.h" +#include "utils/display.h" +#include "utils/format_int.hpp" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/sdl_geometry.h" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +bool invflag; + +/** + * Maps from inventory slot to screen position. The inventory slots are + * arranged as follows: + * + * @code{.unparsed} + * 00 00 + * 00 00 03 + * + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 + * + * 01 02 + * + * 07 08 09 10 11 12 13 14 15 16 + * 17 18 19 20 21 22 23 24 25 26 + * 27 28 29 30 31 32 33 34 35 36 + * 37 38 39 40 41 42 43 44 45 46 + * + * 47 48 49 50 51 52 53 54 + * @endcode + */ +const Rectangle InvRect[] = { + // clang-format off + //{ X, Y }, { W, H } + { { 132, 2 }, { 58, 59 } }, // helmet + { { 47, 177 }, { 28, 29 } }, // left ring + { { 248, 177 }, { 28, 29 } }, // right ring + { { 205, 32 }, { 28, 29 } }, // amulet + { { 17, 75 }, { 58, 86 } }, // left hand + { { 248, 75 }, { 58, 87 } }, // right hand + { { 132, 75 }, { 58, 87 } }, // chest + { { 17, 222 }, { 29, 29 } }, // inv row 1 + { { 46, 222 }, { 29, 29 } }, // inv row 1 + { { 75, 222 }, { 29, 29 } }, // inv row 1 + { { 104, 222 }, { 29, 29 } }, // inv row 1 + { { 133, 222 }, { 29, 29 } }, // inv row 1 + { { 162, 222 }, { 29, 29 } }, // inv row 1 + { { 191, 222 }, { 29, 29 } }, // inv row 1 + { { 220, 222 }, { 29, 29 } }, // inv row 1 + { { 249, 222 }, { 29, 29 } }, // inv row 1 + { { 278, 222 }, { 29, 29 } }, // inv row 1 + { { 17, 251 }, { 29, 29 } }, // inv row 2 + { { 46, 251 }, { 29, 29 } }, // inv row 2 + { { 75, 251 }, { 29, 29 } }, // inv row 2 + { { 104, 251 }, { 29, 29 } }, // inv row 2 + { { 133, 251 }, { 29, 29 } }, // inv row 2 + { { 162, 251 }, { 29, 29 } }, // inv row 2 + { { 191, 251 }, { 29, 29 } }, // inv row 2 + { { 220, 251 }, { 29, 29 } }, // inv row 2 + { { 249, 251 }, { 29, 29 } }, // inv row 2 + { { 278, 251 }, { 29, 29 } }, // inv row 2 + { { 17, 280 }, { 29, 29 } }, // inv row 3 + { { 46, 280 }, { 29, 29 } }, // inv row 3 + { { 75, 280 }, { 29, 29 } }, // inv row 3 + { { 104, 280 }, { 29, 29 } }, // inv row 3 + { { 133, 280 }, { 29, 29 } }, // inv row 3 + { { 162, 280 }, { 29, 29 } }, // inv row 3 + { { 191, 280 }, { 29, 29 } }, // inv row 3 + { { 220, 280 }, { 29, 29 } }, // inv row 3 + { { 249, 280 }, { 29, 29 } }, // inv row 3 + { { 278, 280 }, { 29, 29 } }, // inv row 3 + { { 17, 309 }, { 29, 29 } }, // inv row 4 + { { 46, 309 }, { 29, 29 } }, // inv row 4 + { { 75, 309 }, { 29, 29 } }, // inv row 4 + { { 104, 309 }, { 29, 29 } }, // inv row 4 + { { 133, 309 }, { 29, 29 } }, // inv row 4 + { { 162, 309 }, { 29, 29 } }, // inv row 4 + { { 191, 309 }, { 29, 29 } }, // inv row 4 + { { 220, 309 }, { 29, 29 } }, // inv row 4 + { { 249, 309 }, { 29, 29 } }, // inv row 4 + { { 278, 309 }, { 29, 29 } }, // inv row 4 + { { 205, 5 }, { 29, 29 } }, // belt + { { 234, 5 }, { 29, 29 } }, // belt + { { 263, 5 }, { 29, 29 } }, // belt + { { 292, 5 }, { 29, 29 } }, // belt + { { 321, 5 }, { 29, 29 } }, // belt + { { 350, 5 }, { 29, 29 } }, // belt + { { 379, 5 }, { 29, 29 } }, // belt + { { 408, 5 }, { 29, 29 } } // belt + // clang-format on +}; + +namespace { + +OptionalOwnedClxSpriteList pInvCels; + +/** + * @brief Adds an item to a player's InvGrid array + * @param player The player reference + * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) + * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) + * @param itemSize Size of item + */ +void AddItemToInvGrid(Player &player, int invGridIndex, int invListIndex, Size itemSize, bool sendNetworkMessage) +{ + const int pitch = 10; + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + if (x == 0 && y == itemSize.height - 1) + player.InvGrid[rowGridIndex + x] = invListIndex; + else + player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell. + } + } + + if (sendNetworkMessage) { + NetSendCmdChInvItem(false, invGridIndex); + } +} + +/** + * @brief Checks whether the given item can fit in a belt slot (i.e. the item's size in inventory cells is 1x1). + * @param item The item to be checked. + * @return 'True' in case the item can fit a belt slot and 'False' otherwise. + */ +bool FitsInBeltSlot(const Item &item) +{ + return GetInventorySize(item) == Size { 1, 1 }; +} + +/** + * @brief Checks whether the given item can be equipped. Since this overload doesn't take player information, it only considers + * general aspects about the item, like if its requirements are met and if the item's target location is valid for the body. + * @param item The item to check. + * @return 'True' in case the item could be equipped in a player, and 'False' otherwise. + */ +bool CanEquip(const Item &item) +{ + return item.isEquipment() + && item._iStatFlag; +} + +/** + * @brief A specialized version of 'CanEquip(int, Item&, int)' that specifically checks whether the item can be equipped + * in one/both of the player's hands. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to check. + * @return 'True' if the player can currently equip the item in either one of his hands (i.e. the required hands are empty and + * allow the item), and 'False' otherwise. + */ +bool CanWield(Player &player, const Item &item) +{ + if (!CanEquip(item) || IsNoneOf(player.GetItemLocation(item), ILOC_ONEHAND, ILOC_TWOHAND)) + return false; + + const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; + const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; + + if (leftHandItem.isEmpty() && rightHandItem.isEmpty()) { + return true; + } + + if (!leftHandItem.isEmpty() && !rightHandItem.isEmpty()) { + return false; + } + + const Item &occupiedHand = !leftHandItem.isEmpty() ? leftHandItem : rightHandItem; + + // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied + // slot is another one-handed weapon. + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { + const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND + && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); + + const bool weaponToEquipIsOneHandedSwordOrMace = player.GetItemLocation(item) == ILOC_ONEHAND + && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace); + + if (occupiedHandIsOneHandedSwordOrMace && weaponToEquipIsOneHandedSwordOrMace) { + return true; + } + } + + return player.GetItemLocation(item) == ILOC_ONEHAND + && player.GetItemLocation(occupiedHand) == ILOC_ONEHAND + && item._iClass != occupiedHand._iClass; +} + +/** + * @brief Checks whether the specified item can be equipped in the desired body location on the player. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to check. + * @param bodyLocation The location in the inventory to be checked against. + * @return 'True' if the player can currently equip the item in the specified body location (i.e. the body location is empty and + * allows the item), and 'False' otherwise. + */ +bool CanEquip(Player &player, const Item &item, inv_body_loc bodyLocation) +{ + if (!CanEquip(item) || player._pmode > PM_WALK_SIDEWAYS || !player.InvBody[bodyLocation].isEmpty()) { + return false; + } + + switch (bodyLocation) { + case INVLOC_AMULET: + return item._iLoc == ILOC_AMULET; + + case INVLOC_CHEST: + return item._iLoc == ILOC_ARMOR; + + case INVLOC_HAND_LEFT: + case INVLOC_HAND_RIGHT: + return CanWield(player, item); + + case INVLOC_HEAD: + return item._iLoc == ILOC_HELM; + + case INVLOC_RING_LEFT: + case INVLOC_RING_RIGHT: + return item._iLoc == ILOC_RING; + + default: + return false; + } +} + +void ChangeEquipment(Player &player, inv_body_loc bodyLocation, const Item &item, bool sendNetworkMessage) +{ + player.InvBody[bodyLocation] = item; + + if (sendNetworkMessage) { + NetSendCmdChItem(false, bodyLocation, true); + } +} + +bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool persistItem, bool sendNetworkMessage) +{ + if (!CanEquip(player, item, bodyLocation)) { + return false; + } + + if (persistItem) { + ChangeEquipment(player, bodyLocation, item, sendNetworkMessage); + + if (sendNetworkMessage && *GetOptions().Audio.autoEquipSound) { + PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); + } + + CalcPlrInv(player, true); + } + + return true; +} + +int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) +{ + Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; + for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) + return r; + } + for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) { + // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. + if (itemSize.height <= 1 && itemSize.width <= 1) { + // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do + return r; + } + // Otherwise work out how far the central cell is from the top-left cell + Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; + // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. + if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { + // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left + hotPixelCellOffset.deltaX++; + } + if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { + // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above + hotPixelCellOffset.deltaY++; + } + // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) + const int hotPixelCell = r - SLOTXY_INV_FIRST; + const int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); + const int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); + return SLOTXY_INV_FIRST + (targetRow * InventorySizeInSlots.width) + targetColumn; + } + } + + panelOffset = Point { 0, 0 } - GetMainPanel().position; + for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) + return r; + } + return NUM_XY_SLOTS; +} + +void ChangeBodyEquipment(Player &player, int slot, item_equip_type location) +{ + const inv_body_loc bodyLocation = [&slot](item_equip_type location) { + switch (location) { + case ILOC_HELM: + return INVLOC_HEAD; + case ILOC_RING: + return (slot == SLOTXY_RING_LEFT ? INVLOC_RING_LEFT : INVLOC_RING_RIGHT); + case ILOC_AMULET: + return INVLOC_AMULET; + case ILOC_ARMOR: + return INVLOC_CHEST; + default: + app_fatal("Unexpected equipment type"); + } + }(location); + const Item previouslyEquippedItem = player.InvBody[slot]; + ChangeEquipment(player, bodyLocation, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } +} + +void ChangeEquippedItem(Player &player, uint8_t slot) +{ + const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; + const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; + + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + + const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) + || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); + + const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); + + const inv_body_loc pasteHand = pasteIntoSelectedHand ? selectedHand : otherHand; + const Item previouslyEquippedItem = dequipTwoHandedWeapon ? player.InvBody[otherHand] : player.InvBody[pasteHand]; + if (dequipTwoHandedWeapon) { + RemoveEquipment(player, otherHand, false); + } + ChangeEquipment(player, pasteHand, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } +} + +void ChangeTwoHandItem(Player &player) +{ + if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && !player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + inv_body_loc locationToUnequip = INVLOC_HAND_LEFT; + if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { + locationToUnequip = INVLOC_HAND_RIGHT; + } + if (!AutoPlaceItemInInventory(player, player.InvBody[locationToUnequip])) { + return; + } + + if (locationToUnequip == INVLOC_HAND_RIGHT) { + RemoveEquipment(player, INVLOC_HAND_RIGHT, false); + } else { + player.InvBody[INVLOC_HAND_LEFT].clear(); + } + } + + if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_LEFT]; + ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } + } else { + const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_RIGHT]; + RemoveEquipment(player, INVLOC_HAND_RIGHT, false); + ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem, &player == MyPlayer); + player.HoldItem = previouslyEquippedItem; + } +} + +int8_t CheckOverlappingItems(int slot, const Player &player, Size itemSize) +{ + // check that the item we're pasting only overlaps one other item (or is going into empty space) + const auto originCell = static_cast(slot - SLOTXY_INV_FIRST); + + int8_t overlappingId = 0; + for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { + + for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { + const unsigned testCell = originCell + rowOffset + columnOffset; + // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. + assert(testCell < sizeof(player.InvGrid)); + if (player.InvGrid[testCell] != 0) { + const int8_t iv = std::abs(player.InvGrid[testCell]); + if (overlappingId != 0) { + if (overlappingId != iv) { + // Found two different items that would be displaced by the held item, can't paste the item here. + return -1; + } + } else { + overlappingId = iv; + } + } + } + } + + return overlappingId; +} + +int8_t GetPrevItemId(int slot, const Player &player, const Size &itemSize) +{ + if (player.HoldItem._itype != ItemType::Gold) + return CheckOverlappingItems(slot, player, itemSize); + const int8_t item_cell_begin = player.InvGrid[slot - SLOTXY_INV_FIRST]; + if (item_cell_begin == 0) + return 0; + if (item_cell_begin <= 0) + return -item_cell_begin; + if (player.InvList[item_cell_begin - 1]._itype != ItemType::Gold) + return item_cell_begin; + return 0; +} + +bool ChangeInvItem(Player &player, int slot, Size itemSize) +{ + int8_t prevItemId = GetPrevItemId(slot, player, itemSize); + if (prevItemId < 0) return false; + + if (player.HoldItem._itype == ItemType::Gold && prevItemId == 0) { + const int ii = slot - SLOTXY_INV_FIRST; + if (player.InvGrid[ii] > 0) { + const int invIndex = player.InvGrid[ii] - 1; + const int gt = player.InvList[invIndex]._ivalue; + int ig = player.HoldItem._ivalue + gt; + if (ig <= MaxGold) { + player.InvList[invIndex]._ivalue = ig; + SetPlrHandGoldCurs(player.InvList[invIndex]); + player._pGold += player.HoldItem._ivalue; + player.HoldItem.clear(); + } else { + ig = MaxGold - gt; + player._pGold += ig; + player.HoldItem._ivalue -= ig; + SetPlrHandGoldCurs(player.HoldItem); + player.InvList[invIndex]._ivalue = MaxGold; + player.InvList[invIndex]._iCurs = ICURS_GOLD_LARGE; + } + } else { + const int invIndex = player._pNumInv; + player._pGold += player.HoldItem._ivalue; + player.InvList[invIndex] = player.HoldItem.pop(); + player._pNumInv++; + player.InvGrid[ii] = player._pNumInv; + } + if (&player == MyPlayer) { + NetSendCmdChInvItem(false, ii); + } + } else { + if (prevItemId == 0) { + player.InvList[player._pNumInv] = player.HoldItem.pop(); + player._pNumInv++; + prevItemId = player._pNumInv; + } else { + const int invIndex = prevItemId - 1; + if (player.HoldItem._itype == ItemType::Gold) + player._pGold += player.HoldItem._ivalue; + std::swap(player.InvList[invIndex], player.HoldItem); + if (player.HoldItem._itype == ItemType::Gold) + player._pGold = CalculateGold(player); + for (int8_t &itemIndex : player.InvGrid) { + if (itemIndex == prevItemId) + itemIndex = 0; + if (itemIndex == -prevItemId) + itemIndex = 0; + } + } + + AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); + } + + return true; +} + +void ChangeBeltItem(Player &player, int slot) +{ + const int ii = slot - SLOTXY_BELT_FIRST; + if (player.SpdList[ii].isEmpty()) { + player.SpdList[ii] = player.HoldItem.pop(); + } else { + std::swap(player.SpdList[ii], player.HoldItem); + + if (player.HoldItem._itype == ItemType::Gold) + player._pGold = CalculateGold(player); + } + if (&player == MyPlayer) { + NetSendCmdChBeltItem(false, ii); + } + RedrawComponent(PanelDrawComponent::Belt); +} + +item_equip_type GetItemEquipType(int slot, item_equip_type desiredLocation) +{ + if (slot == SLOTXY_HEAD) + return ILOC_HELM; + if (slot == SLOTXY_RING_LEFT || slot == SLOTXY_RING_RIGHT) + return ILOC_RING; + if (slot == SLOTXY_AMULET) + return ILOC_AMULET; + if (slot == SLOTXY_HAND_LEFT || slot == SLOTXY_HAND_RIGHT) { + if (desiredLocation == ILOC_TWOHAND) + return ILOC_TWOHAND; + return ILOC_ONEHAND; + } + if (slot == SLOTXY_CHEST) + return ILOC_ARMOR; + if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) + return ILOC_BELT; + + return ILOC_UNEQUIPABLE; +} + +void CheckInvPaste(Player &player, Point cursorPosition) +{ + const Size itemSize = GetInventorySize(player.HoldItem); + + const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); + if (slot == NUM_XY_SLOTS) + return; + + const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); + const item_equip_type location = GetItemEquipType(slot, desiredLocation); + + if (location == ILOC_BELT) { + if (!CanBePlacedOnBelt(player, player.HoldItem)) return; + } else if (location != ILOC_UNEQUIPABLE) { + if (desiredLocation != location) return; + } + + if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { + if (!player.CanUseItem(player.HoldItem)) { + player.Say(HeroSpeech::ICantUseThisYet); + return; + } + if (player._pmode > PM_WALK_SIDEWAYS) + return; + } + + if (&player == MyPlayer) { + PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); + } + + // Select the parameters that go into + // ChangeEquipment and add it to post switch + switch (location) { + case ILOC_HELM: + case ILOC_RING: + case ILOC_AMULET: + case ILOC_ARMOR: + ChangeBodyEquipment(player, slot, location); + break; + case ILOC_ONEHAND: + ChangeEquippedItem(player, slot); + break; + case ILOC_TWOHAND: + ChangeTwoHandItem(player); + break; + case ILOC_UNEQUIPABLE: + if (!ChangeInvItem(player, slot, itemSize)) return; + break; + case ILOC_BELT: + ChangeBeltItem(player, slot); + break; + case ILOC_NONE: + case ILOC_INVALID: + break; + } + + CalcPlrInv(player, true); + if (&player == MyPlayer) { + NewCursor(player.HoldItem); + } +} + +inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) +{ + assert(slot <= SLOTXY_CHEST); + return static_cast(slot); +} + +std::optional FindSlotUnderCursor(Point cursorPosition) +{ + + auto testPosition = static_cast(cursorPosition - GetRightPanel().position); + for (std::underlying_type_t r = SLOTXY_EQUIPPED_FIRST; r != SLOTXY_BELT_FIRST; r++) { + // check which body/inventory rectangle the mouse is in, if any + if (InvRect[r].contains(testPosition)) { + return static_cast(r); + } + } + + testPosition = static_cast(cursorPosition - GetMainPanel().position); + for (std::underlying_type_t r = SLOTXY_BELT_FIRST; r != NUM_XY_SLOTS; r++) { + // check which belt rectangle the mouse is in, if any + if (InvRect[r].contains(testPosition)) { + return static_cast(r); + } + } + + return {}; +} + +/** + * @brief Checks whether an item of the given size can be placed on the specified player's inventory slot. + * @param player The player whose inventory will be checked. + * @param slotIndex The 0-based index of the slot to put the item on. + * @param itemSize The size of the item to be checked. + * @param itemIndexToIgnore can be used to check if an item of the given size would fit if the item with the given (positive) ID was removed. + * @return 'True' in case the item can be placed on the specified player's inventory slot and 'False' otherwise. + */ +bool CheckItemFitsInInventorySlot(const Player &player, int slotIndex, const Size &itemSize, int itemIndexToIgnore) +{ + int yy = (slotIndex > 0) ? (10 * (slotIndex / 10)) : 0; + + for (int j = 0; j < itemSize.height; j++) { + if (yy >= InventoryGridCells) { + return false; + } + int xx = (slotIndex > 0) ? (slotIndex % 10) : 0; + for (int i = 0; i < itemSize.width; i++) { + if (xx >= 10 || (player.InvGrid[xx + yy] != 0 && std::abs(player.InvGrid[xx + yy]) - 1 != itemIndexToIgnore)) { + // The item is too wide to fit in the specified column, or one of the cells is occupied (and not by the item we're planning on removing) + return false; + } + xx++; + } + yy += 10; + } + return true; +} + +/** + * @brief Finds the first slot that could fit an item of the given size + * @param player Player whose inventory will be checked. + * @param itemSize Dimensions of the item. + * @param itemIndexToIgnore Can be used if you want to find whether the new item would fit with this item removed, without performing unnecessary actions. + * @return The first slot that could fit the item or an empty optional. + */ +std::optional FindSlotForItem(const Player &player, const Size &itemSize, int itemIndexToIgnore = -1) +{ + if (itemSize.height == 1) { + for (int i = 30; i <= 39; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + for (int x = 9; x >= 0; x--) { + for (int y = 2; y >= 0; y--) { + if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) + return (10 * y) + x; + } + } + return {}; + } + + if (itemSize.height == 2) { + for (int x = 10 - itemSize.width; x >= 0; x--) { + for (int y = 0; y < 3; y++) { + if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) + return (10 * y) + x; + } + } + return {}; + } + + if (itemSize == Size { 1, 3 }) { + for (int i = 0; i < 20; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + return {}; + } + + if (itemSize == Size { 2, 3 }) { + for (int i = 0; i < 9; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + + for (int i = 10; i < 19; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + return {}; + } + + app_fatal(StrCat("Unknown item size: ", itemSize.width, "x", itemSize.height)); +} + +/** + * @brief Checks if the given item could be placed on the specified players inventory if the other item was removed. + * @param player The player whose inventory will be checked. + * @param item The item to be checked. + * @param itemIndexToIgnore The inventory index of the item that we assume will be removed. + * @return 'True' if the item could fit with the other item removed and 'False' otherwise. + */ +bool CouldFitItemInInventory(const Player &player, const Item &item, int itemIndexToIgnore) +{ + return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); +} + +void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) +{ + if (player._pmode > PM_WALK_SIDEWAYS) { + return; + } + + CloseGoldDrop(); + + std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); + + if (!maybeSlot) { + // not on an inventory slot rectangle + return; + } + + const inv_xy_slot r = *maybeSlot; + + Item &holdItem = player.HoldItem; + holdItem.clear(); + + bool attemptedMove = false; + bool automaticallyMoved = false; + SfxID successSound = SfxID::None; + HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else + + if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { + const inv_body_loc invloc = MapSlotToInvBodyLoc(r); + if (!player.InvBody[invloc].isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; + RemoveEquipment(player, invloc, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = player.InvBody[invloc]; + RemoveEquipment(player, invloc, false); + } + } + } + + if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { + const unsigned ig = r - SLOTXY_INV_FIRST; + const int iv = std::abs(player.InvGrid[ig]) - 1; + if (iv >= 0) { + if (automaticMove) { + attemptedMove = true; + if (CanBePlacedOnBelt(player, player.InvList[iv])) { + automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveInvItem(iv, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else if (CanEquip(player.InvList[iv])) { + failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails + + /* + * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be + * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except + * we don't want to leave the item on the hand if the equip attempt failed. We would end up + * generating wasteful network messages if we did the lift first. Instead we work out whatever slot + * needs to be unequipped (if any): + */ + int invloc = NUM_INVLOC; + switch (player.GetItemLocation(player.InvList[iv])) { + case ILOC_ARMOR: + invloc = INVLOC_CHEST; + break; + case ILOC_HELM: + invloc = INVLOC_HEAD; + break; + case ILOC_AMULET: + invloc = INVLOC_AMULET; + break; + case ILOC_ONEHAND: + if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() + && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass + || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { + // The left hand is not empty and we're either trying to equip the same type of item or + // it's holding a two handed weapon, so it must be unequipped + invloc = INVLOC_HAND_LEFT; + } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { + // The right hand is not empty and we're trying to equip the same type of item, so we need + // to unequip that item + invloc = INVLOC_HAND_RIGHT; + } + // otherwise one hand is empty (and we can let the auto-equip code put the target item into + // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip + // a shield (in which case the attempt will fail). + break; + case ILOC_TWOHAND: + // Moving a two-hand item from inventory to InvBody requires emptying both hands. + if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + // If the right hand is empty then we can simply try equipping this item in the left hand, + // we'll let the common code take care of unequipping anything held there. + invloc = INVLOC_HAND_LEFT; + } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { + // We have an item in the right hand but nothing in the left, so let the common code + // take care of unequipping whatever is held in the right hand. The auto-equip code + // picks the most appropriate location for the item type (which in this case will be + // the left hand), invloc isn't used there. + invloc = INVLOC_HAND_RIGHT; + } else { + // Both hands are holding items, we must unequip one of the items and check that there's + // space for the other before trying to auto-equip + inv_body_loc mainHand = INVLOC_HAND_LEFT; + inv_body_loc offHand = INVLOC_HAND_RIGHT; + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + // No space to move right hand item to inventory, can we move the left instead? + std::swap(mainHand, offHand); + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + break; + } + } + if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { + // No space for the main hand item. Move the other item back to the off hand and abort. + player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; + player.RemoveInvItem(player._pNumInv - 1, false); + break; + } + RemoveEquipment(player, offHand, false); + invloc = mainHand; + } + break; + default: + // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. + failedSpeech = HeroSpeech::ICantDoThat; + break; + } + // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip + if (invloc != NUM_INVLOC + && !player.InvBody[invloc].isEmpty() + && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { + holdItem = player.InvBody[invloc].pop(); + } + automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; + player.RemoveInvItem(iv, false); + + // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv + if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { + holdItem.clear(); + } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. + } else if (!holdItem.isEmpty()) { + // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... + player.InvBody[invloc] = holdItem.pop(); + } + } + } else if (IsVisualStoreOpen && CanSellToCurrentVendor(player.InvList[iv]) && dropItem) { + // If visual store is open, ctrl-click sells the item + SellItemToVisualStore(iv); + automaticallyMoved = true; + } else { + holdItem = player.InvList[iv]; + player.RemoveInvItem(iv, false); + } + } + } + + if (r >= SLOTXY_BELT_FIRST) { + const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; + if (!beltItem.isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = beltItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } + } + } + + if (!holdItem.isEmpty()) { + if (holdItem._itype == ItemType::Gold) { + player._pGold = CalculateGold(player); + } + + CalcPlrInv(player, true); + holdItem._iStatFlag = player.CanUseItem(holdItem); + + if (&player == MyPlayer) { + PlaySFX(SfxID::GrabItem); + NewCursor(holdItem); + } + if (dropItem) { + TryDropItem(); + } + } else if (automaticMove) { + if (automaticallyMoved) { + CalcPlrInv(player, true); + } + if (attemptedMove && &player == MyPlayer) { + if (automaticallyMoved) { + PlaySFX(successSound); + } else { + player.SaySpecific(failedSpeech); + } + } + } +} + +void TryCombineNaKrulNotes(Player &player, Item ¬eItem) +{ + const int idx = noteItem.IDidx; + const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; + + if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { + return; + } + + for (const _item_indexes note : notes) { + if (idx != note && !HasInventoryItemWithId(player, note)) { + return; // the player doesn't have all notes + } + } + + MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); + + for (const _item_indexes note : notes) { + if (idx != note) { + RemoveInventoryItemById(player, note); + } + } + + const Point position = noteItem.position; // copy the position to restore it after re-initialising the item + noteItem = {}; + GetItemAttrs(noteItem, IDI_FULLNOTE, 16); + SetupItem(noteItem); + noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table +} + +void CheckQuestItem(Player &player, Item &questItem) +{ + const Player &myPlayer = *MyPlayer; + + if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE + && (questItem.IDidx == IDI_OPTAMULET + || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { + Quests[Q_BLIND]._qactive = QUEST_DONE; + NetSendCmdQuest(true, Quests[Q_BLIND]); + } + + if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { + player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP + Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; + NetSendCmdQuest(true, Quests[Q_MUSHROOM]); + } + + if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { + Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ANVIL]); + } + if (Quests[Q_ANVIL]._qlog) { + myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); + } + } + + if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { + myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); + } + + if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ROCK]._qactive == QUEST_INIT) { + Quests[Q_ROCK]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ROCK]); + } + if (Quests[Q_ROCK]._qlog) { + myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); + } + } + + if (Quests[Q_BLOOD]._qactive == QUEST_ACTIVE + && (questItem.IDidx == IDI_ARMOFVAL + || (Quests[Q_BLOOD].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 9, 3 })))) { + Quests[Q_BLOOD]._qactive = QUEST_DONE; + NetSendCmdQuest(true, Quests[Q_BLOOD]); + myPlayer.Say(HeroSpeech::MayTheSpiritOfArkaineProtectMe, 20); + } + + if (questItem.IDidx == IDI_MAPOFDOOM) { + Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; + if (Quests[Q_GRAVE]._qvar1 != 1) { + MyPlayer->Say(HeroSpeech::UhHuh, 10); + Quests[Q_GRAVE]._qvar1 = 1; + } + } + + TryCombineNaKrulNotes(player, questItem); +} + +void CleanupItems(int ii) +{ + const Item &item = Items[ii]; + dItem[item.position.x][item.position.y] = 0; + + if (CornerStone.isAvailable() && item.position == CornerStone.position) { + CornerStone.item.clear(); + CornerStone.item.selectionRegion = SelectionRegion::None; + CornerStone.item.position = { 0, 0 }; + CornerStone.item._iAnimFlag = false; + CornerStone.item._iIdentified = false; + CornerStone.item._iPostDraw = false; + } + + int i = 0; + while (i < ActiveItemCount) { + if (ActiveItems[i] == ii) { + DeleteItem(i); + i = 0; + continue; + } + + i++; + } +} + +bool CanUseStaff(Item &staff, SpellID spell) +{ + return !staff.isEmpty() + && IsAnyOf(staff._iMiscId, IMISC_STAFF, IMISC_UNIQUE) + && staff._iSpell == spell + && staff._iCharges > 0; +} + +void StartGoldDrop() +{ + CloseGoldWithdraw(); + + const int8_t invIndex = pcursinvitem; + + const Player &myPlayer = *MyPlayer; + + const int max = (invIndex <= INVITEM_INV_LAST) + ? myPlayer.InvList[invIndex - INVITEM_INV_FIRST]._ivalue + : myPlayer.SpdList[invIndex - INVITEM_BELT_FIRST]._ivalue; + + if (ChatFlag) + ResetChat(); + + const Point start = GetPanelPosition(UiPanels::Inventory, { 67, 128 }); + SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); + SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); + OpenGoldDrop(invIndex, max); +} + +int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) +{ + if (player.InvGrid[slotIndex] != 0) { + return value; + } + + Item &goldItem = player.InvList[player._pNumInv]; + MakeGoldStack(goldItem, std::min(value, MaxGold)); + player._pNumInv++; + player.InvGrid[slotIndex] = player._pNumInv; + if (&player == MyPlayer) { + NetSendCmdChInvItem(false, slotIndex); + } + + value -= goldItem._ivalue; + + return value; +} + +} // namespace + +void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) +{ + SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); + out.Clip(&srcRect, &targetPosition); + if (size.width <= 0 || size.height <= 0) + return; + + uint8_t colorShift; + switch (itemQuality) { + case ITEM_QUALITY_MAGIC: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BLUE : PAL16_ORANGE) - 1; + break; + case ITEM_QUALITY_UNIQUE: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_YELLOW : PAL16_ORANGE) - 1; + break; + default: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BEIGE : PAL16_ORANGE) - 1; + break; + } + + uint8_t *dst = &out[targetPosition]; + const auto dstPitch = out.pitch(); + for (int y = size.height; y != 0; --y, dst -= dstPitch + size.width) { + for (const uint8_t *end = dst + size.width; dst < end; ++dst) { + uint8_t &pix = *dst; + if (pix >= PAL16_GRAY) { + pix -= colorShift; + } + } + } +} + +bool CanBePlacedOnBelt(const Player &player, const Item &item) +{ + return FitsInBeltSlot(item) + && item._itype != ItemType::Gold + && player.CanUseItem(item) + && item.isUsable(); +} + +void FreeInvGFX() +{ + pInvCels = std::nullopt; +} + +void InitInv() +{ + const PlayerData &playerClassData = GetPlayerDataForClass(MyPlayer->_pClass); + const char *invName = playerClassData.inv.c_str(); + if (gbIsSpawn && (playerClassData.inv == "inv_rog" || playerClassData.inv == "inv_sor")) { + invName = "inv"; + } + pInvCels = LoadCel(StrCat("data\\inv\\", invName).c_str(), static_cast(SidePanelSize.width)); +} + +void DrawInv(const Surface &out) +{ + ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 0, 351 }), (*pInvCels)[0]); + + const Size slotSize[] = { + { 2, 2 }, // head + { 1, 1 }, // left ring + { 1, 1 }, // right ring + { 1, 1 }, // amulet + { 2, 3 }, // left hand + { 2, 3 }, // right hand + { 2, 3 }, // chest + }; + + const Point slotPos[] = { + { 133, 59 }, // head + { 48, 205 }, // left ring + { 249, 205 }, // right ring + { 205, 60 }, // amulet + { 17, 160 }, // left hand + { 248, 160 }, // right hand + { 133, 160 }, // chest + }; + + const Player &myPlayer = *InspectPlayer; + + for (int slot = INVLOC_HEAD; slot < NUM_INVLOC; slot++) { + if (!myPlayer.InvBody[slot].isEmpty()) { + int screenX = slotPos[slot].x; + int screenY = slotPos[slot].y; + InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, { screenX, screenY }), { slotSize[slot].width * InventorySlotSizeInPixels.width, slotSize[slot].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); + + const int cursId = myPlayer.InvBody[slot]._iCurs + CURSOR_FIRSTITEM; + + const Size frameSize = GetInvItemSize(cursId); + + // calc item offsets for weapons/armor smaller than 2x3 slots + if (IsAnyOf(slot, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST)) { + screenX += frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0; + screenY += frameSize.height == (3 * InventorySlotSizeInPixels.height) ? 0 : -INV_SLOT_HALF_SIZE_PX; + } + + const ClxSprite sprite = GetInvItemSprite(cursId); + const Point position = GetPanelPosition(UiPanels::Inventory, { screenX, screenY }); + + if (pcursinvitem == slot) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.InvBody[slot], true), position, sprite); + } + + DrawItem(myPlayer.InvBody[slot], out, position, sprite); + + if (slot == INVLOC_HAND_LEFT) { + if (myPlayer.GetItemLocation(myPlayer.InvBody[slot]) == ILOC_TWOHAND) { + InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, slotPos[INVLOC_HAND_RIGHT]), { slotSize[INVLOC_HAND_RIGHT].width * InventorySlotSizeInPixels.width, slotSize[INVLOC_HAND_RIGHT].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); + const int dstX = GetRightPanel().position.x + slotPos[INVLOC_HAND_RIGHT].x + (frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0) - 1; + const int dstY = GetRightPanel().position.y + slotPos[INVLOC_HAND_RIGHT].y; + ClxDrawBlended(out, { dstX, dstY }, sprite); + } + } + } + } + + for (int i = 0; i < InventoryGridCells; i++) { + if (myPlayer.InvGrid[i] != 0) { + InvDrawSlotBack( + out, + GetPanelPosition(UiPanels::Inventory, InvRect[i + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }, + InventorySlotSizeInPixels, + myPlayer.InvList[std::abs(myPlayer.InvGrid[i]) - 1]._iMagical); + } + } + + for (int j = 0; j < InventoryGridCells; j++) { + if (myPlayer.InvGrid[j] > 0) { // first slot of an item + const int ii = myPlayer.InvGrid[j] - 1; + const int cursId = myPlayer.InvList[ii]._iCurs + CURSOR_FIRSTITEM; + + const ClxSprite sprite = GetInvItemSprite(cursId); + const Point position = GetPanelPosition(UiPanels::Inventory, InvRect[j + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }; + if (pcursinvitem == ii + INVITEM_INV_FIRST) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.InvList[ii], true), position, sprite); + } + + DrawItem(myPlayer.InvList[ii], out, position, sprite); + } + } +} + +void DrawInvBelt(const Surface &out) +{ + if (ChatFlag) { + return; + } + + const Point mainPanelPosition = GetMainPanel().position; + + DrawPanelBox(out, { 205, 21, 232, 28 }, mainPanelPosition + Displacement { 205, 5 }); + + const Player &myPlayer = *InspectPlayer; + + for (int i = 0; i < MaxBeltItems; i++) { + if (myPlayer.SpdList[i].isEmpty()) { + continue; + } + + const Point position { InvRect[i + SLOTXY_BELT_FIRST].position.x + mainPanelPosition.x, InvRect[i + SLOTXY_BELT_FIRST].position.y + mainPanelPosition.y + InventorySlotSizeInPixels.height }; + InvDrawSlotBack(out, position, InventorySlotSizeInPixels, myPlayer.SpdList[i]._iMagical); + const int cursId = myPlayer.SpdList[i]._iCurs + CURSOR_FIRSTITEM; + + const ClxSprite sprite = GetInvItemSprite(cursId); + + if (pcursinvitem == i + INVITEM_BELT_FIRST) { + if (ControlMode == ControlTypes::KeyboardAndMouse || invflag) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.SpdList[i], true), position, sprite); + } + } + + DrawItem(myPlayer.SpdList[i], out, position, sprite); + + if (myPlayer.SpdList[i].isUsable() + && myPlayer.SpdList[i]._itype != ItemType::Gold) { + auto beltKey = StrCat("BeltItem", i + 1); + std::string_view keyName = ControlMode == ControlTypes::Gamepad + ? GetOptions().Padmapper.InputNameForAction(beltKey, true) + : GetOptions().Keymapper.KeyNameForAction(beltKey); + + if (keyName.length() > 2) + keyName = {}; + + DrawString(out, keyName, { position - Displacement { 0, 12 }, InventorySlotSizeInPixels }, + { .flags = UiFlags::ColorWhite | UiFlags::AlignRight }); + } + } +} + +void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri) +{ + if (&player == MyPlayer) { + NetSendCmdDelItem(hiPri, bodyLocation); + } + + player.InvBody[bodyLocation].clear(); +} + +bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) +{ + if (!CanBePlacedOnBelt(player, item)) { + return false; + } + + for (Item &beltItem : player.SpdList) { + if (beltItem.isEmpty()) { + if (persistItem) { + beltItem = item; + player.CalcScrolls(); + RedrawComponent(PanelDrawComponent::Belt); + if (sendNetworkMessage) { + const auto beltIndex = static_cast(std::distance(&player.SpdList[0], &beltItem)); + NetSendCmdChBeltItem(false, beltIndex); + } + } + + return true; + } + } + + return false; +} + +bool AutoEquip(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) +{ + if (!CanEquip(item)) { + return false; + } + + for (int bodyLocation = INVLOC_HEAD; bodyLocation < NUM_INVLOC; bodyLocation++) { + if (AutoEquip(player, item, (inv_body_loc)bodyLocation, persistItem, sendNetworkMessage)) { + return true; + } + } + + return false; +} + +bool AutoEquipEnabled(const Player &player, const Item &item) +{ + if (item.isWeapon()) { + // Monk can use unarmed attack as an encouraged option, thus we do not automatically equip weapons on him so as to not + // annoy players who prefer that playstyle. + return player._pClass != HeroClass::Monk && *GetOptions().Gameplay.autoEquipWeapons; + } + + if (item.isArmor()) { + return *GetOptions().Gameplay.autoEquipArmor; + } + + if (item.isHelm()) { + return *GetOptions().Gameplay.autoEquipHelms; + } + + if (item.isShield()) { + return *GetOptions().Gameplay.autoEquipShields; + } + + if (item.isJewelry()) { + return *GetOptions().Gameplay.autoEquipJewelry; + } + + return true; +} + +bool CanFitItemInInventory(const Player &player, const Item &item) +{ + return static_cast(FindSlotForItem(player, GetInventorySize(item))); +} + +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) +{ + const Size itemSize = GetInventorySize(item); + std::optional targetSlot = FindSlotForItem(player, itemSize); + + if (targetSlot) { + player.InvList[player._pNumInv] = item; + player._pNumInv++; + + AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); + player.CalcScrolls(); + + return true; + } + + return false; +} + +std::vector SortItemsBySize(Player &player) +{ + std::vector> itemSizes; // Pair of item size and its index in InvList + itemSizes.reserve(player._pNumInv); // Reserves space for the number of items in the player's inventory + + for (int i = 0; i < player._pNumInv; i++) { + const Size size = GetInventorySize(player.InvList[i]); + itemSizes.emplace_back(size, i); + } + + // Sort items by height first, then by width + std::sort(itemSizes.begin(), itemSizes.end(), [](const auto &a, const auto &b) { + if (a.first.height == b.first.height) return a.first.width > b.first.width; + return a.first.height > b.first.height; + }); + + // Extract sorted indices + std::vector sortedIndices; + sortedIndices.reserve(itemSizes.size()); // Pre-allocate the necessary capacity + + for (const auto &itemSize : itemSizes) { + sortedIndices.push_back(itemSize.second); + } + + return sortedIndices; +} + +void ReorganizeInventory(Player &player) +{ + // Sort items by size + const std::vector sortedIndices = SortItemsBySize(player); + + // Temporary storage for items and a copy of InvGrid + std::vector tempStorage(player._pNumInv); + std::array originalInvGrid; // Declare an array for InvGrid copy + std::copy(std::begin(player.InvGrid), std::end(player.InvGrid), std::begin(originalInvGrid)); // Copy InvGrid to originalInvGrid + + // Move items to temporary storage and clear inventory slots + for (int i = 0; i < player._pNumInv; ++i) { + tempStorage[i] = player.InvList[i]; + player.InvList[i] = {}; + } + player._pNumInv = 0; // Reset inventory count + std::fill(std::begin(player.InvGrid), std::end(player.InvGrid), 0); // Clear InvGrid + + // Attempt to place items back, now from the temp storage + bool reorganizationFailed = false; + for (const int index : sortedIndices) { + const Item &item = tempStorage[index]; + if (!AutoPlaceItemInInventory(player, item, false)) { + reorganizationFailed = true; + break; + } + } + + // If reorganization failed, restore items and InvGrid from tempStorage and originalInvGrid + if (reorganizationFailed) { + for (const Item &item : tempStorage) { + if (!item.isEmpty()) { + player.InvList[player._pNumInv++] = item; + } + } + std::copy(std::begin(originalInvGrid), std::end(originalInvGrid), std::begin(player.InvGrid)); // Restore InvGrid + } +} + +int RoomForGold() +{ + int amount = 0; + for (const int8_t &itemIndex : MyPlayer->InvGrid) { + if (itemIndex < 0) { + continue; + } + if (itemIndex == 0) { + amount += MaxGold; + continue; + } + + const Item &goldItem = MyPlayer->InvList[itemIndex - 1]; + if (goldItem._itype != ItemType::Gold || goldItem._ivalue == MaxGold) { + continue; + } + + amount += MaxGold - goldItem._ivalue; + } + + return amount; +} + +int AddGoldToInventory(Player &player, int value) +{ + // Top off existing piles + for (int i = 0; i < player._pNumInv && value > 0; i++) { + Item &goldItem = player.InvList[i]; + if (goldItem._itype != ItemType::Gold || goldItem._ivalue >= MaxGold) { + continue; + } + + if (goldItem._ivalue + value > MaxGold) { + value -= MaxGold - goldItem._ivalue; + goldItem._ivalue = MaxGold; + } else { + goldItem._ivalue += value; + value = 0; + } + + NetSyncInvItem(player, i); + SetPlrHandGoldCurs(goldItem); + } + + // Last row right to left + for (int i = 39; i >= 30 && value > 0; i--) { + value = CreateGoldItemInInventorySlot(player, i, value); + } + + // Remaining inventory in columns, bottom to top, right to left + for (int x = 9; x >= 0 && value > 0; x--) { + for (int y = 2; y >= 0 && value > 0; y--) { + value = CreateGoldItemInInventorySlot(player, (10 * y) + x, value); + } + } + + return value; +} + +bool GoldAutoPlace(Player &player, Item &goldStack) +{ + goldStack._ivalue = AddGoldToInventory(player, goldStack._ivalue); + SetPlrHandGoldCurs(goldStack); + + player._pGold = CalculateGold(player); + + return goldStack._ivalue == 0; +} + +void CheckInvSwap(Player &player, inv_body_loc bLoc) +{ + const Item &item = player.InvBody[bLoc]; + + if (bLoc == INVLOC_HAND_LEFT && player.GetItemLocation(item) == ILOC_TWOHAND) { + player.InvBody[INVLOC_HAND_RIGHT].clear(); + } else if (bLoc == INVLOC_HAND_RIGHT && player.GetItemLocation(item) == ILOC_TWOHAND) { + player.InvBody[INVLOC_HAND_LEFT].clear(); + } + + CalcPlrInv(player, true); +} + +void inv_update_rem_item(Player &player, inv_body_loc iv) +{ + player.InvBody[iv].clear(); + + CalcPlrInv(player, player._pmode != PM_DEATH); +} + +void CheckInvSwap(Player &player, const Item &item, int invGridIndex) +{ + Size itemSize = GetInventorySize(item); + + const int pitch = 10; + const int invListIndex = [&]() -> int { + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + const int gridIndex = rowGridIndex + x; + if (player.InvGrid[gridIndex] != 0) + return std::abs(player.InvGrid[gridIndex]); + } + } + player._pNumInv++; + return player._pNumInv; + }(); + + if (invListIndex < player._pNumInv) { + for (int8_t &itemIndex : player.InvGrid) { + if (itemIndex == invListIndex) + itemIndex = 0; + if (itemIndex == -invListIndex) + itemIndex = 0; + } + } + + player.InvList[invListIndex - 1] = item; + + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + if (x == 0 && y == itemSize.height - 1) + player.InvGrid[rowGridIndex + x] = invListIndex; + else + player.InvGrid[rowGridIndex + x] = -invListIndex; + } + } + + CalcPlrInv(player, true); +} + +void CheckInvRemove(Player &player, int invGridIndex) +{ + const int invListIndex = std::abs(player.InvGrid[invGridIndex]) - 1; + + if (invListIndex >= 0) { + player.RemoveInvItem(invListIndex); + } +} + +void TransferItemToStash(Player &player, int location) +{ + if (location == -1) { + return; + } + + const Item &item = GetInventoryItem(player, location); + if (!AutoPlaceItemInStash(item, true)) { + player.SaySpecific(HeroSpeech::WhereWouldIPutThis); + return; + } + + PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); + + if (location < INVITEM_INV_FIRST) { + RemoveEquipment(player, static_cast(location), false); + CalcPlrInv(player, true); + } else if (location <= INVITEM_INV_LAST) + player.RemoveInvItem(location - INVITEM_INV_FIRST); + else + player.RemoveSpdBarItem(location - INVITEM_BELT_FIRST); +} + +void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) +{ + if (IsInspectingPlayer()) + return; + if (!MyPlayer->HoldItem.isEmpty()) { + CheckInvPaste(*MyPlayer, MousePosition); + } else if (IsStashOpen && isCtrlHeld) { + TransferItemToStash(*MyPlayer, pcursinvitem); + } else { + CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); + } +} + +void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld) +{ + const Point mainPanelPosition = GetMainPanel().position; + if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x + && MousePosition.y > mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) { + CheckInvItem(isShiftHeld, isCtrlHeld); + } +} + +void InvGetItem(Player &player, int ii) +{ + Item &item = Items[ii]; + CloseGoldDrop(); + + if (dItem[item.position.x][item.position.y] == 0) + return; + + item._iCreateInfo &= ~CF_PREGEN; + CheckQuestItem(player, item); + item.updateRequiredStatsCacheForPlayer(player); + + if (item._itype == ItemType::Gold && GoldAutoPlace(player, item)) { + if (MyPlayer == &player) { + // Non-gold items (or gold when you have a full inventory) go to the hand then provide audible feedback on + // paste. To give the same feedback for auto-placed gold we play the sound effect now. + PlaySFX(SfxID::ItemGold); + } + } else { + // The item needs to go into the players hand + if (MyPlayer == &player && !player.HoldItem.isEmpty()) { + // drop whatever the player is currently holding + NetSendCmdPItem(true, CMD_SYNCPUTITEM, player.position.tile, player.HoldItem); + } + + // need to copy here instead of move so CleanupItems still has access to the position + player.HoldItem = item; + NewCursor(player.HoldItem); + } + + // This potentially moves items in memory so must be done after we've made a copy + CleanupItems(ii); + pcursitem = -1; +} + +std::optional FindAdjacentPositionForItem(Point origin, Direction facing) +{ + if (ActiveItemCount >= MAXITEMS) + return {}; + + if (CanPut(origin + facing)) + return origin + facing; + + if (CanPut(origin + Left(facing))) + return origin + Left(facing); + + if (CanPut(origin + Right(facing))) + return origin + Right(facing); + + if (CanPut(origin + Left(Left(facing)))) + return origin + Left(Left(facing)); + + if (CanPut(origin + Right(Right(facing)))) + return origin + Right(Right(facing)); + + if (CanPut(origin + Left(Left(Left(facing))))) + return origin + Left(Left(Left(facing))); + + if (CanPut(origin + Right(Right(Right(facing))))) + return origin + Right(Right(Right(facing))); + + if (CanPut(origin + Opposite(facing))) + return origin + Opposite(facing); + + if (CanPut(origin)) + return origin; + + return {}; +} + +void AutoGetItem(Player &player, Item *itemPointer, int ii) +{ + Item &item = *itemPointer; + + CloseGoldDrop(); + + if (dItem[item.position.x][item.position.y] == 0) + return; + + item._iCreateInfo &= ~CF_PREGEN; + CheckQuestItem(player, item); + item.updateRequiredStatsCacheForPlayer(player); + + bool done; + bool autoEquipped = false; + + if (item._itype == ItemType::Gold) { + done = GoldAutoPlace(player, item); + if (!done) { + SetPlrHandGoldCurs(item); + } + } else { + done = AutoEquipEnabled(player, item) && AutoEquip(player, item, true, &player == MyPlayer); + if (done) { + autoEquipped = true; + } + + if (!done) { + done = AutoPlaceItemInBelt(player, item, true, &player == MyPlayer); + } + if (!done) { + done = AutoPlaceItemInInventory(player, item, &player == MyPlayer); + } + } + + if (done) { + if (!autoEquipped && *GetOptions().Audio.itemPickupSound && &player == MyPlayer) { + PlaySFX(SfxID::GrabItem); + } + + CleanupItems(ii); + return; + } + + if (&player == MyPlayer) { + player.Say(HeroSpeech::ICantCarryAnymore); + } + RespawnItem(item, true); + NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); +} + +int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t createInfo) +{ + for (uint8_t i = 0; i < ActiveItemCount; i++) { + const Item &item = Items[ActiveItems[i]]; + if (item.keyAttributesMatch(iseed, idx, createInfo)) { + return i; + } + } + + return -1; +} + +void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci) +{ + // Check what the local client has at the target position + int ii = dItem[position.x][position.y] - 1; + + if (ii >= 0 && ii < MAXITEMS) { + // If there was an item there, check that it's the same item as the remote player has + if (!Items[ii].keyAttributesMatch(iseed, idx, ci)) { + // Key attributes don't match so we must've desynced, ignore this index and try find a matching item via lookup + ii = -1; + } + } + + if (ii == -1) { + // Either there's no item at the expected position or it doesn't match what is being picked up, so look for an item that matches the key attributes + ii = FindGetItem(iseed, idx, ci); + + if (ii != -1) { + // Translate to Items index for CleanupItems, FindGetItem returns an ActiveItems index + ii = ActiveItems[ii]; + } + } + + if (ii == -1) { + // Still can't find the expected item, assume it was collected earlier and this caused the desync + return; + } + + CleanupItems(ii); +} + +bool CanPut(Point position) +{ + if (!InDungeonBounds(position)) { + return false; + } + + if (IsTileSolid(position)) { + return false; + } + + if (dItem[position.x][position.y] != 0) { + return false; + } + + if (leveltype == DTYPE_TOWN) { + if (dMonster[position.x][position.y] != 0) { + return false; + } + if (dMonster[position.x + 1][position.y + 1] != 0) { + return false; + } + } + + if (IsItemBlockingObjectAtPosition(position)) { + return false; + } + + return true; +} + +int ClampDurability(const Item &item, int durability) +{ + if (item._iMaxDur == 0) + return 0; + + return std::clamp(durability, 1, item._iMaxDur); +} + +int16_t ClampToHit(const Item &item, int16_t toHit) +{ + if (toHit < item._iPLToHit || toHit > 51) + return item._iPLToHit; + + return toHit; +} + +uint8_t ClampMaxDam(const Item &item, uint8_t maxDam) +{ + if (maxDam < item._iMaxDam || maxDam - item._iMinDam > 30) + return item._iMaxDam; + + return maxDam; +} + +int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam) +{ + if (ActiveItemCount >= MAXITEMS) + return -1; + + Item item; + + RecreateItem(*MyPlayer, item, idx, icreateinfo, iseed, ivalue, ibuff); + if (id != 0) + item._iIdentified = true; + item._iMaxDur = mdur; + item._iDurability = ClampDurability(item, dur); + item._iMaxCharges = std::clamp(mch, 0, item._iMaxCharges); + item._iCharges = std::clamp(ch, 0, item._iMaxCharges); + if (gbIsHellfire) { + item._iPLToHit = ClampToHit(item, toHit); + item._iMaxDam = ClampMaxDam(item, maxDam); + } + + return PlaceItemInWorld(std::move(item), position); +} + +int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname) +{ + if (ActiveItemCount >= MAXITEMS) + return -1; + + Item item; + RecreateEar(item, icreateinfo, iseed, cursval, heroname); + + return PlaceItemInWorld(std::move(item), position); +} + +int8_t CheckInvHLight() +{ + int8_t r = 0; + for (; r < NUM_XY_SLOTS; r++) { + int xo = GetRightPanel().position.x; + int yo = GetRightPanel().position.y; + if (r >= SLOTXY_BELT_FIRST) { + xo = GetMainPanel().position.x; + yo = GetMainPanel().position.y; + } + + if (InvRect[r].contains(MousePosition - Displacement(xo, yo))) { + break; + } + } + + if (r >= NUM_XY_SLOTS) + return -1; + + int8_t rv = -1; + InfoColor = UiFlags::ColorWhite; + Item *pi = nullptr; + Player &myPlayer = *InspectPlayer; + + if (r == SLOTXY_HEAD) { + rv = INVLOC_HEAD; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_RING_LEFT) { + rv = INVLOC_RING_LEFT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_RING_RIGHT) { + rv = INVLOC_RING_RIGHT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_AMULET) { + rv = INVLOC_AMULET; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_HAND_LEFT) { + rv = INVLOC_HAND_LEFT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_HAND_RIGHT) { + pi = &myPlayer.InvBody[INVLOC_HAND_LEFT]; + if (pi->isEmpty() || myPlayer.GetItemLocation(*pi) != ILOC_TWOHAND) { + rv = INVLOC_HAND_RIGHT; + pi = &myPlayer.InvBody[rv]; + } else { + rv = INVLOC_HAND_LEFT; + } + } else if (r == SLOTXY_CHEST) { + rv = INVLOC_CHEST; + pi = &myPlayer.InvBody[rv]; + } else if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { + const int8_t itemId = std::abs(myPlayer.InvGrid[r - SLOTXY_INV_FIRST]); + if (itemId == 0) + return -1; + const int ii = itemId - 1; + rv = ii + INVITEM_INV_FIRST; + pi = &myPlayer.InvList[ii]; + } else if (r >= SLOTXY_BELT_FIRST) { + r -= SLOTXY_BELT_FIRST; + RedrawComponent(PanelDrawComponent::Belt); + pi = &myPlayer.SpdList[r]; + if (pi->isEmpty()) + return -1; + rv = r + INVITEM_BELT_FIRST; + } + + if (pi->isEmpty()) + return -1; + + if (IsVisualStoreOpen && pcurs == CURSOR_REPAIR) { + InfoColor = pi->getTextColor(); + InfoString = pi->getName(); + FloatingInfoString = pi->getName(); + if (pi->_iIdentified) { + PrintItemDetails(*pi); + } else { + PrintItemDur(*pi); + } + int cost = GetRepairCost(*pi); + if (cost > 0) + AddInfoBoxString(StrCat(FormatInteger(cost), " Gold")); + else + AddInfoBoxString(_("Fully Repaired")); + } else if (pi->_itype == ItemType::Gold) { + const int nGold = pi->_ivalue; + InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + } else { + InfoColor = pi->getTextColor(); + InfoString = pi->getName(); + FloatingInfoString = pi->getName(); + if (pi->_iIdentified) { + PrintItemDetails(*pi); + } else { + PrintItemDur(*pi); + } + } + + return rv; +} + +void ConsumeScroll(Player &player) +{ + const SpellID spellId = player.executedSpell.spellId; + + const auto isCurrentSpell = [spellId](const Item &item) -> bool { + return item.isScrollOf(spellId) || item.isRuneOf(spellId); + }; + + // Try to remove the scroll from selected inventory slot + const int8_t itemSlot = player.executedSpell.spellFrom; + if (itemSlot >= INVITEM_INV_FIRST && itemSlot <= INVITEM_INV_LAST) { + const int itemIndex = itemSlot - INVITEM_INV_FIRST; + const Item *item = &player.InvList[itemIndex]; + if (!item->isEmpty() && isCurrentSpell(*item)) { + player.RemoveInvItem(itemIndex); + return; + } + } else if (itemSlot >= INVITEM_BELT_FIRST && itemSlot <= INVITEM_BELT_LAST) { + const int itemIndex = itemSlot - INVITEM_BELT_FIRST; + const Item *item = &player.SpdList[itemIndex]; + if (!item->isEmpty() && isCurrentSpell(*item)) { + player.RemoveSpdBarItem(itemIndex); + return; + } + } else if (itemSlot != 0) { + app_fatal(StrCat("ConsumeScroll: Invalid item index ", itemSlot)); + } + + // Didn't find it at the selected slot, take the first one we find + // This path is always used when the scroll is consumed via spell selection + RemoveInventoryOrBeltItem(player, isCurrentSpell); +} + +bool CanUseScroll(Player &player, SpellID spell) +{ + if (leveltype == DTYPE_TOWN && !GetSpellData(spell).isAllowedInTown()) + return false; + + return HasInventoryOrBeltItem(player, [spell](const Item &item) { + return item.isScrollOf(spell) || item.isRuneOf(spell); + }); +} + +void ConsumeStaffCharge(Player &player) +{ + Item &staff = player.InvBody[INVLOC_HAND_LEFT]; + + if (!CanUseStaff(staff, player.executedSpell.spellId)) + return; + + staff._iCharges--; + CalcPlrInv(player, false); +} + +bool CanUseStaff(Player &player, SpellID spellId) +{ + return CanUseStaff(player.InvBody[INVLOC_HAND_LEFT], spellId); +} + +Item &GetInventoryItem(Player &player, int location) +{ + if (location < INVITEM_INV_FIRST) + return player.InvBody[location]; + + if (location <= INVITEM_INV_LAST) + return player.InvList[location - INVITEM_INV_FIRST]; + + return player.SpdList[location - INVITEM_BELT_FIRST]; +} + +bool UseInvItem(int cii) +{ + if (IsInspectingPlayer()) + return false; + + Player &player = *MyPlayer; + + if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) + return true; + if (pcurs != CURSOR_HAND) + return true; + if (IsPlayerInStore()) + return true; + if (cii < INVITEM_INV_FIRST) + return false; + + bool speedlist = false; + int c; + Item *item; + if (cii <= INVITEM_INV_LAST) { + c = cii - INVITEM_INV_FIRST; + item = &player.InvList[c]; + } else { + if (ChatFlag) + return true; + c = cii - INVITEM_BELT_FIRST; + + item = &player.SpdList[c]; + speedlist = true; + + // If selected speedlist item exists in InvList, use the InvList item. + for (int i = 0; i < player._pNumInv && *GetOptions().Gameplay.autoRefillBelt; i++) { + if (player.InvList[i]._iMiscId == item->_iMiscId && player.InvList[i]._iSpell == item->_iSpell) { + c = i; + item = &player.InvList[c]; + cii = c + INVITEM_INV_FIRST; + speedlist = false; + break; + } + } + + // If speedlist item is not inventory, use same item at the end of the speedlist if exists. + if (speedlist && *GetOptions().Gameplay.autoRefillBelt) { + for (int i = INVITEM_BELT_LAST - INVITEM_BELT_FIRST; i > c; i--) { + Item &candidate = player.SpdList[i]; + + if (!candidate.isEmpty() && candidate._iMiscId == item->_iMiscId && candidate._iSpell == item->_iSpell) { + c = i; + cii = c + INVITEM_BELT_FIRST; + item = &candidate; + break; + } + } + } + } + + constexpr int SpeechDelay = 10; + if (item->IDidx == IDI_MUSHROOM) { + player.Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); + return true; + } + if (item->IDidx == IDI_FUNGALTM) { + + PlaySFX(SfxID::ItemBook); + player.Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); + return true; + } + + if (player.isOnLevel(0)) { + if (UseItemOpensHive(*item, player.position.tile)) { + OpenHive(); + player.RemoveInvItem(c); + return true; + } + if (UseItemOpensGrave(*item, player.position.tile)) { + OpenGrave(); + player.RemoveInvItem(c); + return true; + } + } + + if (!item->isUsable()) + return false; + + if (!player.CanUseItem(*item)) { + player.Say(HeroSpeech::ICantUseThisYet); + return true; + } + + if (item->_iMiscId == IMISC_NONE && item->_itype == ItemType::Gold) { + StartGoldDrop(); + return true; + } + + CloseGoldDrop(); + + if (item->isScroll() && leveltype == DTYPE_TOWN && !GetSpellData(item->_iSpell).isAllowedInTown()) { + return true; + } + + if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { + return true; + } + + if (item->_iMiscId == IMISC_ARENAPOT && !player.isOnArenaLevel()) { + player.Say(HeroSpeech::ThatWontWorkHere); + return true; + } + + const int idata = ItemCAnimTbl[item->_iCurs]; + if (item->_iMiscId == IMISC_BOOK) + PlaySFX(SfxID::ReadBook); + else if (&player == MyPlayer) + PlaySFX(ItemInvSnds[idata]); + + UseItem(player, item->_iMiscId, item->_iSpell, cii); + + if (speedlist) { + if (player.SpdList[c]._iMiscId == IMISC_NOTE) { + InitQTextMsg(TEXT_BOOK9); + CloseInventory(); + return true; + } + if (!item->isScroll() && !item->isRune()) + player.RemoveSpdBarItem(c); + return true; + } + if (player.InvList[c]._iMiscId == IMISC_MAPOFDOOM) + return true; + if (player.InvList[c]._iMiscId == IMISC_NOTE) { + InitQTextMsg(TEXT_BOOK9); + CloseInventory(); + return true; + } + if (!item->isScroll() && !item->isRune()) + player.RemoveInvItem(c); + + return true; +} + +void CloseInventory() +{ + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); + invflag = false; +} + +void CloseStash() +{ + if (!IsStashOpen) + return; + + Player &myPlayer = *MyPlayer; + if (!myPlayer.HoldItem.isEmpty()) { + std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); + if (itemTile) { + NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); + } else { + if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) + && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true) + && !AutoPlaceItemInStash(myPlayer.HoldItem, true)) { + // This can fail for max gold, arena potions and a stash that has been arranged + // to not have room for the item all 3 cases are extremely unlikely + app_fatal(_("No room for item")); + } + PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); + } + myPlayer.HoldItem.clear(); + NewCursor(CURSOR_HAND); + } + + IsStashOpen = false; +} + +void DoTelekinesis() +{ + if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) + NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJT, cursPosition); + if (pcursitem != -1) + NetSendCmdGItem(true, CMD_REQUESTAGITEM, *MyPlayer, pcursitem); + if (pcursmonst != -1) { + const Monster &monter = Monsters[pcursmonst]; + if (!M_Talker(monter) && monter.talkMsg == TEXT_NONE) + NetSendCmdParam1(true, CMD_KNOCKBACK, pcursmonst); + } + NewCursor(CURSOR_HAND); +} + +int CalculateGold(Player &player) +{ + int gold = 0; + + for (int i = 0; i < player._pNumInv; i++) { + if (player.InvList[i]._itype == ItemType::Gold) + gold += player.InvList[i]._ivalue; + } + + return gold; +} + +Size GetInventorySize(const Item &item) +{ + const Size size = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); + + return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; +} + +} // namespace devilution diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index c3b88ae25c3..f2705f12400 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -1,947 +1,947 @@ -/** - * @file trigs.cpp - * - * Implementation of functionality for triggering events when the player enters an area. - */ -#include "levels/trigs.h" - -#include -#include - -#include - -#include "control/control.hpp" -#include "controls/control_mode.hpp" -#include "controls/plrctrls.h" -#include "cursor.h" -#include "diablo_msg.hpp" -#include "game_mode.hpp" -#include "multi.h" -#include "utils/algorithm/container.hpp" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/utf8.hpp" - -namespace devilution { - -bool trigflag; -int numtrigs; -TriggerStruct trigs[MAXTRIGGERS]; -int TWarpFrom; - -namespace { -/** Specifies the dungeon piece IDs which constitute stairways leading down to the cathedral from town. */ -const uint16_t TownDownList[] = { 715, 714, 718, 719, 720, 722, 723, 724, 725, 726 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down to the catacombs from town. */ -const uint16_t TownWarp1List[] = { 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1180, 1182, 1184 }; -const uint16_t TownCryptList[] = { 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337 }; -const uint16_t TownHiveList[] = { 1306, 1307, 1308, 1309 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the cathedral. */ -const uint16_t L1UpList[] = { 126, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the cathedral. */ -const uint16_t L1DownList[] = { 105, 106, 107, 108, 109, 111, 113, 114, 117 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the catacombs. */ -const uint16_t L2UpList[] = { 265, 266 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the catacombs. */ -const uint16_t L2DownList[] = { 268, 269, 270, 271 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the catacombs. */ -const uint16_t L2TWarpUpList[] = { 557, 558 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the caves. */ -const uint16_t L3UpList[] = { 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the caves. */ -const uint16_t L3DownList[] = { 161, 162, 163, 164, 165, 166, 167, 168 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the caves. */ -const uint16_t L3TWarpUpList[] = { 181, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from hell. */ -const uint16_t L4UpList[] = { 81, 82, 89 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from hell. */ -const uint16_t L4DownList[] = { 119, 129, 130, 131, 132 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from hell. */ -const uint16_t L4TWarpUpList[] = { 420, 421, 428 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down to Diablo from hell. */ -const uint16_t L4PentaList[] = { 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383 }; -const uint16_t L5TWarpUpList[] = { 171, 172, 173, 174, 175, 176, 177, 178, 183 }; -const uint16_t L5UpList[] = { 148, 149, 150, 151, 152, 153, 154, 156, 157, 158 }; -const uint16_t L5DownList[] = { 124, 125, 128, 130, 131, 134, 135, 139, 141 }; -const uint16_t L6TWarpUpList[] = { 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }; -const uint16_t L6UpList[] = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77 }; -const uint16_t L6DownList[] = { 56, 57, 58, 59, 60, 61, 62, 63 }; -} // namespace - -void InitNoTriggers() -{ - numtrigs = 0; - trigflag = false; -} - -bool IsWarpOpen(dungeon_type type) -{ - if (gbIsSpawn) - return false; - - if (gbIsMultiplayer && type != DTYPE_NEST) // Opening the nest is part of in town quest - return true; - - const Player &myPlayer = *MyPlayer; - - if (type == DTYPE_CATACOMBS && (myPlayer.pTownWarps & 1) != 0) - return true; - if (type == DTYPE_CAVES && (myPlayer.pTownWarps & 2) != 0) - return true; - if (type == DTYPE_HELL && (myPlayer.pTownWarps & 4) != 0) - return true; - - if (gbIsHellfire) { - if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) - return true; - if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) - return true; - if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) - return true; - if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) - return true; - if (type == DTYPE_CRYPT && Quests[Q_GRAVE]._qactive == QUEST_DONE) - return true; - } - - return false; -} - -void InitTownTriggers() -{ - numtrigs = 0; - - // Cathedral - trigs[numtrigs].position = { 25, 29 }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - - if (IsWarpOpen(DTYPE_CATACOMBS)) { - trigs[numtrigs].position = { 49, 21 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 5; - numtrigs++; - } - if (IsWarpOpen(DTYPE_CAVES)) { - trigs[numtrigs].position = { 17, 69 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 9; - numtrigs++; - } - if (IsWarpOpen(DTYPE_HELL)) { - trigs[numtrigs].position = { 41, 80 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 13; - numtrigs++; - } - if (IsWarpOpen(DTYPE_NEST)) { - trigs[numtrigs].position = { 80, 62 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 17; - numtrigs++; - } - if (IsWarpOpen(DTYPE_CRYPT)) { - trigs[numtrigs].position = { 36, 24 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 21; - numtrigs++; - } - - trigflag = false; -} - -void InitL1Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 128) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - if (dPiece[i][j] == 114) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL2Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 266 && (!Quests[Q_SCHAMB].IsAvailable() || i != Quests[Q_SCHAMB].position.x || j != Quests[Q_SCHAMB].position.y)) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 558) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - - if (dPiece[i][j] == 270) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL3Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 170) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 167) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - - if (dPiece[i][j] == 548) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL4Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 82) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 421) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - - if (dPiece[i][j] == 119) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 369 && Quests[Q_BETRAYER]._qactive == QUEST_DONE) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitHiveTriggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 65) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 62) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - - if (dPiece[i][j] == 79) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitCryptTriggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 183) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - if (dPiece[i][j] == 157) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - if (dPiece[i][j] == 125) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitSKingTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 82, 42 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitSChambTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 70, 39 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitPWaterTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 30, 83 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitVPTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 35, 32 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -bool ForceTownTrig() -{ - for (const uint16_t tileId : TownDownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to dungeon"); - cursPosition = { 25, 29 }; - return true; - } - } - - if (IsWarpOpen(DTYPE_CATACOMBS)) { - for (const uint16_t tileId : TownWarp1List) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to catacombs"); - cursPosition = { 49, 21 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_CAVES)) { - for (uint16_t i = 1198; i <= 1219; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == i) { - InfoString = _("Down to caves"); - cursPosition = { 17, 69 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_HELL)) { - for (uint16_t i = 1239; i <= 1254; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == i) { - InfoString = _("Down to hell"); - cursPosition = { 41, 80 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_NEST)) { - for (const uint16_t tileId : TownHiveList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Hive"); - cursPosition = { 80, 62 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_CRYPT)) { - for (const uint16_t tileId : TownCryptList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Crypt"); - cursPosition = { 36, 24 }; - return true; - } - } - } - - return false; -} - -bool ForceL1Trig() -{ - for (const uint16_t tileId : L1UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - if (currlevel > 1) - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - else - InfoString = _("Up to town"); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - for (const uint16_t tileId : L1DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - return false; -} - -bool ForceL2Trig() -{ - for (const uint16_t tileId : L2UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - - for (const uint16_t tileId : L2DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 5) { - for (const uint16_t tileId : L2TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceL3Trig() -{ - for (const uint16_t tileId : L3UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - for (const uint16_t tileId : L3DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId - || dPiece[cursPosition.x + 1][cursPosition.y] == tileId - || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 9) { - for (const uint16_t tileId : L3TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceL4Trig() -{ - for (const uint16_t tileId : L4UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - for (const uint16_t tileId : L4DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 13) { - for (const uint16_t tileId : L4TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - if (currlevel == 15) { - for (const uint16_t tileId : L4PentaList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Diablo"); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - - return false; -} - -bool ForceHiveTrig() -{ - for (const uint16_t tileId : L6UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to Nest level {:d}")), currlevel - 17); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - for (const uint16_t tileId : L6DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId - || dPiece[cursPosition.x + 1][cursPosition.y] == tileId - || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel - 15); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 17) { - for (const uint16_t tileId : L6TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceCryptTrig() -{ - for (const uint16_t tileId : L5UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to Crypt level {:d}")), currlevel - 21); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - if (dPiece[cursPosition.x][cursPosition.y] == 316) { - InfoString = _("Cornerstone of the World"); - return true; - } - for (const uint16_t tileId : L5DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to Crypt level {:d}")), currlevel - 19); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - if (currlevel == 21) { - for (const uint16_t tileId : L5TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -void Freeupstairs() -{ - for (int i = 0; i < numtrigs; i++) { - const int tx = trigs[i].position.x; - const int ty = trigs[i].position.y; - - for (int yy = -2; yy <= 2; yy++) { - for (int xx = -2; xx <= 2; xx++) { - dFlags[tx + xx][ty + yy] |= DungeonFlag::Populated; - } - } - } -} - -bool ForceSKingTrig() -{ - for (const uint16_t tileId : L1UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SKELKING]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForceSChambTrig() -{ - for (const uint16_t tileId : L2DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SCHAMB]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForcePWaterTrig() -{ - for (const uint16_t tileId : L3DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_PWATER]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForceArenaTrig() -{ - const uint16_t *checkList; - size_t len; - switch (setlvltype) { - case DTYPE_TOWN: - checkList = TownWarp1List; - len = sizeof(TownWarp1List) / sizeof(TownWarp1List[0]); - break; - case DTYPE_CATHEDRAL: - checkList = L1UpList; - len = sizeof(L1UpList) / sizeof(L1UpList[0]); - break; - case DTYPE_CATACOMBS: - checkList = L2TWarpUpList; - len = sizeof(L2TWarpUpList) / sizeof(L2TWarpUpList[0]); - break; - case DTYPE_CAVES: - checkList = L3TWarpUpList; - len = sizeof(L3TWarpUpList) / sizeof(L3TWarpUpList[0]); - break; - case DTYPE_HELL: - checkList = L4TWarpUpList; - len = sizeof(L4TWarpUpList) / sizeof(L4TWarpUpList[0]); - break; - case DTYPE_NEST: - checkList = L5TWarpUpList; - len = sizeof(L5TWarpUpList) / sizeof(L5TWarpUpList[0]); - break; - case DTYPE_CRYPT: - checkList = L6TWarpUpList; - len = sizeof(L6TWarpUpList) / sizeof(L6TWarpUpList[0]); - break; - default: - return false; - } - for (size_t i = 0; i < len; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { - InfoString = _("Up to town"); - cursPosition = trigs[0].position; - return true; - } - } - - return false; -} - -void CheckTrigForce() -{ - trigflag = false; - - if (ControlMode == ControlTypes::KeyboardAndMouse && GetMainPanel().contains(MousePosition)) { - return; - } - - if (!setlevel) { - switch (leveltype) { - case DTYPE_TOWN: - trigflag = ForceTownTrig(); - break; - case DTYPE_CATHEDRAL: - trigflag = ForceL1Trig(); - break; - case DTYPE_CATACOMBS: - trigflag = ForceL2Trig(); - break; - case DTYPE_CAVES: - trigflag = ForceL3Trig(); - break; - case DTYPE_HELL: - trigflag = ForceL4Trig(); - break; - case DTYPE_NEST: - trigflag = ForceHiveTrig(); - break; - case DTYPE_CRYPT: - trigflag = ForceCryptTrig(); - break; - default: - break; - } - if (leveltype != DTYPE_TOWN && !trigflag) { - trigflag = ForceQuests(); - } - } else { - switch (setlvlnum) { - case SL_SKELKING: - trigflag = ForceSKingTrig(); - break; - case SL_BONECHAMB: - trigflag = ForceSChambTrig(); - break; - case SL_POISONWATER: - trigflag = ForcePWaterTrig(); - break; - default: - if (IsArenaLevel(setlvlnum)) - trigflag = ForceArenaTrig(); - break; - } - } -} - -void CheckTriggers() -{ - Player &myPlayer = *MyPlayer; - - if (myPlayer._pmode != PM_STAND) - return; - - for (int i = 0; i < numtrigs; i++) { - if (myPlayer.position.tile != trigs[i].position) { - continue; - } - - switch (trigs[i]._tmsg) { - case WM_DIABNEXTLVL: - if (gbIsSpawn && currlevel >= 2) { - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, { myPlayer.position.tile.x, myPlayer.position.tile.y + 1 }); - myPlayer.Say(HeroSpeech::NotAChance); - InitDiabloMsg(EMSG_NOT_IN_SHAREWARE); - } else { - StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel + 1); - } - break; - case WM_DIABPREVLVL: - StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel - 1); - break; - case WM_DIABRTNLVL: - StartNewLvl(myPlayer, trigs[i]._tmsg, GetMapReturnLevel()); - break; - case WM_DIABTOWNWARP: - if (gbIsMultiplayer) { - bool abort = false; - diablo_message abortflag; - - auto position = myPlayer.position.tile; - if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { - abort = true; - position.y += 1; - abortflag = EMSG_REQUIRES_LVL_8; - } - - if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { - abort = true; - position.x += 1; - abortflag = EMSG_REQUIRES_LVL_13; - } - - if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { - abort = true; - position.y += 1; - abortflag = EMSG_REQUIRES_LVL_17; - } - - if (abort) { - myPlayer.Say(HeroSpeech::ICantGetThereFromHere); - - InitDiabloMsg(abortflag); - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, position); - return; - } - } - - StartNewLvl(myPlayer, trigs[i]._tmsg, trigs[i]._tlvl); - break; - case WM_DIABTWARPUP: - TWarpFrom = currlevel; - StartNewLvl(myPlayer, trigs[i]._tmsg, 0); - break; - default: - app_fatal("Unknown trigger msg"); - } - } -} - -bool EntranceBoundaryContains(Point entrance, Point position) -{ - constexpr Displacement entranceOffsets[7] = { { 0, 0 }, { -1, 0 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -1, -2 }, { -2, -2 } }; - - return c_any_of( - entranceOffsets, - [=](Displacement offset) { return entrance + offset == position; }); -} - -} // namespace devilution +/** + * @file trigs.cpp + * + * Implementation of functionality for triggering events when the player enters an area. + */ +#include "levels/trigs.h" + +#include +#include + +#include + +#include "control/control.hpp" +#include "controls/control_mode.hpp" +#include "controls/plrctrls.h" +#include "cursor.h" +#include "diablo_msg.hpp" +#include "game_mode.hpp" +#include "multi.h" +#include "utils/algorithm/container.hpp" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/utf8.hpp" + +namespace devilution { + +bool trigflag; +int numtrigs; +TriggerStruct trigs[MAXTRIGGERS]; +int TWarpFrom; + +namespace { +/** Specifies the dungeon piece IDs which constitute stairways leading down to the cathedral from town. */ +const uint16_t TownDownList[] = { 715, 714, 718, 719, 720, 722, 723, 724, 725, 726 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down to the catacombs from town. */ +const uint16_t TownWarp1List[] = { 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1180, 1182, 1184 }; +const uint16_t TownCryptList[] = { 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337 }; +const uint16_t TownHiveList[] = { 1306, 1307, 1308, 1309 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the cathedral. */ +const uint16_t L1UpList[] = { 126, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the cathedral. */ +const uint16_t L1DownList[] = { 105, 106, 107, 108, 109, 111, 113, 114, 117 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the catacombs. */ +const uint16_t L2UpList[] = { 265, 266 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the catacombs. */ +const uint16_t L2DownList[] = { 268, 269, 270, 271 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the catacombs. */ +const uint16_t L2TWarpUpList[] = { 557, 558 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the caves. */ +const uint16_t L3UpList[] = { 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the caves. */ +const uint16_t L3DownList[] = { 161, 162, 163, 164, 165, 166, 167, 168 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the caves. */ +const uint16_t L3TWarpUpList[] = { 181, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from hell. */ +const uint16_t L4UpList[] = { 81, 82, 89 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from hell. */ +const uint16_t L4DownList[] = { 119, 129, 130, 131, 132 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from hell. */ +const uint16_t L4TWarpUpList[] = { 420, 421, 428 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down to Diablo from hell. */ +const uint16_t L4PentaList[] = { 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383 }; +const uint16_t L5TWarpUpList[] = { 171, 172, 173, 174, 175, 176, 177, 178, 183 }; +const uint16_t L5UpList[] = { 148, 149, 150, 151, 152, 153, 154, 156, 157, 158 }; +const uint16_t L5DownList[] = { 124, 125, 128, 130, 131, 134, 135, 139, 141 }; +const uint16_t L6TWarpUpList[] = { 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }; +const uint16_t L6UpList[] = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77 }; +const uint16_t L6DownList[] = { 56, 57, 58, 59, 60, 61, 62, 63 }; +} // namespace + +void InitNoTriggers() +{ + numtrigs = 0; + trigflag = false; +} + +bool IsWarpOpen(dungeon_type type) +{ + if (gbIsSpawn) + return false; + + if (gbIsMultiplayer && type != DTYPE_NEST) // Opening the nest is part of in town quest + return true; + + const Player &myPlayer = *MyPlayer; + + if (type == DTYPE_CATACOMBS && (myPlayer.pTownWarps & 1) != 0) + return true; + if (type == DTYPE_CAVES && (myPlayer.pTownWarps & 2) != 0) + return true; + if (type == DTYPE_HELL && (myPlayer.pTownWarps & 4) != 0) + return true; + + if (gbIsHellfire) { + if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) + return true; + if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) + return true; + if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) + return true; + if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) + return true; + if (type == DTYPE_CRYPT && Quests[Q_GRAVE]._qactive == QUEST_DONE) + return true; + } + + return false; +} + +void InitTownTriggers() +{ + numtrigs = 0; + + // Cathedral + trigs[numtrigs].position = { 25, 29 }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + + if (IsWarpOpen(DTYPE_CATACOMBS)) { + trigs[numtrigs].position = { 49, 21 }; + trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; + trigs[numtrigs]._tlvl = 5; + numtrigs++; + } + if (IsWarpOpen(DTYPE_CAVES)) { + trigs[numtrigs].position = { 17, 69 }; + trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; + trigs[numtrigs]._tlvl = 9; + numtrigs++; + } + if (IsWarpOpen(DTYPE_HELL)) { + trigs[numtrigs].position = { 41, 80 }; + trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; + trigs[numtrigs]._tlvl = 13; + numtrigs++; + } + if (IsWarpOpen(DTYPE_NEST)) { + trigs[numtrigs].position = { 80, 62 }; + trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; + trigs[numtrigs]._tlvl = 17; + numtrigs++; + } + if (IsWarpOpen(DTYPE_CRYPT)) { + trigs[numtrigs].position = { 36, 24 }; + trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; + trigs[numtrigs]._tlvl = 21; + numtrigs++; + } + + trigflag = false; +} + +void InitL1Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 128) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + if (dPiece[i][j] == 114) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL2Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 266 && (!Quests[Q_SCHAMB].IsAvailable() || i != Quests[Q_SCHAMB].position.x || j != Quests[Q_SCHAMB].position.y)) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 558) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + + if (dPiece[i][j] == 270) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL3Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 170) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 167) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + + if (dPiece[i][j] == 548) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL4Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 82) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 421) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + + if (dPiece[i][j] == 119) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 369 && Quests[Q_BETRAYER]._qactive == QUEST_DONE) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitHiveTriggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 65) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 62) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + + if (dPiece[i][j] == 79) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitCryptTriggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 183) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + if (dPiece[i][j] == 157) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + if (dPiece[i][j] == 125) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitSKingTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 82, 42 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitSChambTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 70, 39 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitPWaterTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 30, 83 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitVPTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 35, 32 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +bool ForceTownTrig() +{ + for (const uint16_t tileId : TownDownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to dungeon"); + cursPosition = { 25, 29 }; + return true; + } + } + + if (IsWarpOpen(DTYPE_CATACOMBS)) { + for (const uint16_t tileId : TownWarp1List) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to catacombs"); + cursPosition = { 49, 21 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_CAVES)) { + for (uint16_t i = 1198; i <= 1219; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == i) { + InfoString = _("Down to caves"); + cursPosition = { 17, 69 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_HELL)) { + for (uint16_t i = 1239; i <= 1254; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == i) { + InfoString = _("Down to hell"); + cursPosition = { 41, 80 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_NEST)) { + for (const uint16_t tileId : TownHiveList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Hive"); + cursPosition = { 80, 62 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_CRYPT)) { + for (const uint16_t tileId : TownCryptList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Crypt"); + cursPosition = { 36, 24 }; + return true; + } + } + } + + return false; +} + +bool ForceL1Trig() +{ + for (const uint16_t tileId : L1UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + if (currlevel > 1) + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + else + InfoString = _("Up to town"); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + for (const uint16_t tileId : L1DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + return false; +} + +bool ForceL2Trig() +{ + for (const uint16_t tileId : L2UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + + for (const uint16_t tileId : L2DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 5) { + for (const uint16_t tileId : L2TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceL3Trig() +{ + for (const uint16_t tileId : L3UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + for (const uint16_t tileId : L3DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId + || dPiece[cursPosition.x + 1][cursPosition.y] == tileId + || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 9) { + for (const uint16_t tileId : L3TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceL4Trig() +{ + for (const uint16_t tileId : L4UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + for (const uint16_t tileId : L4DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 13) { + for (const uint16_t tileId : L4TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + if (currlevel == 15) { + for (const uint16_t tileId : L4PentaList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Diablo"); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + + return false; +} + +bool ForceHiveTrig() +{ + for (const uint16_t tileId : L6UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to Nest level {:d}")), currlevel - 17); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + for (const uint16_t tileId : L6DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId + || dPiece[cursPosition.x + 1][cursPosition.y] == tileId + || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel - 15); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 17) { + for (const uint16_t tileId : L6TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceCryptTrig() +{ + for (const uint16_t tileId : L5UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to Crypt level {:d}")), currlevel - 21); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + if (dPiece[cursPosition.x][cursPosition.y] == 316) { + InfoString = _("Cornerstone of the World"); + return true; + } + for (const uint16_t tileId : L5DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to Crypt level {:d}")), currlevel - 19); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + if (currlevel == 21) { + for (const uint16_t tileId : L5TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +void Freeupstairs() +{ + for (int i = 0; i < numtrigs; i++) { + const int tx = trigs[i].position.x; + const int ty = trigs[i].position.y; + + for (int yy = -2; yy <= 2; yy++) { + for (int xx = -2; xx <= 2; xx++) { + dFlags[tx + xx][ty + yy] |= DungeonFlag::Populated; + } + } + } +} + +bool ForceSKingTrig() +{ + for (const uint16_t tileId : L1UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SKELKING]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForceSChambTrig() +{ + for (const uint16_t tileId : L2DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SCHAMB]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForcePWaterTrig() +{ + for (const uint16_t tileId : L3DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_PWATER]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForceArenaTrig() +{ + const uint16_t *checkList; + size_t len; + switch (setlvltype) { + case DTYPE_TOWN: + checkList = TownWarp1List; + len = sizeof(TownWarp1List) / sizeof(TownWarp1List[0]); + break; + case DTYPE_CATHEDRAL: + checkList = L1UpList; + len = sizeof(L1UpList) / sizeof(L1UpList[0]); + break; + case DTYPE_CATACOMBS: + checkList = L2TWarpUpList; + len = sizeof(L2TWarpUpList) / sizeof(L2TWarpUpList[0]); + break; + case DTYPE_CAVES: + checkList = L3TWarpUpList; + len = sizeof(L3TWarpUpList) / sizeof(L3TWarpUpList[0]); + break; + case DTYPE_HELL: + checkList = L4TWarpUpList; + len = sizeof(L4TWarpUpList) / sizeof(L4TWarpUpList[0]); + break; + case DTYPE_NEST: + checkList = L5TWarpUpList; + len = sizeof(L5TWarpUpList) / sizeof(L5TWarpUpList[0]); + break; + case DTYPE_CRYPT: + checkList = L6TWarpUpList; + len = sizeof(L6TWarpUpList) / sizeof(L6TWarpUpList[0]); + break; + default: + return false; + } + for (size_t i = 0; i < len; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { + InfoString = _("Up to town"); + cursPosition = trigs[0].position; + return true; + } + } + + return false; +} + +void CheckTrigForce() +{ + trigflag = false; + + if (ControlMode == ControlTypes::KeyboardAndMouse && GetMainPanel().contains(MousePosition)) { + return; + } + + if (!setlevel) { + switch (leveltype) { + case DTYPE_TOWN: + trigflag = ForceTownTrig(); + break; + case DTYPE_CATHEDRAL: + trigflag = ForceL1Trig(); + break; + case DTYPE_CATACOMBS: + trigflag = ForceL2Trig(); + break; + case DTYPE_CAVES: + trigflag = ForceL3Trig(); + break; + case DTYPE_HELL: + trigflag = ForceL4Trig(); + break; + case DTYPE_NEST: + trigflag = ForceHiveTrig(); + break; + case DTYPE_CRYPT: + trigflag = ForceCryptTrig(); + break; + default: + break; + } + if (leveltype != DTYPE_TOWN && !trigflag) { + trigflag = ForceQuests(); + } + } else { + switch (setlvlnum) { + case SL_SKELKING: + trigflag = ForceSKingTrig(); + break; + case SL_BONECHAMB: + trigflag = ForceSChambTrig(); + break; + case SL_POISONWATER: + trigflag = ForcePWaterTrig(); + break; + default: + if (IsArenaLevel(setlvlnum)) + trigflag = ForceArenaTrig(); + break; + } + } +} + +void CheckTriggers() +{ + Player &myPlayer = *MyPlayer; + + if (myPlayer._pmode != PM_STAND) + return; + + for (int i = 0; i < numtrigs; i++) { + if (myPlayer.position.tile != trigs[i].position) { + continue; + } + + switch (trigs[i]._tmsg) { + case WM_DIABNEXTLVL: + if (gbIsSpawn && currlevel >= 2) { + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, { myPlayer.position.tile.x, myPlayer.position.tile.y + 1 }); + myPlayer.Say(HeroSpeech::NotAChance); + InitDiabloMsg(EMSG_NOT_IN_SHAREWARE); + } else { + StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel + 1); + } + break; + case WM_DIABPREVLVL: + StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel - 1); + break; + case WM_DIABRTNLVL: + StartNewLvl(myPlayer, trigs[i]._tmsg, GetMapReturnLevel()); + break; + case WM_DIABTOWNWARP: + if (gbIsMultiplayer) { + bool abort = false; + diablo_message abortflag; + + auto position = myPlayer.position.tile; + if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { + abort = true; + position.y += 1; + abortflag = EMSG_REQUIRES_LVL_8; + } + + if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { + abort = true; + position.x += 1; + abortflag = EMSG_REQUIRES_LVL_13; + } + + if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { + abort = true; + position.y += 1; + abortflag = EMSG_REQUIRES_LVL_17; + } + + if (abort) { + myPlayer.Say(HeroSpeech::ICantGetThereFromHere); + + InitDiabloMsg(abortflag); + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, position); + return; + } + } + + StartNewLvl(myPlayer, trigs[i]._tmsg, trigs[i]._tlvl); + break; + case WM_DIABTWARPUP: + TWarpFrom = currlevel; + StartNewLvl(myPlayer, trigs[i]._tmsg, 0); + break; + default: + app_fatal("Unknown trigger msg"); + } + } +} + +bool EntranceBoundaryContains(Point entrance, Point position) +{ + constexpr Displacement entranceOffsets[7] = { { 0, 0 }, { -1, 0 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -1, -2 }, { -2, -2 } }; + + return c_any_of( + entranceOffsets, + [=](Displacement offset) { return entrance + offset == position; }); +} + +} // namespace devilution diff --git a/Source/lua/lua_global.cpp b/Source/lua/lua_global.cpp index da24610b8e0..4c7c67409ed 100644 --- a/Source/lua/lua_global.cpp +++ b/Source/lua/lua_global.cpp @@ -37,6 +37,8 @@ #include "lua/modules/render.hpp" #include "lua/modules/system.hpp" #include "lua/modules/towners.hpp" +#include "lua/modules/towns.hpp" +#include "monster.h" #include "options.h" #include "plrmsg.h" #include "stores.h" @@ -312,6 +314,7 @@ void LuaInitialize() "devilutionx.player", LuaPlayerModule(lua), "devilutionx.render", LuaRenderModule(lua), "devilutionx.towners", LuaTownersModule(lua), + "devilutionx.towns", LuaTownsModule(lua), "devilutionx.hellfire", LuaHellfireModule(lua), "devilutionx.system", LuaSystemModule(lua), "devilutionx.floatingnumbers", LuaFloatingNumbersModule(lua), diff --git a/Source/lua/lua_global.hpp b/Source/lua/lua_global.hpp index d7812f1a74f..a14a7951797 100644 --- a/Source/lua/lua_global.hpp +++ b/Source/lua/lua_global.hpp @@ -1,23 +1,23 @@ -#pragma once - -#include - -#include -#include -#include - -namespace devilution { - -void LuaInitialize(); -void LuaReloadActiveMods(); -void LuaShutdown(); - -sol::state &GetLuaState(); -sol::environment CreateLuaSandbox(); -sol::object SafeCallResult(sol::protected_function_result result, bool optional); -sol::table *GetLuaEvents(); - -/** Adds a handler to be called when mods status changes after the initial startup. */ -void AddModsChangedHandler(tl::function_ref callback); - -} // namespace devilution +#pragma once + +#include + +#include +#include +#include + +namespace devilution { + +void LuaInitialize(); +void LuaReloadActiveMods(); +void LuaShutdown(); + +sol::state &GetLuaState(); +sol::environment CreateLuaSandbox(); +sol::object SafeCallResult(sol::protected_function_result result, bool optional); +sol::table *GetLuaEvents(); + +/** 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/towns.cpp b/Source/lua/modules/towns.cpp new file mode 100644 index 00000000000..b7e02dd758c --- /dev/null +++ b/Source/lua/modules/towns.cpp @@ -0,0 +1,151 @@ +#include "lua/modules/towns.hpp" + +#include + +#include "interfac.h" +#include "levels/town_data.h" +#include "lua/metadoc.hpp" +#include "msg.h" +#include "player.h" +#include "utils/log.hpp" + +namespace devilution { + +namespace { + +std::string LuaRegisterTown(std::string_view townId, const sol::table &config) +{ + TownConfig townConfig; + sol::optional name = config["name"]; + townConfig.name = name.has_value() ? *name : std::string(townId); + sol::optional saveId = config["saveId"]; + townConfig.saveId = saveId.has_value() ? static_cast(*saveId) : 0; + + if (sol::optional bounds = config["bounds"]) { + if (sol::optional min = (*bounds)["min"]) { + sol::optional minX = (*min)["x"]; + sol::optional minY = (*min)["y"]; + townConfig.dminPosition = { minX.value_or(10), minY.value_or(10) }; + } + if (sol::optional max = (*bounds)["max"]) { + sol::optional maxX = (*max)["x"]; + sol::optional maxY = (*max)["y"]; + townConfig.dmaxPosition = { maxX.value_or(84), maxY.value_or(84) }; + } + } + + sol::optional solFile = config["sol"]; + townConfig.solFile = solFile.value_or(""); + + if (sol::optional sectors = config["sectors"]) { + for (const auto &kv : *sectors) { + sol::table sector = kv.second.as(); + TownSector s; + sol::optional path = sector["path"]; + s.filePath = path.value_or(""); + sol::optional sx = sector["x"]; + sol::optional sy = sector["y"]; + s.x = sx.value_or(0); + s.y = sy.value_or(0); + townConfig.sectors.push_back(s); + } + } + + if (sol::optional entries = config["entries"]) { + for (const auto &kv : *entries) { + sol::table entry = kv.second.as(); + TownEntryPoint ep; + sol::optional typeStr = entry["type"]; + std::string type = typeStr.value_or("main"); + if (type == "prev") ep.entryType = ENTRY_PREV; + else if (type == "twarpdn") ep.entryType = ENTRY_TWARPDN; + else if (type == "twarpup") ep.entryType = ENTRY_TWARPUP; + else if (type == "townswitch") ep.entryType = ENTRY_TOWNSWITCH; + else ep.entryType = ENTRY_MAIN; + sol::optional ex = entry["x"]; + sol::optional ey = entry["y"]; + sol::optional warpFrom = entry["warpFrom"]; + ep.viewPosition = { ex.value_or(75), ey.value_or(68) }; + ep.warpFromLevel = warpFrom.value_or(-1); + townConfig.entries.push_back(ep); + } + } + + if (sol::optional towners = config["towners"]) { + for (const auto &kv : *towners) { + sol::table t = kv.second.as(); + TownerPositionOverride override; + sol::optional tName = t["name"]; + override.shortName = tName.value_or(""); + sol::optional tx = t["x"]; + sol::optional ty = t["y"]; + override.position = { tx.value_or(0), ty.value_or(0) }; + townConfig.townerOverrides.push_back(override); + } + } + + std::string townIdStr(townId); + GetTownRegistry().RegisterTown(townIdStr, townConfig); + return townIdStr; +} + +void LuaTravelToTown(std::string_view townId) +{ + std::string townIdStr(townId); + + if (!GetTownRegistry().HasTown(townIdStr)) { + LogError("Town '{}' not registered", townId); + return; + } + + DestinationTownID = townIdStr; + + if (gbIsMultiplayer) { + NetSendCmdTownTravel(0xFFFFFFFF, townIdStr.c_str()); + return; + } + + if (MyPlayer != nullptr) { + MyPlayer->_pInvincible = true; + SDL_Event event; + CustomEventToSdlEvent(event, WM_DIABTOWNSWITCH); + SDL_PushEvent(&event); + } +} + +std::string LuaGetCurrentTown() +{ + return GetTownRegistry().GetCurrentTown(); +} + +bool LuaHasTown(std::string_view townId) +{ + return GetTownRegistry().HasTown(std::string(townId)); +} + +} // namespace + +sol::table LuaTownsModule(sol::state_view &lua) +{ + sol::table table = lua.create_table(); + + LuaSetDocFn(table, "register", "(townId: string, config: table) -> string", + "Registers a new town from a config table. Returns town ID.", + LuaRegisterTown); + + LuaSetDocFn(table, "travel", "(townId: string)", + "Travels to the specified town.", + LuaTravelToTown); + + LuaSetDocFn(table, "current", "() -> string", + "Returns the current town ID.", + LuaGetCurrentTown); + + LuaSetDocFn(table, "exists", "(townId: string) -> boolean", + "Checks if a town is registered.", + LuaHasTown); + + return table; +} + +} // namespace devilution diff --git a/Source/lua/modules/towns.hpp b/Source/lua/modules/towns.hpp new file mode 100644 index 00000000000..878fc3d926e --- /dev/null +++ b/Source/lua/modules/towns.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace devilution { + +/** + * @brief Creates and returns the Lua towns module + * + * Exposes town registration and travel functionality to Lua mods. + * + * @param lua The Sol2 state view + * @return sol::table The towns module table + */ +sol::table LuaTownsModule(sol::state_view &lua); + +} // namespace devilution diff --git a/Source/utils/screen_reader.cpp b/Source/utils/screen_reader.cpp index 2c75491cf76..d080c24ce63 100644 --- a/Source/utils/screen_reader.cpp +++ b/Source/utils/screen_reader.cpp @@ -1,55 +1,55 @@ -#include "utils/screen_reader.hpp" - -#include -#include - -#ifdef _WIN32 -#include "utils/file_util.h" -#include -#else -#include -#endif - -namespace devilution { - -#ifndef _WIN32 -SPDConnection *Speechd; -#endif - -void InitializeScreenReader() -{ -#ifdef _WIN32 - Tolk_Load(); -#else - Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); -#endif -} - -void ShutDownScreenReader() -{ -#ifdef _WIN32 - Tolk_Unload(); -#else - spd_close(Speechd); -#endif -} - -void SpeakText(std::string_view text, bool force) -{ - static std::string SpokenText; - - if (!force && SpokenText == text) - return; - - SpokenText = text; - -#ifdef _WIN32 - const auto textUtf16 = ToWideChar(SpokenText); - if (textUtf16 != nullptr) - Tolk_Output(textUtf16.get(), true); -#else - spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); -#endif -} - -} // namespace devilution +#include "utils/screen_reader.hpp" + +#include +#include + +#ifdef _WIN32 +#include "utils/file_util.h" +#include +#else +#include +#endif + +namespace devilution { + +#ifndef _WIN32 +SPDConnection *Speechd; +#endif + +void InitializeScreenReader() +{ +#ifdef _WIN32 + Tolk_Load(); +#else + Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); +#endif +} + +void ShutDownScreenReader() +{ +#ifdef _WIN32 + Tolk_Unload(); +#else + spd_close(Speechd); +#endif +} + +void SpeakText(std::string_view text, bool force) +{ + static std::string SpokenText; + + if (!force && SpokenText == text) + return; + + SpokenText = text; + +#ifdef _WIN32 + const auto textUtf16 = ToWideChar(SpokenText); + if (textUtf16 != nullptr) + Tolk_Output(textUtf16.get(), true); +#else + spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); +#endif +} + +} // namespace devilution diff --git a/Source/utils/screen_reader.hpp b/Source/utils/screen_reader.hpp index 5be8f97aa69..84966a57bdd 100644 --- a/Source/utils/screen_reader.hpp +++ b/Source/utils/screen_reader.hpp @@ -1,25 +1,25 @@ -#pragma once - -#include - -namespace devilution { - -#ifdef SCREEN_READER_INTEGRATION -void InitializeScreenReader(); -void ShutDownScreenReader(); -void SpeakText(std::string_view text, bool force = false); -#else -constexpr void InitializeScreenReader() -{ -} - -constexpr void ShutDownScreenReader() -{ -} - -constexpr void SpeakText(std::string_view text, bool force = false) -{ -} -#endif - -} // namespace devilution +#pragma once + +#include + +namespace devilution { + +#ifdef SCREEN_READER_INTEGRATION +void InitializeScreenReader(); +void ShutDownScreenReader(); +void SpeakText(std::string_view text, bool force = false); +#else +constexpr void InitializeScreenReader() +{ +} + +constexpr void ShutDownScreenReader() +{ +} + +constexpr void SpeakText(std::string_view text, bool force = false) +{ +} +#endif + +} // namespace devilution From 7d5c03bc2d4cfd119701a91b0760d8d12f8953c4 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 03/13] Add WM_DIABTOWNSWITCH for town transitions --- Source/interfac.cpp | 25 +++++++++++++++++++++++++ Source/interfac.h | 1 + 2 files changed, 26 insertions(+) diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 7b5b470810c..b9d62f58cfd 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -35,6 +35,7 @@ #include "game_mode.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" +#include "levels/town_data.h" #include "loadsave.h" #include "multi.h" #include "pfile.h" @@ -103,6 +104,7 @@ Cutscenes PickCutscene(interface_mode uMsg) case WM_DIABNEWGAME: return CutStart; case WM_DIABRETOWN: + case WM_DIABTOWNSWITCH: return CutTown; case WM_DIABNEXTLVL: case WM_DIABPREVLVL: @@ -462,6 +464,29 @@ void DoLoad(interface_mode uMsg) loadResult = LoadGameLevel(false, ENTRY_MAIN); if (loadResult.has_value()) IncProgress(); break; + case WM_DIABTOWNSWITCH: + IncProgress(); + if (!gbIsMultiplayer) { + pfile_save_level(); + } else { + DeltaSaveLevel(); + } + IncProgress(); + FreeGameMem(); + GetTownRegistry().SetCurrentTown(DestinationTownID); + + if (MyPlayer != nullptr) { + const TownConfig &destTown = GetTownRegistry().GetTown(DestinationTownID); + MyPlayer->_pCurrentTownId = destTown.saveId; + } + + setlevel = false; + currlevel = 0; + leveltype = DTYPE_TOWN; + IncProgress(); + loadResult = LoadGameLevel(false, ENTRY_TOWNSWITCH); + if (loadResult.has_value()) IncProgress(); + break; default: loadResult = tl::make_unexpected("Unknown progress mode"); break; diff --git a/Source/interfac.h b/Source/interfac.h index ee4ac6c9256..466640c3b8b 100644 --- a/Source/interfac.h +++ b/Source/interfac.h @@ -30,6 +30,7 @@ enum interface_mode : uint8_t { WM_DIABTOWNWARP, WM_DIABTWARPUP, WM_DIABRETOWN, + WM_DIABTOWNSWITCH, WM_DIABNEWGAME, WM_DIABLOADGAME, From 7ad0b000cf0d167ae3e23515f4f04d0b356da9e6 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 04/13] Add CMD_TOWNTRAVEL for multiplayer town sync --- Source/msg.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ Source/msg.h | 5 +++++ 2 files changed, 49 insertions(+) diff --git a/Source/msg.cpp b/Source/msg.cpp index 21d0e7207d4..299ad9c45d0 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -37,6 +37,7 @@ #include "items/validation.h" #include "levels/crypt.h" #include "levels/town.h" +#include "levels/town_data.h" #include "levels/trigs.h" #include "lighting.h" #include "missiles.h" @@ -2458,6 +2459,38 @@ size_t OnSetVitality(const TCmdParam1 &message, Player &player) return sizeof(message); } +size_t OnTownTravel(const TCmd &cmd, size_t maxCmdSize, Player & /*player*/) +{ + const auto &message = reinterpret_cast(cmd); + const size_t headerSize = sizeof(message) - sizeof(message.str); + const size_t maxLength = std::min(MAX_SEND_STR_LEN, maxCmdSize - headerSize); + const std::string_view str { message.str, maxLength }; + const auto tokens = SplitByChar(str, '\0'); + const std::string_view townId = *tokens.begin(); + + if (gbBufferMsgs == 0) { + // Process town travel on all clients + std::string townIdStr(townId); + if (GetTownRegistry().HasTown(townIdStr)) { + DestinationTownID = townIdStr; + LogInfo("Network: Received town travel to '{}'", townId); + + // Trigger town switch for this client + if (MyPlayer != nullptr) { + MyPlayer->_pInvincible = true; + SDL_Event event; + CustomEventToSdlEvent(event, WM_DIABTOWNSWITCH); + SDL_PushEvent(&event); + } + } else { + LogError("Network: Unknown town ID '{}'", townId); + } + } + + const size_t nullSize = str.size() != townId.size() ? 1 : 0; + return headerSize + townId.size() + nullSize; +} + size_t OnString(const TCmd &cmd, size_t maxCmdSize, Player &player) { const auto &message = reinterpret_cast(cmd); @@ -3310,6 +3343,15 @@ void NetSendCmdString(uint32_t pmask, const char *pszStr) multi_send_msg_packet(pmask, reinterpret_cast(&cmd), strlen(cmd.str) + 2); } +void NetSendCmdTownTravel(uint32_t pmask, const char *townId) +{ + TCmdString cmd; + + cmd.bCmd = CMD_TOWNTRAVEL; + CopyUtf8(cmd.str, townId, sizeof(cmd.str)); + multi_send_msg_packet(pmask, reinterpret_cast(&cmd), strlen(cmd.str) + 2); +} + void delta_close_portal(const Player &player) { memset(&sgJunk.portal[player.getId()], 0xFF, sizeof(sgJunk.portal[player.getId()])); @@ -3453,6 +3495,8 @@ size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize) return OnDeactivatePortal(*pCmd, player); case CMD_RETOWN: return OnRestartTown(*pCmd, player); + case CMD_TOWNTRAVEL: + return OnTownTravel(*pCmd, maxCmdSize, player); case CMD_SETSTR: return HandleCmd(OnSetStrength, player, pCmd, maxCmdSize); case CMD_SETMAG: diff --git a/Source/msg.h b/Source/msg.h index 10b6131b6bc..e49ae453824 100644 --- a/Source/msg.h +++ b/Source/msg.h @@ -371,6 +371,10 @@ enum _cmd_id : uint8_t { // // body (TCmd) CMD_RETOWN, + // Travel to a different town (multi-town system). + // + // body (TCmdString): townId (null-terminated string, max 32 chars) + CMD_TOWNTRAVEL, // Cast spell with direction at target location (e.g. firewall). // // body (TCmdLocParam5): @@ -759,6 +763,7 @@ void NetSendCmdChBeltItem(bool bHiPri, int beltIndex); void NetSendCmdDamage(bool bHiPri, const Player &player, uint32_t dwDam, DamageType damageType); void NetSendCmdMonDmg(bool bHiPri, uint16_t wMon, uint32_t dwDam); void NetSendCmdString(uint32_t pmask, const char *pszStr); +void NetSendCmdTownTravel(uint32_t pmask, const char *townId); void delta_close_portal(const Player &player); bool ValidateCmdSize(size_t requiredCmdSize, size_t maxCmdSize, size_t playerId); size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize); From aa74d14b4a3683869f340f7bdf56182cbbae1abd Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 05/13] Persist current and visited towns in save games --- Source/loadsave.cpp | 19 +++++++++++++++++-- Source/player.cpp | 3 +++ Source/player.h | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index b95ad62498a..8ee64bdef97 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -26,6 +26,7 @@ #include "game_mode.hpp" #include "inv.h" #include "levels/dun_tile.hpp" +#include "levels/town_data.h" #include "lighting.h" #include "menu.h" #include "missiles.h" @@ -630,7 +631,9 @@ void LoadPlayer(LoadHelper &file, Player &player) } file.Skip(2); // Available bytes player.wReflections = file.NextLE(); - file.Skip(14); // Available bytes + // Added for multi-town support + player._pCurrentTownId = file.NextLE(); + file.Skip(13); // Available bytes player.pDiabloKillLevel = file.NextLE(); sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE()); @@ -1480,7 +1483,10 @@ void SavePlayer(SaveHelper &file, const Player &player) file.WriteLE(player.pOriginalCathedral ? 1 : 0); file.Skip(2); // Available bytes file.WriteLE(player.wReflections); - file.Skip(14); // Available bytes + + // Added for multi-town support + file.WriteLE(player._pCurrentTownId); + file.Skip(13); // Available bytes file.WriteLE(player.pDiabloKillLevel); file.WriteLE(sgGameInitInfo.nDifficulty); @@ -2520,6 +2526,15 @@ tl::expected LoadGame(bool firstflag) LoadPlayer(file, myPlayer); + // Restore current town from player save data + std::string townId = GetTownRegistry().GetTownBySaveId(myPlayer._pCurrentTownId); + if (GetTownRegistry().HasTown(townId)) { + GetTownRegistry().SetCurrentTown(townId); + } else { + GetTownRegistry().SetCurrentTown("tristram"); + myPlayer._pCurrentTownId = 0; + } + if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) sgGameInitInfo.nDifficulty = DIFF_NORMAL; diff --git a/Source/player.cpp b/Source/player.cpp index 469f9a8163e..b048758efc9 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -2364,6 +2364,9 @@ void CreatePlayer(Player &player, HeroClass c) player.pDamAcFlags = ItemSpecialEffectHf::None; player.wReflections = 0; + // Initialize town data (start in Tristram, ID 0) + player._pCurrentTownId = 0; + InitDungMsgs(player); CreatePlrItems(player); SetRndSeed(0); diff --git a/Source/player.h b/Source/player.h index 5cc99ca1975..4fe8abb279f 100644 --- a/Source/player.h +++ b/Source/player.h @@ -359,6 +359,8 @@ struct Player { uint8_t pDungMsgs2; bool pOriginalCathedral; uint8_t pDiabloKillLevel; + /** @brief Current town ID as a hash/index (0 = Tristram, used for save compatibility) */ + uint8_t _pCurrentTownId; uint16_t wReflections; ItemSpecialEffectHf pDamAcFlags; From 891b2f6477a9d6d74ca490b428debe630f6caf54 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 06/13] Add per-town towner positions and Lua travel hooks --- Source/towners.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Source/towners.cpp b/Source/towners.cpp index 586480e138e..1082395f058 100644 --- a/Source/towners.cpp +++ b/Source/towners.cpp @@ -5,6 +5,8 @@ #include #include "cursor.h" +#include "levels/town_data.h" +#include "lua/lua_event.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" @@ -796,6 +798,22 @@ void InitTowners() InitTownerInfo(Towners.back(), *behaviorIt->second, entry); i++; } + + // Apply towner position overrides from the active town config + const std::string ¤tTown = GetTownRegistry().GetCurrentTown(); + if (GetTownRegistry().HasTown(currentTown)) { + const TownConfig &config = GetTownRegistry().GetTown(currentTown); + for (const auto &override : config.townerOverrides) { + for (auto &towner : Towners) { + auto shortNameIt = TownerShortNames.find(towner._ttype); + if (shortNameIt != TownerShortNames.end() && shortNameIt->second == override.shortName) { + dMonster[towner.position.x][towner.position.y] = 0; + towner.position = override.position; + dMonster[towner.position.x][towner.position.y] = static_cast(&towner - Towners.data() + 1); + } + } + } + } } void FreeTownerGFX() From d3162cbedeb3d33279603e1ab0109dfdca274bd7 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 14:57:25 -0700 Subject: [PATCH 07/13] Scope stores and inventory rotation to active town --- Source/stores.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Source/stores.cpp b/Source/stores.cpp index 70ed54d0672..35dfb74d37e 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -77,16 +77,29 @@ std::vector>> ExtraTowner const char *TownerNameForTalkID(TalkID s) { + const auto lookup = [](const _talker_id id) -> const char * { + auto it = TownerShortNames.find(id); + return it != TownerShortNames.end() ? it->second : nullptr; + }; switch (s) { - case TalkID::Smith: return "griswold"; - case TalkID::Witch: return "adria"; - case TalkID::Boy: return "wirt"; - case TalkID::Healer: return "pepin"; - case TalkID::Storyteller: return "cain"; - case TalkID::Tavern: return "ogden"; - case TalkID::Drunk: return "farnham"; - case TalkID::Barmaid: return "gillian"; - default: return nullptr; + case TalkID::Smith: + return lookup(TOWN_SMITH); + case TalkID::Witch: + return lookup(TOWN_WITCH); + case TalkID::Boy: + return lookup(TOWN_PEGBOY); + case TalkID::Healer: + return lookup(TOWN_HEALER); + case TalkID::Storyteller: + return lookup(TOWN_STORY); + case TalkID::Tavern: + return lookup(TOWN_TAVERN); + case TalkID::Drunk: + return lookup(TOWN_DRUNK); + case TalkID::Barmaid: + return lookup(TOWN_BMAID); + default: + return nullptr; } } @@ -2286,6 +2299,8 @@ void StartStore(TalkID s) ClearSText(0, NumStoreLines); ReleaseStoreBtn(); + ActiveStore = s; + // Fire StoreOpened Lua event for main store entries if (const char *name = TownerNameForTalkID(s); name != nullptr) lua::StoreOpened(name); @@ -2412,8 +2427,6 @@ void StartStore(TalkID s) break; } } - - ActiveStore = s; } void DrawSText(const Surface &out) From d052db0bd38010538cb5d92620c5288b09e530f3 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 31 Mar 2026 15:19:16 -0700 Subject: [PATCH 08/13] Fix build --- Source/lua/modules/towns.cpp | 15 ++++++++++----- Source/stores.cpp | 4 ++-- Source/towners.cpp | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Source/lua/modules/towns.cpp b/Source/lua/modules/towns.cpp index b7e02dd758c..4ed015eba65 100644 --- a/Source/lua/modules/towns.cpp +++ b/Source/lua/modules/towns.cpp @@ -57,11 +57,16 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) TownEntryPoint ep; sol::optional typeStr = entry["type"]; std::string type = typeStr.value_or("main"); - if (type == "prev") ep.entryType = ENTRY_PREV; - else if (type == "twarpdn") ep.entryType = ENTRY_TWARPDN; - else if (type == "twarpup") ep.entryType = ENTRY_TWARPUP; - else if (type == "townswitch") ep.entryType = ENTRY_TOWNSWITCH; - else ep.entryType = ENTRY_MAIN; + if (type == "prev") + ep.entryType = ENTRY_PREV; + else if (type == "twarpdn") + ep.entryType = ENTRY_TWARPDN; + else if (type == "twarpup") + ep.entryType = ENTRY_TWARPUP; + else if (type == "townswitch") + ep.entryType = ENTRY_TOWNSWITCH; + else + ep.entryType = ENTRY_MAIN; sol::optional ex = entry["x"]; sol::optional ey = entry["y"]; sol::optional warpFrom = entry["warpFrom"]; diff --git a/Source/stores.cpp b/Source/stores.cpp index 35dfb74d37e..6aaf32e0992 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -2299,8 +2299,6 @@ void StartStore(TalkID s) ClearSText(0, NumStoreLines); ReleaseStoreBtn(); - ActiveStore = s; - // Fire StoreOpened Lua event for main store entries if (const char *name = TownerNameForTalkID(s); name != nullptr) lua::StoreOpened(name); @@ -2427,6 +2425,8 @@ void StartStore(TalkID s) break; } } + + ActiveStore = s; } void DrawSText(const Surface &out) diff --git a/Source/towners.cpp b/Source/towners.cpp index 1082395f058..8ad96e0df7e 100644 --- a/Source/towners.cpp +++ b/Source/towners.cpp @@ -5,14 +5,14 @@ #include #include "cursor.h" -#include "levels/town_data.h" -#include "lua/lua_event.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "game_mode.hpp" #include "inv.h" +#include "levels/town_data.h" +#include "lua/lua_event.hpp" #include "minitext.h" #include "stores.h" #include "tables/textdat.h" From ae873c7a648e7cbc15176c9248850831199a247c Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Fri, 3 Apr 2026 13:28:44 -0700 Subject: [PATCH 09/13] Handle town triggers and warps --- Source/diablo.cpp | 7029 +++++++++++++++++----------------- Source/levels/town_data.cpp | 219 +- Source/levels/town_data.h | 179 +- Source/levels/trigs.cpp | 1879 +++++---- Source/lua/lua_event.hpp | 62 +- Source/lua/modules/towns.cpp | 57 +- 6 files changed, 4746 insertions(+), 4679 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index ac8a1ff178c..3503685b4aa 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1,3513 +1,3516 @@ -/** - * @file diablo.cpp - * - * Implementation of the main game initialization functions. - */ -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include -#include -#else -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif -#endif - -#include - -#include - -#include "DiabloUI/selstart.h" -#include "appfat.h" -#include "automap.h" -#include "capture.h" -#include "control/control.hpp" -#include "cursor.h" -#include "dead.h" -#ifdef _DEBUG -#include "debug.h" -#endif -#include "DiabloUI/diabloui.h" -#include "controls/control_mode.hpp" -#include "controls/keymapper.hpp" -#include "controls/plrctrls.h" -#include "controls/remap_keyboard.h" -#include "diablo.h" -#include "diablo_msg.hpp" -#include "discord/discord.h" -#include "doom.h" -#include "encrypt.h" -#include "engine/backbuffer_state.hpp" -#include "engine/clx_sprite.hpp" -#include "engine/demomode.h" -#include "engine/dx.h" -#include "engine/events.hpp" -#include "engine/load_cel.hpp" -#include "engine/load_file.hpp" -#include "engine/random.hpp" -#include "engine/render/clx_render.hpp" -#include "engine/sound.h" -#include "game_mode.hpp" -#include "gamemenu.h" -#include "gmenu.h" -#include "headless_mode.hpp" -#include "help.h" -#include "hwcursor.hpp" -#include "init.hpp" -#include "inv.h" -#include "levels/drlg_l1.h" -#include "levels/drlg_l2.h" -#include "levels/drlg_l3.h" -#include "levels/drlg_l4.h" -#include "levels/gendung.h" -#include "levels/setmaps.h" -#include "levels/themes.h" -#include "levels/town.h" -#include "levels/trigs.h" -#include "lighting.h" -#include "loadsave.h" -#include "lua/lua_event.hpp" -#include "lua/lua_global.hpp" -#include "menu.h" -#include "minitext.h" -#include "missiles.h" -#include "movie.h" -#include "multi.h" -#include "nthread.h" -#include "objects.h" -#include "options.h" -#include "panels/console.hpp" -#include "panels/info_box.hpp" -#include "panels/partypanel.hpp" -#include "panels/spell_book.hpp" -#include "panels/spell_list.hpp" -#include "pfile.h" -#include "plrmsg.h" -#include "qol/chatlog.h" -#include "qol/floatingnumbers.h" -#include "qol/itemlabels.h" -#include "qol/monhealthbar.h" -#include "qol/stash.h" -#include "qol/visual_store.h" -#include "qol/xpbar.h" -#include "quick_messages.hpp" -#include "restrict.h" -#include "stores.h" -#include "storm/storm_net.hpp" -#include "storm/storm_svid.h" -#include "tables/monstdat.h" -#include "tables/playerdat.hpp" -#include "towners.h" -#include "track.h" -#include "utils/console.h" -#include "utils/display.h" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/parse_int.hpp" -#include "utils/paths.h" -#include "utils/screen_reader.hpp" -#include "utils/sdl_compat.h" -#include "utils/sdl_thread.h" -#include "utils/status_macros.hpp" -#include "utils/str_cat.hpp" -#include "utils/utf8.hpp" - -#ifndef USE_SDL1 -#include "controls/touch/gamepad.h" -#include "controls/touch/renderers.h" -#endif - -#ifdef __vita__ -#include "platform/vita/touch.h" -#endif - -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION -#include -#endif - -namespace devilution { - -uint32_t DungeonSeeds[NUMLEVELS]; -std::optional LevelSeeds[NUMLEVELS]; -Point MousePosition; -bool gbRunGameResult; -bool ReturnToMainMenu; -/** Enable updating of player character, set to false once Diablo dies */ -bool gbProcessPlayers; -bool gbLoadGame; -bool cineflag; -int PauseMode; -clicktype sgbMouseDown; -uint16_t gnTickDelay = 50; -char gszProductName[64] = "DevilutionX vUnknown"; - -#ifdef _DEBUG -bool DebugDisableNetworkTimeout = false; -std::vector DebugCmdsFromCommandLine; -#endif -GameLogicStep gGameLogicStep = GameLogicStep::None; - -/** This and the following mouse variables are for handling in-game click-and-hold actions */ -PlayerActionType LastPlayerAction = PlayerActionType::None; - -// Controller support: Actions to run after updating the cursor state. -// Defined in SourceX/controls/plctrls.cpp. -extern void plrctrls_after_check_curs_move(); -extern void plrctrls_every_frame(); -extern void plrctrls_after_game_logic(); - -namespace { - -char gszVersionNumber[64] = "internal version unknown"; - -bool gbGameLoopStartup; -bool forceSpawn; -bool forceDiablo; -int sgnTimeoutCurs; -bool gbShowIntro = true; -/** To know if these things have been done when we get to the diablo_deinit() function */ -bool was_archives_init = false; -/** To know if surfaces have been initialized or not */ -bool was_window_init = false; -bool was_ui_init = false; - -void StartGame(interface_mode uMsg) -{ - CalcViewportGeometry(); - cineflag = false; - InitCursor(); -#ifdef _DEBUG - LoadDebugGFX(); -#endif - assert(HeadlessMode || ghMainWnd); - music_stop(); - InitMonsterHealthBar(); - InitXPBar(); - ShowProgress(uMsg); - gmenu_init_menu(); - InitLevelCursor(); - sgnTimeoutCurs = CURSOR_NONE; - sgbMouseDown = CLICK_NONE; - LastPlayerAction = PlayerActionType::None; -} - -void FreeGame() -{ - FreeMonsterHealthBar(); - FreeXPBar(); - FreeControlPan(); - FreeInvGFX(); - FreeGMenu(); - FreeQuestText(); - FreeInfoBoxGfx(); - FreeStoreMem(); - - for (Player &player : Players) - ResetPlayerGFX(player); - - FreeCursor(); -#ifdef _DEBUG - FreeDebugGFX(); -#endif - FreeGameMem(); - stream_stop(); - music_stop(); -} - -bool ProcessInput() -{ - if (PauseMode == 2) { - return false; - } - - plrctrls_every_frame(); - - if (!gbIsMultiplayer && gmenu_is_active()) { - RedrawViewport(); - return false; - } - - if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { -#ifdef __vita__ - FinishSimulatedMouseClicks(MousePosition); -#endif - CheckCursMove(); - plrctrls_after_check_curs_move(); - RepeatPlayerAction(); - } - - return true; -} - -void LeftMouseCmd(bool bShift) -{ - bool bNear; - - assert(!GetMainPanel().contains(MousePosition)); - - if (leveltype == DTYPE_TOWN) { - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); - if (pcursitem != -1 && pcurs == CURSOR_HAND) - NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); - if (pcursmonst != -1) - NetSendCmdLocParam1(true, CMD_TALKXY, cursPosition, pcursmonst); - if (pcursitem == -1 && pcursmonst == -1 && PlayerUnderCursor == nullptr) { - LastPlayerAction = PlayerActionType::Walk; - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); - } - return; - } - - const Player &myPlayer = *MyPlayer; - bNear = myPlayer.position.tile.WalkingDistance(cursPosition) < 2; - if (pcursitem != -1 && pcurs == CURSOR_HAND && !bShift) { - NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); - } else if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled() && (!bShift || (bNear && ObjectUnderCursor->_oBreak == 1))) { - LastPlayerAction = PlayerActionType::OperateObject; - NetSendCmdLoc(MyPlayerId, true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, cursPosition); - } else if (myPlayer.UsesRangedWeapon()) { - if (bShift) { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_RATTACKXY, cursPosition); - } else if (pcursmonst != -1) { - if (CanTalkToMonst(Monsters[pcursmonst])) { - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else { - LastPlayerAction = PlayerActionType::AttackMonsterTarget; - NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); - } - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - LastPlayerAction = PlayerActionType::AttackPlayerTarget; - NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId()); - } - } else { - if (bShift) { - if (pcursmonst != -1) { - if (CanTalkToMonst(Monsters[pcursmonst])) { - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); - } - } else { - LastPlayerAction = PlayerActionType::Attack; - NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); - } - } else if (pcursmonst != -1) { - LastPlayerAction = PlayerActionType::AttackMonsterTarget; - NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - LastPlayerAction = PlayerActionType::AttackPlayerTarget; - NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId()); - } - } - if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { - LastPlayerAction = PlayerActionType::Walk; - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); - } -} - -bool TryOpenDungeonWithMouse() -{ - if (leveltype != DTYPE_TOWN) - return false; - - const Item &holdItem = MyPlayer->HoldItem; - if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) - OpenHive(); - else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) - OpenGrave(); - else - return false; - - NewCursor(CURSOR_HAND); - return true; -} - -void LeftMouseDown(uint16_t modState) -{ - LastPlayerAction = PlayerActionType::None; - - if (gmenu_left_mouse(true)) - return; - - if (CheckMuteButton()) - return; - - if (sgnTimeoutCurs != CURSOR_NONE) - return; - - if (MyPlayerIsDead) { - CheckMainPanelButtonDead(); - return; - } - - if (PauseMode == 2) { - return; - } - if (DoomFlag) { - doom_close(); - return; - } - - if (SpellSelectFlag) { - SetSpell(); - return; - } - - if (IsPlayerInStore()) { - CheckStoreBtn(); - return; - } - - const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; - const bool isCtrlHeld = (modState & SDL_KMOD_CTRL) != 0; - - if (!GetMainPanel().contains(MousePosition)) { - if (!gmenu_is_active() && !TryIconCurs()) { - if (QuestLogIsOpen && GetLeftPanel().contains(MousePosition)) { - QuestlogESC(); - } else if (qtextflag) { - qtextflag = false; - stream_stop(); - } else if (CharFlag && GetLeftPanel().contains(MousePosition)) { - CheckChrBtns(); - } else if (invflag && GetRightPanel().contains(MousePosition)) { - if (!DropGoldFlag) - CheckInvItem(isShiftHeld, isCtrlHeld); - } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { - if (!IsWithdrawGoldOpen) - CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); - CheckStashButtonPress(MousePosition); - } else if (IsVisualStoreOpen && GetLeftPanel().contains(MousePosition)) { - if (!MyPlayer->HoldItem.isEmpty()) { - CheckVisualStorePaste(MousePosition); - } else { - CheckVisualStoreItem(MousePosition, isCtrlHeld, isShiftHeld); - } - CheckVisualStoreButtonPress(MousePosition); - } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { - CheckSBook(); - } else if (!MyPlayer->HoldItem.isEmpty()) { - if (!TryOpenDungeonWithMouse()) { - const Point currentPosition = MyPlayer->position.tile; - std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, cursPosition)); - if (itemTile) { - NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, MyPlayer->HoldItem); - NewCursor(CURSOR_HAND); - } - } - } else { - CheckLevelButton(); - if (!LevelButtonDown) - LeftMouseCmd(isShiftHeld); - } - } - } else { - if (!ChatFlag && !DropGoldFlag && !IsWithdrawGoldOpen && !gmenu_is_active()) - CheckInvScrn(isShiftHeld, isCtrlHeld); - CheckMainPanelButton(); - CheckStashButtonPress(MousePosition); - if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) - NewCursor(CURSOR_HAND); - } -} - -void LeftMouseUp(uint16_t modState) -{ - gmenu_left_mouse(false); - CheckMuteButtonUp(); - if (MainPanelButtonDown) - CheckMainPanelButtonUp(); - CheckStashButtonRelease(MousePosition); - CheckVisualStoreButtonRelease(MousePosition); - if (CharPanelButtonActive) { - const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; - ReleaseChrBtns(isShiftHeld); - } - if (LevelButtonDown) - CheckLevelButtonUp(); - if (IsPlayerInStore()) - ReleaseStoreBtn(); -} - -void RightMouseDown(bool isShiftHeld) -{ - LastPlayerAction = PlayerActionType::None; - - if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { - return; - } - - if (qtextflag) { - qtextflag = false; - stream_stop(); - return; - } - - if (DoomFlag) { - doom_close(); - return; - } - if (IsPlayerInStore()) - return; - if (SpellSelectFlag) { - SetSpell(); - return; - } - if (SpellbookFlag && GetRightPanel().contains(MousePosition)) - return; - if (TryIconCurs()) - return; - if (pcursinvitem != -1 && UseInvItem(pcursinvitem)) - return; - if (pcursstashitem != StashStruct::EmptyCell && UseStashItem(pcursstashitem)) - return; - if (DidRightClickPartyPortrait()) - return; - if (pcurs == CURSOR_HAND) { - CheckPlrSpell(isShiftHeld); - } else if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { - NewCursor(CURSOR_HAND); - } -} - -void ReleaseKey(SDL_Keycode vkey) -{ - remap_keyboard_key(&vkey); - if (sgnTimeoutCurs != CURSOR_NONE) - return; - KeymapperRelease(vkey); -} - -void ClosePanels() -{ - if (CanPanelsCoverView()) { - if (!IsLeftPanelOpen() && IsRightPanelOpen() && MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } else if (!IsRightPanelOpen() && IsLeftPanelOpen() && MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - CloseInventory(); - CloseCharPanel(); - SpellbookFlag = false; - QuestLogIsOpen = false; -} - -void PressKey(SDL_Keycode vkey, uint16_t modState) -{ - Options &options = GetOptions(); - remap_keyboard_key(&vkey); - - if (vkey == SDLK_UNKNOWN) - return; - - if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { - return; - } - - if (MyPlayerIsDead) { - if (vkey == SDLK_ESCAPE) { - if (!gbIsMultiplayer) { - if (gbValidSaveFile) - gamemenu_load_game(false); - else - gamemenu_exit_game(false); - } else { - NetSendCmd(true, CMD_RETOWN); - } - return; - } - if (sgnTimeoutCurs != CURSOR_NONE) { - return; - } - KeymapperPress(vkey); - if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { - if ((modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } else { - TypeChatMessage(); - } - } - if (vkey != SDLK_ESCAPE) { - return; - } - } - // Disallow player from accessing escape menu during the frames before the death message appears - if (vkey == SDLK_ESCAPE && MyPlayer->_pHitPoints > 0) { - if (!PressEscKey()) { - LastPlayerAction = PlayerActionType::None; - gamemenu_on(); - } - return; - } - - if (DropGoldFlag) { - control_drop_gold(vkey); - return; - } - if (IsWithdrawGoldOpen) { - WithdrawGoldKeyPress(vkey); - return; - } - - if (sgnTimeoutCurs != CURSOR_NONE) { - return; - } - - KeymapperPress(vkey); - - if (PauseMode == 2) { - if ((vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) && (modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } - return; - } - - if (DoomFlag) { - doom_close(); - return; - } - - switch (vkey) { - case SDLK_PLUS: - case SDLK_KP_PLUS: - case SDLK_EQUALS: - case SDLK_KP_EQUALS: - if (AutomapActive) { - AutomapZoomIn(); - } - return; - case SDLK_MINUS: - case SDLK_KP_MINUS: - case SDLK_UNDERSCORE: - if (AutomapActive) { - AutomapZoomOut(); - } - return; -#ifdef _DEBUG - case SDLK_V: - if ((modState & SDL_KMOD_SHIFT) != 0) - NextDebugMonster(); - else - GetDebugMonster(); - return; -#endif - case SDLK_RETURN: - case SDLK_KP_ENTER: - if ((modState & SDL_KMOD_ALT) != 0) { - options.Graphics.fullscreen.SetValue(!IsFullScreen()); - if (!demo::IsRunning()) SaveOptions(); - } else if (IsPlayerInStore()) { - StoreEnter(); - } else if (QuestLogIsOpen) { - QuestlogEnter(); - } else { - TypeChatMessage(); - } - return; - case SDLK_UP: - if (IsPlayerInStore()) { - StoreUp(); - } else if (QuestLogIsOpen) { - QuestlogUp(); - } else if (HelpFlag) { - HelpScrollUp(); - } else if (ChatLogFlag) { - ChatLogScrollUp(); - } else if (AutomapActive) { - AutomapUp(); - } else if (IsStashOpen) { - Stash.PreviousPage(); - } - return; - case SDLK_DOWN: - if (IsPlayerInStore()) { - StoreDown(); - } else if (QuestLogIsOpen) { - QuestlogDown(); - } else if (HelpFlag) { - HelpScrollDown(); - } else if (ChatLogFlag) { - ChatLogScrollDown(); - } else if (AutomapActive) { - AutomapDown(); - } else if (IsStashOpen) { - Stash.NextPage(); - } - return; - case SDLK_PAGEUP: - if (IsPlayerInStore()) { - StorePrior(); - } else if (ChatLogFlag) { - ChatLogScrollTop(); - } - return; - case SDLK_PAGEDOWN: - if (IsPlayerInStore()) { - StoreNext(); - } else if (ChatLogFlag) { - ChatLogScrollBottom(); - } - return; - case SDLK_LEFT: - if (AutomapActive && !ChatFlag) - AutomapLeft(); - return; - case SDLK_RIGHT: - if (AutomapActive && !ChatFlag) - AutomapRight(); - return; - default: - break; - } -} - -void HandleMouseButtonDown(Uint8 button, uint16_t modState) -{ - if (IsPlayerInStore() && (button == SDL_BUTTON_X1 -#if !SDL_VERSION_ATLEAST(2, 0, 0) - || button == 8 -#endif - )) { - StoreESC(); - return; - } - - switch (button) { - case SDL_BUTTON_LEFT: - if (sgbMouseDown == CLICK_NONE) { - sgbMouseDown = CLICK_LEFT; - LeftMouseDown(modState); - } - break; - case SDL_BUTTON_RIGHT: - if (sgbMouseDown == CLICK_NONE) { - sgbMouseDown = CLICK_RIGHT; - RightMouseDown((modState & SDL_KMOD_SHIFT) != 0); - } - break; - default: - KeymapperPress(static_cast(button | KeymapperMouseButtonMask)); - break; - } -} - -void HandleMouseButtonUp(Uint8 button, uint16_t modState) -{ - if (sgbMouseDown == CLICK_LEFT && button == SDL_BUTTON_LEFT) { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - LeftMouseUp(modState); - } else if (sgbMouseDown == CLICK_RIGHT && button == SDL_BUTTON_RIGHT) { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - } else { - KeymapperRelease(static_cast(button | KeymapperMouseButtonMask)); - } -} - -[[maybe_unused]] void LogUnhandledEvent(const char *name, int value) -{ - LogVerbose("Unhandled SDL event: {} {}", name, value); -} - -void PrepareForFadeIn() -{ - if (HeadlessMode) return; - BlackPalette(); - - // Render the game to the buffer(s) with a fully black palette. - // Palette fade-in will gradually make it visible. - RedrawEverything(); - while (IsRedrawEverything()) { - DrawAndBlit(); - } -} - -void GameEventHandler(const SDL_Event &event, uint16_t modState) -{ - [[maybe_unused]] const Options &options = GetOptions(); - StaticVector ctrlEvents = ToControllerButtonEvents(event); - for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { - GameAction action; - if (HandleControllerButtonEvent(event, ctrlEvent, action) && action.type == GameActionType_SEND_KEY) { - if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { - const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; - if (!action.send_key.up) - HandleMouseButtonDown(static_cast(button), modState); - else - HandleMouseButtonUp(static_cast(button), modState); - } else { - if (!action.send_key.up) - PressKey(static_cast(action.send_key.vk_code), modState); - else - ReleaseKey(static_cast(action.send_key.vk_code)); - } - } - } - if (ctrlEvents.size() > 0 && ctrlEvents[0].button != ControllerButton_NONE) { - return; - } - -#ifdef _DEBUG - if (ConsoleHandleEvent(event)) { - return; - } -#endif - - if (IsChatActive() && HandleTalkTextInputEvent(event)) { - return; - } - if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { - return; - } - if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { - return; - } - - switch (event.type) { - case SDL_EVENT_KEY_DOWN: - PressKey(SDLC_EventKey(event), modState); - return; - case SDL_EVENT_KEY_UP: - ReleaseKey(SDLC_EventKey(event)); - return; - case SDL_EVENT_MOUSE_MOTION: - if (ControlMode == ControlTypes::KeyboardAndMouse && invflag) - InvalidateInventorySlot(); - MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; - gmenu_on_mouse_move(); - return; - case SDL_EVENT_MOUSE_BUTTON_DOWN: - MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; - HandleMouseButtonDown(event.button.button, modState); - return; - case SDL_EVENT_MOUSE_BUTTON_UP: - MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; - HandleMouseButtonUp(event.button.button, modState); - return; -#if SDL_VERSION_ATLEAST(2, 0, 0) - case SDL_EVENT_MOUSE_WHEEL: - if (SDLC_EventWheelIntY(event) > 0) { // Up - if (IsPlayerInStore()) { - StoreUp(); - } else if (QuestLogIsOpen) { - QuestlogUp(); - } else if (HelpFlag) { - HelpScrollUp(); - } else if (ChatLogFlag) { - ChatLogScrollUp(); - } else if (IsStashOpen) { - Stash.PreviousPage(); - } else if (SDL_GetModState() & SDL_KMOD_CTRL) { - if (AutomapActive) { - AutomapZoomIn(); - } - } else { - KeymapperPress(MouseScrollUpButton); - } - } else if (SDLC_EventWheelIntY(event) < 0) { // down - if (IsPlayerInStore()) { - StoreDown(); - } else if (QuestLogIsOpen) { - QuestlogDown(); - } else if (HelpFlag) { - HelpScrollDown(); - } else if (ChatLogFlag) { - ChatLogScrollDown(); - } else if (IsStashOpen) { - Stash.NextPage(); - } else if (SDL_GetModState() & SDL_KMOD_CTRL) { - if (AutomapActive) { - AutomapZoomOut(); - } - } else { - KeymapperPress(MouseScrollDownButton); - } - } else if (SDLC_EventWheelIntX(event) > 0) { // left - KeymapperPress(MouseScrollLeftButton); - } else if (SDLC_EventWheelIntX(event) < 0) { // right - KeymapperPress(MouseScrollRightButton); - } - break; -#endif - default: - if (IsCustomEvent(event.type)) { - if (gbIsMultiplayer) - pfile_write_hero(); - nthread_ignore_mutex(true); - PaletteFadeOut(8); - sound_stop(); - ShowProgress(GetCustomEvent(event)); - - PrepareForFadeIn(); - LoadPWaterPalette(); - if (gbRunGame) - PaletteFadeIn(8); - nthread_ignore_mutex(false); - gbGameLoopStartup = true; - return; - } - MainWndProc(event); - break; - } -} - -void RunGameLoop(interface_mode uMsg) -{ - demo::NotifyGameLoopStart(); - - nthread_ignore_mutex(true); - StartGame(uMsg); - assert(HeadlessMode || ghMainWnd); - EventHandler previousHandler = SetEventHandler(GameEventHandler); - run_delta_info(); - gbRunGame = true; - gbProcessPlayers = IsDiabloAlive(true); - gbRunGameResult = true; - - PrepareForFadeIn(); - LoadPWaterPalette(); - PaletteFadeIn(8); - InitBackbufferState(); - RedrawEverything(); - gbGameLoopStartup = true; - nthread_ignore_mutex(false); - - discord_manager::StartGame(); - lua::GameStart(); -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION - unsigned run_game_iteration = 0; -#endif - - while (gbRunGame) { - -#ifdef _DEBUG - if (!gbGameLoopStartup && !DebugCmdsFromCommandLine.empty()) { - InitConsole(); - for (const std::string &cmd : DebugCmdsFromCommandLine) { - RunInConsole(cmd); - } - DebugCmdsFromCommandLine.clear(); - } -#endif - - SDL_Event event; - uint16_t modState; - while (FetchMessage(&event, &modState)) { - if (event.type == SDL_EVENT_QUIT) { - gbRunGameResult = false; - gbRunGame = false; - break; - } - HandleMessage(event, modState); - } - if (!gbRunGame) - break; - - bool drawGame = true; - bool processInput = true; - const bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed(&drawGame); - if (demo::IsRecording()) - demo::RecordGameLoopResult(runGameLoop); - - discord_manager::UpdateGame(); - - if (!runGameLoop) { - if (processInput) - ProcessInput(); - DvlNet_ProcessNetworkPackets(); - if (!drawGame) - continue; - RedrawViewport(); - DrawAndBlit(); - continue; - } - - ProcessGameMessagePackets(); - if (game_loop(gbGameLoopStartup)) - diablo_color_cyc_logic(); - gbGameLoopStartup = false; - if (drawGame) - DrawAndBlit(); -#ifdef GPERF_HEAP_FIRST_GAME_ITERATION - if (run_game_iteration++ == 0) - HeapProfilerDump("first_game_iteration"); -#endif - } - - demo::NotifyGameLoopEnd(); - - if (gbIsMultiplayer) { - pfile_write_hero(/*writeGameData=*/false); - sfile_write_stash(); - } - - PaletteFadeOut(8); - NewCursor(CURSOR_NONE); - ClearScreenBuffer(); - RedrawEverything(); - scrollrt_draw_game_screen(); - previousHandler = SetEventHandler(previousHandler); - assert(HeadlessMode || previousHandler == GameEventHandler); - FreeGame(); - - if (cineflag) { - cineflag = false; - DoEnding(); - } -} - -void PrintWithRightPadding(std::string_view str, size_t width) -{ - printInConsole(str); - if (str.size() >= width) - return; - printInConsole(std::string(width - str.size(), ' ')); -} - -void PrintHelpOption(std::string_view flags, std::string_view description) -{ - printInConsole(" "); - PrintWithRightPadding(flags, 20); - printInConsole(" "); - PrintWithRightPadding(description, 30); - printNewlineInConsole(); -} - -#if SDL_VERSION_ATLEAST(2, 0, 0) -FILE *SdlLogFile = nullptr; - -extern "C" void SdlLogToFile(void *userdata, int /*category*/, SDL_LogPriority priority, const char *message) -{ - FILE *file = reinterpret_cast(userdata); - static const char *const LogPriorityPrefixes[SDL_LOG_PRIORITY_COUNT] = { - "", - "VERBOSE", - "DEBUG", - "INFO", - "WARN", - "ERROR", - "CRITICAL" - }; - std::fprintf(file, "%s: %s\n", LogPriorityPrefixes[priority], message); - std::fflush(file); -} -#endif - -[[noreturn]] void PrintHelpAndExit() -{ - printInConsole((/* TRANSLATORS: Commandline Option */ "Options:")); - printNewlineInConsole(); - PrintHelpOption("-h, --help", _(/* TRANSLATORS: Commandline Option */ "Print this message and exit")); - PrintHelpOption("--version", _(/* TRANSLATORS: Commandline Option */ "Print the version and exit")); - PrintHelpOption("--data-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of diabdat.mpq")); - PrintHelpOption("--save-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of save files")); - PrintHelpOption("--config-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the location of diablo.ini")); - PrintHelpOption("--lang", _(/* TRANSLATORS: Commandline Option */ "Specify the language code (e.g. en or pt_BR)")); - PrintHelpOption("-n", _(/* TRANSLATORS: Commandline Option */ "Skip startup videos")); - PrintHelpOption("-f", _(/* TRANSLATORS: Commandline Option */ "Display frames per second")); - PrintHelpOption("--verbose", _(/* TRANSLATORS: Commandline Option */ "Enable verbose logging")); -#if SDL_VERSION_ATLEAST(2, 0, 0) - PrintHelpOption("--log-to-file ", _(/* TRANSLATORS: Commandline Option */ "Log to a file instead of stderr")); -#endif -#ifndef DISABLE_DEMOMODE - PrintHelpOption("--record <#>", _(/* TRANSLATORS: Commandline Option */ "Record a demo file")); - PrintHelpOption("--demo <#>", _(/* TRANSLATORS: Commandline Option */ "Play a demo file")); - PrintHelpOption("--timedemo", _(/* TRANSLATORS: Commandline Option */ "Disable all frame limiting during demo playback")); -#endif - printNewlineInConsole(); - printInConsole(_(/* TRANSLATORS: Commandline Option */ "Game selection:")); - printNewlineInConsole(); - PrintHelpOption("--spawn", _(/* TRANSLATORS: Commandline Option */ "Force Shareware mode")); - PrintHelpOption("--diablo", _(/* TRANSLATORS: Commandline Option */ "Force Diablo mode")); - PrintHelpOption("--hellfire", _(/* TRANSLATORS: Commandline Option */ "Force Hellfire mode")); - printInConsole(_(/* TRANSLATORS: Commandline Option */ "Hellfire options:")); - printNewlineInConsole(); -#ifdef _DEBUG - printNewlineInConsole(); - printInConsole("Debug options:"); - printNewlineInConsole(); - PrintHelpOption("-i", "Ignore network timeout"); - PrintHelpOption("+", "Pass commands to the engine"); -#endif - printNewlineInConsole(); - printInConsole(_("Report bugs at https://github.com/diasurgical/devilutionX/")); - printNewlineInConsole(); - diablo_quit(0); -} - -void PrintFlagMessage(std::string_view flag, std::string_view message) -{ - printInConsole(flag); - printInConsole(message); - printNewlineInConsole(); -} - -void PrintFlagRequiresArgument(std::string_view flag) -{ - PrintFlagMessage(flag, " requires an argument"); -} - -void DiabloParseFlags(int argc, char **argv) -{ -#ifdef _DEBUG - int argumentIndexOfLastCommandPart = -1; - std::string currentCommand; -#endif -#ifndef DISABLE_DEMOMODE - bool timedemo = false; - int demoNumber = -1; - int recordNumber = -1; - bool createDemoReference = false; -#endif - for (int i = 1; i < argc; i++) { - const std::string_view arg = argv[i]; - if (arg == "-h" || arg == "--help") { - PrintHelpAndExit(); - } else if (arg == "--version") { - printInConsole(PROJECT_NAME); - printInConsole(" v"); - printInConsole(PROJECT_VERSION); - printNewlineInConsole(); - diablo_quit(0); - } else if (arg == "--data-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--data-dir"); - diablo_quit(64); - } - paths::SetBasePath(argv[++i]); - } else if (arg == "--save-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--save-dir"); - diablo_quit(64); - } - paths::SetPrefPath(argv[++i]); - } else if (arg == "--config-dir") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--config-dir"); - diablo_quit(64); - } - paths::SetConfigPath(argv[++i]); - } else if (arg == "--lang") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--lang"); - diablo_quit(64); - } - forceLocale = argv[++i]; -#ifndef DISABLE_DEMOMODE - } else if (arg == "--demo") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--demo"); - diablo_quit(64); - } - ParseIntResult parsedParam = ParseInt(argv[++i]); - if (!parsedParam.has_value()) { - PrintFlagMessage("--demo", " must be a number"); - diablo_quit(64); - } - demoNumber = parsedParam.value(); - gbShowIntro = false; - } else if (arg == "--timedemo") { - timedemo = true; - } else if (arg == "--record") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--record"); - diablo_quit(64); - } - ParseIntResult parsedParam = ParseInt(argv[++i]); - if (!parsedParam.has_value()) { - PrintFlagMessage("--record", " must be a number"); - diablo_quit(64); - } - recordNumber = parsedParam.value(); - } else if (arg == "--create-reference") { - createDemoReference = true; -#else - } else if (arg == "--demo" || arg == "--timedemo" || arg == "--record" || arg == "--create-reference") { - printInConsole("Binary compiled without demo mode support."); - printNewlineInConsole(); - diablo_quit(1); -#endif - } else if (arg == "-n") { - gbShowIntro = false; - } else if (arg == "-f") { - EnableFrameCount(); - } else if (arg == "--spawn") { - forceSpawn = true; - } else if (arg == "--diablo") { - forceDiablo = true; - } else if (arg == "--hellfire") { - forceHellfire = true; - } else if (arg == "--vanilla") { - gbVanilla = true; - } else if (arg == "--verbose") { - SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); -#if SDL_VERSION_ATLEAST(2, 0, 0) - } else if (arg == "--log-to-file") { - if (i + 1 == argc) { - PrintFlagRequiresArgument("--log-to-file"); - diablo_quit(64); - } - SdlLogFile = OpenFile(argv[++i], "wb"); - if (SdlLogFile == nullptr) { - printInConsole("Failed to open log file for writing"); - diablo_quit(64); - } - SDL_SetLogOutputFunction(&SdlLogToFile, /*userdata=*/SdlLogFile); -#endif -#ifdef _DEBUG - } else if (arg == "-i") { - DebugDisableNetworkTimeout = true; - } else if (arg[0] == '+') { - if (!currentCommand.empty()) - DebugCmdsFromCommandLine.push_back(currentCommand); - argumentIndexOfLastCommandPart = i; - currentCommand = arg.substr(1); - } else if (arg[0] != '-' && (argumentIndexOfLastCommandPart + 1) == i) { - currentCommand.append(" "); - currentCommand.append(arg); - argumentIndexOfLastCommandPart = i; -#endif - } else { - printInConsole("unrecognized option '"); - printInConsole(argv[i]); - printInConsole("'"); - printNewlineInConsole(); - PrintHelpAndExit(); - } - } - -#ifdef _DEBUG - if (!currentCommand.empty()) - DebugCmdsFromCommandLine.push_back(currentCommand); -#endif - -#ifndef DISABLE_DEMOMODE - if (demoNumber != -1) - demo::InitPlayBack(demoNumber, timedemo); - if (recordNumber != -1) - demo::InitRecording(recordNumber, createDemoReference); -#endif -} - -void DiabloInitScreen() -{ - MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; - if (ControlMode == ControlTypes::KeyboardAndMouse) - SetCursorPos(MousePosition); - - ClrDiabloMsg(); -} - -void SetApplicationVersions() -{ - *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; - *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; -} - -void CheckArchivesUpToDate() -{ - const bool devilutionxMpqOutOfDate = IsDevilutionXMpqOutOfDate(); - const bool fontsMpqOutOfDate = AreExtraFontsOutOfDate(); - - if (devilutionxMpqOutOfDate && fontsMpqOutOfDate) { - app_fatal(_("Please update devilutionx.mpq and fonts.mpq to the latest version")); - } else if (devilutionxMpqOutOfDate) { - app_fatal(_("Failed to load UI resources.\n" - "\n" - "Make sure devilutionx.mpq is in the game folder and that it is up to date.")); - } else if (fontsMpqOutOfDate) { - app_fatal(_("Please update fonts.mpq to the latest version")); - } -} - -void ApplicationInit() -{ - if (*GetOptions().Graphics.showFPS) - EnableFrameCount(); - - init_create_window(); - was_window_init = true; - - InitializeScreenReader(); - LanguageInitialize(); - - SetApplicationVersions(); - - ReadOnlyTest(); -} - -void DiabloInit() -{ - if (forceSpawn || *GetOptions().GameMode.shareware) - gbIsSpawn = 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; - - for (size_t i = 0; i < QuickMessages.size(); i++) { - auto &messages = GetOptions().Chat.szHotKeyMsgs[i]; - if (messages.empty()) { - messages.emplace_back(_(QuickMessages[i].message)); - } - } - -#ifndef USE_SDL1 - InitializeVirtualGamepad(); -#endif - - UiInitialize(); - was_ui_init = true; - - if (wasHellfireDiscovered) { - UiSelStartUpGameOption(); - if (!gbIsHellfire) { - // Reinitialize the UI Elements because we changed the game - UnloadUiGFX(); - UiInitialize(); - if (IsHardwareCursor()) - SetHardwareCursor(CursorInfo::UnknownCursor()); - } - } - - DiabloInitScreen(); - - snd_init(); - - ui_sound_init(); - - // Item graphics are loaded early, they already get touched during hero selection. - InitItemGFX(); - - // Always available. - LoadSmallSelectionSpinner(); - - CheckArchivesUpToDate(); -} - -void DiabloSplash() -{ - if (!gbShowIntro) - return; - - if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) - play_movie("gendata\\logo.smk", true); - - auto &intro = gbIsHellfire ? GetOptions().StartUp.hellfireIntro : GetOptions().StartUp.diabloIntro; - - if (*intro != StartUpIntro::Off) { - if (gbIsHellfire) - play_movie("gendata\\Hellfire.smk", true); - else - play_movie("gendata\\diablo1.smk", true); - if (*intro == StartUpIntro::Once) { - intro.SetValue(StartUpIntro::Off); - if (!demo::IsRunning()) SaveOptions(); - } - } - - if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) - UiTitleDialog(); -} - -void DiabloDeinit() -{ - FreeItemGFX(); - - LuaShutdown(); - ShutDownScreenReader(); - - if (gbSndInited) - effects_cleanup_sfx(); - snd_deinit(); - if (was_ui_init) - UiDestroy(); - if (was_archives_init) - init_cleanup(); - if (was_window_init) - dx_cleanup(); // Cleanup SDL surfaces stuff, so we have to do it before SDL_Quit(). - UnloadFonts(); - if (SDL_WasInit((~0U) & ~SDL_INIT_HAPTIC) != 0) - SDL_Quit(); -} - -tl::expected LoadLvlGFX() -{ - assert(pDungeonCels == nullptr); - constexpr int SpecialCelWidth = 64; - - const auto loadAll = [](const char *cel, const char *til, const char *special) -> tl::expected { - ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus(cel)); - ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus(til)); - ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus(special, SpecialCelWidth)); - return {}; - }; - - switch (leveltype) { - case DTYPE_TOWN: { - auto cel = LoadFileInMemWithStatus("nlevels\\towndata\\town.cel"); - if (!cel.has_value()) { - ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus("levels\\towndata\\town.cel")); - } else { - pDungeonCels = std::move(*cel); - } - auto til = LoadFileInMemWithStatus("nlevels\\towndata\\town.til"); - if (!til.has_value()) { - ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus("levels\\towndata\\town.til")); - } else { - pMegaTiles = std::move(*til); - } - ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus("levels\\towndata\\towns", SpecialCelWidth)); - return {}; - } - case DTYPE_CATHEDRAL: - return loadAll( - "levels\\l1data\\l1.cel", - "levels\\l1data\\l1.til", - "levels\\l1data\\l1s"); - case DTYPE_CATACOMBS: - return loadAll( - "levels\\l2data\\l2.cel", - "levels\\l2data\\l2.til", - "levels\\l2data\\l2s"); - case DTYPE_CAVES: - return loadAll( - "levels\\l3data\\l3.cel", - "levels\\l3data\\l3.til", - "levels\\l1data\\l1s"); - case DTYPE_HELL: - return loadAll( - "levels\\l4data\\l4.cel", - "levels\\l4data\\l4.til", - "levels\\l2data\\l2s"); - case DTYPE_NEST: - return loadAll( - "nlevels\\l6data\\l6.cel", - "nlevels\\l6data\\l6.til", - "levels\\l1data\\l1s"); - case DTYPE_CRYPT: - return loadAll( - "nlevels\\l5data\\l5.cel", - "nlevels\\l5data\\l5.til", - "nlevels\\l5data\\l5s"); - default: - return tl::make_unexpected("LoadLvlGFX"); - } -} - -tl::expected LoadAllGFX() -{ - IncProgress(); -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - IncProgress(); - RETURN_IF_ERROR(InitObjectGFX()); - IncProgress(); - RETURN_IF_ERROR(InitMissileGFX()); - IncProgress(); - return {}; -} - -/** - * @param entry Where is the player entering from - */ -void CreateLevel(lvl_entry entry) -{ - CreateDungeon(DungeonSeeds[currlevel], entry); - - switch (leveltype) { - case DTYPE_TOWN: - InitTownTriggers(); - break; - case DTYPE_CATHEDRAL: - InitL1Triggers(); - break; - case DTYPE_CATACOMBS: - InitL2Triggers(); - break; - case DTYPE_CAVES: - InitL3Triggers(); - break; - case DTYPE_HELL: - InitL4Triggers(); - break; - case DTYPE_NEST: - InitHiveTriggers(); - break; - case DTYPE_CRYPT: - InitCryptTriggers(); - break; - default: - app_fatal("CreateLevel"); - } - - if (leveltype != DTYPE_TOWN) { - Freeupstairs(); - } - LoadRndLvlPal(leveltype); -} - -void UnstuckChargers() -{ - if (gbIsMultiplayer) { - for (Player &player : Players) { - if (!player.plractive) - continue; - if (player._pLvlChanging) - continue; - if (!player.isOnActiveLevel()) - continue; - if (&player == MyPlayer) - continue; - return; - } - } - for (size_t i = 0; i < ActiveMonsterCount; i++) { - Monster &monster = Monsters[ActiveMonsters[i]]; - if (monster.mode == MonsterMode::Charge) - monster.mode = MonsterMode::Stand; - } -} - -void UpdateMonsterLights() -{ - for (size_t i = 0; i < ActiveMonsterCount; i++) { - Monster &monster = Monsters[ActiveMonsters[i]]; - - if ((monster.flags & MFLAG_BERSERK) != 0) { - const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; - monster.lightId = AddLight(monster.position.tile, lightRadius); - } - - if (monster.lightId != NO_LIGHT) { - if (monster.lightId == MyPlayer->lightId) { // Fix old saves where some monsters had 0 instead of NO_LIGHT - monster.lightId = NO_LIGHT; - continue; - } - - const Light &light = Lights[monster.lightId]; - if (monster.position.tile != light.position.tile) { - ChangeLightXY(monster.lightId, monster.position.tile); - } - } - } -} - -void GameLogic() -{ - if (!ProcessInput()) { - return; - } - if (gbProcessPlayers) { - gGameLogicStep = GameLogicStep::ProcessPlayers; - ProcessPlayers(); - } - if (leveltype != DTYPE_TOWN) { - gGameLogicStep = GameLogicStep::ProcessMonsters; -#ifdef _DEBUG - if (!DebugInvisible) -#endif - ProcessMonsters(); - gGameLogicStep = GameLogicStep::ProcessObjects; - ProcessObjects(); - gGameLogicStep = GameLogicStep::ProcessMissiles; - ProcessMissiles(); - gGameLogicStep = GameLogicStep::ProcessItems; - ProcessItems(); - ProcessLightList(); - ProcessVisionList(); - } else { - gGameLogicStep = GameLogicStep::ProcessTowners; - ProcessTowners(); - gGameLogicStep = GameLogicStep::ProcessItemsTown; - ProcessItems(); - gGameLogicStep = GameLogicStep::ProcessMissilesTown; - ProcessMissiles(); - } - gGameLogicStep = GameLogicStep::None; - -#ifdef _DEBUG - if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { - ScrollView(); - } -#endif - - sound_update(); - CheckTriggers(); - CheckQuests(); - RedrawViewport(); - pfile_update(false); - - plrctrls_after_game_logic(); -} - -void TimeoutCursor(bool bTimeout) -{ - if (bTimeout) { - if (sgnTimeoutCurs == CURSOR_NONE && sgbMouseDown == CLICK_NONE) { - sgnTimeoutCurs = pcurs; - multi_net_ping(); - InfoString = StringOrView {}; - AddInfoBoxString(_("-- Network timeout --")); - AddInfoBoxString(_("-- Waiting for players --")); - for (uint8_t i = 0; i < Players.size(); i++) { - bool isConnected = (player_state[i] & PS_CONNECTED) != 0; - bool isActive = (player_state[i] & PS_ACTIVE) != 0; - if (!isConnected || isActive) continue; - - DvlNetLatencies latencies = DvlNet_GetLatencies(i); - - std::string ping = fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Player {:s} is timing out!")), - Players[i].name()); - - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); - - if (latencies.providerLatency) { - if (latencies.isRelayed && *latencies.isRelayed) { - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); - } else { - StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); - } - } - EventPlrMsg(ping); - } - NewCursor(CURSOR_HOURGLASS); - RedrawEverything(); - } - scrollrt_draw_game_screen(); - } else if (sgnTimeoutCurs != CURSOR_NONE) { - // Timeout is gone, we should restore the previous cursor. - // But the timeout cursor could already be changed by the now processed messages (for example item cursor from CMD_GETITEM). - // Changing the item cursor back to the previous (hand) cursor could result in deleted items, because this resets Player.HoldItem (see NewCursor). - if (pcurs == CURSOR_HOURGLASS) - NewCursor(sgnTimeoutCurs); - sgnTimeoutCurs = CURSOR_NONE; - InfoString = StringOrView {}; - RedrawEverything(); - } -} - -void HelpKeyPressed() -{ - if (HelpFlag) { - HelpFlag = false; - } else if (IsPlayerInStore()) { - InfoString = StringOrView {}; - AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed - AddInfoBoxString(_("while in stores")); - LastPlayerAction = PlayerActionType::None; - } else { - CloseInventory(); - CloseCharPanel(); - SpellbookFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - QuestLogIsOpen = false; - CancelCurrentDiabloMsg(); - gamemenu_off(); - DisplayHelp(); - doom_close(); - } -} - -void InventoryKeyPressed() -{ - if (IsPlayerInStore()) - return; - invflag = !invflag; - if (!IsLeftPanelOpen() && CanPanelsCoverView()) { - if (!invflag) { // We closed the inventory - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } else if (!SpellbookFlag) { // We opened the inventory - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - } - SpellbookFlag = false; - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); -} - -void CharacterSheetKeyPressed() -{ - if (IsPlayerInStore()) - return; - if (!IsRightPanelOpen() && CanPanelsCoverView()) { - if (CharFlag) { // We are closing the character sheet - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } else if (!QuestLogIsOpen) { // We opened the character sheet - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } - } - ToggleCharPanel(); -} - -void PartyPanelSideToggleKeyPressed() -{ - PartySidePanelOpen = !PartySidePanelOpen; -} - -void QuestLogKeyPressed() -{ - if (IsPlayerInStore()) - return; - if (!QuestLogIsOpen) { - StartQuestlog(); - } else { - QuestLogIsOpen = false; - } - if (!IsRightPanelOpen() && CanPanelsCoverView()) { - if (!QuestLogIsOpen) { // We closed the quest log - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } else if (!CharFlag) { // We opened the character quest log - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } - } - CloseCharPanel(); - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); -} - -void DisplaySpellsKeyPressed() -{ - if (IsPlayerInStore()) - return; - CloseCharPanel(); - QuestLogIsOpen = false; - CloseInventory(); - SpellbookFlag = false; - if (!SpellSelectFlag) { - DoSpeedBook(); - } else { - SpellSelectFlag = false; - } - LastPlayerAction = PlayerActionType::None; -} - -void SpellBookKeyPressed() -{ - if (IsPlayerInStore()) - return; - SpellbookFlag = !SpellbookFlag; - if (!IsLeftPanelOpen() && CanPanelsCoverView()) { - if (!SpellbookFlag) { // We closed the inventory - if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition + Displacement { 160, 0 }); - } - } else if (!invflag) { // We opened the inventory - if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { - SetCursorPos(MousePosition - Displacement { 160, 0 }); - } - } - } - CloseInventory(); - CloseVisualStore(); -} - -void CycleSpellHotkeys(bool next) -{ - StaticVector validHotKeyIndexes; - std::optional currentIndex; - for (size_t slot = 0; slot < NumHotkeys; slot++) { - if (!IsValidSpeedSpell(slot)) - continue; - if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) { - // found current - currentIndex = validHotKeyIndexes.size(); - } - validHotKeyIndexes.emplace_back(slot); - } - if (validHotKeyIndexes.size() == 0) - return; - - size_t newIndex; - if (!currentIndex) { - newIndex = next ? 0 : (validHotKeyIndexes.size() - 1); - } else if (next) { - newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1); - } else { - newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1); - } - ToggleSpell(validHotKeyIndexes[newIndex]); -} - -bool IsPlayerDead() -{ - return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; -} - -bool IsGameRunning() -{ - return PauseMode != 2; -} - -bool CanPlayerTakeAction() -{ - return !IsPlayerDead() && IsGameRunning(); -} - -bool CanAutomapBeToggledOff() -{ - // check if every window is closed - if yes, automap can be toggled off - return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !IsVisualStoreOpen && !CharFlag - && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag - && !ChatLogFlag && !HelpFlag; -} - -void OptionLanguageCodeChanged() -{ - UnloadFonts(); - LanguageInitialize(); - LoadLanguageArchive(); - effects_cleanup_sfx(false); - if (gbRunGame) - sound_init(); - else - ui_sound_init(); -} - -const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); - -} // namespace - -void InitKeymapActions() -{ - Options &options = GetOptions(); - for (uint32_t i = 0; i < 8; ++i) { - options.Keymapper.AddAction( - "BeltItem{}", - N_("Belt item {}"), - N_("Use Belt item."), - '1' + i, - [i] { - const Player &myPlayer = *MyPlayer; - if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { - UseInvItem(INVITEM_BELT_FIRST + i); - } - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - for (uint32_t i = 0; i < NumHotkeys; ++i) { - options.Keymapper.AddAction( - "QuickSpell{}", - N_("Quick spell {}"), - N_("Hotkey for skill or spell."), - i < 4 ? static_cast(SDLK_F5) + i : static_cast(SDLK_UNKNOWN), - [i]() { - if (SpellSelectFlag) { - SetSpeedSpell(i); - return; - } - if (!*GetOptions().Gameplay.quickCast) - ToggleSpell(i); - else - QuickCast(i); - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - options.Keymapper.AddAction( - "QuickSpellPrevious", - N_("Previous quick spell"), - N_("Selects the previous quick spell (cycles)."), - MouseScrollUpButton, - [] { CycleSpellHotkeys(false); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuickSpellNext", - N_("Next quick spell"), - N_("Selects the next quick spell (cycles)."), - MouseScrollDownButton, - [] { CycleSpellHotkeys(true); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "UseHealthPotion", - N_("Use health potion"), - N_("Use health potions from belt."), - SDLK_UNKNOWN, - [] { UseBeltItem(BeltItemType::Healing); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "UseManaPotion", - N_("Use mana potion"), - N_("Use mana potions from belt."), - SDLK_UNKNOWN, - [] { UseBeltItem(BeltItemType::Mana); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "DisplaySpells", - N_("Speedbook"), - N_("Open Speedbook."), - 'S', - DisplaySpellsKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuickSave", - N_("Quick save"), - N_("Saves the game."), - SDLK_F2, - [] { gamemenu_save_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); - options.Keymapper.AddAction( - "QuickLoad", - N_("Quick load"), - N_("Loads the game."), - SDLK_F3, - [] { gamemenu_load_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); -#ifndef NOEXIT - options.Keymapper.AddAction( - "QuitGame", - N_("Quit game"), - N_("Closes the game."), - SDLK_UNKNOWN, - [] { gamemenu_quit_game(false); }); -#endif - options.Keymapper.AddAction( - "StopHero", - N_("Stop hero"), - N_("Stops walking and cancel pending actions."), - SDLK_UNKNOWN, - [] { MyPlayer->Stop(); }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "ItemHighlighting", - N_("Item highlighting"), - N_("Show/hide items on ground."), - SDLK_LALT, - [] { HighlightKeyPressed(true); }, - [] { HighlightKeyPressed(false); }); - options.Keymapper.AddAction( - "ToggleItemHighlighting", - N_("Toggle item highlighting"), - N_("Permanent show/hide items on ground."), - SDLK_RCTRL, - nullptr, - [] { ToggleItemLabelHighlight(); }); - options.Keymapper.AddAction( - "ToggleAutomap", - N_("Toggle automap"), - N_("Toggles if automap is displayed."), - SDLK_TAB, - DoAutoMap, - nullptr, - IsGameRunning); - options.Keymapper.AddAction( - "CycleAutomapType", - N_("Cycle map type"), - N_("Opaque -> Transparent -> Minimap -> None"), - SDLK_M, - CycleAutomapType, - nullptr, - IsGameRunning); - - options.Keymapper.AddAction( - "Inventory", - N_("Inventory"), - N_("Open Inventory screen."), - 'I', - InventoryKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Character", - N_("Character"), - N_("Open Character screen."), - 'C', - CharacterSheetKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Party", - N_("Party"), - N_("Open side Party panel."), - 'Y', - PartyPanelSideToggleKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "QuestLog", - N_("Quest log"), - N_("Open Quest log."), - 'Q', - QuestLogKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "SpellBook", - N_("Spellbook"), - N_("Open Spellbook."), - 'B', - SpellBookKeyPressed, - nullptr, - CanPlayerTakeAction); - for (uint32_t i = 0; i < QuickMessages.size(); ++i) { - options.Keymapper.AddAction( - "QuickMessage{}", - N_("Quick Message {}"), - N_("Use Quick Message in chat."), - (i < 4) ? static_cast(SDLK_F9) + i : static_cast(SDLK_UNKNOWN), - [i]() { DiabloHotkeyMsg(i); }, - nullptr, - nullptr, - i + 1); - } - options.Keymapper.AddAction( - "HideInfoScreens", - N_("Hide Info Screens"), - N_("Hide all info screens."), - SDLK_SPACE, - [] { - if (CanAutomapBeToggledOff()) - AutomapActive = false; - - ClosePanels(); - HelpFlag = false; - ChatLogFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - - CancelCurrentDiabloMsg(); - gamemenu_off(); - doom_close(); - }, - nullptr, - IsGameRunning); - options.Keymapper.AddAction( - "Zoom", - N_("Zoom"), - N_("Zoom Game Screen."), - 'Z', - [] { - GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); - CalcViewportGeometry(); - }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "PauseGame", - N_("Pause Game"), - N_("Pauses the game."), - 'P', - diablo_pause_game); - options.Keymapper.AddAction( - "PauseGameAlternate", - N_("Pause Game (Alternate)"), - N_("Pauses the game."), - SDLK_PAUSE, - diablo_pause_game); - options.Keymapper.AddAction( - "DecreaseBrightness", - N_("Decrease Brightness"), - N_("Reduce screen brightness."), - 'F', - DecreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "IncreaseBrightness", - N_("Increase Brightness"), - N_("Increase screen brightness."), - 'G', - IncreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Help", - N_("Help"), - N_("Open Help Screen."), - SDLK_F1, - HelpKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "Screenshot", - N_("Screenshot"), - N_("Takes a screenshot."), - SDLK_PRINTSCREEN, - nullptr, - CaptureScreen); - options.Keymapper.AddAction( - "GameInfo", - N_("Game info"), - N_("Displays game infos."), - 'V', - [] { - EventPlrMsg(fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), - PROJECT_NAME, - PROJECT_VERSION), - UiFlags::ColorWhite); - }, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "ChatLog", - N_("Chat Log"), - N_("Displays chat log."), - 'L', - [] { - ToggleChatLog(); - }); - options.Keymapper.AddAction( - "SortInv", - N_("Sort Inventory"), - N_("Sorts the inventory."), - 'R', - [] { - ReorganizeInventory(*MyPlayer); - }); -#ifdef _DEBUG - options.Keymapper.AddAction( - "OpenConsole", - N_("Console"), - N_("Opens Lua console."), - SDLK_GRAVE, - OpenConsole); - options.Keymapper.AddAction( - "DebugToggle", - "Debug toggle", - "Programming is like magic.", - 'X', - [] { - DebugToggle = !DebugToggle; - }); -#endif - options.Keymapper.CommitActions(); -} - -void InitPadmapActions() -{ - Options &options = GetOptions(); - for (int i = 0; i < 8; ++i) { - options.Padmapper.AddAction( - "BeltItem{}", - N_("Belt item {}"), - N_("Use Belt item."), - ControllerButton_NONE, - [i] { - const Player &myPlayer = *MyPlayer; - if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { - UseInvItem(INVITEM_BELT_FIRST + i); - } - }, - nullptr, - CanPlayerTakeAction, - i + 1); - } - for (uint32_t i = 0; i < NumHotkeys; ++i) { - options.Padmapper.AddAction( - "QuickSpell{}", - N_("Quick spell {}"), - N_("Hotkey for skill or spell."), - ControllerButton_NONE, - [i]() { - if (SpellSelectFlag) { - SetSpeedSpell(i); - return; - } - if (!*GetOptions().Gameplay.quickCast) - ToggleSpell(i); - else - QuickCast(i); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }, - i + 1); - } - options.Padmapper.AddAction( - "PrimaryAction", - N_("Primary action"), - N_("Attack monsters, talk to towners, lift and place inventory items."), - ControllerButton_BUTTON_B, - [] { - ControllerActionHeld = GameActionType_PRIMARY_ACTION; - LastPlayerAction = PlayerActionType::None; - PerformPrimaryAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SecondaryAction", - N_("Secondary action"), - N_("Open chests, interact with doors, pick up items."), - ControllerButton_BUTTON_Y, - [] { - ControllerActionHeld = GameActionType_SECONDARY_ACTION; - LastPlayerAction = PlayerActionType::None; - PerformSecondaryAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SpellAction", - N_("Spell action"), - N_("Cast the active spell."), - ControllerButton_BUTTON_X, - [] { - ControllerActionHeld = GameActionType_CAST_SPELL; - LastPlayerAction = PlayerActionType::None; - PerformSpellAction(); - }, - [] { - ControllerActionHeld = GameActionType_NONE; - LastPlayerAction = PlayerActionType::None; - }, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "CancelAction", - N_("Cancel action"), - N_("Close menus."), - ControllerButton_BUTTON_A, - [] { - if (DoomFlag) { - doom_close(); - return; - } - - GameAction action; - if (SpellSelectFlag) - action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); - else if (invflag) - action = GameAction(GameActionType_TOGGLE_INVENTORY); - else if (SpellbookFlag) - action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); - else if (QuestLogIsOpen) - action = GameAction(GameActionType_TOGGLE_QUEST_LOG); - else if (CharFlag) - action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); - ProcessGameAction(action); - }, - nullptr, - [] { return DoomFlag || SpellSelectFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag; }); - options.Padmapper.AddAction( - "MoveUp", - N_("Move up"), - N_("Moves the player character up."), - ControllerButton_BUTTON_DPAD_UP, - [] {}); - options.Padmapper.AddAction( - "MoveDown", - N_("Move down"), - N_("Moves the player character down."), - ControllerButton_BUTTON_DPAD_DOWN, - [] {}); - options.Padmapper.AddAction( - "MoveLeft", - N_("Move left"), - N_("Moves the player character left."), - ControllerButton_BUTTON_DPAD_LEFT, - [] {}); - options.Padmapper.AddAction( - "MoveRight", - N_("Move right"), - N_("Moves the player character right."), - ControllerButton_BUTTON_DPAD_RIGHT, - [] {}); - options.Padmapper.AddAction( - "StandGround", - N_("Stand ground"), - N_("Hold to prevent the player from moving."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "ToggleStandGround", - N_("Toggle stand ground"), - N_("Toggle whether the player moves."), - ControllerButton_NONE, - [] { StandToggle = !StandToggle; }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "UseHealthPotion", - N_("Use health potion"), - N_("Use health potions from belt."), - ControllerButton_BUTTON_LEFTSHOULDER, - [] { UseBeltItem(BeltItemType::Healing); }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "UseManaPotion", - N_("Use mana potion"), - N_("Use mana potions from belt."), - ControllerButton_BUTTON_RIGHTSHOULDER, - [] { UseBeltItem(BeltItemType::Mana); }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Character", - N_("Character"), - N_("Open Character screen."), - ControllerButton_AXIS_TRIGGERLEFT, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "Inventory", - N_("Inventory"), - N_("Open Inventory screen."), - ControllerButton_AXIS_TRIGGERRIGHT, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "QuestLog", - N_("Quest log"), - N_("Open Quest log."), - { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "SpellBook", - N_("Spellbook"), - N_("Open Spellbook."), - { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "DisplaySpells", - N_("Speedbook"), - N_("Open Speedbook."), - ControllerButton_BUTTON_A, - [] { - ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); - }, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Padmapper.AddAction( - "ToggleAutomap", - N_("Toggle automap"), - N_("Toggles if automap is displayed."), - ControllerButton_BUTTON_LEFTSTICK, - DoAutoMap); - options.Padmapper.AddAction( - "AutomapMoveUp", - N_("Automap Move Up"), - N_("Moves the automap up when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveDown", - N_("Automap Move Down"), - N_("Moves the automap down when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveLeft", - N_("Automap Move Left"), - N_("Moves the automap left when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "AutomapMoveRight", - N_("Automap Move Right"), - N_("Moves the automap right when active."), - ControllerButton_NONE, - [] {}); - options.Padmapper.AddAction( - "MouseUp", - N_("Move mouse up"), - N_("Simulates upward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, - [] {}); - options.Padmapper.AddAction( - "MouseDown", - N_("Move mouse down"), - N_("Simulates downward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, - [] {}); - options.Padmapper.AddAction( - "MouseLeft", - N_("Move mouse left"), - N_("Simulates leftward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, - [] {}); - options.Padmapper.AddAction( - "MouseRight", - N_("Move mouse right"), - N_("Simulates rightward mouse movement."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, - [] {}); - auto leftMouseDown = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - sgbMouseDown = CLICK_LEFT; - LeftMouseDown(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); - }; - auto leftMouseUp = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - LeftMouseUp(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); - }; - options.Padmapper.AddAction( - "LeftMouseClick1", - N_("Left mouse click"), - N_("Simulates the left mouse button."), - ControllerButton_BUTTON_RIGHTSTICK, - leftMouseDown, - leftMouseUp); - options.Padmapper.AddAction( - "LeftMouseClick2", - N_("Left mouse click"), - N_("Simulates the left mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, - leftMouseDown, - leftMouseUp); - auto rightMouseDown = [] { - const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); - const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_RIGHT; - RightMouseDown(standGround); - }; - auto rightMouseUp = [] { - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - }; - options.Padmapper.AddAction( - "RightMouseClick1", - N_("Right mouse click"), - N_("Simulates the right mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, - rightMouseDown, - rightMouseUp); - options.Padmapper.AddAction( - "RightMouseClick2", - N_("Right mouse click"), - N_("Simulates the right mouse button."), - { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, - rightMouseDown, - rightMouseUp); - options.Padmapper.AddAction( - "PadHotspellMenu", - N_("Gamepad hotspell menu"), - N_("Hold to set or use spell hotkeys."), - ControllerButton_BUTTON_BACK, - [] { PadHotspellMenuActive = true; }, - [] { PadHotspellMenuActive = false; }); - options.Padmapper.AddAction( - "PadMenuNavigator", - N_("Gamepad menu navigator"), - N_("Hold to access gamepad menu navigation."), - ControllerButton_BUTTON_START, - [] { PadMenuNavigatorActive = true; }, - [] { PadMenuNavigatorActive = false; }); - auto toggleGameMenu = [] { - const bool inMenu = gmenu_is_active(); - PressEscKey(); - LastPlayerAction = PlayerActionType::None; - PadHotspellMenuActive = false; - PadMenuNavigatorActive = false; - if (!inMenu) - gamemenu_on(); - }; - options.Padmapper.AddAction( - "ToggleGameMenu1", - N_("Toggle game menu"), - N_("Opens the game menu."), - { - ControllerButton_BUTTON_BACK, - ControllerButton_BUTTON_START, - }, - toggleGameMenu); - options.Padmapper.AddAction( - "ToggleGameMenu2", - N_("Toggle game menu"), - N_("Opens the game menu."), - { - ControllerButton_BUTTON_START, - ControllerButton_BUTTON_BACK, - }, - toggleGameMenu); - options.Padmapper.AddAction( - "QuickSave", - N_("Quick save"), - N_("Saves the game."), - ControllerButton_NONE, - [] { gamemenu_save_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); - options.Padmapper.AddAction( - "QuickLoad", - N_("Quick load"), - N_("Loads the game."), - ControllerButton_NONE, - [] { gamemenu_load_game(false); }, - nullptr, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); - options.Padmapper.AddAction( - "ItemHighlighting", - N_("Item highlighting"), - N_("Show/hide items on ground."), - ControllerButton_NONE, - [] { HighlightKeyPressed(true); }, - [] { HighlightKeyPressed(false); }); - options.Padmapper.AddAction( - "ToggleItemHighlighting", - N_("Toggle item highlighting"), - N_("Permanent show/hide items on ground."), - ControllerButton_NONE, - nullptr, - [] { ToggleItemLabelHighlight(); }); - options.Padmapper.AddAction( - "HideInfoScreens", - N_("Hide Info Screens"), - N_("Hide all info screens."), - ControllerButton_NONE, - [] { - if (CanAutomapBeToggledOff()) - AutomapActive = false; - - ClosePanels(); - HelpFlag = false; - ChatLogFlag = false; - SpellSelectFlag = false; - if (qtextflag && leveltype == DTYPE_TOWN) { - qtextflag = false; - stream_stop(); - } - - CancelCurrentDiabloMsg(); - gamemenu_off(); - doom_close(); - }, - nullptr, - IsGameRunning); - options.Padmapper.AddAction( - "Zoom", - N_("Zoom"), - N_("Zoom Game Screen."), - ControllerButton_NONE, - [] { - GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); - CalcViewportGeometry(); - }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "PauseGame", - N_("Pause Game"), - N_("Pauses the game."), - ControllerButton_NONE, - diablo_pause_game); - options.Padmapper.AddAction( - "DecreaseBrightness", - N_("Decrease Brightness"), - N_("Reduce screen brightness."), - ControllerButton_NONE, - DecreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "IncreaseBrightness", - N_("Increase Brightness"), - N_("Increase screen brightness."), - ControllerButton_NONE, - IncreaseBrightness, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Help", - N_("Help"), - N_("Open Help Screen."), - ControllerButton_NONE, - HelpKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "Screenshot", - N_("Screenshot"), - N_("Takes a screenshot."), - ControllerButton_NONE, - nullptr, - CaptureScreen); - options.Padmapper.AddAction( - "GameInfo", - N_("Game info"), - N_("Displays game infos."), - ControllerButton_NONE, - [] { - EventPlrMsg(fmt::format( - fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), - PROJECT_NAME, - PROJECT_VERSION), - UiFlags::ColorWhite); - }, - nullptr, - CanPlayerTakeAction); - options.Padmapper.AddAction( - "SortInv", - N_("Sort Inventory"), - N_("Sorts the inventory."), - ControllerButton_NONE, - [] { - ReorganizeInventory(*MyPlayer); - }); - options.Padmapper.AddAction( - "ChatLog", - N_("Chat Log"), - N_("Displays chat log."), - ControllerButton_NONE, - [] { - ToggleChatLog(); - }); - options.Padmapper.CommitActions(); -} - -void SetCursorPos(Point position) -{ - if (ControlDevice != ControlTypes::KeyboardAndMouse) { - MousePosition = position; - return; - } - - LogicalToOutput(&position.x, &position.y); - if (!demo::IsRunning()) - SDL_WarpMouseInWindow(ghMainWnd, position.x, position.y); -} - -void FreeGameMem() -{ - pDungeonCels = nullptr; - pMegaTiles = nullptr; - pSpecialCels = std::nullopt; - - FreeMonsters(); - FreeMissileGFX(); - FreeObjectGFX(); - FreeTownerGFX(); - FreeStashGFX(); - FreeVisualStoreGFX(); -#ifndef USE_SDL1 - DeactivateVirtualGamepad(); - FreeVirtualGamepadGFX(); -#endif -} - -bool StartGame(bool bNewGame, bool bSinglePlayer) -{ - gbSelectProvider = true; - ReturnToMainMenu = false; - - do { - gbLoadGame = false; - - if (!NetInit(bSinglePlayer)) { - gbRunGameResult = true; - break; - } - - // Save 2.8 MiB of RAM by freeing all main menu resources - // before starting the game. - UiDestroy(); - - gbSelectProvider = false; - - if (bNewGame || !gbValidSaveFile) { - InitLevels(); - InitQuests(); - InitPortals(); - InitDungMsgs(*MyPlayer); - DeltaSyncJunk(); - } - giNumberOfLevels = gbIsHellfire ? 25 : 17; - interface_mode uMsg = WM_DIABNEWGAME; - if (gbValidSaveFile && gbLoadGame) { - uMsg = WM_DIABLOADGAME; - } - RunGameLoop(uMsg); - NetClose(); - UnloadFonts(); - - // If the player left the game into the main menu, - // initialize main menu resources. - if (gbRunGameResult) - UiInitialize(); - if (ReturnToMainMenu) - return true; - } while (gbRunGameResult); - - SNetDestroy(); - return gbRunGameResult; -} - -void diablo_quit(int exitStatus) -{ - FreeGameMem(); - music_stop(); - DiabloDeinit(); - -#if SDL_VERSION_ATLEAST(2, 0, 0) - if (SdlLogFile != nullptr) std::fclose(SdlLogFile); -#endif - - exit(exitStatus); -} - -#ifdef __UWP__ -void (*onInitialized)() = NULL; - -void setOnInitialized(void (*callback)()) -{ - onInitialized = callback; -} -#endif - -int DiabloMain(int argc, char **argv) -{ -#ifdef _DEBUG - SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); -#endif - - DiabloParseFlags(argc, argv); - InitKeymapActions(); - InitPadmapActions(); - - // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings - LoadCoreArchives(); - was_archives_init = true; - - // Read settings including translation next. This will use the presence of fonts.mpq and look for assets in devilutionx.mpq - LoadOptions(); - if (demo::IsRunning()) demo::OverrideOptions(); - - // Then look for a voice pack file based on the selected translation - LoadLanguageArchive(); - - ApplicationInit(); - LuaInitialize(); - if (!demo::IsRunning()) SaveOptions(); - - // Finally load game data - LoadGameArchives(); - - LoadTextData(); - - // Load dynamic data before we go into the menu as we need to initialise player characters in memory pretty early. - LoadPlayerDataFiles(); - - // TODO: We can probably load this much later (when the game is starting). - LoadSpellData(); - LoadMissileData(); - LoadMonsterData(); - LoadItemData(); - LoadObjectData(); - LoadQuestData(); - - DiabloInit(); -#ifdef __UWP__ - onInitialized(); -#endif - if (!demo::IsRunning()) SaveOptions(); - - DiabloSplash(); - mainmenu_loop(); - DiabloDeinit(); - - return 0; -} - -bool TryIconCurs() -{ - if (pcurs == CURSOR_RESURRECT) { - if (PlayerUnderCursor != nullptr) { - NetSendCmdParam1(true, CMD_RESURRECT, PlayerUnderCursor->getId()); - NewCursor(CURSOR_HAND); - return true; - } - - return false; - } - - if (pcurs == CURSOR_HEALOTHER) { - if (PlayerUnderCursor != nullptr) { - NetSendCmdParam1(true, CMD_HEALOTHER, PlayerUnderCursor->getId()); - NewCursor(CURSOR_HAND); - return true; - } - - return false; - } - - if (pcurs == CURSOR_TELEKINESIS) { - DoTelekinesis(); - return true; - } - - Player &myPlayer = *MyPlayer; - - if (pcurs == CURSOR_IDENTIFY) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) - CheckIdentify(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - item._iIdentified = true; - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_REPAIR) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) { - if (IsVisualStoreOpen) - VisualStoreRepairItem(pcursinvitem); - else - DoRepair(myPlayer, pcursinvitem); - } else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - RepairItem(item, myPlayer.getCharacterLevel()); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_RECHARGE) { - if (pcursinvitem != -1 && !IsInspectingPlayer()) - DoRecharge(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - RechargeItem(item, myPlayer); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_OIL) { - bool changeCursor = true; - if (pcursinvitem != -1 && !IsInspectingPlayer()) - changeCursor = DoOil(myPlayer, pcursinvitem); - else if (pcursstashitem != StashStruct::EmptyCell) { - Item &item = Stash.stashList[pcursstashitem]; - changeCursor = ApplyOilToItem(item, myPlayer); - } - if (changeCursor) - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_TELEPORT) { - const SpellID spellID = myPlayer.inventorySpell; - const SpellType spellType = SpellType::Scroll; - const int spellFrom = myPlayer.spellFrom; - if (IsWallSpell(spellID)) { - const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); - NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); - } else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) { - NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); - } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { - NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); - } else { - NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast(spellID), static_cast(spellType), spellFrom); - } - NewCursor(CURSOR_HAND); - return true; - } - - if (pcurs == CURSOR_DISARM && ObjectUnderCursor == nullptr) { - NewCursor(CURSOR_HAND); - return true; - } - - return false; -} - -void diablo_pause_game() -{ - if (!gbIsMultiplayer) { - if (PauseMode != 0) { - PauseMode = 0; - } else { - PauseMode = 2; - sound_stop(); - qtextflag = false; - LastPlayerAction = PlayerActionType::None; - } - - RedrawEverything(); - } -} - -bool GameWasAlreadyPaused = false; -bool MinimizePaused = false; - -bool diablo_is_focused() -{ -#ifndef USE_SDL1 - return SDL_GetKeyboardFocus() == ghMainWnd; -#else - Uint8 appState = SDL_GetAppState(); - return (appState & SDL_APPINPUTFOCUS) != 0; -#endif -} - -void diablo_focus_pause() -{ - if (!movie_playing && (gbIsMultiplayer || MinimizePaused)) { - return; - } - - GameWasAlreadyPaused = PauseMode != 0; - - if (!GameWasAlreadyPaused) { - PauseMode = 2; - sound_stop(); - LastPlayerAction = PlayerActionType::None; - } - - SVidMute(); - music_mute(); - - MinimizePaused = true; -} - -void diablo_focus_unpause() -{ - if (!GameWasAlreadyPaused) { - PauseMode = 0; - } - - SVidUnmute(); - music_unmute(); - - MinimizePaused = false; -} - -bool PressEscKey() -{ - bool rv = false; - - if (DoomFlag) { - doom_close(); - rv = true; - } - - if (HelpFlag) { - HelpFlag = false; - rv = true; - } - - if (ChatLogFlag) { - ChatLogFlag = false; - rv = true; - } - - if (qtextflag) { - qtextflag = false; - stream_stop(); - rv = true; - } - - if (IsPlayerInStore()) { - StoreESC(); - rv = true; - } - - if (IsDiabloMsgAvailable()) { - CancelCurrentDiabloMsg(); - rv = true; - } - - if (ChatFlag) { - ResetChat(); - rv = true; - } - - if (DropGoldFlag) { - control_drop_gold(SDLK_ESCAPE); - rv = true; - } - - if (IsWithdrawGoldOpen) { - WithdrawGoldKeyPress(SDLK_ESCAPE); - rv = true; - } - - if (SpellSelectFlag) { - SpellSelectFlag = false; - rv = true; - } - - if (IsLeftPanelOpen() || IsRightPanelOpen()) { - ClosePanels(); - rv = true; - } - - return rv; -} - -void DisableInputEventHandler(const SDL_Event &event, uint16_t /*modState*/) -{ - switch (event.type) { - case SDL_EVENT_MOUSE_MOTION: - MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; - return; - case SDL_EVENT_MOUSE_BUTTON_DOWN: - if (sgbMouseDown != CLICK_NONE) - return; - switch (event.button.button) { - case SDL_BUTTON_LEFT: - sgbMouseDown = CLICK_LEFT; - return; - case SDL_BUTTON_RIGHT: - sgbMouseDown = CLICK_RIGHT; - return; - default: - return; - } - case SDL_EVENT_MOUSE_BUTTON_UP: - sgbMouseDown = CLICK_NONE; - return; - } - - MainWndProc(event); -} - -void LoadGameLevelStopMusic(_music_id neededTrack) -{ - if (neededTrack != sgnMusicTrack) - music_stop(); -} - -void LoadGameLevelStartMusic(_music_id neededTrack) -{ - if (sgnMusicTrack != neededTrack) - music_start(neededTrack); - - if (MinimizePaused) { - music_mute(); - } -} - -void LoadGameLevelResetCursor() -{ - if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { - NewCursor(CURSOR_HAND); - } -} - -void SetRndSeedForDungeonLevel() -{ - if (setlevel) { - // Maps are not randomly generated, but the monsters max hitpoints are. - // So we need to ensure that we have a stable seed when generating quest/set-maps. - // For this purpose we reuse the normal dungeon seeds. - SetRndSeed(DungeonSeeds[static_cast(setlvlnum)]); - } else { - SetRndSeed(DungeonSeeds[currlevel]); - } -} - -void LoadGameLevelFirstFlagEntry() -{ - CloseInventory(); - qtextflag = false; - if (!HeadlessMode) { - InitInv(); - ClearUniqueItemFlags(); - InitQuestText(); - InitInfoBoxGfx(); - InitHelp(); - } - InitStores(); - InitAutomapOnce(); -} - -void LoadGameLevelStores() -{ - if (leveltype == DTYPE_TOWN) { - SetupTownStores(); - } else { - FreeStoreMem(); - } -} - -void LoadGameLevelStash() -{ - const bool isHellfireSaveGame = gbIsHellfireSaveGame; - - gbIsHellfireSaveGame = gbIsHellfire; - LoadStash(); - gbIsHellfireSaveGame = isHellfireSaveGame; -} - -tl::expected LoadGameLevelDungeon(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pLvlVisited[currlevel] || gbIsMultiplayer) { - HoldThemeRooms(); - [[maybe_unused]] const uint32_t mid1Seed = GetLCGEngineState(); - InitGolems(); - InitObjects(); - [[maybe_unused]] const uint32_t mid2Seed = GetLCGEngineState(); - - IncProgress(); - - RETURN_IF_ERROR(InitMonsters()); - InitItems(); - CreateThemeRooms(); - - IncProgress(); - - [[maybe_unused]] const uint32_t mid3Seed = GetLCGEngineState(); - InitMissiles(); - InitCorpses(); -#ifdef _DEBUG - SetDebugLevelSeedInfos(mid1Seed, mid2Seed, mid3Seed, GetLCGEngineState()); -#endif - SavePreLighting(); - - IncProgress(); - - if (gbIsMultiplayer) - DeltaLoadLevel(); - } else { - HoldThemeRooms(); - InitGolems(); - RETURN_IF_ERROR(InitMonsters()); - InitMissiles(); - InitCorpses(); - - IncProgress(); - - RETURN_IF_ERROR(LoadLevel()); - - IncProgress(); - } - return {}; -} - -void LoadGameLevelSyncPlayerEntry(lvl_entry lvldir) -{ - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel() && (!player._pLvlChanging || &player == MyPlayer)) { - if (player._pHitPoints > 0) { - if (lvldir != ENTRY_LOAD) - SyncInitPlrPos(player); - } else { - dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; - } - } - } -} - -void LoadGameLevelLightVision() -{ - if (leveltype != DTYPE_TOWN) { - memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light - ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh - ProcessLightList(); - ProcessVisionList(); - } -} - -void LoadGameLevelReturn() -{ - ViewPosition = GetMapReturnPosition(); - if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) - Quests[Q_BETRAYER]._qvar2 = 2; -} - -void LoadGameLevelInitPlayers(bool firstflag, lvl_entry lvldir) -{ - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel()) { - InitPlayerGFX(player); - if (lvldir != ENTRY_LOAD) - InitPlayer(player, firstflag); - } - } -} - -void LoadGameLevelSetVisited() -{ - bool visited = false; - for (const Player &player : Players) { - if (player.plractive) - visited = visited || player._pLvlVisited[currlevel]; - } -} - -tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) - for (int j = 0; j < MAXDUNY; j++) { - dFlags[i][j] |= DungeonFlag::Lit; - } - } - - InitTowners(); - InitStash(); - InitVisualStore(); - InitItems(); - InitMissiles(); - - IncProgress(); - - if (!firstflag && lvldir != ENTRY_LOAD && myPlayer._pLvlVisited[currlevel] && !gbIsMultiplayer) - RETURN_IF_ERROR(LoadLevel()); - if (gbIsMultiplayer) - DeltaLoadLevel(); - - IncProgress(); - - for (int x = 0; x < DMAXX; x++) - for (int y = 0; y < DMAXY; y++) - UpdateAutomapExplorer({ x, y }, MAP_EXP_SELF); - return {}; -} - -tl::expected LoadGameLevelSetLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - LoadSetMap(); - IncProgress(); - RETURN_IF_ERROR(GetLevelMTypes()); - IncProgress(); - InitGolems(); - RETURN_IF_ERROR(InitMonsters()); - IncProgress(); - if (!HeadlessMode) { -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - RETURN_IF_ERROR(InitMissileGFX()); - IncProgress(); - } - InitCorpses(); - IncProgress(); - - if (lvldir == ENTRY_WARPLVL) - GetPortalLvlPos(); - IncProgress(); - - for (Player &player : Players) { - if (player.plractive && player.isOnActiveLevel()) { - InitPlayerGFX(player); - if (lvldir != ENTRY_LOAD) - InitPlayer(player, firstflag); - } - } - IncProgress(); - InitMultiView(); - IncProgress(); - - if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pSLvlVisited[setlvlnum] || gbIsMultiplayer) { - InitItems(); - SavePreLighting(); - } else { - RETURN_IF_ERROR(LoadLevel()); - } - if (gbIsMultiplayer) { - DeltaLoadLevel(); - if (!UseMultiplayerQuests()) - ResyncQuests(); - } - - PlayDungMsgs(); - InitMissiles(); - IncProgress(); - return {}; -} - -tl::expected LoadGameLevelStandardLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) -{ - CreateLevel(lvldir); - - IncProgress(); - - SetRndSeedForDungeonLevel(); - - if (leveltype != DTYPE_TOWN) { - RETURN_IF_ERROR(GetLevelMTypes()); - InitThemes(); - if (!HeadlessMode) - RETURN_IF_ERROR(LoadAllGFX()); - } else if (!HeadlessMode) { - IncProgress(); - -#if !defined(USE_SDL1) && !defined(__vita__) - InitVirtualGamepadGFX(); -#endif - - IncProgress(); - - RETURN_IF_ERROR(InitMissileGFX()); - - IncProgress(); - IncProgress(); - } - - IncProgress(); - - if (lvldir == ENTRY_RTNLVL) { - LoadGameLevelReturn(); - } - - if (lvldir == ENTRY_WARPLVL) - GetPortalLvlPos(); - - IncProgress(); - - LoadGameLevelInitPlayers(firstflag, lvldir); - InitMultiView(); - - IncProgress(); - - LoadGameLevelSetVisited(); - - SetRndSeedForDungeonLevel(); - - if (leveltype == DTYPE_TOWN) { - LoadGameLevelTown(firstflag, lvldir, myPlayer); - } else { - LoadGameLevelDungeon(firstflag, lvldir, myPlayer); - } - - PlayDungMsgs(); - - if (UseMultiplayerQuests()) - ResyncMPQuests(); - else - ResyncQuests(); - return {}; -} - -void LoadGameLevelCrypt() -{ - if (CornerStone.isAvailable()) { - CornerstoneLoad(CornerStone.position); - } - if (Quests[Q_NAKRUL]._qactive == QUEST_DONE && currlevel == 24) { - SyncNakrulRoom(); - } -} - -void LoadGameLevelCalculateCursor() -{ - // Recalculate mouse selection of entities after level change/load - LastPlayerAction = PlayerActionType::None; - sgbMouseDown = CLICK_NONE; - ResetItemlabelHighlighted(); // level changed => item changed - pcursmonst = -1; // ensure pcurstemp is set to a valid value - CheckCursMove(); -} - -tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) -{ - const _music_id neededTrack = GetLevelMusic(leveltype); - - ClearFloatingNumbers(); - LoadGameLevelStopMusic(neededTrack); - LoadGameLevelResetCursor(); - SetRndSeedForDungeonLevel(); - NaKrulTomeSequence = 0; - - IncProgress(); - - RETURN_IF_ERROR(LoadTrns()); - MakeLightTable(); - RETURN_IF_ERROR(LoadLevelSOLData()); - - IncProgress(); - - RETURN_IF_ERROR(LoadLvlGFX()); - SetDungeonMicros(pDungeonCels, MicroTileLen); - ClearClxDrawCache(); - - IncProgress(); - - if (firstflag) { - LoadGameLevelFirstFlagEntry(); - } - - SetRndSeedForDungeonLevel(); - - LoadGameLevelStores(); - - if (firstflag || lvldir == ENTRY_LOAD) { - LoadGameLevelStash(); - } - - IncProgress(); - - InitAutomap(); - - if (leveltype != DTYPE_TOWN && lvldir != ENTRY_LOAD) { - InitLighting(); - } - - InitLevelMonsters(); - - IncProgress(); - - const Player &myPlayer = *MyPlayer; - - if (setlevel) { - RETURN_IF_ERROR(LoadGameLevelSetLevel(firstflag, lvldir, myPlayer)); - } else { - RETURN_IF_ERROR(LoadGameLevelStandardLevel(firstflag, lvldir, myPlayer)); - } - - SyncPortals(); - LoadGameLevelSyncPlayerEntry(lvldir); - - IncProgress(); - IncProgress(); - - if (firstflag) { - RETURN_IF_ERROR(InitMainPanel()); - } - - IncProgress(); - - UpdateMonsterLights(); - UnstuckChargers(); - - LoadGameLevelLightVision(); - - if (leveltype == DTYPE_CRYPT) { - LoadGameLevelCrypt(); - } - -#ifndef USE_SDL1 - ActivateVirtualGamepad(); -#endif - LoadGameLevelStartMusic(neededTrack); - - CompleteProgress(); - - LoadGameLevelCalculateCursor(); - return {}; -} - -bool game_loop(bool bStartup) -{ - const uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; - - for (unsigned i = 0; i < wait; i++) { - if (!multi_handle_delta()) { - TimeoutCursor(true); - return false; - } - TimeoutCursor(false); - GameLogic(); - ClearLastSentPlayerCmd(); - - if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) - break; - } - return true; -} - -void diablo_color_cyc_logic() -{ - if (!*GetOptions().Graphics.colorCycling) - return; - - if (PauseMode != 0) - return; - - if (leveltype == DTYPE_CAVES) { - if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { - UpdatePWaterPalette(); - } else { - palette_update_caves(); - } - } else if (leveltype == DTYPE_HELL) { - lighting_color_cycling(); - } else if (leveltype == DTYPE_NEST) { - palette_update_hive(); - } else if (leveltype == DTYPE_CRYPT) { - palette_update_crypt(); - } -} - -bool IsDiabloAlive(bool playSFX) -{ - if (Quests[Q_DIABLO]._qactive == QUEST_DONE && !gbIsMultiplayer) { - if (playSFX) - PlaySFX(SfxID::DiabloDeath); - return false; - } - - return true; -} - -void PrintScreen(SDL_Keycode vkey) -{ - ReleaseKey(vkey); -} - -} // namespace devilution +/** + * @file diablo.cpp + * + * Implementation of the main game initialization functions. + */ +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif + +#include + +#include + +#include "DiabloUI/selstart.h" +#include "appfat.h" +#include "automap.h" +#include "capture.h" +#include "control/control.hpp" +#include "cursor.h" +#include "dead.h" +#ifdef _DEBUG +#include "debug.h" +#endif +#include "DiabloUI/diabloui.h" +#include "controls/control_mode.hpp" +#include "controls/keymapper.hpp" +#include "controls/plrctrls.h" +#include "controls/remap_keyboard.h" +#include "diablo.h" +#include "diablo_msg.hpp" +#include "discord/discord.h" +#include "doom.h" +#include "encrypt.h" +#include "engine/backbuffer_state.hpp" +#include "engine/clx_sprite.hpp" +#include "engine/demomode.h" +#include "engine/dx.h" +#include "engine/events.hpp" +#include "engine/load_cel.hpp" +#include "engine/load_file.hpp" +#include "engine/random.hpp" +#include "engine/render/clx_render.hpp" +#include "engine/sound.h" +#include "game_mode.hpp" +#include "gamemenu.h" +#include "gmenu.h" +#include "headless_mode.hpp" +#include "help.h" +#include "hwcursor.hpp" +#include "init.hpp" +#include "inv.h" +#include "levels/drlg_l1.h" +#include "levels/drlg_l2.h" +#include "levels/drlg_l3.h" +#include "levels/drlg_l4.h" +#include "levels/gendung.h" +#include "levels/setmaps.h" +#include "levels/themes.h" +#include "levels/town.h" +#include "levels/trigs.h" +#include "lighting.h" +#include "loadsave.h" +#include "lua/lua_event.hpp" +#include "lua/lua_global.hpp" +#include "menu.h" +#include "minitext.h" +#include "missiles.h" +#include "movie.h" +#include "multi.h" +#include "nthread.h" +#include "objects.h" +#include "options.h" +#include "panels/console.hpp" +#include "panels/info_box.hpp" +#include "panels/partypanel.hpp" +#include "panels/spell_book.hpp" +#include "panels/spell_list.hpp" +#include "pfile.h" +#include "plrmsg.h" +#include "qol/chatlog.h" +#include "qol/floatingnumbers.h" +#include "qol/itemlabels.h" +#include "qol/monhealthbar.h" +#include "qol/stash.h" +#include "qol/visual_store.h" +#include "qol/xpbar.h" +#include "quick_messages.hpp" +#include "restrict.h" +#include "stores.h" +#include "storm/storm_net.hpp" +#include "storm/storm_svid.h" +#include "tables/monstdat.h" +#include "tables/playerdat.hpp" +#include "towners.h" +#include "track.h" +#include "utils/console.h" +#include "utils/display.h" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/parse_int.hpp" +#include "utils/paths.h" +#include "utils/screen_reader.hpp" +#include "utils/sdl_compat.h" +#include "utils/sdl_thread.h" +#include "utils/status_macros.hpp" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +#ifndef USE_SDL1 +#include "controls/touch/gamepad.h" +#include "controls/touch/renderers.h" +#endif + +#ifdef __vita__ +#include "platform/vita/touch.h" +#endif + +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION +#include +#endif + +namespace devilution { + +uint32_t DungeonSeeds[NUMLEVELS]; +std::optional LevelSeeds[NUMLEVELS]; +Point MousePosition; +bool gbRunGameResult; +bool ReturnToMainMenu; +/** Enable updating of player character, set to false once Diablo dies */ +bool gbProcessPlayers; +bool gbLoadGame; +bool cineflag; +int PauseMode; +clicktype sgbMouseDown; +uint16_t gnTickDelay = 50; +char gszProductName[64] = "DevilutionX vUnknown"; + +#ifdef _DEBUG +bool DebugDisableNetworkTimeout = false; +std::vector DebugCmdsFromCommandLine; +#endif +GameLogicStep gGameLogicStep = GameLogicStep::None; + +/** This and the following mouse variables are for handling in-game click-and-hold actions */ +PlayerActionType LastPlayerAction = PlayerActionType::None; + +// Controller support: Actions to run after updating the cursor state. +// Defined in SourceX/controls/plctrls.cpp. +extern void plrctrls_after_check_curs_move(); +extern void plrctrls_every_frame(); +extern void plrctrls_after_game_logic(); + +namespace { + +char gszVersionNumber[64] = "internal version unknown"; + +bool gbGameLoopStartup; +bool forceSpawn; +bool forceDiablo; +int sgnTimeoutCurs; +bool gbShowIntro = true; +/** To know if these things have been done when we get to the diablo_deinit() function */ +bool was_archives_init = false; +/** To know if surfaces have been initialized or not */ +bool was_window_init = false; +bool was_ui_init = false; + +void StartGame(interface_mode uMsg) +{ + CalcViewportGeometry(); + cineflag = false; + InitCursor(); +#ifdef _DEBUG + LoadDebugGFX(); +#endif + assert(HeadlessMode || ghMainWnd); + music_stop(); + InitMonsterHealthBar(); + InitXPBar(); + ShowProgress(uMsg); + gmenu_init_menu(); + InitLevelCursor(); + sgnTimeoutCurs = CURSOR_NONE; + sgbMouseDown = CLICK_NONE; + LastPlayerAction = PlayerActionType::None; +} + +void FreeGame() +{ + FreeMonsterHealthBar(); + FreeXPBar(); + FreeControlPan(); + FreeInvGFX(); + FreeGMenu(); + FreeQuestText(); + FreeInfoBoxGfx(); + FreeStoreMem(); + + for (Player &player : Players) + ResetPlayerGFX(player); + + FreeCursor(); +#ifdef _DEBUG + FreeDebugGFX(); +#endif + FreeGameMem(); + stream_stop(); + music_stop(); +} + +bool ProcessInput() +{ + if (PauseMode == 2) { + return false; + } + + plrctrls_every_frame(); + + if (!gbIsMultiplayer && gmenu_is_active()) { + RedrawViewport(); + return false; + } + + if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { +#ifdef __vita__ + FinishSimulatedMouseClicks(MousePosition); +#endif + CheckCursMove(); + plrctrls_after_check_curs_move(); + RepeatPlayerAction(); + } + + return true; +} + +void LeftMouseCmd(bool bShift) +{ + bool bNear; + + assert(!GetMainPanel().contains(MousePosition)); + + if (leveltype == DTYPE_TOWN) { + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); + if (pcursitem != -1 && pcurs == CURSOR_HAND) + NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); + if (pcursmonst != -1) + NetSendCmdLocParam1(true, CMD_TALKXY, cursPosition, pcursmonst); + if (pcursitem == -1 && pcursmonst == -1 && PlayerUnderCursor == nullptr) { + LastPlayerAction = PlayerActionType::Walk; + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); + } + return; + } + + const Player &myPlayer = *MyPlayer; + bNear = myPlayer.position.tile.WalkingDistance(cursPosition) < 2; + if (pcursitem != -1 && pcurs == CURSOR_HAND && !bShift) { + NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); + } else if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled() && (!bShift || (bNear && ObjectUnderCursor->_oBreak == 1))) { + LastPlayerAction = PlayerActionType::OperateObject; + NetSendCmdLoc(MyPlayerId, true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, cursPosition); + } else if (myPlayer.UsesRangedWeapon()) { + if (bShift) { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_RATTACKXY, cursPosition); + } else if (pcursmonst != -1) { + if (CanTalkToMonst(Monsters[pcursmonst])) { + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else { + LastPlayerAction = PlayerActionType::AttackMonsterTarget; + NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); + } + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + LastPlayerAction = PlayerActionType::AttackPlayerTarget; + NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId()); + } + } else { + if (bShift) { + if (pcursmonst != -1) { + if (CanTalkToMonst(Monsters[pcursmonst])) { + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); + } + } else { + LastPlayerAction = PlayerActionType::Attack; + NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); + } + } else if (pcursmonst != -1) { + LastPlayerAction = PlayerActionType::AttackMonsterTarget; + NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + LastPlayerAction = PlayerActionType::AttackPlayerTarget; + NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId()); + } + } + if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { + LastPlayerAction = PlayerActionType::Walk; + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); + } +} + +bool TryOpenDungeonWithMouse() +{ + if (leveltype != DTYPE_TOWN) + return false; + + const Item &holdItem = MyPlayer->HoldItem; + if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) + OpenHive(); + else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) + OpenGrave(); + else + return false; + + NewCursor(CURSOR_HAND); + return true; +} + +void LeftMouseDown(uint16_t modState) +{ + LastPlayerAction = PlayerActionType::None; + + if (gmenu_left_mouse(true)) + return; + + if (CheckMuteButton()) + return; + + if (sgnTimeoutCurs != CURSOR_NONE) + return; + + if (MyPlayerIsDead) { + CheckMainPanelButtonDead(); + return; + } + + if (PauseMode == 2) { + return; + } + if (DoomFlag) { + doom_close(); + return; + } + + if (SpellSelectFlag) { + SetSpell(); + return; + } + + if (IsPlayerInStore()) { + CheckStoreBtn(); + return; + } + + const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; + const bool isCtrlHeld = (modState & SDL_KMOD_CTRL) != 0; + + if (!GetMainPanel().contains(MousePosition)) { + if (!gmenu_is_active() && !TryIconCurs()) { + if (QuestLogIsOpen && GetLeftPanel().contains(MousePosition)) { + QuestlogESC(); + } else if (qtextflag) { + qtextflag = false; + stream_stop(); + } else if (CharFlag && GetLeftPanel().contains(MousePosition)) { + CheckChrBtns(); + } else if (invflag && GetRightPanel().contains(MousePosition)) { + if (!DropGoldFlag) + CheckInvItem(isShiftHeld, isCtrlHeld); + } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { + if (!IsWithdrawGoldOpen) + CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); + CheckStashButtonPress(MousePosition); + } else if (IsVisualStoreOpen && GetLeftPanel().contains(MousePosition)) { + if (!MyPlayer->HoldItem.isEmpty()) { + CheckVisualStorePaste(MousePosition); + } else { + CheckVisualStoreItem(MousePosition, isCtrlHeld, isShiftHeld); + } + CheckVisualStoreButtonPress(MousePosition); + } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { + CheckSBook(); + } else if (!MyPlayer->HoldItem.isEmpty()) { + if (!TryOpenDungeonWithMouse()) { + const Point currentPosition = MyPlayer->position.tile; + std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, cursPosition)); + if (itemTile) { + NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, MyPlayer->HoldItem); + NewCursor(CURSOR_HAND); + } + } + } else { + CheckLevelButton(); + if (!LevelButtonDown) + LeftMouseCmd(isShiftHeld); + } + } + } else { + if (!ChatFlag && !DropGoldFlag && !IsWithdrawGoldOpen && !gmenu_is_active()) + CheckInvScrn(isShiftHeld, isCtrlHeld); + CheckMainPanelButton(); + CheckStashButtonPress(MousePosition); + if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) + NewCursor(CURSOR_HAND); + } +} + +void LeftMouseUp(uint16_t modState) +{ + gmenu_left_mouse(false); + CheckMuteButtonUp(); + if (MainPanelButtonDown) + CheckMainPanelButtonUp(); + CheckStashButtonRelease(MousePosition); + CheckVisualStoreButtonRelease(MousePosition); + if (CharPanelButtonActive) { + const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; + ReleaseChrBtns(isShiftHeld); + } + if (LevelButtonDown) + CheckLevelButtonUp(); + if (IsPlayerInStore()) + ReleaseStoreBtn(); +} + +void RightMouseDown(bool isShiftHeld) +{ + LastPlayerAction = PlayerActionType::None; + + if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { + return; + } + + if (qtextflag) { + qtextflag = false; + stream_stop(); + return; + } + + if (DoomFlag) { + doom_close(); + return; + } + if (IsPlayerInStore()) + return; + if (SpellSelectFlag) { + SetSpell(); + return; + } + if (SpellbookFlag && GetRightPanel().contains(MousePosition)) + return; + if (TryIconCurs()) + return; + if (pcursinvitem != -1 && UseInvItem(pcursinvitem)) + return; + if (pcursstashitem != StashStruct::EmptyCell && UseStashItem(pcursstashitem)) + return; + if (DidRightClickPartyPortrait()) + return; + if (pcurs == CURSOR_HAND) { + CheckPlrSpell(isShiftHeld); + } else if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { + NewCursor(CURSOR_HAND); + } +} + +void ReleaseKey(SDL_Keycode vkey) +{ + remap_keyboard_key(&vkey); + if (sgnTimeoutCurs != CURSOR_NONE) + return; + KeymapperRelease(vkey); +} + +void ClosePanels() +{ + if (CanPanelsCoverView()) { + if (!IsLeftPanelOpen() && IsRightPanelOpen() && MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } else if (!IsRightPanelOpen() && IsLeftPanelOpen() && MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + CloseInventory(); + CloseCharPanel(); + SpellbookFlag = false; + QuestLogIsOpen = false; +} + +void PressKey(SDL_Keycode vkey, uint16_t modState) +{ + Options &options = GetOptions(); + remap_keyboard_key(&vkey); + + if (vkey == SDLK_UNKNOWN) + return; + + if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { + return; + } + + if (MyPlayerIsDead) { + if (vkey == SDLK_ESCAPE) { + if (!gbIsMultiplayer) { + if (gbValidSaveFile) + gamemenu_load_game(false); + else + gamemenu_exit_game(false); + } else { + NetSendCmd(true, CMD_RETOWN); + } + return; + } + if (sgnTimeoutCurs != CURSOR_NONE) { + return; + } + KeymapperPress(vkey); + if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { + if ((modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } else { + TypeChatMessage(); + } + } + if (vkey != SDLK_ESCAPE) { + return; + } + } + // Disallow player from accessing escape menu during the frames before the death message appears + if (vkey == SDLK_ESCAPE && MyPlayer->_pHitPoints > 0) { + if (!PressEscKey()) { + LastPlayerAction = PlayerActionType::None; + gamemenu_on(); + } + return; + } + + if (DropGoldFlag) { + control_drop_gold(vkey); + return; + } + if (IsWithdrawGoldOpen) { + WithdrawGoldKeyPress(vkey); + return; + } + + if (sgnTimeoutCurs != CURSOR_NONE) { + return; + } + + KeymapperPress(vkey); + + if (PauseMode == 2) { + if ((vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) && (modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } + return; + } + + if (DoomFlag) { + doom_close(); + return; + } + + switch (vkey) { + case SDLK_PLUS: + case SDLK_KP_PLUS: + case SDLK_EQUALS: + case SDLK_KP_EQUALS: + if (AutomapActive) { + AutomapZoomIn(); + } + return; + case SDLK_MINUS: + case SDLK_KP_MINUS: + case SDLK_UNDERSCORE: + if (AutomapActive) { + AutomapZoomOut(); + } + return; +#ifdef _DEBUG + case SDLK_V: + if ((modState & SDL_KMOD_SHIFT) != 0) + NextDebugMonster(); + else + GetDebugMonster(); + return; +#endif + case SDLK_RETURN: + case SDLK_KP_ENTER: + if ((modState & SDL_KMOD_ALT) != 0) { + options.Graphics.fullscreen.SetValue(!IsFullScreen()); + if (!demo::IsRunning()) SaveOptions(); + } else if (IsPlayerInStore()) { + StoreEnter(); + } else if (QuestLogIsOpen) { + QuestlogEnter(); + } else { + TypeChatMessage(); + } + return; + case SDLK_UP: + if (IsPlayerInStore()) { + StoreUp(); + } else if (QuestLogIsOpen) { + QuestlogUp(); + } else if (HelpFlag) { + HelpScrollUp(); + } else if (ChatLogFlag) { + ChatLogScrollUp(); + } else if (AutomapActive) { + AutomapUp(); + } else if (IsStashOpen) { + Stash.PreviousPage(); + } + return; + case SDLK_DOWN: + if (IsPlayerInStore()) { + StoreDown(); + } else if (QuestLogIsOpen) { + QuestlogDown(); + } else if (HelpFlag) { + HelpScrollDown(); + } else if (ChatLogFlag) { + ChatLogScrollDown(); + } else if (AutomapActive) { + AutomapDown(); + } else if (IsStashOpen) { + Stash.NextPage(); + } + return; + case SDLK_PAGEUP: + if (IsPlayerInStore()) { + StorePrior(); + } else if (ChatLogFlag) { + ChatLogScrollTop(); + } + return; + case SDLK_PAGEDOWN: + if (IsPlayerInStore()) { + StoreNext(); + } else if (ChatLogFlag) { + ChatLogScrollBottom(); + } + return; + case SDLK_LEFT: + if (AutomapActive && !ChatFlag) + AutomapLeft(); + return; + case SDLK_RIGHT: + if (AutomapActive && !ChatFlag) + AutomapRight(); + return; + default: + break; + } +} + +void HandleMouseButtonDown(Uint8 button, uint16_t modState) +{ + if (IsPlayerInStore() && (button == SDL_BUTTON_X1 +#if !SDL_VERSION_ATLEAST(2, 0, 0) + || button == 8 +#endif + )) { + StoreESC(); + return; + } + + switch (button) { + case SDL_BUTTON_LEFT: + if (sgbMouseDown == CLICK_NONE) { + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(modState); + } + break; + case SDL_BUTTON_RIGHT: + if (sgbMouseDown == CLICK_NONE) { + sgbMouseDown = CLICK_RIGHT; + RightMouseDown((modState & SDL_KMOD_SHIFT) != 0); + } + break; + default: + KeymapperPress(static_cast(button | KeymapperMouseButtonMask)); + break; + } +} + +void HandleMouseButtonUp(Uint8 button, uint16_t modState) +{ + if (sgbMouseDown == CLICK_LEFT && button == SDL_BUTTON_LEFT) { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(modState); + } else if (sgbMouseDown == CLICK_RIGHT && button == SDL_BUTTON_RIGHT) { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + } else { + KeymapperRelease(static_cast(button | KeymapperMouseButtonMask)); + } +} + +[[maybe_unused]] void LogUnhandledEvent(const char *name, int value) +{ + LogVerbose("Unhandled SDL event: {} {}", name, value); +} + +void PrepareForFadeIn() +{ + if (HeadlessMode) return; + BlackPalette(); + + // Render the game to the buffer(s) with a fully black palette. + // Palette fade-in will gradually make it visible. + RedrawEverything(); + while (IsRedrawEverything()) { + DrawAndBlit(); + } +} + +void GameEventHandler(const SDL_Event &event, uint16_t modState) +{ + [[maybe_unused]] const Options &options = GetOptions(); + StaticVector ctrlEvents = ToControllerButtonEvents(event); + for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { + GameAction action; + if (HandleControllerButtonEvent(event, ctrlEvent, action) && action.type == GameActionType_SEND_KEY) { + if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { + const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; + if (!action.send_key.up) + HandleMouseButtonDown(static_cast(button), modState); + else + HandleMouseButtonUp(static_cast(button), modState); + } else { + if (!action.send_key.up) + PressKey(static_cast(action.send_key.vk_code), modState); + else + ReleaseKey(static_cast(action.send_key.vk_code)); + } + } + } + if (ctrlEvents.size() > 0 && ctrlEvents[0].button != ControllerButton_NONE) { + return; + } + +#ifdef _DEBUG + if (ConsoleHandleEvent(event)) { + return; + } +#endif + + if (IsChatActive() && HandleTalkTextInputEvent(event)) { + return; + } + if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { + return; + } + if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { + return; + } + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + PressKey(SDLC_EventKey(event), modState); + return; + case SDL_EVENT_KEY_UP: + ReleaseKey(SDLC_EventKey(event)); + return; + case SDL_EVENT_MOUSE_MOTION: + if (ControlMode == ControlTypes::KeyboardAndMouse && invflag) + InvalidateInventorySlot(); + MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; + gmenu_on_mouse_move(); + return; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; + HandleMouseButtonDown(event.button.button, modState); + return; + case SDL_EVENT_MOUSE_BUTTON_UP: + MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; + HandleMouseButtonUp(event.button.button, modState); + return; +#if SDL_VERSION_ATLEAST(2, 0, 0) + case SDL_EVENT_MOUSE_WHEEL: + if (SDLC_EventWheelIntY(event) > 0) { // Up + if (IsPlayerInStore()) { + StoreUp(); + } else if (QuestLogIsOpen) { + QuestlogUp(); + } else if (HelpFlag) { + HelpScrollUp(); + } else if (ChatLogFlag) { + ChatLogScrollUp(); + } else if (IsStashOpen) { + Stash.PreviousPage(); + } else if (SDL_GetModState() & SDL_KMOD_CTRL) { + if (AutomapActive) { + AutomapZoomIn(); + } + } else { + KeymapperPress(MouseScrollUpButton); + } + } else if (SDLC_EventWheelIntY(event) < 0) { // down + if (IsPlayerInStore()) { + StoreDown(); + } else if (QuestLogIsOpen) { + QuestlogDown(); + } else if (HelpFlag) { + HelpScrollDown(); + } else if (ChatLogFlag) { + ChatLogScrollDown(); + } else if (IsStashOpen) { + Stash.NextPage(); + } else if (SDL_GetModState() & SDL_KMOD_CTRL) { + if (AutomapActive) { + AutomapZoomOut(); + } + } else { + KeymapperPress(MouseScrollDownButton); + } + } else if (SDLC_EventWheelIntX(event) > 0) { // left + KeymapperPress(MouseScrollLeftButton); + } else if (SDLC_EventWheelIntX(event) < 0) { // right + KeymapperPress(MouseScrollRightButton); + } + break; +#endif + default: + if (IsCustomEvent(event.type)) { + if (gbIsMultiplayer) + pfile_write_hero(); + nthread_ignore_mutex(true); + PaletteFadeOut(8); + sound_stop(); + ShowProgress(GetCustomEvent(event)); + + PrepareForFadeIn(); + LoadPWaterPalette(); + if (gbRunGame) + PaletteFadeIn(8); + nthread_ignore_mutex(false); + gbGameLoopStartup = true; + return; + } + MainWndProc(event); + break; + } +} + +void RunGameLoop(interface_mode uMsg) +{ + demo::NotifyGameLoopStart(); + + nthread_ignore_mutex(true); + StartGame(uMsg); + assert(HeadlessMode || ghMainWnd); + EventHandler previousHandler = SetEventHandler(GameEventHandler); + run_delta_info(); + gbRunGame = true; + gbProcessPlayers = IsDiabloAlive(true); + gbRunGameResult = true; + + PrepareForFadeIn(); + LoadPWaterPalette(); + PaletteFadeIn(8); + InitBackbufferState(); + RedrawEverything(); + gbGameLoopStartup = true; + nthread_ignore_mutex(false); + + discord_manager::StartGame(); + lua::GameStart(); +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION + unsigned run_game_iteration = 0; +#endif + + while (gbRunGame) { + +#ifdef _DEBUG + if (!gbGameLoopStartup && !DebugCmdsFromCommandLine.empty()) { + InitConsole(); + for (const std::string &cmd : DebugCmdsFromCommandLine) { + RunInConsole(cmd); + } + DebugCmdsFromCommandLine.clear(); + } +#endif + + SDL_Event event; + uint16_t modState; + while (FetchMessage(&event, &modState)) { + if (event.type == SDL_EVENT_QUIT) { + gbRunGameResult = false; + gbRunGame = false; + break; + } + HandleMessage(event, modState); + } + if (!gbRunGame) + break; + + bool drawGame = true; + bool processInput = true; + const bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed(&drawGame); + if (demo::IsRecording()) + demo::RecordGameLoopResult(runGameLoop); + + discord_manager::UpdateGame(); + + if (!runGameLoop) { + if (processInput) + ProcessInput(); + DvlNet_ProcessNetworkPackets(); + if (!drawGame) + continue; + RedrawViewport(); + DrawAndBlit(); + continue; + } + + ProcessGameMessagePackets(); + if (game_loop(gbGameLoopStartup)) + diablo_color_cyc_logic(); + gbGameLoopStartup = false; + if (drawGame) + DrawAndBlit(); +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION + if (run_game_iteration++ == 0) + HeapProfilerDump("first_game_iteration"); +#endif + } + + demo::NotifyGameLoopEnd(); + + if (gbIsMultiplayer) { + pfile_write_hero(/*writeGameData=*/false); + sfile_write_stash(); + } + + PaletteFadeOut(8); + NewCursor(CURSOR_NONE); + ClearScreenBuffer(); + RedrawEverything(); + scrollrt_draw_game_screen(); + previousHandler = SetEventHandler(previousHandler); + assert(HeadlessMode || previousHandler == GameEventHandler); + FreeGame(); + + if (cineflag) { + cineflag = false; + DoEnding(); + } +} + +void PrintWithRightPadding(std::string_view str, size_t width) +{ + printInConsole(str); + if (str.size() >= width) + return; + printInConsole(std::string(width - str.size(), ' ')); +} + +void PrintHelpOption(std::string_view flags, std::string_view description) +{ + printInConsole(" "); + PrintWithRightPadding(flags, 20); + printInConsole(" "); + PrintWithRightPadding(description, 30); + printNewlineInConsole(); +} + +#if SDL_VERSION_ATLEAST(2, 0, 0) +FILE *SdlLogFile = nullptr; + +extern "C" void SdlLogToFile(void *userdata, int /*category*/, SDL_LogPriority priority, const char *message) +{ + FILE *file = reinterpret_cast(userdata); + static const char *const LogPriorityPrefixes[SDL_LOG_PRIORITY_COUNT] = { + "", + "VERBOSE", + "DEBUG", + "INFO", + "WARN", + "ERROR", + "CRITICAL" + }; + std::fprintf(file, "%s: %s\n", LogPriorityPrefixes[priority], message); + std::fflush(file); +} +#endif + +[[noreturn]] void PrintHelpAndExit() +{ + printInConsole((/* TRANSLATORS: Commandline Option */ "Options:")); + printNewlineInConsole(); + PrintHelpOption("-h, --help", _(/* TRANSLATORS: Commandline Option */ "Print this message and exit")); + PrintHelpOption("--version", _(/* TRANSLATORS: Commandline Option */ "Print the version and exit")); + PrintHelpOption("--data-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of diabdat.mpq")); + PrintHelpOption("--save-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of save files")); + PrintHelpOption("--config-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the location of diablo.ini")); + PrintHelpOption("--lang", _(/* TRANSLATORS: Commandline Option */ "Specify the language code (e.g. en or pt_BR)")); + PrintHelpOption("-n", _(/* TRANSLATORS: Commandline Option */ "Skip startup videos")); + PrintHelpOption("-f", _(/* TRANSLATORS: Commandline Option */ "Display frames per second")); + PrintHelpOption("--verbose", _(/* TRANSLATORS: Commandline Option */ "Enable verbose logging")); +#if SDL_VERSION_ATLEAST(2, 0, 0) + PrintHelpOption("--log-to-file ", _(/* TRANSLATORS: Commandline Option */ "Log to a file instead of stderr")); +#endif +#ifndef DISABLE_DEMOMODE + PrintHelpOption("--record <#>", _(/* TRANSLATORS: Commandline Option */ "Record a demo file")); + PrintHelpOption("--demo <#>", _(/* TRANSLATORS: Commandline Option */ "Play a demo file")); + PrintHelpOption("--timedemo", _(/* TRANSLATORS: Commandline Option */ "Disable all frame limiting during demo playback")); +#endif + printNewlineInConsole(); + printInConsole(_(/* TRANSLATORS: Commandline Option */ "Game selection:")); + printNewlineInConsole(); + PrintHelpOption("--spawn", _(/* TRANSLATORS: Commandline Option */ "Force Shareware mode")); + PrintHelpOption("--diablo", _(/* TRANSLATORS: Commandline Option */ "Force Diablo mode")); + PrintHelpOption("--hellfire", _(/* TRANSLATORS: Commandline Option */ "Force Hellfire mode")); + printInConsole(_(/* TRANSLATORS: Commandline Option */ "Hellfire options:")); + printNewlineInConsole(); +#ifdef _DEBUG + printNewlineInConsole(); + printInConsole("Debug options:"); + printNewlineInConsole(); + PrintHelpOption("-i", "Ignore network timeout"); + PrintHelpOption("+", "Pass commands to the engine"); +#endif + printNewlineInConsole(); + printInConsole(_("Report bugs at https://github.com/diasurgical/devilutionX/")); + printNewlineInConsole(); + diablo_quit(0); +} + +void PrintFlagMessage(std::string_view flag, std::string_view message) +{ + printInConsole(flag); + printInConsole(message); + printNewlineInConsole(); +} + +void PrintFlagRequiresArgument(std::string_view flag) +{ + PrintFlagMessage(flag, " requires an argument"); +} + +void DiabloParseFlags(int argc, char **argv) +{ +#ifdef _DEBUG + int argumentIndexOfLastCommandPart = -1; + std::string currentCommand; +#endif +#ifndef DISABLE_DEMOMODE + bool timedemo = false; + int demoNumber = -1; + int recordNumber = -1; + bool createDemoReference = false; +#endif + for (int i = 1; i < argc; i++) { + const std::string_view arg = argv[i]; + if (arg == "-h" || arg == "--help") { + PrintHelpAndExit(); + } else if (arg == "--version") { + printInConsole(PROJECT_NAME); + printInConsole(" v"); + printInConsole(PROJECT_VERSION); + printNewlineInConsole(); + diablo_quit(0); + } else if (arg == "--data-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--data-dir"); + diablo_quit(64); + } + paths::SetBasePath(argv[++i]); + } else if (arg == "--save-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--save-dir"); + diablo_quit(64); + } + paths::SetPrefPath(argv[++i]); + } else if (arg == "--config-dir") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--config-dir"); + diablo_quit(64); + } + paths::SetConfigPath(argv[++i]); + } else if (arg == "--lang") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--lang"); + diablo_quit(64); + } + forceLocale = argv[++i]; +#ifndef DISABLE_DEMOMODE + } else if (arg == "--demo") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--demo"); + diablo_quit(64); + } + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.has_value()) { + PrintFlagMessage("--demo", " must be a number"); + diablo_quit(64); + } + demoNumber = parsedParam.value(); + gbShowIntro = false; + } else if (arg == "--timedemo") { + timedemo = true; + } else if (arg == "--record") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--record"); + diablo_quit(64); + } + ParseIntResult parsedParam = ParseInt(argv[++i]); + if (!parsedParam.has_value()) { + PrintFlagMessage("--record", " must be a number"); + diablo_quit(64); + } + recordNumber = parsedParam.value(); + } else if (arg == "--create-reference") { + createDemoReference = true; +#else + } else if (arg == "--demo" || arg == "--timedemo" || arg == "--record" || arg == "--create-reference") { + printInConsole("Binary compiled without demo mode support."); + printNewlineInConsole(); + diablo_quit(1); +#endif + } else if (arg == "-n") { + gbShowIntro = false; + } else if (arg == "-f") { + EnableFrameCount(); + } else if (arg == "--spawn") { + forceSpawn = true; + } else if (arg == "--diablo") { + forceDiablo = true; + } else if (arg == "--hellfire") { + forceHellfire = true; + } else if (arg == "--vanilla") { + gbVanilla = true; + } else if (arg == "--verbose") { + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); +#if SDL_VERSION_ATLEAST(2, 0, 0) + } else if (arg == "--log-to-file") { + if (i + 1 == argc) { + PrintFlagRequiresArgument("--log-to-file"); + diablo_quit(64); + } + SdlLogFile = OpenFile(argv[++i], "wb"); + if (SdlLogFile == nullptr) { + printInConsole("Failed to open log file for writing"); + diablo_quit(64); + } + SDL_SetLogOutputFunction(&SdlLogToFile, /*userdata=*/SdlLogFile); +#endif +#ifdef _DEBUG + } else if (arg == "-i") { + DebugDisableNetworkTimeout = true; + } else if (arg[0] == '+') { + if (!currentCommand.empty()) + DebugCmdsFromCommandLine.push_back(currentCommand); + argumentIndexOfLastCommandPart = i; + currentCommand = arg.substr(1); + } else if (arg[0] != '-' && (argumentIndexOfLastCommandPart + 1) == i) { + currentCommand.append(" "); + currentCommand.append(arg); + argumentIndexOfLastCommandPart = i; +#endif + } else { + printInConsole("unrecognized option '"); + printInConsole(argv[i]); + printInConsole("'"); + printNewlineInConsole(); + PrintHelpAndExit(); + } + } + +#ifdef _DEBUG + if (!currentCommand.empty()) + DebugCmdsFromCommandLine.push_back(currentCommand); +#endif + +#ifndef DISABLE_DEMOMODE + if (demoNumber != -1) + demo::InitPlayBack(demoNumber, timedemo); + if (recordNumber != -1) + demo::InitRecording(recordNumber, createDemoReference); +#endif +} + +void DiabloInitScreen() +{ + MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; + if (ControlMode == ControlTypes::KeyboardAndMouse) + SetCursorPos(MousePosition); + + ClrDiabloMsg(); +} + +void SetApplicationVersions() +{ + *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; + *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; +} + +void CheckArchivesUpToDate() +{ + const bool devilutionxMpqOutOfDate = IsDevilutionXMpqOutOfDate(); + const bool fontsMpqOutOfDate = AreExtraFontsOutOfDate(); + + if (devilutionxMpqOutOfDate && fontsMpqOutOfDate) { + app_fatal(_("Please update devilutionx.mpq and fonts.mpq to the latest version")); + } else if (devilutionxMpqOutOfDate) { + app_fatal(_("Failed to load UI resources.\n" + "\n" + "Make sure devilutionx.mpq is in the game folder and that it is up to date.")); + } else if (fontsMpqOutOfDate) { + app_fatal(_("Please update fonts.mpq to the latest version")); + } +} + +void ApplicationInit() +{ + if (*GetOptions().Graphics.showFPS) + EnableFrameCount(); + + init_create_window(); + was_window_init = true; + + InitializeScreenReader(); + LanguageInitialize(); + + SetApplicationVersions(); + + ReadOnlyTest(); +} + +void DiabloInit() +{ + if (forceSpawn || *GetOptions().GameMode.shareware) + gbIsSpawn = 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; + + for (size_t i = 0; i < QuickMessages.size(); i++) { + auto &messages = GetOptions().Chat.szHotKeyMsgs[i]; + if (messages.empty()) { + messages.emplace_back(_(QuickMessages[i].message)); + } + } + +#ifndef USE_SDL1 + InitializeVirtualGamepad(); +#endif + + UiInitialize(); + was_ui_init = true; + + if (wasHellfireDiscovered) { + UiSelStartUpGameOption(); + if (!gbIsHellfire) { + // Reinitialize the UI Elements because we changed the game + UnloadUiGFX(); + UiInitialize(); + if (IsHardwareCursor()) + SetHardwareCursor(CursorInfo::UnknownCursor()); + } + } + + DiabloInitScreen(); + + snd_init(); + + ui_sound_init(); + + // Item graphics are loaded early, they already get touched during hero selection. + InitItemGFX(); + + // Always available. + LoadSmallSelectionSpinner(); + + CheckArchivesUpToDate(); +} + +void DiabloSplash() +{ + if (!gbShowIntro) + return; + + if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) + play_movie("gendata\\logo.smk", true); + + auto &intro = gbIsHellfire ? GetOptions().StartUp.hellfireIntro : GetOptions().StartUp.diabloIntro; + + if (*intro != StartUpIntro::Off) { + if (gbIsHellfire) + play_movie("gendata\\Hellfire.smk", true); + else + play_movie("gendata\\diablo1.smk", true); + if (*intro == StartUpIntro::Once) { + intro.SetValue(StartUpIntro::Off); + if (!demo::IsRunning()) SaveOptions(); + } + } + + if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) + UiTitleDialog(); +} + +void DiabloDeinit() +{ + FreeItemGFX(); + + LuaShutdown(); + ShutDownScreenReader(); + + if (gbSndInited) + effects_cleanup_sfx(); + snd_deinit(); + if (was_ui_init) + UiDestroy(); + if (was_archives_init) + init_cleanup(); + if (was_window_init) + dx_cleanup(); // Cleanup SDL surfaces stuff, so we have to do it before SDL_Quit(). + UnloadFonts(); + if (SDL_WasInit((~0U) & ~SDL_INIT_HAPTIC) != 0) + SDL_Quit(); +} + +tl::expected LoadLvlGFX() +{ + assert(pDungeonCels == nullptr); + constexpr int SpecialCelWidth = 64; + + const auto loadAll = [](const char *cel, const char *til, const char *special) -> tl::expected { + ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus(cel)); + ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus(til)); + ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus(special, SpecialCelWidth)); + return {}; + }; + + switch (leveltype) { + case DTYPE_TOWN: { + auto cel = LoadFileInMemWithStatus("nlevels\\towndata\\town.cel"); + if (!cel.has_value()) { + ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus("levels\\towndata\\town.cel")); + } else { + pDungeonCels = std::move(*cel); + } + auto til = LoadFileInMemWithStatus("nlevels\\towndata\\town.til"); + if (!til.has_value()) { + ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus("levels\\towndata\\town.til")); + } else { + pMegaTiles = std::move(*til); + } + ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus("levels\\towndata\\towns", SpecialCelWidth)); + return {}; + } + case DTYPE_CATHEDRAL: + return loadAll( + "levels\\l1data\\l1.cel", + "levels\\l1data\\l1.til", + "levels\\l1data\\l1s"); + case DTYPE_CATACOMBS: + return loadAll( + "levels\\l2data\\l2.cel", + "levels\\l2data\\l2.til", + "levels\\l2data\\l2s"); + case DTYPE_CAVES: + return loadAll( + "levels\\l3data\\l3.cel", + "levels\\l3data\\l3.til", + "levels\\l1data\\l1s"); + case DTYPE_HELL: + return loadAll( + "levels\\l4data\\l4.cel", + "levels\\l4data\\l4.til", + "levels\\l2data\\l2s"); + case DTYPE_NEST: + return loadAll( + "nlevels\\l6data\\l6.cel", + "nlevels\\l6data\\l6.til", + "levels\\l1data\\l1s"); + case DTYPE_CRYPT: + return loadAll( + "nlevels\\l5data\\l5.cel", + "nlevels\\l5data\\l5.til", + "nlevels\\l5data\\l5s"); + default: + return tl::make_unexpected("LoadLvlGFX"); + } +} + +tl::expected LoadAllGFX() +{ + IncProgress(); +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + IncProgress(); + RETURN_IF_ERROR(InitObjectGFX()); + IncProgress(); + RETURN_IF_ERROR(InitMissileGFX()); + IncProgress(); + return {}; +} + +/** + * @param entry Where is the player entering from + */ +void CreateLevel(lvl_entry entry) +{ + CreateDungeon(DungeonSeeds[currlevel], entry); + + switch (leveltype) { + case DTYPE_TOWN: + InitTownTriggers(); + break; + case DTYPE_CATHEDRAL: + InitL1Triggers(); + break; + case DTYPE_CATACOMBS: + InitL2Triggers(); + break; + case DTYPE_CAVES: + InitL3Triggers(); + break; + case DTYPE_HELL: + InitL4Triggers(); + break; + case DTYPE_NEST: + InitHiveTriggers(); + break; + case DTYPE_CRYPT: + InitCryptTriggers(); + break; + default: + app_fatal("CreateLevel"); + } + + if (leveltype != DTYPE_TOWN) { + Freeupstairs(); + } + LoadRndLvlPal(leveltype); +} + +void UnstuckChargers() +{ + if (gbIsMultiplayer) { + for (Player &player : Players) { + if (!player.plractive) + continue; + if (player._pLvlChanging) + continue; + if (!player.isOnActiveLevel()) + continue; + if (&player == MyPlayer) + continue; + return; + } + } + for (size_t i = 0; i < ActiveMonsterCount; i++) { + Monster &monster = Monsters[ActiveMonsters[i]]; + if (monster.mode == MonsterMode::Charge) + monster.mode = MonsterMode::Stand; + } +} + +void UpdateMonsterLights() +{ + for (size_t i = 0; i < ActiveMonsterCount; i++) { + Monster &monster = Monsters[ActiveMonsters[i]]; + + if ((monster.flags & MFLAG_BERSERK) != 0) { + const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; + monster.lightId = AddLight(monster.position.tile, lightRadius); + } + + if (monster.lightId != NO_LIGHT) { + if (monster.lightId == MyPlayer->lightId) { // Fix old saves where some monsters had 0 instead of NO_LIGHT + monster.lightId = NO_LIGHT; + continue; + } + + const Light &light = Lights[monster.lightId]; + if (monster.position.tile != light.position.tile) { + ChangeLightXY(monster.lightId, monster.position.tile); + } + } + } +} + +void GameLogic() +{ + if (!ProcessInput()) { + return; + } + if (gbProcessPlayers) { + gGameLogicStep = GameLogicStep::ProcessPlayers; + ProcessPlayers(); + } + if (leveltype != DTYPE_TOWN) { + gGameLogicStep = GameLogicStep::ProcessMonsters; +#ifdef _DEBUG + if (!DebugInvisible) +#endif + ProcessMonsters(); + gGameLogicStep = GameLogicStep::ProcessObjects; + ProcessObjects(); + gGameLogicStep = GameLogicStep::ProcessMissiles; + ProcessMissiles(); + gGameLogicStep = GameLogicStep::ProcessItems; + ProcessItems(); + ProcessLightList(); + ProcessVisionList(); + } else { + gGameLogicStep = GameLogicStep::ProcessTowners; + ProcessTowners(); + gGameLogicStep = GameLogicStep::ProcessItemsTown; + ProcessItems(); + gGameLogicStep = GameLogicStep::ProcessMissilesTown; + ProcessMissiles(); + } + gGameLogicStep = GameLogicStep::None; + +#ifdef _DEBUG + if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { + ScrollView(); + } +#endif + + sound_update(); + CheckTriggers(); + CheckQuests(); + RedrawViewport(); + pfile_update(false); + + plrctrls_after_game_logic(); +} + +void TimeoutCursor(bool bTimeout) +{ + if (bTimeout) { + if (sgnTimeoutCurs == CURSOR_NONE && sgbMouseDown == CLICK_NONE) { + sgnTimeoutCurs = pcurs; + multi_net_ping(); + InfoString = StringOrView {}; + AddInfoBoxString(_("-- Network timeout --")); + AddInfoBoxString(_("-- Waiting for players --")); + for (uint8_t i = 0; i < Players.size(); i++) { + bool isConnected = (player_state[i] & PS_CONNECTED) != 0; + bool isActive = (player_state[i] & PS_ACTIVE) != 0; + if (!isConnected || isActive) continue; + + DvlNetLatencies latencies = DvlNet_GetLatencies(i); + + std::string ping = fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Player {:s} is timing out!")), + Players[i].name()); + + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); + + if (latencies.providerLatency) { + if (latencies.isRelayed && *latencies.isRelayed) { + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); + } else { + StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); + } + } + EventPlrMsg(ping); + } + NewCursor(CURSOR_HOURGLASS); + RedrawEverything(); + } + scrollrt_draw_game_screen(); + } else if (sgnTimeoutCurs != CURSOR_NONE) { + // Timeout is gone, we should restore the previous cursor. + // But the timeout cursor could already be changed by the now processed messages (for example item cursor from CMD_GETITEM). + // Changing the item cursor back to the previous (hand) cursor could result in deleted items, because this resets Player.HoldItem (see NewCursor). + if (pcurs == CURSOR_HOURGLASS) + NewCursor(sgnTimeoutCurs); + sgnTimeoutCurs = CURSOR_NONE; + InfoString = StringOrView {}; + RedrawEverything(); + } +} + +void HelpKeyPressed() +{ + if (HelpFlag) { + HelpFlag = false; + } else if (IsPlayerInStore()) { + InfoString = StringOrView {}; + AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed + AddInfoBoxString(_("while in stores")); + LastPlayerAction = PlayerActionType::None; + } else { + CloseInventory(); + CloseCharPanel(); + SpellbookFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + QuestLogIsOpen = false; + CancelCurrentDiabloMsg(); + gamemenu_off(); + DisplayHelp(); + doom_close(); + } +} + +void InventoryKeyPressed() +{ + if (IsPlayerInStore()) + return; + invflag = !invflag; + if (!IsLeftPanelOpen() && CanPanelsCoverView()) { + if (!invflag) { // We closed the inventory + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } else if (!SpellbookFlag) { // We opened the inventory + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + } + SpellbookFlag = false; + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); +} + +void CharacterSheetKeyPressed() +{ + if (IsPlayerInStore()) + return; + if (!IsRightPanelOpen() && CanPanelsCoverView()) { + if (CharFlag) { // We are closing the character sheet + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } else if (!QuestLogIsOpen) { // We opened the character sheet + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } + } + ToggleCharPanel(); +} + +void PartyPanelSideToggleKeyPressed() +{ + PartySidePanelOpen = !PartySidePanelOpen; +} + +void QuestLogKeyPressed() +{ + if (IsPlayerInStore()) + return; + if (!QuestLogIsOpen) { + StartQuestlog(); + } else { + QuestLogIsOpen = false; + } + if (!IsRightPanelOpen() && CanPanelsCoverView()) { + if (!QuestLogIsOpen) { // We closed the quest log + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } else if (!CharFlag) { // We opened the character quest log + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } + } + CloseCharPanel(); + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); +} + +void DisplaySpellsKeyPressed() +{ + if (IsPlayerInStore()) + return; + CloseCharPanel(); + QuestLogIsOpen = false; + CloseInventory(); + SpellbookFlag = false; + if (!SpellSelectFlag) { + DoSpeedBook(); + } else { + SpellSelectFlag = false; + } + LastPlayerAction = PlayerActionType::None; +} + +void SpellBookKeyPressed() +{ + if (IsPlayerInStore()) + return; + SpellbookFlag = !SpellbookFlag; + if (!IsLeftPanelOpen() && CanPanelsCoverView()) { + if (!SpellbookFlag) { // We closed the inventory + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } else if (!invflag) { // We opened the inventory + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + } + CloseInventory(); + CloseVisualStore(); +} + +void CycleSpellHotkeys(bool next) +{ + StaticVector validHotKeyIndexes; + std::optional currentIndex; + for (size_t slot = 0; slot < NumHotkeys; slot++) { + if (!IsValidSpeedSpell(slot)) + continue; + if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) { + // found current + currentIndex = validHotKeyIndexes.size(); + } + validHotKeyIndexes.emplace_back(slot); + } + if (validHotKeyIndexes.size() == 0) + return; + + size_t newIndex; + if (!currentIndex) { + newIndex = next ? 0 : (validHotKeyIndexes.size() - 1); + } else if (next) { + newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1); + } else { + newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1); + } + ToggleSpell(validHotKeyIndexes[newIndex]); +} + +bool IsPlayerDead() +{ + return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; +} + +bool IsGameRunning() +{ + return PauseMode != 2; +} + +bool CanPlayerTakeAction() +{ + return !IsPlayerDead() && IsGameRunning(); +} + +bool CanAutomapBeToggledOff() +{ + // check if every window is closed - if yes, automap can be toggled off + return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !IsVisualStoreOpen && !CharFlag + && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag + && !ChatLogFlag && !HelpFlag; +} + +void OptionLanguageCodeChanged() +{ + UnloadFonts(); + LanguageInitialize(); + LoadLanguageArchive(); + effects_cleanup_sfx(false); + if (gbRunGame) + sound_init(); + else + ui_sound_init(); +} + +const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); + +} // namespace + +void InitKeymapActions() +{ + Options &options = GetOptions(); + for (uint32_t i = 0; i < 8; ++i) { + options.Keymapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), + '1' + i, + [i] { + const Player &myPlayer = *MyPlayer; + if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { + UseInvItem(INVITEM_BELT_FIRST + i); + } + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + for (uint32_t i = 0; i < NumHotkeys; ++i) { + options.Keymapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), + i < 4 ? static_cast(SDLK_F5) + i : static_cast(SDLK_UNKNOWN), + [i]() { + if (SpellSelectFlag) { + SetSpeedSpell(i); + return; + } + if (!*GetOptions().Gameplay.quickCast) + ToggleSpell(i); + else + QuickCast(i); + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + options.Keymapper.AddAction( + "QuickSpellPrevious", + N_("Previous quick spell"), + N_("Selects the previous quick spell (cycles)."), + MouseScrollUpButton, + [] { CycleSpellHotkeys(false); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuickSpellNext", + N_("Next quick spell"), + N_("Selects the next quick spell (cycles)."), + MouseScrollDownButton, + [] { CycleSpellHotkeys(true); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "UseHealthPotion", + N_("Use health potion"), + N_("Use health potions from belt."), + SDLK_UNKNOWN, + [] { UseBeltItem(BeltItemType::Healing); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "UseManaPotion", + N_("Use mana potion"), + N_("Use mana potions from belt."), + SDLK_UNKNOWN, + [] { UseBeltItem(BeltItemType::Mana); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), + 'S', + DisplaySpellsKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuickSave", + N_("Quick save"), + N_("Saves the game."), + SDLK_F2, + [] { gamemenu_save_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); + options.Keymapper.AddAction( + "QuickLoad", + N_("Quick load"), + N_("Loads the game."), + SDLK_F3, + [] { gamemenu_load_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); +#ifndef NOEXIT + options.Keymapper.AddAction( + "QuitGame", + N_("Quit game"), + N_("Closes the game."), + SDLK_UNKNOWN, + [] { gamemenu_quit_game(false); }); +#endif + options.Keymapper.AddAction( + "StopHero", + N_("Stop hero"), + N_("Stops walking and cancel pending actions."), + SDLK_UNKNOWN, + [] { MyPlayer->Stop(); }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "ItemHighlighting", + N_("Item highlighting"), + N_("Show/hide items on ground."), + SDLK_LALT, + [] { HighlightKeyPressed(true); }, + [] { HighlightKeyPressed(false); }); + options.Keymapper.AddAction( + "ToggleItemHighlighting", + N_("Toggle item highlighting"), + N_("Permanent show/hide items on ground."), + SDLK_RCTRL, + nullptr, + [] { ToggleItemLabelHighlight(); }); + options.Keymapper.AddAction( + "ToggleAutomap", + N_("Toggle automap"), + N_("Toggles if automap is displayed."), + SDLK_TAB, + DoAutoMap, + nullptr, + IsGameRunning); + options.Keymapper.AddAction( + "CycleAutomapType", + N_("Cycle map type"), + N_("Opaque -> Transparent -> Minimap -> None"), + SDLK_M, + CycleAutomapType, + nullptr, + IsGameRunning); + + options.Keymapper.AddAction( + "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), + 'I', + InventoryKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Character", + N_("Character"), + N_("Open Character screen."), + 'C', + CharacterSheetKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Party", + N_("Party"), + N_("Open side Party panel."), + 'Y', + PartyPanelSideToggleKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "QuestLog", + N_("Quest log"), + N_("Open Quest log."), + 'Q', + QuestLogKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), + 'B', + SpellBookKeyPressed, + nullptr, + CanPlayerTakeAction); + for (uint32_t i = 0; i < QuickMessages.size(); ++i) { + options.Keymapper.AddAction( + "QuickMessage{}", + N_("Quick Message {}"), + N_("Use Quick Message in chat."), + (i < 4) ? static_cast(SDLK_F9) + i : static_cast(SDLK_UNKNOWN), + [i]() { DiabloHotkeyMsg(i); }, + nullptr, + nullptr, + i + 1); + } + options.Keymapper.AddAction( + "HideInfoScreens", + N_("Hide Info Screens"), + N_("Hide all info screens."), + SDLK_SPACE, + [] { + if (CanAutomapBeToggledOff()) + AutomapActive = false; + + ClosePanels(); + HelpFlag = false; + ChatLogFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + + CancelCurrentDiabloMsg(); + gamemenu_off(); + doom_close(); + }, + nullptr, + IsGameRunning); + options.Keymapper.AddAction( + "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), + 'Z', + [] { + GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "PauseGame", + N_("Pause Game"), + N_("Pauses the game."), + 'P', + diablo_pause_game); + options.Keymapper.AddAction( + "PauseGameAlternate", + N_("Pause Game (Alternate)"), + N_("Pauses the game."), + SDLK_PAUSE, + diablo_pause_game); + options.Keymapper.AddAction( + "DecreaseBrightness", + N_("Decrease Brightness"), + N_("Reduce screen brightness."), + 'F', + DecreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "IncreaseBrightness", + N_("Increase Brightness"), + N_("Increase screen brightness."), + 'G', + IncreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Help", + N_("Help"), + N_("Open Help Screen."), + SDLK_F1, + HelpKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "Screenshot", + N_("Screenshot"), + N_("Takes a screenshot."), + SDLK_PRINTSCREEN, + nullptr, + CaptureScreen); + options.Keymapper.AddAction( + "GameInfo", + N_("Game info"), + N_("Displays game infos."), + 'V', + [] { + EventPlrMsg(fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), + PROJECT_NAME, + PROJECT_VERSION), + UiFlags::ColorWhite); + }, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "ChatLog", + N_("Chat Log"), + N_("Displays chat log."), + 'L', + [] { + ToggleChatLog(); + }); + options.Keymapper.AddAction( + "SortInv", + N_("Sort Inventory"), + N_("Sorts the inventory."), + 'R', + [] { + ReorganizeInventory(*MyPlayer); + }); +#ifdef _DEBUG + options.Keymapper.AddAction( + "OpenConsole", + N_("Console"), + N_("Opens Lua console."), + SDLK_GRAVE, + OpenConsole); + options.Keymapper.AddAction( + "DebugToggle", + "Debug toggle", + "Programming is like magic.", + 'X', + [] { + DebugToggle = !DebugToggle; + }); +#endif + options.Keymapper.CommitActions(); +} + +void InitPadmapActions() +{ + Options &options = GetOptions(); + for (int i = 0; i < 8; ++i) { + options.Padmapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), + ControllerButton_NONE, + [i] { + const Player &myPlayer = *MyPlayer; + if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { + UseInvItem(INVITEM_BELT_FIRST + i); + } + }, + nullptr, + CanPlayerTakeAction, + i + 1); + } + for (uint32_t i = 0; i < NumHotkeys; ++i) { + options.Padmapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), + ControllerButton_NONE, + [i]() { + if (SpellSelectFlag) { + SetSpeedSpell(i); + return; + } + if (!*GetOptions().Gameplay.quickCast) + ToggleSpell(i); + else + QuickCast(i); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }, + i + 1); + } + options.Padmapper.AddAction( + "PrimaryAction", + N_("Primary action"), + N_("Attack monsters, talk to towners, lift and place inventory items."), + ControllerButton_BUTTON_B, + [] { + ControllerActionHeld = GameActionType_PRIMARY_ACTION; + LastPlayerAction = PlayerActionType::None; + PerformPrimaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SecondaryAction", + N_("Secondary action"), + N_("Open chests, interact with doors, pick up items."), + ControllerButton_BUTTON_Y, + [] { + ControllerActionHeld = GameActionType_SECONDARY_ACTION; + LastPlayerAction = PlayerActionType::None; + PerformSecondaryAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SpellAction", + N_("Spell action"), + N_("Cast the active spell."), + ControllerButton_BUTTON_X, + [] { + ControllerActionHeld = GameActionType_CAST_SPELL; + LastPlayerAction = PlayerActionType::None; + PerformSpellAction(); + }, + [] { + ControllerActionHeld = GameActionType_NONE; + LastPlayerAction = PlayerActionType::None; + }, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "CancelAction", + N_("Cancel action"), + N_("Close menus."), + ControllerButton_BUTTON_A, + [] { + if (DoomFlag) { + doom_close(); + return; + } + + GameAction action; + if (SpellSelectFlag) + action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); + else if (invflag) + action = GameAction(GameActionType_TOGGLE_INVENTORY); + else if (SpellbookFlag) + action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); + else if (QuestLogIsOpen) + action = GameAction(GameActionType_TOGGLE_QUEST_LOG); + else if (CharFlag) + action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); + ProcessGameAction(action); + }, + nullptr, + [] { return DoomFlag || SpellSelectFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag; }); + options.Padmapper.AddAction( + "MoveUp", + N_("Move up"), + N_("Moves the player character up."), + ControllerButton_BUTTON_DPAD_UP, + [] {}); + options.Padmapper.AddAction( + "MoveDown", + N_("Move down"), + N_("Moves the player character down."), + ControllerButton_BUTTON_DPAD_DOWN, + [] {}); + options.Padmapper.AddAction( + "MoveLeft", + N_("Move left"), + N_("Moves the player character left."), + ControllerButton_BUTTON_DPAD_LEFT, + [] {}); + options.Padmapper.AddAction( + "MoveRight", + N_("Move right"), + N_("Moves the player character right."), + ControllerButton_BUTTON_DPAD_RIGHT, + [] {}); + options.Padmapper.AddAction( + "StandGround", + N_("Stand ground"), + N_("Hold to prevent the player from moving."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "ToggleStandGround", + N_("Toggle stand ground"), + N_("Toggle whether the player moves."), + ControllerButton_NONE, + [] { StandToggle = !StandToggle; }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "UseHealthPotion", + N_("Use health potion"), + N_("Use health potions from belt."), + ControllerButton_BUTTON_LEFTSHOULDER, + [] { UseBeltItem(BeltItemType::Healing); }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "UseManaPotion", + N_("Use mana potion"), + N_("Use mana potions from belt."), + ControllerButton_BUTTON_RIGHTSHOULDER, + [] { UseBeltItem(BeltItemType::Mana); }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Character", + N_("Character"), + N_("Open Character screen."), + ControllerButton_AXIS_TRIGGERLEFT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), + ControllerButton_AXIS_TRIGGERRIGHT, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "QuestLog", + N_("Quest log"), + N_("Open Quest log."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), + { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), + ControllerButton_BUTTON_A, + [] { + ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); + }, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Padmapper.AddAction( + "ToggleAutomap", + N_("Toggle automap"), + N_("Toggles if automap is displayed."), + ControllerButton_BUTTON_LEFTSTICK, + DoAutoMap); + options.Padmapper.AddAction( + "AutomapMoveUp", + N_("Automap Move Up"), + N_("Moves the automap up when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveDown", + N_("Automap Move Down"), + N_("Moves the automap down when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveLeft", + N_("Automap Move Left"), + N_("Moves the automap left when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "AutomapMoveRight", + N_("Automap Move Right"), + N_("Moves the automap right when active."), + ControllerButton_NONE, + [] {}); + options.Padmapper.AddAction( + "MouseUp", + N_("Move mouse up"), + N_("Simulates upward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, + [] {}); + options.Padmapper.AddAction( + "MouseDown", + N_("Move mouse down"), + N_("Simulates downward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, + [] {}); + options.Padmapper.AddAction( + "MouseLeft", + N_("Move mouse left"), + N_("Simulates leftward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, + [] {}); + options.Padmapper.AddAction( + "MouseRight", + N_("Move mouse right"), + N_("Simulates rightward mouse movement."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, + [] {}); + auto leftMouseDown = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + sgbMouseDown = CLICK_LEFT; + LeftMouseDown(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); + }; + auto leftMouseUp = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + LeftMouseUp(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); + }; + options.Padmapper.AddAction( + "LeftMouseClick1", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + ControllerButton_BUTTON_RIGHTSTICK, + leftMouseDown, + leftMouseUp); + options.Padmapper.AddAction( + "LeftMouseClick2", + N_("Left mouse click"), + N_("Simulates the left mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, + leftMouseDown, + leftMouseUp); + auto rightMouseDown = [] { + const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); + const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_RIGHT; + RightMouseDown(standGround); + }; + auto rightMouseUp = [] { + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + }; + options.Padmapper.AddAction( + "RightMouseClick1", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, + rightMouseDown, + rightMouseUp); + options.Padmapper.AddAction( + "RightMouseClick2", + N_("Right mouse click"), + N_("Simulates the right mouse button."), + { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, + rightMouseDown, + rightMouseUp); + options.Padmapper.AddAction( + "PadHotspellMenu", + N_("Gamepad hotspell menu"), + N_("Hold to set or use spell hotkeys."), + ControllerButton_BUTTON_BACK, + [] { PadHotspellMenuActive = true; }, + [] { PadHotspellMenuActive = false; }); + options.Padmapper.AddAction( + "PadMenuNavigator", + N_("Gamepad menu navigator"), + N_("Hold to access gamepad menu navigation."), + ControllerButton_BUTTON_START, + [] { PadMenuNavigatorActive = true; }, + [] { PadMenuNavigatorActive = false; }); + auto toggleGameMenu = [] { + const bool inMenu = gmenu_is_active(); + PressEscKey(); + LastPlayerAction = PlayerActionType::None; + PadHotspellMenuActive = false; + PadMenuNavigatorActive = false; + if (!inMenu) + gamemenu_on(); + }; + options.Padmapper.AddAction( + "ToggleGameMenu1", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_BACK, + ControllerButton_BUTTON_START, + }, + toggleGameMenu); + options.Padmapper.AddAction( + "ToggleGameMenu2", + N_("Toggle game menu"), + N_("Opens the game menu."), + { + ControllerButton_BUTTON_START, + ControllerButton_BUTTON_BACK, + }, + toggleGameMenu); + options.Padmapper.AddAction( + "QuickSave", + N_("Quick save"), + N_("Saves the game."), + ControllerButton_NONE, + [] { gamemenu_save_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); + options.Padmapper.AddAction( + "QuickLoad", + N_("Quick load"), + N_("Loads the game."), + ControllerButton_NONE, + [] { gamemenu_load_game(false); }, + nullptr, + [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); + options.Padmapper.AddAction( + "ItemHighlighting", + N_("Item highlighting"), + N_("Show/hide items on ground."), + ControllerButton_NONE, + [] { HighlightKeyPressed(true); }, + [] { HighlightKeyPressed(false); }); + options.Padmapper.AddAction( + "ToggleItemHighlighting", + N_("Toggle item highlighting"), + N_("Permanent show/hide items on ground."), + ControllerButton_NONE, + nullptr, + [] { ToggleItemLabelHighlight(); }); + options.Padmapper.AddAction( + "HideInfoScreens", + N_("Hide Info Screens"), + N_("Hide all info screens."), + ControllerButton_NONE, + [] { + if (CanAutomapBeToggledOff()) + AutomapActive = false; + + ClosePanels(); + HelpFlag = false; + ChatLogFlag = false; + SpellSelectFlag = false; + if (qtextflag && leveltype == DTYPE_TOWN) { + qtextflag = false; + stream_stop(); + } + + CancelCurrentDiabloMsg(); + gamemenu_off(); + doom_close(); + }, + nullptr, + IsGameRunning); + options.Padmapper.AddAction( + "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), + ControllerButton_NONE, + [] { + GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "PauseGame", + N_("Pause Game"), + N_("Pauses the game."), + ControllerButton_NONE, + diablo_pause_game); + options.Padmapper.AddAction( + "DecreaseBrightness", + N_("Decrease Brightness"), + N_("Reduce screen brightness."), + ControllerButton_NONE, + DecreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "IncreaseBrightness", + N_("Increase Brightness"), + N_("Increase screen brightness."), + ControllerButton_NONE, + IncreaseBrightness, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Help", + N_("Help"), + N_("Open Help Screen."), + ControllerButton_NONE, + HelpKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "Screenshot", + N_("Screenshot"), + N_("Takes a screenshot."), + ControllerButton_NONE, + nullptr, + CaptureScreen); + options.Padmapper.AddAction( + "GameInfo", + N_("Game info"), + N_("Displays game infos."), + ControllerButton_NONE, + [] { + EventPlrMsg(fmt::format( + fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), + PROJECT_NAME, + PROJECT_VERSION), + UiFlags::ColorWhite); + }, + nullptr, + CanPlayerTakeAction); + options.Padmapper.AddAction( + "SortInv", + N_("Sort Inventory"), + N_("Sorts the inventory."), + ControllerButton_NONE, + [] { + ReorganizeInventory(*MyPlayer); + }); + options.Padmapper.AddAction( + "ChatLog", + N_("Chat Log"), + N_("Displays chat log."), + ControllerButton_NONE, + [] { + ToggleChatLog(); + }); + options.Padmapper.CommitActions(); +} + +void SetCursorPos(Point position) +{ + if (ControlDevice != ControlTypes::KeyboardAndMouse) { + MousePosition = position; + return; + } + + LogicalToOutput(&position.x, &position.y); + if (!demo::IsRunning()) + SDL_WarpMouseInWindow(ghMainWnd, position.x, position.y); +} + +void FreeGameMem() +{ + pDungeonCels = nullptr; + pMegaTiles = nullptr; + pSpecialCels = std::nullopt; + + FreeMonsters(); + FreeMissileGFX(); + FreeObjectGFX(); + FreeTownerGFX(); + FreeStashGFX(); + FreeVisualStoreGFX(); +#ifndef USE_SDL1 + DeactivateVirtualGamepad(); + FreeVirtualGamepadGFX(); +#endif +} + +bool StartGame(bool bNewGame, bool bSinglePlayer) +{ + gbSelectProvider = true; + ReturnToMainMenu = false; + + do { + gbLoadGame = false; + + if (!NetInit(bSinglePlayer)) { + gbRunGameResult = true; + break; + } + + // Save 2.8 MiB of RAM by freeing all main menu resources + // before starting the game. + UiDestroy(); + + gbSelectProvider = false; + + if (bNewGame || !gbValidSaveFile) { + InitLevels(); + InitQuests(); + InitPortals(); + InitDungMsgs(*MyPlayer); + DeltaSyncJunk(); + } + giNumberOfLevels = gbIsHellfire ? 25 : 17; + interface_mode uMsg = WM_DIABNEWGAME; + if (gbValidSaveFile && gbLoadGame) { + uMsg = WM_DIABLOADGAME; + } + RunGameLoop(uMsg); + NetClose(); + UnloadFonts(); + + // If the player left the game into the main menu, + // initialize main menu resources. + if (gbRunGameResult) + UiInitialize(); + if (ReturnToMainMenu) + return true; + } while (gbRunGameResult); + + SNetDestroy(); + return gbRunGameResult; +} + +void diablo_quit(int exitStatus) +{ + FreeGameMem(); + music_stop(); + DiabloDeinit(); + +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (SdlLogFile != nullptr) std::fclose(SdlLogFile); +#endif + + exit(exitStatus); +} + +#ifdef __UWP__ +void (*onInitialized)() = NULL; + +void setOnInitialized(void (*callback)()) +{ + onInitialized = callback; +} +#endif + +int DiabloMain(int argc, char **argv) +{ +#ifdef _DEBUG + SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); +#endif + + DiabloParseFlags(argc, argv); + InitKeymapActions(); + InitPadmapActions(); + + // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings + LoadCoreArchives(); + was_archives_init = true; + + // Read settings including translation next. This will use the presence of fonts.mpq and look for assets in devilutionx.mpq + LoadOptions(); + if (demo::IsRunning()) demo::OverrideOptions(); + + // Then look for a voice pack file based on the selected translation + LoadLanguageArchive(); + + ApplicationInit(); + LuaInitialize(); + if (!demo::IsRunning()) SaveOptions(); + + // Finally load game data + LoadGameArchives(); + + LoadTextData(); + + // Load dynamic data before we go into the menu as we need to initialise player characters in memory pretty early. + LoadPlayerDataFiles(); + + // TODO: We can probably load this much later (when the game is starting). + LoadSpellData(); + LoadMissileData(); + LoadMonsterData(); + LoadItemData(); + LoadObjectData(); + LoadQuestData(); + + DiabloInit(); +#ifdef __UWP__ + onInitialized(); +#endif + if (!demo::IsRunning()) SaveOptions(); + + DiabloSplash(); + mainmenu_loop(); + DiabloDeinit(); + + return 0; +} + +bool TryIconCurs() +{ + if (pcurs == CURSOR_RESURRECT) { + if (PlayerUnderCursor != nullptr) { + NetSendCmdParam1(true, CMD_RESURRECT, PlayerUnderCursor->getId()); + NewCursor(CURSOR_HAND); + return true; + } + + return false; + } + + if (pcurs == CURSOR_HEALOTHER) { + if (PlayerUnderCursor != nullptr) { + NetSendCmdParam1(true, CMD_HEALOTHER, PlayerUnderCursor->getId()); + NewCursor(CURSOR_HAND); + return true; + } + + return false; + } + + if (pcurs == CURSOR_TELEKINESIS) { + DoTelekinesis(); + return true; + } + + Player &myPlayer = *MyPlayer; + + if (pcurs == CURSOR_IDENTIFY) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) + CheckIdentify(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + item._iIdentified = true; + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_REPAIR) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) { + if (IsVisualStoreOpen) + VisualStoreRepairItem(pcursinvitem); + else + DoRepair(myPlayer, pcursinvitem); + } else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + RepairItem(item, myPlayer.getCharacterLevel()); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_RECHARGE) { + if (pcursinvitem != -1 && !IsInspectingPlayer()) + DoRecharge(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + RechargeItem(item, myPlayer); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_OIL) { + bool changeCursor = true; + if (pcursinvitem != -1 && !IsInspectingPlayer()) + changeCursor = DoOil(myPlayer, pcursinvitem); + else if (pcursstashitem != StashStruct::EmptyCell) { + Item &item = Stash.stashList[pcursstashitem]; + changeCursor = ApplyOilToItem(item, myPlayer); + } + if (changeCursor) + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_TELEPORT) { + const SpellID spellID = myPlayer.inventorySpell; + const SpellType spellType = SpellType::Scroll; + const int spellFrom = myPlayer.spellFrom; + if (IsWallSpell(spellID)) { + const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); + NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); + } else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) { + NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); + } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { + NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); + } else { + NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast(spellID), static_cast(spellType), spellFrom); + } + NewCursor(CURSOR_HAND); + return true; + } + + if (pcurs == CURSOR_DISARM && ObjectUnderCursor == nullptr) { + NewCursor(CURSOR_HAND); + return true; + } + + return false; +} + +void diablo_pause_game() +{ + if (!gbIsMultiplayer) { + if (PauseMode != 0) { + PauseMode = 0; + } else { + PauseMode = 2; + sound_stop(); + qtextflag = false; + LastPlayerAction = PlayerActionType::None; + } + + RedrawEverything(); + } +} + +bool GameWasAlreadyPaused = false; +bool MinimizePaused = false; + +bool diablo_is_focused() +{ +#ifndef USE_SDL1 + return SDL_GetKeyboardFocus() == ghMainWnd; +#else + Uint8 appState = SDL_GetAppState(); + return (appState & SDL_APPINPUTFOCUS) != 0; +#endif +} + +void diablo_focus_pause() +{ + if (!movie_playing && (gbIsMultiplayer || MinimizePaused)) { + return; + } + + GameWasAlreadyPaused = PauseMode != 0; + + if (!GameWasAlreadyPaused) { + PauseMode = 2; + sound_stop(); + LastPlayerAction = PlayerActionType::None; + } + + SVidMute(); + music_mute(); + + MinimizePaused = true; +} + +void diablo_focus_unpause() +{ + if (!GameWasAlreadyPaused) { + PauseMode = 0; + } + + SVidUnmute(); + music_unmute(); + + MinimizePaused = false; +} + +bool PressEscKey() +{ + bool rv = false; + + if (DoomFlag) { + doom_close(); + rv = true; + } + + if (HelpFlag) { + HelpFlag = false; + rv = true; + } + + if (ChatLogFlag) { + ChatLogFlag = false; + rv = true; + } + + if (qtextflag) { + qtextflag = false; + stream_stop(); + rv = true; + } + + if (IsPlayerInStore()) { + StoreESC(); + rv = true; + } + + if (IsDiabloMsgAvailable()) { + CancelCurrentDiabloMsg(); + rv = true; + } + + if (ChatFlag) { + ResetChat(); + rv = true; + } + + if (DropGoldFlag) { + control_drop_gold(SDLK_ESCAPE); + rv = true; + } + + if (IsWithdrawGoldOpen) { + WithdrawGoldKeyPress(SDLK_ESCAPE); + rv = true; + } + + if (SpellSelectFlag) { + SpellSelectFlag = false; + rv = true; + } + + if (IsLeftPanelOpen() || IsRightPanelOpen()) { + ClosePanels(); + rv = true; + } + + return rv; +} + +void DisableInputEventHandler(const SDL_Event &event, uint16_t /*modState*/) +{ + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; + return; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + if (sgbMouseDown != CLICK_NONE) + return; + switch (event.button.button) { + case SDL_BUTTON_LEFT: + sgbMouseDown = CLICK_LEFT; + return; + case SDL_BUTTON_RIGHT: + sgbMouseDown = CLICK_RIGHT; + return; + default: + return; + } + case SDL_EVENT_MOUSE_BUTTON_UP: + sgbMouseDown = CLICK_NONE; + return; + } + + MainWndProc(event); +} + +void LoadGameLevelStopMusic(_music_id neededTrack) +{ + if (neededTrack != sgnMusicTrack) + music_stop(); +} + +void LoadGameLevelStartMusic(_music_id neededTrack) +{ + if (sgnMusicTrack != neededTrack) + music_start(neededTrack); + + if (MinimizePaused) { + music_mute(); + } +} + +void LoadGameLevelResetCursor() +{ + if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { + NewCursor(CURSOR_HAND); + } +} + +void SetRndSeedForDungeonLevel() +{ + if (setlevel) { + // Maps are not randomly generated, but the monsters max hitpoints are. + // So we need to ensure that we have a stable seed when generating quest/set-maps. + // For this purpose we reuse the normal dungeon seeds. + SetRndSeed(DungeonSeeds[static_cast(setlvlnum)]); + } else { + SetRndSeed(DungeonSeeds[currlevel]); + } +} + +void LoadGameLevelFirstFlagEntry() +{ + CloseInventory(); + qtextflag = false; + if (!HeadlessMode) { + InitInv(); + ClearUniqueItemFlags(); + InitQuestText(); + InitInfoBoxGfx(); + InitHelp(); + } + InitStores(); + InitAutomapOnce(); +} + +void LoadGameLevelStores() +{ + if (leveltype == DTYPE_TOWN) { + SetupTownStores(); + } else { + FreeStoreMem(); + } +} + +void LoadGameLevelStash() +{ + const bool isHellfireSaveGame = gbIsHellfireSaveGame; + + gbIsHellfireSaveGame = gbIsHellfire; + LoadStash(); + gbIsHellfireSaveGame = isHellfireSaveGame; +} + +tl::expected LoadGameLevelDungeon(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pLvlVisited[currlevel] || gbIsMultiplayer) { + HoldThemeRooms(); + [[maybe_unused]] const uint32_t mid1Seed = GetLCGEngineState(); + InitGolems(); + InitObjects(); + [[maybe_unused]] const uint32_t mid2Seed = GetLCGEngineState(); + + IncProgress(); + + RETURN_IF_ERROR(InitMonsters()); + InitItems(); + CreateThemeRooms(); + + IncProgress(); + + [[maybe_unused]] const uint32_t mid3Seed = GetLCGEngineState(); + InitMissiles(); + InitCorpses(); +#ifdef _DEBUG + SetDebugLevelSeedInfos(mid1Seed, mid2Seed, mid3Seed, GetLCGEngineState()); +#endif + SavePreLighting(); + + IncProgress(); + + if (gbIsMultiplayer) + DeltaLoadLevel(); + } else { + HoldThemeRooms(); + InitGolems(); + RETURN_IF_ERROR(InitMonsters()); + InitMissiles(); + InitCorpses(); + + IncProgress(); + + RETURN_IF_ERROR(LoadLevel()); + + IncProgress(); + } + return {}; +} + +void LoadGameLevelSyncPlayerEntry(lvl_entry lvldir) +{ + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel() && (!player._pLvlChanging || &player == MyPlayer)) { + if (player._pHitPoints > 0) { + if (lvldir != ENTRY_LOAD) + SyncInitPlrPos(player); + } else { + dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; + } + } + } +} + +void LoadGameLevelLightVision() +{ + if (leveltype != DTYPE_TOWN) { + memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light + ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh + ProcessLightList(); + ProcessVisionList(); + } +} + +void LoadGameLevelReturn() +{ + ViewPosition = GetMapReturnPosition(); + if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) + Quests[Q_BETRAYER]._qvar2 = 2; +} + +void LoadGameLevelInitPlayers(bool firstflag, lvl_entry lvldir) +{ + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel()) { + InitPlayerGFX(player); + if (lvldir != ENTRY_LOAD) + InitPlayer(player, firstflag); + } + } +} + +void LoadGameLevelSetVisited() +{ + bool visited = false; + for (const Player &player : Players) { + if (player.plractive) + visited = visited || player._pLvlVisited[currlevel]; + } +} + +tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) + for (int j = 0; j < MAXDUNY; j++) { + dFlags[i][j] |= DungeonFlag::Lit; + } + } + + InitTowners(); + InitStash(); + InitVisualStore(); + InitItems(); + InitMissiles(); + + IncProgress(); + + if (!firstflag && lvldir != ENTRY_LOAD && lvldir != ENTRY_TOWNSWITCH && myPlayer._pLvlVisited[currlevel] && !gbIsMultiplayer) + RETURN_IF_ERROR(LoadLevel()); + if (gbIsMultiplayer) + DeltaLoadLevel(); + + if (lvldir == ENTRY_TOWNSWITCH) + InitTownTriggers(); + + IncProgress(); + + for (int x = 0; x < DMAXX; x++) + for (int y = 0; y < DMAXY; y++) + UpdateAutomapExplorer({ x, y }, MAP_EXP_SELF); + return {}; +} + +tl::expected LoadGameLevelSetLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + LoadSetMap(); + IncProgress(); + RETURN_IF_ERROR(GetLevelMTypes()); + IncProgress(); + InitGolems(); + RETURN_IF_ERROR(InitMonsters()); + IncProgress(); + if (!HeadlessMode) { +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + RETURN_IF_ERROR(InitMissileGFX()); + IncProgress(); + } + InitCorpses(); + IncProgress(); + + if (lvldir == ENTRY_WARPLVL) + GetPortalLvlPos(); + IncProgress(); + + for (Player &player : Players) { + if (player.plractive && player.isOnActiveLevel()) { + InitPlayerGFX(player); + if (lvldir != ENTRY_LOAD) + InitPlayer(player, firstflag); + } + } + IncProgress(); + InitMultiView(); + IncProgress(); + + if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pSLvlVisited[setlvlnum] || gbIsMultiplayer) { + InitItems(); + SavePreLighting(); + } else { + RETURN_IF_ERROR(LoadLevel()); + } + if (gbIsMultiplayer) { + DeltaLoadLevel(); + if (!UseMultiplayerQuests()) + ResyncQuests(); + } + + PlayDungMsgs(); + InitMissiles(); + IncProgress(); + return {}; +} + +tl::expected LoadGameLevelStandardLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) +{ + CreateLevel(lvldir); + + IncProgress(); + + SetRndSeedForDungeonLevel(); + + if (leveltype != DTYPE_TOWN) { + RETURN_IF_ERROR(GetLevelMTypes()); + InitThemes(); + if (!HeadlessMode) + RETURN_IF_ERROR(LoadAllGFX()); + } else if (!HeadlessMode) { + IncProgress(); + +#if !defined(USE_SDL1) && !defined(__vita__) + InitVirtualGamepadGFX(); +#endif + + IncProgress(); + + RETURN_IF_ERROR(InitMissileGFX()); + + IncProgress(); + IncProgress(); + } + + IncProgress(); + + if (lvldir == ENTRY_RTNLVL) { + LoadGameLevelReturn(); + } + + if (lvldir == ENTRY_WARPLVL) + GetPortalLvlPos(); + + IncProgress(); + + LoadGameLevelInitPlayers(firstflag, lvldir); + InitMultiView(); + + IncProgress(); + + LoadGameLevelSetVisited(); + + SetRndSeedForDungeonLevel(); + + if (leveltype == DTYPE_TOWN) { + LoadGameLevelTown(firstflag, lvldir, myPlayer); + } else { + LoadGameLevelDungeon(firstflag, lvldir, myPlayer); + } + + PlayDungMsgs(); + + if (UseMultiplayerQuests()) + ResyncMPQuests(); + else + ResyncQuests(); + return {}; +} + +void LoadGameLevelCrypt() +{ + if (CornerStone.isAvailable()) { + CornerstoneLoad(CornerStone.position); + } + if (Quests[Q_NAKRUL]._qactive == QUEST_DONE && currlevel == 24) { + SyncNakrulRoom(); + } +} + +void LoadGameLevelCalculateCursor() +{ + // Recalculate mouse selection of entities after level change/load + LastPlayerAction = PlayerActionType::None; + sgbMouseDown = CLICK_NONE; + ResetItemlabelHighlighted(); // level changed => item changed + pcursmonst = -1; // ensure pcurstemp is set to a valid value + CheckCursMove(); +} + +tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) +{ + const _music_id neededTrack = GetLevelMusic(leveltype); + + ClearFloatingNumbers(); + LoadGameLevelStopMusic(neededTrack); + LoadGameLevelResetCursor(); + SetRndSeedForDungeonLevel(); + NaKrulTomeSequence = 0; + + IncProgress(); + + RETURN_IF_ERROR(LoadTrns()); + MakeLightTable(); + RETURN_IF_ERROR(LoadLevelSOLData()); + + IncProgress(); + + RETURN_IF_ERROR(LoadLvlGFX()); + SetDungeonMicros(pDungeonCels, MicroTileLen); + ClearClxDrawCache(); + + IncProgress(); + + if (firstflag) { + LoadGameLevelFirstFlagEntry(); + } + + SetRndSeedForDungeonLevel(); + + LoadGameLevelStores(); + + if (firstflag || lvldir == ENTRY_LOAD) { + LoadGameLevelStash(); + } + + IncProgress(); + + InitAutomap(); + + if (leveltype != DTYPE_TOWN && lvldir != ENTRY_LOAD) { + InitLighting(); + } + + InitLevelMonsters(); + + IncProgress(); + + const Player &myPlayer = *MyPlayer; + + if (setlevel) { + RETURN_IF_ERROR(LoadGameLevelSetLevel(firstflag, lvldir, myPlayer)); + } else { + RETURN_IF_ERROR(LoadGameLevelStandardLevel(firstflag, lvldir, myPlayer)); + } + + SyncPortals(); + LoadGameLevelSyncPlayerEntry(lvldir); + + IncProgress(); + IncProgress(); + + if (firstflag) { + RETURN_IF_ERROR(InitMainPanel()); + } + + IncProgress(); + + UpdateMonsterLights(); + UnstuckChargers(); + + LoadGameLevelLightVision(); + + if (leveltype == DTYPE_CRYPT) { + LoadGameLevelCrypt(); + } + +#ifndef USE_SDL1 + ActivateVirtualGamepad(); +#endif + LoadGameLevelStartMusic(neededTrack); + + CompleteProgress(); + + LoadGameLevelCalculateCursor(); + return {}; +} + +bool game_loop(bool bStartup) +{ + const uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; + + for (unsigned i = 0; i < wait; i++) { + if (!multi_handle_delta()) { + TimeoutCursor(true); + return false; + } + TimeoutCursor(false); + GameLogic(); + ClearLastSentPlayerCmd(); + + if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) + break; + } + return true; +} + +void diablo_color_cyc_logic() +{ + if (!*GetOptions().Graphics.colorCycling) + return; + + if (PauseMode != 0) + return; + + if (leveltype == DTYPE_CAVES) { + if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { + UpdatePWaterPalette(); + } else { + palette_update_caves(); + } + } else if (leveltype == DTYPE_HELL) { + lighting_color_cycling(); + } else if (leveltype == DTYPE_NEST) { + palette_update_hive(); + } else if (leveltype == DTYPE_CRYPT) { + palette_update_crypt(); + } +} + +bool IsDiabloAlive(bool playSFX) +{ + if (Quests[Q_DIABLO]._qactive == QUEST_DONE && !gbIsMultiplayer) { + if (playSFX) + PlaySFX(SfxID::DiabloDeath); + return false; + } + + return true; +} + +void PrintScreen(SDL_Keycode vkey) +{ + ReleaseKey(vkey); +} + +} // namespace devilution diff --git a/Source/levels/town_data.cpp b/Source/levels/town_data.cpp index d4950dc58e2..28ef7e3d8d6 100644 --- a/Source/levels/town_data.cpp +++ b/Source/levels/town_data.cpp @@ -1,105 +1,114 @@ -#include "levels/town_data.h" - -#include "utils/log.hpp" - -namespace devilution { - -namespace { - -TownRegistry g_townRegistry; - -} // namespace - -std::string DestinationTownID; - -TownRegistry &GetTownRegistry() -{ - return g_townRegistry; -} - -void TownRegistry::RegisterTown(const std::string &id, const TownConfig &config) -{ - towns[id] = config; - LogInfo("Registered town: {}", id); -} - -TownConfig &TownRegistry::GetTown(const std::string &id) -{ - return towns.at(id); -} - -bool TownRegistry::HasTown(const std::string &id) const -{ - return towns.count(id) > 0; -} - -void TownRegistry::SetCurrentTown(const std::string &id) -{ - currentTownID = id; -} - -std::string TownRegistry::GetCurrentTown() const -{ - return currentTownID; -} - -std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const -{ - for (const auto &[id, config] : towns) { - if (config.saveId == saveId) { - return id; - } - } - return "tristram"; -} - -Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const -{ - // For ENTRY_TWARPUP, match both entry type and warpFromLevel - if (entry == ENTRY_TWARPUP) { - for (const auto &ep : entries) { - if (ep.entryType == entry && ep.warpFromLevel == warpFrom) { - return ep.viewPosition; - } - } - } - - // For other entry types, just match the type - for (const auto &ep : entries) { - if (ep.entryType == entry) { - return ep.viewPosition; - } - } - - // Default fallback - return { 75, 68 }; -} - -void InitializeTristram() -{ - TownConfig tristram; - tristram.name = "Tristram"; - tristram.saveId = 0; - tristram.dminPosition = { 10, 10 }; - tristram.dmaxPosition = { 84, 84 }; - tristram.sectors = { - { "levels\\towndata\\sector1s.dun", 46, 46 }, - { "levels\\towndata\\sector2s.dun", 46, 0 }, - { "levels\\towndata\\sector3s.dun", 0, 46 }, - { "levels\\towndata\\sector4s.dun", 0, 0 }, - }; - tristram.entries = { - { ENTRY_MAIN, { 75, 68 }, -1 }, - { ENTRY_PREV, { 25, 31 }, -1 }, - { ENTRY_TWARPUP, { 49, 22 }, 5 }, - { ENTRY_TWARPUP, { 18, 69 }, 9 }, - { ENTRY_TWARPUP, { 41, 81 }, 13 }, - { ENTRY_TWARPUP, { 36, 25 }, 21 }, - { ENTRY_TWARPUP, { 79, 62 }, 17 }, - { ENTRY_TOWNSWITCH, { 75, 68 }, -1 }, - }; - GetTownRegistry().RegisterTown("tristram", tristram); - GetTownRegistry().SetCurrentTown("tristram"); -} - -} // namespace devilution +#include "levels/town_data.h" + +#include "utils/log.hpp" + +namespace devilution { + +namespace { + +TownRegistry g_townRegistry; + +} // namespace + +std::string DestinationTownID; + +TownRegistry &GetTownRegistry() +{ + return g_townRegistry; +} + +void TownRegistry::RegisterTown(const std::string &id, const TownConfig &config) +{ + towns[id] = config; + LogInfo("Registered town: {}", id); +} + +TownConfig &TownRegistry::GetTown(const std::string &id) +{ + return towns.at(id); +} + +bool TownRegistry::HasTown(const std::string &id) const +{ + return towns.count(id) > 0; +} + +void TownRegistry::SetCurrentTown(const std::string &id) +{ + currentTownID = id; +} + +std::string TownRegistry::GetCurrentTown() const +{ + return currentTownID; +} + +std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const +{ + for (const auto &[id, config] : towns) { + if (config.saveId == saveId) { + return id; + } + } + return "tristram"; +} + +Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const +{ + // For ENTRY_TWARPUP, match both entry type and warpFromLevel + if (entry == ENTRY_TWARPUP) { + for (const auto &ep : entries) { + if (ep.entryType == entry && ep.warpFromLevel == warpFrom) { + return ep.viewPosition; + } + } + } + + // For other entry types, just match the type + for (const auto &ep : entries) { + if (ep.entryType == entry) { + return ep.viewPosition; + } + } + + // Default fallback + return { 75, 68 }; +} + +void InitializeTristram() +{ + TownConfig tristram; + tristram.name = "Tristram"; + tristram.saveId = 0; + tristram.dminPosition = { 10, 10 }; + tristram.dmaxPosition = { 84, 84 }; + tristram.sectors = { + { "levels\\towndata\\sector1s.dun", 46, 46 }, + { "levels\\towndata\\sector2s.dun", 46, 0 }, + { "levels\\towndata\\sector3s.dun", 0, 46 }, + { "levels\\towndata\\sector4s.dun", 0, 0 }, + }; + tristram.entries = { + { ENTRY_MAIN, { 75, 68 }, -1 }, + { ENTRY_PREV, { 25, 31 }, -1 }, + { ENTRY_TWARPUP, { 49, 22 }, 5 }, + { ENTRY_TWARPUP, { 18, 69 }, 9 }, + { ENTRY_TWARPUP, { 41, 81 }, 13 }, + { ENTRY_TWARPUP, { 36, 25 }, 21 }, + { ENTRY_TWARPUP, { 79, 62 }, 17 }, + { ENTRY_TOWNSWITCH, { 75, 68 }, -1 }, + }; + // Matches legacy InitTownTriggers (cathedral + gated town warps) + tristram.triggers = { + { { 25, 29 }, WM_DIABNEXTLVL, 0, std::nullopt }, + { { 49, 21 }, WM_DIABTOWNWARP, 5, DTYPE_CATACOMBS }, + { { 17, 69 }, WM_DIABTOWNWARP, 9, DTYPE_CAVES }, + { { 41, 80 }, WM_DIABTOWNWARP, 13, DTYPE_HELL }, + { { 80, 62 }, WM_DIABTOWNWARP, 17, DTYPE_NEST }, + { { 36, 24 }, WM_DIABTOWNWARP, 21, DTYPE_CRYPT }, + }; + GetTownRegistry().RegisterTown("tristram", tristram); + GetTownRegistry().SetCurrentTown("tristram"); +} + +} // namespace devilution diff --git a/Source/levels/town_data.h b/Source/levels/town_data.h index 47ca05eb231..432b32a694f 100644 --- a/Source/levels/town_data.h +++ b/Source/levels/town_data.h @@ -1,82 +1,97 @@ -#pragma once - -#include -#include -#include - -#include "engine/point.hpp" -#include "levels/gendung_defs.hpp" - -namespace devilution { - -/** - * @brief Represents a town sector (map piece) - */ -struct TownSector { - std::string filePath; - int x, y; -}; - -/** - * @brief Represents a town entry/spawn point - */ -struct TownEntryPoint { - lvl_entry entryType; - Point viewPosition; - int warpFromLevel; // Source level for ENTRY_TWARPUP (-1 for any) -}; - -/** - * @brief Position override for a towner NPC - */ -struct TownerPositionOverride { - std::string shortName; // e.g. "griswold", "farnham" - Point position; -}; - -/** - * @brief Complete configuration for a town - */ -struct TownConfig { - std::string name; - uint8_t saveId = 0; - Point dminPosition = { 10, 10 }; - Point dmaxPosition = { 84, 84 }; - std::vector sectors; - std::string solFile; - std::vector entries; - std::vector townerOverrides; - - /** - * @brief Gets the spawn point for a given entry type and warp source - */ - Point GetEntryPoint(lvl_entry entry, int warpFrom = -1) const; -}; - -/** - * @brief Registry for managing multiple town configurations - */ -class TownRegistry { -private: - std::unordered_map towns; - std::string currentTownID; - -public: - void RegisterTown(const std::string &id, const TownConfig &config); - TownConfig &GetTown(const std::string &id); - bool HasTown(const std::string &id) const; - void SetCurrentTown(const std::string &id); - std::string GetCurrentTown() const; - const std::unordered_map &GetTowns() const { return towns; } - - /** @brief Finds town string ID by its saveId. Returns "tristram" if not found. */ - std::string GetTownBySaveId(uint8_t saveId) const; -}; - -TownRegistry &GetTownRegistry(); - -extern std::string DestinationTownID; - -void InitializeTristram(); - -} // namespace devilution +#pragma once + +#include +#include +#include +#include + +#include "engine/point.hpp" +#include "interfac.h" +#include "levels/gendung_defs.hpp" + +namespace devilution { + +/** + * @brief Represents a town sector (map piece) + */ +struct TownSector { + std::string filePath; + int x, y; +}; + +/** + * @brief Represents a town entry/spawn point + */ +struct TownEntryPoint { + lvl_entry entryType; + Point viewPosition; + int warpFromLevel; // Source level for ENTRY_TWARPUP (-1 for any) +}; + +/** + * @brief Position override for a towner NPC + */ +struct TownerPositionOverride { + std::string shortName; // e.g. "griswold", "farnham" + Point position; +}; + +/** + * @brief Dungeon entrance / town warp trigger (see InitTownTriggers) + */ +struct TownTrigger { + Point position; + interface_mode message; + /** For WM_DIABTOWNWARP; unused for other message types */ + int targetLevel = 0; + /** If set, trigger is only active when IsWarpOpen(*warpGate) */ + std::optional warpGate; +}; + +/** + * @brief Complete configuration for a town + */ +struct TownConfig { + std::string name; + uint8_t saveId = 0; + Point dminPosition = { 10, 10 }; + Point dmaxPosition = { 84, 84 }; + std::vector sectors; + std::string solFile; + std::vector entries; + std::vector triggers; + std::vector townerOverrides; + + /** + * @brief Gets the spawn point for a given entry type and warp source + */ + Point GetEntryPoint(lvl_entry entry, int warpFrom = -1) const; +}; + +/** + * @brief Registry for managing multiple town configurations + */ +class TownRegistry { +private: + std::unordered_map towns; + std::string currentTownID; + +public: + void RegisterTown(const std::string &id, const TownConfig &config); + TownConfig &GetTown(const std::string &id); + bool HasTown(const std::string &id) const; + void SetCurrentTown(const std::string &id); + std::string GetCurrentTown() const; + const std::unordered_map &GetTowns() const { return towns; } + + /** @brief Finds town string ID by its saveId. Returns "tristram" if not found. */ + std::string GetTownBySaveId(uint8_t saveId) const; +}; + +TownRegistry &GetTownRegistry(); + +extern std::string DestinationTownID; + +void InitializeTristram(); + +} // namespace devilution diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index f2705f12400..755746cf56a 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -1,947 +1,932 @@ -/** - * @file trigs.cpp - * - * Implementation of functionality for triggering events when the player enters an area. - */ -#include "levels/trigs.h" - -#include -#include - -#include - -#include "control/control.hpp" -#include "controls/control_mode.hpp" -#include "controls/plrctrls.h" -#include "cursor.h" -#include "diablo_msg.hpp" -#include "game_mode.hpp" -#include "multi.h" -#include "utils/algorithm/container.hpp" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/utf8.hpp" - -namespace devilution { - -bool trigflag; -int numtrigs; -TriggerStruct trigs[MAXTRIGGERS]; -int TWarpFrom; - -namespace { -/** Specifies the dungeon piece IDs which constitute stairways leading down to the cathedral from town. */ -const uint16_t TownDownList[] = { 715, 714, 718, 719, 720, 722, 723, 724, 725, 726 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down to the catacombs from town. */ -const uint16_t TownWarp1List[] = { 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1180, 1182, 1184 }; -const uint16_t TownCryptList[] = { 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337 }; -const uint16_t TownHiveList[] = { 1306, 1307, 1308, 1309 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the cathedral. */ -const uint16_t L1UpList[] = { 126, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the cathedral. */ -const uint16_t L1DownList[] = { 105, 106, 107, 108, 109, 111, 113, 114, 117 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the catacombs. */ -const uint16_t L2UpList[] = { 265, 266 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the catacombs. */ -const uint16_t L2DownList[] = { 268, 269, 270, 271 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the catacombs. */ -const uint16_t L2TWarpUpList[] = { 557, 558 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from the caves. */ -const uint16_t L3UpList[] = { 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from the caves. */ -const uint16_t L3DownList[] = { 161, 162, 163, 164, 165, 166, 167, 168 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the caves. */ -const uint16_t L3TWarpUpList[] = { 181, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up from hell. */ -const uint16_t L4UpList[] = { 81, 82, 89 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down from hell. */ -const uint16_t L4DownList[] = { 119, 129, 130, 131, 132 }; -/** Specifies the dungeon piece IDs which constitute stairways leading up to town from hell. */ -const uint16_t L4TWarpUpList[] = { 420, 421, 428 }; -/** Specifies the dungeon piece IDs which constitute stairways leading down to Diablo from hell. */ -const uint16_t L4PentaList[] = { 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383 }; -const uint16_t L5TWarpUpList[] = { 171, 172, 173, 174, 175, 176, 177, 178, 183 }; -const uint16_t L5UpList[] = { 148, 149, 150, 151, 152, 153, 154, 156, 157, 158 }; -const uint16_t L5DownList[] = { 124, 125, 128, 130, 131, 134, 135, 139, 141 }; -const uint16_t L6TWarpUpList[] = { 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }; -const uint16_t L6UpList[] = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77 }; -const uint16_t L6DownList[] = { 56, 57, 58, 59, 60, 61, 62, 63 }; -} // namespace - -void InitNoTriggers() -{ - numtrigs = 0; - trigflag = false; -} - -bool IsWarpOpen(dungeon_type type) -{ - if (gbIsSpawn) - return false; - - if (gbIsMultiplayer && type != DTYPE_NEST) // Opening the nest is part of in town quest - return true; - - const Player &myPlayer = *MyPlayer; - - if (type == DTYPE_CATACOMBS && (myPlayer.pTownWarps & 1) != 0) - return true; - if (type == DTYPE_CAVES && (myPlayer.pTownWarps & 2) != 0) - return true; - if (type == DTYPE_HELL && (myPlayer.pTownWarps & 4) != 0) - return true; - - if (gbIsHellfire) { - if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) - return true; - if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) - return true; - if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) - return true; - if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) - return true; - if (type == DTYPE_CRYPT && Quests[Q_GRAVE]._qactive == QUEST_DONE) - return true; - } - - return false; -} - -void InitTownTriggers() -{ - numtrigs = 0; - - // Cathedral - trigs[numtrigs].position = { 25, 29 }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - - if (IsWarpOpen(DTYPE_CATACOMBS)) { - trigs[numtrigs].position = { 49, 21 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 5; - numtrigs++; - } - if (IsWarpOpen(DTYPE_CAVES)) { - trigs[numtrigs].position = { 17, 69 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 9; - numtrigs++; - } - if (IsWarpOpen(DTYPE_HELL)) { - trigs[numtrigs].position = { 41, 80 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 13; - numtrigs++; - } - if (IsWarpOpen(DTYPE_NEST)) { - trigs[numtrigs].position = { 80, 62 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 17; - numtrigs++; - } - if (IsWarpOpen(DTYPE_CRYPT)) { - trigs[numtrigs].position = { 36, 24 }; - trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; - trigs[numtrigs]._tlvl = 21; - numtrigs++; - } - - trigflag = false; -} - -void InitL1Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 128) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - if (dPiece[i][j] == 114) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL2Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 266 && (!Quests[Q_SCHAMB].IsAvailable() || i != Quests[Q_SCHAMB].position.x || j != Quests[Q_SCHAMB].position.y)) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 558) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - - if (dPiece[i][j] == 270) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL3Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 170) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 167) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - - if (dPiece[i][j] == 548) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitL4Triggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 82) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 421) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - - if (dPiece[i][j] == 119) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 369 && Quests[Q_BETRAYER]._qactive == QUEST_DONE) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitHiveTriggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 65) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - - if (dPiece[i][j] == 62) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - - if (dPiece[i][j] == 79) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitCryptTriggers() -{ - numtrigs = 0; - for (WorldTileCoord j = 0; j < MAXDUNY; j++) { - for (WorldTileCoord i = 0; i < MAXDUNX; i++) { - if (dPiece[i][j] == 183) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABTWARPUP; - trigs[numtrigs]._tlvl = 0; - numtrigs++; - } - if (dPiece[i][j] == 157) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABPREVLVL; - numtrigs++; - } - if (dPiece[i][j] == 125) { - trigs[numtrigs].position = { i, j }; - trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; - numtrigs++; - } - } - } - trigflag = false; -} - -void InitSKingTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 82, 42 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitSChambTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 70, 39 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitPWaterTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 30, 83 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -void InitVPTriggers() -{ - trigflag = false; - numtrigs = 1; - trigs[0].position = { 35, 32 }; - trigs[0]._tmsg = WM_DIABRTNLVL; -} - -bool ForceTownTrig() -{ - for (const uint16_t tileId : TownDownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to dungeon"); - cursPosition = { 25, 29 }; - return true; - } - } - - if (IsWarpOpen(DTYPE_CATACOMBS)) { - for (const uint16_t tileId : TownWarp1List) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to catacombs"); - cursPosition = { 49, 21 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_CAVES)) { - for (uint16_t i = 1198; i <= 1219; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == i) { - InfoString = _("Down to caves"); - cursPosition = { 17, 69 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_HELL)) { - for (uint16_t i = 1239; i <= 1254; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == i) { - InfoString = _("Down to hell"); - cursPosition = { 41, 80 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_NEST)) { - for (const uint16_t tileId : TownHiveList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Hive"); - cursPosition = { 80, 62 }; - return true; - } - } - } - - if (IsWarpOpen(DTYPE_CRYPT)) { - for (const uint16_t tileId : TownCryptList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Crypt"); - cursPosition = { 36, 24 }; - return true; - } - } - } - - return false; -} - -bool ForceL1Trig() -{ - for (const uint16_t tileId : L1UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - if (currlevel > 1) - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - else - InfoString = _("Up to town"); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - for (const uint16_t tileId : L1DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - return false; -} - -bool ForceL2Trig() -{ - for (const uint16_t tileId : L2UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - - for (const uint16_t tileId : L2DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 5) { - for (const uint16_t tileId : L2TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceL3Trig() -{ - for (const uint16_t tileId : L3UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - for (const uint16_t tileId : L3DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId - || dPiece[cursPosition.x + 1][cursPosition.y] == tileId - || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 9) { - for (const uint16_t tileId : L3TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceL4Trig() -{ - for (const uint16_t tileId : L4UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - for (const uint16_t tileId : L4DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 13) { - for (const uint16_t tileId : L4TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - if (currlevel == 15) { - for (const uint16_t tileId : L4PentaList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = _("Down to Diablo"); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - - return false; -} - -bool ForceHiveTrig() -{ - for (const uint16_t tileId : L6UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to Nest level {:d}")), currlevel - 17); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - for (const uint16_t tileId : L6DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId - || dPiece[cursPosition.x + 1][cursPosition.y] == tileId - || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel - 15); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - - if (currlevel == 17) { - for (const uint16_t tileId : L6TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -bool ForceCryptTrig() -{ - for (const uint16_t tileId : L5UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Up to Crypt level {:d}")), currlevel - 21); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABPREVLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - if (dPiece[cursPosition.x][cursPosition.y] == 316) { - InfoString = _("Cornerstone of the World"); - return true; - } - for (const uint16_t tileId : L5DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Down to Crypt level {:d}")), currlevel - 19); - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABNEXTLVL) { - cursPosition = trigs[j].position; - return true; - } - } - } - } - if (currlevel == 21) { - for (const uint16_t tileId : L5TWarpUpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - for (int j = 0; j < numtrigs; j++) { - if (trigs[j]._tmsg == WM_DIABTWARPUP) { - const int dx = std::abs(trigs[j].position.x - cursPosition.x); - const int dy = std::abs(trigs[j].position.y - cursPosition.y); - if (dx < 4 && dy < 4) { - InfoString = _("Up to town"); - cursPosition = trigs[j].position; - return true; - } - } - } - } - } - } - - return false; -} - -void Freeupstairs() -{ - for (int i = 0; i < numtrigs; i++) { - const int tx = trigs[i].position.x; - const int ty = trigs[i].position.y; - - for (int yy = -2; yy <= 2; yy++) { - for (int xx = -2; xx <= 2; xx++) { - dFlags[tx + xx][ty + yy] |= DungeonFlag::Populated; - } - } - } -} - -bool ForceSKingTrig() -{ - for (const uint16_t tileId : L1UpList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SKELKING]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForceSChambTrig() -{ - for (const uint16_t tileId : L2DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SCHAMB]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForcePWaterTrig() -{ - for (const uint16_t tileId : L3DownList) { - if (dPiece[cursPosition.x][cursPosition.y] == tileId) { - InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_PWATER]._qlevel); - cursPosition = trigs[0].position; - - return true; - } - } - - return false; -} - -bool ForceArenaTrig() -{ - const uint16_t *checkList; - size_t len; - switch (setlvltype) { - case DTYPE_TOWN: - checkList = TownWarp1List; - len = sizeof(TownWarp1List) / sizeof(TownWarp1List[0]); - break; - case DTYPE_CATHEDRAL: - checkList = L1UpList; - len = sizeof(L1UpList) / sizeof(L1UpList[0]); - break; - case DTYPE_CATACOMBS: - checkList = L2TWarpUpList; - len = sizeof(L2TWarpUpList) / sizeof(L2TWarpUpList[0]); - break; - case DTYPE_CAVES: - checkList = L3TWarpUpList; - len = sizeof(L3TWarpUpList) / sizeof(L3TWarpUpList[0]); - break; - case DTYPE_HELL: - checkList = L4TWarpUpList; - len = sizeof(L4TWarpUpList) / sizeof(L4TWarpUpList[0]); - break; - case DTYPE_NEST: - checkList = L5TWarpUpList; - len = sizeof(L5TWarpUpList) / sizeof(L5TWarpUpList[0]); - break; - case DTYPE_CRYPT: - checkList = L6TWarpUpList; - len = sizeof(L6TWarpUpList) / sizeof(L6TWarpUpList[0]); - break; - default: - return false; - } - for (size_t i = 0; i < len; ++i) { - if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { - InfoString = _("Up to town"); - cursPosition = trigs[0].position; - return true; - } - } - - return false; -} - -void CheckTrigForce() -{ - trigflag = false; - - if (ControlMode == ControlTypes::KeyboardAndMouse && GetMainPanel().contains(MousePosition)) { - return; - } - - if (!setlevel) { - switch (leveltype) { - case DTYPE_TOWN: - trigflag = ForceTownTrig(); - break; - case DTYPE_CATHEDRAL: - trigflag = ForceL1Trig(); - break; - case DTYPE_CATACOMBS: - trigflag = ForceL2Trig(); - break; - case DTYPE_CAVES: - trigflag = ForceL3Trig(); - break; - case DTYPE_HELL: - trigflag = ForceL4Trig(); - break; - case DTYPE_NEST: - trigflag = ForceHiveTrig(); - break; - case DTYPE_CRYPT: - trigflag = ForceCryptTrig(); - break; - default: - break; - } - if (leveltype != DTYPE_TOWN && !trigflag) { - trigflag = ForceQuests(); - } - } else { - switch (setlvlnum) { - case SL_SKELKING: - trigflag = ForceSKingTrig(); - break; - case SL_BONECHAMB: - trigflag = ForceSChambTrig(); - break; - case SL_POISONWATER: - trigflag = ForcePWaterTrig(); - break; - default: - if (IsArenaLevel(setlvlnum)) - trigflag = ForceArenaTrig(); - break; - } - } -} - -void CheckTriggers() -{ - Player &myPlayer = *MyPlayer; - - if (myPlayer._pmode != PM_STAND) - return; - - for (int i = 0; i < numtrigs; i++) { - if (myPlayer.position.tile != trigs[i].position) { - continue; - } - - switch (trigs[i]._tmsg) { - case WM_DIABNEXTLVL: - if (gbIsSpawn && currlevel >= 2) { - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, { myPlayer.position.tile.x, myPlayer.position.tile.y + 1 }); - myPlayer.Say(HeroSpeech::NotAChance); - InitDiabloMsg(EMSG_NOT_IN_SHAREWARE); - } else { - StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel + 1); - } - break; - case WM_DIABPREVLVL: - StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel - 1); - break; - case WM_DIABRTNLVL: - StartNewLvl(myPlayer, trigs[i]._tmsg, GetMapReturnLevel()); - break; - case WM_DIABTOWNWARP: - if (gbIsMultiplayer) { - bool abort = false; - diablo_message abortflag; - - auto position = myPlayer.position.tile; - if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { - abort = true; - position.y += 1; - abortflag = EMSG_REQUIRES_LVL_8; - } - - if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { - abort = true; - position.x += 1; - abortflag = EMSG_REQUIRES_LVL_13; - } - - if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { - abort = true; - position.y += 1; - abortflag = EMSG_REQUIRES_LVL_17; - } - - if (abort) { - myPlayer.Say(HeroSpeech::ICantGetThereFromHere); - - InitDiabloMsg(abortflag); - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, position); - return; - } - } - - StartNewLvl(myPlayer, trigs[i]._tmsg, trigs[i]._tlvl); - break; - case WM_DIABTWARPUP: - TWarpFrom = currlevel; - StartNewLvl(myPlayer, trigs[i]._tmsg, 0); - break; - default: - app_fatal("Unknown trigger msg"); - } - } -} - -bool EntranceBoundaryContains(Point entrance, Point position) -{ - constexpr Displacement entranceOffsets[7] = { { 0, 0 }, { -1, 0 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -1, -2 }, { -2, -2 } }; - - return c_any_of( - entranceOffsets, - [=](Displacement offset) { return entrance + offset == position; }); -} - -} // namespace devilution +/** + * @file trigs.cpp + * + * Implementation of functionality for triggering events when the player enters an area. + */ +#include "levels/trigs.h" + +#include +#include + +#include + +#include "control/control.hpp" +#include "controls/control_mode.hpp" +#include "controls/plrctrls.h" +#include "cursor.h" +#include "diablo_msg.hpp" +#include "game_mode.hpp" +#include "levels/town_data.h" +#include "multi.h" +#include "utils/algorithm/container.hpp" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/log.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +bool trigflag; +int numtrigs; +TriggerStruct trigs[MAXTRIGGERS]; +int TWarpFrom; + +namespace { +/** Specifies the dungeon piece IDs which constitute stairways leading down to the cathedral from town. */ +const uint16_t TownDownList[] = { 715, 714, 718, 719, 720, 722, 723, 724, 725, 726 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down to the catacombs from town. */ +const uint16_t TownWarp1List[] = { 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1180, 1182, 1184 }; +const uint16_t TownCryptList[] = { 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337 }; +const uint16_t TownHiveList[] = { 1306, 1307, 1308, 1309 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the cathedral. */ +const uint16_t L1UpList[] = { 126, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the cathedral. */ +const uint16_t L1DownList[] = { 105, 106, 107, 108, 109, 111, 113, 114, 117 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the catacombs. */ +const uint16_t L2UpList[] = { 265, 266 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the catacombs. */ +const uint16_t L2DownList[] = { 268, 269, 270, 271 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the catacombs. */ +const uint16_t L2TWarpUpList[] = { 557, 558 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from the caves. */ +const uint16_t L3UpList[] = { 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from the caves. */ +const uint16_t L3DownList[] = { 161, 162, 163, 164, 165, 166, 167, 168 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from the caves. */ +const uint16_t L3TWarpUpList[] = { 181, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up from hell. */ +const uint16_t L4UpList[] = { 81, 82, 89 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down from hell. */ +const uint16_t L4DownList[] = { 119, 129, 130, 131, 132 }; +/** Specifies the dungeon piece IDs which constitute stairways leading up to town from hell. */ +const uint16_t L4TWarpUpList[] = { 420, 421, 428 }; +/** Specifies the dungeon piece IDs which constitute stairways leading down to Diablo from hell. */ +const uint16_t L4PentaList[] = { 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383 }; +const uint16_t L5TWarpUpList[] = { 171, 172, 173, 174, 175, 176, 177, 178, 183 }; +const uint16_t L5UpList[] = { 148, 149, 150, 151, 152, 153, 154, 156, 157, 158 }; +const uint16_t L5DownList[] = { 124, 125, 128, 130, 131, 134, 135, 139, 141 }; +const uint16_t L6TWarpUpList[] = { 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }; +const uint16_t L6UpList[] = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77 }; +const uint16_t L6DownList[] = { 56, 57, 58, 59, 60, 61, 62, 63 }; +} // namespace + +void InitNoTriggers() +{ + numtrigs = 0; + trigflag = false; +} + +bool IsWarpOpen(dungeon_type type) +{ + if (gbIsSpawn) + return false; + + if (gbIsMultiplayer && type != DTYPE_NEST) // Opening the nest is part of in town quest + return true; + + const Player &myPlayer = *MyPlayer; + + if (type == DTYPE_CATACOMBS && (myPlayer.pTownWarps & 1) != 0) + return true; + if (type == DTYPE_CAVES && (myPlayer.pTownWarps & 2) != 0) + return true; + if (type == DTYPE_HELL && (myPlayer.pTownWarps & 4) != 0) + return true; + + if (gbIsHellfire) { + if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) + return true; + if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) + return true; + if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) + return true; + if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) + return true; + if (type == DTYPE_CRYPT && Quests[Q_GRAVE]._qactive == QUEST_DONE) + return true; + } + + return false; +} + +void InitTownTriggers() +{ + numtrigs = 0; + trigflag = false; + + const std::string &townId = GetTownRegistry().GetCurrentTown(); + if (!GetTownRegistry().HasTown(townId)) { + LogError("InitTownTriggers: current town '{}' not registered", townId); + return; + } + + const TownConfig &town = GetTownRegistry().GetTown(townId); + for (const TownTrigger &trigger : town.triggers) { + if (trigger.warpGate.has_value() && !IsWarpOpen(*trigger.warpGate)) + continue; + if (numtrigs >= MAXTRIGGERS) { + LogError("InitTownTriggers: more than MAXTRIGGERS ({}) for town '{}'", MAXTRIGGERS, townId); + break; + } + trigs[numtrigs].position = trigger.position; + trigs[numtrigs]._tmsg = trigger.message; + trigs[numtrigs]._tlvl = trigger.targetLevel; + numtrigs++; + } +} + +void InitL1Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 128) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + if (dPiece[i][j] == 114) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL2Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 266 && (!Quests[Q_SCHAMB].IsAvailable() || i != Quests[Q_SCHAMB].position.x || j != Quests[Q_SCHAMB].position.y)) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 558) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + + if (dPiece[i][j] == 270) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL3Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 170) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 167) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + + if (dPiece[i][j] == 548) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitL4Triggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 82) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 421) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + + if (dPiece[i][j] == 119) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 369 && Quests[Q_BETRAYER]._qactive == QUEST_DONE) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitHiveTriggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 65) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + + if (dPiece[i][j] == 62) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + + if (dPiece[i][j] == 79) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitCryptTriggers() +{ + numtrigs = 0; + for (WorldTileCoord j = 0; j < MAXDUNY; j++) { + for (WorldTileCoord i = 0; i < MAXDUNX; i++) { + if (dPiece[i][j] == 183) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABTWARPUP; + trigs[numtrigs]._tlvl = 0; + numtrigs++; + } + if (dPiece[i][j] == 157) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABPREVLVL; + numtrigs++; + } + if (dPiece[i][j] == 125) { + trigs[numtrigs].position = { i, j }; + trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; + numtrigs++; + } + } + } + trigflag = false; +} + +void InitSKingTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 82, 42 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitSChambTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 70, 39 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitPWaterTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 30, 83 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +void InitVPTriggers() +{ + trigflag = false; + numtrigs = 1; + trigs[0].position = { 35, 32 }; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + +bool ForceTownTrig() +{ + for (const uint16_t tileId : TownDownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to dungeon"); + cursPosition = { 25, 29 }; + return true; + } + } + + if (IsWarpOpen(DTYPE_CATACOMBS)) { + for (const uint16_t tileId : TownWarp1List) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to catacombs"); + cursPosition = { 49, 21 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_CAVES)) { + for (uint16_t i = 1198; i <= 1219; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == i) { + InfoString = _("Down to caves"); + cursPosition = { 17, 69 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_HELL)) { + for (uint16_t i = 1239; i <= 1254; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == i) { + InfoString = _("Down to hell"); + cursPosition = { 41, 80 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_NEST)) { + for (const uint16_t tileId : TownHiveList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Hive"); + cursPosition = { 80, 62 }; + return true; + } + } + } + + if (IsWarpOpen(DTYPE_CRYPT)) { + for (const uint16_t tileId : TownCryptList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Crypt"); + cursPosition = { 36, 24 }; + return true; + } + } + } + + return false; +} + +bool ForceL1Trig() +{ + for (const uint16_t tileId : L1UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + if (currlevel > 1) + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + else + InfoString = _("Up to town"); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + for (const uint16_t tileId : L1DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + return false; +} + +bool ForceL2Trig() +{ + for (const uint16_t tileId : L2UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + + for (const uint16_t tileId : L2DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 5) { + for (const uint16_t tileId : L2TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceL3Trig() +{ + for (const uint16_t tileId : L3UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + for (const uint16_t tileId : L3DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId + || dPiece[cursPosition.x + 1][cursPosition.y] == tileId + || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 9) { + for (const uint16_t tileId : L3TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceL4Trig() +{ + for (const uint16_t tileId : L4UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + for (const uint16_t tileId : L4DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 13) { + for (const uint16_t tileId : L4TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + if (currlevel == 15) { + for (const uint16_t tileId : L4PentaList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = _("Down to Diablo"); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + + return false; +} + +bool ForceHiveTrig() +{ + for (const uint16_t tileId : L6UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to Nest level {:d}")), currlevel - 17); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + for (const uint16_t tileId : L6DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId + || dPiece[cursPosition.x + 1][cursPosition.y] == tileId + || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel - 15); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + + if (currlevel == 17) { + for (const uint16_t tileId : L6TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +bool ForceCryptTrig() +{ + for (const uint16_t tileId : L5UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Up to Crypt level {:d}")), currlevel - 21); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABPREVLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + if (dPiece[cursPosition.x][cursPosition.y] == 316) { + InfoString = _("Cornerstone of the World"); + return true; + } + for (const uint16_t tileId : L5DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Down to Crypt level {:d}")), currlevel - 19); + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABNEXTLVL) { + cursPosition = trigs[j].position; + return true; + } + } + } + } + if (currlevel == 21) { + for (const uint16_t tileId : L5TWarpUpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + for (int j = 0; j < numtrigs; j++) { + if (trigs[j]._tmsg == WM_DIABTWARPUP) { + const int dx = std::abs(trigs[j].position.x - cursPosition.x); + const int dy = std::abs(trigs[j].position.y - cursPosition.y); + if (dx < 4 && dy < 4) { + InfoString = _("Up to town"); + cursPosition = trigs[j].position; + return true; + } + } + } + } + } + } + + return false; +} + +void Freeupstairs() +{ + for (int i = 0; i < numtrigs; i++) { + const int tx = trigs[i].position.x; + const int ty = trigs[i].position.y; + + for (int yy = -2; yy <= 2; yy++) { + for (int xx = -2; xx <= 2; xx++) { + dFlags[tx + xx][ty + yy] |= DungeonFlag::Populated; + } + } + } +} + +bool ForceSKingTrig() +{ + for (const uint16_t tileId : L1UpList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SKELKING]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForceSChambTrig() +{ + for (const uint16_t tileId : L2DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SCHAMB]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForcePWaterTrig() +{ + for (const uint16_t tileId : L3DownList) { + if (dPiece[cursPosition.x][cursPosition.y] == tileId) { + InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_PWATER]._qlevel); + cursPosition = trigs[0].position; + + return true; + } + } + + return false; +} + +bool ForceArenaTrig() +{ + const uint16_t *checkList; + size_t len; + switch (setlvltype) { + case DTYPE_TOWN: + checkList = TownWarp1List; + len = sizeof(TownWarp1List) / sizeof(TownWarp1List[0]); + break; + case DTYPE_CATHEDRAL: + checkList = L1UpList; + len = sizeof(L1UpList) / sizeof(L1UpList[0]); + break; + case DTYPE_CATACOMBS: + checkList = L2TWarpUpList; + len = sizeof(L2TWarpUpList) / sizeof(L2TWarpUpList[0]); + break; + case DTYPE_CAVES: + checkList = L3TWarpUpList; + len = sizeof(L3TWarpUpList) / sizeof(L3TWarpUpList[0]); + break; + case DTYPE_HELL: + checkList = L4TWarpUpList; + len = sizeof(L4TWarpUpList) / sizeof(L4TWarpUpList[0]); + break; + case DTYPE_NEST: + checkList = L5TWarpUpList; + len = sizeof(L5TWarpUpList) / sizeof(L5TWarpUpList[0]); + break; + case DTYPE_CRYPT: + checkList = L6TWarpUpList; + len = sizeof(L6TWarpUpList) / sizeof(L6TWarpUpList[0]); + break; + default: + return false; + } + for (size_t i = 0; i < len; ++i) { + if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { + InfoString = _("Up to town"); + cursPosition = trigs[0].position; + return true; + } + } + + return false; +} + +void CheckTrigForce() +{ + trigflag = false; + + if (ControlMode == ControlTypes::KeyboardAndMouse && GetMainPanel().contains(MousePosition)) { + return; + } + + if (!setlevel) { + switch (leveltype) { + case DTYPE_TOWN: + trigflag = ForceTownTrig(); + break; + case DTYPE_CATHEDRAL: + trigflag = ForceL1Trig(); + break; + case DTYPE_CATACOMBS: + trigflag = ForceL2Trig(); + break; + case DTYPE_CAVES: + trigflag = ForceL3Trig(); + break; + case DTYPE_HELL: + trigflag = ForceL4Trig(); + break; + case DTYPE_NEST: + trigflag = ForceHiveTrig(); + break; + case DTYPE_CRYPT: + trigflag = ForceCryptTrig(); + break; + default: + break; + } + if (leveltype != DTYPE_TOWN && !trigflag) { + trigflag = ForceQuests(); + } + } else { + switch (setlvlnum) { + case SL_SKELKING: + trigflag = ForceSKingTrig(); + break; + case SL_BONECHAMB: + trigflag = ForceSChambTrig(); + break; + case SL_POISONWATER: + trigflag = ForcePWaterTrig(); + break; + default: + if (IsArenaLevel(setlvlnum)) + trigflag = ForceArenaTrig(); + break; + } + } +} + +void CheckTriggers() +{ + Player &myPlayer = *MyPlayer; + + if (myPlayer._pmode != PM_STAND) + return; + + for (int i = 0; i < numtrigs; i++) { + if (myPlayer.position.tile != trigs[i].position) { + continue; + } + + switch (trigs[i]._tmsg) { + case WM_DIABNEXTLVL: + if (gbIsSpawn && currlevel >= 2) { + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, { myPlayer.position.tile.x, myPlayer.position.tile.y + 1 }); + myPlayer.Say(HeroSpeech::NotAChance); + InitDiabloMsg(EMSG_NOT_IN_SHAREWARE); + } else { + StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel + 1); + } + break; + case WM_DIABPREVLVL: + StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel - 1); + break; + case WM_DIABRTNLVL: + StartNewLvl(myPlayer, trigs[i]._tmsg, GetMapReturnLevel()); + break; + case WM_DIABTOWNWARP: + if (gbIsMultiplayer) { + bool abort = false; + diablo_message abortflag; + + auto position = myPlayer.position.tile; + if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { + abort = true; + position.y += 1; + abortflag = EMSG_REQUIRES_LVL_8; + } + + if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { + abort = true; + position.x += 1; + abortflag = EMSG_REQUIRES_LVL_13; + } + + if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { + abort = true; + position.y += 1; + abortflag = EMSG_REQUIRES_LVL_17; + } + + if (abort) { + myPlayer.Say(HeroSpeech::ICantGetThereFromHere); + + InitDiabloMsg(abortflag); + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, position); + return; + } + } + + StartNewLvl(myPlayer, trigs[i]._tmsg, trigs[i]._tlvl); + break; + case WM_DIABTWARPUP: + TWarpFrom = currlevel; + StartNewLvl(myPlayer, trigs[i]._tmsg, 0); + break; + default: + app_fatal("Unknown trigger msg"); + } + } +} + +bool EntranceBoundaryContains(Point entrance, Point position) +{ + constexpr Displacement entranceOffsets[7] = { { 0, 0 }, { -1, 0 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -1, -2 }, { -2, -2 } }; + + return c_any_of( + entranceOffsets, + [=](Displacement offset) { return entrance + offset == position; }); +} + +} // namespace devilution diff --git a/Source/lua/lua_event.hpp b/Source/lua/lua_event.hpp index f8f54d91ebc..d6957e18055 100644 --- a/Source/lua/lua_event.hpp +++ b/Source/lua/lua_event.hpp @@ -1,31 +1,31 @@ -#pragma once - -#include -#include - -namespace devilution { - -struct Player; -struct Monster; - -namespace lua { - -void MonsterDataLoaded(); -void UniqueMonsterDataLoaded(); -void ItemDataLoaded(); -void UniqueItemDataLoaded(); - -void StoreOpened(std::string_view name); - -void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType); - -void OnPlayerGainExperience(const Player *player, uint32_t exp); -void OnPlayerTakeDamage(const Player *player, int damage, int damageType); - -void LoadModsComplete(); -void GameDrawComplete(); -void GameStart(); - -} // namespace lua - -} // namespace devilution +#pragma once + +#include +#include + +namespace devilution { + +struct Player; +struct Monster; + +namespace lua { + +void MonsterDataLoaded(); +void UniqueMonsterDataLoaded(); +void ItemDataLoaded(); +void UniqueItemDataLoaded(); + +void StoreOpened(std::string_view name); + +void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType); + +void OnPlayerGainExperience(const Player *player, uint32_t exp); +void OnPlayerTakeDamage(const Player *player, int damage, int damageType); + +void LoadModsComplete(); +void GameDrawComplete(); +void GameStart(); + +} // namespace lua + +} // namespace devilution diff --git a/Source/lua/modules/towns.cpp b/Source/lua/modules/towns.cpp index 4ed015eba65..0c4e7457cbb 100644 --- a/Source/lua/modules/towns.cpp +++ b/Source/lua/modules/towns.cpp @@ -1,8 +1,13 @@ #include "lua/modules/towns.hpp" +#include +#include +#include + #include #include "interfac.h" +#include "levels/gendung_defs.hpp" #include "levels/town_data.h" #include "lua/metadoc.hpp" #include "msg.h" @@ -13,6 +18,21 @@ namespace devilution { namespace { +std::optional ParseWarpGateString(std::string_view w) +{ + if (w == "catacombs") + return DTYPE_CATACOMBS; + if (w == "caves") + return DTYPE_CAVES; + if (w == "hell") + return DTYPE_HELL; + if (w == "nest") + return DTYPE_NEST; + if (w == "crypt") + return DTYPE_CRYPT; + return std::nullopt; +} + std::string LuaRegisterTown(std::string_view townId, const sol::table &config) { TownConfig townConfig; @@ -89,6 +109,39 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) } } + if (sol::optional triggerList = config["triggers"]) { + for (const auto &kv : *triggerList) { + sol::table t = kv.second.as(); + TownTrigger tr; + sol::optional tx = t["x"]; + sol::optional ty = t["y"]; + tr.position = { tx.value_or(0), ty.value_or(0) }; + sol::optional kindStr = t["kind"]; + const std::string kind = kindStr.value_or("nextlevel"); + if (kind == "townwarp") { + tr.message = WM_DIABTOWNWARP; + sol::optional lvl = t["level"]; + tr.targetLevel = lvl.value_or(0); + sol::optional warpStr = t["warp"]; + if (warpStr.has_value() && !warpStr->empty()) { + std::optional gate = ParseWarpGateString(*warpStr); + if (!gate.has_value()) { + LogError("registerTown: unknown triggers[].warp '{}'", *warpStr); + continue; + } + tr.warpGate = gate; + } + } else if (kind == "nextlevel") { + tr.message = WM_DIABNEXTLVL; + tr.targetLevel = 0; + } else { + LogError("registerTown: unknown triggers[].kind '{}', expected nextlevel or townwarp", kind); + continue; + } + townConfig.triggers.push_back(tr); + } + } + std::string townIdStr(townId); GetTownRegistry().RegisterTown(townIdStr, townConfig); return townIdStr; @@ -135,7 +188,9 @@ sol::table LuaTownsModule(sol::state_view &lua) sol::table table = lua.create_table(); LuaSetDocFn(table, "register", "(townId: string, config: table) -> string", - "Registers a new town from a config table. Returns town ID.", + "Registers a new town from a config table. Returns town ID.\n" + "Optional triggers: array of tables with x, y, kind (\"nextlevel\" or \"townwarp\").\n" + "For townwarp, set level (dungeon level) and warp (\"catacombs\", \"caves\", \"hell\", \"nest\", \"crypt\") for IsWarpOpen gating.", LuaRegisterTown); LuaSetDocFn(table, "travel", "(townId: string)", From 76a62f6728b58962de8b322855a4dac6041955b4 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Fri, 3 Apr 2026 13:53:39 -0700 Subject: [PATCH 10/13] Refactoring --- Source/diablo.cpp | 3 - Source/interfac.cpp | 18 +++ Source/interfac.h | 3 + Source/levels/gendung.cpp | 5 + Source/levels/town.cpp | 50 ++++--- Source/levels/town_data.cpp | 283 +++++++++++++++++++++-------------- Source/levels/town_data.h | 229 ++++++++++++++++------------ Source/levels/trigs.cpp | 4 +- Source/loadsave.cpp | 2 +- Source/lua/lua_event.hpp | 62 ++++---- Source/lua/modules/towns.cpp | 18 +-- Source/lua/modules/towns.hpp | 8 - Source/msg.cpp | 11 +- Source/player.h | 2 +- 14 files changed, 400 insertions(+), 298 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 3503685b4aa..ed0c88fa9e6 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -3221,9 +3221,6 @@ tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvld if (gbIsMultiplayer) DeltaLoadLevel(); - if (lvldir == ENTRY_TOWNSWITCH) - InitTownTriggers(); - IncProgress(); for (int x = 0; x < DMAXX; x++) diff --git a/Source/interfac.cpp b/Source/interfac.cpp index b9d62f58cfd..5dce26e40a5 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -473,6 +473,11 @@ void DoLoad(interface_mode uMsg) } IncProgress(); FreeGameMem(); + if (!GetTownRegistry().HasTown(DestinationTownID)) { + LogError("WM_DIABTOWNSWITCH: unknown town '{}'", DestinationTownID); + loadResult = tl::make_unexpected("Unknown destination town"); + break; + } GetTownRegistry().SetCurrentTown(DestinationTownID); if (MyPlayer != nullptr) { @@ -595,6 +600,19 @@ void ProgressEventHandler(const SDL_Event &event, uint16_t modState) } // namespace +void QueueTownSwitch() +{ + if (MyPlayer != nullptr) { + MyPlayer->_pInvincible = true; + SDL_Event event; + CustomEventToSdlEvent(event, WM_DIABTOWNSWITCH); + if (!SDLC_PushEvent(&event)) { + LogError("QueueTownSwitch: {}", SDL_GetError()); + SDL_ClearError(); + } + } +} + void RegisterCustomEvents() { #ifndef USE_SDL1 diff --git a/Source/interfac.h b/Source/interfac.h index 466640c3b8b..70791c0ac2b 100644 --- a/Source/interfac.h +++ b/Source/interfac.h @@ -71,6 +71,9 @@ enum Cutscenes : uint8_t { CutGate, }; +/** @brief Queues WM_DIABTOWNSWITCH for the local player (invincible until load completes). */ +void QueueTownSwitch(); + void interface_msg_pump(); void IncProgress(uint32_t steps = 1); void CompleteProgress(); diff --git a/Source/levels/gendung.cpp b/Source/levels/gendung.cpp index 39ebc3e2410..287191f5f77 100644 --- a/Source/levels/gendung.cpp +++ b/Source/levels/gendung.cpp @@ -813,6 +813,11 @@ bool IsNearThemeRoom(WorldTilePosition testPosition) void InitLevels() { + // Note: InitializeTristram overwrites any existing "tristram" registration. + // Lua mods that called towns.register("tristram", ...) before InitLevels will + // have their config replaced. There is intentionally no registry Reset() here; + // mod-registered towns persist across new games and are re-populated by the + // Lua runtime at mod load time. InitializeTristram(); currlevel = 0; leveltype = DTYPE_TOWN; diff --git a/Source/levels/town.cpp b/Source/levels/town.cpp index e602e77842c..bf655674209 100644 --- a/Source/levels/town.cpp +++ b/Source/levels/town.cpp @@ -13,6 +13,7 @@ #include "player.h" #include "quests.h" #include "utils/endian_swap.hpp" +#include "utils/log.hpp" namespace devilution { @@ -200,33 +201,35 @@ void DrlgTPass3() } } - const TownConfig &config = GetTownRegistry().GetTown(GetTownRegistry().GetCurrentTown()); + const std::string &townId = GetTownRegistry().GetCurrentTown(); + if (!GetTownRegistry().HasTown(townId)) { + LogError("DrlgTPass3: current town '{}' not registered", townId); + return; + } + const TownConfig &config = GetTownRegistry().GetTown(townId); for (const auto §or : config.sectors) { FillSector(sector.filePath.c_str(), sector.x, sector.y); } - if (GetTownRegistry().GetCurrentTown() == "tristram") { + for (const TownWarpPatch &patch : config.warpClosedPatches) { + if (IsWarpOpen(patch.requiredWarp)) + continue; + for (const auto &[pos, cellVal] : patch.dungeonCells) + dungeon[pos.x][pos.y] = static_cast(cellVal); + for (const TownWarpFillTile &ft : patch.fillTiles) + FillTile(ft.x, ft.y, ft.tile); + if (patch.randomGroundStrip.has_value()) { + const TownWarpClosedRandomGroundStrip &strip = *patch.randomGroundStrip; + for (int x = strip.xStart; x < strip.xEndExclusive; x++) { + FillTile(x, strip.y, PickRandomlyAmong({ 1, 2, 3, 4 })); + } + } + } + + if (townId == TristramTownId) { auto dunData = LoadFileInMem("levels\\towndata\\automap.dun"); PlaceDunTiles(dunData.get(), { 0, 0 }); - if (!IsWarpOpen(DTYPE_CATACOMBS)) { - dungeon[20][7] = 10; - dungeon[20][6] = 8; - FillTile(48, 20, 320); - } - if (!IsWarpOpen(DTYPE_CAVES)) { - dungeon[4][30] = 8; - FillTile(16, 68, 332); - FillTile(16, 70, 331); - } - if (!IsWarpOpen(DTYPE_HELL)) { - dungeon[15][35] = 7; - dungeon[16][35] = 7; - dungeon[17][35] = 7; - for (int x = 36; x < 46; x++) { - FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); - } - } if (gbIsHellfire) { if (IsWarpOpen(DTYPE_NEST)) { TownOpenHive(); @@ -361,7 +364,12 @@ void CleanTownFountain() void CreateTown(lvl_entry entry) { - const TownConfig &config = GetTownRegistry().GetTown(GetTownRegistry().GetCurrentTown()); + const std::string &townId = GetTownRegistry().GetCurrentTown(); + if (!GetTownRegistry().HasTown(townId)) { + LogError("CreateTown: current town '{}' not registered", townId); + return; + } + const TownConfig &config = GetTownRegistry().GetTown(townId); dminPosition = config.dminPosition; dmaxPosition = config.dmaxPosition; ViewPosition = config.GetEntryPoint(entry, TWarpFrom); diff --git a/Source/levels/town_data.cpp b/Source/levels/town_data.cpp index 28ef7e3d8d6..3a07b543b1e 100644 --- a/Source/levels/town_data.cpp +++ b/Source/levels/town_data.cpp @@ -1,114 +1,169 @@ -#include "levels/town_data.h" - -#include "utils/log.hpp" - -namespace devilution { - -namespace { - -TownRegistry g_townRegistry; - -} // namespace - -std::string DestinationTownID; - -TownRegistry &GetTownRegistry() -{ - return g_townRegistry; -} - -void TownRegistry::RegisterTown(const std::string &id, const TownConfig &config) -{ - towns[id] = config; - LogInfo("Registered town: {}", id); -} - -TownConfig &TownRegistry::GetTown(const std::string &id) -{ - return towns.at(id); -} - -bool TownRegistry::HasTown(const std::string &id) const -{ - return towns.count(id) > 0; -} - -void TownRegistry::SetCurrentTown(const std::string &id) -{ - currentTownID = id; -} - -std::string TownRegistry::GetCurrentTown() const -{ - return currentTownID; -} - -std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const -{ - for (const auto &[id, config] : towns) { - if (config.saveId == saveId) { - return id; - } - } - return "tristram"; -} - -Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const -{ - // For ENTRY_TWARPUP, match both entry type and warpFromLevel - if (entry == ENTRY_TWARPUP) { - for (const auto &ep : entries) { - if (ep.entryType == entry && ep.warpFromLevel == warpFrom) { - return ep.viewPosition; - } - } - } - - // For other entry types, just match the type - for (const auto &ep : entries) { - if (ep.entryType == entry) { - return ep.viewPosition; - } - } - - // Default fallback - return { 75, 68 }; -} - -void InitializeTristram() -{ - TownConfig tristram; - tristram.name = "Tristram"; - tristram.saveId = 0; - tristram.dminPosition = { 10, 10 }; - tristram.dmaxPosition = { 84, 84 }; - tristram.sectors = { - { "levels\\towndata\\sector1s.dun", 46, 46 }, - { "levels\\towndata\\sector2s.dun", 46, 0 }, - { "levels\\towndata\\sector3s.dun", 0, 46 }, - { "levels\\towndata\\sector4s.dun", 0, 0 }, - }; - tristram.entries = { - { ENTRY_MAIN, { 75, 68 }, -1 }, - { ENTRY_PREV, { 25, 31 }, -1 }, - { ENTRY_TWARPUP, { 49, 22 }, 5 }, - { ENTRY_TWARPUP, { 18, 69 }, 9 }, - { ENTRY_TWARPUP, { 41, 81 }, 13 }, - { ENTRY_TWARPUP, { 36, 25 }, 21 }, - { ENTRY_TWARPUP, { 79, 62 }, 17 }, - { ENTRY_TOWNSWITCH, { 75, 68 }, -1 }, - }; - // Matches legacy InitTownTriggers (cathedral + gated town warps) - tristram.triggers = { - { { 25, 29 }, WM_DIABNEXTLVL, 0, std::nullopt }, - { { 49, 21 }, WM_DIABTOWNWARP, 5, DTYPE_CATACOMBS }, - { { 17, 69 }, WM_DIABTOWNWARP, 9, DTYPE_CAVES }, - { { 41, 80 }, WM_DIABTOWNWARP, 13, DTYPE_HELL }, - { { 80, 62 }, WM_DIABTOWNWARP, 17, DTYPE_NEST }, - { { 36, 24 }, WM_DIABTOWNWARP, 21, DTYPE_CRYPT }, - }; - GetTownRegistry().RegisterTown("tristram", tristram); - GetTownRegistry().SetCurrentTown("tristram"); -} - -} // namespace devilution +#include "levels/town_data.h" + +#include "utils/log.hpp" + +namespace devilution { + +namespace { + +TownRegistry g_townRegistry; + +} // namespace + +std::string DestinationTownID; + +TownRegistry &GetTownRegistry() +{ + return g_townRegistry; +} + +void TownRegistry::RegisterTown(const std::string &id, const TownConfig &config) +{ + if (HasTown(id)) + LogWarn("RegisterTown: overwriting existing town '{}'", id); + towns[id] = config; + LogInfo("Registered town: {}", id); +} + +const TownConfig &TownRegistry::GetTown(const std::string &id) const +{ + return towns.at(id); +} + +TownConfig &TownRegistry::GetTown(const std::string &id) +{ + return towns.at(id); +} + +bool TownRegistry::HasTown(const std::string &id) const +{ + return towns.count(id) > 0; +} + +void TownRegistry::SetCurrentTown(const std::string &id) +{ + currentTownID = id; +} + +const std::string &TownRegistry::GetCurrentTown() const +{ + return currentTownID; +} + +std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const +{ + for (const auto &[id, config] : towns) { + if (config.saveId == saveId) { + return id; + } + } + if (saveId != 0) + LogWarn("GetTownBySaveId: unknown saveId {}, defaulting to Tristram", saveId); + return { TristramTownId }; +} + +Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const +{ + // For ENTRY_TWARPUP, match both entry type and warpFromLevel + if (entry == ENTRY_TWARPUP) { + for (const auto &ep : entries) { + if (ep.entryType == entry && ep.warpFromLevel == warpFrom) { + return ep.viewPosition; + } + } + } + + // For other entry types, just match the type + for (const auto &ep : entries) { + if (ep.entryType == entry) { + return ep.viewPosition; + } + } + + // Default fallback + return { 75, 68 }; +} + +void InitializeTristram() +{ + TownConfig tristram; + tristram.name = "Tristram"; + tristram.saveId = 0; + tristram.dminPosition = { 10, 10 }; + tristram.dmaxPosition = { 84, 84 }; + tristram.sectors = { + { "levels\\towndata\\sector1s.dun", 46, 46 }, + { "levels\\towndata\\sector2s.dun", 46, 0 }, + { "levels\\towndata\\sector3s.dun", 0, 46 }, + { "levels\\towndata\\sector4s.dun", 0, 0 }, + }; + // Spawn positions (view): one tile stepped from matching town triggers / stairs. + tristram.entries = { + { ENTRY_MAIN, { 75, 68 }, -1 }, + { ENTRY_PREV, { 25, 31 }, -1 }, + { ENTRY_TWARPUP, { 49, 22 }, 5 }, + { ENTRY_TWARPUP, { 18, 69 }, 9 }, + { ENTRY_TWARPUP, { 41, 81 }, 13 }, + { ENTRY_TWARPUP, { 36, 25 }, 21 }, + { ENTRY_TWARPUP, { 79, 62 }, 17 }, + { ENTRY_TOWNSWITCH, { 75, 68 }, -1 }, + }; + tristram.triggers = { + // Cathedral stairs (down to dungeon level 1) + { { 25, 29 }, WM_DIABNEXTLVL, 0, std::nullopt }, + // Town warp portals (active only when the respective warp is open) + { { 49, 21 }, WM_DIABTOWNWARP, 5, DTYPE_CATACOMBS }, + { { 17, 69 }, WM_DIABTOWNWARP, 9, DTYPE_CAVES }, + { { 41, 80 }, WM_DIABTOWNWARP, 13, DTYPE_HELL }, + { { 80, 62 }, WM_DIABTOWNWARP, 17, DTYPE_NEST }, + { { 36, 24 }, WM_DIABTOWNWARP, 21, DTYPE_CRYPT }, + }; + tristram.warpClosedPatches = { + /* + if (!IsWarpOpen(DTYPE_CATACOMBS)) { + dungeon[20][7] = 10; + dungeon[20][6] = 8; + FillTile(48, 20, 320); + } + */ + { + DTYPE_CATACOMBS, + { { { 20, 7 }, 10 }, { { 20, 6 }, 8 } }, + { { 48, 20, 320 } }, + std::nullopt, + }, + /* + if (!IsWarpOpen(DTYPE_CAVES)) { + dungeon[4][30] = 8; + FillTile(16, 68, 332); + FillTile(16, 70, 331); + } + */ + { + DTYPE_CAVES, + { { { 4, 30 }, 8 } }, + { { 16, 68, 332 }, { 16, 70, 331 } }, + std::nullopt, + }, + /* + if (!IsWarpOpen(DTYPE_HELL)) { + dungeon[15][35] = 7; + dungeon[16][35] = 7; + dungeon[17][35] = 7; + for (int x = 36; x < 46; x++) { + FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); + } + } + */ + { + DTYPE_HELL, + { { { 15, 35 }, 7 }, { { 16, 35 }, 7 }, { { 17, 35 }, 7 } }, + {}, + TownWarpClosedRandomGroundStrip { 36, 46, 78 }, + }, + }; + GetTownRegistry().RegisterTown(TristramTownId, tristram); + GetTownRegistry().SetCurrentTown(TristramTownId); +} + +} // namespace devilution diff --git a/Source/levels/town_data.h b/Source/levels/town_data.h index 432b32a694f..5061ab307fc 100644 --- a/Source/levels/town_data.h +++ b/Source/levels/town_data.h @@ -1,97 +1,132 @@ -#pragma once - -#include -#include -#include -#include - -#include "engine/point.hpp" -#include "interfac.h" -#include "levels/gendung_defs.hpp" - -namespace devilution { - -/** - * @brief Represents a town sector (map piece) - */ -struct TownSector { - std::string filePath; - int x, y; -}; - -/** - * @brief Represents a town entry/spawn point - */ -struct TownEntryPoint { - lvl_entry entryType; - Point viewPosition; - int warpFromLevel; // Source level for ENTRY_TWARPUP (-1 for any) -}; - -/** - * @brief Position override for a towner NPC - */ -struct TownerPositionOverride { - std::string shortName; // e.g. "griswold", "farnham" - Point position; -}; - -/** - * @brief Dungeon entrance / town warp trigger (see InitTownTriggers) - */ -struct TownTrigger { - Point position; - interface_mode message; - /** For WM_DIABTOWNWARP; unused for other message types */ - int targetLevel = 0; - /** If set, trigger is only active when IsWarpOpen(*warpGate) */ - std::optional warpGate; -}; - -/** - * @brief Complete configuration for a town - */ -struct TownConfig { - std::string name; - uint8_t saveId = 0; - Point dminPosition = { 10, 10 }; - Point dmaxPosition = { 84, 84 }; - std::vector sectors; - std::string solFile; - std::vector entries; - std::vector triggers; - std::vector townerOverrides; - - /** - * @brief Gets the spawn point for a given entry type and warp source - */ - Point GetEntryPoint(lvl_entry entry, int warpFrom = -1) const; -}; - -/** - * @brief Registry for managing multiple town configurations - */ -class TownRegistry { -private: - std::unordered_map towns; - std::string currentTownID; - -public: - void RegisterTown(const std::string &id, const TownConfig &config); - TownConfig &GetTown(const std::string &id); - bool HasTown(const std::string &id) const; - void SetCurrentTown(const std::string &id); - std::string GetCurrentTown() const; - const std::unordered_map &GetTowns() const { return towns; } - - /** @brief Finds town string ID by its saveId. Returns "tristram" if not found. */ - std::string GetTownBySaveId(uint8_t saveId) const; -}; - -TownRegistry &GetTownRegistry(); - -extern std::string DestinationTownID; - -void InitializeTristram(); - -} // namespace devilution +#pragma once + +#include +#include +#include +#include +#include + +#include "engine/point.hpp" +#include "interfac.h" +#include "levels/gendung_defs.hpp" + +namespace devilution { + +/** @brief Canonical town registry id for vanilla Tristram (lowercase). */ +inline constexpr char TristramTownId[] = "tristram"; + +/** + * @brief Represents a town sector (map piece) + */ +struct TownSector { + std::string filePath; + int x, y; +}; + +/** + * @brief Represents a town entry/spawn point + */ +struct TownEntryPoint { + lvl_entry entryType; + Point viewPosition; + int warpFromLevel; // Source level for ENTRY_TWARPUP (-1 for any) +}; + +/** + * @brief Position override for a towner NPC + */ +struct TownerPositionOverride { + std::string shortName; // e.g. "griswold", "farnham" + Point position; +}; + +/** + * @brief Dungeon entrance / town warp trigger (see InitTownTriggers) + */ +struct TownTrigger { + Point position; + interface_mode msg; + /** For WM_DIABTOWNWARP; unused for other message types */ + int level = 0; + /** If set, trigger is only active when IsWarpOpen(*warpGate) */ + std::optional warpGate; +}; + +struct TownWarpFillTile { + int x; + int y; + int tile; +}; + +/** + * @brief Fills random ground micros (1-4) for each x in [xStart, xEnd). + */ +struct TownWarpClosedRandomGroundStrip { + int xStart; + int xEndExclusive; + int y; +}; + +/** + * @brief Visual dungeon/dPiece patches when a town warp is still closed (see DrlgTPass3). + */ +struct TownWarpPatch { + dungeon_type requiredWarp; + std::vector> dungeonCells; + std::vector fillTiles; + std::optional randomGroundStrip; +}; + +/** + * @brief Complete configuration for a town + */ +struct TownConfig { + std::string name; + uint8_t saveId = 0; + Point dminPosition = { 10, 10 }; + Point dmaxPosition = { 84, 84 }; + std::vector sectors; + std::vector entries; + std::vector triggers; + std::vector warpClosedPatches; + std::vector townerOverrides; + + /** + * @brief Gets the spawn point for a given entry type and warp source + */ + Point GetEntryPoint(lvl_entry entry, int warpFrom = -1) const; +}; + +/** + * @brief Registry for managing multiple town configurations + */ +class TownRegistry { +private: + std::unordered_map towns; + std::string currentTownID; + +public: + void RegisterTown(const std::string &id, const TownConfig &config); + const TownConfig &GetTown(const std::string &id) const; + TownConfig &GetTown(const std::string &id); + bool HasTown(const std::string &id) const; + void SetCurrentTown(const std::string &id); + const std::string &GetCurrentTown() const; + const std::unordered_map &GetTowns() const { return towns; } + + /** @brief Finds town string ID by its saveId. Returns TristramTownId if not found. */ + std::string GetTownBySaveId(uint8_t saveId) const; +}; + +TownRegistry &GetTownRegistry(); + +/** + * @brief Town ID to switch to, set before queuing WM_DIABTOWNSWITCH. + * Written by: towns Lua module, OnTownTravel (network), NetSendCmdTownTravel. + * Read by: WM_DIABTOWNSWITCH handler in interfac.cpp. + */ +extern std::string DestinationTownID; + +void InitializeTristram(); + +} // namespace devilution diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index 755746cf56a..800d059dcd2 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -129,8 +129,8 @@ void InitTownTriggers() break; } trigs[numtrigs].position = trigger.position; - trigs[numtrigs]._tmsg = trigger.message; - trigs[numtrigs]._tlvl = trigger.targetLevel; + trigs[numtrigs]._tmsg = trigger.msg; + trigs[numtrigs]._tlvl = trigger.level; numtrigs++; } } diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 8ee64bdef97..b9338873ccc 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -2531,7 +2531,7 @@ tl::expected LoadGame(bool firstflag) if (GetTownRegistry().HasTown(townId)) { GetTownRegistry().SetCurrentTown(townId); } else { - GetTownRegistry().SetCurrentTown("tristram"); + GetTownRegistry().SetCurrentTown(TristramTownId); myPlayer._pCurrentTownId = 0; } diff --git a/Source/lua/lua_event.hpp b/Source/lua/lua_event.hpp index d6957e18055..f8f54d91ebc 100644 --- a/Source/lua/lua_event.hpp +++ b/Source/lua/lua_event.hpp @@ -1,31 +1,31 @@ -#pragma once - -#include -#include - -namespace devilution { - -struct Player; -struct Monster; - -namespace lua { - -void MonsterDataLoaded(); -void UniqueMonsterDataLoaded(); -void ItemDataLoaded(); -void UniqueItemDataLoaded(); - -void StoreOpened(std::string_view name); - -void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType); - -void OnPlayerGainExperience(const Player *player, uint32_t exp); -void OnPlayerTakeDamage(const Player *player, int damage, int damageType); - -void LoadModsComplete(); -void GameDrawComplete(); -void GameStart(); - -} // namespace lua - -} // namespace devilution +#pragma once + +#include +#include + +namespace devilution { + +struct Player; +struct Monster; + +namespace lua { + +void MonsterDataLoaded(); +void UniqueMonsterDataLoaded(); +void ItemDataLoaded(); +void UniqueItemDataLoaded(); + +void StoreOpened(std::string_view name); + +void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType); + +void OnPlayerGainExperience(const Player *player, uint32_t exp); +void OnPlayerTakeDamage(const Player *player, int damage, int damageType); + +void LoadModsComplete(); +void GameDrawComplete(); +void GameStart(); + +} // namespace lua + +} // namespace devilution diff --git a/Source/lua/modules/towns.cpp b/Source/lua/modules/towns.cpp index 0c4e7457cbb..e905eb2e778 100644 --- a/Source/lua/modules/towns.cpp +++ b/Source/lua/modules/towns.cpp @@ -54,9 +54,6 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) } } - sol::optional solFile = config["sol"]; - townConfig.solFile = solFile.value_or(""); - if (sol::optional sectors = config["sectors"]) { for (const auto &kv : *sectors) { sol::table sector = kv.second.as(); @@ -119,9 +116,9 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) sol::optional kindStr = t["kind"]; const std::string kind = kindStr.value_or("nextlevel"); if (kind == "townwarp") { - tr.message = WM_DIABTOWNWARP; + tr.msg = WM_DIABTOWNWARP; sol::optional lvl = t["level"]; - tr.targetLevel = lvl.value_or(0); + tr.level = lvl.value_or(0); sol::optional warpStr = t["warp"]; if (warpStr.has_value() && !warpStr->empty()) { std::optional gate = ParseWarpGateString(*warpStr); @@ -132,8 +129,8 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) tr.warpGate = gate; } } else if (kind == "nextlevel") { - tr.message = WM_DIABNEXTLVL; - tr.targetLevel = 0; + tr.msg = WM_DIABNEXTLVL; + tr.level = 0; } else { LogError("registerTown: unknown triggers[].kind '{}', expected nextlevel or townwarp", kind); continue; @@ -163,12 +160,7 @@ void LuaTravelToTown(std::string_view townId) return; } - if (MyPlayer != nullptr) { - MyPlayer->_pInvincible = true; - SDL_Event event; - CustomEventToSdlEvent(event, WM_DIABTOWNSWITCH); - SDL_PushEvent(&event); - } + QueueTownSwitch(); } std::string LuaGetCurrentTown() diff --git a/Source/lua/modules/towns.hpp b/Source/lua/modules/towns.hpp index 878fc3d926e..6458921d70c 100644 --- a/Source/lua/modules/towns.hpp +++ b/Source/lua/modules/towns.hpp @@ -4,14 +4,6 @@ namespace devilution { -/** - * @brief Creates and returns the Lua towns module - * - * Exposes town registration and travel functionality to Lua mods. - * - * @param lua The Sol2 state view - * @return sol::table The towns module table - */ sol::table LuaTownsModule(sol::state_view &lua); } // namespace devilution diff --git a/Source/msg.cpp b/Source/msg.cpp index 299ad9c45d0..f0cde964c7b 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -34,6 +34,7 @@ #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "gamemenu.h" +#include "interfac.h" #include "items/validation.h" #include "levels/crypt.h" #include "levels/town.h" @@ -2468,6 +2469,8 @@ size_t OnTownTravel(const TCmd &cmd, size_t maxCmdSize, Player & /*player*/) const auto tokens = SplitByChar(str, '\0'); const std::string_view townId = *tokens.begin(); + // gbBufferMsgs != 0 means we are buffering a game-state replay; skip the SDL + // event so we do not queue a second town switch while one is already in flight. if (gbBufferMsgs == 0) { // Process town travel on all clients std::string townIdStr(townId); @@ -2475,13 +2478,7 @@ size_t OnTownTravel(const TCmd &cmd, size_t maxCmdSize, Player & /*player*/) DestinationTownID = townIdStr; LogInfo("Network: Received town travel to '{}'", townId); - // Trigger town switch for this client - if (MyPlayer != nullptr) { - MyPlayer->_pInvincible = true; - SDL_Event event; - CustomEventToSdlEvent(event, WM_DIABTOWNSWITCH); - SDL_PushEvent(&event); - } + QueueTownSwitch(); } else { LogError("Network: Unknown town ID '{}'", townId); } diff --git a/Source/player.h b/Source/player.h index 4fe8abb279f..04fe0d3764c 100644 --- a/Source/player.h +++ b/Source/player.h @@ -359,7 +359,7 @@ struct Player { uint8_t pDungMsgs2; bool pOriginalCathedral; uint8_t pDiabloKillLevel; - /** @brief Current town ID as a hash/index (0 = Tristram, used for save compatibility) */ + /** @brief Save-file ID of the current town (matches TownConfig::saveId; 0 = Tristram) */ uint8_t _pCurrentTownId; uint16_t wReflections; ItemSpecialEffectHf pDamAcFlags; From 0a089fb23b0288a0be26d5118d4c31a87371ca69 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Fri, 3 Apr 2026 14:23:38 -0700 Subject: [PATCH 11/13] Add tests for town registry --- CMake/Tests.cmake | 1 + Source/levels/town_data.cpp | 30 +++--- test/town_registry_test.cpp | 178 ++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 test/town_registry_test.cpp diff --git a/CMake/Tests.cmake b/CMake/Tests.cmake index bfe6bf8a163..d56788e8ac2 100644 --- a/CMake/Tests.cmake +++ b/CMake/Tests.cmake @@ -35,6 +35,7 @@ set(tests stores_test tile_properties_test timedemo_test + town_registry_test townerdat_test writehero_test vendor_test diff --git a/Source/levels/town_data.cpp b/Source/levels/town_data.cpp index 3a07b543b1e..9121782fd3d 100644 --- a/Source/levels/town_data.cpp +++ b/Source/levels/town_data.cpp @@ -8,6 +8,9 @@ namespace { TownRegistry g_townRegistry; +/** @brief Spawn used when no TownEntryPoint matches (matches legacy hard-coded default). */ +constexpr Point kDefaultTownEntryPoint = { 75, 68 }; + } // namespace std::string DestinationTownID; @@ -71,6 +74,7 @@ Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const return ep.viewPosition; } } + return kDefaultTownEntryPoint; } // For other entry types, just match the type @@ -81,7 +85,7 @@ Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const } // Default fallback - return { 75, 68 }; + return kDefaultTownEntryPoint; } void InitializeTristram() @@ -121,9 +125,9 @@ void InitializeTristram() tristram.warpClosedPatches = { /* if (!IsWarpOpen(DTYPE_CATACOMBS)) { - dungeon[20][7] = 10; - dungeon[20][6] = 8; - FillTile(48, 20, 320); + dungeon[20][7] = 10; + dungeon[20][6] = 8; + FillTile(48, 20, 320); } */ { @@ -134,9 +138,9 @@ void InitializeTristram() }, /* if (!IsWarpOpen(DTYPE_CAVES)) { - dungeon[4][30] = 8; - FillTile(16, 68, 332); - FillTile(16, 70, 331); + dungeon[4][30] = 8; + FillTile(16, 68, 332); + FillTile(16, 70, 331); } */ { @@ -147,12 +151,12 @@ void InitializeTristram() }, /* if (!IsWarpOpen(DTYPE_HELL)) { - dungeon[15][35] = 7; - dungeon[16][35] = 7; - dungeon[17][35] = 7; - for (int x = 36; x < 46; x++) { - FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); - } + dungeon[15][35] = 7; + dungeon[16][35] = 7; + dungeon[17][35] = 7; + for (int x = 36; x < 46; x++) { + FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); + } } */ { diff --git a/test/town_registry_test.cpp b/test/town_registry_test.cpp new file mode 100644 index 00000000000..f2f79497e30 --- /dev/null +++ b/test/town_registry_test.cpp @@ -0,0 +1,178 @@ +/** + * @file town_registry_test.cpp + * + * Unit tests for town registry, entry points, and InitializeTristram. + */ + +#include + +#include + +#include "levels/town_data.h" + +using namespace devilution; + +namespace { + +constexpr size_t kTristramTriggerCount = 6; +constexpr size_t kTristramWarpClosedPatchCount = 3; +constexpr size_t kTristramEntryPointCount = 8; + +constexpr Point kTristramCathedralTrigPosition = { 25, 29 }; + +constexpr Point kTownEntryDefaultFallback = { 75, 68 }; + +constexpr uint8_t kTestTownAlphaSaveId = 7; +constexpr uint8_t kTestTownBetaSaveId = 11; +constexpr uint8_t kUnknownSaveId = 99; + +constexpr int kNonMatchingWarpFromLevel = 999; + +TEST(TownRegistry, HasTown_ReturnsFalseForUnregistered) +{ + TownRegistry registry; + EXPECT_FALSE(registry.HasTown("nowhere")); +} + +TEST(TownRegistry, RegisterTown_HasTown_GetTown_RoundTrip) +{ + TownRegistry registry; + TownConfig cfg; + cfg.name = "Testburg"; + registry.RegisterTown("testburg", cfg); + EXPECT_TRUE(registry.HasTown("testburg")); + EXPECT_EQ(registry.GetTown("testburg").name, "Testburg"); +} + +TEST(TownRegistry, GetTown_ThrowsForUnregistered) +{ + TownRegistry registry; + EXPECT_THROW(static_cast(registry.GetTown("missing")), std::out_of_range); +} + +TEST(TownRegistry, SetCurrentTown_GetCurrentTown_RoundTrip) +{ + TownRegistry registry; + TownConfig cfg; + registry.RegisterTown("a", cfg); + registry.SetCurrentTown("a"); + EXPECT_EQ(registry.GetCurrentTown(), "a"); +} + +TEST(TownRegistry, GetTownBySaveId_FindsBySaveId) +{ + TownRegistry registry; + TownConfig alpha; + alpha.name = "Alpha"; + alpha.saveId = kTestTownAlphaSaveId; + TownConfig beta; + beta.name = "Beta"; + beta.saveId = kTestTownBetaSaveId; + registry.RegisterTown("alpha", alpha); + registry.RegisterTown("beta", beta); + EXPECT_EQ(registry.GetTownBySaveId(kTestTownAlphaSaveId), "alpha"); + EXPECT_EQ(registry.GetTownBySaveId(kTestTownBetaSaveId), "beta"); +} + +TEST(TownRegistry, GetTownBySaveId_ReturnsTristramForUnknownNonZeroSaveId) +{ + TownRegistry registry; + EXPECT_EQ(registry.GetTownBySaveId(kUnknownSaveId), TristramTownId); +} + +TEST(TownRegistry, GetTownBySaveId_ReturnsTristramWhenZeroNotRegistered) +{ + TownRegistry registry; + TownConfig cfg; + cfg.saveId = kTestTownAlphaSaveId; + registry.RegisterTown("only_alpha", cfg); + EXPECT_EQ(registry.GetTownBySaveId(0), TristramTownId); +} + +TEST(TownConfig, GetEntryPoint_ENTRY_MAIN) +{ + TownConfig cfg; + cfg.entries = { + { ENTRY_MAIN, { 10, 20 }, -1 }, + }; + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_MAIN), Point(10, 20)); +} + +TEST(TownConfig, GetEntryPoint_ENTRY_TWARPUP_MatchesWarpFromLevel) +{ + TownConfig cfg; + constexpr int kWarpFromLevel = 5; + cfg.entries = { + { ENTRY_TWARPUP, { 49, 22 }, kWarpFromLevel }, + }; + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, kWarpFromLevel), Point(49, 22)); +} + +TEST(TownConfig, GetEntryPoint_ENTRY_TWARPUP_NonMatchingWarpUsesDefault) +{ + TownConfig cfg; + cfg.entries = { + { ENTRY_TWARPUP, { 49, 22 }, 5 }, + }; + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, kNonMatchingWarpFromLevel), kTownEntryDefaultFallback); +} + +TEST(TownConfig, GetEntryPoint_EmptyEntriesUsesDefault) +{ + TownConfig cfg; + cfg.entries.clear(); + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_MAIN), kTownEntryDefaultFallback); +} + +class InitializeTristramTest : public ::testing::Test { +protected: + static void SetUpTestSuite() + { + InitializeTristram(); + } +}; + +TEST_F(InitializeTristramTest, RegistryHasTristram) +{ + TownRegistry ®istry = GetTownRegistry(); + EXPECT_TRUE(registry.HasTown(TristramTownId)); +} + +TEST_F(InitializeTristramTest, CurrentTownIsTristram) +{ + EXPECT_EQ(GetTownRegistry().GetCurrentTown(), TristramTownId); +} + +TEST_F(InitializeTristramTest, GetTownBySaveIdZeroReturnsTristram) +{ + EXPECT_EQ(GetTownRegistry().GetTownBySaveId(0), TristramTownId); +} + +TEST_F(InitializeTristramTest, TristramTriggerCount) +{ + const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); + EXPECT_EQ(tristram.triggers.size(), kTristramTriggerCount); +} + +TEST_F(InitializeTristramTest, TristramFirstTriggerIsCathedralStairs) +{ + const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); + ASSERT_FALSE(tristram.triggers.empty()); + const TownTrigger &first = tristram.triggers.front(); + EXPECT_EQ(first.position, kTristramCathedralTrigPosition); + EXPECT_EQ(first.msg, WM_DIABNEXTLVL); +} + +TEST_F(InitializeTristramTest, TristramWarpClosedPatchCount) +{ + const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); + EXPECT_EQ(tristram.warpClosedPatches.size(), kTristramWarpClosedPatchCount); +} + +TEST_F(InitializeTristramTest, TristramEntryPointCount) +{ + const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); + EXPECT_EQ(tristram.entries.size(), kTristramEntryPointCount); +} + +} // namespace From 9fb9cb00679c009a99aed7056d468ebe6c18cb3f Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 12 Apr 2026 10:14:25 -0700 Subject: [PATCH 12/13] Add portal positions to town config --- Source/levels/town_data.cpp | 16 ++++++-- Source/levels/town_data.h | 18 +++++++++ Source/lua/modules/towns.cpp | 18 ++++++++- Source/portal.cpp | 15 ++----- test/town_registry_test.cpp | 77 +++++++++++++++++++++++++----------- 5 files changed, 105 insertions(+), 39 deletions(-) diff --git a/Source/levels/town_data.cpp b/Source/levels/town_data.cpp index 9121782fd3d..20f557c6de5 100644 --- a/Source/levels/town_data.cpp +++ b/Source/levels/town_data.cpp @@ -9,7 +9,7 @@ namespace { TownRegistry g_townRegistry; /** @brief Spawn used when no TownEntryPoint matches (matches legacy hard-coded default). */ -constexpr Point kDefaultTownEntryPoint = { 75, 68 }; +constexpr Point DefaultTownEntryPoint = { 75, 68 }; } // namespace @@ -65,6 +65,16 @@ std::string TownRegistry::GetTownBySaveId(uint8_t saveId) const return { TristramTownId }; } +Point GetPortalTownPosition(size_t portalIndex) +{ + if (portalIndex >= NumTownPortalSlots) + return DefaultTristramPortalPositions[0]; + const std::string &townId = GetTownRegistry().GetCurrentTown(); + if (GetTownRegistry().HasTown(townId)) + return GetTownRegistry().GetTown(townId).portalPositions[portalIndex]; + return DefaultTristramPortalPositions[portalIndex]; +} + Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const { // For ENTRY_TWARPUP, match both entry type and warpFromLevel @@ -74,7 +84,7 @@ Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const return ep.viewPosition; } } - return kDefaultTownEntryPoint; + return DefaultTownEntryPoint; } // For other entry types, just match the type @@ -85,7 +95,7 @@ Point TownConfig::GetEntryPoint(lvl_entry entry, int warpFrom) const } // Default fallback - return kDefaultTownEntryPoint; + return DefaultTownEntryPoint; } void InitializeTristram() diff --git a/Source/levels/town_data.h b/Source/levels/town_data.h index 5061ab307fc..f597be981c8 100644 --- a/Source/levels/town_data.h +++ b/Source/levels/town_data.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -15,6 +16,22 @@ namespace devilution { /** @brief Canonical town registry id for vanilla Tristram (lowercase). */ inline constexpr char TristramTownId[] = "tristram"; +/** @brief Town portal spell anchor slots; must match MAXPORTAL in portal.h. */ +inline constexpr size_t NumTownPortalSlots = 4; + +/** @brief Legacy positions for town portal missiles (one per player slot). */ +inline constexpr std::array DefaultTristramPortalPositions = { { + Point { 57, 40 }, + Point { 59, 40 }, + Point { 61, 40 }, + Point { 63, 40 }, +} }; + +/** + * @brief World position for the town portal missile / portal entry for player slot `portalIndex`. + */ +Point GetPortalTownPosition(size_t portalIndex); + /** * @brief Represents a town sector (map piece) */ @@ -90,6 +107,7 @@ struct TownConfig { std::vector triggers; std::vector warpClosedPatches; std::vector townerOverrides; + std::array portalPositions = DefaultTristramPortalPositions; /** * @brief Gets the spawn point for a given entry type and warp source diff --git a/Source/lua/modules/towns.cpp b/Source/lua/modules/towns.cpp index e905eb2e778..2284a3c0225 100644 --- a/Source/lua/modules/towns.cpp +++ b/Source/lua/modules/towns.cpp @@ -139,6 +139,21 @@ std::string LuaRegisterTown(std::string_view townId, const sol::table &config) } } + if (sol::optional portals = config["portals"]) { + size_t slot = 0; + for (const auto &kv : *portals) { + if (slot >= NumTownPortalSlots) { + LogWarn("registerTown: portals list has more than {} entries; ignoring extras", NumTownPortalSlots); + break; + } + sol::table t = kv.second.as(); + sol::optional px = t["x"]; + sol::optional py = t["y"]; + townConfig.portalPositions[slot] = { px.value_or(0), py.value_or(0) }; + ++slot; + } + } + std::string townIdStr(townId); GetTownRegistry().RegisterTown(townIdStr, townConfig); return townIdStr; @@ -182,7 +197,8 @@ sol::table LuaTownsModule(sol::state_view &lua) LuaSetDocFn(table, "register", "(townId: string, config: table) -> string", "Registers a new town from a config table. Returns town ID.\n" "Optional triggers: array of tables with x, y, kind (\"nextlevel\" or \"townwarp\").\n" - "For townwarp, set level (dungeon level) and warp (\"catacombs\", \"caves\", \"hell\", \"nest\", \"crypt\") for IsWarpOpen gating.", + "For townwarp, set level (dungeon level) and warp (\"catacombs\", \"caves\", \"hell\", \"nest\", \"crypt\") for IsWarpOpen gating.\n" + "Optional portals: up to four { x, y } tables for town portal spell positions (defaults match Tristram).", LuaRegisterTown); LuaSetDocFn(table, "travel", "(townId: string)", diff --git a/Source/portal.cpp b/Source/portal.cpp index 4d02adda5f6..6d55efc5351 100644 --- a/Source/portal.cpp +++ b/Source/portal.cpp @@ -5,6 +5,7 @@ */ #include "portal.h" +#include "levels/town_data.h" #include "lighting.h" #include "missiles.h" #include "multi.h" @@ -21,14 +22,6 @@ namespace { /** Current portal number (a portal array index). */ size_t portalindex; -/** Coordinate of each player's portal in town. */ -Point PortalTownPosition[MAXPORTAL] = { - { 57, 40 }, - { 59, 40 }, - { 61, 40 }, - { 63, 40 }, -}; - } // namespace void InitPortals() @@ -67,7 +60,7 @@ void SyncPortals() continue; const Player &player = Players[i]; if (leveltype == DTYPE_TOWN) - AddPortalMissile(player, PortalTownPosition[i], true); + AddPortalMissile(player, GetPortalTownPosition(static_cast(i)), true); else { int lvl = currlevel; if (setlevel) @@ -80,7 +73,7 @@ void SyncPortals() void AddPortalInTown(const Player &player) { - AddPortalMissile(player, PortalTownPosition[player.getId()], false); + AddPortalMissile(player, GetPortalTownPosition(player.getId()), false); } void ActivatePortal(const Player &player, Point position, int lvl, dungeon_type dungeonType, bool isSetLevel) @@ -163,7 +156,7 @@ void GetPortalLevel() void GetPortalLvlPos() { if (leveltype == DTYPE_TOWN) { - ViewPosition = PortalTownPosition[portalindex] + Displacement { 1, 1 }; + ViewPosition = GetPortalTownPosition(portalindex) + Displacement { 1, 1 }; } else { ViewPosition = Portals[portalindex].position; diff --git a/test/town_registry_test.cpp b/test/town_registry_test.cpp index f2f79497e30..32922a6ebe6 100644 --- a/test/town_registry_test.cpp +++ b/test/town_registry_test.cpp @@ -14,19 +14,19 @@ using namespace devilution; namespace { -constexpr size_t kTristramTriggerCount = 6; -constexpr size_t kTristramWarpClosedPatchCount = 3; -constexpr size_t kTristramEntryPointCount = 8; +constexpr size_t TristramTriggerCount = 6; +constexpr size_t TristramWarpClosedPatchCount = 3; +constexpr size_t TristramEntryPointCount = 8; -constexpr Point kTristramCathedralTrigPosition = { 25, 29 }; +constexpr Point TristramCathedralTrigPosition = { 25, 29 }; -constexpr Point kTownEntryDefaultFallback = { 75, 68 }; +constexpr Point TownEntryDefaultFallback = { 75, 68 }; -constexpr uint8_t kTestTownAlphaSaveId = 7; -constexpr uint8_t kTestTownBetaSaveId = 11; -constexpr uint8_t kUnknownSaveId = 99; +constexpr uint8_t TestTownAlphaSaveId = 7; +constexpr uint8_t TestTownBetaSaveId = 11; +constexpr uint8_t UnknownSaveId = 99; -constexpr int kNonMatchingWarpFromLevel = 999; +constexpr int NonMatchingWarpFromLevel = 999; TEST(TownRegistry, HasTown_ReturnsFalseForUnregistered) { @@ -64,27 +64,27 @@ TEST(TownRegistry, GetTownBySaveId_FindsBySaveId) TownRegistry registry; TownConfig alpha; alpha.name = "Alpha"; - alpha.saveId = kTestTownAlphaSaveId; + alpha.saveId = TestTownAlphaSaveId; TownConfig beta; beta.name = "Beta"; - beta.saveId = kTestTownBetaSaveId; + beta.saveId = TestTownBetaSaveId; registry.RegisterTown("alpha", alpha); registry.RegisterTown("beta", beta); - EXPECT_EQ(registry.GetTownBySaveId(kTestTownAlphaSaveId), "alpha"); - EXPECT_EQ(registry.GetTownBySaveId(kTestTownBetaSaveId), "beta"); + EXPECT_EQ(registry.GetTownBySaveId(TestTownAlphaSaveId), "alpha"); + EXPECT_EQ(registry.GetTownBySaveId(TestTownBetaSaveId), "beta"); } TEST(TownRegistry, GetTownBySaveId_ReturnsTristramForUnknownNonZeroSaveId) { TownRegistry registry; - EXPECT_EQ(registry.GetTownBySaveId(kUnknownSaveId), TristramTownId); + EXPECT_EQ(registry.GetTownBySaveId(UnknownSaveId), TristramTownId); } TEST(TownRegistry, GetTownBySaveId_ReturnsTristramWhenZeroNotRegistered) { TownRegistry registry; TownConfig cfg; - cfg.saveId = kTestTownAlphaSaveId; + cfg.saveId = TestTownAlphaSaveId; registry.RegisterTown("only_alpha", cfg); EXPECT_EQ(registry.GetTownBySaveId(0), TristramTownId); } @@ -101,11 +101,11 @@ TEST(TownConfig, GetEntryPoint_ENTRY_MAIN) TEST(TownConfig, GetEntryPoint_ENTRY_TWARPUP_MatchesWarpFromLevel) { TownConfig cfg; - constexpr int kWarpFromLevel = 5; + constexpr int WarpFromLevel = 5; cfg.entries = { - { ENTRY_TWARPUP, { 49, 22 }, kWarpFromLevel }, + { ENTRY_TWARPUP, { 49, 22 }, WarpFromLevel }, }; - EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, kWarpFromLevel), Point(49, 22)); + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, WarpFromLevel), Point(49, 22)); } TEST(TownConfig, GetEntryPoint_ENTRY_TWARPUP_NonMatchingWarpUsesDefault) @@ -114,14 +114,14 @@ TEST(TownConfig, GetEntryPoint_ENTRY_TWARPUP_NonMatchingWarpUsesDefault) cfg.entries = { { ENTRY_TWARPUP, { 49, 22 }, 5 }, }; - EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, kNonMatchingWarpFromLevel), kTownEntryDefaultFallback); + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_TWARPUP, NonMatchingWarpFromLevel), TownEntryDefaultFallback); } TEST(TownConfig, GetEntryPoint_EmptyEntriesUsesDefault) { TownConfig cfg; cfg.entries.clear(); - EXPECT_EQ(cfg.GetEntryPoint(ENTRY_MAIN), kTownEntryDefaultFallback); + EXPECT_EQ(cfg.GetEntryPoint(ENTRY_MAIN), TownEntryDefaultFallback); } class InitializeTristramTest : public ::testing::Test { @@ -151,7 +151,7 @@ TEST_F(InitializeTristramTest, GetTownBySaveIdZeroReturnsTristram) TEST_F(InitializeTristramTest, TristramTriggerCount) { const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); - EXPECT_EQ(tristram.triggers.size(), kTristramTriggerCount); + EXPECT_EQ(tristram.triggers.size(), TristramTriggerCount); } TEST_F(InitializeTristramTest, TristramFirstTriggerIsCathedralStairs) @@ -159,20 +159,49 @@ TEST_F(InitializeTristramTest, TristramFirstTriggerIsCathedralStairs) const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); ASSERT_FALSE(tristram.triggers.empty()); const TownTrigger &first = tristram.triggers.front(); - EXPECT_EQ(first.position, kTristramCathedralTrigPosition); + EXPECT_EQ(first.position, TristramCathedralTrigPosition); EXPECT_EQ(first.msg, WM_DIABNEXTLVL); } TEST_F(InitializeTristramTest, TristramWarpClosedPatchCount) { const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); - EXPECT_EQ(tristram.warpClosedPatches.size(), kTristramWarpClosedPatchCount); + EXPECT_EQ(tristram.warpClosedPatches.size(), TristramWarpClosedPatchCount); } TEST_F(InitializeTristramTest, TristramEntryPointCount) { const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); - EXPECT_EQ(tristram.entries.size(), kTristramEntryPointCount); + EXPECT_EQ(tristram.entries.size(), TristramEntryPointCount); +} + +TEST_F(InitializeTristramTest, TristramPortalPositionsMatchLegacy) +{ + const TownConfig &tristram = GetTownRegistry().GetTown(TristramTownId); + for (size_t i = 0; i < NumTownPortalSlots; ++i) { + EXPECT_EQ(tristram.portalPositions[i], DefaultTristramPortalPositions[i]); + } +} + +TEST_F(InitializeTristramTest, GetPortalTownPositionMatchesTristramWhenCurrent) +{ + for (size_t i = 0; i < NumTownPortalSlots; ++i) { + EXPECT_EQ(GetPortalTownPosition(i), DefaultTristramPortalPositions[i]); + } +} + +TEST_F(InitializeTristramTest, GetPortalTownPositionUsesTownOverrides) +{ + TownConfig c; + c.name = "PortTest"; + c.saveId = 7; + c.portalPositions = DefaultTristramPortalPositions; + c.portalPositions[0] = { 99, 88 }; + GetTownRegistry().RegisterTown("porttest", c); + GetTownRegistry().SetCurrentTown("porttest"); + EXPECT_EQ(GetPortalTownPosition(0), Point(99, 88)); + EXPECT_EQ(GetPortalTownPosition(1), DefaultTristramPortalPositions[1]); + GetTownRegistry().SetCurrentTown(TristramTownId); } } // namespace From f9d48980e9b327e2a32543f2f613860a39559b49 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 12 Apr 2026 10:18:20 -0700 Subject: [PATCH 13/13] Revert line break changes --- Source/diablo.h | 254 +- Source/gamemenu.cpp | 782 +++--- Source/inv.cpp | 4598 ++++++++++++++++---------------- Source/lua/lua_global.hpp | 46 +- Source/utils/screen_reader.cpp | 110 +- Source/utils/screen_reader.hpp | 50 +- 6 files changed, 2920 insertions(+), 2920 deletions(-) diff --git a/Source/diablo.h b/Source/diablo.h index 7ac017c0e48..46d2586fd1c 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -1,127 +1,127 @@ -/** - * @file diablo.h - * - * Interface of the main game initialization functions. - */ -#pragma once - -#include - -#ifdef USE_SDL3 -#include -#include -#else -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif -#endif - -#ifdef _DEBUG -#include "tables/monstdat.h" -#endif -#include "levels/gendung.h" -#include "utils/attributes.h" -#include "utils/endian_read.hpp" - -namespace devilution { - -constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); -constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); -constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); -constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); -#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) - -#define NUMLEVELS 25 - -enum clicktype : int8_t { - CLICK_NONE, - CLICK_LEFT, - CLICK_RIGHT, -}; - -/** - * @brief Specifies what game logic step is currently executed - */ -enum class GameLogicStep : uint8_t { - None, - ProcessPlayers, - ProcessMonsters, - ProcessObjects, - ProcessMissiles, - ProcessItems, - ProcessTowners, - ProcessItemsTown, - ProcessMissilesTown, -}; - -enum class PlayerActionType : uint8_t { - None, - Walk, - Spell, - SpellMonsterTarget, - SpellPlayerTarget, - Attack, - AttackMonsterTarget, - AttackPlayerTarget, - OperateObject, -}; - -extern uint32_t DungeonSeeds[NUMLEVELS]; -extern DVL_API_FOR_TEST std::optional LevelSeeds[NUMLEVELS]; -extern DVL_API_FOR_TEST Point MousePosition; - -extern bool gbRunGameResult; -extern bool ReturnToMainMenu; -extern bool gbProcessPlayers; -extern DVL_API_FOR_TEST bool gbLoadGame; -extern bool cineflag; -/* These are defined in fonts.h */ -extern void FontsCleanup(); -extern DVL_API_FOR_TEST int PauseMode; -extern clicktype sgbMouseDown; -extern uint16_t gnTickDelay; -extern char gszProductName[64]; - -extern PlayerActionType LastPlayerAction; - -void InitKeymapActions(); -void SetCursorPos(Point position); -void FreeGameMem(); -bool StartGame(bool bNewGame, bool bSinglePlayer); -[[noreturn]] void diablo_quit(int exitStatus); -int DiabloMain(int argc, char **argv); -bool TryIconCurs(); -void diablo_pause_game(); -bool diablo_is_focused(); -void diablo_focus_pause(); -void diablo_focus_unpause(); -bool PressEscKey(); -void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); -tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); -bool IsDiabloAlive(bool playSFX); -void PrintScreen(SDL_Keycode vkey); - -/** - * @param bStartup Process additional ticks before returning - */ -bool game_loop(bool bStartup); -void diablo_color_cyc_logic(); - -/* rdata */ - -#ifdef _DEBUG -extern bool DebugDisableNetworkTimeout; -#endif - -/** - * @brief Specifies what game logic step is currently executed - */ -extern GameLogicStep gGameLogicStep; - -#ifdef __UWP__ -void setOnInitialized(void (*)()); -#endif - -} // namespace devilution +/** + * @file diablo.h + * + * Interface of the main game initialization functions. + */ +#pragma once + +#include + +#ifdef USE_SDL3 +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif + +#ifdef _DEBUG +#include "tables/monstdat.h" +#endif +#include "levels/gendung.h" +#include "utils/attributes.h" +#include "utils/endian_read.hpp" + +namespace devilution { + +constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); +constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); +constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); +constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); +#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) + +#define NUMLEVELS 25 + +enum clicktype : int8_t { + CLICK_NONE, + CLICK_LEFT, + CLICK_RIGHT, +}; + +/** + * @brief Specifies what game logic step is currently executed + */ +enum class GameLogicStep : uint8_t { + None, + ProcessPlayers, + ProcessMonsters, + ProcessObjects, + ProcessMissiles, + ProcessItems, + ProcessTowners, + ProcessItemsTown, + ProcessMissilesTown, +}; + +enum class PlayerActionType : uint8_t { + None, + Walk, + Spell, + SpellMonsterTarget, + SpellPlayerTarget, + Attack, + AttackMonsterTarget, + AttackPlayerTarget, + OperateObject, +}; + +extern uint32_t DungeonSeeds[NUMLEVELS]; +extern DVL_API_FOR_TEST std::optional LevelSeeds[NUMLEVELS]; +extern DVL_API_FOR_TEST Point MousePosition; + +extern bool gbRunGameResult; +extern bool ReturnToMainMenu; +extern bool gbProcessPlayers; +extern DVL_API_FOR_TEST bool gbLoadGame; +extern bool cineflag; +/* These are defined in fonts.h */ +extern void FontsCleanup(); +extern DVL_API_FOR_TEST int PauseMode; +extern clicktype sgbMouseDown; +extern uint16_t gnTickDelay; +extern char gszProductName[64]; + +extern PlayerActionType LastPlayerAction; + +void InitKeymapActions(); +void SetCursorPos(Point position); +void FreeGameMem(); +bool StartGame(bool bNewGame, bool bSinglePlayer); +[[noreturn]] void diablo_quit(int exitStatus); +int DiabloMain(int argc, char **argv); +bool TryIconCurs(); +void diablo_pause_game(); +bool diablo_is_focused(); +void diablo_focus_pause(); +void diablo_focus_unpause(); +bool PressEscKey(); +void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); +tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); +bool IsDiabloAlive(bool playSFX); +void PrintScreen(SDL_Keycode vkey); + +/** + * @param bStartup Process additional ticks before returning + */ +bool game_loop(bool bStartup); +void diablo_color_cyc_logic(); + +/* rdata */ + +#ifdef _DEBUG +extern bool DebugDisableNetworkTimeout; +#endif + +/** + * @brief Specifies what game logic step is currently executed + */ +extern GameLogicStep gGameLogicStep; + +#ifdef __UWP__ +void setOnInitialized(void (*)()); +#endif + +} // namespace devilution diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index cb7c1773bf9..f1f997c7556 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -1,391 +1,391 @@ -/** - * @file gamemenu.cpp - * - * Implementation of the in-game menu functions. - */ -#include "gamemenu.h" - -#ifdef USE_SDL3 -#include -#endif - -#include "cursor.h" -#include "diablo_msg.hpp" -#include "engine/backbuffer_state.hpp" -#include "engine/demomode.h" -#include "engine/events.hpp" -#include "engine/sound.h" -#include "engine/sound_defs.hpp" -#include "game_mode.hpp" -#include "gmenu.h" -#include "headless_mode.hpp" -#include "loadsave.h" -#include "multi.h" -#include "options.h" -#include "pfile.h" -#include "qol/floatingnumbers.h" -#include "utils/language.h" - -#ifndef USE_SDL1 -#include "controls/touch/renderers.h" -#endif - -namespace devilution { - -bool isGameMenuOpen = false; - -namespace { - -// Forward-declare menu handlers, used by the global menu structs below. -void GamemenuPrevious(bool bActivate); -void GamemenuNewGame(bool bActivate); -void GamemenuOptions(bool bActivate); -void GamemenuMusicVolume(bool bActivate); -void GamemenuSoundVolume(bool bActivate); -void GamemenuBrightness(bool bActivate); -void GamemenuSpeed(bool bActivate); - -/** Contains the game menu items of the single player menu. */ -TMenuItem sgSingleMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, - { GMENU_ENABLED, N_("Save Game"), &gamemenu_save_game }, - { GMENU_ENABLED, N_("Load Game"), &gamemenu_load_game }, - { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, - { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, - { GMENU_ENABLED, nullptr, nullptr }, - // clang-format on -}; -/** Contains the game menu items of the multi player menu. */ -TMenuItem sgMultiMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, - { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, - { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, - { GMENU_ENABLED, nullptr, nullptr }, - // clang-format on -}; -TMenuItem sgOptionsMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, - { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, - { GMENU_ENABLED , nullptr, nullptr }, - // clang-format on -}; -/** Specifies the menu names for music enabled and disabled. */ -const char *const MusicToggleNames[] = { - N_("Music"), - N_("Music Disabled"), -}; -/** Specifies the menu names for sound enabled and disabled. */ -const char *const SoundToggleNames[] = { - N_("Sound"), - N_("Sound Disabled"), -}; - -void GamemenuUpdateSingle() -{ - sgSingleMenu[2].setEnabled(gbValidSaveFile); - - const bool enable = MyPlayer->_pmode != PM_DEATH && !MyPlayerIsDead; - - sgSingleMenu[0].setEnabled(enable); -} - -void GamemenuPrevious(bool /*bActivate*/) -{ - gamemenu_on(); -} - -void GamemenuNewGame(bool /*bActivate*/) -{ - for (Player &player : Players) { - player._pmode = PM_QUIT; - player._pInvincible = true; - } - - MyPlayerIsDead = false; - if (!HeadlessMode) { - RedrawEverything(); - scrollrt_draw_game_screen(); - } - CornerStone.activated = false; - gbRunGame = false; - gamemenu_off(); -} - -void GamemenuSoundMusicToggle(const char *const *names, TMenuItem *menuItem, int volume) -{ - if (gbSndInited) { - menuItem->addFlags(GMENU_ENABLED | GMENU_SLIDER); - menuItem->pszStr = names[0]; - gmenu_slider_steps(menuItem, VOLUME_STEPS); - gmenu_slider_set(menuItem, VOLUME_MIN, VOLUME_MAX, volume); - return; - } - - menuItem->removeFlags(GMENU_ENABLED | GMENU_SLIDER); - menuItem->pszStr = names[1]; -} - -int GamemenuSliderMusicSound(TMenuItem *menuItem) -{ - return gmenu_slider_get(menuItem, VOLUME_MIN, VOLUME_MAX); -} - -void GamemenuGetMusic() -{ - GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); -} - -void GamemenuGetSound() -{ - GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); -} - -void GamemenuGetBrightness() -{ - gmenu_slider_steps(&sgOptionsMenu[2], 21); - gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); -} - -void GamemenuGetSpeed() -{ - if (gbIsMultiplayer) { - sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); - if (sgGameInitInfo.nTickRate >= 50) - sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); - else if (sgGameInitInfo.nTickRate >= 40) - sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); - else if (sgGameInitInfo.nTickRate >= 30) - sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); - else if (sgGameInitInfo.nTickRate == 20) - sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); - return; - } - - sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); - - sgOptionsMenu[3].pszStr = _("Speed").data(); - gmenu_slider_steps(&sgOptionsMenu[3], 46); - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); -} - -int GamemenuSliderBrightness() -{ - return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); -} - -void GamemenuOptions(bool /*bActivate*/) -{ - GamemenuGetMusic(); - GamemenuGetSound(); - GamemenuGetBrightness(); - GamemenuGetSpeed(); - gmenu_set_items(sgOptionsMenu, nullptr); -} - -void GamemenuMusicVolume(bool bActivate) -{ - if (bActivate) { - if (gbMusicOn) { - gbMusicOn = false; - music_stop(); - sound_get_or_set_music_volume(VOLUME_MIN); - } else { - gbMusicOn = true; - sound_get_or_set_music_volume(VOLUME_MAX); - music_start(GetLevelMusic(leveltype)); - } - } else { - const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[0]); - sound_get_or_set_music_volume(volume); - if (volume == VOLUME_MIN) { - if (gbMusicOn) { - gbMusicOn = false; - music_stop(); - } - } else if (!gbMusicOn) { - gbMusicOn = true; - music_start(GetLevelMusic(leveltype)); - } - } - - GamemenuGetMusic(); -} - -void GamemenuSoundVolume(bool bActivate) -{ - if (bActivate) { - if (gbSoundOn) { - gbSoundOn = false; - sound_stop(); - sound_get_or_set_sound_volume(VOLUME_MIN); - } else { - gbSoundOn = true; - sound_get_or_set_sound_volume(VOLUME_MAX); - } - } else { - const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[1]); - sound_get_or_set_sound_volume(volume); - if (volume == VOLUME_MIN) { - if (gbSoundOn) { - gbSoundOn = false; - sound_stop(); - } - } else if (!gbSoundOn) { - gbSoundOn = true; - } - } - PlaySFX(SfxID::MenuMove); - GamemenuGetSound(); -} - -void GamemenuBrightness(bool bActivate) -{ - int brightness; - if (bActivate) { - brightness = UpdateBrightness(-1); - brightness = (brightness == 0) ? 100 : 0; - } else { - brightness = GamemenuSliderBrightness(); - } - - UpdateBrightness(brightness); - GamemenuGetBrightness(); -} - -void GamemenuSpeed(bool bActivate) -{ - if (bActivate) { - if (sgGameInitInfo.nTickRate != 20) - sgGameInitInfo.nTickRate = 20; - else - sgGameInitInfo.nTickRate = 50; - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); - } else { - sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); - } - - GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); - gnTickDelay = 1000 / sgGameInitInfo.nTickRate; -} - -} // namespace - -void gamemenu_exit_game(bool bActivate) -{ - GamemenuNewGame(bActivate); -} - -void gamemenu_quit_game(bool bActivate) -{ - GamemenuNewGame(bActivate); -#ifndef NOEXIT - gbRunGameResult = false; -#else - ReturnToMainMenu = true; -#endif -} - -void gamemenu_load_game(bool /*bActivate*/) -{ - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); - gamemenu_off(); - ClearFloatingNumbers(); - NewCursor(CURSOR_NONE); - InitDiabloMsg(EMSG_LOADING); - RedrawEverything(); - DrawAndBlit(); - - const std::array prevPalette = logical_palette; -#ifndef USE_SDL1 - DeactivateVirtualGamepad(); - FreeVirtualGamepadTextures(); -#endif - if (tl::expected result = LoadGame(false); !result.has_value()) { - app_fatal(result.error()); - } -#if !defined(USE_SDL1) && !defined(__vita__) - if (renderer != nullptr) { - InitVirtualGamepadTextures(*renderer); - } -#endif - ClrDiabloMsg(); - PaletteFadeOut(8, prevPalette); - - LoadPWaterPalette(); - NewCursor(CURSOR_HAND); - CornerStone.activated = false; - MyPlayerIsDead = false; - RedrawEverything(); - DrawAndBlit(); - PaletteFadeIn(8); - NewCursor(CURSOR_HAND); - interface_msg_pump(); - SetEventHandler(saveProc); -} - -void gamemenu_save_game(bool /*bActivate*/) -{ - if (pcurs != CURSOR_HAND) { - return; - } - - if (MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead) { - gamemenu_off(); - return; - } - - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); - NewCursor(CURSOR_NONE); - gamemenu_off(); - InitDiabloMsg(EMSG_SAVING); - RedrawEverything(); - DrawAndBlit(); - const uint32_t currentTime = SDL_GetTicks(); - SaveGame(); - ClrDiabloMsg(); - InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks()); - RedrawEverything(); - NewCursor(CURSOR_HAND); - if (CornerStone.activated) { - CornerstoneSave(); - if (!demo::IsRunning()) SaveOptions(); - } - interface_msg_pump(); - SetEventHandler(saveProc); -} - -void gamemenu_on() -{ - isGameMenuOpen = true; - if (!gbIsMultiplayer) { - gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); - } else { - gmenu_set_items(sgMultiMenu, nullptr); - } - PressEscKey(); -} - -void gamemenu_off() -{ - isGameMenuOpen = false; - gmenu_set_items(nullptr, nullptr); -} - -void gamemenu_handle_previous() -{ - if (gmenu_is_active()) - gamemenu_off(); - else - gamemenu_on(); -} - -} // namespace devilution +/** + * @file gamemenu.cpp + * + * Implementation of the in-game menu functions. + */ +#include "gamemenu.h" + +#ifdef USE_SDL3 +#include +#endif + +#include "cursor.h" +#include "diablo_msg.hpp" +#include "engine/backbuffer_state.hpp" +#include "engine/demomode.h" +#include "engine/events.hpp" +#include "engine/sound.h" +#include "engine/sound_defs.hpp" +#include "game_mode.hpp" +#include "gmenu.h" +#include "headless_mode.hpp" +#include "loadsave.h" +#include "multi.h" +#include "options.h" +#include "pfile.h" +#include "qol/floatingnumbers.h" +#include "utils/language.h" + +#ifndef USE_SDL1 +#include "controls/touch/renderers.h" +#endif + +namespace devilution { + +bool isGameMenuOpen = false; + +namespace { + +// Forward-declare menu handlers, used by the global menu structs below. +void GamemenuPrevious(bool bActivate); +void GamemenuNewGame(bool bActivate); +void GamemenuOptions(bool bActivate); +void GamemenuMusicVolume(bool bActivate); +void GamemenuSoundVolume(bool bActivate); +void GamemenuBrightness(bool bActivate); +void GamemenuSpeed(bool bActivate); + +/** Contains the game menu items of the single player menu. */ +TMenuItem sgSingleMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, + { GMENU_ENABLED, N_("Save Game"), &gamemenu_save_game }, + { GMENU_ENABLED, N_("Load Game"), &gamemenu_load_game }, + { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, + { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, + { GMENU_ENABLED, nullptr, nullptr }, + // clang-format on +}; +/** Contains the game menu items of the multi player menu. */ +TMenuItem sgMultiMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, + { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, + { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, + { GMENU_ENABLED, nullptr, nullptr }, + // clang-format on +}; +TMenuItem sgOptionsMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, + { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, + { GMENU_ENABLED , nullptr, nullptr }, + // clang-format on +}; +/** Specifies the menu names for music enabled and disabled. */ +const char *const MusicToggleNames[] = { + N_("Music"), + N_("Music Disabled"), +}; +/** Specifies the menu names for sound enabled and disabled. */ +const char *const SoundToggleNames[] = { + N_("Sound"), + N_("Sound Disabled"), +}; + +void GamemenuUpdateSingle() +{ + sgSingleMenu[2].setEnabled(gbValidSaveFile); + + const bool enable = MyPlayer->_pmode != PM_DEATH && !MyPlayerIsDead; + + sgSingleMenu[0].setEnabled(enable); +} + +void GamemenuPrevious(bool /*bActivate*/) +{ + gamemenu_on(); +} + +void GamemenuNewGame(bool /*bActivate*/) +{ + for (Player &player : Players) { + player._pmode = PM_QUIT; + player._pInvincible = true; + } + + MyPlayerIsDead = false; + if (!HeadlessMode) { + RedrawEverything(); + scrollrt_draw_game_screen(); + } + CornerStone.activated = false; + gbRunGame = false; + gamemenu_off(); +} + +void GamemenuSoundMusicToggle(const char *const *names, TMenuItem *menuItem, int volume) +{ + if (gbSndInited) { + menuItem->addFlags(GMENU_ENABLED | GMENU_SLIDER); + menuItem->pszStr = names[0]; + gmenu_slider_steps(menuItem, VOLUME_STEPS); + gmenu_slider_set(menuItem, VOLUME_MIN, VOLUME_MAX, volume); + return; + } + + menuItem->removeFlags(GMENU_ENABLED | GMENU_SLIDER); + menuItem->pszStr = names[1]; +} + +int GamemenuSliderMusicSound(TMenuItem *menuItem) +{ + return gmenu_slider_get(menuItem, VOLUME_MIN, VOLUME_MAX); +} + +void GamemenuGetMusic() +{ + GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); +} + +void GamemenuGetSound() +{ + GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); +} + +void GamemenuGetBrightness() +{ + gmenu_slider_steps(&sgOptionsMenu[2], 21); + gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); +} + +void GamemenuGetSpeed() +{ + if (gbIsMultiplayer) { + sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); + if (sgGameInitInfo.nTickRate >= 50) + sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); + else if (sgGameInitInfo.nTickRate >= 40) + sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); + else if (sgGameInitInfo.nTickRate >= 30) + sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); + else if (sgGameInitInfo.nTickRate == 20) + sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); + return; + } + + sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); + + sgOptionsMenu[3].pszStr = _("Speed").data(); + gmenu_slider_steps(&sgOptionsMenu[3], 46); + gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); +} + +int GamemenuSliderBrightness() +{ + return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); +} + +void GamemenuOptions(bool /*bActivate*/) +{ + GamemenuGetMusic(); + GamemenuGetSound(); + GamemenuGetBrightness(); + GamemenuGetSpeed(); + gmenu_set_items(sgOptionsMenu, nullptr); +} + +void GamemenuMusicVolume(bool bActivate) +{ + if (bActivate) { + if (gbMusicOn) { + gbMusicOn = false; + music_stop(); + sound_get_or_set_music_volume(VOLUME_MIN); + } else { + gbMusicOn = true; + sound_get_or_set_music_volume(VOLUME_MAX); + music_start(GetLevelMusic(leveltype)); + } + } else { + const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[0]); + sound_get_or_set_music_volume(volume); + if (volume == VOLUME_MIN) { + if (gbMusicOn) { + gbMusicOn = false; + music_stop(); + } + } else if (!gbMusicOn) { + gbMusicOn = true; + music_start(GetLevelMusic(leveltype)); + } + } + + GamemenuGetMusic(); +} + +void GamemenuSoundVolume(bool bActivate) +{ + if (bActivate) { + if (gbSoundOn) { + gbSoundOn = false; + sound_stop(); + sound_get_or_set_sound_volume(VOLUME_MIN); + } else { + gbSoundOn = true; + sound_get_or_set_sound_volume(VOLUME_MAX); + } + } else { + const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[1]); + sound_get_or_set_sound_volume(volume); + if (volume == VOLUME_MIN) { + if (gbSoundOn) { + gbSoundOn = false; + sound_stop(); + } + } else if (!gbSoundOn) { + gbSoundOn = true; + } + } + PlaySFX(SfxID::MenuMove); + GamemenuGetSound(); +} + +void GamemenuBrightness(bool bActivate) +{ + int brightness; + if (bActivate) { + brightness = UpdateBrightness(-1); + brightness = (brightness == 0) ? 100 : 0; + } else { + brightness = GamemenuSliderBrightness(); + } + + UpdateBrightness(brightness); + GamemenuGetBrightness(); +} + +void GamemenuSpeed(bool bActivate) +{ + if (bActivate) { + if (sgGameInitInfo.nTickRate != 20) + sgGameInitInfo.nTickRate = 20; + else + sgGameInitInfo.nTickRate = 50; + gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); + } else { + sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); + } + + GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); + gnTickDelay = 1000 / sgGameInitInfo.nTickRate; +} + +} // namespace + +void gamemenu_exit_game(bool bActivate) +{ + GamemenuNewGame(bActivate); +} + +void gamemenu_quit_game(bool bActivate) +{ + GamemenuNewGame(bActivate); +#ifndef NOEXIT + gbRunGameResult = false; +#else + ReturnToMainMenu = true; +#endif +} + +void gamemenu_load_game(bool /*bActivate*/) +{ + EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + gamemenu_off(); + ClearFloatingNumbers(); + NewCursor(CURSOR_NONE); + InitDiabloMsg(EMSG_LOADING); + RedrawEverything(); + DrawAndBlit(); + + const std::array prevPalette = logical_palette; +#ifndef USE_SDL1 + DeactivateVirtualGamepad(); + FreeVirtualGamepadTextures(); +#endif + if (tl::expected result = LoadGame(false); !result.has_value()) { + app_fatal(result.error()); + } +#if !defined(USE_SDL1) && !defined(__vita__) + if (renderer != nullptr) { + InitVirtualGamepadTextures(*renderer); + } +#endif + ClrDiabloMsg(); + PaletteFadeOut(8, prevPalette); + + LoadPWaterPalette(); + NewCursor(CURSOR_HAND); + CornerStone.activated = false; + MyPlayerIsDead = false; + RedrawEverything(); + DrawAndBlit(); + PaletteFadeIn(8); + NewCursor(CURSOR_HAND); + interface_msg_pump(); + SetEventHandler(saveProc); +} + +void gamemenu_save_game(bool /*bActivate*/) +{ + if (pcurs != CURSOR_HAND) { + return; + } + + if (MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead) { + gamemenu_off(); + return; + } + + EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + NewCursor(CURSOR_NONE); + gamemenu_off(); + InitDiabloMsg(EMSG_SAVING); + RedrawEverything(); + DrawAndBlit(); + const uint32_t currentTime = SDL_GetTicks(); + SaveGame(); + ClrDiabloMsg(); + InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks()); + RedrawEverything(); + NewCursor(CURSOR_HAND); + if (CornerStone.activated) { + CornerstoneSave(); + if (!demo::IsRunning()) SaveOptions(); + } + interface_msg_pump(); + SetEventHandler(saveProc); +} + +void gamemenu_on() +{ + isGameMenuOpen = true; + if (!gbIsMultiplayer) { + gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); + } else { + gmenu_set_items(sgMultiMenu, nullptr); + } + PressEscKey(); +} + +void gamemenu_off() +{ + isGameMenuOpen = false; + gmenu_set_items(nullptr, nullptr); +} + +void gamemenu_handle_previous() +{ + if (gmenu_is_active()) + gamemenu_off(); + else + gamemenu_on(); +} + +} // namespace devilution diff --git a/Source/inv.cpp b/Source/inv.cpp index 7fd3f6e68cb..5dae0e53216 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -1,2299 +1,2299 @@ -/** - * @file inv.cpp - * - * Implementation of player inventory. - */ -#include -#include -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include -#else -#include -#endif - -#include - -#include "DiabloUI/ui_flags.hpp" -#include "controls/control_mode.hpp" -#include "controls/plrctrls.h" -#include "cursor.h" -#include "engine/backbuffer_state.hpp" -#include "engine/clx_sprite.hpp" -#include "engine/load_cel.hpp" -#include "engine/palette.h" -#include "engine/render/clx_render.hpp" -#include "engine/render/text_render.hpp" -#include "engine/size.hpp" -#include "hwcursor.hpp" -#include "inv_iterators.hpp" -#include "levels/tile_properties.hpp" -#include "levels/town.h" -#include "minitext.h" -#include "options.h" -#include "panels/ui_panels.hpp" -#include "player.h" -#include "plrmsg.h" -#include "qol/stash.h" -#include "qol/visual_store.h" -#include "stores.h" -#include "towners.h" -#include "utils/display.h" -#include "utils/format_int.hpp" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/sdl_geometry.h" -#include "utils/str_cat.hpp" -#include "utils/utf8.hpp" - -namespace devilution { - -bool invflag; - -/** - * Maps from inventory slot to screen position. The inventory slots are - * arranged as follows: - * - * @code{.unparsed} - * 00 00 - * 00 00 03 - * - * 04 04 06 06 05 05 - * 04 04 06 06 05 05 - * 04 04 06 06 05 05 - * - * 01 02 - * - * 07 08 09 10 11 12 13 14 15 16 - * 17 18 19 20 21 22 23 24 25 26 - * 27 28 29 30 31 32 33 34 35 36 - * 37 38 39 40 41 42 43 44 45 46 - * - * 47 48 49 50 51 52 53 54 - * @endcode - */ -const Rectangle InvRect[] = { - // clang-format off - //{ X, Y }, { W, H } - { { 132, 2 }, { 58, 59 } }, // helmet - { { 47, 177 }, { 28, 29 } }, // left ring - { { 248, 177 }, { 28, 29 } }, // right ring - { { 205, 32 }, { 28, 29 } }, // amulet - { { 17, 75 }, { 58, 86 } }, // left hand - { { 248, 75 }, { 58, 87 } }, // right hand - { { 132, 75 }, { 58, 87 } }, // chest - { { 17, 222 }, { 29, 29 } }, // inv row 1 - { { 46, 222 }, { 29, 29 } }, // inv row 1 - { { 75, 222 }, { 29, 29 } }, // inv row 1 - { { 104, 222 }, { 29, 29 } }, // inv row 1 - { { 133, 222 }, { 29, 29 } }, // inv row 1 - { { 162, 222 }, { 29, 29 } }, // inv row 1 - { { 191, 222 }, { 29, 29 } }, // inv row 1 - { { 220, 222 }, { 29, 29 } }, // inv row 1 - { { 249, 222 }, { 29, 29 } }, // inv row 1 - { { 278, 222 }, { 29, 29 } }, // inv row 1 - { { 17, 251 }, { 29, 29 } }, // inv row 2 - { { 46, 251 }, { 29, 29 } }, // inv row 2 - { { 75, 251 }, { 29, 29 } }, // inv row 2 - { { 104, 251 }, { 29, 29 } }, // inv row 2 - { { 133, 251 }, { 29, 29 } }, // inv row 2 - { { 162, 251 }, { 29, 29 } }, // inv row 2 - { { 191, 251 }, { 29, 29 } }, // inv row 2 - { { 220, 251 }, { 29, 29 } }, // inv row 2 - { { 249, 251 }, { 29, 29 } }, // inv row 2 - { { 278, 251 }, { 29, 29 } }, // inv row 2 - { { 17, 280 }, { 29, 29 } }, // inv row 3 - { { 46, 280 }, { 29, 29 } }, // inv row 3 - { { 75, 280 }, { 29, 29 } }, // inv row 3 - { { 104, 280 }, { 29, 29 } }, // inv row 3 - { { 133, 280 }, { 29, 29 } }, // inv row 3 - { { 162, 280 }, { 29, 29 } }, // inv row 3 - { { 191, 280 }, { 29, 29 } }, // inv row 3 - { { 220, 280 }, { 29, 29 } }, // inv row 3 - { { 249, 280 }, { 29, 29 } }, // inv row 3 - { { 278, 280 }, { 29, 29 } }, // inv row 3 - { { 17, 309 }, { 29, 29 } }, // inv row 4 - { { 46, 309 }, { 29, 29 } }, // inv row 4 - { { 75, 309 }, { 29, 29 } }, // inv row 4 - { { 104, 309 }, { 29, 29 } }, // inv row 4 - { { 133, 309 }, { 29, 29 } }, // inv row 4 - { { 162, 309 }, { 29, 29 } }, // inv row 4 - { { 191, 309 }, { 29, 29 } }, // inv row 4 - { { 220, 309 }, { 29, 29 } }, // inv row 4 - { { 249, 309 }, { 29, 29 } }, // inv row 4 - { { 278, 309 }, { 29, 29 } }, // inv row 4 - { { 205, 5 }, { 29, 29 } }, // belt - { { 234, 5 }, { 29, 29 } }, // belt - { { 263, 5 }, { 29, 29 } }, // belt - { { 292, 5 }, { 29, 29 } }, // belt - { { 321, 5 }, { 29, 29 } }, // belt - { { 350, 5 }, { 29, 29 } }, // belt - { { 379, 5 }, { 29, 29 } }, // belt - { { 408, 5 }, { 29, 29 } } // belt - // clang-format on -}; - -namespace { - -OptionalOwnedClxSpriteList pInvCels; - -/** - * @brief Adds an item to a player's InvGrid array - * @param player The player reference - * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) - * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) - * @param itemSize Size of item - */ -void AddItemToInvGrid(Player &player, int invGridIndex, int invListIndex, Size itemSize, bool sendNetworkMessage) -{ - const int pitch = 10; - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - if (x == 0 && y == itemSize.height - 1) - player.InvGrid[rowGridIndex + x] = invListIndex; - else - player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell. - } - } - - if (sendNetworkMessage) { - NetSendCmdChInvItem(false, invGridIndex); - } -} - -/** - * @brief Checks whether the given item can fit in a belt slot (i.e. the item's size in inventory cells is 1x1). - * @param item The item to be checked. - * @return 'True' in case the item can fit a belt slot and 'False' otherwise. - */ -bool FitsInBeltSlot(const Item &item) -{ - return GetInventorySize(item) == Size { 1, 1 }; -} - -/** - * @brief Checks whether the given item can be equipped. Since this overload doesn't take player information, it only considers - * general aspects about the item, like if its requirements are met and if the item's target location is valid for the body. - * @param item The item to check. - * @return 'True' in case the item could be equipped in a player, and 'False' otherwise. - */ -bool CanEquip(const Item &item) -{ - return item.isEquipment() - && item._iStatFlag; -} - -/** - * @brief A specialized version of 'CanEquip(int, Item&, int)' that specifically checks whether the item can be equipped - * in one/both of the player's hands. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to check. - * @return 'True' if the player can currently equip the item in either one of his hands (i.e. the required hands are empty and - * allow the item), and 'False' otherwise. - */ -bool CanWield(Player &player, const Item &item) -{ - if (!CanEquip(item) || IsNoneOf(player.GetItemLocation(item), ILOC_ONEHAND, ILOC_TWOHAND)) - return false; - - const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; - const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; - - if (leftHandItem.isEmpty() && rightHandItem.isEmpty()) { - return true; - } - - if (!leftHandItem.isEmpty() && !rightHandItem.isEmpty()) { - return false; - } - - const Item &occupiedHand = !leftHandItem.isEmpty() ? leftHandItem : rightHandItem; - - // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied - // slot is another one-handed weapon. - const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); - if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { - const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND - && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); - - const bool weaponToEquipIsOneHandedSwordOrMace = player.GetItemLocation(item) == ILOC_ONEHAND - && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace); - - if (occupiedHandIsOneHandedSwordOrMace && weaponToEquipIsOneHandedSwordOrMace) { - return true; - } - } - - return player.GetItemLocation(item) == ILOC_ONEHAND - && player.GetItemLocation(occupiedHand) == ILOC_ONEHAND - && item._iClass != occupiedHand._iClass; -} - -/** - * @brief Checks whether the specified item can be equipped in the desired body location on the player. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to check. - * @param bodyLocation The location in the inventory to be checked against. - * @return 'True' if the player can currently equip the item in the specified body location (i.e. the body location is empty and - * allows the item), and 'False' otherwise. - */ -bool CanEquip(Player &player, const Item &item, inv_body_loc bodyLocation) -{ - if (!CanEquip(item) || player._pmode > PM_WALK_SIDEWAYS || !player.InvBody[bodyLocation].isEmpty()) { - return false; - } - - switch (bodyLocation) { - case INVLOC_AMULET: - return item._iLoc == ILOC_AMULET; - - case INVLOC_CHEST: - return item._iLoc == ILOC_ARMOR; - - case INVLOC_HAND_LEFT: - case INVLOC_HAND_RIGHT: - return CanWield(player, item); - - case INVLOC_HEAD: - return item._iLoc == ILOC_HELM; - - case INVLOC_RING_LEFT: - case INVLOC_RING_RIGHT: - return item._iLoc == ILOC_RING; - - default: - return false; - } -} - -void ChangeEquipment(Player &player, inv_body_loc bodyLocation, const Item &item, bool sendNetworkMessage) -{ - player.InvBody[bodyLocation] = item; - - if (sendNetworkMessage) { - NetSendCmdChItem(false, bodyLocation, true); - } -} - -bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool persistItem, bool sendNetworkMessage) -{ - if (!CanEquip(player, item, bodyLocation)) { - return false; - } - - if (persistItem) { - ChangeEquipment(player, bodyLocation, item, sendNetworkMessage); - - if (sendNetworkMessage && *GetOptions().Audio.autoEquipSound) { - PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); - } - - CalcPlrInv(player, true); - } - - return true; -} - -int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) -{ - Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; - for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) - return r; - } - for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) { - // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. - if (itemSize.height <= 1 && itemSize.width <= 1) { - // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do - return r; - } - // Otherwise work out how far the central cell is from the top-left cell - Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; - // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. - if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { - // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left - hotPixelCellOffset.deltaX++; - } - if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { - // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above - hotPixelCellOffset.deltaY++; - } - // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) - const int hotPixelCell = r - SLOTXY_INV_FIRST; - const int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); - const int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); - return SLOTXY_INV_FIRST + (targetRow * InventorySizeInSlots.width) + targetColumn; - } - } - - panelOffset = Point { 0, 0 } - GetMainPanel().position; - for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { - if (InvRect[r].contains(cursorPosition + panelOffset)) - return r; - } - return NUM_XY_SLOTS; -} - -void ChangeBodyEquipment(Player &player, int slot, item_equip_type location) -{ - const inv_body_loc bodyLocation = [&slot](item_equip_type location) { - switch (location) { - case ILOC_HELM: - return INVLOC_HEAD; - case ILOC_RING: - return (slot == SLOTXY_RING_LEFT ? INVLOC_RING_LEFT : INVLOC_RING_RIGHT); - case ILOC_AMULET: - return INVLOC_AMULET; - case ILOC_ARMOR: - return INVLOC_CHEST; - default: - app_fatal("Unexpected equipment type"); - } - }(location); - const Item previouslyEquippedItem = player.InvBody[slot]; - ChangeEquipment(player, bodyLocation, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } -} - -void ChangeEquippedItem(Player &player, uint8_t slot) -{ - const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; - const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; - - const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); - - const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) - || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); - - const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); - - const inv_body_loc pasteHand = pasteIntoSelectedHand ? selectedHand : otherHand; - const Item previouslyEquippedItem = dequipTwoHandedWeapon ? player.InvBody[otherHand] : player.InvBody[pasteHand]; - if (dequipTwoHandedWeapon) { - RemoveEquipment(player, otherHand, false); - } - ChangeEquipment(player, pasteHand, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } -} - -void ChangeTwoHandItem(Player &player) -{ - if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && !player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - inv_body_loc locationToUnequip = INVLOC_HAND_LEFT; - if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { - locationToUnequip = INVLOC_HAND_RIGHT; - } - if (!AutoPlaceItemInInventory(player, player.InvBody[locationToUnequip])) { - return; - } - - if (locationToUnequip == INVLOC_HAND_RIGHT) { - RemoveEquipment(player, INVLOC_HAND_RIGHT, false); - } else { - player.InvBody[INVLOC_HAND_LEFT].clear(); - } - } - - if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_LEFT]; - ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem.pop(), &player == MyPlayer); - if (!previouslyEquippedItem.isEmpty()) { - player.HoldItem = previouslyEquippedItem; - } - } else { - const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_RIGHT]; - RemoveEquipment(player, INVLOC_HAND_RIGHT, false); - ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem, &player == MyPlayer); - player.HoldItem = previouslyEquippedItem; - } -} - -int8_t CheckOverlappingItems(int slot, const Player &player, Size itemSize) -{ - // check that the item we're pasting only overlaps one other item (or is going into empty space) - const auto originCell = static_cast(slot - SLOTXY_INV_FIRST); - - int8_t overlappingId = 0; - for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { - - for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { - const unsigned testCell = originCell + rowOffset + columnOffset; - // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. - assert(testCell < sizeof(player.InvGrid)); - if (player.InvGrid[testCell] != 0) { - const int8_t iv = std::abs(player.InvGrid[testCell]); - if (overlappingId != 0) { - if (overlappingId != iv) { - // Found two different items that would be displaced by the held item, can't paste the item here. - return -1; - } - } else { - overlappingId = iv; - } - } - } - } - - return overlappingId; -} - -int8_t GetPrevItemId(int slot, const Player &player, const Size &itemSize) -{ - if (player.HoldItem._itype != ItemType::Gold) - return CheckOverlappingItems(slot, player, itemSize); - const int8_t item_cell_begin = player.InvGrid[slot - SLOTXY_INV_FIRST]; - if (item_cell_begin == 0) - return 0; - if (item_cell_begin <= 0) - return -item_cell_begin; - if (player.InvList[item_cell_begin - 1]._itype != ItemType::Gold) - return item_cell_begin; - return 0; -} - -bool ChangeInvItem(Player &player, int slot, Size itemSize) -{ - int8_t prevItemId = GetPrevItemId(slot, player, itemSize); - if (prevItemId < 0) return false; - - if (player.HoldItem._itype == ItemType::Gold && prevItemId == 0) { - const int ii = slot - SLOTXY_INV_FIRST; - if (player.InvGrid[ii] > 0) { - const int invIndex = player.InvGrid[ii] - 1; - const int gt = player.InvList[invIndex]._ivalue; - int ig = player.HoldItem._ivalue + gt; - if (ig <= MaxGold) { - player.InvList[invIndex]._ivalue = ig; - SetPlrHandGoldCurs(player.InvList[invIndex]); - player._pGold += player.HoldItem._ivalue; - player.HoldItem.clear(); - } else { - ig = MaxGold - gt; - player._pGold += ig; - player.HoldItem._ivalue -= ig; - SetPlrHandGoldCurs(player.HoldItem); - player.InvList[invIndex]._ivalue = MaxGold; - player.InvList[invIndex]._iCurs = ICURS_GOLD_LARGE; - } - } else { - const int invIndex = player._pNumInv; - player._pGold += player.HoldItem._ivalue; - player.InvList[invIndex] = player.HoldItem.pop(); - player._pNumInv++; - player.InvGrid[ii] = player._pNumInv; - } - if (&player == MyPlayer) { - NetSendCmdChInvItem(false, ii); - } - } else { - if (prevItemId == 0) { - player.InvList[player._pNumInv] = player.HoldItem.pop(); - player._pNumInv++; - prevItemId = player._pNumInv; - } else { - const int invIndex = prevItemId - 1; - if (player.HoldItem._itype == ItemType::Gold) - player._pGold += player.HoldItem._ivalue; - std::swap(player.InvList[invIndex], player.HoldItem); - if (player.HoldItem._itype == ItemType::Gold) - player._pGold = CalculateGold(player); - for (int8_t &itemIndex : player.InvGrid) { - if (itemIndex == prevItemId) - itemIndex = 0; - if (itemIndex == -prevItemId) - itemIndex = 0; - } - } - - AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); - } - - return true; -} - -void ChangeBeltItem(Player &player, int slot) -{ - const int ii = slot - SLOTXY_BELT_FIRST; - if (player.SpdList[ii].isEmpty()) { - player.SpdList[ii] = player.HoldItem.pop(); - } else { - std::swap(player.SpdList[ii], player.HoldItem); - - if (player.HoldItem._itype == ItemType::Gold) - player._pGold = CalculateGold(player); - } - if (&player == MyPlayer) { - NetSendCmdChBeltItem(false, ii); - } - RedrawComponent(PanelDrawComponent::Belt); -} - -item_equip_type GetItemEquipType(int slot, item_equip_type desiredLocation) -{ - if (slot == SLOTXY_HEAD) - return ILOC_HELM; - if (slot == SLOTXY_RING_LEFT || slot == SLOTXY_RING_RIGHT) - return ILOC_RING; - if (slot == SLOTXY_AMULET) - return ILOC_AMULET; - if (slot == SLOTXY_HAND_LEFT || slot == SLOTXY_HAND_RIGHT) { - if (desiredLocation == ILOC_TWOHAND) - return ILOC_TWOHAND; - return ILOC_ONEHAND; - } - if (slot == SLOTXY_CHEST) - return ILOC_ARMOR; - if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) - return ILOC_BELT; - - return ILOC_UNEQUIPABLE; -} - -void CheckInvPaste(Player &player, Point cursorPosition) -{ - const Size itemSize = GetInventorySize(player.HoldItem); - - const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); - if (slot == NUM_XY_SLOTS) - return; - - const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); - const item_equip_type location = GetItemEquipType(slot, desiredLocation); - - if (location == ILOC_BELT) { - if (!CanBePlacedOnBelt(player, player.HoldItem)) return; - } else if (location != ILOC_UNEQUIPABLE) { - if (desiredLocation != location) return; - } - - if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { - if (!player.CanUseItem(player.HoldItem)) { - player.Say(HeroSpeech::ICantUseThisYet); - return; - } - if (player._pmode > PM_WALK_SIDEWAYS) - return; - } - - if (&player == MyPlayer) { - PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); - } - - // Select the parameters that go into - // ChangeEquipment and add it to post switch - switch (location) { - case ILOC_HELM: - case ILOC_RING: - case ILOC_AMULET: - case ILOC_ARMOR: - ChangeBodyEquipment(player, slot, location); - break; - case ILOC_ONEHAND: - ChangeEquippedItem(player, slot); - break; - case ILOC_TWOHAND: - ChangeTwoHandItem(player); - break; - case ILOC_UNEQUIPABLE: - if (!ChangeInvItem(player, slot, itemSize)) return; - break; - case ILOC_BELT: - ChangeBeltItem(player, slot); - break; - case ILOC_NONE: - case ILOC_INVALID: - break; - } - - CalcPlrInv(player, true); - if (&player == MyPlayer) { - NewCursor(player.HoldItem); - } -} - -inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) -{ - assert(slot <= SLOTXY_CHEST); - return static_cast(slot); -} - -std::optional FindSlotUnderCursor(Point cursorPosition) -{ - - auto testPosition = static_cast(cursorPosition - GetRightPanel().position); - for (std::underlying_type_t r = SLOTXY_EQUIPPED_FIRST; r != SLOTXY_BELT_FIRST; r++) { - // check which body/inventory rectangle the mouse is in, if any - if (InvRect[r].contains(testPosition)) { - return static_cast(r); - } - } - - testPosition = static_cast(cursorPosition - GetMainPanel().position); - for (std::underlying_type_t r = SLOTXY_BELT_FIRST; r != NUM_XY_SLOTS; r++) { - // check which belt rectangle the mouse is in, if any - if (InvRect[r].contains(testPosition)) { - return static_cast(r); - } - } - - return {}; -} - -/** - * @brief Checks whether an item of the given size can be placed on the specified player's inventory slot. - * @param player The player whose inventory will be checked. - * @param slotIndex The 0-based index of the slot to put the item on. - * @param itemSize The size of the item to be checked. - * @param itemIndexToIgnore can be used to check if an item of the given size would fit if the item with the given (positive) ID was removed. - * @return 'True' in case the item can be placed on the specified player's inventory slot and 'False' otherwise. - */ -bool CheckItemFitsInInventorySlot(const Player &player, int slotIndex, const Size &itemSize, int itemIndexToIgnore) -{ - int yy = (slotIndex > 0) ? (10 * (slotIndex / 10)) : 0; - - for (int j = 0; j < itemSize.height; j++) { - if (yy >= InventoryGridCells) { - return false; - } - int xx = (slotIndex > 0) ? (slotIndex % 10) : 0; - for (int i = 0; i < itemSize.width; i++) { - if (xx >= 10 || (player.InvGrid[xx + yy] != 0 && std::abs(player.InvGrid[xx + yy]) - 1 != itemIndexToIgnore)) { - // The item is too wide to fit in the specified column, or one of the cells is occupied (and not by the item we're planning on removing) - return false; - } - xx++; - } - yy += 10; - } - return true; -} - -/** - * @brief Finds the first slot that could fit an item of the given size - * @param player Player whose inventory will be checked. - * @param itemSize Dimensions of the item. - * @param itemIndexToIgnore Can be used if you want to find whether the new item would fit with this item removed, without performing unnecessary actions. - * @return The first slot that could fit the item or an empty optional. - */ -std::optional FindSlotForItem(const Player &player, const Size &itemSize, int itemIndexToIgnore = -1) -{ - if (itemSize.height == 1) { - for (int i = 30; i <= 39; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - for (int x = 9; x >= 0; x--) { - for (int y = 2; y >= 0; y--) { - if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) - return (10 * y) + x; - } - } - return {}; - } - - if (itemSize.height == 2) { - for (int x = 10 - itemSize.width; x >= 0; x--) { - for (int y = 0; y < 3; y++) { - if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) - return (10 * y) + x; - } - } - return {}; - } - - if (itemSize == Size { 1, 3 }) { - for (int i = 0; i < 20; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - return {}; - } - - if (itemSize == Size { 2, 3 }) { - for (int i = 0; i < 9; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - - for (int i = 10; i < 19; i++) { - if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) - return i; - } - return {}; - } - - app_fatal(StrCat("Unknown item size: ", itemSize.width, "x", itemSize.height)); -} - -/** - * @brief Checks if the given item could be placed on the specified players inventory if the other item was removed. - * @param player The player whose inventory will be checked. - * @param item The item to be checked. - * @param itemIndexToIgnore The inventory index of the item that we assume will be removed. - * @return 'True' if the item could fit with the other item removed and 'False' otherwise. - */ -bool CouldFitItemInInventory(const Player &player, const Item &item, int itemIndexToIgnore) -{ - return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); -} - -void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) -{ - if (player._pmode > PM_WALK_SIDEWAYS) { - return; - } - - CloseGoldDrop(); - - std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); - - if (!maybeSlot) { - // not on an inventory slot rectangle - return; - } - - const inv_xy_slot r = *maybeSlot; - - Item &holdItem = player.HoldItem; - holdItem.clear(); - - bool attemptedMove = false; - bool automaticallyMoved = false; - SfxID successSound = SfxID::None; - HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else - - if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { - const inv_body_loc invloc = MapSlotToInvBodyLoc(r); - if (!player.InvBody[invloc].isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; - RemoveEquipment(player, invloc, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = player.InvBody[invloc]; - RemoveEquipment(player, invloc, false); - } - } - } - - if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { - const unsigned ig = r - SLOTXY_INV_FIRST; - const int iv = std::abs(player.InvGrid[ig]) - 1; - if (iv >= 0) { - if (automaticMove) { - attemptedMove = true; - if (CanBePlacedOnBelt(player, player.InvList[iv])) { - automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveInvItem(iv, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else if (CanEquip(player.InvList[iv])) { - failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails - - /* - * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be - * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except - * we don't want to leave the item on the hand if the equip attempt failed. We would end up - * generating wasteful network messages if we did the lift first. Instead we work out whatever slot - * needs to be unequipped (if any): - */ - int invloc = NUM_INVLOC; - switch (player.GetItemLocation(player.InvList[iv])) { - case ILOC_ARMOR: - invloc = INVLOC_CHEST; - break; - case ILOC_HELM: - invloc = INVLOC_HEAD; - break; - case ILOC_AMULET: - invloc = INVLOC_AMULET; - break; - case ILOC_ONEHAND: - if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() - && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass - || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { - // The left hand is not empty and we're either trying to equip the same type of item or - // it's holding a two handed weapon, so it must be unequipped - invloc = INVLOC_HAND_LEFT; - } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { - // The right hand is not empty and we're trying to equip the same type of item, so we need - // to unequip that item - invloc = INVLOC_HAND_RIGHT; - } - // otherwise one hand is empty (and we can let the auto-equip code put the target item into - // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip - // a shield (in which case the attempt will fail). - break; - case ILOC_TWOHAND: - // Moving a two-hand item from inventory to InvBody requires emptying both hands. - if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - // If the right hand is empty then we can simply try equipping this item in the left hand, - // we'll let the common code take care of unequipping anything held there. - invloc = INVLOC_HAND_LEFT; - } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { - // We have an item in the right hand but nothing in the left, so let the common code - // take care of unequipping whatever is held in the right hand. The auto-equip code - // picks the most appropriate location for the item type (which in this case will be - // the left hand), invloc isn't used there. - invloc = INVLOC_HAND_RIGHT; - } else { - // Both hands are holding items, we must unequip one of the items and check that there's - // space for the other before trying to auto-equip - inv_body_loc mainHand = INVLOC_HAND_LEFT; - inv_body_loc offHand = INVLOC_HAND_RIGHT; - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - // No space to move right hand item to inventory, can we move the left instead? - std::swap(mainHand, offHand); - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - break; - } - } - if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { - // No space for the main hand item. Move the other item back to the off hand and abort. - player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; - player.RemoveInvItem(player._pNumInv - 1, false); - break; - } - RemoveEquipment(player, offHand, false); - invloc = mainHand; - } - break; - default: - // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. - failedSpeech = HeroSpeech::ICantDoThat; - break; - } - // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip - if (invloc != NUM_INVLOC - && !player.InvBody[invloc].isEmpty() - && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { - holdItem = player.InvBody[invloc].pop(); - } - automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; - player.RemoveInvItem(iv, false); - - // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv - if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { - holdItem.clear(); - } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. - } else if (!holdItem.isEmpty()) { - // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... - player.InvBody[invloc] = holdItem.pop(); - } - } - } else if (IsVisualStoreOpen && CanSellToCurrentVendor(player.InvList[iv]) && dropItem) { - // If visual store is open, ctrl-click sells the item - SellItemToVisualStore(iv); - automaticallyMoved = true; - } else { - holdItem = player.InvList[iv]; - player.RemoveInvItem(iv, false); - } - } - } - - if (r >= SLOTXY_BELT_FIRST) { - const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; - if (!beltItem.isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = beltItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } - } - } - - if (!holdItem.isEmpty()) { - if (holdItem._itype == ItemType::Gold) { - player._pGold = CalculateGold(player); - } - - CalcPlrInv(player, true); - holdItem._iStatFlag = player.CanUseItem(holdItem); - - if (&player == MyPlayer) { - PlaySFX(SfxID::GrabItem); - NewCursor(holdItem); - } - if (dropItem) { - TryDropItem(); - } - } else if (automaticMove) { - if (automaticallyMoved) { - CalcPlrInv(player, true); - } - if (attemptedMove && &player == MyPlayer) { - if (automaticallyMoved) { - PlaySFX(successSound); - } else { - player.SaySpecific(failedSpeech); - } - } - } -} - -void TryCombineNaKrulNotes(Player &player, Item ¬eItem) -{ - const int idx = noteItem.IDidx; - const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; - - if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { - return; - } - - for (const _item_indexes note : notes) { - if (idx != note && !HasInventoryItemWithId(player, note)) { - return; // the player doesn't have all notes - } - } - - MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - - for (const _item_indexes note : notes) { - if (idx != note) { - RemoveInventoryItemById(player, note); - } - } - - const Point position = noteItem.position; // copy the position to restore it after re-initialising the item - noteItem = {}; - GetItemAttrs(noteItem, IDI_FULLNOTE, 16); - SetupItem(noteItem); - noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table -} - -void CheckQuestItem(Player &player, Item &questItem) -{ - const Player &myPlayer = *MyPlayer; - - if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE - && (questItem.IDidx == IDI_OPTAMULET - || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { - Quests[Q_BLIND]._qactive = QUEST_DONE; - NetSendCmdQuest(true, Quests[Q_BLIND]); - } - - if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { - player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP - Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; - NetSendCmdQuest(true, Quests[Q_MUSHROOM]); - } - - if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { - Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ANVIL]); - } - if (Quests[Q_ANVIL]._qlog) { - myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); - } - } - - if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { - myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); - } - - if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ROCK]._qactive == QUEST_INIT) { - Quests[Q_ROCK]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ROCK]); - } - if (Quests[Q_ROCK]._qlog) { - myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); - } - } - - if (Quests[Q_BLOOD]._qactive == QUEST_ACTIVE - && (questItem.IDidx == IDI_ARMOFVAL - || (Quests[Q_BLOOD].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 9, 3 })))) { - Quests[Q_BLOOD]._qactive = QUEST_DONE; - NetSendCmdQuest(true, Quests[Q_BLOOD]); - myPlayer.Say(HeroSpeech::MayTheSpiritOfArkaineProtectMe, 20); - } - - if (questItem.IDidx == IDI_MAPOFDOOM) { - Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; - if (Quests[Q_GRAVE]._qvar1 != 1) { - MyPlayer->Say(HeroSpeech::UhHuh, 10); - Quests[Q_GRAVE]._qvar1 = 1; - } - } - - TryCombineNaKrulNotes(player, questItem); -} - -void CleanupItems(int ii) -{ - const Item &item = Items[ii]; - dItem[item.position.x][item.position.y] = 0; - - if (CornerStone.isAvailable() && item.position == CornerStone.position) { - CornerStone.item.clear(); - CornerStone.item.selectionRegion = SelectionRegion::None; - CornerStone.item.position = { 0, 0 }; - CornerStone.item._iAnimFlag = false; - CornerStone.item._iIdentified = false; - CornerStone.item._iPostDraw = false; - } - - int i = 0; - while (i < ActiveItemCount) { - if (ActiveItems[i] == ii) { - DeleteItem(i); - i = 0; - continue; - } - - i++; - } -} - -bool CanUseStaff(Item &staff, SpellID spell) -{ - return !staff.isEmpty() - && IsAnyOf(staff._iMiscId, IMISC_STAFF, IMISC_UNIQUE) - && staff._iSpell == spell - && staff._iCharges > 0; -} - -void StartGoldDrop() -{ - CloseGoldWithdraw(); - - const int8_t invIndex = pcursinvitem; - - const Player &myPlayer = *MyPlayer; - - const int max = (invIndex <= INVITEM_INV_LAST) - ? myPlayer.InvList[invIndex - INVITEM_INV_FIRST]._ivalue - : myPlayer.SpdList[invIndex - INVITEM_BELT_FIRST]._ivalue; - - if (ChatFlag) - ResetChat(); - - const Point start = GetPanelPosition(UiPanels::Inventory, { 67, 128 }); - SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); - SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); - OpenGoldDrop(invIndex, max); -} - -int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) -{ - if (player.InvGrid[slotIndex] != 0) { - return value; - } - - Item &goldItem = player.InvList[player._pNumInv]; - MakeGoldStack(goldItem, std::min(value, MaxGold)); - player._pNumInv++; - player.InvGrid[slotIndex] = player._pNumInv; - if (&player == MyPlayer) { - NetSendCmdChInvItem(false, slotIndex); - } - - value -= goldItem._ivalue; - - return value; -} - -} // namespace - -void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) -{ - SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); - out.Clip(&srcRect, &targetPosition); - if (size.width <= 0 || size.height <= 0) - return; - - uint8_t colorShift; - switch (itemQuality) { - case ITEM_QUALITY_MAGIC: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BLUE : PAL16_ORANGE) - 1; - break; - case ITEM_QUALITY_UNIQUE: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_YELLOW : PAL16_ORANGE) - 1; - break; - default: - colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BEIGE : PAL16_ORANGE) - 1; - break; - } - - uint8_t *dst = &out[targetPosition]; - const auto dstPitch = out.pitch(); - for (int y = size.height; y != 0; --y, dst -= dstPitch + size.width) { - for (const uint8_t *end = dst + size.width; dst < end; ++dst) { - uint8_t &pix = *dst; - if (pix >= PAL16_GRAY) { - pix -= colorShift; - } - } - } -} - -bool CanBePlacedOnBelt(const Player &player, const Item &item) -{ - return FitsInBeltSlot(item) - && item._itype != ItemType::Gold - && player.CanUseItem(item) - && item.isUsable(); -} - -void FreeInvGFX() -{ - pInvCels = std::nullopt; -} - -void InitInv() -{ - const PlayerData &playerClassData = GetPlayerDataForClass(MyPlayer->_pClass); - const char *invName = playerClassData.inv.c_str(); - if (gbIsSpawn && (playerClassData.inv == "inv_rog" || playerClassData.inv == "inv_sor")) { - invName = "inv"; - } - pInvCels = LoadCel(StrCat("data\\inv\\", invName).c_str(), static_cast(SidePanelSize.width)); -} - -void DrawInv(const Surface &out) -{ - ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 0, 351 }), (*pInvCels)[0]); - - const Size slotSize[] = { - { 2, 2 }, // head - { 1, 1 }, // left ring - { 1, 1 }, // right ring - { 1, 1 }, // amulet - { 2, 3 }, // left hand - { 2, 3 }, // right hand - { 2, 3 }, // chest - }; - - const Point slotPos[] = { - { 133, 59 }, // head - { 48, 205 }, // left ring - { 249, 205 }, // right ring - { 205, 60 }, // amulet - { 17, 160 }, // left hand - { 248, 160 }, // right hand - { 133, 160 }, // chest - }; - - const Player &myPlayer = *InspectPlayer; - - for (int slot = INVLOC_HEAD; slot < NUM_INVLOC; slot++) { - if (!myPlayer.InvBody[slot].isEmpty()) { - int screenX = slotPos[slot].x; - int screenY = slotPos[slot].y; - InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, { screenX, screenY }), { slotSize[slot].width * InventorySlotSizeInPixels.width, slotSize[slot].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); - - const int cursId = myPlayer.InvBody[slot]._iCurs + CURSOR_FIRSTITEM; - - const Size frameSize = GetInvItemSize(cursId); - - // calc item offsets for weapons/armor smaller than 2x3 slots - if (IsAnyOf(slot, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST)) { - screenX += frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0; - screenY += frameSize.height == (3 * InventorySlotSizeInPixels.height) ? 0 : -INV_SLOT_HALF_SIZE_PX; - } - - const ClxSprite sprite = GetInvItemSprite(cursId); - const Point position = GetPanelPosition(UiPanels::Inventory, { screenX, screenY }); - - if (pcursinvitem == slot) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.InvBody[slot], true), position, sprite); - } - - DrawItem(myPlayer.InvBody[slot], out, position, sprite); - - if (slot == INVLOC_HAND_LEFT) { - if (myPlayer.GetItemLocation(myPlayer.InvBody[slot]) == ILOC_TWOHAND) { - InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, slotPos[INVLOC_HAND_RIGHT]), { slotSize[INVLOC_HAND_RIGHT].width * InventorySlotSizeInPixels.width, slotSize[INVLOC_HAND_RIGHT].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); - const int dstX = GetRightPanel().position.x + slotPos[INVLOC_HAND_RIGHT].x + (frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0) - 1; - const int dstY = GetRightPanel().position.y + slotPos[INVLOC_HAND_RIGHT].y; - ClxDrawBlended(out, { dstX, dstY }, sprite); - } - } - } - } - - for (int i = 0; i < InventoryGridCells; i++) { - if (myPlayer.InvGrid[i] != 0) { - InvDrawSlotBack( - out, - GetPanelPosition(UiPanels::Inventory, InvRect[i + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }, - InventorySlotSizeInPixels, - myPlayer.InvList[std::abs(myPlayer.InvGrid[i]) - 1]._iMagical); - } - } - - for (int j = 0; j < InventoryGridCells; j++) { - if (myPlayer.InvGrid[j] > 0) { // first slot of an item - const int ii = myPlayer.InvGrid[j] - 1; - const int cursId = myPlayer.InvList[ii]._iCurs + CURSOR_FIRSTITEM; - - const ClxSprite sprite = GetInvItemSprite(cursId); - const Point position = GetPanelPosition(UiPanels::Inventory, InvRect[j + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }; - if (pcursinvitem == ii + INVITEM_INV_FIRST) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.InvList[ii], true), position, sprite); - } - - DrawItem(myPlayer.InvList[ii], out, position, sprite); - } - } -} - -void DrawInvBelt(const Surface &out) -{ - if (ChatFlag) { - return; - } - - const Point mainPanelPosition = GetMainPanel().position; - - DrawPanelBox(out, { 205, 21, 232, 28 }, mainPanelPosition + Displacement { 205, 5 }); - - const Player &myPlayer = *InspectPlayer; - - for (int i = 0; i < MaxBeltItems; i++) { - if (myPlayer.SpdList[i].isEmpty()) { - continue; - } - - const Point position { InvRect[i + SLOTXY_BELT_FIRST].position.x + mainPanelPosition.x, InvRect[i + SLOTXY_BELT_FIRST].position.y + mainPanelPosition.y + InventorySlotSizeInPixels.height }; - InvDrawSlotBack(out, position, InventorySlotSizeInPixels, myPlayer.SpdList[i]._iMagical); - const int cursId = myPlayer.SpdList[i]._iCurs + CURSOR_FIRSTITEM; - - const ClxSprite sprite = GetInvItemSprite(cursId); - - if (pcursinvitem == i + INVITEM_BELT_FIRST) { - if (ControlMode == ControlTypes::KeyboardAndMouse || invflag) { - ClxDrawOutline(out, GetOutlineColor(myPlayer.SpdList[i], true), position, sprite); - } - } - - DrawItem(myPlayer.SpdList[i], out, position, sprite); - - if (myPlayer.SpdList[i].isUsable() - && myPlayer.SpdList[i]._itype != ItemType::Gold) { - auto beltKey = StrCat("BeltItem", i + 1); - std::string_view keyName = ControlMode == ControlTypes::Gamepad - ? GetOptions().Padmapper.InputNameForAction(beltKey, true) - : GetOptions().Keymapper.KeyNameForAction(beltKey); - - if (keyName.length() > 2) - keyName = {}; - - DrawString(out, keyName, { position - Displacement { 0, 12 }, InventorySlotSizeInPixels }, - { .flags = UiFlags::ColorWhite | UiFlags::AlignRight }); - } - } -} - -void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri) -{ - if (&player == MyPlayer) { - NetSendCmdDelItem(hiPri, bodyLocation); - } - - player.InvBody[bodyLocation].clear(); -} - -bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) -{ - if (!CanBePlacedOnBelt(player, item)) { - return false; - } - - for (Item &beltItem : player.SpdList) { - if (beltItem.isEmpty()) { - if (persistItem) { - beltItem = item; - player.CalcScrolls(); - RedrawComponent(PanelDrawComponent::Belt); - if (sendNetworkMessage) { - const auto beltIndex = static_cast(std::distance(&player.SpdList[0], &beltItem)); - NetSendCmdChBeltItem(false, beltIndex); - } - } - - return true; - } - } - - return false; -} - -bool AutoEquip(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) -{ - if (!CanEquip(item)) { - return false; - } - - for (int bodyLocation = INVLOC_HEAD; bodyLocation < NUM_INVLOC; bodyLocation++) { - if (AutoEquip(player, item, (inv_body_loc)bodyLocation, persistItem, sendNetworkMessage)) { - return true; - } - } - - return false; -} - -bool AutoEquipEnabled(const Player &player, const Item &item) -{ - if (item.isWeapon()) { - // Monk can use unarmed attack as an encouraged option, thus we do not automatically equip weapons on him so as to not - // annoy players who prefer that playstyle. - return player._pClass != HeroClass::Monk && *GetOptions().Gameplay.autoEquipWeapons; - } - - if (item.isArmor()) { - return *GetOptions().Gameplay.autoEquipArmor; - } - - if (item.isHelm()) { - return *GetOptions().Gameplay.autoEquipHelms; - } - - if (item.isShield()) { - return *GetOptions().Gameplay.autoEquipShields; - } - - if (item.isJewelry()) { - return *GetOptions().Gameplay.autoEquipJewelry; - } - - return true; -} - -bool CanFitItemInInventory(const Player &player, const Item &item) -{ - return static_cast(FindSlotForItem(player, GetInventorySize(item))); -} - -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) -{ - const Size itemSize = GetInventorySize(item); - std::optional targetSlot = FindSlotForItem(player, itemSize); - - if (targetSlot) { - player.InvList[player._pNumInv] = item; - player._pNumInv++; - - AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); - player.CalcScrolls(); - - return true; - } - - return false; -} - -std::vector SortItemsBySize(Player &player) -{ - std::vector> itemSizes; // Pair of item size and its index in InvList - itemSizes.reserve(player._pNumInv); // Reserves space for the number of items in the player's inventory - - for (int i = 0; i < player._pNumInv; i++) { - const Size size = GetInventorySize(player.InvList[i]); - itemSizes.emplace_back(size, i); - } - - // Sort items by height first, then by width - std::sort(itemSizes.begin(), itemSizes.end(), [](const auto &a, const auto &b) { - if (a.first.height == b.first.height) return a.first.width > b.first.width; - return a.first.height > b.first.height; - }); - - // Extract sorted indices - std::vector sortedIndices; - sortedIndices.reserve(itemSizes.size()); // Pre-allocate the necessary capacity - - for (const auto &itemSize : itemSizes) { - sortedIndices.push_back(itemSize.second); - } - - return sortedIndices; -} - -void ReorganizeInventory(Player &player) -{ - // Sort items by size - const std::vector sortedIndices = SortItemsBySize(player); - - // Temporary storage for items and a copy of InvGrid - std::vector tempStorage(player._pNumInv); - std::array originalInvGrid; // Declare an array for InvGrid copy - std::copy(std::begin(player.InvGrid), std::end(player.InvGrid), std::begin(originalInvGrid)); // Copy InvGrid to originalInvGrid - - // Move items to temporary storage and clear inventory slots - for (int i = 0; i < player._pNumInv; ++i) { - tempStorage[i] = player.InvList[i]; - player.InvList[i] = {}; - } - player._pNumInv = 0; // Reset inventory count - std::fill(std::begin(player.InvGrid), std::end(player.InvGrid), 0); // Clear InvGrid - - // Attempt to place items back, now from the temp storage - bool reorganizationFailed = false; - for (const int index : sortedIndices) { - const Item &item = tempStorage[index]; - if (!AutoPlaceItemInInventory(player, item, false)) { - reorganizationFailed = true; - break; - } - } - - // If reorganization failed, restore items and InvGrid from tempStorage and originalInvGrid - if (reorganizationFailed) { - for (const Item &item : tempStorage) { - if (!item.isEmpty()) { - player.InvList[player._pNumInv++] = item; - } - } - std::copy(std::begin(originalInvGrid), std::end(originalInvGrid), std::begin(player.InvGrid)); // Restore InvGrid - } -} - -int RoomForGold() -{ - int amount = 0; - for (const int8_t &itemIndex : MyPlayer->InvGrid) { - if (itemIndex < 0) { - continue; - } - if (itemIndex == 0) { - amount += MaxGold; - continue; - } - - const Item &goldItem = MyPlayer->InvList[itemIndex - 1]; - if (goldItem._itype != ItemType::Gold || goldItem._ivalue == MaxGold) { - continue; - } - - amount += MaxGold - goldItem._ivalue; - } - - return amount; -} - -int AddGoldToInventory(Player &player, int value) -{ - // Top off existing piles - for (int i = 0; i < player._pNumInv && value > 0; i++) { - Item &goldItem = player.InvList[i]; - if (goldItem._itype != ItemType::Gold || goldItem._ivalue >= MaxGold) { - continue; - } - - if (goldItem._ivalue + value > MaxGold) { - value -= MaxGold - goldItem._ivalue; - goldItem._ivalue = MaxGold; - } else { - goldItem._ivalue += value; - value = 0; - } - - NetSyncInvItem(player, i); - SetPlrHandGoldCurs(goldItem); - } - - // Last row right to left - for (int i = 39; i >= 30 && value > 0; i--) { - value = CreateGoldItemInInventorySlot(player, i, value); - } - - // Remaining inventory in columns, bottom to top, right to left - for (int x = 9; x >= 0 && value > 0; x--) { - for (int y = 2; y >= 0 && value > 0; y--) { - value = CreateGoldItemInInventorySlot(player, (10 * y) + x, value); - } - } - - return value; -} - -bool GoldAutoPlace(Player &player, Item &goldStack) -{ - goldStack._ivalue = AddGoldToInventory(player, goldStack._ivalue); - SetPlrHandGoldCurs(goldStack); - - player._pGold = CalculateGold(player); - - return goldStack._ivalue == 0; -} - -void CheckInvSwap(Player &player, inv_body_loc bLoc) -{ - const Item &item = player.InvBody[bLoc]; - - if (bLoc == INVLOC_HAND_LEFT && player.GetItemLocation(item) == ILOC_TWOHAND) { - player.InvBody[INVLOC_HAND_RIGHT].clear(); - } else if (bLoc == INVLOC_HAND_RIGHT && player.GetItemLocation(item) == ILOC_TWOHAND) { - player.InvBody[INVLOC_HAND_LEFT].clear(); - } - - CalcPlrInv(player, true); -} - -void inv_update_rem_item(Player &player, inv_body_loc iv) -{ - player.InvBody[iv].clear(); - - CalcPlrInv(player, player._pmode != PM_DEATH); -} - -void CheckInvSwap(Player &player, const Item &item, int invGridIndex) -{ - Size itemSize = GetInventorySize(item); - - const int pitch = 10; - const int invListIndex = [&]() -> int { - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - const int gridIndex = rowGridIndex + x; - if (player.InvGrid[gridIndex] != 0) - return std::abs(player.InvGrid[gridIndex]); - } - } - player._pNumInv++; - return player._pNumInv; - }(); - - if (invListIndex < player._pNumInv) { - for (int8_t &itemIndex : player.InvGrid) { - if (itemIndex == invListIndex) - itemIndex = 0; - if (itemIndex == -invListIndex) - itemIndex = 0; - } - } - - player.InvList[invListIndex - 1] = item; - - for (int y = 0; y < itemSize.height; y++) { - const int rowGridIndex = invGridIndex + (pitch * y); - for (int x = 0; x < itemSize.width; x++) { - if (x == 0 && y == itemSize.height - 1) - player.InvGrid[rowGridIndex + x] = invListIndex; - else - player.InvGrid[rowGridIndex + x] = -invListIndex; - } - } - - CalcPlrInv(player, true); -} - -void CheckInvRemove(Player &player, int invGridIndex) -{ - const int invListIndex = std::abs(player.InvGrid[invGridIndex]) - 1; - - if (invListIndex >= 0) { - player.RemoveInvItem(invListIndex); - } -} - -void TransferItemToStash(Player &player, int location) -{ - if (location == -1) { - return; - } - - const Item &item = GetInventoryItem(player, location); - if (!AutoPlaceItemInStash(item, true)) { - player.SaySpecific(HeroSpeech::WhereWouldIPutThis); - return; - } - - PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); - - if (location < INVITEM_INV_FIRST) { - RemoveEquipment(player, static_cast(location), false); - CalcPlrInv(player, true); - } else if (location <= INVITEM_INV_LAST) - player.RemoveInvItem(location - INVITEM_INV_FIRST); - else - player.RemoveSpdBarItem(location - INVITEM_BELT_FIRST); -} - -void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) -{ - if (IsInspectingPlayer()) - return; - if (!MyPlayer->HoldItem.isEmpty()) { - CheckInvPaste(*MyPlayer, MousePosition); - } else if (IsStashOpen && isCtrlHeld) { - TransferItemToStash(*MyPlayer, pcursinvitem); - } else { - CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); - } -} - -void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld) -{ - const Point mainPanelPosition = GetMainPanel().position; - if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x - && MousePosition.y > mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) { - CheckInvItem(isShiftHeld, isCtrlHeld); - } -} - -void InvGetItem(Player &player, int ii) -{ - Item &item = Items[ii]; - CloseGoldDrop(); - - if (dItem[item.position.x][item.position.y] == 0) - return; - - item._iCreateInfo &= ~CF_PREGEN; - CheckQuestItem(player, item); - item.updateRequiredStatsCacheForPlayer(player); - - if (item._itype == ItemType::Gold && GoldAutoPlace(player, item)) { - if (MyPlayer == &player) { - // Non-gold items (or gold when you have a full inventory) go to the hand then provide audible feedback on - // paste. To give the same feedback for auto-placed gold we play the sound effect now. - PlaySFX(SfxID::ItemGold); - } - } else { - // The item needs to go into the players hand - if (MyPlayer == &player && !player.HoldItem.isEmpty()) { - // drop whatever the player is currently holding - NetSendCmdPItem(true, CMD_SYNCPUTITEM, player.position.tile, player.HoldItem); - } - - // need to copy here instead of move so CleanupItems still has access to the position - player.HoldItem = item; - NewCursor(player.HoldItem); - } - - // This potentially moves items in memory so must be done after we've made a copy - CleanupItems(ii); - pcursitem = -1; -} - -std::optional FindAdjacentPositionForItem(Point origin, Direction facing) -{ - if (ActiveItemCount >= MAXITEMS) - return {}; - - if (CanPut(origin + facing)) - return origin + facing; - - if (CanPut(origin + Left(facing))) - return origin + Left(facing); - - if (CanPut(origin + Right(facing))) - return origin + Right(facing); - - if (CanPut(origin + Left(Left(facing)))) - return origin + Left(Left(facing)); - - if (CanPut(origin + Right(Right(facing)))) - return origin + Right(Right(facing)); - - if (CanPut(origin + Left(Left(Left(facing))))) - return origin + Left(Left(Left(facing))); - - if (CanPut(origin + Right(Right(Right(facing))))) - return origin + Right(Right(Right(facing))); - - if (CanPut(origin + Opposite(facing))) - return origin + Opposite(facing); - - if (CanPut(origin)) - return origin; - - return {}; -} - -void AutoGetItem(Player &player, Item *itemPointer, int ii) -{ - Item &item = *itemPointer; - - CloseGoldDrop(); - - if (dItem[item.position.x][item.position.y] == 0) - return; - - item._iCreateInfo &= ~CF_PREGEN; - CheckQuestItem(player, item); - item.updateRequiredStatsCacheForPlayer(player); - - bool done; - bool autoEquipped = false; - - if (item._itype == ItemType::Gold) { - done = GoldAutoPlace(player, item); - if (!done) { - SetPlrHandGoldCurs(item); - } - } else { - done = AutoEquipEnabled(player, item) && AutoEquip(player, item, true, &player == MyPlayer); - if (done) { - autoEquipped = true; - } - - if (!done) { - done = AutoPlaceItemInBelt(player, item, true, &player == MyPlayer); - } - if (!done) { - done = AutoPlaceItemInInventory(player, item, &player == MyPlayer); - } - } - - if (done) { - if (!autoEquipped && *GetOptions().Audio.itemPickupSound && &player == MyPlayer) { - PlaySFX(SfxID::GrabItem); - } - - CleanupItems(ii); - return; - } - - if (&player == MyPlayer) { - player.Say(HeroSpeech::ICantCarryAnymore); - } - RespawnItem(item, true); - NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); -} - -int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t createInfo) -{ - for (uint8_t i = 0; i < ActiveItemCount; i++) { - const Item &item = Items[ActiveItems[i]]; - if (item.keyAttributesMatch(iseed, idx, createInfo)) { - return i; - } - } - - return -1; -} - -void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci) -{ - // Check what the local client has at the target position - int ii = dItem[position.x][position.y] - 1; - - if (ii >= 0 && ii < MAXITEMS) { - // If there was an item there, check that it's the same item as the remote player has - if (!Items[ii].keyAttributesMatch(iseed, idx, ci)) { - // Key attributes don't match so we must've desynced, ignore this index and try find a matching item via lookup - ii = -1; - } - } - - if (ii == -1) { - // Either there's no item at the expected position or it doesn't match what is being picked up, so look for an item that matches the key attributes - ii = FindGetItem(iseed, idx, ci); - - if (ii != -1) { - // Translate to Items index for CleanupItems, FindGetItem returns an ActiveItems index - ii = ActiveItems[ii]; - } - } - - if (ii == -1) { - // Still can't find the expected item, assume it was collected earlier and this caused the desync - return; - } - - CleanupItems(ii); -} - -bool CanPut(Point position) -{ - if (!InDungeonBounds(position)) { - return false; - } - - if (IsTileSolid(position)) { - return false; - } - - if (dItem[position.x][position.y] != 0) { - return false; - } - - if (leveltype == DTYPE_TOWN) { - if (dMonster[position.x][position.y] != 0) { - return false; - } - if (dMonster[position.x + 1][position.y + 1] != 0) { - return false; - } - } - - if (IsItemBlockingObjectAtPosition(position)) { - return false; - } - - return true; -} - -int ClampDurability(const Item &item, int durability) -{ - if (item._iMaxDur == 0) - return 0; - - return std::clamp(durability, 1, item._iMaxDur); -} - -int16_t ClampToHit(const Item &item, int16_t toHit) -{ - if (toHit < item._iPLToHit || toHit > 51) - return item._iPLToHit; - - return toHit; -} - -uint8_t ClampMaxDam(const Item &item, uint8_t maxDam) -{ - if (maxDam < item._iMaxDam || maxDam - item._iMinDam > 30) - return item._iMaxDam; - - return maxDam; -} - -int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam) -{ - if (ActiveItemCount >= MAXITEMS) - return -1; - - Item item; - - RecreateItem(*MyPlayer, item, idx, icreateinfo, iseed, ivalue, ibuff); - if (id != 0) - item._iIdentified = true; - item._iMaxDur = mdur; - item._iDurability = ClampDurability(item, dur); - item._iMaxCharges = std::clamp(mch, 0, item._iMaxCharges); - item._iCharges = std::clamp(ch, 0, item._iMaxCharges); - if (gbIsHellfire) { - item._iPLToHit = ClampToHit(item, toHit); - item._iMaxDam = ClampMaxDam(item, maxDam); - } - - return PlaceItemInWorld(std::move(item), position); -} - -int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname) -{ - if (ActiveItemCount >= MAXITEMS) - return -1; - - Item item; - RecreateEar(item, icreateinfo, iseed, cursval, heroname); - - return PlaceItemInWorld(std::move(item), position); -} - -int8_t CheckInvHLight() -{ - int8_t r = 0; - for (; r < NUM_XY_SLOTS; r++) { - int xo = GetRightPanel().position.x; - int yo = GetRightPanel().position.y; - if (r >= SLOTXY_BELT_FIRST) { - xo = GetMainPanel().position.x; - yo = GetMainPanel().position.y; - } - - if (InvRect[r].contains(MousePosition - Displacement(xo, yo))) { - break; - } - } - - if (r >= NUM_XY_SLOTS) - return -1; - - int8_t rv = -1; - InfoColor = UiFlags::ColorWhite; - Item *pi = nullptr; - Player &myPlayer = *InspectPlayer; - - if (r == SLOTXY_HEAD) { - rv = INVLOC_HEAD; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_RING_LEFT) { - rv = INVLOC_RING_LEFT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_RING_RIGHT) { - rv = INVLOC_RING_RIGHT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_AMULET) { - rv = INVLOC_AMULET; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_HAND_LEFT) { - rv = INVLOC_HAND_LEFT; - pi = &myPlayer.InvBody[rv]; - } else if (r == SLOTXY_HAND_RIGHT) { - pi = &myPlayer.InvBody[INVLOC_HAND_LEFT]; - if (pi->isEmpty() || myPlayer.GetItemLocation(*pi) != ILOC_TWOHAND) { - rv = INVLOC_HAND_RIGHT; - pi = &myPlayer.InvBody[rv]; - } else { - rv = INVLOC_HAND_LEFT; - } - } else if (r == SLOTXY_CHEST) { - rv = INVLOC_CHEST; - pi = &myPlayer.InvBody[rv]; - } else if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { - const int8_t itemId = std::abs(myPlayer.InvGrid[r - SLOTXY_INV_FIRST]); - if (itemId == 0) - return -1; - const int ii = itemId - 1; - rv = ii + INVITEM_INV_FIRST; - pi = &myPlayer.InvList[ii]; - } else if (r >= SLOTXY_BELT_FIRST) { - r -= SLOTXY_BELT_FIRST; - RedrawComponent(PanelDrawComponent::Belt); - pi = &myPlayer.SpdList[r]; - if (pi->isEmpty()) - return -1; - rv = r + INVITEM_BELT_FIRST; - } - - if (pi->isEmpty()) - return -1; - - if (IsVisualStoreOpen && pcurs == CURSOR_REPAIR) { - InfoColor = pi->getTextColor(); - InfoString = pi->getName(); - FloatingInfoString = pi->getName(); - if (pi->_iIdentified) { - PrintItemDetails(*pi); - } else { - PrintItemDur(*pi); - } - int cost = GetRepairCost(*pi); - if (cost > 0) - AddInfoBoxString(StrCat(FormatInteger(cost), " Gold")); - else - AddInfoBoxString(_("Fully Repaired")); - } else if (pi->_itype == ItemType::Gold) { - const int nGold = pi->_ivalue; - InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); - FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); - } else { - InfoColor = pi->getTextColor(); - InfoString = pi->getName(); - FloatingInfoString = pi->getName(); - if (pi->_iIdentified) { - PrintItemDetails(*pi); - } else { - PrintItemDur(*pi); - } - } - - return rv; -} - -void ConsumeScroll(Player &player) -{ - const SpellID spellId = player.executedSpell.spellId; - - const auto isCurrentSpell = [spellId](const Item &item) -> bool { - return item.isScrollOf(spellId) || item.isRuneOf(spellId); - }; - - // Try to remove the scroll from selected inventory slot - const int8_t itemSlot = player.executedSpell.spellFrom; - if (itemSlot >= INVITEM_INV_FIRST && itemSlot <= INVITEM_INV_LAST) { - const int itemIndex = itemSlot - INVITEM_INV_FIRST; - const Item *item = &player.InvList[itemIndex]; - if (!item->isEmpty() && isCurrentSpell(*item)) { - player.RemoveInvItem(itemIndex); - return; - } - } else if (itemSlot >= INVITEM_BELT_FIRST && itemSlot <= INVITEM_BELT_LAST) { - const int itemIndex = itemSlot - INVITEM_BELT_FIRST; - const Item *item = &player.SpdList[itemIndex]; - if (!item->isEmpty() && isCurrentSpell(*item)) { - player.RemoveSpdBarItem(itemIndex); - return; - } - } else if (itemSlot != 0) { - app_fatal(StrCat("ConsumeScroll: Invalid item index ", itemSlot)); - } - - // Didn't find it at the selected slot, take the first one we find - // This path is always used when the scroll is consumed via spell selection - RemoveInventoryOrBeltItem(player, isCurrentSpell); -} - -bool CanUseScroll(Player &player, SpellID spell) -{ - if (leveltype == DTYPE_TOWN && !GetSpellData(spell).isAllowedInTown()) - return false; - - return HasInventoryOrBeltItem(player, [spell](const Item &item) { - return item.isScrollOf(spell) || item.isRuneOf(spell); - }); -} - -void ConsumeStaffCharge(Player &player) -{ - Item &staff = player.InvBody[INVLOC_HAND_LEFT]; - - if (!CanUseStaff(staff, player.executedSpell.spellId)) - return; - - staff._iCharges--; - CalcPlrInv(player, false); -} - -bool CanUseStaff(Player &player, SpellID spellId) -{ - return CanUseStaff(player.InvBody[INVLOC_HAND_LEFT], spellId); -} - -Item &GetInventoryItem(Player &player, int location) -{ - if (location < INVITEM_INV_FIRST) - return player.InvBody[location]; - - if (location <= INVITEM_INV_LAST) - return player.InvList[location - INVITEM_INV_FIRST]; - - return player.SpdList[location - INVITEM_BELT_FIRST]; -} - -bool UseInvItem(int cii) -{ - if (IsInspectingPlayer()) - return false; - - Player &player = *MyPlayer; - - if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) - return true; - if (pcurs != CURSOR_HAND) - return true; - if (IsPlayerInStore()) - return true; - if (cii < INVITEM_INV_FIRST) - return false; - - bool speedlist = false; - int c; - Item *item; - if (cii <= INVITEM_INV_LAST) { - c = cii - INVITEM_INV_FIRST; - item = &player.InvList[c]; - } else { - if (ChatFlag) - return true; - c = cii - INVITEM_BELT_FIRST; - - item = &player.SpdList[c]; - speedlist = true; - - // If selected speedlist item exists in InvList, use the InvList item. - for (int i = 0; i < player._pNumInv && *GetOptions().Gameplay.autoRefillBelt; i++) { - if (player.InvList[i]._iMiscId == item->_iMiscId && player.InvList[i]._iSpell == item->_iSpell) { - c = i; - item = &player.InvList[c]; - cii = c + INVITEM_INV_FIRST; - speedlist = false; - break; - } - } - - // If speedlist item is not inventory, use same item at the end of the speedlist if exists. - if (speedlist && *GetOptions().Gameplay.autoRefillBelt) { - for (int i = INVITEM_BELT_LAST - INVITEM_BELT_FIRST; i > c; i--) { - Item &candidate = player.SpdList[i]; - - if (!candidate.isEmpty() && candidate._iMiscId == item->_iMiscId && candidate._iSpell == item->_iSpell) { - c = i; - cii = c + INVITEM_BELT_FIRST; - item = &candidate; - break; - } - } - } - } - - constexpr int SpeechDelay = 10; - if (item->IDidx == IDI_MUSHROOM) { - player.Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); - return true; - } - if (item->IDidx == IDI_FUNGALTM) { - - PlaySFX(SfxID::ItemBook); - player.Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); - return true; - } - - if (player.isOnLevel(0)) { - if (UseItemOpensHive(*item, player.position.tile)) { - OpenHive(); - player.RemoveInvItem(c); - return true; - } - if (UseItemOpensGrave(*item, player.position.tile)) { - OpenGrave(); - player.RemoveInvItem(c); - return true; - } - } - - if (!item->isUsable()) - return false; - - if (!player.CanUseItem(*item)) { - player.Say(HeroSpeech::ICantUseThisYet); - return true; - } - - if (item->_iMiscId == IMISC_NONE && item->_itype == ItemType::Gold) { - StartGoldDrop(); - return true; - } - - CloseGoldDrop(); - - if (item->isScroll() && leveltype == DTYPE_TOWN && !GetSpellData(item->_iSpell).isAllowedInTown()) { - return true; - } - - if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { - return true; - } - - if (item->_iMiscId == IMISC_ARENAPOT && !player.isOnArenaLevel()) { - player.Say(HeroSpeech::ThatWontWorkHere); - return true; - } - - const int idata = ItemCAnimTbl[item->_iCurs]; - if (item->_iMiscId == IMISC_BOOK) - PlaySFX(SfxID::ReadBook); - else if (&player == MyPlayer) - PlaySFX(ItemInvSnds[idata]); - - UseItem(player, item->_iMiscId, item->_iSpell, cii); - - if (speedlist) { - if (player.SpdList[c]._iMiscId == IMISC_NOTE) { - InitQTextMsg(TEXT_BOOK9); - CloseInventory(); - return true; - } - if (!item->isScroll() && !item->isRune()) - player.RemoveSpdBarItem(c); - return true; - } - if (player.InvList[c]._iMiscId == IMISC_MAPOFDOOM) - return true; - if (player.InvList[c]._iMiscId == IMISC_NOTE) { - InitQTextMsg(TEXT_BOOK9); - CloseInventory(); - return true; - } - if (!item->isScroll() && !item->isRune()) - player.RemoveInvItem(c); - - return true; -} - -void CloseInventory() -{ - CloseGoldWithdraw(); - CloseStash(); - CloseVisualStore(); - invflag = false; -} - -void CloseStash() -{ - if (!IsStashOpen) - return; - - Player &myPlayer = *MyPlayer; - if (!myPlayer.HoldItem.isEmpty()) { - std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); - if (itemTile) { - NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); - } else { - if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) - && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true) - && !AutoPlaceItemInStash(myPlayer.HoldItem, true)) { - // This can fail for max gold, arena potions and a stash that has been arranged - // to not have room for the item all 3 cases are extremely unlikely - app_fatal(_("No room for item")); - } - PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); - } - myPlayer.HoldItem.clear(); - NewCursor(CURSOR_HAND); - } - - IsStashOpen = false; -} - -void DoTelekinesis() -{ - if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) - NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJT, cursPosition); - if (pcursitem != -1) - NetSendCmdGItem(true, CMD_REQUESTAGITEM, *MyPlayer, pcursitem); - if (pcursmonst != -1) { - const Monster &monter = Monsters[pcursmonst]; - if (!M_Talker(monter) && monter.talkMsg == TEXT_NONE) - NetSendCmdParam1(true, CMD_KNOCKBACK, pcursmonst); - } - NewCursor(CURSOR_HAND); -} - -int CalculateGold(Player &player) -{ - int gold = 0; - - for (int i = 0; i < player._pNumInv; i++) { - if (player.InvList[i]._itype == ItemType::Gold) - gold += player.InvList[i]._ivalue; - } - - return gold; -} - -Size GetInventorySize(const Item &item) -{ - const Size size = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); - - return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; -} - -} // namespace devilution +/** + * @file inv.cpp + * + * Implementation of player inventory. + */ +#include +#include +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include +#else +#include +#endif + +#include + +#include "DiabloUI/ui_flags.hpp" +#include "controls/control_mode.hpp" +#include "controls/plrctrls.h" +#include "cursor.h" +#include "engine/backbuffer_state.hpp" +#include "engine/clx_sprite.hpp" +#include "engine/load_cel.hpp" +#include "engine/palette.h" +#include "engine/render/clx_render.hpp" +#include "engine/render/text_render.hpp" +#include "engine/size.hpp" +#include "hwcursor.hpp" +#include "inv_iterators.hpp" +#include "levels/tile_properties.hpp" +#include "levels/town.h" +#include "minitext.h" +#include "options.h" +#include "panels/ui_panels.hpp" +#include "player.h" +#include "plrmsg.h" +#include "qol/stash.h" +#include "qol/visual_store.h" +#include "stores.h" +#include "towners.h" +#include "utils/display.h" +#include "utils/format_int.hpp" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/sdl_geometry.h" +#include "utils/str_cat.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +bool invflag; + +/** + * Maps from inventory slot to screen position. The inventory slots are + * arranged as follows: + * + * @code{.unparsed} + * 00 00 + * 00 00 03 + * + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 + * + * 01 02 + * + * 07 08 09 10 11 12 13 14 15 16 + * 17 18 19 20 21 22 23 24 25 26 + * 27 28 29 30 31 32 33 34 35 36 + * 37 38 39 40 41 42 43 44 45 46 + * + * 47 48 49 50 51 52 53 54 + * @endcode + */ +const Rectangle InvRect[] = { + // clang-format off + //{ X, Y }, { W, H } + { { 132, 2 }, { 58, 59 } }, // helmet + { { 47, 177 }, { 28, 29 } }, // left ring + { { 248, 177 }, { 28, 29 } }, // right ring + { { 205, 32 }, { 28, 29 } }, // amulet + { { 17, 75 }, { 58, 86 } }, // left hand + { { 248, 75 }, { 58, 87 } }, // right hand + { { 132, 75 }, { 58, 87 } }, // chest + { { 17, 222 }, { 29, 29 } }, // inv row 1 + { { 46, 222 }, { 29, 29 } }, // inv row 1 + { { 75, 222 }, { 29, 29 } }, // inv row 1 + { { 104, 222 }, { 29, 29 } }, // inv row 1 + { { 133, 222 }, { 29, 29 } }, // inv row 1 + { { 162, 222 }, { 29, 29 } }, // inv row 1 + { { 191, 222 }, { 29, 29 } }, // inv row 1 + { { 220, 222 }, { 29, 29 } }, // inv row 1 + { { 249, 222 }, { 29, 29 } }, // inv row 1 + { { 278, 222 }, { 29, 29 } }, // inv row 1 + { { 17, 251 }, { 29, 29 } }, // inv row 2 + { { 46, 251 }, { 29, 29 } }, // inv row 2 + { { 75, 251 }, { 29, 29 } }, // inv row 2 + { { 104, 251 }, { 29, 29 } }, // inv row 2 + { { 133, 251 }, { 29, 29 } }, // inv row 2 + { { 162, 251 }, { 29, 29 } }, // inv row 2 + { { 191, 251 }, { 29, 29 } }, // inv row 2 + { { 220, 251 }, { 29, 29 } }, // inv row 2 + { { 249, 251 }, { 29, 29 } }, // inv row 2 + { { 278, 251 }, { 29, 29 } }, // inv row 2 + { { 17, 280 }, { 29, 29 } }, // inv row 3 + { { 46, 280 }, { 29, 29 } }, // inv row 3 + { { 75, 280 }, { 29, 29 } }, // inv row 3 + { { 104, 280 }, { 29, 29 } }, // inv row 3 + { { 133, 280 }, { 29, 29 } }, // inv row 3 + { { 162, 280 }, { 29, 29 } }, // inv row 3 + { { 191, 280 }, { 29, 29 } }, // inv row 3 + { { 220, 280 }, { 29, 29 } }, // inv row 3 + { { 249, 280 }, { 29, 29 } }, // inv row 3 + { { 278, 280 }, { 29, 29 } }, // inv row 3 + { { 17, 309 }, { 29, 29 } }, // inv row 4 + { { 46, 309 }, { 29, 29 } }, // inv row 4 + { { 75, 309 }, { 29, 29 } }, // inv row 4 + { { 104, 309 }, { 29, 29 } }, // inv row 4 + { { 133, 309 }, { 29, 29 } }, // inv row 4 + { { 162, 309 }, { 29, 29 } }, // inv row 4 + { { 191, 309 }, { 29, 29 } }, // inv row 4 + { { 220, 309 }, { 29, 29 } }, // inv row 4 + { { 249, 309 }, { 29, 29 } }, // inv row 4 + { { 278, 309 }, { 29, 29 } }, // inv row 4 + { { 205, 5 }, { 29, 29 } }, // belt + { { 234, 5 }, { 29, 29 } }, // belt + { { 263, 5 }, { 29, 29 } }, // belt + { { 292, 5 }, { 29, 29 } }, // belt + { { 321, 5 }, { 29, 29 } }, // belt + { { 350, 5 }, { 29, 29 } }, // belt + { { 379, 5 }, { 29, 29 } }, // belt + { { 408, 5 }, { 29, 29 } } // belt + // clang-format on +}; + +namespace { + +OptionalOwnedClxSpriteList pInvCels; + +/** + * @brief Adds an item to a player's InvGrid array + * @param player The player reference + * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) + * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) + * @param itemSize Size of item + */ +void AddItemToInvGrid(Player &player, int invGridIndex, int invListIndex, Size itemSize, bool sendNetworkMessage) +{ + const int pitch = 10; + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + if (x == 0 && y == itemSize.height - 1) + player.InvGrid[rowGridIndex + x] = invListIndex; + else + player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell. + } + } + + if (sendNetworkMessage) { + NetSendCmdChInvItem(false, invGridIndex); + } +} + +/** + * @brief Checks whether the given item can fit in a belt slot (i.e. the item's size in inventory cells is 1x1). + * @param item The item to be checked. + * @return 'True' in case the item can fit a belt slot and 'False' otherwise. + */ +bool FitsInBeltSlot(const Item &item) +{ + return GetInventorySize(item) == Size { 1, 1 }; +} + +/** + * @brief Checks whether the given item can be equipped. Since this overload doesn't take player information, it only considers + * general aspects about the item, like if its requirements are met and if the item's target location is valid for the body. + * @param item The item to check. + * @return 'True' in case the item could be equipped in a player, and 'False' otherwise. + */ +bool CanEquip(const Item &item) +{ + return item.isEquipment() + && item._iStatFlag; +} + +/** + * @brief A specialized version of 'CanEquip(int, Item&, int)' that specifically checks whether the item can be equipped + * in one/both of the player's hands. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to check. + * @return 'True' if the player can currently equip the item in either one of his hands (i.e. the required hands are empty and + * allow the item), and 'False' otherwise. + */ +bool CanWield(Player &player, const Item &item) +{ + if (!CanEquip(item) || IsNoneOf(player.GetItemLocation(item), ILOC_ONEHAND, ILOC_TWOHAND)) + return false; + + const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; + const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; + + if (leftHandItem.isEmpty() && rightHandItem.isEmpty()) { + return true; + } + + if (!leftHandItem.isEmpty() && !rightHandItem.isEmpty()) { + return false; + } + + const Item &occupiedHand = !leftHandItem.isEmpty() ? leftHandItem : rightHandItem; + + // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied + // slot is another one-handed weapon. + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { + const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND + && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); + + const bool weaponToEquipIsOneHandedSwordOrMace = player.GetItemLocation(item) == ILOC_ONEHAND + && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace); + + if (occupiedHandIsOneHandedSwordOrMace && weaponToEquipIsOneHandedSwordOrMace) { + return true; + } + } + + return player.GetItemLocation(item) == ILOC_ONEHAND + && player.GetItemLocation(occupiedHand) == ILOC_ONEHAND + && item._iClass != occupiedHand._iClass; +} + +/** + * @brief Checks whether the specified item can be equipped in the desired body location on the player. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to check. + * @param bodyLocation The location in the inventory to be checked against. + * @return 'True' if the player can currently equip the item in the specified body location (i.e. the body location is empty and + * allows the item), and 'False' otherwise. + */ +bool CanEquip(Player &player, const Item &item, inv_body_loc bodyLocation) +{ + if (!CanEquip(item) || player._pmode > PM_WALK_SIDEWAYS || !player.InvBody[bodyLocation].isEmpty()) { + return false; + } + + switch (bodyLocation) { + case INVLOC_AMULET: + return item._iLoc == ILOC_AMULET; + + case INVLOC_CHEST: + return item._iLoc == ILOC_ARMOR; + + case INVLOC_HAND_LEFT: + case INVLOC_HAND_RIGHT: + return CanWield(player, item); + + case INVLOC_HEAD: + return item._iLoc == ILOC_HELM; + + case INVLOC_RING_LEFT: + case INVLOC_RING_RIGHT: + return item._iLoc == ILOC_RING; + + default: + return false; + } +} + +void ChangeEquipment(Player &player, inv_body_loc bodyLocation, const Item &item, bool sendNetworkMessage) +{ + player.InvBody[bodyLocation] = item; + + if (sendNetworkMessage) { + NetSendCmdChItem(false, bodyLocation, true); + } +} + +bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool persistItem, bool sendNetworkMessage) +{ + if (!CanEquip(player, item, bodyLocation)) { + return false; + } + + if (persistItem) { + ChangeEquipment(player, bodyLocation, item, sendNetworkMessage); + + if (sendNetworkMessage && *GetOptions().Audio.autoEquipSound) { + PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); + } + + CalcPlrInv(player, true); + } + + return true; +} + +int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) +{ + Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; + for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) + return r; + } + for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) { + // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. + if (itemSize.height <= 1 && itemSize.width <= 1) { + // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do + return r; + } + // Otherwise work out how far the central cell is from the top-left cell + Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; + // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. + if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { + // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left + hotPixelCellOffset.deltaX++; + } + if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { + // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above + hotPixelCellOffset.deltaY++; + } + // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) + const int hotPixelCell = r - SLOTXY_INV_FIRST; + const int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); + const int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); + return SLOTXY_INV_FIRST + (targetRow * InventorySizeInSlots.width) + targetColumn; + } + } + + panelOffset = Point { 0, 0 } - GetMainPanel().position; + for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) + return r; + } + return NUM_XY_SLOTS; +} + +void ChangeBodyEquipment(Player &player, int slot, item_equip_type location) +{ + const inv_body_loc bodyLocation = [&slot](item_equip_type location) { + switch (location) { + case ILOC_HELM: + return INVLOC_HEAD; + case ILOC_RING: + return (slot == SLOTXY_RING_LEFT ? INVLOC_RING_LEFT : INVLOC_RING_RIGHT); + case ILOC_AMULET: + return INVLOC_AMULET; + case ILOC_ARMOR: + return INVLOC_CHEST; + default: + app_fatal("Unexpected equipment type"); + } + }(location); + const Item previouslyEquippedItem = player.InvBody[slot]; + ChangeEquipment(player, bodyLocation, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } +} + +void ChangeEquippedItem(Player &player, uint8_t slot) +{ + const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; + const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; + + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + + const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) + || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); + + const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); + + const inv_body_loc pasteHand = pasteIntoSelectedHand ? selectedHand : otherHand; + const Item previouslyEquippedItem = dequipTwoHandedWeapon ? player.InvBody[otherHand] : player.InvBody[pasteHand]; + if (dequipTwoHandedWeapon) { + RemoveEquipment(player, otherHand, false); + } + ChangeEquipment(player, pasteHand, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } +} + +void ChangeTwoHandItem(Player &player) +{ + if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && !player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + inv_body_loc locationToUnequip = INVLOC_HAND_LEFT; + if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { + locationToUnequip = INVLOC_HAND_RIGHT; + } + if (!AutoPlaceItemInInventory(player, player.InvBody[locationToUnequip])) { + return; + } + + if (locationToUnequip == INVLOC_HAND_RIGHT) { + RemoveEquipment(player, INVLOC_HAND_RIGHT, false); + } else { + player.InvBody[INVLOC_HAND_LEFT].clear(); + } + } + + if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_LEFT]; + ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem.pop(), &player == MyPlayer); + if (!previouslyEquippedItem.isEmpty()) { + player.HoldItem = previouslyEquippedItem; + } + } else { + const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_RIGHT]; + RemoveEquipment(player, INVLOC_HAND_RIGHT, false); + ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem, &player == MyPlayer); + player.HoldItem = previouslyEquippedItem; + } +} + +int8_t CheckOverlappingItems(int slot, const Player &player, Size itemSize) +{ + // check that the item we're pasting only overlaps one other item (or is going into empty space) + const auto originCell = static_cast(slot - SLOTXY_INV_FIRST); + + int8_t overlappingId = 0; + for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { + + for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { + const unsigned testCell = originCell + rowOffset + columnOffset; + // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. + assert(testCell < sizeof(player.InvGrid)); + if (player.InvGrid[testCell] != 0) { + const int8_t iv = std::abs(player.InvGrid[testCell]); + if (overlappingId != 0) { + if (overlappingId != iv) { + // Found two different items that would be displaced by the held item, can't paste the item here. + return -1; + } + } else { + overlappingId = iv; + } + } + } + } + + return overlappingId; +} + +int8_t GetPrevItemId(int slot, const Player &player, const Size &itemSize) +{ + if (player.HoldItem._itype != ItemType::Gold) + return CheckOverlappingItems(slot, player, itemSize); + const int8_t item_cell_begin = player.InvGrid[slot - SLOTXY_INV_FIRST]; + if (item_cell_begin == 0) + return 0; + if (item_cell_begin <= 0) + return -item_cell_begin; + if (player.InvList[item_cell_begin - 1]._itype != ItemType::Gold) + return item_cell_begin; + return 0; +} + +bool ChangeInvItem(Player &player, int slot, Size itemSize) +{ + int8_t prevItemId = GetPrevItemId(slot, player, itemSize); + if (prevItemId < 0) return false; + + if (player.HoldItem._itype == ItemType::Gold && prevItemId == 0) { + const int ii = slot - SLOTXY_INV_FIRST; + if (player.InvGrid[ii] > 0) { + const int invIndex = player.InvGrid[ii] - 1; + const int gt = player.InvList[invIndex]._ivalue; + int ig = player.HoldItem._ivalue + gt; + if (ig <= MaxGold) { + player.InvList[invIndex]._ivalue = ig; + SetPlrHandGoldCurs(player.InvList[invIndex]); + player._pGold += player.HoldItem._ivalue; + player.HoldItem.clear(); + } else { + ig = MaxGold - gt; + player._pGold += ig; + player.HoldItem._ivalue -= ig; + SetPlrHandGoldCurs(player.HoldItem); + player.InvList[invIndex]._ivalue = MaxGold; + player.InvList[invIndex]._iCurs = ICURS_GOLD_LARGE; + } + } else { + const int invIndex = player._pNumInv; + player._pGold += player.HoldItem._ivalue; + player.InvList[invIndex] = player.HoldItem.pop(); + player._pNumInv++; + player.InvGrid[ii] = player._pNumInv; + } + if (&player == MyPlayer) { + NetSendCmdChInvItem(false, ii); + } + } else { + if (prevItemId == 0) { + player.InvList[player._pNumInv] = player.HoldItem.pop(); + player._pNumInv++; + prevItemId = player._pNumInv; + } else { + const int invIndex = prevItemId - 1; + if (player.HoldItem._itype == ItemType::Gold) + player._pGold += player.HoldItem._ivalue; + std::swap(player.InvList[invIndex], player.HoldItem); + if (player.HoldItem._itype == ItemType::Gold) + player._pGold = CalculateGold(player); + for (int8_t &itemIndex : player.InvGrid) { + if (itemIndex == prevItemId) + itemIndex = 0; + if (itemIndex == -prevItemId) + itemIndex = 0; + } + } + + AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); + } + + return true; +} + +void ChangeBeltItem(Player &player, int slot) +{ + const int ii = slot - SLOTXY_BELT_FIRST; + if (player.SpdList[ii].isEmpty()) { + player.SpdList[ii] = player.HoldItem.pop(); + } else { + std::swap(player.SpdList[ii], player.HoldItem); + + if (player.HoldItem._itype == ItemType::Gold) + player._pGold = CalculateGold(player); + } + if (&player == MyPlayer) { + NetSendCmdChBeltItem(false, ii); + } + RedrawComponent(PanelDrawComponent::Belt); +} + +item_equip_type GetItemEquipType(int slot, item_equip_type desiredLocation) +{ + if (slot == SLOTXY_HEAD) + return ILOC_HELM; + if (slot == SLOTXY_RING_LEFT || slot == SLOTXY_RING_RIGHT) + return ILOC_RING; + if (slot == SLOTXY_AMULET) + return ILOC_AMULET; + if (slot == SLOTXY_HAND_LEFT || slot == SLOTXY_HAND_RIGHT) { + if (desiredLocation == ILOC_TWOHAND) + return ILOC_TWOHAND; + return ILOC_ONEHAND; + } + if (slot == SLOTXY_CHEST) + return ILOC_ARMOR; + if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) + return ILOC_BELT; + + return ILOC_UNEQUIPABLE; +} + +void CheckInvPaste(Player &player, Point cursorPosition) +{ + const Size itemSize = GetInventorySize(player.HoldItem); + + const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); + if (slot == NUM_XY_SLOTS) + return; + + const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); + const item_equip_type location = GetItemEquipType(slot, desiredLocation); + + if (location == ILOC_BELT) { + if (!CanBePlacedOnBelt(player, player.HoldItem)) return; + } else if (location != ILOC_UNEQUIPABLE) { + if (desiredLocation != location) return; + } + + if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { + if (!player.CanUseItem(player.HoldItem)) { + player.Say(HeroSpeech::ICantUseThisYet); + return; + } + if (player._pmode > PM_WALK_SIDEWAYS) + return; + } + + if (&player == MyPlayer) { + PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); + } + + // Select the parameters that go into + // ChangeEquipment and add it to post switch + switch (location) { + case ILOC_HELM: + case ILOC_RING: + case ILOC_AMULET: + case ILOC_ARMOR: + ChangeBodyEquipment(player, slot, location); + break; + case ILOC_ONEHAND: + ChangeEquippedItem(player, slot); + break; + case ILOC_TWOHAND: + ChangeTwoHandItem(player); + break; + case ILOC_UNEQUIPABLE: + if (!ChangeInvItem(player, slot, itemSize)) return; + break; + case ILOC_BELT: + ChangeBeltItem(player, slot); + break; + case ILOC_NONE: + case ILOC_INVALID: + break; + } + + CalcPlrInv(player, true); + if (&player == MyPlayer) { + NewCursor(player.HoldItem); + } +} + +inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) +{ + assert(slot <= SLOTXY_CHEST); + return static_cast(slot); +} + +std::optional FindSlotUnderCursor(Point cursorPosition) +{ + + auto testPosition = static_cast(cursorPosition - GetRightPanel().position); + for (std::underlying_type_t r = SLOTXY_EQUIPPED_FIRST; r != SLOTXY_BELT_FIRST; r++) { + // check which body/inventory rectangle the mouse is in, if any + if (InvRect[r].contains(testPosition)) { + return static_cast(r); + } + } + + testPosition = static_cast(cursorPosition - GetMainPanel().position); + for (std::underlying_type_t r = SLOTXY_BELT_FIRST; r != NUM_XY_SLOTS; r++) { + // check which belt rectangle the mouse is in, if any + if (InvRect[r].contains(testPosition)) { + return static_cast(r); + } + } + + return {}; +} + +/** + * @brief Checks whether an item of the given size can be placed on the specified player's inventory slot. + * @param player The player whose inventory will be checked. + * @param slotIndex The 0-based index of the slot to put the item on. + * @param itemSize The size of the item to be checked. + * @param itemIndexToIgnore can be used to check if an item of the given size would fit if the item with the given (positive) ID was removed. + * @return 'True' in case the item can be placed on the specified player's inventory slot and 'False' otherwise. + */ +bool CheckItemFitsInInventorySlot(const Player &player, int slotIndex, const Size &itemSize, int itemIndexToIgnore) +{ + int yy = (slotIndex > 0) ? (10 * (slotIndex / 10)) : 0; + + for (int j = 0; j < itemSize.height; j++) { + if (yy >= InventoryGridCells) { + return false; + } + int xx = (slotIndex > 0) ? (slotIndex % 10) : 0; + for (int i = 0; i < itemSize.width; i++) { + if (xx >= 10 || (player.InvGrid[xx + yy] != 0 && std::abs(player.InvGrid[xx + yy]) - 1 != itemIndexToIgnore)) { + // The item is too wide to fit in the specified column, or one of the cells is occupied (and not by the item we're planning on removing) + return false; + } + xx++; + } + yy += 10; + } + return true; +} + +/** + * @brief Finds the first slot that could fit an item of the given size + * @param player Player whose inventory will be checked. + * @param itemSize Dimensions of the item. + * @param itemIndexToIgnore Can be used if you want to find whether the new item would fit with this item removed, without performing unnecessary actions. + * @return The first slot that could fit the item or an empty optional. + */ +std::optional FindSlotForItem(const Player &player, const Size &itemSize, int itemIndexToIgnore = -1) +{ + if (itemSize.height == 1) { + for (int i = 30; i <= 39; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + for (int x = 9; x >= 0; x--) { + for (int y = 2; y >= 0; y--) { + if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) + return (10 * y) + x; + } + } + return {}; + } + + if (itemSize.height == 2) { + for (int x = 10 - itemSize.width; x >= 0; x--) { + for (int y = 0; y < 3; y++) { + if (CheckItemFitsInInventorySlot(player, (10 * y) + x, itemSize, itemIndexToIgnore)) + return (10 * y) + x; + } + } + return {}; + } + + if (itemSize == Size { 1, 3 }) { + for (int i = 0; i < 20; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + return {}; + } + + if (itemSize == Size { 2, 3 }) { + for (int i = 0; i < 9; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + + for (int i = 10; i < 19; i++) { + if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) + return i; + } + return {}; + } + + app_fatal(StrCat("Unknown item size: ", itemSize.width, "x", itemSize.height)); +} + +/** + * @brief Checks if the given item could be placed on the specified players inventory if the other item was removed. + * @param player The player whose inventory will be checked. + * @param item The item to be checked. + * @param itemIndexToIgnore The inventory index of the item that we assume will be removed. + * @return 'True' if the item could fit with the other item removed and 'False' otherwise. + */ +bool CouldFitItemInInventory(const Player &player, const Item &item, int itemIndexToIgnore) +{ + return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); +} + +void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) +{ + if (player._pmode > PM_WALK_SIDEWAYS) { + return; + } + + CloseGoldDrop(); + + std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); + + if (!maybeSlot) { + // not on an inventory slot rectangle + return; + } + + const inv_xy_slot r = *maybeSlot; + + Item &holdItem = player.HoldItem; + holdItem.clear(); + + bool attemptedMove = false; + bool automaticallyMoved = false; + SfxID successSound = SfxID::None; + HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else + + if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { + const inv_body_loc invloc = MapSlotToInvBodyLoc(r); + if (!player.InvBody[invloc].isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; + RemoveEquipment(player, invloc, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = player.InvBody[invloc]; + RemoveEquipment(player, invloc, false); + } + } + } + + if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { + const unsigned ig = r - SLOTXY_INV_FIRST; + const int iv = std::abs(player.InvGrid[ig]) - 1; + if (iv >= 0) { + if (automaticMove) { + attemptedMove = true; + if (CanBePlacedOnBelt(player, player.InvList[iv])) { + automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveInvItem(iv, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else if (CanEquip(player.InvList[iv])) { + failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails + + /* + * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be + * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except + * we don't want to leave the item on the hand if the equip attempt failed. We would end up + * generating wasteful network messages if we did the lift first. Instead we work out whatever slot + * needs to be unequipped (if any): + */ + int invloc = NUM_INVLOC; + switch (player.GetItemLocation(player.InvList[iv])) { + case ILOC_ARMOR: + invloc = INVLOC_CHEST; + break; + case ILOC_HELM: + invloc = INVLOC_HEAD; + break; + case ILOC_AMULET: + invloc = INVLOC_AMULET; + break; + case ILOC_ONEHAND: + if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() + && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass + || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { + // The left hand is not empty and we're either trying to equip the same type of item or + // it's holding a two handed weapon, so it must be unequipped + invloc = INVLOC_HAND_LEFT; + } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { + // The right hand is not empty and we're trying to equip the same type of item, so we need + // to unequip that item + invloc = INVLOC_HAND_RIGHT; + } + // otherwise one hand is empty (and we can let the auto-equip code put the target item into + // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip + // a shield (in which case the attempt will fail). + break; + case ILOC_TWOHAND: + // Moving a two-hand item from inventory to InvBody requires emptying both hands. + if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + // If the right hand is empty then we can simply try equipping this item in the left hand, + // we'll let the common code take care of unequipping anything held there. + invloc = INVLOC_HAND_LEFT; + } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { + // We have an item in the right hand but nothing in the left, so let the common code + // take care of unequipping whatever is held in the right hand. The auto-equip code + // picks the most appropriate location for the item type (which in this case will be + // the left hand), invloc isn't used there. + invloc = INVLOC_HAND_RIGHT; + } else { + // Both hands are holding items, we must unequip one of the items and check that there's + // space for the other before trying to auto-equip + inv_body_loc mainHand = INVLOC_HAND_LEFT; + inv_body_loc offHand = INVLOC_HAND_RIGHT; + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + // No space to move right hand item to inventory, can we move the left instead? + std::swap(mainHand, offHand); + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + break; + } + } + if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { + // No space for the main hand item. Move the other item back to the off hand and abort. + player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; + player.RemoveInvItem(player._pNumInv - 1, false); + break; + } + RemoveEquipment(player, offHand, false); + invloc = mainHand; + } + break; + default: + // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. + failedSpeech = HeroSpeech::ICantDoThat; + break; + } + // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip + if (invloc != NUM_INVLOC + && !player.InvBody[invloc].isEmpty() + && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { + holdItem = player.InvBody[invloc].pop(); + } + automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; + player.RemoveInvItem(iv, false); + + // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv + if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { + holdItem.clear(); + } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. + } else if (!holdItem.isEmpty()) { + // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... + player.InvBody[invloc] = holdItem.pop(); + } + } + } else if (IsVisualStoreOpen && CanSellToCurrentVendor(player.InvList[iv]) && dropItem) { + // If visual store is open, ctrl-click sells the item + SellItemToVisualStore(iv); + automaticallyMoved = true; + } else { + holdItem = player.InvList[iv]; + player.RemoveInvItem(iv, false); + } + } + } + + if (r >= SLOTXY_BELT_FIRST) { + const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; + if (!beltItem.isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = beltItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } + } + } + + if (!holdItem.isEmpty()) { + if (holdItem._itype == ItemType::Gold) { + player._pGold = CalculateGold(player); + } + + CalcPlrInv(player, true); + holdItem._iStatFlag = player.CanUseItem(holdItem); + + if (&player == MyPlayer) { + PlaySFX(SfxID::GrabItem); + NewCursor(holdItem); + } + if (dropItem) { + TryDropItem(); + } + } else if (automaticMove) { + if (automaticallyMoved) { + CalcPlrInv(player, true); + } + if (attemptedMove && &player == MyPlayer) { + if (automaticallyMoved) { + PlaySFX(successSound); + } else { + player.SaySpecific(failedSpeech); + } + } + } +} + +void TryCombineNaKrulNotes(Player &player, Item ¬eItem) +{ + const int idx = noteItem.IDidx; + const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; + + if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { + return; + } + + for (const _item_indexes note : notes) { + if (idx != note && !HasInventoryItemWithId(player, note)) { + return; // the player doesn't have all notes + } + } + + MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); + + for (const _item_indexes note : notes) { + if (idx != note) { + RemoveInventoryItemById(player, note); + } + } + + const Point position = noteItem.position; // copy the position to restore it after re-initialising the item + noteItem = {}; + GetItemAttrs(noteItem, IDI_FULLNOTE, 16); + SetupItem(noteItem); + noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table +} + +void CheckQuestItem(Player &player, Item &questItem) +{ + const Player &myPlayer = *MyPlayer; + + if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE + && (questItem.IDidx == IDI_OPTAMULET + || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { + Quests[Q_BLIND]._qactive = QUEST_DONE; + NetSendCmdQuest(true, Quests[Q_BLIND]); + } + + if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { + player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP + Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; + NetSendCmdQuest(true, Quests[Q_MUSHROOM]); + } + + if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { + Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ANVIL]); + } + if (Quests[Q_ANVIL]._qlog) { + myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); + } + } + + if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { + myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); + } + + if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ROCK]._qactive == QUEST_INIT) { + Quests[Q_ROCK]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ROCK]); + } + if (Quests[Q_ROCK]._qlog) { + myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); + } + } + + if (Quests[Q_BLOOD]._qactive == QUEST_ACTIVE + && (questItem.IDidx == IDI_ARMOFVAL + || (Quests[Q_BLOOD].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 9, 3 })))) { + Quests[Q_BLOOD]._qactive = QUEST_DONE; + NetSendCmdQuest(true, Quests[Q_BLOOD]); + myPlayer.Say(HeroSpeech::MayTheSpiritOfArkaineProtectMe, 20); + } + + if (questItem.IDidx == IDI_MAPOFDOOM) { + Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; + if (Quests[Q_GRAVE]._qvar1 != 1) { + MyPlayer->Say(HeroSpeech::UhHuh, 10); + Quests[Q_GRAVE]._qvar1 = 1; + } + } + + TryCombineNaKrulNotes(player, questItem); +} + +void CleanupItems(int ii) +{ + const Item &item = Items[ii]; + dItem[item.position.x][item.position.y] = 0; + + if (CornerStone.isAvailable() && item.position == CornerStone.position) { + CornerStone.item.clear(); + CornerStone.item.selectionRegion = SelectionRegion::None; + CornerStone.item.position = { 0, 0 }; + CornerStone.item._iAnimFlag = false; + CornerStone.item._iIdentified = false; + CornerStone.item._iPostDraw = false; + } + + int i = 0; + while (i < ActiveItemCount) { + if (ActiveItems[i] == ii) { + DeleteItem(i); + i = 0; + continue; + } + + i++; + } +} + +bool CanUseStaff(Item &staff, SpellID spell) +{ + return !staff.isEmpty() + && IsAnyOf(staff._iMiscId, IMISC_STAFF, IMISC_UNIQUE) + && staff._iSpell == spell + && staff._iCharges > 0; +} + +void StartGoldDrop() +{ + CloseGoldWithdraw(); + + const int8_t invIndex = pcursinvitem; + + const Player &myPlayer = *MyPlayer; + + const int max = (invIndex <= INVITEM_INV_LAST) + ? myPlayer.InvList[invIndex - INVITEM_INV_FIRST]._ivalue + : myPlayer.SpdList[invIndex - INVITEM_BELT_FIRST]._ivalue; + + if (ChatFlag) + ResetChat(); + + const Point start = GetPanelPosition(UiPanels::Inventory, { 67, 128 }); + SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); + SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); + OpenGoldDrop(invIndex, max); +} + +int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) +{ + if (player.InvGrid[slotIndex] != 0) { + return value; + } + + Item &goldItem = player.InvList[player._pNumInv]; + MakeGoldStack(goldItem, std::min(value, MaxGold)); + player._pNumInv++; + player.InvGrid[slotIndex] = player._pNumInv; + if (&player == MyPlayer) { + NetSendCmdChInvItem(false, slotIndex); + } + + value -= goldItem._ivalue; + + return value; +} + +} // namespace + +void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) +{ + SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); + out.Clip(&srcRect, &targetPosition); + if (size.width <= 0 || size.height <= 0) + return; + + uint8_t colorShift; + switch (itemQuality) { + case ITEM_QUALITY_MAGIC: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BLUE : PAL16_ORANGE) - 1; + break; + case ITEM_QUALITY_UNIQUE: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_YELLOW : PAL16_ORANGE) - 1; + break; + default: + colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BEIGE : PAL16_ORANGE) - 1; + break; + } + + uint8_t *dst = &out[targetPosition]; + const auto dstPitch = out.pitch(); + for (int y = size.height; y != 0; --y, dst -= dstPitch + size.width) { + for (const uint8_t *end = dst + size.width; dst < end; ++dst) { + uint8_t &pix = *dst; + if (pix >= PAL16_GRAY) { + pix -= colorShift; + } + } + } +} + +bool CanBePlacedOnBelt(const Player &player, const Item &item) +{ + return FitsInBeltSlot(item) + && item._itype != ItemType::Gold + && player.CanUseItem(item) + && item.isUsable(); +} + +void FreeInvGFX() +{ + pInvCels = std::nullopt; +} + +void InitInv() +{ + const PlayerData &playerClassData = GetPlayerDataForClass(MyPlayer->_pClass); + const char *invName = playerClassData.inv.c_str(); + if (gbIsSpawn && (playerClassData.inv == "inv_rog" || playerClassData.inv == "inv_sor")) { + invName = "inv"; + } + pInvCels = LoadCel(StrCat("data\\inv\\", invName).c_str(), static_cast(SidePanelSize.width)); +} + +void DrawInv(const Surface &out) +{ + ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 0, 351 }), (*pInvCels)[0]); + + const Size slotSize[] = { + { 2, 2 }, // head + { 1, 1 }, // left ring + { 1, 1 }, // right ring + { 1, 1 }, // amulet + { 2, 3 }, // left hand + { 2, 3 }, // right hand + { 2, 3 }, // chest + }; + + const Point slotPos[] = { + { 133, 59 }, // head + { 48, 205 }, // left ring + { 249, 205 }, // right ring + { 205, 60 }, // amulet + { 17, 160 }, // left hand + { 248, 160 }, // right hand + { 133, 160 }, // chest + }; + + const Player &myPlayer = *InspectPlayer; + + for (int slot = INVLOC_HEAD; slot < NUM_INVLOC; slot++) { + if (!myPlayer.InvBody[slot].isEmpty()) { + int screenX = slotPos[slot].x; + int screenY = slotPos[slot].y; + InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, { screenX, screenY }), { slotSize[slot].width * InventorySlotSizeInPixels.width, slotSize[slot].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); + + const int cursId = myPlayer.InvBody[slot]._iCurs + CURSOR_FIRSTITEM; + + const Size frameSize = GetInvItemSize(cursId); + + // calc item offsets for weapons/armor smaller than 2x3 slots + if (IsAnyOf(slot, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST)) { + screenX += frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0; + screenY += frameSize.height == (3 * InventorySlotSizeInPixels.height) ? 0 : -INV_SLOT_HALF_SIZE_PX; + } + + const ClxSprite sprite = GetInvItemSprite(cursId); + const Point position = GetPanelPosition(UiPanels::Inventory, { screenX, screenY }); + + if (pcursinvitem == slot) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.InvBody[slot], true), position, sprite); + } + + DrawItem(myPlayer.InvBody[slot], out, position, sprite); + + if (slot == INVLOC_HAND_LEFT) { + if (myPlayer.GetItemLocation(myPlayer.InvBody[slot]) == ILOC_TWOHAND) { + InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, slotPos[INVLOC_HAND_RIGHT]), { slotSize[INVLOC_HAND_RIGHT].width * InventorySlotSizeInPixels.width, slotSize[INVLOC_HAND_RIGHT].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); + const int dstX = GetRightPanel().position.x + slotPos[INVLOC_HAND_RIGHT].x + (frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0) - 1; + const int dstY = GetRightPanel().position.y + slotPos[INVLOC_HAND_RIGHT].y; + ClxDrawBlended(out, { dstX, dstY }, sprite); + } + } + } + } + + for (int i = 0; i < InventoryGridCells; i++) { + if (myPlayer.InvGrid[i] != 0) { + InvDrawSlotBack( + out, + GetPanelPosition(UiPanels::Inventory, InvRect[i + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }, + InventorySlotSizeInPixels, + myPlayer.InvList[std::abs(myPlayer.InvGrid[i]) - 1]._iMagical); + } + } + + for (int j = 0; j < InventoryGridCells; j++) { + if (myPlayer.InvGrid[j] > 0) { // first slot of an item + const int ii = myPlayer.InvGrid[j] - 1; + const int cursId = myPlayer.InvList[ii]._iCurs + CURSOR_FIRSTITEM; + + const ClxSprite sprite = GetInvItemSprite(cursId); + const Point position = GetPanelPosition(UiPanels::Inventory, InvRect[j + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }; + if (pcursinvitem == ii + INVITEM_INV_FIRST) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.InvList[ii], true), position, sprite); + } + + DrawItem(myPlayer.InvList[ii], out, position, sprite); + } + } +} + +void DrawInvBelt(const Surface &out) +{ + if (ChatFlag) { + return; + } + + const Point mainPanelPosition = GetMainPanel().position; + + DrawPanelBox(out, { 205, 21, 232, 28 }, mainPanelPosition + Displacement { 205, 5 }); + + const Player &myPlayer = *InspectPlayer; + + for (int i = 0; i < MaxBeltItems; i++) { + if (myPlayer.SpdList[i].isEmpty()) { + continue; + } + + const Point position { InvRect[i + SLOTXY_BELT_FIRST].position.x + mainPanelPosition.x, InvRect[i + SLOTXY_BELT_FIRST].position.y + mainPanelPosition.y + InventorySlotSizeInPixels.height }; + InvDrawSlotBack(out, position, InventorySlotSizeInPixels, myPlayer.SpdList[i]._iMagical); + const int cursId = myPlayer.SpdList[i]._iCurs + CURSOR_FIRSTITEM; + + const ClxSprite sprite = GetInvItemSprite(cursId); + + if (pcursinvitem == i + INVITEM_BELT_FIRST) { + if (ControlMode == ControlTypes::KeyboardAndMouse || invflag) { + ClxDrawOutline(out, GetOutlineColor(myPlayer.SpdList[i], true), position, sprite); + } + } + + DrawItem(myPlayer.SpdList[i], out, position, sprite); + + if (myPlayer.SpdList[i].isUsable() + && myPlayer.SpdList[i]._itype != ItemType::Gold) { + auto beltKey = StrCat("BeltItem", i + 1); + std::string_view keyName = ControlMode == ControlTypes::Gamepad + ? GetOptions().Padmapper.InputNameForAction(beltKey, true) + : GetOptions().Keymapper.KeyNameForAction(beltKey); + + if (keyName.length() > 2) + keyName = {}; + + DrawString(out, keyName, { position - Displacement { 0, 12 }, InventorySlotSizeInPixels }, + { .flags = UiFlags::ColorWhite | UiFlags::AlignRight }); + } + } +} + +void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri) +{ + if (&player == MyPlayer) { + NetSendCmdDelItem(hiPri, bodyLocation); + } + + player.InvBody[bodyLocation].clear(); +} + +bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) +{ + if (!CanBePlacedOnBelt(player, item)) { + return false; + } + + for (Item &beltItem : player.SpdList) { + if (beltItem.isEmpty()) { + if (persistItem) { + beltItem = item; + player.CalcScrolls(); + RedrawComponent(PanelDrawComponent::Belt); + if (sendNetworkMessage) { + const auto beltIndex = static_cast(std::distance(&player.SpdList[0], &beltItem)); + NetSendCmdChBeltItem(false, beltIndex); + } + } + + return true; + } + } + + return false; +} + +bool AutoEquip(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) +{ + if (!CanEquip(item)) { + return false; + } + + for (int bodyLocation = INVLOC_HEAD; bodyLocation < NUM_INVLOC; bodyLocation++) { + if (AutoEquip(player, item, (inv_body_loc)bodyLocation, persistItem, sendNetworkMessage)) { + return true; + } + } + + return false; +} + +bool AutoEquipEnabled(const Player &player, const Item &item) +{ + if (item.isWeapon()) { + // Monk can use unarmed attack as an encouraged option, thus we do not automatically equip weapons on him so as to not + // annoy players who prefer that playstyle. + return player._pClass != HeroClass::Monk && *GetOptions().Gameplay.autoEquipWeapons; + } + + if (item.isArmor()) { + return *GetOptions().Gameplay.autoEquipArmor; + } + + if (item.isHelm()) { + return *GetOptions().Gameplay.autoEquipHelms; + } + + if (item.isShield()) { + return *GetOptions().Gameplay.autoEquipShields; + } + + if (item.isJewelry()) { + return *GetOptions().Gameplay.autoEquipJewelry; + } + + return true; +} + +bool CanFitItemInInventory(const Player &player, const Item &item) +{ + return static_cast(FindSlotForItem(player, GetInventorySize(item))); +} + +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) +{ + const Size itemSize = GetInventorySize(item); + std::optional targetSlot = FindSlotForItem(player, itemSize); + + if (targetSlot) { + player.InvList[player._pNumInv] = item; + player._pNumInv++; + + AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); + player.CalcScrolls(); + + return true; + } + + return false; +} + +std::vector SortItemsBySize(Player &player) +{ + std::vector> itemSizes; // Pair of item size and its index in InvList + itemSizes.reserve(player._pNumInv); // Reserves space for the number of items in the player's inventory + + for (int i = 0; i < player._pNumInv; i++) { + const Size size = GetInventorySize(player.InvList[i]); + itemSizes.emplace_back(size, i); + } + + // Sort items by height first, then by width + std::sort(itemSizes.begin(), itemSizes.end(), [](const auto &a, const auto &b) { + if (a.first.height == b.first.height) return a.first.width > b.first.width; + return a.first.height > b.first.height; + }); + + // Extract sorted indices + std::vector sortedIndices; + sortedIndices.reserve(itemSizes.size()); // Pre-allocate the necessary capacity + + for (const auto &itemSize : itemSizes) { + sortedIndices.push_back(itemSize.second); + } + + return sortedIndices; +} + +void ReorganizeInventory(Player &player) +{ + // Sort items by size + const std::vector sortedIndices = SortItemsBySize(player); + + // Temporary storage for items and a copy of InvGrid + std::vector tempStorage(player._pNumInv); + std::array originalInvGrid; // Declare an array for InvGrid copy + std::copy(std::begin(player.InvGrid), std::end(player.InvGrid), std::begin(originalInvGrid)); // Copy InvGrid to originalInvGrid + + // Move items to temporary storage and clear inventory slots + for (int i = 0; i < player._pNumInv; ++i) { + tempStorage[i] = player.InvList[i]; + player.InvList[i] = {}; + } + player._pNumInv = 0; // Reset inventory count + std::fill(std::begin(player.InvGrid), std::end(player.InvGrid), 0); // Clear InvGrid + + // Attempt to place items back, now from the temp storage + bool reorganizationFailed = false; + for (const int index : sortedIndices) { + const Item &item = tempStorage[index]; + if (!AutoPlaceItemInInventory(player, item, false)) { + reorganizationFailed = true; + break; + } + } + + // If reorganization failed, restore items and InvGrid from tempStorage and originalInvGrid + if (reorganizationFailed) { + for (const Item &item : tempStorage) { + if (!item.isEmpty()) { + player.InvList[player._pNumInv++] = item; + } + } + std::copy(std::begin(originalInvGrid), std::end(originalInvGrid), std::begin(player.InvGrid)); // Restore InvGrid + } +} + +int RoomForGold() +{ + int amount = 0; + for (const int8_t &itemIndex : MyPlayer->InvGrid) { + if (itemIndex < 0) { + continue; + } + if (itemIndex == 0) { + amount += MaxGold; + continue; + } + + const Item &goldItem = MyPlayer->InvList[itemIndex - 1]; + if (goldItem._itype != ItemType::Gold || goldItem._ivalue == MaxGold) { + continue; + } + + amount += MaxGold - goldItem._ivalue; + } + + return amount; +} + +int AddGoldToInventory(Player &player, int value) +{ + // Top off existing piles + for (int i = 0; i < player._pNumInv && value > 0; i++) { + Item &goldItem = player.InvList[i]; + if (goldItem._itype != ItemType::Gold || goldItem._ivalue >= MaxGold) { + continue; + } + + if (goldItem._ivalue + value > MaxGold) { + value -= MaxGold - goldItem._ivalue; + goldItem._ivalue = MaxGold; + } else { + goldItem._ivalue += value; + value = 0; + } + + NetSyncInvItem(player, i); + SetPlrHandGoldCurs(goldItem); + } + + // Last row right to left + for (int i = 39; i >= 30 && value > 0; i--) { + value = CreateGoldItemInInventorySlot(player, i, value); + } + + // Remaining inventory in columns, bottom to top, right to left + for (int x = 9; x >= 0 && value > 0; x--) { + for (int y = 2; y >= 0 && value > 0; y--) { + value = CreateGoldItemInInventorySlot(player, (10 * y) + x, value); + } + } + + return value; +} + +bool GoldAutoPlace(Player &player, Item &goldStack) +{ + goldStack._ivalue = AddGoldToInventory(player, goldStack._ivalue); + SetPlrHandGoldCurs(goldStack); + + player._pGold = CalculateGold(player); + + return goldStack._ivalue == 0; +} + +void CheckInvSwap(Player &player, inv_body_loc bLoc) +{ + const Item &item = player.InvBody[bLoc]; + + if (bLoc == INVLOC_HAND_LEFT && player.GetItemLocation(item) == ILOC_TWOHAND) { + player.InvBody[INVLOC_HAND_RIGHT].clear(); + } else if (bLoc == INVLOC_HAND_RIGHT && player.GetItemLocation(item) == ILOC_TWOHAND) { + player.InvBody[INVLOC_HAND_LEFT].clear(); + } + + CalcPlrInv(player, true); +} + +void inv_update_rem_item(Player &player, inv_body_loc iv) +{ + player.InvBody[iv].clear(); + + CalcPlrInv(player, player._pmode != PM_DEATH); +} + +void CheckInvSwap(Player &player, const Item &item, int invGridIndex) +{ + Size itemSize = GetInventorySize(item); + + const int pitch = 10; + const int invListIndex = [&]() -> int { + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + const int gridIndex = rowGridIndex + x; + if (player.InvGrid[gridIndex] != 0) + return std::abs(player.InvGrid[gridIndex]); + } + } + player._pNumInv++; + return player._pNumInv; + }(); + + if (invListIndex < player._pNumInv) { + for (int8_t &itemIndex : player.InvGrid) { + if (itemIndex == invListIndex) + itemIndex = 0; + if (itemIndex == -invListIndex) + itemIndex = 0; + } + } + + player.InvList[invListIndex - 1] = item; + + for (int y = 0; y < itemSize.height; y++) { + const int rowGridIndex = invGridIndex + (pitch * y); + for (int x = 0; x < itemSize.width; x++) { + if (x == 0 && y == itemSize.height - 1) + player.InvGrid[rowGridIndex + x] = invListIndex; + else + player.InvGrid[rowGridIndex + x] = -invListIndex; + } + } + + CalcPlrInv(player, true); +} + +void CheckInvRemove(Player &player, int invGridIndex) +{ + const int invListIndex = std::abs(player.InvGrid[invGridIndex]) - 1; + + if (invListIndex >= 0) { + player.RemoveInvItem(invListIndex); + } +} + +void TransferItemToStash(Player &player, int location) +{ + if (location == -1) { + return; + } + + const Item &item = GetInventoryItem(player, location); + if (!AutoPlaceItemInStash(item, true)) { + player.SaySpecific(HeroSpeech::WhereWouldIPutThis); + return; + } + + PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); + + if (location < INVITEM_INV_FIRST) { + RemoveEquipment(player, static_cast(location), false); + CalcPlrInv(player, true); + } else if (location <= INVITEM_INV_LAST) + player.RemoveInvItem(location - INVITEM_INV_FIRST); + else + player.RemoveSpdBarItem(location - INVITEM_BELT_FIRST); +} + +void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) +{ + if (IsInspectingPlayer()) + return; + if (!MyPlayer->HoldItem.isEmpty()) { + CheckInvPaste(*MyPlayer, MousePosition); + } else if (IsStashOpen && isCtrlHeld) { + TransferItemToStash(*MyPlayer, pcursinvitem); + } else { + CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); + } +} + +void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld) +{ + const Point mainPanelPosition = GetMainPanel().position; + if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x + && MousePosition.y > mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) { + CheckInvItem(isShiftHeld, isCtrlHeld); + } +} + +void InvGetItem(Player &player, int ii) +{ + Item &item = Items[ii]; + CloseGoldDrop(); + + if (dItem[item.position.x][item.position.y] == 0) + return; + + item._iCreateInfo &= ~CF_PREGEN; + CheckQuestItem(player, item); + item.updateRequiredStatsCacheForPlayer(player); + + if (item._itype == ItemType::Gold && GoldAutoPlace(player, item)) { + if (MyPlayer == &player) { + // Non-gold items (or gold when you have a full inventory) go to the hand then provide audible feedback on + // paste. To give the same feedback for auto-placed gold we play the sound effect now. + PlaySFX(SfxID::ItemGold); + } + } else { + // The item needs to go into the players hand + if (MyPlayer == &player && !player.HoldItem.isEmpty()) { + // drop whatever the player is currently holding + NetSendCmdPItem(true, CMD_SYNCPUTITEM, player.position.tile, player.HoldItem); + } + + // need to copy here instead of move so CleanupItems still has access to the position + player.HoldItem = item; + NewCursor(player.HoldItem); + } + + // This potentially moves items in memory so must be done after we've made a copy + CleanupItems(ii); + pcursitem = -1; +} + +std::optional FindAdjacentPositionForItem(Point origin, Direction facing) +{ + if (ActiveItemCount >= MAXITEMS) + return {}; + + if (CanPut(origin + facing)) + return origin + facing; + + if (CanPut(origin + Left(facing))) + return origin + Left(facing); + + if (CanPut(origin + Right(facing))) + return origin + Right(facing); + + if (CanPut(origin + Left(Left(facing)))) + return origin + Left(Left(facing)); + + if (CanPut(origin + Right(Right(facing)))) + return origin + Right(Right(facing)); + + if (CanPut(origin + Left(Left(Left(facing))))) + return origin + Left(Left(Left(facing))); + + if (CanPut(origin + Right(Right(Right(facing))))) + return origin + Right(Right(Right(facing))); + + if (CanPut(origin + Opposite(facing))) + return origin + Opposite(facing); + + if (CanPut(origin)) + return origin; + + return {}; +} + +void AutoGetItem(Player &player, Item *itemPointer, int ii) +{ + Item &item = *itemPointer; + + CloseGoldDrop(); + + if (dItem[item.position.x][item.position.y] == 0) + return; + + item._iCreateInfo &= ~CF_PREGEN; + CheckQuestItem(player, item); + item.updateRequiredStatsCacheForPlayer(player); + + bool done; + bool autoEquipped = false; + + if (item._itype == ItemType::Gold) { + done = GoldAutoPlace(player, item); + if (!done) { + SetPlrHandGoldCurs(item); + } + } else { + done = AutoEquipEnabled(player, item) && AutoEquip(player, item, true, &player == MyPlayer); + if (done) { + autoEquipped = true; + } + + if (!done) { + done = AutoPlaceItemInBelt(player, item, true, &player == MyPlayer); + } + if (!done) { + done = AutoPlaceItemInInventory(player, item, &player == MyPlayer); + } + } + + if (done) { + if (!autoEquipped && *GetOptions().Audio.itemPickupSound && &player == MyPlayer) { + PlaySFX(SfxID::GrabItem); + } + + CleanupItems(ii); + return; + } + + if (&player == MyPlayer) { + player.Say(HeroSpeech::ICantCarryAnymore); + } + RespawnItem(item, true); + NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); +} + +int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t createInfo) +{ + for (uint8_t i = 0; i < ActiveItemCount; i++) { + const Item &item = Items[ActiveItems[i]]; + if (item.keyAttributesMatch(iseed, idx, createInfo)) { + return i; + } + } + + return -1; +} + +void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci) +{ + // Check what the local client has at the target position + int ii = dItem[position.x][position.y] - 1; + + if (ii >= 0 && ii < MAXITEMS) { + // If there was an item there, check that it's the same item as the remote player has + if (!Items[ii].keyAttributesMatch(iseed, idx, ci)) { + // Key attributes don't match so we must've desynced, ignore this index and try find a matching item via lookup + ii = -1; + } + } + + if (ii == -1) { + // Either there's no item at the expected position or it doesn't match what is being picked up, so look for an item that matches the key attributes + ii = FindGetItem(iseed, idx, ci); + + if (ii != -1) { + // Translate to Items index for CleanupItems, FindGetItem returns an ActiveItems index + ii = ActiveItems[ii]; + } + } + + if (ii == -1) { + // Still can't find the expected item, assume it was collected earlier and this caused the desync + return; + } + + CleanupItems(ii); +} + +bool CanPut(Point position) +{ + if (!InDungeonBounds(position)) { + return false; + } + + if (IsTileSolid(position)) { + return false; + } + + if (dItem[position.x][position.y] != 0) { + return false; + } + + if (leveltype == DTYPE_TOWN) { + if (dMonster[position.x][position.y] != 0) { + return false; + } + if (dMonster[position.x + 1][position.y + 1] != 0) { + return false; + } + } + + if (IsItemBlockingObjectAtPosition(position)) { + return false; + } + + return true; +} + +int ClampDurability(const Item &item, int durability) +{ + if (item._iMaxDur == 0) + return 0; + + return std::clamp(durability, 1, item._iMaxDur); +} + +int16_t ClampToHit(const Item &item, int16_t toHit) +{ + if (toHit < item._iPLToHit || toHit > 51) + return item._iPLToHit; + + return toHit; +} + +uint8_t ClampMaxDam(const Item &item, uint8_t maxDam) +{ + if (maxDam < item._iMaxDam || maxDam - item._iMinDam > 30) + return item._iMaxDam; + + return maxDam; +} + +int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam) +{ + if (ActiveItemCount >= MAXITEMS) + return -1; + + Item item; + + RecreateItem(*MyPlayer, item, idx, icreateinfo, iseed, ivalue, ibuff); + if (id != 0) + item._iIdentified = true; + item._iMaxDur = mdur; + item._iDurability = ClampDurability(item, dur); + item._iMaxCharges = std::clamp(mch, 0, item._iMaxCharges); + item._iCharges = std::clamp(ch, 0, item._iMaxCharges); + if (gbIsHellfire) { + item._iPLToHit = ClampToHit(item, toHit); + item._iMaxDam = ClampMaxDam(item, maxDam); + } + + return PlaceItemInWorld(std::move(item), position); +} + +int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname) +{ + if (ActiveItemCount >= MAXITEMS) + return -1; + + Item item; + RecreateEar(item, icreateinfo, iseed, cursval, heroname); + + return PlaceItemInWorld(std::move(item), position); +} + +int8_t CheckInvHLight() +{ + int8_t r = 0; + for (; r < NUM_XY_SLOTS; r++) { + int xo = GetRightPanel().position.x; + int yo = GetRightPanel().position.y; + if (r >= SLOTXY_BELT_FIRST) { + xo = GetMainPanel().position.x; + yo = GetMainPanel().position.y; + } + + if (InvRect[r].contains(MousePosition - Displacement(xo, yo))) { + break; + } + } + + if (r >= NUM_XY_SLOTS) + return -1; + + int8_t rv = -1; + InfoColor = UiFlags::ColorWhite; + Item *pi = nullptr; + Player &myPlayer = *InspectPlayer; + + if (r == SLOTXY_HEAD) { + rv = INVLOC_HEAD; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_RING_LEFT) { + rv = INVLOC_RING_LEFT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_RING_RIGHT) { + rv = INVLOC_RING_RIGHT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_AMULET) { + rv = INVLOC_AMULET; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_HAND_LEFT) { + rv = INVLOC_HAND_LEFT; + pi = &myPlayer.InvBody[rv]; + } else if (r == SLOTXY_HAND_RIGHT) { + pi = &myPlayer.InvBody[INVLOC_HAND_LEFT]; + if (pi->isEmpty() || myPlayer.GetItemLocation(*pi) != ILOC_TWOHAND) { + rv = INVLOC_HAND_RIGHT; + pi = &myPlayer.InvBody[rv]; + } else { + rv = INVLOC_HAND_LEFT; + } + } else if (r == SLOTXY_CHEST) { + rv = INVLOC_CHEST; + pi = &myPlayer.InvBody[rv]; + } else if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { + const int8_t itemId = std::abs(myPlayer.InvGrid[r - SLOTXY_INV_FIRST]); + if (itemId == 0) + return -1; + const int ii = itemId - 1; + rv = ii + INVITEM_INV_FIRST; + pi = &myPlayer.InvList[ii]; + } else if (r >= SLOTXY_BELT_FIRST) { + r -= SLOTXY_BELT_FIRST; + RedrawComponent(PanelDrawComponent::Belt); + pi = &myPlayer.SpdList[r]; + if (pi->isEmpty()) + return -1; + rv = r + INVITEM_BELT_FIRST; + } + + if (pi->isEmpty()) + return -1; + + if (IsVisualStoreOpen && pcurs == CURSOR_REPAIR) { + InfoColor = pi->getTextColor(); + InfoString = pi->getName(); + FloatingInfoString = pi->getName(); + if (pi->_iIdentified) { + PrintItemDetails(*pi); + } else { + PrintItemDur(*pi); + } + int cost = GetRepairCost(*pi); + if (cost > 0) + AddInfoBoxString(StrCat(FormatInteger(cost), " Gold")); + else + AddInfoBoxString(_("Fully Repaired")); + } else if (pi->_itype == ItemType::Gold) { + const int nGold = pi->_ivalue; + InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); + } else { + InfoColor = pi->getTextColor(); + InfoString = pi->getName(); + FloatingInfoString = pi->getName(); + if (pi->_iIdentified) { + PrintItemDetails(*pi); + } else { + PrintItemDur(*pi); + } + } + + return rv; +} + +void ConsumeScroll(Player &player) +{ + const SpellID spellId = player.executedSpell.spellId; + + const auto isCurrentSpell = [spellId](const Item &item) -> bool { + return item.isScrollOf(spellId) || item.isRuneOf(spellId); + }; + + // Try to remove the scroll from selected inventory slot + const int8_t itemSlot = player.executedSpell.spellFrom; + if (itemSlot >= INVITEM_INV_FIRST && itemSlot <= INVITEM_INV_LAST) { + const int itemIndex = itemSlot - INVITEM_INV_FIRST; + const Item *item = &player.InvList[itemIndex]; + if (!item->isEmpty() && isCurrentSpell(*item)) { + player.RemoveInvItem(itemIndex); + return; + } + } else if (itemSlot >= INVITEM_BELT_FIRST && itemSlot <= INVITEM_BELT_LAST) { + const int itemIndex = itemSlot - INVITEM_BELT_FIRST; + const Item *item = &player.SpdList[itemIndex]; + if (!item->isEmpty() && isCurrentSpell(*item)) { + player.RemoveSpdBarItem(itemIndex); + return; + } + } else if (itemSlot != 0) { + app_fatal(StrCat("ConsumeScroll: Invalid item index ", itemSlot)); + } + + // Didn't find it at the selected slot, take the first one we find + // This path is always used when the scroll is consumed via spell selection + RemoveInventoryOrBeltItem(player, isCurrentSpell); +} + +bool CanUseScroll(Player &player, SpellID spell) +{ + if (leveltype == DTYPE_TOWN && !GetSpellData(spell).isAllowedInTown()) + return false; + + return HasInventoryOrBeltItem(player, [spell](const Item &item) { + return item.isScrollOf(spell) || item.isRuneOf(spell); + }); +} + +void ConsumeStaffCharge(Player &player) +{ + Item &staff = player.InvBody[INVLOC_HAND_LEFT]; + + if (!CanUseStaff(staff, player.executedSpell.spellId)) + return; + + staff._iCharges--; + CalcPlrInv(player, false); +} + +bool CanUseStaff(Player &player, SpellID spellId) +{ + return CanUseStaff(player.InvBody[INVLOC_HAND_LEFT], spellId); +} + +Item &GetInventoryItem(Player &player, int location) +{ + if (location < INVITEM_INV_FIRST) + return player.InvBody[location]; + + if (location <= INVITEM_INV_LAST) + return player.InvList[location - INVITEM_INV_FIRST]; + + return player.SpdList[location - INVITEM_BELT_FIRST]; +} + +bool UseInvItem(int cii) +{ + if (IsInspectingPlayer()) + return false; + + Player &player = *MyPlayer; + + if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) + return true; + if (pcurs != CURSOR_HAND) + return true; + if (IsPlayerInStore()) + return true; + if (cii < INVITEM_INV_FIRST) + return false; + + bool speedlist = false; + int c; + Item *item; + if (cii <= INVITEM_INV_LAST) { + c = cii - INVITEM_INV_FIRST; + item = &player.InvList[c]; + } else { + if (ChatFlag) + return true; + c = cii - INVITEM_BELT_FIRST; + + item = &player.SpdList[c]; + speedlist = true; + + // If selected speedlist item exists in InvList, use the InvList item. + for (int i = 0; i < player._pNumInv && *GetOptions().Gameplay.autoRefillBelt; i++) { + if (player.InvList[i]._iMiscId == item->_iMiscId && player.InvList[i]._iSpell == item->_iSpell) { + c = i; + item = &player.InvList[c]; + cii = c + INVITEM_INV_FIRST; + speedlist = false; + break; + } + } + + // If speedlist item is not inventory, use same item at the end of the speedlist if exists. + if (speedlist && *GetOptions().Gameplay.autoRefillBelt) { + for (int i = INVITEM_BELT_LAST - INVITEM_BELT_FIRST; i > c; i--) { + Item &candidate = player.SpdList[i]; + + if (!candidate.isEmpty() && candidate._iMiscId == item->_iMiscId && candidate._iSpell == item->_iSpell) { + c = i; + cii = c + INVITEM_BELT_FIRST; + item = &candidate; + break; + } + } + } + } + + constexpr int SpeechDelay = 10; + if (item->IDidx == IDI_MUSHROOM) { + player.Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); + return true; + } + if (item->IDidx == IDI_FUNGALTM) { + + PlaySFX(SfxID::ItemBook); + player.Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); + return true; + } + + if (player.isOnLevel(0)) { + if (UseItemOpensHive(*item, player.position.tile)) { + OpenHive(); + player.RemoveInvItem(c); + return true; + } + if (UseItemOpensGrave(*item, player.position.tile)) { + OpenGrave(); + player.RemoveInvItem(c); + return true; + } + } + + if (!item->isUsable()) + return false; + + if (!player.CanUseItem(*item)) { + player.Say(HeroSpeech::ICantUseThisYet); + return true; + } + + if (item->_iMiscId == IMISC_NONE && item->_itype == ItemType::Gold) { + StartGoldDrop(); + return true; + } + + CloseGoldDrop(); + + if (item->isScroll() && leveltype == DTYPE_TOWN && !GetSpellData(item->_iSpell).isAllowedInTown()) { + return true; + } + + if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { + return true; + } + + if (item->_iMiscId == IMISC_ARENAPOT && !player.isOnArenaLevel()) { + player.Say(HeroSpeech::ThatWontWorkHere); + return true; + } + + const int idata = ItemCAnimTbl[item->_iCurs]; + if (item->_iMiscId == IMISC_BOOK) + PlaySFX(SfxID::ReadBook); + else if (&player == MyPlayer) + PlaySFX(ItemInvSnds[idata]); + + UseItem(player, item->_iMiscId, item->_iSpell, cii); + + if (speedlist) { + if (player.SpdList[c]._iMiscId == IMISC_NOTE) { + InitQTextMsg(TEXT_BOOK9); + CloseInventory(); + return true; + } + if (!item->isScroll() && !item->isRune()) + player.RemoveSpdBarItem(c); + return true; + } + if (player.InvList[c]._iMiscId == IMISC_MAPOFDOOM) + return true; + if (player.InvList[c]._iMiscId == IMISC_NOTE) { + InitQTextMsg(TEXT_BOOK9); + CloseInventory(); + return true; + } + if (!item->isScroll() && !item->isRune()) + player.RemoveInvItem(c); + + return true; +} + +void CloseInventory() +{ + CloseGoldWithdraw(); + CloseStash(); + CloseVisualStore(); + invflag = false; +} + +void CloseStash() +{ + if (!IsStashOpen) + return; + + Player &myPlayer = *MyPlayer; + if (!myPlayer.HoldItem.isEmpty()) { + std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); + if (itemTile) { + NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); + } else { + if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) + && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true) + && !AutoPlaceItemInStash(myPlayer.HoldItem, true)) { + // This can fail for max gold, arena potions and a stash that has been arranged + // to not have room for the item all 3 cases are extremely unlikely + app_fatal(_("No room for item")); + } + PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); + } + myPlayer.HoldItem.clear(); + NewCursor(CURSOR_HAND); + } + + IsStashOpen = false; +} + +void DoTelekinesis() +{ + if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) + NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJT, cursPosition); + if (pcursitem != -1) + NetSendCmdGItem(true, CMD_REQUESTAGITEM, *MyPlayer, pcursitem); + if (pcursmonst != -1) { + const Monster &monter = Monsters[pcursmonst]; + if (!M_Talker(monter) && monter.talkMsg == TEXT_NONE) + NetSendCmdParam1(true, CMD_KNOCKBACK, pcursmonst); + } + NewCursor(CURSOR_HAND); +} + +int CalculateGold(Player &player) +{ + int gold = 0; + + for (int i = 0; i < player._pNumInv; i++) { + if (player.InvList[i]._itype == ItemType::Gold) + gold += player.InvList[i]._ivalue; + } + + return gold; +} + +Size GetInventorySize(const Item &item) +{ + const Size size = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); + + return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; +} + +} // namespace devilution diff --git a/Source/lua/lua_global.hpp b/Source/lua/lua_global.hpp index a14a7951797..d7812f1a74f 100644 --- a/Source/lua/lua_global.hpp +++ b/Source/lua/lua_global.hpp @@ -1,23 +1,23 @@ -#pragma once - -#include - -#include -#include -#include - -namespace devilution { - -void LuaInitialize(); -void LuaReloadActiveMods(); -void LuaShutdown(); - -sol::state &GetLuaState(); -sol::environment CreateLuaSandbox(); -sol::object SafeCallResult(sol::protected_function_result result, bool optional); -sol::table *GetLuaEvents(); - -/** Adds a handler to be called when mods status changes after the initial startup. */ -void AddModsChangedHandler(tl::function_ref callback); - -} // namespace devilution +#pragma once + +#include + +#include +#include +#include + +namespace devilution { + +void LuaInitialize(); +void LuaReloadActiveMods(); +void LuaShutdown(); + +sol::state &GetLuaState(); +sol::environment CreateLuaSandbox(); +sol::object SafeCallResult(sol::protected_function_result result, bool optional); +sol::table *GetLuaEvents(); + +/** 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/utils/screen_reader.cpp b/Source/utils/screen_reader.cpp index d080c24ce63..2c75491cf76 100644 --- a/Source/utils/screen_reader.cpp +++ b/Source/utils/screen_reader.cpp @@ -1,55 +1,55 @@ -#include "utils/screen_reader.hpp" - -#include -#include - -#ifdef _WIN32 -#include "utils/file_util.h" -#include -#else -#include -#endif - -namespace devilution { - -#ifndef _WIN32 -SPDConnection *Speechd; -#endif - -void InitializeScreenReader() -{ -#ifdef _WIN32 - Tolk_Load(); -#else - Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); -#endif -} - -void ShutDownScreenReader() -{ -#ifdef _WIN32 - Tolk_Unload(); -#else - spd_close(Speechd); -#endif -} - -void SpeakText(std::string_view text, bool force) -{ - static std::string SpokenText; - - if (!force && SpokenText == text) - return; - - SpokenText = text; - -#ifdef _WIN32 - const auto textUtf16 = ToWideChar(SpokenText); - if (textUtf16 != nullptr) - Tolk_Output(textUtf16.get(), true); -#else - spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); -#endif -} - -} // namespace devilution +#include "utils/screen_reader.hpp" + +#include +#include + +#ifdef _WIN32 +#include "utils/file_util.h" +#include +#else +#include +#endif + +namespace devilution { + +#ifndef _WIN32 +SPDConnection *Speechd; +#endif + +void InitializeScreenReader() +{ +#ifdef _WIN32 + Tolk_Load(); +#else + Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); +#endif +} + +void ShutDownScreenReader() +{ +#ifdef _WIN32 + Tolk_Unload(); +#else + spd_close(Speechd); +#endif +} + +void SpeakText(std::string_view text, bool force) +{ + static std::string SpokenText; + + if (!force && SpokenText == text) + return; + + SpokenText = text; + +#ifdef _WIN32 + const auto textUtf16 = ToWideChar(SpokenText); + if (textUtf16 != nullptr) + Tolk_Output(textUtf16.get(), true); +#else + spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); +#endif +} + +} // namespace devilution diff --git a/Source/utils/screen_reader.hpp b/Source/utils/screen_reader.hpp index 84966a57bdd..5be8f97aa69 100644 --- a/Source/utils/screen_reader.hpp +++ b/Source/utils/screen_reader.hpp @@ -1,25 +1,25 @@ -#pragma once - -#include - -namespace devilution { - -#ifdef SCREEN_READER_INTEGRATION -void InitializeScreenReader(); -void ShutDownScreenReader(); -void SpeakText(std::string_view text, bool force = false); -#else -constexpr void InitializeScreenReader() -{ -} - -constexpr void ShutDownScreenReader() -{ -} - -constexpr void SpeakText(std::string_view text, bool force = false) -{ -} -#endif - -} // namespace devilution +#pragma once + +#include + +namespace devilution { + +#ifdef SCREEN_READER_INTEGRATION +void InitializeScreenReader(); +void ShutDownScreenReader(); +void SpeakText(std::string_view text, bool force = false); +#else +constexpr void InitializeScreenReader() +{ +} + +constexpr void ShutDownScreenReader() +{ +} + +constexpr void SpeakText(std::string_view text, bool force = false) +{ +} +#endif + +} // namespace devilution