diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 93bd22a71..0646bd366 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -41,6 +41,7 @@ set(libdevilutionx_SRCS control/control_infobox.cpp control/control_panel.cpp + controls/accessibility_keys.cpp controls/axis_direction.cpp controls/controller_motion.cpp controls/controller.cpp @@ -51,6 +52,8 @@ set(libdevilutionx_SRCS controls/menu_controls.cpp controls/modifier_hints.cpp controls/plrctrls.cpp + controls/town_npc_nav.cpp + controls/tracker.cpp DiabloUI/button.cpp DiabloUI/credits.cpp @@ -160,13 +163,16 @@ set(libdevilutionx_SRCS tables/textdat.cpp tables/townerdat.cpp + utils/accessibility_announcements.cpp utils/display.cpp - utils/language.cpp - utils/proximity_audio.cpp - utils/sdl_bilinear_scale.cpp - utils/sdl_thread.cpp - utils/surface_to_clx.cpp - utils/timer.cpp) + utils/language.cpp + utils/navigation_speech.cpp + utils/proximity_audio.cpp + utils/sdl_bilinear_scale.cpp + utils/sdl_thread.cpp + utils/surface_to_clx.cpp + utils/timer.cpp + utils/walk_path_speech.cpp) # These files are responsible for most of the runtime in Debug mode. # Apply some optimizations to them even in Debug mode to get reasonable performance. @@ -739,15 +745,15 @@ target_link_dependencies(libdevilutionx_utf8 PRIVATE SheenBidi::SheenBidi ) -if(NOSOUND) - add_devilutionx_object_library(libdevilutionx_sound - effects_stubs.cpp - engine/sound_pool_stubs.cpp - engine/sound_stubs.cpp - ) - target_link_dependencies(libdevilutionx_sound PUBLIC - DevilutionX::SDL - fmt::fmt +if(NOSOUND) + add_devilutionx_object_library(libdevilutionx_sound + effects_stubs.cpp + engine/sound_pool_stubs.cpp + engine/sound_stubs.cpp + ) + target_link_dependencies(libdevilutionx_sound PUBLIC + DevilutionX::SDL + fmt::fmt magic_enum::magic_enum tl unordered_dense::unordered_dense @@ -755,15 +761,15 @@ if(NOSOUND) libdevilutionx_random libdevilutionx_sdl2_to_1_2_backports ) -else() - add_devilutionx_object_library(libdevilutionx_sound - effects.cpp - engine/sound_pool.cpp - engine/sound.cpp - utils/soundsample.cpp - ) - if(USE_SDL3) - target_link_dependencies(libdevilutionx_sound PUBLIC +else() + add_devilutionx_object_library(libdevilutionx_sound + effects.cpp + engine/sound_pool.cpp + engine/sound.cpp + utils/soundsample.cpp + ) + if(USE_SDL3) + target_link_dependencies(libdevilutionx_sound PUBLIC SDL3_mixer::SDL3_mixer ) else() diff --git a/Source/controls/accessibility_keys.cpp b/Source/controls/accessibility_keys.cpp new file mode 100644 index 000000000..3e076ce03 --- /dev/null +++ b/Source/controls/accessibility_keys.cpp @@ -0,0 +1,340 @@ +/** + * @file controls/accessibility_keys.cpp + * + * UI accessibility key handlers and action-guard helpers. + */ +#include "controls/accessibility_keys.hpp" + +#include +#include +#include +#include + +#include + +#include "control/control.hpp" +#ifdef USE_SDL3 +#include +#else +#include +#endif + +#include "controls/plrctrls.h" +#include "utils/sdl_compat.h" +#include "diablo.h" +#include "gamemenu.h" +#include "help.h" +#include "inv.h" +#include "levels/gendung.h" +#include "levels/setmaps.h" +#include "minitext.h" +#include "options.h" +#include "panels/charpanel.hpp" +#include "panels/partypanel.hpp" +#include "panels/spell_book.hpp" +#include "panels/spell_list.hpp" +#include "player.h" +#include "qol/chatlog.h" +#include "qol/stash.h" +#include "quests.h" +#include "stores.h" +#include "utils/format_int.hpp" +#include "utils/language.h" +#include "utils/screen_reader.hpp" +#include "utils/str_cat.hpp" + +namespace devilution { + +namespace { + +/** Computes a rounded percentage (0--100) from a current and maximum value. */ +int ComputePercentage(int current, int maximum) +{ + const int clamped = std::max(current, 0); + int percent = static_cast((static_cast(clamped) * 100 + maximum / 2) / maximum); + return std::clamp(percent, 0, 100); +} + +} // namespace + +void SpeakPlayerHealthPercentageKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (MyPlayer == nullptr) + return; + + const SDL_Keymod modState = SDL_GetModState(); + const bool speakMana = (modState & SDL_KMOD_SHIFT) != 0; + if (speakMana) { + if (MyPlayer->_pMaxMana <= 0) + return; + SpeakText(fmt::format("{:d}%", ComputePercentage(MyPlayer->_pMana, MyPlayer->_pMaxMana)), /*force=*/true); + return; + } + + if (MyPlayer->_pMaxHP <= 0) + return; + SpeakText(fmt::format("{:d}%", ComputePercentage(MyPlayer->_pHitPoints, MyPlayer->_pMaxHP)), /*force=*/true); +} + +void SpeakExperienceToNextLevelKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (MyPlayer == nullptr) + return; + + const Player &myPlayer = *MyPlayer; + if (myPlayer.isMaxCharacterLevel()) { + SpeakText(_("Max level."), /*force=*/true); + return; + } + + const uint32_t nextExperienceThreshold = myPlayer.getNextExperienceThreshold(); + const uint32_t currentExperience = myPlayer._pExperience; + const uint32_t remainingExperience = currentExperience >= nextExperienceThreshold ? 0 : nextExperienceThreshold - currentExperience; + const int nextLevel = myPlayer.getCharacterLevel() + 1; + SpeakText( + fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(remainingExperience), nextLevel), + /*force=*/true); +} + +std::string BuildCurrentLocationForSpeech() +{ + // Quest Level Name + if (setlevel) { + const char *const questLevelName = QuestLevelNames[setlvlnum]; + if (questLevelName == nullptr || questLevelName[0] == '\0') + return std::string { _("Set level") }; + + return fmt::format("{:s}: {:s}", _("Set level"), _(questLevelName)); + } + + // Dungeon Name + constexpr std::array DungeonStrs = { + N_("Town"), + N_("Cathedral"), + N_("Catacombs"), + N_("Caves"), + N_("Hell"), + N_("Nest"), + N_("Crypt"), + }; + std::string dungeonStr; + if (leveltype >= DTYPE_TOWN && leveltype <= DTYPE_LAST) { + dungeonStr = _(DungeonStrs[static_cast(leveltype)]); + } else { + dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); + } + + if (leveltype == DTYPE_TOWN || currlevel <= 0) + return dungeonStr; + + // Dungeon Level + int level = currlevel; + if (leveltype == DTYPE_CATACOMBS) + level -= 4; + else if (leveltype == DTYPE_CAVES) + level -= 8; + else if (leveltype == DTYPE_HELL) + level -= 12; + else if (leveltype == DTYPE_NEST) + level -= 16; + else if (leveltype == DTYPE_CRYPT) + level -= 20; + + if (level <= 0) + return dungeonStr; + + return fmt::format(fmt::runtime(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}")), dungeonStr, level); +} + +void SpeakCurrentLocationKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + + SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true); +} + +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(); + if (invflag) + FocusOnInventory(); +} + +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 quest log + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } + } + CloseCharPanel(); + CloseGoldWithdraw(); + CloseStash(); +} + +void SpeakSelectedSpeedbookSpell() +{ + for (const auto &spellListItem : GetSpellListItems()) { + if (spellListItem.isSelected) { + SpeakText(pgettext("spell", GetSpellData(spellListItem.id).sNameText), /*force=*/true); + return; + } + } + SpeakText(_("No spell selected."), /*force=*/true); +} + +void DisplaySpellsKeyPressed() +{ + if (IsPlayerInStore()) + return; + CloseCharPanel(); + QuestLogIsOpen = false; + CloseInventory(); + SpellbookFlag = false; + if (!SpellSelectFlag) { + DoSpeedBook(); + SpeakSelectedSpeedbookSpell(); + } else { + SpellSelectFlag = false; + } + LastPlayerAction = PlayerActionType::None; +} + +void SpellBookKeyPressed() +{ + if (IsPlayerInStore()) + return; + SpellbookFlag = !SpellbookFlag; + if (SpellbookFlag && MyPlayer != nullptr) { + const Player &player = *MyPlayer; + if (IsValidSpell(player._pRSpell)) { + SpeakText(pgettext("spell", GetSpellData(player._pRSpell).sNameText), /*force=*/true); + } else { + SpeakText(_("No spell selected."), /*force=*/true); + } + } + if (!IsLeftPanelOpen() && CanPanelsCoverView()) { + if (!SpellbookFlag) { // We closed the spellbook + if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition + Displacement { 160, 0 }); + } + } else if (!invflag) { // We opened the spellbook + if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { + SetCursorPos(MousePosition - Displacement { 160, 0 }); + } + } + } + CloseInventory(); +} + +void CycleSpellHotkeys(bool next) +{ + if (MyPlayer == nullptr) + return; + 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.empty()) + 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() +{ + if (MyPlayer == nullptr) + return true; + return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; +} + +bool IsGameRunning() +{ + return PauseMode != 2; +} + +bool CanPlayerTakeAction() +{ + return !IsPlayerDead() && IsGameRunning(); +} + +bool CanAutomapBeToggledOff() +{ + return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !CharFlag + && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag + && !ChatLogFlag && !HelpFlag; +} + +} // namespace devilution diff --git a/Source/controls/accessibility_keys.hpp b/Source/controls/accessibility_keys.hpp new file mode 100644 index 000000000..694a438db --- /dev/null +++ b/Source/controls/accessibility_keys.hpp @@ -0,0 +1,30 @@ +/** + * @file controls/accessibility_keys.hpp + * + * UI accessibility key handlers and action-guard helpers. + */ +#pragma once + +#include + +namespace devilution { + +void SpeakPlayerHealthPercentageKeyPressed(); +void SpeakExperienceToNextLevelKeyPressed(); +std::string BuildCurrentLocationForSpeech(); +void SpeakCurrentLocationKeyPressed(); +void InventoryKeyPressed(); +void CharacterSheetKeyPressed(); +void PartyPanelSideToggleKeyPressed(); +void QuestLogKeyPressed(); +void SpeakSelectedSpeedbookSpell(); +void DisplaySpellsKeyPressed(); +void SpellBookKeyPressed(); +void CycleSpellHotkeys(bool next); + +bool IsPlayerDead(); +bool IsGameRunning(); +bool CanPlayerTakeAction(); +bool CanAutomapBeToggledOff(); + +} // namespace devilution diff --git a/Source/controls/town_npc_nav.cpp b/Source/controls/town_npc_nav.cpp new file mode 100644 index 000000000..a66d1eb97 --- /dev/null +++ b/Source/controls/town_npc_nav.cpp @@ -0,0 +1,348 @@ +/** + * @file controls/town_npc_nav.cpp + * + * Town NPC navigation for accessibility. + */ +#include "controls/town_npc_nav.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "controls/accessibility_keys.hpp" +#include "controls/plrctrls.h" +#include "diablo.h" +#include "engine/path.h" +#include "help.h" +#include "levels/gendung.h" +#include "levels/tile_properties.hpp" +#include "multi.h" +#include "options.h" +#include "player.h" +#include "qol/chatlog.h" +#include "stores.h" +#include "towners.h" +#include "utils/language.h" +#include "utils/screen_reader.hpp" +#include "utils/str_cat.hpp" +#include "utils/walk_path_speech.hpp" + +namespace devilution { + +namespace { + +std::vector TownNpcOrder; +int SelectedTownNpc = -1; +int AutoWalkTownNpcTarget = -1; + +void ResetTownNpcSelection() +{ + TownNpcOrder.clear(); + SelectedTownNpc = -1; +} + +void RefreshTownNpcOrder(bool selectFirst = false) +{ + TownNpcOrder.clear(); + if (leveltype != DTYPE_TOWN) + return; + if (MyPlayer == nullptr) { + SelectedTownNpc = -1; + return; + } + + const Point playerPosition = MyPlayer->position.future; + + for (size_t i = 0; i < GetNumTowners(); ++i) { + const Towner &towner = Towners[i]; + if (!IsTownerPresent(towner._ttype)) + continue; + if (towner._ttype == TOWN_COW) + continue; + TownNpcOrder.push_back(static_cast(i)); + } + + if (TownNpcOrder.empty()) { + SelectedTownNpc = -1; + return; + } + + std::sort(TownNpcOrder.begin(), TownNpcOrder.end(), [&playerPosition](int a, int b) { + const Towner &townerA = Towners[a]; + const Towner &townerB = Towners[b]; + const int distanceA = playerPosition.WalkingDistance(townerA.position); + const int distanceB = playerPosition.WalkingDistance(townerB.position); + if (distanceA != distanceB) + return distanceA < distanceB; + return townerA.name < townerB.name; + }); + + if (selectFirst) { + SelectedTownNpc = TownNpcOrder.front(); + return; + } + + const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); + if (it == TownNpcOrder.end()) + SelectedTownNpc = TownNpcOrder.front(); +} + +void EnsureTownNpcOrder() +{ + if (leveltype != DTYPE_TOWN) { + ResetTownNpcSelection(); + return; + } + if (TownNpcOrder.empty()) { + RefreshTownNpcOrder(true); + return; + } + if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { + RefreshTownNpcOrder(true); + return; + } + const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); + if (it == TownNpcOrder.end()) + SelectedTownNpc = TownNpcOrder.front(); +} + +void SelectTownNpcRelative(int delta) +{ + if (!IsTownNpcActionAllowed()) + return; + + EnsureTownNpcOrder(); + if (TownNpcOrder.empty()) { + SpeakText(_("No town NPCs found."), true); + return; + } + + auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); + int currentIndex = (it != TownNpcOrder.end()) ? static_cast(it - TownNpcOrder.begin()) : 0; + + const int size = static_cast(TownNpcOrder.size()); + int newIndex = (currentIndex + delta) % size; + if (newIndex < 0) + newIndex += size; + SelectedTownNpc = TownNpcOrder[static_cast(newIndex)]; + SpeakSelectedTownNpc(); +} + +} // namespace + +bool IsTownNpcActionAllowed() +{ + return CanPlayerTakeAction() + && leveltype == DTYPE_TOWN + && !IsPlayerInStore() + && !ChatLogFlag + && !HelpFlag; +} + +void SpeakSelectedTownNpc() +{ + EnsureTownNpcOrder(); + + if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { + SpeakText(_("No NPC selected."), true); + return; + } + if (MyPlayer == nullptr) { + SpeakText(_("No NPC selected."), true); + return; + } + + const Towner &towner = Towners[SelectedTownNpc]; + const Point playerPosition = MyPlayer->position.future; + const int distance = playerPosition.WalkingDistance(towner.position); + + std::string msg; + StrAppend(msg, towner.name); + StrAppend(msg, "\n", _("Distance: "), distance); + StrAppend(msg, "\n", _("Position: "), towner.position.x, ", ", towner.position.y); + SpeakText(msg, true); +} + +void SelectNextTownNpcKeyPressed() +{ + SelectTownNpcRelative(+1); +} + +void SelectPreviousTownNpcKeyPressed() +{ + SelectTownNpcRelative(-1); +} + +void GoToSelectedTownNpcKeyPressed() +{ + if (!IsTownNpcActionAllowed()) + return; + + EnsureTownNpcOrder(); + if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { + SpeakText(_("No NPC selected."), true); + return; + } + + const Towner &towner = Towners[SelectedTownNpc]; + + std::string msg; + StrAppend(msg, _("Going to: "), towner.name); + SpeakText(msg, true); + + AutoWalkTownNpcTarget = SelectedTownNpc; + UpdateAutoWalkTownNpc(); +} + +void UpdateAutoWalkTownNpc() +{ + if (AutoWalkTownNpcTarget < 0) + return; + if (leveltype != DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag) { + AutoWalkTownNpcTarget = -1; + return; + } + if (!CanPlayerTakeAction()) + return; + if (MyPlayer == nullptr) { + AutoWalkTownNpcTarget = -1; + return; + } + + if (MyPlayer->_pmode != PM_STAND) + return; + if (MyPlayer->walkpath[0] != WALK_NONE) + return; + if (MyPlayer->destAction != ACTION_NONE) + return; + + if (AutoWalkTownNpcTarget >= static_cast(GetNumTowners())) { + AutoWalkTownNpcTarget = -1; + SpeakText(_("No NPC selected."), true); + return; + } + + const Towner &towner = Towners[AutoWalkTownNpcTarget]; + if (!IsTownerPresent(towner._ttype) || towner._ttype == TOWN_COW) { + AutoWalkTownNpcTarget = -1; + SpeakText(_("No NPC selected."), true); + return; + } + + Player &myPlayer = *MyPlayer; + const Point playerPosition = myPlayer.position.future; + if (playerPosition.WalkingDistance(towner.position) < 2) { + const int townerIdx = AutoWalkTownNpcTarget; + AutoWalkTownNpcTarget = -1; + NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); + return; + } + + constexpr size_t MaxAutoWalkPathLength = 512; + std::array path; + path.fill(WALK_NONE); + + const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, towner.position, path.data(), path.size()); + if (steps == 0) { + AutoWalkTownNpcTarget = -1; + std::string error; + StrAppend(error, _("Can't find a path to: "), towner.name); + SpeakText(error, true); + return; + } + + // FindPath returns 0 when it fails to find a usable path. + // The player walkpath buffer is MaxPathLengthPlayer, so keep segments strictly shorter. + if (steps < static_cast(MaxPathLengthPlayer)) { + const int townerIdx = AutoWalkTownNpcTarget; + AutoWalkTownNpcTarget = -1; + NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); + return; + } + + const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); + const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); +} + +void ResetAutoWalkTownNpc() +{ + AutoWalkTownNpcTarget = -1; +} + +void ListTownNpcsKeyPressed() +{ + if (leveltype != DTYPE_TOWN) { + ResetTownNpcSelection(); + SpeakText(_("Not in town."), true); + return; + } + if (IsPlayerInStore()) + return; + + std::vector townNpcs; + std::vector cows; + + townNpcs.reserve(Towners.size()); + cows.reserve(Towners.size()); + if (MyPlayer == nullptr) { + SpeakText(_("No town NPCs found."), true); + return; + } + + const Point playerPosition = MyPlayer->position.future; + + for (const Towner &towner : Towners) { + if (!IsTownerPresent(towner._ttype)) + continue; + + if (towner._ttype == TOWN_COW) { + cows.push_back(&towner); + continue; + } + + townNpcs.push_back(&towner); + } + + if (townNpcs.empty() && cows.empty()) { + ResetTownNpcSelection(); + SpeakText(_("No town NPCs found."), true); + return; + } + + std::sort(townNpcs.begin(), townNpcs.end(), [&playerPosition](const Towner *a, const Towner *b) { + const int distanceA = playerPosition.WalkingDistance(a->position); + const int distanceB = playerPosition.WalkingDistance(b->position); + if (distanceA != distanceB) + return distanceA < distanceB; + return a->name < b->name; + }); + + std::string output; + StrAppend(output, _("Town NPCs:")); + for (size_t i = 0; i < townNpcs.size(); ++i) { + StrAppend(output, "\n", i + 1, ". ", townNpcs[i]->name); + } + if (!cows.empty()) { + StrAppend(output, "\n", _("Cows: "), static_cast(cows.size())); + } + + RefreshTownNpcOrder(true); + if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast(GetNumTowners())) { + const Towner &towner = Towners[SelectedTownNpc]; + StrAppend(output, "\n", _("Selected: "), towner.name); + StrAppend(output, "\n", _("PageUp/PageDown: select. Home: go. End: repeat.")); + } + const std::string_view exitKey = GetOptions().Keymapper.KeyNameForAction("SpeakNearestExit"); + if (!exitKey.empty()) { + StrAppend(output, "\n", fmt::format(fmt::runtime(_("Cathedral entrance: press {:s}.")), exitKey)); + } + + SpeakText(output, true); +} + +} // namespace devilution diff --git a/Source/controls/town_npc_nav.hpp b/Source/controls/town_npc_nav.hpp new file mode 100644 index 000000000..c282af802 --- /dev/null +++ b/Source/controls/town_npc_nav.hpp @@ -0,0 +1,19 @@ +/** + * @file controls/town_npc_nav.hpp + * + * Town NPC navigation for accessibility. + */ +#pragma once + +namespace devilution { + +void SelectNextTownNpcKeyPressed(); +void SelectPreviousTownNpcKeyPressed(); +void GoToSelectedTownNpcKeyPressed(); +void SpeakSelectedTownNpc(); +void ListTownNpcsKeyPressed(); +void UpdateAutoWalkTownNpc(); +bool IsTownNpcActionAllowed(); +void ResetAutoWalkTownNpc(); + +} // namespace devilution diff --git a/Source/controls/tracker.cpp b/Source/controls/tracker.cpp new file mode 100644 index 000000000..3d0ff5c94 --- /dev/null +++ b/Source/controls/tracker.cpp @@ -0,0 +1,2859 @@ +/** + * @file controls/tracker.cpp + * + * Tracker system for accessibility: target cycling, pathfinding, and auto-walk. + */ +#include "controls/tracker.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef USE_SDL3 +#include +#else +#include +#endif + +#include "appfat.h" +#include "automap.h" +#include "controls/accessibility_keys.hpp" +#include "controls/plrctrls.h" +#include "diablo.h" +#include "engine/path.h" +#include "gamemenu.h" +#include "help.h" +#include "items.h" +#include "levels/gendung.h" +#include "levels/setmaps.h" +#include "levels/tile_properties.hpp" +#include "levels/trigs.h" +#include "missiles.h" +#include "monster.h" +#include "multi.h" +#include "objects.h" +#include "player.h" +#include "portal.h" +#include "qol/chatlog.h" +#include "quests.h" +#include "stores.h" +#include "towners.h" +#include "utils/accessibility_announcements.hpp" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/navigation_speech.hpp" +#include "utils/screen_reader.hpp" +#include "utils/sdl_compat.h" +#include "utils/str_cat.hpp" +#include "utils/string_or_view.hpp" +#include "utils/walk_path_speech.hpp" + +namespace devilution { + +namespace { + +TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items; + +TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items; ///< Category of the active auto-walk target. +int AutoWalkTrackerTargetId = -1; ///< ID of the target being auto-walked to, or -1 if inactive. + +/// Maximum Chebyshev distance (in tiles) at which the player is considered +/// close enough to interact with a tracker target. +constexpr int TrackerInteractDistanceTiles = 1; +constexpr int TrackerCycleDistanceTiles = 12; + +int LockedTrackerItemId = -1; +int LockedTrackerChestId = -1; +int LockedTrackerDoorId = -1; +int LockedTrackerShrineId = -1; +int LockedTrackerObjectId = -1; +int LockedTrackerBreakableId = -1; +int LockedTrackerMonsterId = -1; +int LockedTrackerDeadBodyId = -1; +int LockedTrackerNpcId = -1; +int LockedTrackerPlayerId = -1; +int LockedTrackerDungeonEntranceId = -1; +int LockedTrackerStairsId = -1; +int LockedTrackerQuestLocationId = -1; +int LockedTrackerPortalId = -1; + +struct TrackerLevelKey { + dungeon_type levelType; + int currLevel; + bool isSetLevel; + int setLevelNum; + + friend bool operator==(const TrackerLevelKey &lhs, const TrackerLevelKey &rhs) + { + return lhs.levelType == rhs.levelType && lhs.currLevel == rhs.currLevel + && lhs.isSetLevel == rhs.isSetLevel && lhs.setLevelNum == rhs.setLevelNum; + } + friend bool operator!=(const TrackerLevelKey &lhs, const TrackerLevelKey &rhs) + { + return !(lhs == rhs); + } +}; + +std::optional LockedTrackerLevelKey; + +void ClearTrackerLocks() +{ + LockedTrackerItemId = -1; + LockedTrackerChestId = -1; + LockedTrackerDoorId = -1; + LockedTrackerShrineId = -1; + LockedTrackerObjectId = -1; + LockedTrackerBreakableId = -1; + LockedTrackerMonsterId = -1; + LockedTrackerDeadBodyId = -1; + LockedTrackerNpcId = -1; + LockedTrackerPlayerId = -1; + LockedTrackerDungeonEntranceId = -1; + LockedTrackerStairsId = -1; + LockedTrackerQuestLocationId = -1; + LockedTrackerPortalId = -1; +} + +void EnsureTrackerLocksMatchCurrentLevel() +{ + const TrackerLevelKey current { + .levelType = leveltype, + .currLevel = currlevel, + .isSetLevel = setlevel, + .setLevelNum = setlvlnum, + }; + + if (!LockedTrackerLevelKey || *LockedTrackerLevelKey != current) { + ClearTrackerLocks(); + LockedTrackerLevelKey = current; + } +} + +int &LockedTrackerTargetId(TrackerTargetCategory category) +{ + switch (category) { + case TrackerTargetCategory::Items: + return LockedTrackerItemId; + case TrackerTargetCategory::Chests: + return LockedTrackerChestId; + case TrackerTargetCategory::Doors: + return LockedTrackerDoorId; + case TrackerTargetCategory::Shrines: + return LockedTrackerShrineId; + case TrackerTargetCategory::Objects: + return LockedTrackerObjectId; + case TrackerTargetCategory::Breakables: + return LockedTrackerBreakableId; + case TrackerTargetCategory::Monsters: + return LockedTrackerMonsterId; + case TrackerTargetCategory::DeadBodies: + return LockedTrackerDeadBodyId; + case TrackerTargetCategory::Npcs: + return LockedTrackerNpcId; + case TrackerTargetCategory::Players: + return LockedTrackerPlayerId; + case TrackerTargetCategory::DungeonEntrances: + return LockedTrackerDungeonEntranceId; + case TrackerTargetCategory::Stairs: + return LockedTrackerStairsId; + case TrackerTargetCategory::QuestLocations: + return LockedTrackerQuestLocationId; + case TrackerTargetCategory::Portals: + return LockedTrackerPortalId; + } + app_fatal("Invalid TrackerTargetCategory"); +} + +std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category) +{ + switch (category) { + case TrackerTargetCategory::Items: + return _("items"); + case TrackerTargetCategory::Chests: + return _("chests"); + case TrackerTargetCategory::Doors: + return _("doors"); + case TrackerTargetCategory::Shrines: + return _("shrines"); + case TrackerTargetCategory::Objects: + return _("objects"); + case TrackerTargetCategory::Breakables: + return _("breakables"); + case TrackerTargetCategory::Monsters: + return _("monsters"); + case TrackerTargetCategory::DeadBodies: + return _("dead bodies"); + case TrackerTargetCategory::Npcs: + return _("NPCs"); + case TrackerTargetCategory::Players: + return _("players"); + case TrackerTargetCategory::DungeonEntrances: + if (leveltype != DTYPE_TOWN) + return _("exits"); + return _("dungeon entrances"); + case TrackerTargetCategory::Stairs: + return _("stairs"); + case TrackerTargetCategory::QuestLocations: + return _("quest locations"); + case TrackerTargetCategory::Portals: + return _("portals"); + } + app_fatal("Invalid TrackerTargetCategory"); +} + +void SpeakTrackerTargetCategory() +{ + SpeakText(TrackerTargetCategoryLabel(SelectedTrackerTargetCategory), true); +} + +std::optional FindNearestGroundItemId(Point playerPosition) +{ + std::optional bestId; + int bestDistance = 0; + + for (int y = 0; y < MAXDUNY; ++y) { + for (int x = 0; x < MAXDUNX; ++x) { + const int itemId = std::abs(dItem[x][y]) - 1; + if (itemId < 0 || itemId > MAXITEMS) + continue; + + const Item &item = Items[itemId]; + if (item.isEmpty() || item._iClass == ICLASS_NONE) + continue; + + const int distance = playerPosition.WalkingDistance(Point { x, y }); + if (!bestId || distance < bestDistance) { + bestId = itemId; + bestDistance = distance; + } + } + } + + return bestId; +} + +[[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position) +{ + return position.x + position.y * MAXDUNX; +} + +[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId) +{ + return { corpseId % MAXDUNX, corpseId / MAXDUNX }; +} + +std::optional FindNearestCorpseId(Point playerPosition) +{ + std::optional bestId; + int bestDistance = 0; + + for (int y = 0; y < MAXDUNY; ++y) { + for (int x = 0; x < MAXDUNX; ++x) { + if (dCorpse[x][y] == 0) + continue; + + const Point position { x, y }; + const int distance = playerPosition.WalkingDistance(position); + if (!bestId || distance < bestDistance) { + bestId = CorpseTrackerIdForPosition(position); + bestDistance = distance; + } + } + } + + return bestId; +} + +struct TrackerCandidate { + int id; + int distance; + StringOrView name; +}; + +[[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b) +{ + if (a.distance != b.distance) + return a.distance < b.distance; + return a.id < b.id; +} + +[[nodiscard]] std::vector CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance) +{ + std::vector result; + result.reserve(ActiveItemCount); + + const int minX = std::max(0, playerPosition.x - maxDistance); + const int minY = std::max(0, playerPosition.y - maxDistance); + const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); + const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); + + std::array seen {}; + + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + const int itemId = std::abs(dItem[x][y]) - 1; + if (itemId < 0 || itemId > MAXITEMS) + continue; + if (seen[itemId]) + continue; + seen[itemId] = true; + + const Item &item = Items[itemId]; + if (item.isEmpty() || item._iClass == ICLASS_NONE) + continue; + + const int distance = playerPosition.WalkingDistance(Point { x, y }); + if (distance > maxDistance) + continue; + + result.push_back(TrackerCandidate { + .id = itemId, + .distance = distance, + .name = item.getName(), + }); + } + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::vector CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance) +{ + std::vector result; + + const int minX = std::max(0, playerPosition.x - maxDistance); + const int minY = std::max(0, playerPosition.y - maxDistance); + const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); + const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); + + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + if (dCorpse[x][y] == 0) + continue; + + const Point position { x, y }; + const int distance = playerPosition.WalkingDistance(position); + if (distance > maxDistance) + continue; + + result.push_back(TrackerCandidate { + .id = CorpseTrackerIdForPosition(position), + .distance = distance, + .name = _("Dead body"), + }); + } + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object) +{ + return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST); +} + +[[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object) +{ + return object.isDoor() && object.canInteractWith(); +} + +[[nodiscard]] constexpr bool IsShrineLikeObject(const Object &object) +{ + return object.canInteractWith() + && (object.IsShrine() + || IsAnyOf(object._otype, _object_id::OBJ_BLOODFTN, _object_id::OBJ_PURIFYINGFTN, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON, + _object_id::OBJ_MURKYFTN, _object_id::OBJ_TEARFTN)); +} + +[[nodiscard]] constexpr bool IsTrackedBreakableObject(const Object &object) +{ + return object.IsBreakable(); +} + +[[nodiscard]] constexpr bool IsTrackedMiscInteractableObject(const Object &object) +{ + if (!object.canInteractWith()) + return false; + if (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST) + return false; + if (object.isDoor()) + return false; + if (IsShrineLikeObject(object)) + return false; + if (object.IsBreakable()) + return false; + return true; +} + +[[nodiscard]] bool IsTrackedMonster(const Monster &monster) +{ + return !monster.isInvalid + && (monster.flags & MFLAG_HIDDEN) == 0 + && monster.hitPoints > 0; +} + +template +[[nodiscard]] std::vector CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate) +{ + std::vector result; + result.reserve(ActiveObjectCount); + + const int minX = std::max(0, playerPosition.x - maxDistance); + const int minY = std::max(0, playerPosition.y - maxDistance); + const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); + const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); + + std::array bestDistanceById {}; + bestDistanceById.fill(std::numeric_limits::max()); + + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + const int objectId = std::abs(dObject[x][y]) - 1; + if (objectId < 0 || objectId >= MAXOBJECTS) + continue; + + const Object &object = Objects[objectId]; + if (object._otype == OBJ_NULL) + continue; + if (!predicate(object)) + continue; + + const int distance = playerPosition.WalkingDistance(Point { x, y }); + if (distance > maxDistance) + continue; + + int &bestDistance = bestDistanceById[objectId]; + if (distance < bestDistance) + bestDistance = distance; + } + } + + for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { + const int distance = bestDistanceById[objectId]; + if (distance == std::numeric_limits::max()) + continue; + + const Object &object = Objects[objectId]; + result.push_back(TrackerCandidate { + .id = objectId, + .distance = distance, + .name = object.name(), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +template +[[nodiscard]] std::optional FindNearestObjectId(Point playerPosition, Predicate predicate) +{ + std::array bestDistanceById {}; + bestDistanceById.fill(std::numeric_limits::max()); + + for (int y = 0; y < MAXDUNY; ++y) { + for (int x = 0; x < MAXDUNX; ++x) { + const int objectId = std::abs(dObject[x][y]) - 1; + if (objectId < 0 || objectId >= MAXOBJECTS) + continue; + + const Object &object = Objects[objectId]; + if (object._otype == OBJ_NULL) + continue; + if (!predicate(object)) + continue; + + const int distance = playerPosition.WalkingDistance(Point { x, y }); + int &bestDistance = bestDistanceById[objectId]; + if (distance < bestDistance) + bestDistance = distance; + } + } + + std::optional bestId; + int bestDistance = 0; + for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { + const int distance = bestDistanceById[objectId]; + if (distance == std::numeric_limits::max()) + continue; + + if (!bestId || distance < bestDistance) { + bestId = objectId; + bestDistance = distance; + } + } + + return bestId; +} + +[[nodiscard]] std::vector CollectNearbyChestTrackerCandidates(Point playerPosition, int maxDistance) +{ + return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedChestObject); +} + +[[nodiscard]] std::vector CollectNearbyDoorTrackerCandidates(Point playerPosition, int maxDistance) +{ + return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedDoorObject); +} + +[[nodiscard]] std::vector CollectNearbyShrineTrackerCandidates(Point playerPosition, int maxDistance) +{ + return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsShrineLikeObject); +} + +[[nodiscard]] std::vector CollectNearbyBreakableTrackerCandidates(Point playerPosition, int maxDistance) +{ + return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedBreakableObject); +} + +[[nodiscard]] std::vector CollectNearbyObjectInteractableTrackerCandidates(Point playerPosition, int maxDistance) +{ + return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedMiscInteractableObject); +} + +[[nodiscard]] std::vector CollectNearbyMonsterTrackerCandidates(Point playerPosition, int maxDistance) +{ + std::vector result; + result.reserve(ActiveMonsterCount); + + for (size_t i = 0; i < ActiveMonsterCount; ++i) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; + if (!IsTrackedMonster(monster)) + continue; + + const int distance = playerPosition.ApproxDistance(monster.position.future); + if (distance > maxDistance) + continue; + + result.push_back(TrackerCandidate { + .id = monsterId, + .distance = distance, + .name = monster.name(), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::vector CollectNpcTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (leveltype != DTYPE_TOWN) + return result; + + result.reserve(GetNumTowners()); + for (size_t i = 0; i < GetNumTowners(); ++i) { + const Towner &towner = Towners[i]; + if (!IsTownerPresent(towner._ttype)) + continue; + + const int distance = playerPosition.WalkingDistance(towner.position); + result.push_back(TrackerCandidate { + .id = static_cast(i), + .distance = distance, + .name = towner.name, + }); + } + + std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { + if (a.distance != b.distance) + return a.distance < b.distance; + return a.name.str() < b.name.str(); + }); + return result; +} + +[[nodiscard]] std::vector CollectPlayerTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (!gbIsMultiplayer || MyPlayer == nullptr) + return result; + + result.reserve(MAX_PLRS); + + const uint8_t currentLevel = MyPlayer->plrlevel; + const bool currentIsSetLevel = setlevel; + + for (int i = 0; i < MAX_PLRS; ++i) { + if (i == MyPlayerId) + continue; + const Player &player = Players[i]; + if (!player.plractive) + continue; + if (player._pLvlChanging) + continue; + if (player.plrlevel != currentLevel) + continue; + if (player.plrIsOnSetLevel != currentIsSetLevel) + continue; + + const Point otherPosition = player.position.future; + if (!InDungeonBounds(otherPosition)) + continue; + + const int distance = playerPosition.WalkingDistance(otherPosition); + result.push_back(TrackerCandidate { + .id = i, + .distance = distance, + .name = player.name(), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::vector CollectDungeonEntranceTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (MyPlayer == nullptr) + return result; + + if (leveltype == DTYPE_TOWN) { + const std::vector candidates = CollectTownDungeonTriggerIndices(); + result.reserve(candidates.size()); + + for (const int triggerIndex : candidates) { + if (triggerIndex < 0 || triggerIndex >= numtrigs) + continue; + const TriggerStruct &trigger = trigs[triggerIndex]; + const Point triggerPosition { trigger.position.x, trigger.position.y }; + const int distance = playerPosition.WalkingDistance(triggerPosition); + result.push_back(TrackerCandidate { + .id = triggerIndex, + .distance = distance, + .name = TriggerLabelForSpeech(trigger), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; + } + + for (int i = 0; i < numtrigs; ++i) { + const TriggerStruct &trigger = trigs[i]; + if (setlevel) { + if (trigger._tmsg != WM_DIABRTNLVL) + continue; + } else { + if (trigger._tmsg != WM_DIABPREVLVL) + continue; + } + + const Point triggerPosition { trigger.position.x, trigger.position.y }; + const int distance = playerPosition.WalkingDistance(triggerPosition); + result.push_back(TrackerCandidate { + .id = i, + .distance = distance, + .name = TriggerLabelForSpeech(trigger), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::optional FindTownPortalPositionInTownByPortalIndex(int portalIndex) +{ + if (portalIndex < 0 || portalIndex >= MAXPORTAL) + return std::nullopt; + + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::TownPortal) + continue; + if (missile._misource != portalIndex) + continue; + return missile.position.tile; + } + + return std::nullopt; +} + +[[nodiscard]] bool IsTownPortalOpenOnCurrentLevel(int portalIndex) +{ + if (portalIndex < 0 || portalIndex >= MAXPORTAL) + return false; + const Portal &portal = Portals[portalIndex]; + if (!portal.open) + return false; + if (portal.setlvl != setlevel) + return false; + if (portal.level != currlevel) + return false; + if (portal.ltype != leveltype) + return false; + return InDungeonBounds(portal.position); +} + +[[nodiscard]] std::vector CollectPortalTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (MyPlayer == nullptr) + return result; + + if (leveltype == DTYPE_TOWN) { + std::array seen {}; + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::TownPortal) + continue; + const int portalIndex = missile._misource; + if (portalIndex < 0 || portalIndex >= MAXPORTAL) + continue; + if (seen[portalIndex]) + continue; + seen[portalIndex] = true; + + const Point portalPosition = missile.position.tile; + const int distance = playerPosition.WalkingDistance(portalPosition); + result.push_back(TrackerCandidate { + .id = portalIndex, + .distance = distance, + .name = TownPortalLabelForSpeech(Portals[portalIndex]), + }); + } + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; + } + + for (int i = 0; i < MAXPORTAL; ++i) { + if (!IsTownPortalOpenOnCurrentLevel(i)) + continue; + const Portal &portal = Portals[i]; + const int distance = playerPosition.WalkingDistance(portal.position); + result.push_back(TrackerCandidate { + .id = i, + .distance = distance, + .name = TownPortalLabelForSpeech(portal), + }); + } + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::vector CollectStairsTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) + return result; + + for (int i = 0; i < numtrigs; ++i) { + const TriggerStruct &trigger = trigs[i]; + if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL)) + continue; + + const Point triggerPosition { trigger.position.x, trigger.position.y }; + const int distance = playerPosition.WalkingDistance(triggerPosition); + result.push_back(TrackerCandidate { + .id = i, + .distance = distance, + .name = TriggerLabelForSpeech(trigger), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::vector CollectQuestLocationTrackerCandidates(Point playerPosition) +{ + std::vector result; + if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) + return result; + + if (setlevel) { + for (int i = 0; i < numtrigs; ++i) { + const TriggerStruct &trigger = trigs[i]; + if (trigger._tmsg != WM_DIABRTNLVL) + continue; + + const Point triggerPosition { trigger.position.x, trigger.position.y }; + const int distance = playerPosition.WalkingDistance(triggerPosition); + result.push_back(TrackerCandidate { + .id = i, + .distance = distance, + .name = TriggerLabelForSpeech(trigger), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; + } + + constexpr size_t NumQuests = sizeof(Quests) / sizeof(Quests[0]); + result.reserve(NumQuests); + for (size_t questIndex = 0; questIndex < NumQuests; ++questIndex) { + const Quest &quest = Quests[questIndex]; + if (quest._qslvl == SL_NONE) + continue; + if (quest._qactive == QUEST_NOTAVAIL) + continue; + if (quest._qlevel != currlevel) + continue; + if (!InDungeonBounds(quest.position)) + continue; + + const char *questLevelName = QuestLevelNames[quest._qslvl]; + if (questLevelName == nullptr || questLevelName[0] == '\0') + questLevelName = N_("Set level"); + + const int distance = playerPosition.WalkingDistance(quest.position); + result.push_back(TrackerCandidate { + .id = static_cast(questIndex), + .distance = distance, + .name = _(questLevelName), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} + +[[nodiscard]] std::optional FindNextTrackerCandidateId(const std::vector &candidates, int currentId) +{ + if (candidates.empty()) + return std::nullopt; + if (currentId < 0) + return candidates.front().id; + + const auto it = std::find_if(candidates.begin(), candidates.end(), [currentId](const TrackerCandidate &c) { return c.id == currentId; }); + if (it == candidates.end()) + return candidates.front().id; + + if (candidates.size() <= 1) + return std::nullopt; + + const size_t idx = static_cast(it - candidates.begin()); + const size_t nextIdx = (idx + 1) % candidates.size(); + return candidates[nextIdx].id; +} + +[[nodiscard]] std::optional FindPreviousTrackerCandidateId(const std::vector &candidates, int currentId) +{ + if (candidates.empty()) + return std::nullopt; + if (currentId < 0) + return candidates.back().id; + + const auto it = std::find_if(candidates.begin(), candidates.end(), [currentId](const TrackerCandidate &c) { return c.id == currentId; }); + if (it == candidates.end()) + return candidates.back().id; + + if (candidates.size() <= 1) + return std::nullopt; + + const size_t idx = static_cast(it - candidates.begin()); + const size_t prevIdx = (idx + candidates.size() - 1) % candidates.size(); + return candidates[prevIdx].id; +} + +void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &targetName, const std::vector &candidates) +{ + if (targetName.empty()) + return; + + const std::string_view baseName = targetName.str(); + int total = 0; + for (const TrackerCandidate &c : candidates) { + if (c.name.str() == baseName) + ++total; + } + if (total <= 1) + return; + + int ordinal = 0; + int seen = 0; + for (const TrackerCandidate &c : candidates) { + if (c.name.str() != baseName) + continue; + ++seen; + if (c.id == targetId) { + ordinal = seen; + break; + } + } + if (ordinal <= 0) + return; + + std::string decorated; + StrAppend(decorated, baseName, " ", ordinal); + targetName = std::move(decorated); +} + +[[nodiscard]] bool IsGroundItemPresent(int itemId) +{ + if (itemId < 0 || itemId > MAXITEMS) + return false; + + for (uint8_t i = 0; i < ActiveItemCount; ++i) { + if (ActiveItems[i] == itemId) + return true; + } + + return false; +} + +[[nodiscard]] bool IsCorpsePresent(int corpseId) +{ + if (corpseId < 0 || corpseId >= MAXDUNX * MAXDUNY) + return false; + + const Point position = CorpsePositionForTrackerId(corpseId); + return InDungeonBounds(position) && dCorpse[position.x][position.y] != 0; +} + +std::optional FindNearestUnopenedChestObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedChestObject); +} + +std::optional FindNearestDoorObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedDoorObject); +} + +std::optional FindNearestShrineObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsShrineLikeObject); +} + +std::optional FindNearestBreakableObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedBreakableObject); +} + +std::optional FindNearestMiscInteractableObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject); +} + +std::optional FindNearestMonsterId(Point playerPosition) +{ + std::optional bestId; + int bestDistance = 0; + + for (size_t i = 0; i < ActiveMonsterCount; ++i) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; + if (!IsTrackedMonster(monster)) + continue; + + const int distance = playerPosition.ApproxDistance(monster.position.future); + if (!bestId || distance < bestDistance) { + bestId = monsterId; + bestDistance = distance; + } + } + + return bestId; +} + +std::optional FindBestAdjacentApproachTile(const Player &player, Point playerPosition, Point targetPosition) +{ + std::optional best; + size_t bestPathLength = 0; + int bestDistance = 0; + + std::optional bestFallback; + int bestFallbackDistance = 0; + + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + + const Point tile { targetPosition.x + dx, targetPosition.y + dy }; + if (!PosOkPlayer(player, tile)) + continue; + + const int distance = playerPosition.WalkingDistance(tile); + + if (!bestFallback || distance < bestFallbackDistance) { + bestFallback = tile; + bestFallbackDistance = distance; + } + + const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); + if (!path) + continue; + + const size_t pathLength = path->size(); + if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { + best = tile; + bestPathLength = pathLength; + bestDistance = distance; + } + } + } + + if (best) + return best; + + return bestFallback; +} + +std::optional FindBestApproachTileForObject(const Player &player, Point playerPosition, const Object &object) +{ + if (!object._oSolidFlag && PosOkPlayer(player, object.position)) + return object.position; + + std::optional best; + size_t bestPathLength = 0; + int bestDistance = 0; + + std::optional bestFallback; + int bestFallbackDistance = 0; + + const auto considerTile = [&](Point tile) { + if (!PosOkPlayerIgnoreDoors(player, tile)) + return; + + const int distance = playerPosition.WalkingDistance(tile); + if (!bestFallback || distance < bestFallbackDistance) { + bestFallback = tile; + bestFallbackDistance = distance; + } + + const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); + if (!path) + return; + + const size_t pathLength = path->size(); + if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { + best = tile; + bestPathLength = pathLength; + bestDistance = distance; + } + }; + + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + considerTile(object.position + Displacement { dx, dy }); + } + } + + if (FindObjectAtPosition(object.position + Direction::NorthEast) == &object) { + for (int dx = -1; dx <= 1; ++dx) { + considerTile(object.position + Displacement { dx, -2 }); + } + } + + if (best) + return best; + + return bestFallback; +} + +struct DoorBlockInfo { + Point beforeDoor; + Point doorPosition; +}; + +std::optional FindFirstClosedDoorOnWalkPath(Point startPosition, const int8_t *path, int steps) +{ + Point position = startPosition; + for (int i = 0; i < steps; ++i) { + const Point next = NextPositionForWalkDirection(position, path[i]); + Object *object = FindObjectAtPosition(next); + if (object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) { + return DoorBlockInfo { .beforeDoor = position, .doorPosition = object->position }; + } + position = next; + } + return std::nullopt; +} + +enum class TrackerPathBlockType : uint8_t { + Door, + Monster, + Breakable, +}; + +struct TrackerPathBlockInfo { + TrackerPathBlockType type; + size_t stepIndex; + Point beforeBlock; + Point blockPosition; +}; + +[[nodiscard]] std::optional FindFirstTrackerPathBlock(Point startPosition, const int8_t *path, size_t steps, bool considerDoors, bool considerMonsters, bool considerBreakables, Point targetPosition) +{ + Point position = startPosition; + for (size_t i = 0; i < steps; ++i) { + const Point next = NextPositionForWalkDirection(position, path[i]); + if (next == targetPosition) { + position = next; + continue; + } + + Object *object = FindObjectAtPosition(next); + if (considerDoors && object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) { + return TrackerPathBlockInfo { + .type = TrackerPathBlockType::Door, + .stepIndex = i, + .beforeBlock = position, + .blockPosition = object->position, + }; + } + if (considerBreakables && object != nullptr && object->_oSolidFlag && object->IsBreakable()) { + return TrackerPathBlockInfo { + .type = TrackerPathBlockType::Breakable, + .stepIndex = i, + .beforeBlock = position, + .blockPosition = next, + }; + } + + if (considerMonsters && leveltype != DTYPE_TOWN && dMonster[next.x][next.y] != 0) { + const int monsterRef = dMonster[next.x][next.y]; + const int monsterId = std::abs(monsterRef) - 1; + const bool blocks = monsterRef <= 0 || (monsterId >= 0 && monsterId < static_cast(MaxMonsters) && !Monsters[monsterId].hasNoLife()); + if (blocks) { + return TrackerPathBlockInfo { + .type = TrackerPathBlockType::Monster, + .stepIndex = i, + .beforeBlock = position, + .blockPosition = next, + }; + } + } + + position = next; + } + + return std::nullopt; +} + +/** + * Validates an object-category auto-walk target and computes the walk destination. + */ +template +bool ValidateAutoWalkObjectTarget( + const Player &myPlayer, Point playerPosition, + Predicate isValid, const char *goneMessage, const char *inRangeMessage, + std::optional &destination) +{ + const int objectId = AutoWalkTrackerTargetId; + if (objectId < 0 || objectId >= MAXOBJECTS) { + AutoWalkTrackerTargetId = -1; + SpeakText(_(goneMessage), true); + return false; + } + const Object &object = Objects[objectId]; + if (!isValid(object)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_(goneMessage), true); + return false; + } + if (playerPosition.WalkingDistance(object.position) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_(inRangeMessage), true); + return false; + } + destination = FindBestApproachTileForObject(myPlayer, playerPosition, object); + return true; +} + +/** + * Resolves which object to walk toward for the given tracker category. + */ +template +std::optional ResolveObjectTrackerTarget( + int &lockedTargetId, Point playerPosition, + Predicate isValid, FindNearest findNearest, GetName getName, + const char *notFoundMessage, StringOrView &targetName) +{ + std::optional targetId; + if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = findNearest(playerPosition); + } + if (!targetId) { + SpeakText(_(notFoundMessage), true); + return std::nullopt; + } + if (!isValid(Objects[*targetId])) { + lockedTargetId = -1; + targetId = findNearest(playerPosition); + if (!targetId) { + SpeakText(_(notFoundMessage), true); + return std::nullopt; + } + if (!isValid(Objects[*targetId])) { + SpeakText(_(notFoundMessage), true); + return std::nullopt; + } + } + lockedTargetId = *targetId; + targetName = getName(*targetId); + return targetId; +} + +[[nodiscard]] std::vector TrackerTargetCategoriesForCurrentLevel() +{ + if (leveltype == DTYPE_TOWN) { + return { + TrackerTargetCategory::Items, + TrackerTargetCategory::DeadBodies, + TrackerTargetCategory::Npcs, + TrackerTargetCategory::Players, + TrackerTargetCategory::DungeonEntrances, + TrackerTargetCategory::Portals, + }; + } + + return { + TrackerTargetCategory::Items, + TrackerTargetCategory::Chests, + TrackerTargetCategory::Doors, + TrackerTargetCategory::Shrines, + TrackerTargetCategory::Objects, + TrackerTargetCategory::Breakables, + TrackerTargetCategory::Monsters, + TrackerTargetCategory::DeadBodies, + TrackerTargetCategory::DungeonEntrances, + TrackerTargetCategory::Stairs, + TrackerTargetCategory::QuestLocations, + TrackerTargetCategory::Players, + TrackerTargetCategory::Portals, + }; +} + +void SelectTrackerTargetCategoryRelative(int delta) +{ + if (!CanPlayerTakeAction() || InGameMenu()) + return; + + AutoWalkTrackerTargetId = -1; + + const std::vector categories = TrackerTargetCategoriesForCurrentLevel(); + if (categories.empty()) + return; + + auto it = std::find(categories.begin(), categories.end(), SelectedTrackerTargetCategory); + int currentIndex = 0; + if (it == categories.end()) { + currentIndex = delta > 0 ? -1 : 0; + } else { + currentIndex = static_cast(it - categories.begin()); + } + + const int count = static_cast(categories.size()); + int newIndex = (currentIndex + delta) % count; + if (newIndex < 0) + newIndex += count; + + SelectedTrackerTargetCategory = categories[static_cast(newIndex)]; + SpeakTrackerTargetCategory(); +} + +[[nodiscard]] std::vector CollectTrackerCandidatesForSelection(TrackerTargetCategory category, Point playerPosition) +{ + switch (category) { + case TrackerTargetCategory::Items: + return CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Chests: + return CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Doors: { + std::vector candidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + for (TrackerCandidate &c : candidates) { + if (c.id < 0 || c.id >= MAXOBJECTS) + continue; + c.name = DoorLabelForSpeech(Objects[c.id]); + } + return candidates; + } + case TrackerTargetCategory::Shrines: + return CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Objects: + return CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Breakables: + return CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Monsters: + return CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::DeadBodies: + return CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + case TrackerTargetCategory::Npcs: + return CollectNpcTrackerCandidates(playerPosition); + case TrackerTargetCategory::Players: + return CollectPlayerTrackerCandidates(playerPosition); + case TrackerTargetCategory::DungeonEntrances: + return CollectDungeonEntranceTrackerCandidates(playerPosition); + case TrackerTargetCategory::Stairs: + return CollectStairsTrackerCandidates(playerPosition); + case TrackerTargetCategory::QuestLocations: + return CollectQuestLocationTrackerCandidates(playerPosition); + case TrackerTargetCategory::Portals: + return CollectPortalTrackerCandidates(playerPosition); + } + app_fatal("Invalid TrackerTargetCategory"); +} + +[[nodiscard]] std::string_view TrackerCategoryNoCandidatesFoundMessage(TrackerTargetCategory category) +{ + switch (category) { + case TrackerTargetCategory::Items: + return _("No items found."); + case TrackerTargetCategory::Chests: + return _("No chests found."); + case TrackerTargetCategory::Doors: + return _("No doors found."); + case TrackerTargetCategory::Shrines: + return _("No shrines found."); + case TrackerTargetCategory::Objects: + return _("No objects found."); + case TrackerTargetCategory::Breakables: + return _("No breakables found."); + case TrackerTargetCategory::Monsters: + return _("No monsters found."); + case TrackerTargetCategory::DeadBodies: + return _("No dead bodies found."); + case TrackerTargetCategory::Npcs: + return _("No NPCs found."); + case TrackerTargetCategory::Players: + return _("No players found."); + case TrackerTargetCategory::DungeonEntrances: + if (leveltype != DTYPE_TOWN) + return _("No exits found."); + return _("No dungeon entrances found."); + case TrackerTargetCategory::Stairs: + return _("No stairs found."); + case TrackerTargetCategory::QuestLocations: + return _("No quest locations found."); + case TrackerTargetCategory::Portals: + return _("No portals found."); + } + app_fatal("Invalid TrackerTargetCategory"); +} + +[[nodiscard]] std::string_view TrackerCategoryNoNextMessage(TrackerTargetCategory category) +{ + switch (category) { + case TrackerTargetCategory::Items: + return _("No next item."); + case TrackerTargetCategory::Chests: + return _("No next chest."); + case TrackerTargetCategory::Doors: + return _("No next door."); + case TrackerTargetCategory::Shrines: + return _("No next shrine."); + case TrackerTargetCategory::Objects: + return _("No next object."); + case TrackerTargetCategory::Breakables: + return _("No next breakable."); + case TrackerTargetCategory::Monsters: + return _("No next monster."); + case TrackerTargetCategory::DeadBodies: + return _("No next dead body."); + case TrackerTargetCategory::Npcs: + return _("No next NPC."); + case TrackerTargetCategory::Players: + return _("No next player."); + case TrackerTargetCategory::DungeonEntrances: + return _("No next dungeon entrance."); + case TrackerTargetCategory::Stairs: + return _("No next stairs."); + case TrackerTargetCategory::QuestLocations: + return _("No next quest location."); + case TrackerTargetCategory::Portals: + return _("No next portal."); + } + app_fatal("Invalid TrackerTargetCategory"); +} + +[[nodiscard]] std::string_view TrackerCategoryNoPreviousMessage(TrackerTargetCategory category) +{ + switch (category) { + case TrackerTargetCategory::Items: + return _("No previous item."); + case TrackerTargetCategory::Chests: + return _("No previous chest."); + case TrackerTargetCategory::Doors: + return _("No previous door."); + case TrackerTargetCategory::Shrines: + return _("No previous shrine."); + case TrackerTargetCategory::Objects: + return _("No previous object."); + case TrackerTargetCategory::Breakables: + return _("No previous breakable."); + case TrackerTargetCategory::Monsters: + return _("No previous monster."); + case TrackerTargetCategory::DeadBodies: + return _("No previous dead body."); + case TrackerTargetCategory::Npcs: + return _("No previous NPC."); + case TrackerTargetCategory::Players: + return _("No previous player."); + case TrackerTargetCategory::DungeonEntrances: + return _("No previous dungeon entrance."); + case TrackerTargetCategory::Stairs: + return _("No previous stairs."); + case TrackerTargetCategory::QuestLocations: + return _("No previous quest location."); + case TrackerTargetCategory::Portals: + return _("No previous portal."); + } + app_fatal("Invalid TrackerTargetCategory"); +} + +/** + * Returns true if the given tracker category requires a dungeon (i.e. is not + * available in town). + */ +[[nodiscard]] bool IsDungeonOnlyTrackerCategory(TrackerTargetCategory category) +{ + return IsNoneOf(category, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies, + TrackerTargetCategory::Npcs, TrackerTargetCategory::Players, + TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals); +} + +void SelectTrackerTargetRelative(int delta) +{ + if (!CanPlayerTakeAction() || InGameMenu()) + return; + if (MyPlayer == nullptr) + return; + + if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) { + SpeakText(_("Not in a dungeon."), true); + return; + } + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + + EnsureTrackerLocksMatchCurrentLevel(); + + const Point playerPosition = MyPlayer->position.future; + AutoWalkTrackerTargetId = -1; + + const std::vector candidates = CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition); + if (candidates.empty()) { + LockedTrackerTargetId(SelectedTrackerTargetCategory) = -1; + SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + return; + } + + int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); + if (candidates.size() == 1) { + lockedTargetId = candidates.front().id; + SpeakText(candidates.front().name.str(), /*force=*/true); + return; + } + const std::optional targetId = delta > 0 ? FindNextTrackerCandidateId(candidates, lockedTargetId) : FindPreviousTrackerCandidateId(candidates, lockedTargetId); + if (!targetId) { + SpeakText(delta > 0 ? TrackerCategoryNoNextMessage(SelectedTrackerTargetCategory) : TrackerCategoryNoPreviousMessage(SelectedTrackerTargetCategory), true); + return; + } + + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == candidates.end()) { + lockedTargetId = -1; + SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + return; + } + + lockedTargetId = *targetId; + StringOrView targetName = it->name.str(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, candidates); + SpeakText(targetName.str(), /*force=*/true); +} + +} // namespace + +namespace { + +void NavigateToTrackerTargetKeyPressed() +{ + if (!CanPlayerTakeAction() || InGameMenu()) + return; + if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) { + SpeakText(_("Not in a dungeon."), true); + return; + } + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (MyPlayer == nullptr) + return; + + EnsureTrackerLocksMatchCurrentLevel(); + + const SDL_Keymod modState = SDL_GetModState(); + const bool cycleTarget = (modState & SDL_KMOD_SHIFT) != 0; + const bool clearTarget = (modState & SDL_KMOD_CTRL) != 0; + + const Point playerPosition = MyPlayer->position.future; + AutoWalkTrackerTargetId = -1; + + int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); + if (clearTarget) { + lockedTargetId = -1; + SpeakText(_("Tracker target cleared."), true); + return; + } + + std::optional targetId; + std::optional targetPosition; + std::optional alternateTargetPosition; + StringOrView targetName; + + switch (SelectedTrackerTargetCategory) { + case TrackerTargetCategory::Items: { + const std::vector nearbyCandidates = CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No items found."), true); + else + SpeakText(_("No next item."), true); + return; + } + } else if (IsGroundItemPresent(lockedTargetId)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestGroundItemId(playerPosition); + } + if (!targetId) { + SpeakText(_("No items found."), true); + return; + } + + if (!IsGroundItemPresent(*targetId)) { + lockedTargetId = -1; + SpeakText(_("No items found."), true); + return; + } + + lockedTargetId = *targetId; + const Item &tracked = Items[*targetId]; + + targetName = tracked.getName(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + targetPosition = tracked.position; + break; + } + case TrackerTargetCategory::Chests: { + const std::vector nearbyCandidates = CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No chests found."), true); + else + SpeakText(_("No next chest."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = FindNearestUnopenedChestObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No chests found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsTrackedChestObject(object)) { + lockedTargetId = -1; + targetId = FindNearestUnopenedChestObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No chests found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + targetName = tracked.name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; + } + break; + } + case TrackerTargetCategory::Doors: { + std::vector nearbyCandidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + for (TrackerCandidate &c : nearbyCandidates) { + if (c.id < 0 || c.id >= MAXOBJECTS) + continue; + c.name = DoorLabelForSpeech(Objects[c.id]); + } + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No doors found."), true); + else + SpeakText(_("No next door."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = FindNearestDoorObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No doors found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsTrackedDoorObject(object)) { + lockedTargetId = -1; + targetId = FindNearestDoorObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No doors found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + targetName = DoorLabelForSpeech(tracked); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; + } + break; + } + case TrackerTargetCategory::Shrines: { + const std::vector nearbyCandidates = CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No shrines found."), true); + else + SpeakText(_("No next shrine."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = FindNearestShrineObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No shrines found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsShrineLikeObject(object)) { + lockedTargetId = -1; + targetId = FindNearestShrineObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No shrines found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + targetName = tracked.name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; + } + break; + } + case TrackerTargetCategory::Objects: { + const std::vector nearbyCandidates = CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No objects found."), true); + else + SpeakText(_("No next object."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = FindNearestMiscInteractableObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No objects found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsTrackedMiscInteractableObject(object)) { + lockedTargetId = -1; + targetId = FindNearestMiscInteractableObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No objects found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + targetName = tracked.name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; + } + break; + } + case TrackerTargetCategory::Breakables: { + const std::vector nearbyCandidates = CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No breakables found."), true); + else + SpeakText(_("No next breakable."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { + targetId = lockedTargetId; + } else { + targetId = FindNearestBreakableObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No breakables found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsTrackedBreakableObject(object)) { + lockedTargetId = -1; + targetId = FindNearestBreakableObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No breakables found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + targetName = tracked.name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; + } + break; + } + case TrackerTargetCategory::Monsters: { + const std::vector nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No monsters found."), true); + else + SpeakText(_("No next monster."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestMonsterId(playerPosition); + } + if (!targetId) { + SpeakText(_("No monsters found."), true); + return; + } + + const Monster &monster = Monsters[*targetId]; + if (!IsTrackedMonster(monster)) { + lockedTargetId = -1; + targetId = FindNearestMonsterId(playerPosition); + if (!targetId) { + SpeakText(_("No monsters found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Monster &tracked = Monsters[*targetId]; + + targetName = tracked.name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = tracked.position.tile; + } + break; + } + case TrackerTargetCategory::DeadBodies: { + const std::vector nearbyCandidates = CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No dead bodies found."), true); + else + SpeakText(_("No next dead body."), true); + return; + } + } else if (IsCorpsePresent(lockedTargetId)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestCorpseId(playerPosition); + } + if (!targetId) { + SpeakText(_("No dead bodies found."), true); + return; + } + + if (!IsCorpsePresent(*targetId)) { + lockedTargetId = -1; + SpeakText(_("No dead bodies found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = _("Dead body"); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = CorpsePositionForTrackerId(*targetId); + } + break; + } + case TrackerTargetCategory::Npcs: { + const std::vector nearbyCandidates = CollectNpcTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No NPCs found."), true); + else + SpeakText(_("No next NPC."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < static_cast(GetNumTowners())) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No NPCs found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No NPCs found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = Towners[*targetId].name; + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = Towners[*targetId].position; + } + break; + } + case TrackerTargetCategory::Players: { + const std::vector nearbyCandidates = CollectPlayerTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No players found."), true); + else + SpeakText(_("No next player."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No players found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No players found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = Players[*targetId].name(); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = Players[*targetId].position.future; + } + break; + } + case TrackerTargetCategory::DungeonEntrances: { + const std::vector nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No dungeon entrances found."), true); + else + SpeakText(_("No next dungeon entrance."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No dungeon entrances found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No dungeon entrances found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = TriggerLabelForSpeech(trigs[*targetId]); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + const TriggerStruct &trigger = trigs[*targetId]; + targetPosition = Point { trigger.position.x, trigger.position.y }; + } + break; + } + case TrackerTargetCategory::Stairs: { + const std::vector nearbyCandidates = CollectStairsTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No stairs found."), true); + else + SpeakText(_("No next stairs."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No stairs found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No stairs found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = std::string(it->name.str()); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + const TriggerStruct &trigger = trigs[*targetId]; + targetPosition = Point { trigger.position.x, trigger.position.y }; + } + break; + } + case TrackerTargetCategory::QuestLocations: { + const std::vector nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No quest locations found."), true); + else + SpeakText(_("No next quest location."), true); + return; + } + } else if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast(sizeof(Quests) / sizeof(Quests[0])))) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No quest locations found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No quest locations found."), true); + return; + } + + lockedTargetId = *targetId; + targetName = std::string(it->name.str()); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + if (setlevel) { + const TriggerStruct &trigger = trigs[*targetId]; + targetPosition = Point { trigger.position.x, trigger.position.y }; + } else { + const Quest &quest = Quests[static_cast(*targetId)]; + targetPosition = quest.position; + } + } + break; + } + case TrackerTargetCategory::Portals: { + const std::vector nearbyCandidates = CollectPortalTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + if (!targetId) { + if (nearbyCandidates.empty()) + SpeakText(_("No portals found."), true); + else + SpeakText(_("No next portal."), true); + return; + } + } else if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) { + targetId = lockedTargetId; + } else if (!nearbyCandidates.empty()) { + targetId = nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No portals found."), true); + return; + } + + const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); + if (it == nearbyCandidates.end()) { + lockedTargetId = -1; + SpeakText(_("No portals found."), true); + return; + } + + Point portalPosition; + if (leveltype == DTYPE_TOWN) { + const std::optional townPos = FindTownPortalPositionInTownByPortalIndex(*targetId); + if (!townPos) { + lockedTargetId = -1; + SpeakText(_("No portals found."), true); + return; + } + portalPosition = *townPos; + } else { + if (!IsTownPortalOpenOnCurrentLevel(*targetId)) { + lockedTargetId = -1; + SpeakText(_("No portals found."), true); + return; + } + portalPosition = Portals[*targetId].position; + } + + lockedTargetId = *targetId; + targetName = TownPortalLabelForSpeech(Portals[*targetId]); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + targetPosition = portalPosition; + } + break; + } + } + + if (cycleTarget) { + SpeakText(targetName.str(), /*force=*/true); + return; + } + + if (!targetPosition) { + SpeakText(_("Can't find a nearby tile to walk to."), true); + return; + } + + Point chosenTargetPosition = *targetPosition; + enum class TrackerPathMode : uint8_t { + RespectDoorsAndMonsters, + IgnoreDoors, + IgnoreMonsters, + IgnoreDoorsAndMonsters, + Lenient, + }; + + auto findPathToTarget = [&](Point destination, TrackerPathMode mode) -> std::optional> { + const bool allowDestinationNonWalkable = !PosOkPlayer(*MyPlayer, destination); + switch (mode) { + case TrackerPathMode::RespectDoorsAndMonsters: + return FindKeyboardWalkPathForSpeechRespectingDoors(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); + case TrackerPathMode::IgnoreDoors: + return FindKeyboardWalkPathForSpeech(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); + case TrackerPathMode::IgnoreMonsters: + return FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); + case TrackerPathMode::IgnoreDoorsAndMonsters: + return FindKeyboardWalkPathForSpeechIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); + case TrackerPathMode::Lenient: + return FindKeyboardWalkPathForSpeechLenient(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); + default: + return std::nullopt; + } + }; + + std::optional> spokenPath; + bool pathIgnoresDoors = false; + bool pathIgnoresMonsters = false; + bool pathIgnoresBreakables = false; + + const auto considerDestination = [&](Point destination, TrackerPathMode mode) { + const std::optional> candidate = findPathToTarget(destination, mode); + if (!candidate) + return; + if (!spokenPath || candidate->size() < spokenPath->size()) { + spokenPath = *candidate; + chosenTargetPosition = destination; + + pathIgnoresDoors = mode == TrackerPathMode::IgnoreDoors || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; + pathIgnoresMonsters = mode == TrackerPathMode::IgnoreMonsters || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; + pathIgnoresBreakables = mode == TrackerPathMode::Lenient; + } + }; + + considerDestination(*targetPosition, TrackerPathMode::RespectDoorsAndMonsters); + if (alternateTargetPosition) + considerDestination(*alternateTargetPosition, TrackerPathMode::RespectDoorsAndMonsters); + + if (!spokenPath) { + considerDestination(*targetPosition, TrackerPathMode::IgnoreDoors); + if (alternateTargetPosition) + considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoors); + } + + if (!spokenPath) { + considerDestination(*targetPosition, TrackerPathMode::IgnoreMonsters); + if (alternateTargetPosition) + considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreMonsters); + } + + if (!spokenPath) { + considerDestination(*targetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); + if (alternateTargetPosition) + considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); + } + + if (!spokenPath) { + considerDestination(*targetPosition, TrackerPathMode::Lenient); + if (alternateTargetPosition) + considerDestination(*alternateTargetPosition, TrackerPathMode::Lenient); + } + + bool showUnreachableWarning = false; + if (!spokenPath) { + showUnreachableWarning = true; + Point closestPosition; + spokenPath = FindKeyboardWalkPathToClosestReachableForSpeech(*MyPlayer, playerPosition, chosenTargetPosition, closestPosition); + pathIgnoresDoors = true; + pathIgnoresMonsters = false; + pathIgnoresBreakables = false; + } + + if (spokenPath && !showUnreachableWarning && !PosOkPlayer(*MyPlayer, chosenTargetPosition)) { + if (!spokenPath->empty()) + spokenPath->pop_back(); + } + + if (spokenPath && (pathIgnoresDoors || pathIgnoresMonsters || pathIgnoresBreakables)) { + const std::optional block = FindFirstTrackerPathBlock(playerPosition, spokenPath->data(), spokenPath->size(), pathIgnoresDoors, pathIgnoresMonsters, pathIgnoresBreakables, chosenTargetPosition); + if (block) { + if (playerPosition.WalkingDistance(block->blockPosition) <= TrackerInteractDistanceTiles) { + switch (block->type) { + case TrackerPathBlockType::Door: + SpeakText(_("A door is blocking the path. Open it and try again."), true); + return; + case TrackerPathBlockType::Monster: + SpeakText(_("A monster is blocking the path. Clear it and try again."), true); + return; + case TrackerPathBlockType::Breakable: + SpeakText(_("A breakable object is blocking the path. Destroy it and try again."), true); + return; + } + } + + spokenPath = std::vector(spokenPath->begin(), spokenPath->begin() + block->stepIndex); + } + } + + std::string message; + if (!targetName.empty()) + StrAppend(message, targetName, "\n"); + if (showUnreachableWarning) { + message.append(_("Can't find a path to the target.")); + if (spokenPath && !spokenPath->empty()) + message.append("\n"); + } + if (spokenPath) { + if (!showUnreachableWarning || !spokenPath->empty()) + AppendKeyboardWalkPathForSpeech(message, *spokenPath); + } + + SpeakText(message, true); +} + +void AutoWalkToTrackerTargetKeyPressed() +{ + if (AutoWalkTrackerTargetId >= 0) { + CancelAutoWalk(); + SpeakText(_("Walk cancelled."), true); + return; + } + + if (!CanPlayerTakeAction() || InGameMenu()) + return; + + if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) { + SpeakText(_("Not in a dungeon."), true); + return; + } + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (MyPlayer == nullptr) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Cannot walk right now."), true); + return; + } + + EnsureTrackerLocksMatchCurrentLevel(); + + const Point playerPosition = MyPlayer->position.future; + int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); + + std::optional targetId; + StringOrView targetName; + + switch (SelectedTrackerTargetCategory) { + case TrackerTargetCategory::Items: { + if (IsGroundItemPresent(lockedTargetId)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestGroundItemId(playerPosition); + } + if (!targetId) { + SpeakText(_("No items found."), true); + return; + } + if (!IsGroundItemPresent(*targetId)) { + lockedTargetId = -1; + SpeakText(_("No items found."), true); + return; + } + lockedTargetId = *targetId; + targetName = Items[*targetId].getName(); + break; + } + case TrackerTargetCategory::Chests: + targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedChestObject, FindNearestUnopenedChestObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No chests found."), targetName); + if (!targetId) + return; + break; + case TrackerTargetCategory::Doors: + targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedDoorObject, FindNearestDoorObjectId, [](int id) -> StringOrView { return DoorLabelForSpeech(Objects[id]); }, N_("No doors found."), targetName); + if (!targetId) + return; + break; + case TrackerTargetCategory::Shrines: + targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsShrineLikeObject, FindNearestShrineObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No shrines found."), targetName); + if (!targetId) + return; + break; + case TrackerTargetCategory::Objects: + targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedMiscInteractableObject, FindNearestMiscInteractableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No objects found."), targetName); + if (!targetId) + return; + break; + case TrackerTargetCategory::Breakables: + targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedBreakableObject, FindNearestBreakableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No breakables found."), targetName); + if (!targetId) + return; + break; + case TrackerTargetCategory::Monsters: { + if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestMonsterId(playerPosition); + } + if (!targetId) { + SpeakText(_("No monsters found."), true); + return; + } + const Monster &monster = Monsters[*targetId]; + if (!IsTrackedMonster(monster)) { + lockedTargetId = -1; + targetId = FindNearestMonsterId(playerPosition); + if (!targetId) { + SpeakText(_("No monsters found."), true); + return; + } + } + lockedTargetId = *targetId; + targetName = Monsters[*targetId].name(); + break; + } + case TrackerTargetCategory::DeadBodies: { + if (IsCorpsePresent(lockedTargetId)) { + targetId = lockedTargetId; + } else { + targetId = FindNearestCorpseId(playerPosition); + } + if (!targetId) { + SpeakText(_("No dead bodies found."), true); + return; + } + if (!IsCorpsePresent(*targetId)) { + lockedTargetId = -1; + SpeakText(_("No dead bodies found."), true); + return; + } + lockedTargetId = *targetId; + targetName = _("Dead body"); + break; + } + case TrackerTargetCategory::Npcs: { + const std::vector candidates = CollectNpcTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No NPCs found."), true); + return; + } + + if (lockedTargetId >= 0 && lockedTargetId < static_cast(GetNumTowners())) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = Towners[*targetId].name; + break; + } + case TrackerTargetCategory::Players: { + const std::vector candidates = CollectPlayerTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No players found."), true); + return; + } + + if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = Players[*targetId].name(); + break; + } + case TrackerTargetCategory::DungeonEntrances: { + const std::vector candidates = CollectDungeonEntranceTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No dungeon entrances found."), true); + return; + } + + if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = TriggerLabelForSpeech(trigs[*targetId]); + break; + } + case TrackerTargetCategory::Stairs: { + const std::vector candidates = CollectStairsTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No stairs found."), true); + return; + } + + if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = TriggerLabelForSpeech(trigs[*targetId]); + break; + } + case TrackerTargetCategory::QuestLocations: { + const std::vector candidates = CollectQuestLocationTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No quest locations found."), true); + return; + } + + if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast(sizeof(Quests) / sizeof(Quests[0])))) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = std::string(candidates.front().name.str()); + if (const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); it != candidates.end()) + targetName = std::string(it->name.str()); + break; + } + case TrackerTargetCategory::Portals: { + const std::vector candidates = CollectPortalTrackerCandidates(playerPosition); + if (candidates.empty()) { + SpeakText(_("No portals found."), true); + return; + } + + if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) { + const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + if (it != candidates.end()) + targetId = lockedTargetId; + } + if (!targetId) + targetId = candidates.front().id; + + lockedTargetId = *targetId; + targetName = TownPortalLabelForSpeech(Portals[*targetId]); + break; + } + } + + if (!targetId) + return; + + std::string msg; + StrAppend(msg, _("Going to: "), targetName); + SpeakText(msg, true); + + AutoWalkTrackerTargetId = *targetId; + AutoWalkTrackerTargetCategory = SelectedTrackerTargetCategory; + UpdateAutoWalkTracker(); +} + +} // namespace + +void UpdateAutoWalkTracker() +{ + if (AutoWalkTrackerTargetId < 0) + return; + if (IsPlayerInStore() || ChatLogFlag || HelpFlag || InGameMenu()) { + AutoWalkTrackerTargetId = -1; + return; + } + if (leveltype == DTYPE_TOWN + && IsDungeonOnlyTrackerCategory(AutoWalkTrackerTargetCategory)) { + AutoWalkTrackerTargetId = -1; + return; + } + if (!CanPlayerTakeAction()) + return; + + if (MyPlayer == nullptr) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Cannot walk right now."), true); + return; + } + if (MyPlayer->_pmode != PM_STAND) + return; + if (MyPlayer->walkpath[0] != WALK_NONE) + return; + if (MyPlayer->destAction != ACTION_NONE) + return; + + Player &myPlayer = *MyPlayer; + const Point playerPosition = myPlayer.position.future; + + std::optional destination; + + switch (AutoWalkTrackerTargetCategory) { + case TrackerTargetCategory::Items: { + const int itemId = AutoWalkTrackerTargetId; + if (itemId < 0 || itemId > MAXITEMS) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target item is gone."), true); + return; + } + if (!IsGroundItemPresent(itemId)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target item is gone."), true); + return; + } + const Item &item = Items[itemId]; + if (playerPosition.WalkingDistance(item.position) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Item in range."), true); + return; + } + destination = item.position; + break; + } + case TrackerTargetCategory::Chests: + if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, + IsTrackedChestObject, N_("Target chest is gone."), N_("Chest in range."), destination)) + return; + break; + case TrackerTargetCategory::Doors: + if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedDoorObject, N_("Target door is gone."), N_("Door in range."), destination)) + return; + break; + case TrackerTargetCategory::Shrines: + if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsShrineLikeObject, N_("Target shrine is gone."), N_("Shrine in range."), destination)) + return; + break; + case TrackerTargetCategory::Objects: + if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedMiscInteractableObject, N_("Target object is gone."), N_("Object in range."), destination)) + return; + break; + case TrackerTargetCategory::Breakables: + if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedBreakableObject, N_("Target breakable is gone."), N_("Breakable in range."), destination)) + return; + break; + case TrackerTargetCategory::Monsters: { + const int monsterId = AutoWalkTrackerTargetId; + if (monsterId < 0 || monsterId >= static_cast(MaxMonsters)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target monster is gone."), true); + return; + } + const Monster &monster = Monsters[monsterId]; + if (!IsTrackedMonster(monster)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target monster is gone."), true); + return; + } + const Point monsterPosition { monster.position.tile }; + if (playerPosition.WalkingDistance(monsterPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Monster in range."), true); + return; + } + destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, monsterPosition); + break; + } + case TrackerTargetCategory::DeadBodies: { + const int corpseId = AutoWalkTrackerTargetId; + if (!IsCorpsePresent(corpseId)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target dead body is gone."), true); + return; + } + + const Point corpsePosition = CorpsePositionForTrackerId(corpseId); + if (playerPosition.WalkingDistance(corpsePosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Dead body in range."), true); + return; + } + + destination = corpsePosition; + break; + } + case TrackerTargetCategory::Npcs: { + const int npcId = AutoWalkTrackerTargetId; + if (leveltype != DTYPE_TOWN || npcId < 0 || npcId >= static_cast(GetNumTowners())) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target NPC is gone."), true); + return; + } + const Towner &towner = Towners[npcId]; + if (!IsTownerPresent(towner._ttype)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target NPC is gone."), true); + return; + } + if (playerPosition.WalkingDistance(towner.position) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("NPC in range."), true); + return; + } + destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, towner.position); + break; + } + case TrackerTargetCategory::Players: { + const int playerId = AutoWalkTrackerTargetId; + if (playerId < 0 || playerId >= MAX_PLRS) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target player is gone."), true); + return; + } + const Player &player = Players[playerId]; + if (!player.plractive || player._pLvlChanging || player.plrIsOnSetLevel != setlevel || player.plrlevel != MyPlayer->plrlevel) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target player is gone."), true); + return; + } + const Point targetPosition = player.position.future; + if (!InDungeonBounds(targetPosition)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target player is gone."), true); + return; + } + if (playerPosition.WalkingDistance(targetPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Player in range."), true); + return; + } + destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, targetPosition); + break; + } + case TrackerTargetCategory::DungeonEntrances: { + const int triggerIndex = AutoWalkTrackerTargetId; + if (triggerIndex < 0 || triggerIndex >= numtrigs) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target entrance is gone."), true); + return; + } + const TriggerStruct &trigger = trigs[triggerIndex]; + const bool valid = leveltype == DTYPE_TOWN + ? IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP) + : (setlevel ? trigger._tmsg == WM_DIABRTNLVL : trigger._tmsg == WM_DIABPREVLVL); + if (!valid) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target entrance is gone."), true); + return; + } + const Point triggerPosition { trigger.position.x, trigger.position.y }; + if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Entrance in range."), true); + return; + } + destination = triggerPosition; + break; + } + case TrackerTargetCategory::Stairs: { + const int triggerIndex = AutoWalkTrackerTargetId; + if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target stairs are gone."), true); + return; + } + const TriggerStruct &trigger = trigs[triggerIndex]; + if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target stairs are gone."), true); + return; + } + const Point triggerPosition { trigger.position.x, trigger.position.y }; + if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Stairs in range."), true); + return; + } + destination = triggerPosition; + break; + } + case TrackerTargetCategory::QuestLocations: { + if (setlevel) { + const int triggerIndex = AutoWalkTrackerTargetId; + if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target quest location is gone."), true); + return; + } + const TriggerStruct &trigger = trigs[triggerIndex]; + if (trigger._tmsg != WM_DIABRTNLVL) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target quest location is gone."), true); + return; + } + const Point triggerPosition { trigger.position.x, trigger.position.y }; + if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Quest exit in range."), true); + return; + } + destination = triggerPosition; + break; + } + + const int questIndex = AutoWalkTrackerTargetId; + if (questIndex < 0 || questIndex >= static_cast(sizeof(Quests) / sizeof(Quests[0]))) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target quest location is gone."), true); + return; + } + const Quest &quest = Quests[static_cast(questIndex)]; + if (quest._qslvl == SL_NONE || quest._qactive == QUEST_NOTAVAIL || quest._qlevel != currlevel || !InDungeonBounds(quest.position)) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target quest location is gone."), true); + return; + } + if (playerPosition.WalkingDistance(quest.position) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Quest entrance in range."), true); + return; + } + destination = quest.position; + break; + } + case TrackerTargetCategory::Portals: { + const int portalIndex = AutoWalkTrackerTargetId; + std::optional portalPosition; + if (leveltype == DTYPE_TOWN) { + portalPosition = FindTownPortalPositionInTownByPortalIndex(portalIndex); + } else if (IsTownPortalOpenOnCurrentLevel(portalIndex)) { + portalPosition = Portals[portalIndex].position; + } + + if (!portalPosition) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Target portal is gone."), true); + return; + } + if (playerPosition.WalkingDistance(*portalPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Portal in range."), true); + return; + } + destination = *portalPosition; + break; + } + } + + if (!destination) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Can't find a nearby tile to walk to."), true); + return; + } + + constexpr size_t MaxAutoWalkPathLength = 512; + std::array path; + path.fill(WALK_NONE); + + int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); + if (steps == 0) { + std::array ignoreDoorPath; + ignoreDoorPath.fill(WALK_NONE); + + const int ignoreDoorSteps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayerIgnoreDoors(myPlayer, position); }, playerPosition, *destination, ignoreDoorPath.data(), ignoreDoorPath.size()); + if (ignoreDoorSteps != 0) { + const std::optional block = FindFirstClosedDoorOnWalkPath(playerPosition, ignoreDoorPath.data(), ignoreDoorSteps); + if (block) { + if (playerPosition.WalkingDistance(block->doorPosition) <= TrackerInteractDistanceTiles) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("A door is blocking the path. Open it and try again."), true); + return; + } + + *destination = block->beforeDoor; + path.fill(WALK_NONE); + steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); + } + } + + if (steps == 0) { + AutoWalkTrackerTargetId = -1; + SpeakText(_("Can't find a path to the target."), true); + return; + } + } + + if (steps < static_cast(MaxPathLengthPlayer)) { + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, *destination); + return; + } + + const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); + const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); +} + +void TrackerPageUpKeyPressed() +{ + const SDL_Keymod modState = SDL_GetModState(); + const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0; + + if (cycleCategory) { + SelectTrackerTargetCategoryRelative(-1); + if (MyPlayer != nullptr) { + const Point playerPosition = MyPlayer->position.future; + if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) + SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + } + return; + } + + SelectTrackerTargetRelative(-1); +} + +void TrackerPageDownKeyPressed() +{ + const SDL_Keymod modState = SDL_GetModState(); + const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0; + + if (cycleCategory) { + SelectTrackerTargetCategoryRelative(+1); + if (MyPlayer != nullptr) { + const Point playerPosition = MyPlayer->position.future; + if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) + SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + } + return; + } + + SelectTrackerTargetRelative(+1); +} + +void TrackerHomeKeyPressed() +{ + const SDL_Keymod modState = SDL_GetModState(); + const bool autoWalk = (modState & SDL_KMOD_SHIFT) != 0; + + if (autoWalk) + AutoWalkToTrackerTargetKeyPressed(); + else + NavigateToTrackerTargetKeyPressed(); +} + +void ResetAutoWalkTracker() +{ + AutoWalkTrackerTargetId = -1; +} + +} // namespace devilution diff --git a/Source/controls/tracker.hpp b/Source/controls/tracker.hpp new file mode 100644 index 000000000..62063ab70 --- /dev/null +++ b/Source/controls/tracker.hpp @@ -0,0 +1,35 @@ +/** + * @file controls/tracker.hpp + * + * Tracker system for accessibility: target cycling, pathfinding, and auto-walk. + */ +#pragma once + +#include + +namespace devilution { + +enum class TrackerTargetCategory : uint8_t { + Items, + Chests, + Doors, + Shrines, + Objects, + Breakables, + Monsters, + DeadBodies, + Npcs, + Players, + DungeonEntrances, + Stairs, + QuestLocations, + Portals, +}; + +void TrackerPageUpKeyPressed(); +void TrackerPageDownKeyPressed(); +void TrackerHomeKeyPressed(); +void UpdateAutoWalkTracker(); +void ResetAutoWalkTracker(); + +} // namespace devilution diff --git a/Source/diablo.cpp b/Source/diablo.cpp index b2a1c5cb4..6faa5cdde 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1,7132 +1,1962 @@ -/** - * @file diablo.cpp - * - * Implementation of the main game initialization functions. - */ -#include -#include -#include -#include -#include -#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/path.h" -#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/tile_properties.hpp" -#include "levels/town.h" -#include "levels/trigs.h" -#include "lighting.h" -#include "loadsave.h" -#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/charpanel.hpp" -#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 "portal.h" -#include "qol/chatlog.h" -#include "qol/floatingnumbers.h" -#include "qol/itemlabels.h" -#include "qol/monhealthbar.h" -#include "qol/stash.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/format_int.hpp" -#include "utils/is_of.hpp" -#include "utils/language.h" -#include "utils/parse_int.hpp" -#include "utils/paths.h" -#include "utils/proximity_audio.hpp" -#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"; - - void SelectNextTownNpcKeyPressed(); - void SelectPreviousTownNpcKeyPressed(); - void UpdateAutoWalkTownNpc(); - void UpdateAutoWalkTracker(); - void AutoWalkToTrackerTargetKeyPressed(); - void SpeakSelectedSpeedbookSpell(); - void SpellBookKeyPressed(); -std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); -std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); -std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); -std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); -std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); -std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition); - void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path); - void AppendDirectionalFallback(std::string &message, const Displacement &delta); - std::string TriggerLabelForSpeech(const TriggerStruct &trigger); - [[nodiscard]] std::string TownPortalLabelForSpeech(const Portal &portal); - std::vector CollectTownDungeonTriggerIndices(); - - 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(); - 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 (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); - 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 (CharFlag) { - CharacterScreenActivateSelection((modState & SDL_KMOD_SHIFT) != 0); - } else if (IsPlayerInStore()) { - StoreEnter(); - } else if (QuestLogIsOpen) { - QuestlogEnter(); - } else if (SpellSelectFlag) { - SetSpell(); - } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { - const Player &player = *MyPlayer; - if (IsValidSpell(player._pRSpell)) { - std::string msg; - StrAppend(msg, _("Selected: "), pgettext("spell", GetSpellData(player._pRSpell).sNameText)); - SpeakText(msg, /*force=*/true); - } else { - SpeakText(_("No spell selected."), /*force=*/true); - } - SpellBookKeyPressed(); - } else { - TypeChatMessage(); - } - return; - case SDLK_UP: - if (IsPlayerInStore()) { - StoreUp(); - } else if (QuestLogIsOpen) { - QuestlogUp(); - } else if (CharFlag) { - CharacterScreenMoveSelection(-1); - } else if (HelpFlag) { - HelpScrollUp(); - } else if (ChatLogFlag) { - ChatLogScrollUp(); - } else if (SpellSelectFlag) { - HotSpellMove({ AxisDirectionX_NONE, AxisDirectionY_UP }); - SpeakSelectedSpeedbookSpell(); - } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { - const std::optional next = GetSpellBookAdjacentAvailableSpell(SpellbookTab, *MyPlayer, MyPlayer->_pRSpell, -1); - if (next) { - MyPlayer->_pRSpell = *next; - MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill - : (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges - : SpellType::Spell; - UpdateSpellTarget(*next); - RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); - } - } else if (invflag) { - InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_UP }); - } else if (AutomapActive) { - AutomapUp(); - } else if (IsStashOpen) { - Stash.PreviousPage(); - } - return; - case SDLK_DOWN: - if (IsPlayerInStore()) { - StoreDown(); - } else if (QuestLogIsOpen) { - QuestlogDown(); - } else if (CharFlag) { - CharacterScreenMoveSelection(+1); - } else if (HelpFlag) { - HelpScrollDown(); - } else if (ChatLogFlag) { - ChatLogScrollDown(); - } else if (SpellSelectFlag) { - HotSpellMove({ AxisDirectionX_NONE, AxisDirectionY_DOWN }); - SpeakSelectedSpeedbookSpell(); - } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { - const std::optional next = GetSpellBookAdjacentAvailableSpell(SpellbookTab, *MyPlayer, MyPlayer->_pRSpell, +1); - if (next) { - MyPlayer->_pRSpell = *next; - MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill - : (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges - : SpellType::Spell; - UpdateSpellTarget(*next); - RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); - } - } else if (invflag) { - InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_DOWN }); - } else if (AutomapActive) { - AutomapDown(); - } else if (IsStashOpen) { - Stash.NextPage(); - } - return; - case SDLK_PAGEUP: - if (IsPlayerInStore()) { - StorePrior(); - } else if (ChatLogFlag) { - ChatLogScrollTop(); - } else { - const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(vkey)); - if (action == nullptr || !action->isEnabled()) - SelectPreviousTownNpcKeyPressed(); - } - return; - case SDLK_PAGEDOWN: - if (IsPlayerInStore()) { - StoreNext(); - } else if (ChatLogFlag) { - ChatLogScrollBottom(); - } else { - const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(vkey)); - if (action == nullptr || !action->isEnabled()) - SelectNextTownNpcKeyPressed(); - } - return; - case SDLK_LEFT: - if (CharFlag) { - CharacterScreenMoveSelection(-1); - } else if (SpellSelectFlag) { - HotSpellMove({ AxisDirectionX_LEFT, AxisDirectionY_NONE }); - SpeakSelectedSpeedbookSpell(); - } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { - if (SpellbookTab > 0) { - SpellbookTab--; - const std::optional first = GetSpellBookFirstAvailableSpell(SpellbookTab, *MyPlayer); - if (first) { - MyPlayer->_pRSpell = *first; - MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill - : (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges - : SpellType::Spell; - UpdateSpellTarget(*first); - RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); - } - } - } else if (invflag) { - InventoryMoveFromKeyboard({ AxisDirectionX_LEFT, AxisDirectionY_NONE }); - } else if (AutomapActive && !ChatFlag) { - AutomapLeft(); - } - return; - case SDLK_RIGHT: - if (CharFlag) { - CharacterScreenMoveSelection(+1); - } else if (SpellSelectFlag) { - HotSpellMove({ AxisDirectionX_RIGHT, AxisDirectionY_NONE }); - SpeakSelectedSpeedbookSpell(); - } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { - const int maxTab = gbIsHellfire ? 4 : 3; - if (SpellbookTab < maxTab) { - SpellbookTab++; - const std::optional first = GetSpellBookFirstAvailableSpell(SpellbookTab, *MyPlayer); - if (first) { - MyPlayer->_pRSpell = *first; - MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill - : (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges - : SpellType::Spell; - UpdateSpellTarget(*first); - RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); - } - } - } else if (invflag) { - InventoryMoveFromKeyboard({ AxisDirectionX_RIGHT, AxisDirectionY_NONE }); - } else 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(); - LuaEvent("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); - } - } - } -} - -#ifdef NOSOUND -void UpdatePlayerLowHpWarningSound() -{ -} -#else -namespace { - -std::unique_ptr PlayerLowHpWarningSound; -bool TriedLoadingPlayerLowHpWarningSound = false; - -TSnd *GetPlayerLowHpWarningSound() -{ - if (TriedLoadingPlayerLowHpWarningSound) - return PlayerLowHpWarningSound.get(); - TriedLoadingPlayerLowHpWarningSound = true; - - if (!gbSndInited) - return nullptr; - - PlayerLowHpWarningSound = std::make_unique(); - PlayerLowHpWarningSound->start_tc = SDL_GetTicks() - 80 - 1; - - // Support both the new "playerhaslowhp" name and the older underscore version. - if (PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 - && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0) { - PlayerLowHpWarningSound = nullptr; - } - - return PlayerLowHpWarningSound.get(); -} - -void StopPlayerLowHpWarningSound() -{ - if (PlayerLowHpWarningSound != nullptr) - PlayerLowHpWarningSound->DSB.Stop(); -} - -[[nodiscard]] uint32_t LowHpIntervalMs(int hpPercent) -{ - // The sound starts at 50% HP (slow) and speeds up every 10% down to 0%. - if (hpPercent > 40) - return 1500; - if (hpPercent > 30) - return 1200; - if (hpPercent > 20) - return 900; - if (hpPercent > 10) - return 600; - return 300; -} - -} // namespace - -void UpdatePlayerLowHpWarningSound() -{ - static uint32_t LastWarningStartMs = 0; - - if (!gbSndInited || !gbSoundOn || MyPlayer == nullptr || InGameMenu()) { - StopPlayerLowHpWarningSound(); - LastWarningStartMs = 0; - return; - } - - // Stop immediately when dead. - if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { - StopPlayerLowHpWarningSound(); - LastWarningStartMs = 0; - return; - } - - const int maxHp = MyPlayer->_pMaxHP; - if (maxHp <= 0) { - StopPlayerLowHpWarningSound(); - LastWarningStartMs = 0; - return; - } - - const int hp = std::clamp(MyPlayer->_pHitPoints, 0, maxHp); - const int hpPercent = std::clamp(hp * 100 / maxHp, 0, 100); - - // Only play below (or equal to) 50% and above 0%. - if (hpPercent > 50 || hpPercent <= 0) { - StopPlayerLowHpWarningSound(); - LastWarningStartMs = 0; - return; - } - - TSnd *snd = GetPlayerLowHpWarningSound(); - if (snd == nullptr || !snd->DSB.IsLoaded()) - return; - - const uint32_t now = SDL_GetTicks(); - const uint32_t intervalMs = LowHpIntervalMs(hpPercent); - if (LastWarningStartMs == 0) - LastWarningStartMs = now - intervalMs; - if (now - LastWarningStartMs < intervalMs) - return; - - // Restart the cue even if it's already playing so the "tempo" is controlled by HP. - snd->DSB.Stop(); - snd_play_snd(snd, /*lVolume=*/0, /*lPan=*/0); - LastWarningStartMs = now; -} -#endif // NOSOUND - -namespace { - -[[nodiscard]] bool IsBossMonsterForHpAnnouncement(const Monster &monster) -{ - return monster.isUnique() || monster.ai == MonsterAIID::Diablo; -} - -} // namespace - -void UpdateLowDurabilityWarnings() -{ - static std::array WarnedSeeds {}; - static std::array HasWarned {}; - - if (MyPlayer == nullptr) - return; - if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) - return; - - std::vector newlyLow; - newlyLow.reserve(NUM_INVLOC); - - for (int slot = 0; slot < NUM_INVLOC; ++slot) { - const Item &item = MyPlayer->InvBody[slot]; - if (item.isEmpty() || item._iMaxDur <= 0 || item._iMaxDur == DUR_INDESTRUCTIBLE || item._iDurability == DUR_INDESTRUCTIBLE) { - HasWarned[slot] = false; - continue; - } - - const int maxDur = item._iMaxDur; - const int durability = item._iDurability; - if (durability <= 0) { - HasWarned[slot] = false; - continue; - } - - int threshold = std::max(2, maxDur / 10); - threshold = std::clamp(threshold, 1, maxDur); - - const bool isLow = durability <= threshold; - if (!isLow) { - HasWarned[slot] = false; - continue; - } - - if (HasWarned[slot] && WarnedSeeds[slot] == item._iSeed) - continue; - - HasWarned[slot] = true; - WarnedSeeds[slot] = item._iSeed; - - const StringOrView name = item.getName(); - if (!name.empty()) - newlyLow.emplace_back(name.str().data(), name.str().size()); - } - - if (newlyLow.empty()) - return; - - // Add ordinal numbers for duplicates (e.g. two rings with the same name). - for (size_t i = 0; i < newlyLow.size(); ++i) { - int total = 0; - for (size_t j = 0; j < newlyLow.size(); ++j) { - if (newlyLow[j] == newlyLow[i]) - ++total; - } - if (total <= 1) - continue; - - int ordinal = 1; - for (size_t j = 0; j < i; ++j) { - if (newlyLow[j] == newlyLow[i]) - ++ordinal; - } - newlyLow[i] = fmt::format("{} {}", newlyLow[i], ordinal); - } - - std::string joined; - for (size_t i = 0; i < newlyLow.size(); ++i) { - if (i != 0) - joined += ", "; - joined += newlyLow[i]; - } - - SpeakText(fmt::format(fmt::runtime(_("Low durability: {:s}")), joined), /*force=*/true); -} - -void UpdateBossHealthAnnouncements() -{ - static dungeon_type LastLevelType = DTYPE_NONE; - static int LastCurrLevel = -1; - static bool LastSetLevel = false; - static _setlevels LastSetLevelNum = SL_NONE; - static std::array LastAnnouncedBucket {}; - - if (MyPlayer == nullptr) - return; - if (leveltype == DTYPE_TOWN) - return; - - const bool levelChanged = LastLevelType != leveltype || LastCurrLevel != currlevel || LastSetLevel != setlevel || LastSetLevelNum != setlvlnum; - if (levelChanged) { - LastAnnouncedBucket.fill(-1); - LastLevelType = leveltype; - LastCurrLevel = currlevel; - LastSetLevel = setlevel; - LastSetLevelNum = setlvlnum; - } - - for (size_t monsterId = 0; monsterId < MaxMonsters; ++monsterId) { - if (LastAnnouncedBucket[monsterId] < 0) - continue; - - const Monster &monster = Monsters[monsterId]; - if (monster.isInvalid || monster.hitPoints <= 0 || !IsBossMonsterForHpAnnouncement(monster)) - LastAnnouncedBucket[monsterId] = -1; - } - - for (size_t i = 0; i < ActiveMonsterCount; i++) { - const int monsterId = static_cast(ActiveMonsters[i]); - const Monster &monster = Monsters[monsterId]; - - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (!IsBossMonsterForHpAnnouncement(monster)) - continue; - if (monster.hitPoints <= 0 || monster.maxHitPoints <= 0) - continue; - - const int64_t hp = std::clamp(monster.hitPoints, 0, monster.maxHitPoints); - const int64_t maxHp = monster.maxHitPoints; - const int hpPercent = static_cast(std::clamp(hp * 100 / maxHp, 0, 100)); - const int bucket = ((hpPercent + 9) / 10) * 10; - - int8_t &lastBucket = LastAnnouncedBucket[monsterId]; - if (lastBucket < 0) { - lastBucket = static_cast(((hpPercent + 9) / 10) * 10); - continue; - } - - if (bucket >= lastBucket) - continue; - - lastBucket = static_cast(bucket); - SpeakText(fmt::format(fmt::runtime(_("{:s} health: {:d}%")), monster.name(), bucket), /*force=*/false); - } -} - -void UpdateAttackableMonsterAnnouncements() -{ - static std::optional LastAttackableMonsterId; - - if (MyPlayer == nullptr) { - LastAttackableMonsterId = std::nullopt; - return; - } - if (leveltype == DTYPE_TOWN) { - LastAttackableMonsterId = std::nullopt; - return; - } - if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { - LastAttackableMonsterId = std::nullopt; - return; - } - if (InGameMenu() || invflag) { - LastAttackableMonsterId = std::nullopt; - return; - } - - const Player &player = *MyPlayer; - const Point playerPosition = player.position.tile; - - int bestRotations = 5; - std::optional bestId; - - for (size_t i = 0; i < ActiveMonsterCount; i++) { - const int monsterId = static_cast(ActiveMonsters[i]); - const Monster &monster = Monsters[monsterId]; - - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (monster.hitPoints <= 0) - continue; - if (monster.isPlayerMinion()) - continue; - if (!monster.isPossibleToHit()) - continue; - - const Point monsterPosition = monster.position.tile; - if (playerPosition.WalkingDistance(monsterPosition) > 1) - continue; - - const int d1 = static_cast(player._pdir); - const int d2 = static_cast(GetDirection(playerPosition, monsterPosition)); - - int rotations = std::abs(d1 - d2); - if (rotations > 4) - rotations = 4 - (rotations % 4); - - if (!bestId || rotations < bestRotations || (rotations == bestRotations && monsterId < *bestId)) { - bestRotations = rotations; - bestId = monsterId; - } - } - - if (!bestId) { - LastAttackableMonsterId = std::nullopt; - return; - } - - if (LastAttackableMonsterId && *LastAttackableMonsterId == *bestId) - return; - - LastAttackableMonsterId = *bestId; - - const std::string_view name = Monsters[*bestId].name(); - if (!name.empty()) - SpeakText(name, /*force=*/true); -} - -[[nodiscard]] StringOrView DoorLabelForSpeech(const Object &door) -{ - if (!door.isDoor()) - return door.name(); - - // Catacombs doors are grates, so differentiate them for the screen reader / tracker. - if (IsAnyOf(door._otype, _object_id::OBJ_L2LDOOR, _object_id::OBJ_L2RDOOR)) { - if (door._oVar4 == DOOR_OPEN) - return _("Open Grate Door"); - if (door._oVar4 == DOOR_CLOSED) - return _("Closed Grate Door"); - if (door._oVar4 == DOOR_BLOCKED) - return _("Blocked Grate Door"); - return _("Grate Door"); - } - - return door.name(); -} - -void UpdateInteractableDoorAnnouncements() -{ - static std::optional LastInteractableDoorId; - static std::optional LastInteractableDoorState; - - if (MyPlayer == nullptr) { - LastInteractableDoorId = std::nullopt; - LastInteractableDoorState = std::nullopt; - return; - } - if (leveltype == DTYPE_TOWN) { - LastInteractableDoorId = std::nullopt; - LastInteractableDoorState = std::nullopt; - return; - } - if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { - LastInteractableDoorId = std::nullopt; - LastInteractableDoorState = std::nullopt; - return; - } - if (InGameMenu() || invflag) { - LastInteractableDoorId = std::nullopt; - LastInteractableDoorState = std::nullopt; - return; - } - - const Player &player = *MyPlayer; - const Point playerPosition = player.position.tile; - - std::optional bestId; - int bestRotations = 5; - int bestDistance = 0; - - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - - const Point pos = playerPosition + Displacement { dx, dy }; - if (!InDungeonBounds(pos)) - continue; - - const int objectId = std::abs(dObject[pos.x][pos.y]) - 1; - if (objectId < 0 || objectId >= MAXOBJECTS) - continue; - - const Object &door = Objects[objectId]; - if (!door.isDoor() || !door.canInteractWith()) - continue; - - const int distance = playerPosition.WalkingDistance(door.position); - if (distance > 1) - continue; - - const int d1 = static_cast(player._pdir); - const int d2 = static_cast(GetDirection(playerPosition, door.position)); - - int rotations = std::abs(d1 - d2); - if (rotations > 4) - rotations = 4 - (rotations % 4); - - if (!bestId || rotations < bestRotations || (rotations == bestRotations && distance < bestDistance) - || (rotations == bestRotations && distance == bestDistance && objectId < *bestId)) { - bestRotations = rotations; - bestDistance = distance; - bestId = objectId; - } - } - } - - if (!bestId) { - LastInteractableDoorId = std::nullopt; - LastInteractableDoorState = std::nullopt; - return; - } - - const Object &door = Objects[*bestId]; - const int state = door._oVar4; - if (LastInteractableDoorId && LastInteractableDoorState && *LastInteractableDoorId == *bestId && *LastInteractableDoorState == state) - return; - - LastInteractableDoorId = *bestId; - LastInteractableDoorState = state; - - const StringOrView label = DoorLabelForSpeech(door); - if (!label.empty()) - SpeakText(label.str(), /*force=*/true); -} - -void GameLogic() -{ - if (!ProcessInput()) { - return; - } - if (gbProcessPlayers) { - gGameLogicStep = GameLogicStep::ProcessPlayers; - ProcessPlayers(); - UpdateAutoWalkTownNpc(); - UpdateAutoWalkTracker(); - UpdateLowDurabilityWarnings(); - } - 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(); - UpdateBossHealthAnnouncements(); - UpdateProximityAudioCues(); - UpdateAttackableMonsterAnnouncements(); - UpdateInteractableDoorAnnouncements(); - } else { - gGameLogicStep = GameLogicStep::ProcessTowners; - ProcessTowners(); - gGameLogicStep = GameLogicStep::ProcessItemsTown; - ProcessItems(); - gGameLogicStep = GameLogicStep::ProcessMissilesTown; - ProcessMissiles(); - UpdateProximityAudioCues(); - } - - UpdatePlayerLowHpWarningSound(); - - 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(); - } -} - -bool CanPlayerTakeAction(); - -std::vector TownNpcOrder; -int SelectedTownNpc = -1; -int AutoWalkTownNpcTarget = -1; - -enum class TrackerTargetCategory : uint8_t { - Items, - Chests, - Doors, - Shrines, - Objects, - Breakables, - Monsters, - DeadBodies, - Npcs, - Players, - DungeonEntrances, - Stairs, - QuestLocations, - Portals, -}; - -TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items; -TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items; ///< Category of the active auto-walk target. -int AutoWalkTrackerTargetId = -1; ///< ID of the target being auto-walked to, or -1 if inactive. - -Point NextPositionForWalkDirection(Point position, int8_t walkDir) -{ - switch (walkDir) { - case WALK_NE: - return { position.x, position.y - 1 }; - case WALK_NW: - return { position.x - 1, position.y }; - case WALK_SE: - return { position.x + 1, position.y }; - case WALK_SW: - return { position.x, position.y + 1 }; - case WALK_N: - return { position.x - 1, position.y - 1 }; - case WALK_E: - return { position.x + 1, position.y - 1 }; - case WALK_S: - return { position.x + 1, position.y + 1 }; - case WALK_W: - return { position.x - 1, position.y + 1 }; - default: - return position; - } -} - -Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps) -{ - Point position = start; - for (int i = 0; i < steps; ++i) { - position = NextPositionForWalkDirection(position, path[i]); - } - return position; -} - -int8_t OppositeWalkDirection(int8_t walkDir) -{ - switch (walkDir) { - case WALK_NE: - return WALK_SW; - case WALK_SW: - return WALK_NE; - case WALK_NW: - return WALK_SE; - case WALK_SE: - return WALK_NW; - case WALK_N: - return WALK_S; - case WALK_S: - return WALK_N; - case WALK_E: - return WALK_W; - case WALK_W: - return WALK_E; - default: - return WALK_NONE; - } -} - -bool IsTownNpcActionAllowed() -{ - return CanPlayerTakeAction() - && leveltype == DTYPE_TOWN - && !IsPlayerInStore() - && !ChatLogFlag - && !HelpFlag; -} - -void ResetTownNpcSelection() -{ - TownNpcOrder.clear(); - SelectedTownNpc = -1; -} - -void RefreshTownNpcOrder(bool selectFirst = false) -{ - TownNpcOrder.clear(); - if (leveltype != DTYPE_TOWN) - return; - - const Point playerPosition = MyPlayer->position.future; - - for (size_t i = 0; i < GetNumTowners(); ++i) { - const Towner &towner = Towners[i]; - if (!IsTownerPresent(towner._ttype)) - continue; - if (towner._ttype == TOWN_COW) - continue; - TownNpcOrder.push_back(static_cast(i)); - } - - if (TownNpcOrder.empty()) { - SelectedTownNpc = -1; - return; - } - - std::sort(TownNpcOrder.begin(), TownNpcOrder.end(), [&playerPosition](int a, int b) { - const Towner &townerA = Towners[a]; - const Towner &townerB = Towners[b]; - const int distanceA = playerPosition.WalkingDistance(townerA.position); - const int distanceB = playerPosition.WalkingDistance(townerB.position); - if (distanceA != distanceB) - return distanceA < distanceB; - return townerA.name < townerB.name; - }); - - if (selectFirst) { - SelectedTownNpc = TownNpcOrder.front(); - return; - } - - const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); - if (it == TownNpcOrder.end()) - SelectedTownNpc = TownNpcOrder.front(); -} - -void EnsureTownNpcOrder() -{ - if (leveltype != DTYPE_TOWN) { - ResetTownNpcSelection(); - return; - } - if (TownNpcOrder.empty()) { - RefreshTownNpcOrder(true); - return; - } - if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { - RefreshTownNpcOrder(true); - return; - } - const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); - if (it == TownNpcOrder.end()) - SelectedTownNpc = TownNpcOrder.front(); -} - -void SpeakSelectedTownNpc() -{ - EnsureTownNpcOrder(); - - if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { - SpeakText(_("No NPC selected."), true); - return; - } - - const Towner &towner = Towners[SelectedTownNpc]; - const Point playerPosition = MyPlayer->position.future; - const int distance = playerPosition.WalkingDistance(towner.position); - - std::string msg; - StrAppend(msg, towner.name); - StrAppend(msg, "\n", _("Distance: "), distance); - StrAppend(msg, "\n", _("Position: "), towner.position.x, ", ", towner.position.y); - SpeakText(msg, true); -} - -void SelectTownNpcRelative(int delta) -{ - if (!IsTownNpcActionAllowed()) - return; - - EnsureTownNpcOrder(); - if (TownNpcOrder.empty()) { - SpeakText(_("No town NPCs found."), true); - return; - } - - auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); - int currentIndex = (it != TownNpcOrder.end()) ? static_cast(it - TownNpcOrder.begin()) : 0; - - const int size = static_cast(TownNpcOrder.size()); - int newIndex = (currentIndex + delta) % size; - if (newIndex < 0) - newIndex += size; - SelectedTownNpc = TownNpcOrder[static_cast(newIndex)]; - SpeakSelectedTownNpc(); -} - -void SelectNextTownNpcKeyPressed() -{ - SelectTownNpcRelative(+1); -} - -void SelectPreviousTownNpcKeyPressed() -{ - SelectTownNpcRelative(-1); -} - -void GoToSelectedTownNpcKeyPressed() -{ - if (!IsTownNpcActionAllowed()) - return; - - EnsureTownNpcOrder(); - if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { - SpeakText(_("No NPC selected."), true); - return; - } - - const Towner &towner = Towners[SelectedTownNpc]; - - std::string msg; - StrAppend(msg, _("Going to: "), towner.name); - SpeakText(msg, true); - - AutoWalkTownNpcTarget = SelectedTownNpc; - UpdateAutoWalkTownNpc(); -} - -void UpdateAutoWalkTownNpc() -{ - if (AutoWalkTownNpcTarget < 0) - return; - if (leveltype != DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag) { - AutoWalkTownNpcTarget = -1; - return; - } - if (!CanPlayerTakeAction()) - return; - - if (MyPlayer->_pmode != PM_STAND) - return; - if (MyPlayer->walkpath[0] != WALK_NONE) - return; - if (MyPlayer->destAction != ACTION_NONE) - return; - - if (AutoWalkTownNpcTarget >= static_cast(GetNumTowners())) { - AutoWalkTownNpcTarget = -1; - SpeakText(_("No NPC selected."), true); - return; - } - - const Towner &towner = Towners[AutoWalkTownNpcTarget]; - if (!IsTownerPresent(towner._ttype) || towner._ttype == TOWN_COW) { - AutoWalkTownNpcTarget = -1; - SpeakText(_("No NPC selected."), true); - return; - } - - Player &myPlayer = *MyPlayer; - const Point playerPosition = myPlayer.position.future; - if (playerPosition.WalkingDistance(towner.position) < 2) { - const int townerIdx = AutoWalkTownNpcTarget; - AutoWalkTownNpcTarget = -1; - NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); - return; - } - - constexpr size_t MaxAutoWalkPathLength = 512; - std::array path; - path.fill(WALK_NONE); - - const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, towner.position, path.data(), path.size()); - if (steps == 0) { - AutoWalkTownNpcTarget = -1; - std::string error; - StrAppend(error, _("Can't find a path to: "), towner.name); - SpeakText(error, true); - return; - } - - // FindPath returns 0 if the path length is equal to the maximum. - // The player walkpath buffer is MaxPathLengthPlayer, so keep segments strictly shorter. - if (steps < static_cast(MaxPathLengthPlayer)) { - const int townerIdx = AutoWalkTownNpcTarget; - AutoWalkTownNpcTarget = -1; - NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); - return; - } - - const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); - const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); -} - -namespace { - -/// Maximum Chebyshev distance (in tiles) at which the player is considered -/// close enough to interact with a tracker target. -constexpr int TrackerInteractDistanceTiles = 1; -constexpr int TrackerCycleDistanceTiles = 12; - -int LockedTrackerItemId = -1; -int LockedTrackerChestId = -1; -int LockedTrackerDoorId = -1; -int LockedTrackerShrineId = -1; -int LockedTrackerObjectId = -1; -int LockedTrackerBreakableId = -1; -int LockedTrackerMonsterId = -1; -int LockedTrackerDeadBodyId = -1; -int LockedTrackerNpcId = -1; -int LockedTrackerPlayerId = -1; -int LockedTrackerDungeonEntranceId = -1; -int LockedTrackerStairsId = -1; -int LockedTrackerQuestLocationId = -1; -int LockedTrackerPortalId = -1; - -struct TrackerLevelKey { - dungeon_type levelType; - int currLevel; - bool isSetLevel; - int setLevelNum; -}; - -std::optional LockedTrackerLevelKey; - -void ClearTrackerLocks() -{ - LockedTrackerItemId = -1; - LockedTrackerChestId = -1; - LockedTrackerDoorId = -1; - LockedTrackerShrineId = -1; - LockedTrackerObjectId = -1; - LockedTrackerBreakableId = -1; - LockedTrackerMonsterId = -1; - LockedTrackerDeadBodyId = -1; - LockedTrackerNpcId = -1; - LockedTrackerPlayerId = -1; - LockedTrackerDungeonEntranceId = -1; - LockedTrackerStairsId = -1; - LockedTrackerQuestLocationId = -1; - LockedTrackerPortalId = -1; -} - -void EnsureTrackerLocksMatchCurrentLevel() -{ - const TrackerLevelKey current { - .levelType = leveltype, - .currLevel = currlevel, - .isSetLevel = setlevel, - .setLevelNum = setlvlnum, - }; - - if (!LockedTrackerLevelKey || LockedTrackerLevelKey->levelType != current.levelType || LockedTrackerLevelKey->currLevel != current.currLevel - || LockedTrackerLevelKey->isSetLevel != current.isSetLevel || LockedTrackerLevelKey->setLevelNum != current.setLevelNum) { - ClearTrackerLocks(); - LockedTrackerLevelKey = current; - } -} - -int &LockedTrackerTargetId(TrackerTargetCategory category) -{ - switch (category) { - case TrackerTargetCategory::Items: - return LockedTrackerItemId; - case TrackerTargetCategory::Chests: - return LockedTrackerChestId; - case TrackerTargetCategory::Doors: - return LockedTrackerDoorId; - case TrackerTargetCategory::Shrines: - return LockedTrackerShrineId; - case TrackerTargetCategory::Objects: - return LockedTrackerObjectId; - case TrackerTargetCategory::Breakables: - return LockedTrackerBreakableId; - case TrackerTargetCategory::Monsters: - return LockedTrackerMonsterId; - case TrackerTargetCategory::DeadBodies: - return LockedTrackerDeadBodyId; - case TrackerTargetCategory::Npcs: - return LockedTrackerNpcId; - case TrackerTargetCategory::Players: - return LockedTrackerPlayerId; - case TrackerTargetCategory::DungeonEntrances: - return LockedTrackerDungeonEntranceId; - case TrackerTargetCategory::Stairs: - return LockedTrackerStairsId; - case TrackerTargetCategory::QuestLocations: - return LockedTrackerQuestLocationId; - case TrackerTargetCategory::Portals: - return LockedTrackerPortalId; - } - app_fatal("Invalid TrackerTargetCategory"); -} - -std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category) -{ - switch (category) { - case TrackerTargetCategory::Items: - return _("items"); - case TrackerTargetCategory::Chests: - return _("chests"); - case TrackerTargetCategory::Doors: - return _("doors"); - case TrackerTargetCategory::Shrines: - return _("shrines"); - case TrackerTargetCategory::Objects: - return _("objects"); - case TrackerTargetCategory::Breakables: - return _("breakables"); - case TrackerTargetCategory::Monsters: - return _("monsters"); - case TrackerTargetCategory::DeadBodies: - return _("dead bodies"); - case TrackerTargetCategory::Npcs: - return _("NPCs"); - case TrackerTargetCategory::Players: - return _("players"); - case TrackerTargetCategory::DungeonEntrances: - return _("dungeon entrances"); - case TrackerTargetCategory::Stairs: - return _("stairs"); - case TrackerTargetCategory::QuestLocations: - return _("quest locations"); - case TrackerTargetCategory::Portals: - return _("portals"); - default: - return _("items"); - } -} - -void SpeakTrackerTargetCategory() -{ - SpeakText(TrackerTargetCategoryLabel(SelectedTrackerTargetCategory), true); -} - -[[nodiscard]] std::vector TrackerTargetCategoriesForCurrentLevel() -{ - if (leveltype == DTYPE_TOWN) { - return { - TrackerTargetCategory::Items, - TrackerTargetCategory::DeadBodies, - TrackerTargetCategory::Npcs, - TrackerTargetCategory::Players, - TrackerTargetCategory::DungeonEntrances, - TrackerTargetCategory::Portals, - }; - } +/** + * @file diablo.cpp + * + * Implementation of the main game initialization functions. + */ +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include +#include +#else +#include + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif +#endif - return { - TrackerTargetCategory::Items, - TrackerTargetCategory::Chests, - TrackerTargetCategory::Doors, - TrackerTargetCategory::Shrines, - TrackerTargetCategory::Objects, - TrackerTargetCategory::Breakables, - TrackerTargetCategory::Monsters, - TrackerTargetCategory::DeadBodies, - TrackerTargetCategory::DungeonEntrances, - TrackerTargetCategory::Stairs, - TrackerTargetCategory::QuestLocations, - TrackerTargetCategory::Players, - TrackerTargetCategory::Portals, - }; -} +#include -void SelectTrackerTargetCategoryRelative(int delta) -{ - if (!CanPlayerTakeAction() || InGameMenu()) - return; +#include - AutoWalkTrackerTargetId = -1; +#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/accessibility_keys.hpp" +#include "controls/control_mode.hpp" +#include "controls/keymapper.hpp" +#include "controls/plrctrls.h" +#include "controls/remap_keyboard.h" +#include "controls/town_npc_nav.hpp" +#include "controls/tracker.hpp" +#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/path.h" +#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_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/charpanel.hpp" +#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 "portal.h" +#include "qol/chatlog.h" +#include "qol/floatingnumbers.h" +#include "qol/itemlabels.h" +#include "qol/monhealthbar.h" +#include "qol/stash.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/accessibility_announcements.hpp" +#include "utils/navigation_speech.hpp" +#include "utils/proximity_audio.hpp" +#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 - const std::vector categories = TrackerTargetCategoriesForCurrentLevel(); - if (categories.empty()) - return; +#ifdef __vita__ +#include "platform/vita/touch.h" +#endif - auto it = std::find(categories.begin(), categories.end(), SelectedTrackerTargetCategory); - int currentIndex = 0; - if (it == categories.end()) { - currentIndex = delta > 0 ? -1 : 0; - } else { - currentIndex = static_cast(it - categories.begin()); - } +#ifdef GPERF_HEAP_FIRST_GAME_ITERATION +#include +#endif - const int count = static_cast(categories.size()); - int newIndex = (currentIndex + delta) % count; - if (newIndex < 0) - newIndex += count; +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; - SelectedTrackerTargetCategory = categories[static_cast(newIndex)]; - SpeakTrackerTargetCategory(); -} +/** This and the following mouse variables are for handling in-game click-and-hold actions */ +PlayerActionType LastPlayerAction = PlayerActionType::None; -std::optional FindNearestGroundItemId(Point playerPosition) -{ - std::optional bestId; - int bestDistance = 0; - - for (int y = 0; y < MAXDUNY; ++y) { - for (int x = 0; x < MAXDUNX; ++x) { - const int itemId = std::abs(dItem[x][y]) - 1; - if (itemId < 0 || itemId > MAXITEMS) - continue; - - const Item &item = Items[itemId]; - if (item.isEmpty() || item._iClass == ICLASS_NONE) - continue; - - const int distance = playerPosition.WalkingDistance(Point { x, y }); - if (!bestId || distance < bestDistance) { - bestId = itemId; - bestDistance = distance; - } - } - } - - return bestId; -} - -[[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position) -{ - return position.x + position.y * MAXDUNX; -} - -[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId) -{ - return { corpseId % MAXDUNX, corpseId / MAXDUNX }; -} - -std::optional FindNearestCorpseId(Point playerPosition) -{ - std::optional bestId; - int bestDistance = 0; - - for (int y = 0; y < MAXDUNY; ++y) { - for (int x = 0; x < MAXDUNX; ++x) { - if (dCorpse[x][y] == 0) - continue; - - const Point position { x, y }; - const int distance = playerPosition.WalkingDistance(position); - if (!bestId || distance < bestDistance) { - bestId = CorpseTrackerIdForPosition(position); - bestDistance = distance; - } - } - } - - return bestId; -} - -struct TrackerCandidate { - int id; - int distance; - StringOrView name; -}; - -[[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b) -{ - if (a.distance != b.distance) - return a.distance < b.distance; - return a.id < b.id; -} - -[[nodiscard]] std::vector CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance) -{ - std::vector result; - result.reserve(ActiveItemCount); - - const int minX = std::max(0, playerPosition.x - maxDistance); - const int minY = std::max(0, playerPosition.y - maxDistance); - const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); - const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); - - std::array seen {}; - - for (int y = minY; y <= maxY; ++y) { - for (int x = minX; x <= maxX; ++x) { - const int itemId = std::abs(dItem[x][y]) - 1; - if (itemId < 0 || itemId > MAXITEMS) - continue; - if (seen[itemId]) - continue; - seen[itemId] = true; - - const Item &item = Items[itemId]; - if (item.isEmpty() || item._iClass == ICLASS_NONE) - continue; - - const int distance = playerPosition.WalkingDistance(Point { x, y }); - if (distance > maxDistance) - continue; - - result.push_back(TrackerCandidate { - .id = itemId, - .distance = distance, - .name = item.getName(), - }); - } - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} - -[[nodiscard]] std::vector CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance) -{ - std::vector result; - - const int minX = std::max(0, playerPosition.x - maxDistance); - const int minY = std::max(0, playerPosition.y - maxDistance); - const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); - const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); - - for (int y = minY; y <= maxY; ++y) { - for (int x = minX; x <= maxX; ++x) { - if (dCorpse[x][y] == 0) - continue; - - const Point position { x, y }; - const int distance = playerPosition.WalkingDistance(position); - if (distance > maxDistance) - continue; - - result.push_back(TrackerCandidate { - .id = CorpseTrackerIdForPosition(position), - .distance = distance, - .name = _("Dead body"), - }); - } - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} - -[[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object) -{ - return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST); -} - -[[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object) -{ - // Track both closed and open doors (to match proximity audio cues). - return object.isDoor() && object.canInteractWith(); -} - -[[nodiscard]] constexpr bool IsShrineLikeObject(const Object &object) -{ - return object.canInteractWith() - && (object.IsShrine() - || IsAnyOf(object._otype, _object_id::OBJ_BLOODFTN, _object_id::OBJ_PURIFYINGFTN, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON, - _object_id::OBJ_MURKYFTN, _object_id::OBJ_TEARFTN)); -} - -[[nodiscard]] constexpr bool IsTrackedBreakableObject(const Object &object) -{ - return object.IsBreakable(); -} - -[[nodiscard]] constexpr bool IsTrackedMiscInteractableObject(const Object &object) -{ - if (!object.canInteractWith()) - return false; - if (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST) - return false; - if (object.isDoor()) - return false; - if (IsShrineLikeObject(object)) - return false; - if (object.IsBreakable()) - return false; - return true; -} - -[[nodiscard]] bool IsTrackedMonster(const Monster &monster) -{ - return !monster.isInvalid - && (monster.flags & MFLAG_HIDDEN) == 0 - && monster.hitPoints > 0; -} - -template -[[nodiscard]] std::vector CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate) -{ - std::vector result; - result.reserve(ActiveObjectCount); - - const int minX = std::max(0, playerPosition.x - maxDistance); - const int minY = std::max(0, playerPosition.y - maxDistance); - const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); - const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); - - std::array bestDistanceById {}; - bestDistanceById.fill(std::numeric_limits::max()); - - for (int y = minY; y <= maxY; ++y) { - for (int x = minX; x <= maxX; ++x) { - const int objectId = std::abs(dObject[x][y]) - 1; - if (objectId < 0 || objectId >= MAXOBJECTS) - continue; - - const Object &object = Objects[objectId]; - if (object._otype == OBJ_NULL) - continue; - if (!predicate(object)) - continue; - - const int distance = playerPosition.WalkingDistance(Point { x, y }); - if (distance > maxDistance) - continue; - - int &bestDistance = bestDistanceById[objectId]; - if (distance < bestDistance) - bestDistance = distance; - } - } - - for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { - const int distance = bestDistanceById[objectId]; - if (distance == std::numeric_limits::max()) - continue; - - const Object &object = Objects[objectId]; - result.push_back(TrackerCandidate { - .id = objectId, - .distance = distance, - .name = object.name(), - }); - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} - -template -[[nodiscard]] std::optional FindNearestObjectId(Point playerPosition, Predicate predicate) -{ - std::array bestDistanceById {}; - bestDistanceById.fill(std::numeric_limits::max()); - - for (int y = 0; y < MAXDUNY; ++y) { - for (int x = 0; x < MAXDUNX; ++x) { - const int objectId = std::abs(dObject[x][y]) - 1; - if (objectId < 0 || objectId >= MAXOBJECTS) - continue; - - const Object &object = Objects[objectId]; - if (object._otype == OBJ_NULL) - continue; - if (!predicate(object)) - continue; - - const int distance = playerPosition.WalkingDistance(Point { x, y }); - int &bestDistance = bestDistanceById[objectId]; - if (distance < bestDistance) - bestDistance = distance; - } - } - - std::optional bestId; - int bestDistance = 0; - for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { - const int distance = bestDistanceById[objectId]; - if (distance == std::numeric_limits::max()) - continue; - - if (!bestId || distance < bestDistance) { - bestId = objectId; - bestDistance = distance; - } - } - - return bestId; -} - -[[nodiscard]] std::vector CollectNearbyChestTrackerCandidates(Point playerPosition, int maxDistance) -{ - return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedChestObject); -} - -[[nodiscard]] std::vector CollectNearbyDoorTrackerCandidates(Point playerPosition, int maxDistance) -{ - return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedDoorObject); -} - -[[nodiscard]] std::vector CollectNearbyShrineTrackerCandidates(Point playerPosition, int maxDistance) -{ - return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsShrineLikeObject); -} - -[[nodiscard]] std::vector CollectNearbyBreakableTrackerCandidates(Point playerPosition, int maxDistance) -{ - return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedBreakableObject); -} - -[[nodiscard]] std::vector CollectNearbyObjectInteractableTrackerCandidates(Point playerPosition, int maxDistance) -{ - return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedMiscInteractableObject); -} - -[[nodiscard]] std::vector CollectNearbyMonsterTrackerCandidates(Point playerPosition, int maxDistance) -{ - std::vector result; - result.reserve(ActiveMonsterCount); - - for (size_t i = 0; i < ActiveMonsterCount; ++i) { - const int monsterId = static_cast(ActiveMonsters[i]); - const Monster &monster = Monsters[monsterId]; - - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (monster.hitPoints <= 0) - continue; - - const Point monsterDistancePosition { monster.position.future }; - const int distance = playerPosition.ApproxDistance(monsterDistancePosition); - if (distance > maxDistance) - continue; - - result.push_back(TrackerCandidate { - .id = monsterId, - .distance = distance, - .name = monster.name(), - }); - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} +// Controller support: Actions to run after updating the cursor state. +// Defined in Source/controls/plrctrls.cpp. +extern void plrctrls_after_check_curs_move(); +extern void plrctrls_every_frame(); +extern void plrctrls_after_game_logic(); -[[nodiscard]] std::vector CollectNpcTrackerCandidates(Point playerPosition) -{ - std::vector result; - if (leveltype != DTYPE_TOWN) - return result; +namespace { - result.reserve(GetNumTowners()); - for (size_t i = 0; i < GetNumTowners(); ++i) { - const Towner &towner = Towners[i]; - if (!IsTownerPresent(towner._ttype)) - continue; +char gszVersionNumber[64] = "internal version unknown"; - const int distance = playerPosition.WalkingDistance(towner.position); - result.push_back(TrackerCandidate { - .id = static_cast(i), - .distance = distance, - .name = towner.name, - }); - } +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; - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { - if (a.distance != b.distance) - return a.distance < b.distance; - return a.name.str() < b.name.str(); - }); - return result; +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; } -[[nodiscard]] std::vector CollectPlayerTrackerCandidates(Point playerPosition) +void FreeGame() { - std::vector result; - if (!gbIsMultiplayer || MyPlayer == nullptr) - return result; - - result.reserve(MAX_PLRS); - - const uint8_t currentLevel = MyPlayer->plrlevel; - const bool currentIsSetLevel = setlevel; - - for (int i = 0; i < MAX_PLRS; ++i) { - if (i == MyPlayerId) - continue; - const Player &player = Players[i]; - if (!player.plractive) - continue; - if (player._pLvlChanging) - continue; - if (player.plrlevel != currentLevel) - continue; - if (player.plrIsOnSetLevel != currentIsSetLevel) - continue; - - const Point otherPosition = player.position.future; - if (!InDungeonBounds(otherPosition)) - continue; - - const int distance = playerPosition.WalkingDistance(otherPosition); - result.push_back(TrackerCandidate { - .id = i, - .distance = distance, - .name = player.name(), - }); - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; + FreeMonsterHealthBar(); + FreeXPBar(); + FreeControlPan(); + FreeInvGFX(); + FreeGMenu(); + FreeQuestText(); + FreeInfoBoxGfx(); + FreeStoreMem(); + + for (Player &player : Players) + ResetPlayerGFX(player); + + FreeCursor(); +#ifdef _DEBUG + FreeDebugGFX(); +#endif + FreeGameMem(); + stream_stop(); + music_stop(); } -[[nodiscard]] std::vector CollectDungeonEntranceTrackerCandidates(Point playerPosition) +bool ProcessInput() { - std::vector result; - if (MyPlayer == nullptr) - return result; - - if (leveltype == DTYPE_TOWN) { - const std::vector candidates = CollectTownDungeonTriggerIndices(); - result.reserve(candidates.size()); - - for (const int triggerIndex : candidates) { - if (triggerIndex < 0 || triggerIndex >= numtrigs) - continue; - const TriggerStruct &trigger = trigs[triggerIndex]; - const Point triggerPosition { trigger.position.x, trigger.position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - result.push_back(TrackerCandidate { - .id = triggerIndex, - .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); - } - - // Preserve the trigger ordering from CollectTownDungeonTriggerIndices() for stable cycling. - return result; + if (PauseMode == 2) { + return false; } - // In a dungeon, treat this category as the exit from the current area: - // - On quest levels: return trigger back to the main level. - // - On normal levels: stairs up to the previous level (or to town on level 1). - for (int i = 0; i < numtrigs; ++i) { - const TriggerStruct &trigger = trigs[i]; - if (setlevel) { - if (trigger._tmsg != WM_DIABRTNLVL) - continue; - } else { - if (trigger._tmsg != WM_DIABPREVLVL) - continue; - } + plrctrls_every_frame(); - const Point triggerPosition { trigger.position.x, trigger.position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - result.push_back(TrackerCandidate { - .id = i, - .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); + if (!gbIsMultiplayer && gmenu_is_active()) { + RedrawViewport(); + return false; } - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} - -[[nodiscard]] std::optional FindTownPortalPositionInTownByPortalIndex(int portalIndex) -{ - if (portalIndex < 0 || portalIndex >= MAXPORTAL) - return std::nullopt; - - for (const Missile &missile : Missiles) { - if (missile._mitype != MissileID::TownPortal) - continue; - if (missile._misource != portalIndex) - continue; - return missile.position.tile; + if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { +#ifdef __vita__ + FinishSimulatedMouseClicks(MousePosition); +#endif + CheckCursMove(); + plrctrls_after_check_curs_move(); + RepeatPlayerAction(); } - return std::nullopt; + return true; } -[[nodiscard]] bool IsTownPortalOpenOnCurrentLevel(int portalIndex) +void LeftMouseCmd(bool bShift) { - if (portalIndex < 0 || portalIndex >= MAXPORTAL) - return false; - const Portal &portal = Portals[portalIndex]; - if (!portal.open) - return false; - if (portal.setlvl != setlevel) - return false; - if (portal.level != currlevel) - return false; - if (portal.ltype != leveltype) - return false; - return InDungeonBounds(portal.position); -} + bool bNear; -[[nodiscard]] std::vector CollectPortalTrackerCandidates(Point playerPosition) -{ - std::vector result; - if (MyPlayer == nullptr) - return result; + assert(!GetMainPanel().contains(MousePosition)); if (leveltype == DTYPE_TOWN) { - std::array seen {}; - for (const Missile &missile : Missiles) { - if (missile._mitype != MissileID::TownPortal) - continue; - const int portalIndex = missile._misource; - if (portalIndex < 0 || portalIndex >= MAXPORTAL) - continue; - if (seen[portalIndex]) - continue; - seen[portalIndex] = true; - - const Point portalPosition = missile.position.tile; - const int distance = playerPosition.WalkingDistance(portalPosition); - result.push_back(TrackerCandidate { - .id = portalIndex, - .distance = distance, - .name = TownPortalLabelForSpeech(Portals[portalIndex]), - }); + CloseGoldWithdraw(); + CloseStash(); + 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); } - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; + return; } - for (int i = 0; i < MAXPORTAL; ++i) { - if (!IsTownPortalOpenOnCurrentLevel(i)) - continue; - const Portal &portal = Portals[i]; - const int distance = playerPosition.WalkingDistance(portal.position); - result.push_back(TrackerCandidate { - .id = i, - .distance = distance, - .name = TownPortalLabelForSpeech(portal), - }); + 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()); + } } - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} - -[[nodiscard]] std::vector CollectStairsTrackerCandidates(Point playerPosition) -{ - std::vector result; - if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) - return result; - - for (int i = 0; i < numtrigs; ++i) { - const TriggerStruct &trigger = trigs[i]; - if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL)) - continue; - - const Point triggerPosition { trigger.position.x, trigger.position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - result.push_back(TrackerCandidate { - .id = i, - .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); + if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { + LastPlayerAction = PlayerActionType::Walk; + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; } -[[nodiscard]] std::vector CollectQuestLocationTrackerCandidates(Point playerPosition) +bool TryOpenDungeonWithMouse() { - std::vector result; - if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) - return result; - - if (setlevel) { - for (int i = 0; i < numtrigs; ++i) { - const TriggerStruct &trigger = trigs[i]; - if (trigger._tmsg != WM_DIABRTNLVL) - continue; - - const Point triggerPosition { trigger.position.x, trigger.position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - result.push_back(TrackerCandidate { - .id = i, - .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); - } - - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; - } - - constexpr size_t NumQuests = sizeof(Quests) / sizeof(Quests[0]); - result.reserve(NumQuests); - for (size_t questIndex = 0; questIndex < NumQuests; ++questIndex) { - const Quest &quest = Quests[questIndex]; - if (quest._qslvl == SL_NONE) - continue; - if (quest._qactive == QUEST_NOTAVAIL) - continue; - if (quest._qlevel != currlevel) - continue; - if (!InDungeonBounds(quest.position)) - continue; - - const char *questLevelName = QuestLevelNames[quest._qslvl]; - if (questLevelName == nullptr || questLevelName[0] == '\0') - questLevelName = N_("Set level"); - - const int distance = playerPosition.WalkingDistance(quest.position); - result.push_back(TrackerCandidate { - .id = static_cast(questIndex), - .distance = distance, - .name = _(questLevelName), - }); - } + if (leveltype != DTYPE_TOWN) + return false; - std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); - return result; -} + 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; -[[nodiscard]] std::optional FindNextTrackerCandidateId(const std::vector &candidates, int currentId) -{ - if (candidates.empty()) - return std::nullopt; - if (currentId < 0) - return candidates.front().id; - - const auto it = std::find_if(candidates.begin(), candidates.end(), [currentId](const TrackerCandidate &c) { return c.id == currentId; }); - if (it == candidates.end()) - return candidates.front().id; - - if (candidates.size() <= 1) - return std::nullopt; - - const size_t idx = static_cast(it - candidates.begin()); - const size_t nextIdx = (idx + 1) % candidates.size(); - return candidates[nextIdx].id; + NewCursor(CURSOR_HAND); + return true; } -[[nodiscard]] std::optional FindPreviousTrackerCandidateId(const std::vector &candidates, int currentId) +void LeftMouseDown(uint16_t modState) { - if (candidates.empty()) - return std::nullopt; - if (currentId < 0) - return candidates.back().id; + LastPlayerAction = PlayerActionType::None; - const auto it = std::find_if(candidates.begin(), candidates.end(), [currentId](const TrackerCandidate &c) { return c.id == currentId; }); - if (it == candidates.end()) - return candidates.back().id; + if (gmenu_left_mouse(true)) + return; - if (candidates.size() <= 1) - return std::nullopt; + if (CheckMuteButton()) + return; - const size_t idx = static_cast(it - candidates.begin()); - const size_t prevIdx = (idx + candidates.size() - 1) % candidates.size(); - return candidates[prevIdx].id; -} + if (sgnTimeoutCurs != CURSOR_NONE) + return; -void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &targetName, const std::vector &candidates) -{ - if (targetName.empty()) + if (MyPlayerIsDead) { + CheckMainPanelButtonDead(); return; - - const std::string_view baseName = targetName.str(); - int total = 0; - for (const TrackerCandidate &c : candidates) { - if (c.name.str() == baseName) - ++total; - } - if (total <= 1) - return; - - int ordinal = 0; - int seen = 0; - for (const TrackerCandidate &c : candidates) { - if (c.name.str() != baseName) - continue; - ++seen; - if (c.id == targetId) { - ordinal = seen; - break; - } - } - if (ordinal <= 0) - return; - - std::string decorated; - StrAppend(decorated, baseName, " ", ordinal); - targetName = std::move(decorated); -} + } -[[nodiscard]] std::vector CollectTrackerCandidatesForSelection(TrackerTargetCategory category, Point playerPosition) -{ - switch (category) { - case TrackerTargetCategory::Items: - return CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Chests: - return CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Doors: { - std::vector candidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - for (TrackerCandidate &c : candidates) { - if (c.id < 0 || c.id >= MAXOBJECTS) - continue; - c.name = DoorLabelForSpeech(Objects[c.id]); - } - return candidates; + if (PauseMode == 2) { + return; } - case TrackerTargetCategory::Shrines: - return CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Objects: - return CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Breakables: - return CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Monsters: - return CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::DeadBodies: - return CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - case TrackerTargetCategory::Npcs: - return CollectNpcTrackerCandidates(playerPosition); - case TrackerTargetCategory::Players: - return CollectPlayerTrackerCandidates(playerPosition); - case TrackerTargetCategory::DungeonEntrances: - return CollectDungeonEntranceTrackerCandidates(playerPosition); - case TrackerTargetCategory::Stairs: - return CollectStairsTrackerCandidates(playerPosition); - case TrackerTargetCategory::QuestLocations: - return CollectQuestLocationTrackerCandidates(playerPosition); - case TrackerTargetCategory::Portals: - return CollectPortalTrackerCandidates(playerPosition); - default: - return {}; + if (DoomFlag) { + doom_close(); + return; } -} -[[nodiscard]] std::string_view TrackerCategoryNoCandidatesFoundMessage(TrackerTargetCategory category) -{ - switch (category) { - case TrackerTargetCategory::Items: - return _("No items found."); - case TrackerTargetCategory::Chests: - return _("No chests found."); - case TrackerTargetCategory::Doors: - return _("No doors found."); - case TrackerTargetCategory::Shrines: - return _("No shrines found."); - case TrackerTargetCategory::Objects: - return _("No objects found."); - case TrackerTargetCategory::Breakables: - return _("No breakables found."); - case TrackerTargetCategory::Monsters: - return _("No monsters found."); - case TrackerTargetCategory::DeadBodies: - return _("No dead bodies found."); - case TrackerTargetCategory::Npcs: - return _("No NPCs found."); - case TrackerTargetCategory::Players: - return _("No players found."); - case TrackerTargetCategory::DungeonEntrances: - if (leveltype != DTYPE_TOWN) - return _("No exits found."); - return _("No dungeon entrances found."); - case TrackerTargetCategory::Stairs: - return _("No stairs found."); - case TrackerTargetCategory::QuestLocations: - return _("No quest locations found."); - case TrackerTargetCategory::Portals: - return _("No portals found."); - default: - return _("No targets found."); + if (SpellSelectFlag) { + SetSpell(); + return; } -} -[[nodiscard]] std::string_view TrackerCategoryNoNextMessage(TrackerTargetCategory category) -{ - switch (category) { - case TrackerTargetCategory::Items: - return _("No next item."); - case TrackerTargetCategory::Chests: - return _("No next chest."); - case TrackerTargetCategory::Doors: - return _("No next door."); - case TrackerTargetCategory::Shrines: - return _("No next shrine."); - case TrackerTargetCategory::Objects: - return _("No next object."); - case TrackerTargetCategory::Breakables: - return _("No next breakable."); - case TrackerTargetCategory::Monsters: - return _("No next monster."); - case TrackerTargetCategory::DeadBodies: - return _("No next dead body."); - case TrackerTargetCategory::Npcs: - return _("No next NPC."); - case TrackerTargetCategory::Players: - return _("No next player."); - case TrackerTargetCategory::DungeonEntrances: - return _("No next dungeon entrance."); - case TrackerTargetCategory::Stairs: - return _("No next stairs."); - case TrackerTargetCategory::QuestLocations: - return _("No next quest location."); - case TrackerTargetCategory::Portals: - return _("No next portal."); - default: - return _("No next target."); + 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 (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); } } -[[nodiscard]] std::string_view TrackerCategoryNoPreviousMessage(TrackerTargetCategory category) +void LeftMouseUp(uint16_t modState) { - switch (category) { - case TrackerTargetCategory::Items: - return _("No previous item."); - case TrackerTargetCategory::Chests: - return _("No previous chest."); - case TrackerTargetCategory::Doors: - return _("No previous door."); - case TrackerTargetCategory::Shrines: - return _("No previous shrine."); - case TrackerTargetCategory::Objects: - return _("No previous object."); - case TrackerTargetCategory::Breakables: - return _("No previous breakable."); - case TrackerTargetCategory::Monsters: - return _("No previous monster."); - case TrackerTargetCategory::DeadBodies: - return _("No previous dead body."); - case TrackerTargetCategory::Npcs: - return _("No previous NPC."); - case TrackerTargetCategory::Players: - return _("No previous player."); - case TrackerTargetCategory::DungeonEntrances: - return _("No previous dungeon entrance."); - case TrackerTargetCategory::Stairs: - return _("No previous stairs."); - case TrackerTargetCategory::QuestLocations: - return _("No previous quest location."); - case TrackerTargetCategory::Portals: - return _("No previous portal."); - default: - return _("No previous target."); + gmenu_left_mouse(false); + CheckMuteButtonUp(); + if (MainPanelButtonDown) + CheckMainPanelButtonUp(); + CheckStashButtonRelease(MousePosition); + if (CharPanelButtonActive) { + const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; + ReleaseChrBtns(isShiftHeld); } + if (LevelButtonDown) + CheckLevelButtonUp(); + if (IsPlayerInStore()) + ReleaseStoreBtn(); } -void SelectTrackerTargetRelative(int delta) +void RightMouseDown(bool isShiftHeld) { - if (!CanPlayerTakeAction() || InGameMenu()) - return; - if (MyPlayer == nullptr) - return; + LastPlayerAction = PlayerActionType::None; - if (leveltype == DTYPE_TOWN - && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies, TrackerTargetCategory::Npcs, - TrackerTargetCategory::Players, TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals)) { - SpeakText(_("Not in a dungeon."), true); - return; - } - if (AutomapActive) { - SpeakText(_("Close the map first."), true); + if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { return; } - EnsureTrackerLocksMatchCurrentLevel(); - - const Point playerPosition = MyPlayer->position.future; - AutoWalkTrackerTargetId = -1; - - const std::vector candidates = CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition); - if (candidates.empty()) { - LockedTrackerTargetId(SelectedTrackerTargetCategory) = -1; - SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + if (qtextflag) { + qtextflag = false; + stream_stop(); return; } - int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); - if (candidates.size() == 1) { - lockedTargetId = candidates.front().id; - SpeakText(candidates.front().name.str(), /*force=*/true); + if (DoomFlag) { + doom_close(); return; } - const std::optional targetId = delta > 0 ? FindNextTrackerCandidateId(candidates, lockedTargetId) : FindPreviousTrackerCandidateId(candidates, lockedTargetId); - if (!targetId) { - SpeakText(delta > 0 ? TrackerCategoryNoNextMessage(SelectedTrackerTargetCategory) : TrackerCategoryNoPreviousMessage(SelectedTrackerTargetCategory), true); + if (IsPlayerInStore()) + return; + if (SpellSelectFlag) { + SetSpell(); return; } - - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == candidates.end()) { - lockedTargetId = -1; - SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + 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); } - - lockedTargetId = *targetId; - StringOrView targetName = it->name.str(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, candidates); - SpeakText(targetName.str(), /*force=*/true); } -void TrackerPageUpKeyPressed() +void ReleaseKey(SDL_Keycode vkey) { - const SDL_Keymod modState = SDL_GetModState(); - const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0; - - if (cycleCategory) { - SelectTrackerTargetCategoryRelative(-1); - if (MyPlayer != nullptr) { - const Point playerPosition = MyPlayer->position.future; - if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) - SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); - } + remap_keyboard_key(&vkey); + if (sgnTimeoutCurs != CURSOR_NONE) return; - } - - SelectTrackerTargetRelative(-1); + KeymapperRelease(vkey); } -void TrackerPageDownKeyPressed() +void ClosePanels() { - const SDL_Keymod modState = SDL_GetModState(); - const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0; - - if (cycleCategory) { - SelectTrackerTargetCategoryRelative(+1); - if (MyPlayer != nullptr) { - const Point playerPosition = MyPlayer->position.future; - if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) - SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true); + 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 }); } - return; } - - SelectTrackerTargetRelative(+1); + CloseInventory(); + CloseCharPanel(); + SpellbookFlag = false; + QuestLogIsOpen = false; } -[[nodiscard]] bool IsGroundItemPresent(int itemId) +void PressKey(SDL_Keycode vkey, uint16_t modState) { - if (itemId < 0 || itemId > MAXITEMS) - return false; - - for (uint8_t i = 0; i < ActiveItemCount; ++i) { - if (ActiveItems[i] == itemId) - return true; - } - - return false; -} - -[[nodiscard]] bool IsCorpsePresent(int corpseId) -{ - if (corpseId < 0 || corpseId >= MAXDUNX * MAXDUNY) - return false; - - const Point position = CorpsePositionForTrackerId(corpseId); - return InDungeonBounds(position) && dCorpse[position.x][position.y] != 0; -} - -std::optional FindNearestUnopenedChestObjectId(Point playerPosition) -{ - return FindNearestObjectId(playerPosition, IsTrackedChestObject); -} - -std::optional FindNearestDoorObjectId(Point playerPosition) -{ - return FindNearestObjectId(playerPosition, IsTrackedDoorObject); -} - -std::optional FindNearestShrineObjectId(Point playerPosition) -{ - return FindNearestObjectId(playerPosition, IsShrineLikeObject); -} - -std::optional FindNearestBreakableObjectId(Point playerPosition) -{ - return FindNearestObjectId(playerPosition, IsTrackedBreakableObject); -} - -std::optional FindNearestMiscInteractableObjectId(Point playerPosition) -{ - return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject); -} - -std::optional FindNearestMonsterId(Point playerPosition) -{ - std::optional bestId; - int bestDistance = 0; - - for (size_t i = 0; i < ActiveMonsterCount; ++i) { - const int monsterId = static_cast(ActiveMonsters[i]); - const Monster &monster = Monsters[monsterId]; - - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (monster.hitPoints <= 0) - continue; - - const Point monsterDistancePosition { monster.position.future }; - const int distance = playerPosition.ApproxDistance(monsterDistancePosition); - if (!bestId || distance < bestDistance) { - bestId = monsterId; - bestDistance = distance; - } - } - - return bestId; -} - -std::optional FindBestAdjacentApproachTile(const Player &player, Point playerPosition, Point targetPosition) -{ - std::optional best; - size_t bestPathLength = 0; - int bestDistance = 0; - - std::optional bestFallback; - int bestFallbackDistance = 0; - - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - - const Point tile { targetPosition.x + dx, targetPosition.y + dy }; - if (!PosOkPlayer(player, tile)) - continue; - - const int distance = playerPosition.WalkingDistance(tile); - - if (!bestFallback || distance < bestFallbackDistance) { - bestFallback = tile; - bestFallbackDistance = distance; - } - - const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); - if (!path) - continue; - - const size_t pathLength = path->size(); - if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { - best = tile; - bestPathLength = pathLength; - bestDistance = distance; - } - } - } - - if (best) - return best; - - return bestFallback; -} - -bool PosOkPlayerIgnoreDoors(const Player &player, Point position) -{ - if (!InDungeonBounds(position)) - return false; - if (!IsTileWalkable(position, /*ignoreDoors=*/true)) - return false; - - Player *otherPlayer = PlayerAtPosition(position); - if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) - return false; - - if (dMonster[position.x][position.y] != 0) { - if (leveltype == DTYPE_TOWN) - return false; - if (dMonster[position.x][position.y] <= 0) - return false; - if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife()) - return false; - } - - return true; -} - -[[nodiscard]] bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables) -{ - Object *object = FindObjectAtPosition(position); - if (object != nullptr) { - if (ignoreDoors && object->isDoor()) { - return true; - } - if (ignoreBreakables && object->_oSolidFlag && object->IsBreakable()) { - return true; - } - if (object->_oSolidFlag) { - return false; - } - } - - return IsTileNotSolid(position); -} - -bool PosOkPlayerIgnoreMonsters(const Player &player, Point position) -{ - if (!InDungeonBounds(position)) - return false; - if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/false, /*ignoreBreakables=*/false)) - return false; - - Player *otherPlayer = PlayerAtPosition(position); - if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) - return false; - - return true; -} - -bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position) -{ - if (!InDungeonBounds(position)) - return false; - if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/false)) - return false; - - Player *otherPlayer = PlayerAtPosition(position); - if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) - return false; - - return true; -} - -bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position) -{ - if (!InDungeonBounds(position)) - return false; - if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/true)) - return false; - - Player *otherPlayer = PlayerAtPosition(position); - if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) - return false; - - return true; -} - -std::optional FindBestApproachTileForObject(const Player &player, Point playerPosition, const Object &object) -{ - // Some interactable objects are placed on a walkable tile (e.g. floor switches). Prefer stepping on the tile in that case. - if (!object._oSolidFlag && PosOkPlayer(player, object.position)) - return object.position; - - std::optional best; - size_t bestPathLength = 0; - int bestDistance = 0; - - std::optional bestFallback; - int bestFallbackDistance = 0; - - const auto considerTile = [&](Point tile) { - if (!PosOkPlayerIgnoreDoors(player, tile)) - return; - - const int distance = playerPosition.WalkingDistance(tile); - if (!bestFallback || distance < bestFallbackDistance) { - bestFallback = tile; - bestFallbackDistance = distance; - } - - const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); - if (!path) - return; - - const size_t pathLength = path->size(); - if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { - best = tile; - bestPathLength = pathLength; - bestDistance = distance; - } - }; - - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - considerTile(object.position + Displacement { dx, dy }); - } - } - - if (FindObjectAtPosition(object.position + Direction::NorthEast) == &object) { - // Special case for large objects (e.g. sarcophagi): allow approaching from one tile further to the north. - for (int dx = -1; dx <= 1; ++dx) { - considerTile(object.position + Displacement { dx, -2 }); - } - } - - if (best) - return best; - - return bestFallback; -} - -struct DoorBlockInfo { - Point beforeDoor; - Point doorPosition; -}; - -std::optional FindFirstClosedDoorOnWalkPath(Point startPosition, const int8_t *path, int steps) -{ - Point position = startPosition; - for (int i = 0; i < steps; ++i) { - const Point next = NextPositionForWalkDirection(position, path[i]); - Object *object = FindObjectAtPosition(next); - // Only closed doors block the path; blocked doors (DOOR_BLOCKED) are physically passable. - if (object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) { - return DoorBlockInfo { .beforeDoor = position, .doorPosition = object->position }; - } - position = next; - } - return std::nullopt; -} - -enum class TrackerPathBlockType : uint8_t { - Door, - Monster, - Breakable, -}; - -struct TrackerPathBlockInfo { - TrackerPathBlockType type; - size_t stepIndex; - Point beforeBlock; - Point blockPosition; -}; - -[[nodiscard]] std::optional FindFirstTrackerPathBlock(Point startPosition, const int8_t *path, size_t steps, bool considerDoors, bool considerMonsters, bool considerBreakables, Point targetPosition) -{ - Point position = startPosition; - for (size_t i = 0; i < steps; ++i) { - const Point next = NextPositionForWalkDirection(position, path[i]); - if (next == targetPosition) { - position = next; - continue; - } - - Object *object = FindObjectAtPosition(next); - // Only closed doors block the path; blocked doors (DOOR_BLOCKED) are physically passable. - if (considerDoors && object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) { - return TrackerPathBlockInfo { - .type = TrackerPathBlockType::Door, - .stepIndex = i, - .beforeBlock = position, - .blockPosition = object->position, - }; - } - if (considerBreakables && object != nullptr && object->_oSolidFlag && object->IsBreakable()) { - return TrackerPathBlockInfo { - .type = TrackerPathBlockType::Breakable, - .stepIndex = i, - .beforeBlock = position, - .blockPosition = next, - }; - } - - if (considerMonsters && leveltype != DTYPE_TOWN && dMonster[next.x][next.y] != 0) { - const int monsterRef = dMonster[next.x][next.y]; - const int monsterId = std::abs(monsterRef) - 1; - const bool blocks = monsterRef <= 0 || (monsterId >= 0 && monsterId < static_cast(MaxMonsters) && !Monsters[monsterId].hasNoLife()); - if (blocks) { - return TrackerPathBlockInfo { - .type = TrackerPathBlockType::Monster, - .stepIndex = i, - .beforeBlock = position, - .blockPosition = next, - }; - } - } - - position = next; - } - - return std::nullopt; -} - -void NavigateToTrackerTargetKeyPressed() -{ - if (!CanPlayerTakeAction() || InGameMenu()) - return; - if (leveltype == DTYPE_TOWN - && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies, TrackerTargetCategory::Npcs, - TrackerTargetCategory::Players, TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals)) { - SpeakText(_("Not in a dungeon."), true); + Options &options = GetOptions(); + remap_keyboard_key(&vkey); + + if (vkey == SDLK_UNKNOWN) + return; + + if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { return; } - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (MyPlayer == nullptr) - return; - - EnsureTrackerLocksMatchCurrentLevel(); - - const SDL_Keymod modState = SDL_GetModState(); - const bool cycleTarget = (modState & SDL_KMOD_SHIFT) != 0; - const bool clearTarget = (modState & SDL_KMOD_CTRL) != 0; - - const Point playerPosition = MyPlayer->position.future; - AutoWalkTrackerTargetId = -1; - - int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); - if (clearTarget) { - lockedTargetId = -1; - SpeakText(_("Tracker target cleared."), true); - return; - } - - std::optional targetId; - std::optional targetPosition; - std::optional alternateTargetPosition; - StringOrView targetName; - - switch (SelectedTrackerTargetCategory) { - case TrackerTargetCategory::Items: { - const std::vector nearbyCandidates = CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No items found."), true); - else - SpeakText(_("No next item."), true); - return; - } - } else if (IsGroundItemPresent(lockedTargetId)) { - targetId = lockedTargetId; - } else { - targetId = FindNearestGroundItemId(playerPosition); - } - if (!targetId) { - SpeakText(_("No items found."), true); - return; - } - - if (!IsGroundItemPresent(*targetId)) { - lockedTargetId = -1; - SpeakText(_("No items found."), true); - return; - } - - lockedTargetId = *targetId; - const Item &tracked = Items[*targetId]; - - targetName = tracked.getName(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - targetPosition = tracked.position; - break; - } - case TrackerTargetCategory::Chests: { - const std::vector nearbyCandidates = CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No chests found."), true); - else - SpeakText(_("No next chest."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = FindNearestUnopenedChestObjectId(playerPosition); - } - if (!targetId) { - SpeakText(_("No chests found."), true); - return; - } - - const Object &object = Objects[*targetId]; - if (!IsTrackedChestObject(object)) { - lockedTargetId = -1; - targetId = FindNearestUnopenedChestObjectId(playerPosition); - if (!targetId) { - SpeakText(_("No chests found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Object &tracked = Objects[*targetId]; - - targetName = tracked.name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position; - if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) - alternateTargetPosition = tracked.position + Direction::NorthEast; - } - break; - } - case TrackerTargetCategory::Doors: { - std::vector nearbyCandidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - for (TrackerCandidate &c : nearbyCandidates) { - if (c.id < 0 || c.id >= MAXOBJECTS) - continue; - c.name = DoorLabelForSpeech(Objects[c.id]); - } - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No doors found."), true); - else - SpeakText(_("No next door."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = FindNearestDoorObjectId(playerPosition); - } - if (!targetId) { - SpeakText(_("No doors found."), true); - return; - } - - const Object &object = Objects[*targetId]; - if (!IsTrackedDoorObject(object)) { - lockedTargetId = -1; - targetId = FindNearestDoorObjectId(playerPosition); - if (!targetId) { - SpeakText(_("No doors found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Object &tracked = Objects[*targetId]; - - targetName = DoorLabelForSpeech(tracked); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position; - if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) - alternateTargetPosition = tracked.position + Direction::NorthEast; - } - break; - } - case TrackerTargetCategory::Shrines: { - const std::vector nearbyCandidates = CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No shrines found."), true); - else - SpeakText(_("No next shrine."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = FindNearestShrineObjectId(playerPosition); - } - if (!targetId) { - SpeakText(_("No shrines found."), true); - return; - } - - const Object &object = Objects[*targetId]; - if (!IsShrineLikeObject(object)) { - lockedTargetId = -1; - targetId = FindNearestShrineObjectId(playerPosition); - if (!targetId) { - SpeakText(_("No shrines found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Object &tracked = Objects[*targetId]; - - targetName = tracked.name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position; - if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) - alternateTargetPosition = tracked.position + Direction::NorthEast; - } - break; - } - case TrackerTargetCategory::Objects: { - const std::vector nearbyCandidates = CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No objects found."), true); - else - SpeakText(_("No next object."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = FindNearestMiscInteractableObjectId(playerPosition); - } - if (!targetId) { - SpeakText(_("No objects found."), true); - return; - } - - const Object &object = Objects[*targetId]; - if (!IsTrackedMiscInteractableObject(object)) { - lockedTargetId = -1; - targetId = FindNearestMiscInteractableObjectId(playerPosition); - if (!targetId) { - SpeakText(_("No objects found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Object &tracked = Objects[*targetId]; - - targetName = tracked.name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position; - if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) - alternateTargetPosition = tracked.position + Direction::NorthEast; - } - break; - } - case TrackerTargetCategory::Breakables: { - const std::vector nearbyCandidates = CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No breakables found."), true); - else - SpeakText(_("No next breakable."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = FindNearestBreakableObjectId(playerPosition); - } - if (!targetId) { - SpeakText(_("No breakables found."), true); - return; - } - - const Object &object = Objects[*targetId]; - if (!IsTrackedBreakableObject(object)) { - lockedTargetId = -1; - targetId = FindNearestBreakableObjectId(playerPosition); - if (!targetId) { - SpeakText(_("No breakables found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Object &tracked = Objects[*targetId]; - - targetName = tracked.name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position; - if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) - alternateTargetPosition = tracked.position + Direction::NorthEast; - } - break; - } - case TrackerTargetCategory::Monsters: { - const std::vector nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No monsters found."), true); - else - SpeakText(_("No next monster."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { - targetId = lockedTargetId; - } else { - targetId = FindNearestMonsterId(playerPosition); - } - if (!targetId) { - SpeakText(_("No monsters found."), true); - return; - } - - const Monster &monster = Monsters[*targetId]; - if (!IsTrackedMonster(monster)) { - lockedTargetId = -1; - targetId = FindNearestMonsterId(playerPosition); - if (!targetId) { - SpeakText(_("No monsters found."), true); - return; - } - } - - lockedTargetId = *targetId; - const Monster &tracked = Monsters[*targetId]; - - targetName = tracked.name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = tracked.position.tile; - } - break; - } - case TrackerTargetCategory::DeadBodies: { - const std::vector nearbyCandidates = CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No dead bodies found."), true); - else - SpeakText(_("No next dead body."), true); - return; - } - } else if (IsCorpsePresent(lockedTargetId)) { - targetId = lockedTargetId; - } else { - targetId = FindNearestCorpseId(playerPosition); - } - if (!targetId) { - SpeakText(_("No dead bodies found."), true); - return; - } - - if (!IsCorpsePresent(*targetId)) { - lockedTargetId = -1; - SpeakText(_("No dead bodies found."), true); - return; - } - - lockedTargetId = *targetId; - targetName = _("Dead body"); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = CorpsePositionForTrackerId(*targetId); - } - break; - } - case TrackerTargetCategory::Npcs: { - const std::vector nearbyCandidates = CollectNpcTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No NPCs found."), true); + + if (MyPlayerIsDead) { + if (vkey == SDLK_ESCAPE) { + if (!gbIsMultiplayer) { + if (gbValidSaveFile) + gamemenu_load_game(false); else - SpeakText(_("No next NPC."), true); - return; + gamemenu_exit_game(false); + } else { + NetSendCmd(true, CMD_RETOWN); } - } else if (lockedTargetId >= 0 && lockedTargetId < static_cast(GetNumTowners())) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No NPCs found."), true); return; } - - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No NPCs found."), true); + if (sgnTimeoutCurs != CURSOR_NONE) { return; } - - lockedTargetId = *targetId; - targetName = Towners[*targetId].name; - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = Towners[*targetId].position; - } - break; - } - case TrackerTargetCategory::Players: { - const std::vector nearbyCandidates = CollectPlayerTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No players found."), true); - else - SpeakText(_("No next player."), true); - 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(); } - } else if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No players found."), true); - return; } - - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No players found."), true); + if (vkey != SDLK_ESCAPE) { return; } - - lockedTargetId = *targetId; - targetName = Players[*targetId].name(); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = Players[*targetId].position.future; - } - break; } - case TrackerTargetCategory::DungeonEntrances: { - const std::vector nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No dungeon entrances found."), true); - else - SpeakText(_("No next dungeon entrance."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No dungeon entrances found."), true); - 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; + } - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No dungeon entrances found."), true); - return; - } + if (DropGoldFlag) { + control_drop_gold(vkey); + return; + } + if (IsWithdrawGoldOpen) { + WithdrawGoldKeyPress(vkey); + return; + } - lockedTargetId = *targetId; - targetName = TriggerLabelForSpeech(trigs[*targetId]); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - const TriggerStruct &trigger = trigs[*targetId]; - targetPosition = Point { trigger.position.x, trigger.position.y }; - } - break; + if (sgnTimeoutCurs != CURSOR_NONE) { + return; } - case TrackerTargetCategory::Stairs: { - const std::vector nearbyCandidates = CollectStairsTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No stairs found."), true); - else - SpeakText(_("No next stairs."), true); - return; - } - } else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No stairs found."), true); - return; - } - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No stairs found."), true); - return; - } + KeymapperPress(vkey); - lockedTargetId = *targetId; - targetName = std::string(it->name.str()); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - const TriggerStruct &trigger = trigs[*targetId]; - targetPosition = Point { trigger.position.x, trigger.position.y }; + 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(); } - break; + return; } - case TrackerTargetCategory::QuestLocations: { - const std::vector nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No quest locations found."), true); - else - SpeakText(_("No next quest location."), true); - return; - } - } else if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast(sizeof(Quests) / sizeof(Quests[0])))) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No quest locations found."), true); - return; - } - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No quest locations found."), true); - return; - } + if (DoomFlag) { + doom_close(); + return; + } - lockedTargetId = *targetId; - targetName = std::string(it->name.str()); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - if (setlevel) { - const TriggerStruct &trigger = trigs[*targetId]; - targetPosition = Point { trigger.position.x, trigger.position.y }; + 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 (CharFlag) { + CharacterScreenActivateSelection((modState & SDL_KMOD_SHIFT) != 0); + } else if (IsPlayerInStore()) { + StoreEnter(); + } else if (QuestLogIsOpen) { + QuestlogEnter(); + } else if (SpellSelectFlag) { + SetSpell(); + } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { + const Player &player = *MyPlayer; + if (IsValidSpell(player._pRSpell)) { + std::string msg; + StrAppend(msg, _("Selected: "), pgettext("spell", GetSpellData(player._pRSpell).sNameText)); + SpeakText(msg, /*force=*/true); } else { - const Quest &quest = Quests[static_cast(*targetId)]; - targetPosition = quest.position; + SpeakText(_("No spell selected."), /*force=*/true); } + SpellBookKeyPressed(); + } else { + TypeChatMessage(); } - break; - } - case TrackerTargetCategory::Portals: { - const std::vector nearbyCandidates = CollectPortalTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); - if (!targetId) { - if (nearbyCandidates.empty()) - SpeakText(_("No portals found."), true); - else - SpeakText(_("No next portal."), true); - return; + return; + case SDLK_UP: + if (IsPlayerInStore()) { + StoreUp(); + } else if (QuestLogIsOpen) { + QuestlogUp(); + } else if (CharFlag) { + CharacterScreenMoveSelection(-1); + } else if (HelpFlag) { + HelpScrollUp(); + } else if (ChatLogFlag) { + ChatLogScrollUp(); + } else if (SpellSelectFlag) { + HotSpellMove({ AxisDirectionX_NONE, AxisDirectionY_UP }); + SpeakSelectedSpeedbookSpell(); + } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { + const std::optional next = GetSpellBookAdjacentAvailableSpell(SpellbookTab, *MyPlayer, MyPlayer->_pRSpell, -1); + if (next) { + MyPlayer->_pRSpell = *next; + MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill + : (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges + : SpellType::Spell; + UpdateSpellTarget(*next); + RedrawEverything(); + SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); } - } else if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; + } else if (invflag) { + InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_UP }); + } else if (AutomapActive) { + AutomapUp(); + } else if (IsStashOpen) { + Stash.PreviousPage(); } - if (!targetId) { - SpeakText(_("No portals found."), true); - return; + return; + case SDLK_DOWN: + if (IsPlayerInStore()) { + StoreDown(); + } else if (QuestLogIsOpen) { + QuestlogDown(); + } else if (CharFlag) { + CharacterScreenMoveSelection(+1); + } else if (HelpFlag) { + HelpScrollDown(); + } else if (ChatLogFlag) { + ChatLogScrollDown(); + } else if (SpellSelectFlag) { + HotSpellMove({ AxisDirectionX_NONE, AxisDirectionY_DOWN }); + SpeakSelectedSpeedbookSpell(); + } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { + const std::optional next = GetSpellBookAdjacentAvailableSpell(SpellbookTab, *MyPlayer, MyPlayer->_pRSpell, +1); + if (next) { + MyPlayer->_pRSpell = *next; + MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill + : (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges + : SpellType::Spell; + UpdateSpellTarget(*next); + RedrawEverything(); + SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); + } + } else if (invflag) { + InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_DOWN }); + } else if (AutomapActive) { + AutomapDown(); + } else if (IsStashOpen) { + Stash.NextPage(); } - - const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); - if (it == nearbyCandidates.end()) { - lockedTargetId = -1; - SpeakText(_("No portals found."), true); - return; + return; + case SDLK_PAGEUP: + if (IsPlayerInStore()) { + StorePrior(); + } else if (ChatLogFlag) { + ChatLogScrollTop(); + } else { + const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(vkey)); + if (action == nullptr || !action->isEnabled()) + SelectPreviousTownNpcKeyPressed(); } - - Point portalPosition; - if (leveltype == DTYPE_TOWN) { - const std::optional townPos = FindTownPortalPositionInTownByPortalIndex(*targetId); - if (!townPos) { - lockedTargetId = -1; - SpeakText(_("No portals found."), true); - return; - } - portalPosition = *townPos; + return; + case SDLK_PAGEDOWN: + if (IsPlayerInStore()) { + StoreNext(); + } else if (ChatLogFlag) { + ChatLogScrollBottom(); } else { - if (!IsTownPortalOpenOnCurrentLevel(*targetId)) { - lockedTargetId = -1; - SpeakText(_("No portals found."), true); - return; - } - portalPosition = Portals[*targetId].position; + const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(vkey)); + if (action == nullptr || !action->isEnabled()) + SelectNextTownNpcKeyPressed(); } - - lockedTargetId = *targetId; - targetName = TownPortalLabelForSpeech(Portals[*targetId]); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - targetPosition = portalPosition; + return; + case SDLK_LEFT: + if (CharFlag) { + CharacterScreenMoveSelection(-1); + } else if (SpellSelectFlag) { + HotSpellMove({ AxisDirectionX_LEFT, AxisDirectionY_NONE }); + SpeakSelectedSpeedbookSpell(); + } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { + if (SpellbookTab > 0) { + SpellbookTab--; + const std::optional first = GetSpellBookFirstAvailableSpell(SpellbookTab, *MyPlayer); + if (first) { + MyPlayer->_pRSpell = *first; + MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill + : (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges + : SpellType::Spell; + UpdateSpellTarget(*first); + RedrawEverything(); + SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); + } + } + } else if (invflag) { + InventoryMoveFromKeyboard({ AxisDirectionX_LEFT, AxisDirectionY_NONE }); + } else if (AutomapActive && !ChatFlag) { + AutomapLeft(); + } + return; + case SDLK_RIGHT: + if (CharFlag) { + CharacterScreenMoveSelection(+1); + } else if (SpellSelectFlag) { + HotSpellMove({ AxisDirectionX_RIGHT, AxisDirectionY_NONE }); + SpeakSelectedSpeedbookSpell(); + } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { + const int maxTab = gbIsHellfire ? 4 : 3; + if (SpellbookTab < maxTab) { + SpellbookTab++; + const std::optional first = GetSpellBookFirstAvailableSpell(SpellbookTab, *MyPlayer); + if (first) { + MyPlayer->_pRSpell = *first; + MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill + : (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges + : SpellType::Spell; + UpdateSpellTarget(*first); + RedrawEverything(); + SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); + } + } + } else if (invflag) { + InventoryMoveFromKeyboard({ AxisDirectionX_RIGHT, AxisDirectionY_NONE }); + } else 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; } - - if (cycleTarget) { - SpeakText(targetName.str(), /*force=*/true); - return; - } - - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } - - Point chosenTargetPosition = *targetPosition; - enum class TrackerPathMode : uint8_t { - RespectDoorsAndMonsters, - IgnoreDoors, - IgnoreMonsters, - IgnoreDoorsAndMonsters, - Lenient, - }; - - auto findPathToTarget = [&](Point destination, TrackerPathMode mode) -> std::optional> { - const bool allowDestinationNonWalkable = !PosOkPlayer(*MyPlayer, destination); - switch (mode) { - case TrackerPathMode::RespectDoorsAndMonsters: - return FindKeyboardWalkPathForSpeechRespectingDoors(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); - case TrackerPathMode::IgnoreDoors: - return FindKeyboardWalkPathForSpeech(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); - case TrackerPathMode::IgnoreMonsters: - return FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); - case TrackerPathMode::IgnoreDoorsAndMonsters: - return FindKeyboardWalkPathForSpeechIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); - case TrackerPathMode::Lenient: - return FindKeyboardWalkPathForSpeechLenient(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); - default: - return std::nullopt; - } - }; - - std::optional> spokenPath; - bool pathIgnoresDoors = false; - bool pathIgnoresMonsters = false; - bool pathIgnoresBreakables = false; - - const auto considerDestination = [&](Point destination, TrackerPathMode mode) { - const std::optional> candidate = findPathToTarget(destination, mode); - if (!candidate) - return; - if (!spokenPath || candidate->size() < spokenPath->size()) { - spokenPath = *candidate; - chosenTargetPosition = destination; - - pathIgnoresDoors = mode == TrackerPathMode::IgnoreDoors || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; - pathIgnoresMonsters = mode == TrackerPathMode::IgnoreMonsters || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; - pathIgnoresBreakables = mode == TrackerPathMode::Lenient; - } - }; - - considerDestination(*targetPosition, TrackerPathMode::RespectDoorsAndMonsters); - if (alternateTargetPosition) - considerDestination(*alternateTargetPosition, TrackerPathMode::RespectDoorsAndMonsters); - - if (!spokenPath) { - considerDestination(*targetPosition, TrackerPathMode::IgnoreDoors); - if (alternateTargetPosition) - considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoors); - } - - if (!spokenPath) { - considerDestination(*targetPosition, TrackerPathMode::IgnoreMonsters); - if (alternateTargetPosition) - considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreMonsters); - } - - if (!spokenPath) { - considerDestination(*targetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); - if (alternateTargetPosition) - considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); - } - - if (!spokenPath) { - considerDestination(*targetPosition, TrackerPathMode::Lenient); - if (alternateTargetPosition) - considerDestination(*alternateTargetPosition, TrackerPathMode::Lenient); - } - - bool showUnreachableWarning = false; - if (!spokenPath) { - showUnreachableWarning = true; - Point closestPosition; - spokenPath = FindKeyboardWalkPathToClosestReachableForSpeech(*MyPlayer, playerPosition, chosenTargetPosition, closestPosition); - pathIgnoresDoors = true; - pathIgnoresMonsters = false; - pathIgnoresBreakables = false; - } - - if (spokenPath && !showUnreachableWarning && !PosOkPlayer(*MyPlayer, chosenTargetPosition)) { - if (!spokenPath->empty()) - spokenPath->pop_back(); - } - - if (spokenPath && (pathIgnoresDoors || pathIgnoresMonsters || pathIgnoresBreakables)) { - const std::optional block = FindFirstTrackerPathBlock(playerPosition, spokenPath->data(), spokenPath->size(), pathIgnoresDoors, pathIgnoresMonsters, pathIgnoresBreakables, chosenTargetPosition); - if (block) { - if (playerPosition.WalkingDistance(block->blockPosition) <= TrackerInteractDistanceTiles) { - switch (block->type) { - case TrackerPathBlockType::Door: - SpeakText(_("A door is blocking the path. Open it and try again."), true); - return; - case TrackerPathBlockType::Monster: - SpeakText(_("A monster is blocking the path. Clear it and try again."), true); - return; - case TrackerPathBlockType::Breakable: - SpeakText(_("A breakable object is blocking the path. Destroy it and try again."), true); - return; - } - } - - spokenPath = std::vector(spokenPath->begin(), spokenPath->begin() + block->stepIndex); - } - } - - std::string message; - const bool speakPath = spokenPath && (!showUnreachableWarning || !spokenPath->empty()); - const bool suppressTrivialPath = spokenPath && spokenPath->empty() && !showUnreachableWarning && !targetName.empty(); - if (!targetName.empty()) { - if (showUnreachableWarning || (speakPath && !suppressTrivialPath)) - StrAppend(message, targetName, "\n"); - else - StrAppend(message, targetName); - } - if (showUnreachableWarning) { - message.append(_("Can't find a path to the target.")); - if (spokenPath && !spokenPath->empty()) - message.append("\n"); - } - if (speakPath && !suppressTrivialPath) - AppendKeyboardWalkPathForSpeech(message, *spokenPath); - SpeakText(message, true); + 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; + } } - -} // namespace - -/** - * Validates an object-category auto-walk target and computes the walk destination. - * - * Checks bounds, applies the validity predicate, tests interaction distance, and - * computes the approach tile. On failure the walk is cancelled and a spoken - * message is emitted. - * - * @param[out] destination Set to the approach tile when the target is valid. - * @return true if the walk should continue (destination is set), false if cancelled. - */ -template -bool ValidateAutoWalkObjectTarget( - const Player &myPlayer, Point playerPosition, - Predicate isValid, const char *goneMessage, const char *inRangeMessage, - std::optional &destination) -{ - const int objectId = AutoWalkTrackerTargetId; - if (objectId < 0 || objectId >= MAXOBJECTS) { - AutoWalkTrackerTargetId = -1; - SpeakText(_(goneMessage), true); - return false; - } - const Object &object = Objects[objectId]; - if (!isValid(object)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_(goneMessage), true); - return false; - } - if (playerPosition.WalkingDistance(object.position) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_(inRangeMessage), true); - return false; - } - destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, object.position); - return true; -} - -/** - * Resolves which object to walk toward for the given tracker category. - * - * Uses the locked target if it is still valid, otherwise falls back to the - * nearest matching object. On success, updates lockedTargetId and targetName. - * - * @return The resolved object ID, or nullopt if nothing was found (a spoken - * message will already have been emitted). - */ -template -std::optional ResolveObjectTrackerTarget( - int &lockedTargetId, Point playerPosition, - Predicate isValid, FindNearest findNearest, GetName getName, - const char *notFoundMessage, StringOrView &targetName) -{ - std::optional targetId; - if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { - targetId = lockedTargetId; - } else { - targetId = findNearest(playerPosition); - } - if (!targetId) { - SpeakText(_(notFoundMessage), true); - return std::nullopt; - } - if (!isValid(Objects[*targetId])) { - lockedTargetId = -1; - targetId = findNearest(playerPosition); - if (!targetId) { - SpeakText(_(notFoundMessage), true); - return std::nullopt; - } - // findNearest guarantees the result passes isValid, but verify defensively. - if (!isValid(Objects[*targetId])) { - SpeakText(_(notFoundMessage), true); - return std::nullopt; - } - } - lockedTargetId = *targetId; - targetName = getName(*targetId); - return targetId; + +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)); + } } -void TrackerHomeKeyPressed() +[[maybe_unused]] void LogUnhandledEvent(const char *name, int value) { - const SDL_Keymod modState = SDL_GetModState(); - const bool autoWalk = (modState & SDL_KMOD_SHIFT) != 0; + LogVerbose("Unhandled SDL event: {} {}", name, value); +} - if (autoWalk) - AutoWalkToTrackerTargetKeyPressed(); - else - NavigateToTrackerTargetKeyPressed(); +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(); + } } -/** - * Called each game tick to advance auto-walk toward the current tracker target. - * Does nothing if no target is active (AutoWalkTrackerTargetId < 0) or if the - * player is not idle. Validates the target still exists and is reachable, then - * computes a path. If a closed door blocks the path, reroutes to the tile - * before the door. Long paths are sent in segments. - */ -void UpdateAutoWalkTracker() +void GameEventHandler(const SDL_Event &event, uint16_t modState) { - if (AutoWalkTrackerTargetId < 0) - return; - if (IsPlayerInStore() || ChatLogFlag || HelpFlag || InGameMenu()) { - AutoWalkTrackerTargetId = -1; + [[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; } - if (leveltype == DTYPE_TOWN - && IsNoneOf(AutoWalkTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies, TrackerTargetCategory::Npcs, - TrackerTargetCategory::Players, TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals)) { - AutoWalkTrackerTargetId = -1; + +#ifdef _DEBUG + if (ConsoleHandleEvent(event)) { return; } - if (!CanPlayerTakeAction()) +#endif + + if (IsChatActive() && HandleTalkTextInputEvent(event)) { return; - - if (MyPlayer == nullptr) { - SpeakText(_("Cannot walk right now."), true); - return; - } - if (MyPlayer->_pmode != PM_STAND) - return; - if (MyPlayer->walkpath[0] != WALK_NONE) - return; - if (MyPlayer->destAction != ACTION_NONE) - return; - - Player &myPlayer = *MyPlayer; - const Point playerPosition = myPlayer.position.future; - - std::optional destination; - - switch (AutoWalkTrackerTargetCategory) { - case TrackerTargetCategory::Items: { - const int itemId = AutoWalkTrackerTargetId; - if (itemId < 0 || itemId > MAXITEMS) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target item is gone."), true); - return; - } - if (!IsGroundItemPresent(itemId)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target item is gone."), true); - return; - } - const Item &item = Items[itemId]; - if (playerPosition.WalkingDistance(item.position) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Item in range."), true); - return; - } - destination = item.position; - break; - } - case TrackerTargetCategory::Chests: - if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, - IsTrackedChestObject, N_("Target chest is gone."), N_("Chest in range."), destination)) - return; - break; - case TrackerTargetCategory::Doors: - if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedDoorObject, N_("Target door is gone."), N_("Door in range."), destination)) - return; - break; - case TrackerTargetCategory::Shrines: - if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsShrineLikeObject, N_("Target shrine is gone."), N_("Shrine in range."), destination)) - return; - break; - case TrackerTargetCategory::Objects: - if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedMiscInteractableObject, N_("Target object is gone."), N_("Object in range."), destination)) - return; - break; - case TrackerTargetCategory::Breakables: - if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedBreakableObject, N_("Target breakable is gone."), N_("Breakable in range."), destination)) - return; - break; - case TrackerTargetCategory::Monsters: { - const int monsterId = AutoWalkTrackerTargetId; - if (monsterId < 0 || monsterId >= static_cast(MaxMonsters)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target monster is gone."), true); - return; - } - const Monster &monster = Monsters[monsterId]; - if (!IsTrackedMonster(monster)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target monster is gone."), true); - return; - } - const Point monsterPosition { monster.position.tile }; - if (playerPosition.WalkingDistance(monsterPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Monster in range."), true); - return; - } - destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, monsterPosition); - break; - } - case TrackerTargetCategory::DeadBodies: { - const int corpseId = AutoWalkTrackerTargetId; - if (!IsCorpsePresent(corpseId)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target dead body is gone."), true); - return; - } - - const Point corpsePosition = CorpsePositionForTrackerId(corpseId); - if (playerPosition.WalkingDistance(corpsePosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Dead body in range."), true); - return; - } - - destination = corpsePosition; - break; } - case TrackerTargetCategory::Npcs: { - const int npcId = AutoWalkTrackerTargetId; - if (leveltype != DTYPE_TOWN || npcId < 0 || npcId >= static_cast(GetNumTowners())) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target NPC is gone."), true); - return; - } - const Towner &towner = Towners[npcId]; - if (!IsTownerPresent(towner._ttype)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target NPC is gone."), true); - return; - } - if (playerPosition.WalkingDistance(towner.position) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("NPC in range."), true); - return; - } - destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, towner.position); - break; + if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { + return; } - case TrackerTargetCategory::Players: { - const int playerId = AutoWalkTrackerTargetId; - if (playerId < 0 || playerId >= MAX_PLRS) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target player is gone."), true); - return; - } - const Player &player = Players[playerId]; - if (!player.plractive || player._pLvlChanging || player.plrIsOnSetLevel != setlevel || player.plrlevel != MyPlayer->plrlevel) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target player is gone."), true); - return; - } - const Point targetPosition = player.position.future; - if (!InDungeonBounds(targetPosition)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target player is gone."), true); - return; - } - if (playerPosition.WalkingDistance(targetPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Player in range."), true); - return; - } - destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, targetPosition); - break; + if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { + return; } - case TrackerTargetCategory::DungeonEntrances: { - const int triggerIndex = AutoWalkTrackerTargetId; - if (triggerIndex < 0 || triggerIndex >= numtrigs) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target entrance is gone."), true); - return; - } - const TriggerStruct &trigger = trigs[triggerIndex]; - const bool valid = leveltype == DTYPE_TOWN - ? IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP) - : (setlevel ? trigger._tmsg == WM_DIABRTNLVL : trigger._tmsg == WM_DIABPREVLVL); - if (!valid) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target entrance is gone."), true); - return; - } - const Point triggerPosition { trigger.position.x, trigger.position.y }; - if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Entrance in range."), true); - 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); } - destination = triggerPosition; break; - } - case TrackerTargetCategory::Stairs: { - const int triggerIndex = AutoWalkTrackerTargetId; - if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target stairs are gone."), true); - return; - } - const TriggerStruct &trigger = trigs[triggerIndex]; - if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target stairs are gone."), true); - return; - } - const Point triggerPosition { trigger.position.x, trigger.position.y }; - if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Stairs in range."), true); +#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; } - destination = triggerPosition; + MainWndProc(event); break; } - case TrackerTargetCategory::QuestLocations: { - if (setlevel) { - const int triggerIndex = AutoWalkTrackerTargetId; - if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target quest location is gone."), true); - return; - } - const TriggerStruct &trigger = trigs[triggerIndex]; - if (trigger._tmsg != WM_DIABRTNLVL) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target quest location is gone."), true); - return; - } - const Point triggerPosition { trigger.position.x, trigger.position.y }; - if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Quest exit in range."), true); - return; +} + +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(); + LuaEvent("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); } - destination = triggerPosition; - break; + DebugCmdsFromCommandLine.clear(); } +#endif - const int questIndex = AutoWalkTrackerTargetId; - if (questIndex < 0 || questIndex >= static_cast(sizeof(Quests) / sizeof(Quests[0]))) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target quest location is gone."), true); - return; - } - const Quest &quest = Quests[static_cast(questIndex)]; - if (quest._qslvl == SL_NONE || quest._qactive == QUEST_NOTAVAIL || quest._qlevel != currlevel || !InDungeonBounds(quest.position)) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target quest location is gone."), true); - return; + 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 (playerPosition.WalkingDistance(quest.position) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Quest entrance in range."), true); - return; + 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; } - destination = quest.position; - break; + + 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 } - case TrackerTargetCategory::Portals: { - const int portalIndex = AutoWalkTrackerTargetId; - std::optional portalPosition; - if (leveltype == DTYPE_TOWN) { - portalPosition = FindTownPortalPositionInTownByPortalIndex(portalIndex); - } else if (IsTownPortalOpenOnCurrentLevel(portalIndex)) { - portalPosition = Portals[portalIndex].position; - } - if (!portalPosition) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Target portal is gone."), true); - return; - } - if (playerPosition.WalkingDistance(*portalPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Portal in range."), true); - return; - } - destination = *portalPosition; - break; + 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(); } - - if (!destination) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } - - constexpr size_t MaxAutoWalkPathLength = 512; - std::array path; - path.fill(WALK_NONE); - - int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); - // If no direct path exists, try pathfinding that treats closed doors as walkable. - // If that finds a path, identify the first closed door along it and re-route the - // player to the tile just before that door, so they can open it and retry. - if (steps == 0) { - std::array ignoreDoorPath; - ignoreDoorPath.fill(WALK_NONE); - - const int ignoreDoorSteps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayerIgnoreDoors(myPlayer, position); }, playerPosition, *destination, ignoreDoorPath.data(), ignoreDoorPath.size()); - if (ignoreDoorSteps != 0) { - const std::optional block = FindFirstClosedDoorOnWalkPath(playerPosition, ignoreDoorPath.data(), ignoreDoorSteps); - if (block) { - if (playerPosition.WalkingDistance(block->doorPosition) <= TrackerInteractDistanceTiles) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("A door is blocking the path. Open it and try again."), true); - return; - } - - *destination = block->beforeDoor; - path.fill(WALK_NONE); - steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); - } - } - - if (steps == 0) { - AutoWalkTrackerTargetId = -1; - SpeakText(_("Can't find a path to the target."), true); - return; - } - } - - // FindPath returns 0 if the path length is equal to the maximum. - // The player walkpath buffer is MaxPathLengthPlayer, so keep segments strictly shorter. - if (steps < static_cast(MaxPathLengthPlayer)) { - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, *destination); - return; - } - - const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); - const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); -} - -/** - * Initiates auto-walk toward the currently selected tracker target (Shift+Home). - * Resolves the target from the locked tracker target or the nearest of the - * selected category. Sets AutoWalkTrackerTargetId/Category, then calls - * UpdateAutoWalkTracker() to begin the first walk segment. Subsequent segments - * are issued per-tick. Press Shift+Home again to cancel. - */ -void AutoWalkToTrackerTargetKeyPressed() -{ - // Cancel in-progress auto-walk (must be checked before the action guard - // so that cancellation works even if the player is mid-walk). - if (AutoWalkTrackerTargetId >= 0) { - CancelAutoWalk(); - SpeakText(_("Walk cancelled."), true); - return; - } - - // Defense-in-depth: keymapper canTrigger also checks these, but guard here - // in case the function is called from another code path. - if (!CanPlayerTakeAction() || InGameMenu()) - return; - - if (leveltype == DTYPE_TOWN - && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies, TrackerTargetCategory::Npcs, - TrackerTargetCategory::Players, TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals)) { - SpeakText(_("Not in a dungeon."), true); +} + +void PrintWithRightPadding(std::string_view str, size_t width) +{ + printInConsole(str); + if (str.size() >= width) return; - } - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (MyPlayer == nullptr) { - SpeakText(_("Cannot walk right now."), true); - return; - } - - EnsureTrackerLocksMatchCurrentLevel(); - - const Point playerPosition = MyPlayer->position.future; - int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); - - std::optional targetId; - StringOrView targetName; - - switch (SelectedTrackerTargetCategory) { - case TrackerTargetCategory::Items: { - if (IsGroundItemPresent(lockedTargetId)) { - targetId = lockedTargetId; - } else { - targetId = FindNearestGroundItemId(playerPosition); - } - if (!targetId) { - SpeakText(_("No items found."), true); - return; - } - if (!IsGroundItemPresent(*targetId)) { - lockedTargetId = -1; - SpeakText(_("No items found."), true); - return; - } - lockedTargetId = *targetId; - targetName = Items[*targetId].getName(); - break; - } - case TrackerTargetCategory::Chests: - targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedChestObject, FindNearestUnopenedChestObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No chests found."), targetName); - if (!targetId) - return; - break; - case TrackerTargetCategory::Doors: - targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedDoorObject, FindNearestDoorObjectId, [](int id) -> StringOrView { return DoorLabelForSpeech(Objects[id]); }, N_("No doors found."), targetName); - if (!targetId) - return; - break; - case TrackerTargetCategory::Shrines: - targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsShrineLikeObject, FindNearestShrineObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No shrines found."), targetName); - if (!targetId) - return; - break; - case TrackerTargetCategory::Objects: - targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedMiscInteractableObject, FindNearestMiscInteractableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No objects found."), targetName); - if (!targetId) - return; - break; - case TrackerTargetCategory::Breakables: - targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedBreakableObject, FindNearestBreakableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No breakables found."), targetName); - if (!targetId) - return; - break; - case TrackerTargetCategory::Monsters: { - if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { - targetId = lockedTargetId; - } else { - targetId = FindNearestMonsterId(playerPosition); - } - if (!targetId) { - SpeakText(_("No monsters found."), true); - return; - } - const Monster &monster = Monsters[*targetId]; - if (!IsTrackedMonster(monster)) { - lockedTargetId = -1; - targetId = FindNearestMonsterId(playerPosition); - if (!targetId) { - SpeakText(_("No monsters found."), true); - return; - } - } - lockedTargetId = *targetId; - targetName = Monsters[*targetId].name(); - break; - } - case TrackerTargetCategory::DeadBodies: { - if (IsCorpsePresent(lockedTargetId)) { - targetId = lockedTargetId; + 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 { - targetId = FindNearestCorpseId(playerPosition); - } - if (!targetId) { - SpeakText(_("No dead bodies found."), true); - return; - } - if (!IsCorpsePresent(*targetId)) { - lockedTargetId = -1; - SpeakText(_("No dead bodies found."), true); - return; - } - lockedTargetId = *targetId; - targetName = _("Dead body"); - break; - } - case TrackerTargetCategory::Npcs: { - const std::vector candidates = CollectNpcTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No NPCs found."), true); - return; + printInConsole("unrecognized option '"); + printInConsole(argv[i]); + printInConsole("'"); + printNewlineInConsole(); + PrintHelpAndExit(); } + } - if (lockedTargetId >= 0 && lockedTargetId < static_cast(GetNumTowners())) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; +#ifdef _DEBUG + if (!currentCommand.empty()) + DebugCmdsFromCommandLine.push_back(currentCommand); +#endif - lockedTargetId = *targetId; - targetName = Towners[*targetId].name; - break; - } - case TrackerTargetCategory::Players: { - const std::vector candidates = CollectPlayerTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No players found."), true); - return; - } +#ifndef DISABLE_DEMOMODE + if (demoNumber != -1) + demo::InitPlayBack(demoNumber, timedemo); + if (recordNumber != -1) + demo::InitRecording(recordNumber, createDemoReference); +#endif +} - if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; +void DiabloInitScreen() +{ + MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; + if (ControlMode == ControlTypes::KeyboardAndMouse) + SetCursorPos(MousePosition); - lockedTargetId = *targetId; - targetName = Players[*targetId].name(); - break; - } - case TrackerTargetCategory::DungeonEntrances: { - const std::vector candidates = CollectDungeonEntranceTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No dungeon entrances found."), true); - return; - } + ClrDiabloMsg(); +} - if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; +void SetApplicationVersions() +{ + *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; + *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; +} - lockedTargetId = *targetId; - targetName = TriggerLabelForSpeech(trigs[*targetId]); - break; +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")); } - case TrackerTargetCategory::Stairs: { - const std::vector candidates = CollectStairsTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No stairs found."), true); - return; - } +} - if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; +void ApplicationInit() +{ + if (*GetOptions().Graphics.showFPS) + EnableFrameCount(); - lockedTargetId = *targetId; - targetName = TriggerLabelForSpeech(trigs[*targetId]); - break; - } - case TrackerTargetCategory::QuestLocations: { - const std::vector candidates = CollectQuestLocationTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No quest locations found."), true); - return; - } + init_create_window(); + was_window_init = true; - if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast(sizeof(Quests) / sizeof(Quests[0])))) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; + InitializeScreenReader(); + LanguageInitialize(); - lockedTargetId = *targetId; - targetName = std::string(candidates.front().name.str()); - if (const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); it != candidates.end()) - targetName = std::string(it->name.str()); - break; + 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); } - case TrackerTargetCategory::Portals: { - const std::vector candidates = CollectPortalTrackerCandidates(playerPosition); - if (candidates.empty()) { - SpeakText(_("No portals found."), true); - return; - } - if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) { - const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); - if (it != candidates.end()) - targetId = lockedTargetId; - } - if (!targetId) - targetId = candidates.front().id; + gbIsHellfireSaveGame = gbIsHellfire; - lockedTargetId = *targetId; - targetName = TownPortalLabelForSpeech(Portals[*targetId]); - break; + 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()); + } } - - std::string msg; - StrAppend(msg, _("Going to: "), targetName); - SpeakText(msg, true); - - AutoWalkTrackerTargetId = *targetId; - AutoWalkTrackerTargetCategory = SelectedTrackerTargetCategory; - UpdateAutoWalkTracker(); -} - -void ListTownNpcsKeyPressed() -{ - if (leveltype != DTYPE_TOWN) { - ResetTownNpcSelection(); - SpeakText(_("Not in town."), true); - return; - } - if (IsPlayerInStore()) - return; - - std::vector townNpcs; - std::vector cows; - - townNpcs.reserve(Towners.size()); - cows.reserve(Towners.size()); - - const Point playerPosition = MyPlayer->position.future; - - for (const Towner &towner : Towners) { - if (!IsTownerPresent(towner._ttype)) - continue; - - if (towner._ttype == TOWN_COW) { - cows.push_back(&towner); - continue; - } - - townNpcs.push_back(&towner); - } - - if (townNpcs.empty() && cows.empty()) { - ResetTownNpcSelection(); - SpeakText(_("No town NPCs found."), true); - return; - } - - std::sort(townNpcs.begin(), townNpcs.end(), [&playerPosition](const Towner *a, const Towner *b) { - const int distanceA = playerPosition.WalkingDistance(a->position); - const int distanceB = playerPosition.WalkingDistance(b->position); - if (distanceA != distanceB) - return distanceA < distanceB; - return a->name < b->name; - }); - - std::string output; - StrAppend(output, _("Town NPCs:")); - for (size_t i = 0; i < townNpcs.size(); ++i) { - StrAppend(output, "\n", i + 1, ". ", townNpcs[i]->name); - } - if (!cows.empty()) { - StrAppend(output, "\n", _("Cows: "), static_cast(cows.size())); - } - - RefreshTownNpcOrder(true); - if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast(GetNumTowners())) { - const Towner &towner = Towners[SelectedTownNpc]; - StrAppend(output, "\n", _("Selected: "), towner.name); - StrAppend(output, "\n", _("PageUp/PageDown: select. Home: go. End: repeat.")); - } - const std::string_view exitKey = GetOptions().Keymapper.KeyNameForAction("SpeakNearestExit"); - if (!exitKey.empty()) { - StrAppend(output, "\n", fmt::format(fmt::runtime(_("Cathedral entrance: press {:s}.")), exitKey)); - } - - SpeakText(output, true); -} - -namespace { - -using PosOkForSpeechFn = bool (*)(const Player &, Point); - -template -std::optional> FindKeyboardWalkPathForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, bool allowDestinationNonWalkable) -{ - if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) - return std::nullopt; - - if (startPosition == destinationPosition) - return std::vector {}; - - std::array visited {}; - std::array parentDir {}; - parentDir.fill(WALK_NONE); - - std::queue queue; - - const auto indexOf = [](Point position) -> size_t { - return static_cast(position.x) + static_cast(position.y) * MAXDUNX; - }; - - const auto enqueue = [&](Point current, int8_t dir) { - const Point next = NextPositionForWalkDirection(current, dir); - if (!InDungeonBounds(next)) - return; - - const size_t idx = indexOf(next); - if (visited[idx]) - return; - - const bool ok = posOk(player, next); - if (ok) { - if (!CanStep(current, next)) - return; - } else { - if (!allowDestinationNonWalkable || next != destinationPosition) - return; - } - - visited[idx] = true; - parentDir[idx] = dir; - queue.push(next); - }; - - visited[indexOf(startPosition)] = true; - queue.push(startPosition); - - const auto hasReachedDestination = [&]() -> bool { - return visited[indexOf(destinationPosition)]; - }; - - while (!queue.empty() && !hasReachedDestination()) { - const Point current = queue.front(); - queue.pop(); - - const Displacement delta = destinationPosition - current; - const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX; - const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY; - - std::array prioritizedDirs; - size_t prioritizedCount = 0; - - const auto addUniqueDir = [&](int8_t dir) { - if (dir == WALK_NONE) - return; - for (size_t i = 0; i < prioritizedCount; ++i) { - if (prioritizedDirs[i] == dir) - return; - } - prioritizedDirs[prioritizedCount++] = dir; - }; - - const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE); - const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE); - - if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) { - const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N); - addUniqueDir(diagDir); - } - - if (deltaAbsX >= deltaAbsY) { - addUniqueDir(xDir); - addUniqueDir(yDir); - } else { - addUniqueDir(yDir); - addUniqueDir(xDir); - } - for (const int8_t dir : walkDirections) { - addUniqueDir(dir); - } - - for (size_t i = 0; i < prioritizedCount; ++i) { - enqueue(current, prioritizedDirs[i]); - } - } - - if (!hasReachedDestination()) - return std::nullopt; - - std::vector path; - Point position = destinationPosition; - while (position != startPosition) { - const int8_t dir = parentDir[indexOf(position)]; - if (dir == WALK_NONE) - return std::nullopt; - - path.push_back(dir); - position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir)); - } - - std::reverse(path.begin(), path.end()); - return path; -} - -std::optional> FindKeyboardWalkPathForSpeechWithPosOk(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, bool allowDestinationNonWalkable) -{ - constexpr std::array AxisDirections = { - WALK_NE, - WALK_SW, - WALK_SE, - WALK_NW, - }; - - constexpr std::array AllDirections = { - WALK_NE, - WALK_SW, - WALK_SE, - WALK_NW, - WALK_N, - WALK_E, - WALK_S, - WALK_W, - }; - - if (const std::optional> axisPath = FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AxisDirections, /*allowDiagonalSteps=*/false, allowDestinationNonWalkable); axisPath) { - return axisPath; - } - - return FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AllDirections, /*allowDiagonalSteps=*/true, allowDestinationNonWalkable); -} - -} // namespace - -std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) -{ - return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, allowDestinationNonWalkable); -} - -std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) -{ - return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayer, allowDestinationNonWalkable); -} - -std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) -{ - return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsAndMonsters, allowDestinationNonWalkable); -} - -std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) -{ - return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreMonsters, allowDestinationNonWalkable); -} - -std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) -{ - return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsMonstersAndBreakables, allowDestinationNonWalkable); -} - -namespace { - -template -std::optional> FindKeyboardWalkPathToClosestReachableForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, Point &closestPosition) -{ - if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) - return std::nullopt; - - if (startPosition == destinationPosition) { - closestPosition = destinationPosition; - return std::vector {}; - } - - std::array visited {}; - std::array parentDir {}; - std::array depth {}; - parentDir.fill(WALK_NONE); - depth.fill(0); - - std::queue queue; - - const auto indexOf = [](Point position) -> size_t { - return static_cast(position.x) + static_cast(position.y) * MAXDUNX; - }; - - const auto enqueue = [&](Point current, int8_t dir) { - const Point next = NextPositionForWalkDirection(current, dir); - if (!InDungeonBounds(next)) - return; - - const size_t nextIdx = indexOf(next); - if (visited[nextIdx]) - return; - - if (!posOk(player, next)) - return; - if (!CanStep(current, next)) - return; - - const size_t currentIdx = indexOf(current); - visited[nextIdx] = true; - parentDir[nextIdx] = dir; - depth[nextIdx] = static_cast(depth[currentIdx] + 1); - queue.push(next); - }; - - const size_t startIdx = indexOf(startPosition); - visited[startIdx] = true; - queue.push(startPosition); - - Point best = startPosition; - int bestDistance = startPosition.WalkingDistance(destinationPosition); - uint16_t bestDepth = 0; - - const auto considerBest = [&](Point position) { - const int distance = position.WalkingDistance(destinationPosition); - const uint16_t posDepth = depth[indexOf(position)]; - if (distance < bestDistance || (distance == bestDistance && posDepth < bestDepth)) { - best = position; - bestDistance = distance; - bestDepth = posDepth; - } - }; - - while (!queue.empty()) { - const Point current = queue.front(); - queue.pop(); - - considerBest(current); - - const Displacement delta = destinationPosition - current; - const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX; - const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY; - - std::array prioritizedDirs; - size_t prioritizedCount = 0; - - const auto addUniqueDir = [&](int8_t dir) { - if (dir == WALK_NONE) - return; - for (size_t i = 0; i < prioritizedCount; ++i) { - if (prioritizedDirs[i] == dir) - return; - } - prioritizedDirs[prioritizedCount++] = dir; - }; - - const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE); - const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE); - - if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) { - const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N); - addUniqueDir(diagDir); - } - - if (deltaAbsX >= deltaAbsY) { - addUniqueDir(xDir); - addUniqueDir(yDir); - } else { - addUniqueDir(yDir); - addUniqueDir(xDir); - } - for (const int8_t dir : walkDirections) { - addUniqueDir(dir); - } - - for (size_t i = 0; i < prioritizedCount; ++i) { - enqueue(current, prioritizedDirs[i]); - } - } - - closestPosition = best; - if (best == startPosition) - return std::vector {}; - - std::vector path; - Point position = best; - while (position != startPosition) { - const int8_t dir = parentDir[indexOf(position)]; - if (dir == WALK_NONE) - return std::nullopt; - - path.push_back(dir); - position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir)); - } - - std::reverse(path.begin(), path.end()); - return path; -} - -} // namespace - -std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition) -{ - constexpr std::array AxisDirections = { - WALK_NE, - WALK_SW, - WALK_SE, - WALK_NW, - }; - - constexpr std::array AllDirections = { - WALK_NE, - WALK_SW, - WALK_SE, - WALK_NW, - WALK_N, - WALK_E, - WALK_S, - WALK_W, - }; - - Point axisClosest; - const std::optional> axisPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AxisDirections, /*allowDiagonalSteps=*/false, axisClosest); - - Point diagClosest; - const std::optional> diagPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AllDirections, /*allowDiagonalSteps=*/true, diagClosest); - - if (!axisPath && !diagPath) - return std::nullopt; - if (!axisPath) { - closestPosition = diagClosest; - return diagPath; - } - if (!diagPath) { - closestPosition = axisClosest; - return axisPath; - } - - const int axisDistance = axisClosest.WalkingDistance(destinationPosition); - const int diagDistance = diagClosest.WalkingDistance(destinationPosition); - if (diagDistance < axisDistance) { - closestPosition = diagClosest; - return diagPath; - } - - closestPosition = axisClosest; - return axisPath; -} - -void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path) -{ - if (path.empty()) { - message.append(_("here")); - return; - } - - bool any = false; - const auto appendPart = [&](std::string_view label, int distance) { - if (distance == 0) - return; - if (any) - message.append(", "); - StrAppend(message, label, " ", distance); - any = true; - }; - - const auto labelForWalkDirection = [](int8_t dir) -> std::string_view { - switch (dir) { - case WALK_NE: - return _("north"); - case WALK_SW: - return _("south"); - case WALK_SE: - return _("east"); - case WALK_NW: - return _("west"); - case WALK_N: - return _("northwest"); - case WALK_E: - return _("northeast"); - case WALK_S: - return _("southeast"); - case WALK_W: - return _("southwest"); - default: - return {}; - } - }; - - int8_t currentDir = path.front(); - int runLength = 1; - for (size_t i = 1; i < path.size(); ++i) { - if (path[i] == currentDir) { - ++runLength; - continue; - } - - const std::string_view label = labelForWalkDirection(currentDir); - if (!label.empty()) - appendPart(label, runLength); - - currentDir = path[i]; - runLength = 1; - } - - const std::string_view label = labelForWalkDirection(currentDir); - if (!label.empty()) - appendPart(label, runLength); - - if (!any) - message.append(_("here")); -} - -void AppendDirectionalFallback(std::string &message, const Displacement &delta) -{ - bool any = false; - const auto appendPart = [&](std::string_view label, int distance) { - if (distance == 0) - return; - if (any) - message.append(", "); - StrAppend(message, label, " ", distance); - any = true; - }; - - if (delta.deltaY < 0) - appendPart(_("north"), -delta.deltaY); - else if (delta.deltaY > 0) - appendPart(_("south"), delta.deltaY); - - if (delta.deltaX > 0) - appendPart(_("east"), delta.deltaX); - else if (delta.deltaX < 0) - appendPart(_("west"), -delta.deltaX); - - if (!any) - message.append(_("here")); -} - -std::optional FindNearestUnexploredTile(Point startPosition) -{ - if (!InDungeonBounds(startPosition)) - return std::nullopt; - - std::array visited {}; - std::queue queue; - - const auto enqueue = [&](Point position) { - if (!InDungeonBounds(position)) - return; - - const size_t index = static_cast(position.x) + static_cast(position.y) * MAXDUNX; - if (visited[index]) - return; - - if (!IsTileWalkable(position, /*ignoreDoors=*/true)) - return; - - visited[index] = true; - queue.push(position); - }; - - enqueue(startPosition); - - constexpr std::array Neighbors = { - Direction::NorthEast, - Direction::SouthWest, - Direction::SouthEast, - Direction::NorthWest, - }; - - while (!queue.empty()) { - const Point position = queue.front(); - queue.pop(); - - if (!HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Explored)) - return position; - - for (const Direction dir : Neighbors) { - enqueue(position + dir); - } - } - - return std::nullopt; -} - -std::string TriggerLabelForSpeech(const TriggerStruct &trigger) -{ - switch (trigger._tmsg) { - case WM_DIABNEXTLVL: - if (leveltype == DTYPE_TOWN) - return std::string { _("Cathedral entrance") }; - return std::string { _("Stairs down") }; - case WM_DIABPREVLVL: - return std::string { _("Stairs up") }; - case WM_DIABTOWNWARP: - switch (trigger._tlvl) { - case 5: - return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Catacombs")); - case 9: - return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Caves")); - case 13: - return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Hell")); - case 17: - return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Nest")); - case 21: - return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Crypt")); - default: - return fmt::format(fmt::runtime(_("Town warp to level {:d}")), trigger._tlvl); - } - case WM_DIABTWARPUP: - return std::string { _("Warp up") }; - case WM_DIABRETOWN: - return std::string { _("Return to town") }; - case WM_DIABWARPLVL: - return std::string { _("Warp") }; - case WM_DIABSETLVL: - return std::string { _("Set level") }; - case WM_DIABRTNLVL: - return std::string { _("Return level") }; - default: - return std::string { _("Exit") }; - } -} - -std::optional LockedTownDungeonTriggerIndex; - -std::vector CollectTownDungeonTriggerIndices() -{ - std::vector result; - result.reserve(static_cast(std::max(0, numtrigs))); - - for (int i = 0; i < numtrigs; ++i) { - if (IsAnyOf(trigs[i]._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP)) - result.push_back(i); - } - - std::sort(result.begin(), result.end(), [](int a, int b) { - const TriggerStruct &ta = trigs[a]; - const TriggerStruct &tb = trigs[b]; - - const int kindA = ta._tmsg == WM_DIABNEXTLVL ? 0 : (ta._tmsg == WM_DIABTOWNWARP ? 1 : 2); - const int kindB = tb._tmsg == WM_DIABNEXTLVL ? 0 : (tb._tmsg == WM_DIABTOWNWARP ? 1 : 2); - if (kindA != kindB) - return kindA < kindB; - - if (ta._tmsg == WM_DIABTOWNWARP && tb._tmsg == WM_DIABTOWNWARP && ta._tlvl != tb._tlvl) - return ta._tlvl < tb._tlvl; - - return a < b; - }); - - return result; -} - -std::optional FindDefaultTownDungeonTriggerIndex(const std::vector &candidates) -{ - for (const int index : candidates) { - if (trigs[index]._tmsg == WM_DIABNEXTLVL) - return index; - } - if (!candidates.empty()) - return candidates.front(); - return std::nullopt; -} - -std::optional FindLockedTownDungeonTriggerIndex(const std::vector &candidates) -{ - if (!LockedTownDungeonTriggerIndex) - return std::nullopt; - if (std::find(candidates.begin(), candidates.end(), *LockedTownDungeonTriggerIndex) != candidates.end()) - return *LockedTownDungeonTriggerIndex; - return std::nullopt; -} - -std::optional FindNextTownDungeonTriggerIndex(const std::vector &candidates, int current) -{ - if (candidates.empty()) - return std::nullopt; - - const auto it = std::find(candidates.begin(), candidates.end(), current); - if (it == candidates.end()) - return candidates.front(); - if (std::next(it) == candidates.end()) - return candidates.front(); - return *std::next(it); -} - -std::optional FindPreferredExitTriggerIndex() -{ - if (numtrigs <= 0) - return std::nullopt; - - if (leveltype == DTYPE_TOWN && MyPlayer != nullptr) { - const Point playerPosition = MyPlayer->position.future; - std::optional bestIndex; - int bestDistance = 0; - - for (int i = 0; i < numtrigs; ++i) { - if (!IsAnyOf(trigs[i]._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP)) - continue; - - const Point triggerPosition { trigs[i].position.x, trigs[i].position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - if (!bestIndex || distance < bestDistance) { - bestIndex = i; - bestDistance = distance; - } - } - - if (bestIndex) - return bestIndex; - } - - const Point playerPosition = MyPlayer->position.future; - std::optional bestIndex; - int bestDistance = 0; - - for (int i = 0; i < numtrigs; ++i) { - const Point triggerPosition { trigs[i].position.x, trigs[i].position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - if (!bestIndex || distance < bestDistance) { - bestIndex = i; - bestDistance = distance; - } - } - - return bestIndex; -} - -std::optional FindNearestTriggerIndexWithMessage(int message) -{ - if (numtrigs <= 0 || MyPlayer == nullptr) - return std::nullopt; - - const Point playerPosition = MyPlayer->position.future; - std::optional bestIndex; - int bestDistance = 0; - - for (int i = 0; i < numtrigs; ++i) { - if (trigs[i]._tmsg != message) - continue; - - const Point triggerPosition { trigs[i].position.x, trigs[i].position.y }; - const int distance = playerPosition.WalkingDistance(triggerPosition); - if (!bestIndex || distance < bestDistance) { - bestIndex = i; - bestDistance = distance; - } - } - - return bestIndex; -} - -std::optional FindNearestTownPortalOnCurrentLevel() -{ - if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) - return std::nullopt; - - const Point playerPosition = MyPlayer->position.future; - const int currentLevel = setlevel ? static_cast(setlvlnum) : currlevel; - - std::optional bestPosition; - int bestDistance = 0; - - for (int i = 0; i < MAXPORTAL; ++i) { - const Portal &portal = Portals[i]; - if (!portal.open) - continue; - if (portal.setlvl != setlevel) - continue; - if (portal.level != currentLevel) - continue; - - const int distance = playerPosition.WalkingDistance(portal.position); - if (!bestPosition || distance < bestDistance) { - bestPosition = portal.position; - bestDistance = distance; - } - } - - return bestPosition; -} - -struct TownPortalInTown { - int portalIndex; - Point position; - int distance; -}; - -std::optional FindNearestTownPortalInTown() -{ - if (MyPlayer == nullptr || leveltype != DTYPE_TOWN) - return std::nullopt; - - const Point playerPosition = MyPlayer->position.future; - - std::optional best; - int bestDistance = 0; - - for (const Missile &missile : Missiles) { - if (missile._mitype != MissileID::TownPortal) - continue; - if (missile._misource < 0 || missile._misource >= MAXPORTAL) - continue; - if (!Portals[missile._misource].open) - continue; - - const Point portalPosition = missile.position.tile; - const int distance = playerPosition.WalkingDistance(portalPosition); - if (!best || distance < bestDistance) { - best = TownPortalInTown { - .portalIndex = missile._misource, - .position = portalPosition, - .distance = distance, - }; - bestDistance = distance; - } - } - - return best; -} - -[[nodiscard]] std::string TownPortalLabelForSpeech(const Portal &portal) -{ - if (portal.level <= 0) - return std::string { _("Town portal") }; - - if (portal.setlvl) { - const auto questLevel = static_cast<_setlevels>(portal.level); - const char *questLevelName = QuestLevelNames[questLevel]; - if (questLevelName == nullptr || questLevelName[0] == '\0') - return std::string { _("Town portal to set level") }; - - return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a set/quest level name. */ "Town portal to {:s}")), _(questLevelName)); - } - - constexpr std::array DungeonStrs = { - N_("Town"), - N_("Cathedral"), - N_("Catacombs"), - N_("Caves"), - N_("Hell"), - N_("Nest"), - N_("Crypt"), - }; - std::string dungeonStr; - if (portal.ltype >= DTYPE_TOWN && portal.ltype <= DTYPE_LAST) { - dungeonStr = _(DungeonStrs[static_cast(portal.ltype)]); - } else { - dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); - } - - int floor = portal.level; - if (portal.ltype == DTYPE_CATACOMBS) - floor -= 4; - else if (portal.ltype == DTYPE_CAVES) - floor -= 8; - else if (portal.ltype == DTYPE_HELL) - floor -= 12; - else if (portal.ltype == DTYPE_NEST) - floor -= 16; - else if (portal.ltype == DTYPE_CRYPT) - floor -= 20; - - if (floor > 0) - return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name and {:d} is a floor number. */ "Town portal to {:s} {:d}")), dungeonStr, floor); - - return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name. */ "Town portal to {:s}")), dungeonStr); -} - -struct QuestSetLevelEntrance { - _setlevels questLevel; - Point entrancePosition; - int distance; -}; - -std::optional FindNearestQuestSetLevelEntranceOnCurrentLevel() -{ - if (MyPlayer == nullptr || setlevel) - return std::nullopt; - - const Point playerPosition = MyPlayer->position.future; - std::optional best; - int bestDistance = 0; - - for (const Quest &quest : Quests) { - if (quest._qslvl == SL_NONE) - continue; - if (quest._qactive == QUEST_NOTAVAIL) - continue; - if (quest._qlevel != currlevel) - continue; - if (!InDungeonBounds(quest.position)) - continue; - - const int distance = playerPosition.WalkingDistance(quest.position); - if (!best || distance < bestDistance) { - best = QuestSetLevelEntrance { - .questLevel = quest._qslvl, - .entrancePosition = quest.position, - .distance = distance, - }; - bestDistance = distance; - } - } - - return best; -} - -void SpeakNearestExitKeyPressed() -{ - if (!CanPlayerTakeAction()) - return; - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (MyPlayer == nullptr) - return; - - const Point startPosition = MyPlayer->position.future; - - const SDL_Keymod modState = SDL_GetModState(); - const bool seekQuestEntrance = (modState & SDL_KMOD_SHIFT) != 0; - const bool cycleTownDungeon = (modState & SDL_KMOD_CTRL) != 0; - - if (seekQuestEntrance) { - if (setlevel) { - const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABRTNLVL); - if (!triggerIndex) { - SpeakText(_("No quest exits found."), true); - return; - } - - const TriggerStruct &trigger = trigs[*triggerIndex]; - const Point targetPosition { trigger.position.x, trigger.position.y }; - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - std::string message = TriggerLabelForSpeech(trigger); - if (!message.empty()) - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - SpeakText(message, true); - return; - } - - if (const std::optional entrance = FindNearestQuestSetLevelEntranceOnCurrentLevel(); entrance) { - const Point targetPosition = entrance->entrancePosition; - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - - std::string message { _(QuestLevelNames[entrance->questLevel]) }; - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - SpeakText(message, true); - return; - } - - SpeakText(_("No quest entrances found."), true); - return; - } - - if (leveltype == DTYPE_TOWN) { - const std::vector dungeonCandidates = CollectTownDungeonTriggerIndices(); - if (dungeonCandidates.empty()) { - SpeakText(_("No exits found."), true); - return; - } - - if (cycleTownDungeon) { - if (dungeonCandidates.size() <= 1) { - SpeakText(_("No other dungeon entrances found."), true); - return; - } - - const int current = LockedTownDungeonTriggerIndex.value_or(-1); - const std::optional next = FindNextTownDungeonTriggerIndex(dungeonCandidates, current); - if (!next) { - SpeakText(_("No other dungeon entrances found."), true); - return; - } - - LockedTownDungeonTriggerIndex = *next; - const std::string label = TriggerLabelForSpeech(trigs[*next]); - if (!label.empty()) - SpeakText(label, true); - return; - } - - const int triggerIndex = FindLockedTownDungeonTriggerIndex(dungeonCandidates) - .value_or(FindDefaultTownDungeonTriggerIndex(dungeonCandidates).value_or(dungeonCandidates.front())); - LockedTownDungeonTriggerIndex = triggerIndex; - - const TriggerStruct &trigger = trigs[triggerIndex]; - const Point targetPosition { trigger.position.x, trigger.position.y }; - - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - std::string message = TriggerLabelForSpeech(trigger); - if (!message.empty()) - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - - SpeakText(message, true); - return; - } - - if (leveltype != DTYPE_TOWN) { - if (const std::optional portalPosition = FindNearestTownPortalOnCurrentLevel(); portalPosition) { - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *portalPosition); - std::string message { _("Return to town") }; - message.append(": "); - if (!path) - AppendDirectionalFallback(message, *portalPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - SpeakText(message, true); - return; - } - - const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABPREVLVL); - if (!triggerIndex) { - SpeakText(_("No exits found."), true); - return; - } - - const TriggerStruct &trigger = trigs[*triggerIndex]; - const Point targetPosition { trigger.position.x, trigger.position.y }; - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - std::string message = TriggerLabelForSpeech(trigger); - if (!message.empty()) - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - SpeakText(message, true); - return; - } - - const std::optional triggerIndex = FindPreferredExitTriggerIndex(); - if (!triggerIndex) { - SpeakText(_("No exits found."), true); - return; - } - - const TriggerStruct &trigger = trigs[*triggerIndex]; - const Point targetPosition { trigger.position.x, trigger.position.y }; - - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - std::string message = TriggerLabelForSpeech(trigger); - if (!message.empty()) - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - - SpeakText(message, true); -} - -void SpeakNearestTownPortalInTownKeyPressed() -{ - if (!CanPlayerTakeAction()) - return; - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (leveltype != DTYPE_TOWN) { - SpeakText(_("Not in town."), true); - return; - } - if (MyPlayer == nullptr) - return; - - const std::optional portal = FindNearestTownPortalInTown(); - if (!portal) { - SpeakText(_("No town portals found."), true); - return; - } - - const Point startPosition = MyPlayer->position.future; - const Point targetPosition = portal->position; - - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - - std::string message = TownPortalLabelForSpeech(Portals[portal->portalIndex]); - message.append(": "); - if (!path) - AppendDirectionalFallback(message, targetPosition - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - - SpeakText(message, true); -} - -void SpeakNearestStairsKeyPressed(int triggerMessage) -{ - if (!CanPlayerTakeAction()) - return; - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (leveltype == DTYPE_TOWN) { - SpeakText(_("Not in a dungeon."), true); - return; - } - if (MyPlayer == nullptr) - return; - - const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(triggerMessage); - if (!triggerIndex) { - SpeakText(_("No exits found."), true); - return; - } - - const TriggerStruct &trigger = trigs[*triggerIndex]; - const Point startPosition = MyPlayer->position.future; - const Point targetPosition { trigger.position.x, trigger.position.y }; - - std::string message; - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); - if (!path) { - AppendDirectionalFallback(message, targetPosition - startPosition); - } else { - AppendKeyboardWalkPathForSpeech(message, *path); - } - - SpeakText(message, true); -} - -void SpeakNearestStairsDownKeyPressed() -{ - SpeakNearestStairsKeyPressed(WM_DIABNEXTLVL); -} - -void SpeakNearestStairsUpKeyPressed() -{ - SpeakNearestStairsKeyPressed(WM_DIABPREVLVL); -} - -bool IsKeyboardWalkAllowed() -{ - return CanPlayerTakeAction() - && !InGameMenu() - && !IsPlayerInStore() - && !QuestLogIsOpen - && !HelpFlag - && !ChatLogFlag - && !ChatFlag - && !DropGoldFlag - && !IsStashOpen - && !IsWithdrawGoldOpen - && !AutomapActive - && !invflag - && !CharFlag - && !SpellbookFlag - && !SpellSelectFlag - && !qtextflag; -} - -void KeyboardWalkKeyPressed(Direction direction) -{ - CancelAutoWalk(); - if (!IsKeyboardWalkAllowed()) - return; - - if (MyPlayer == nullptr) - return; - - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, MyPlayer->position.future + direction); -} - -void KeyboardWalkNorthKeyPressed() -{ - KeyboardWalkKeyPressed(Direction::NorthEast); -} - -void KeyboardWalkSouthKeyPressed() -{ - KeyboardWalkKeyPressed(Direction::SouthWest); -} - -void KeyboardWalkEastKeyPressed() -{ - KeyboardWalkKeyPressed(Direction::SouthEast); -} - -void KeyboardWalkWestKeyPressed() -{ - KeyboardWalkKeyPressed(Direction::NorthWest); -} - -void SpeakNearestUnexploredTileKeyPressed() -{ - if (!CanPlayerTakeAction()) - return; - if (leveltype == DTYPE_TOWN) { - SpeakText(_("Not in a dungeon."), true); - return; - } - if (AutomapActive) { - SpeakText(_("Close the map first."), true); - return; - } - if (MyPlayer == nullptr) - return; - - const Point startPosition = MyPlayer->position.future; - const std::optional target = FindNearestUnexploredTile(startPosition); - if (!target) { - SpeakText(_("No unexplored areas found."), true); - return; - } - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *target); - std::string message; - if (!path) - AppendDirectionalFallback(message, *target - startPosition); - else - AppendKeyboardWalkPathForSpeech(message, *path); - - SpeakText(message, true); -} - -void SpeakPlayerHealthPercentageKeyPressed() + + 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 (!CanPlayerTakeAction()) - return; - if (MyPlayer == nullptr) + if (!gbShowIntro) return; - const SDL_Keymod modState = SDL_GetModState(); - const bool speakMana = (modState & SDL_KMOD_SHIFT) != 0; - if (speakMana) { - const int maxMana = MyPlayer->_pMaxMana; - if (maxMana <= 0) - return; + if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) + play_movie("gendata\\logo.smk", true); - const int currentMana = std::max(MyPlayer->_pMana, 0); - int manaPercent = static_cast((static_cast(currentMana) * 100 + maxMana / 2) / maxMana); - manaPercent = std::clamp(manaPercent, 0, 100); - SpeakText(fmt::format("{:d}%", manaPercent), /*force=*/true); - return; + 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(); + } } - const int maxHp = MyPlayer->_pMaxHP; - if (maxHp <= 0) - return; + if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) + UiTitleDialog(); +} - const int currentHp = std::max(MyPlayer->_pHitPoints, 0); - int hpPercent = static_cast((static_cast(currentHp) * 100 + maxHp / 2) / maxHp); - hpPercent = std::clamp(hpPercent, 0, 100); - SpeakText(fmt::format("{:d}%", hpPercent), /*force=*/true); -} - -void SpeakExperienceToNextLevelKeyPressed() -{ - if (!CanPlayerTakeAction()) - return; - if (MyPlayer == nullptr) - return; - - const Player &myPlayer = *MyPlayer; - if (myPlayer.isMaxCharacterLevel()) { - SpeakText(_("Max level."), /*force=*/true); - return; - } - - const uint32_t nextExperienceThreshold = myPlayer.getNextExperienceThreshold(); - const uint32_t currentExperience = myPlayer._pExperience; - const uint32_t remainingExperience = currentExperience >= nextExperienceThreshold ? 0 : nextExperienceThreshold - currentExperience; - const int nextLevel = myPlayer.getCharacterLevel() + 1; - SpeakText( - fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(remainingExperience), nextLevel), - /*force=*/true); -} - -namespace { -std::string BuildCurrentLocationForSpeech() -{ - // Quest Level Name - if (setlevel) { - const char *const questLevelName = QuestLevelNames[setlvlnum]; - if (questLevelName == nullptr || questLevelName[0] == '\0') - return std::string { _("Set level") }; - - return fmt::format("{:s}: {:s}", _("Set level"), _(questLevelName)); - } - - // Dungeon Name - constexpr std::array DungeonStrs = { - N_("Town"), - N_("Cathedral"), - N_("Catacombs"), - N_("Caves"), - N_("Hell"), - N_("Nest"), - N_("Crypt"), - }; - std::string dungeonStr; - if (leveltype >= DTYPE_TOWN && leveltype <= DTYPE_LAST) { - dungeonStr = _(DungeonStrs[static_cast(leveltype)]); - } else { - dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); - } - - if (leveltype == DTYPE_TOWN || currlevel <= 0) - return dungeonStr; - - // Dungeon Level - int level = currlevel; - if (leveltype == DTYPE_CATACOMBS) - level -= 4; - else if (leveltype == DTYPE_CAVES) - level -= 8; - else if (leveltype == DTYPE_HELL) - level -= 12; - else if (leveltype == DTYPE_NEST) - level -= 16; - else if (leveltype == DTYPE_CRYPT) - level -= 20; - - if (level <= 0) - return dungeonStr; - - return fmt::format(fmt::runtime(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}")), dungeonStr, level); -} -} // namespace - -void SpeakCurrentLocationKeyPressed() -{ - if (!CanPlayerTakeAction()) - return; - - SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true); -} - -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(); - if (invflag) - FocusOnInventory(); -} - -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(); -} - -void SpeakSelectedSpeedbookSpell() -{ - for (const auto &spellListItem : GetSpellListItems()) { - if (spellListItem.isSelected) { - SpeakText(pgettext("spell", GetSpellData(spellListItem.id).sNameText), /*force=*/true); - return; - } - } - SpeakText(_("No spell selected."), /*force=*/true); -} - -void DisplaySpellsKeyPressed() -{ - if (IsPlayerInStore()) - return; - CloseCharPanel(); - QuestLogIsOpen = false; - CloseInventory(); - SpellbookFlag = false; - if (!SpellSelectFlag) { - DoSpeedBook(); - SpeakSelectedSpeedbookSpell(); - } else { - SpellSelectFlag = false; - } - LastPlayerAction = PlayerActionType::None; -} - -void SpellBookKeyPressed() -{ - if (IsPlayerInStore()) - return; - SpellbookFlag = !SpellbookFlag; - if (SpellbookFlag && MyPlayer != nullptr) { - const Player &player = *MyPlayer; - if (IsValidSpell(player._pRSpell)) { - SpeakText(pgettext("spell", GetSpellData(player._pRSpell).sNameText), /*force=*/true); - } else { - SpeakText(_("No spell selected."), /*force=*/true); - } - } - 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(); -} - -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 - if (!QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !CharFlag - && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag - && !ChatLogFlag && !HelpFlag) - return true; - - return false; -} - -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); - -void CancelAutoWalkInternal() -{ - AutoWalkTrackerTargetId = -1; - AutoWalkTownNpcTarget = -1; -} - -} // namespace - -void CancelAutoWalk() -{ - CancelAutoWalkInternal(); - if (MyPlayer != nullptr) - NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, MyPlayer->position.future); -} - -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( - "ListTownNpcs", - N_("List town NPCs"), - N_("Speaks a list of town NPCs."), - SDLK_UNKNOWN, - ListTownNpcsKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "PreviousTownNpc", - N_("Previous town NPC"), - N_("Select previous town NPC (speaks)."), - SDLK_UNKNOWN, - SelectPreviousTownNpcKeyPressed, - nullptr, - IsTownNpcActionAllowed); - options.Keymapper.AddAction( - "NextTownNpc", - N_("Next town NPC"), - N_("Select next town NPC (speaks)."), - SDLK_UNKNOWN, - SelectNextTownNpcKeyPressed, - nullptr, - IsTownNpcActionAllowed); - options.Keymapper.AddAction( - "SpeakSelectedTownNpc", - N_("Speak selected town NPC"), - N_("Speaks the currently selected town NPC."), - SDLK_UNKNOWN, - SpeakSelectedTownNpc, +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(); + UpdateAutoWalkTownNpc(); + UpdateAutoWalkTracker(); + UpdateLowDurabilityWarnings(); + } + 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(); + UpdateBossHealthAnnouncements(); + UpdateProximityAudioCues(); + UpdateAttackableMonsterAnnouncements(); + UpdateInteractableDoorAnnouncements(); + } else { + gGameLogicStep = GameLogicStep::ProcessTowners; + ProcessTowners(); + gGameLogicStep = GameLogicStep::ProcessItemsTown; + ProcessItems(); + gGameLogicStep = GameLogicStep::ProcessMissilesTown; + ProcessMissiles(); + UpdateProximityAudioCues(); + } + + UpdatePlayerLowHpWarningSound(); + + 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 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); + +void CancelAutoWalkInternal() +{ + ResetAutoWalkTracker(); + ResetAutoWalkTownNpc(); +} + +} // namespace + +void CancelAutoWalk() +{ + CancelAutoWalkInternal(); + if (MyPlayer != nullptr) + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, MyPlayer->position.future); +} + +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( + "ListTownNpcs", + N_("List town NPCs"), + N_("Speaks a list of town NPCs."), + SDLK_UNKNOWN, + ListTownNpcsKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "PreviousTownNpc", + N_("Previous town NPC"), + N_("Select previous town NPC (speaks)."), + SDLK_UNKNOWN, + SelectPreviousTownNpcKeyPressed, + nullptr, + IsTownNpcActionAllowed); + options.Keymapper.AddAction( + "NextTownNpc", + N_("Next town NPC"), + N_("Select next town NPC (speaks)."), + SDLK_UNKNOWN, + SelectNextTownNpcKeyPressed, + nullptr, + IsTownNpcActionAllowed); + options.Keymapper.AddAction( + "SpeakSelectedTownNpc", + N_("Speak selected town NPC"), + N_("Speaks the currently selected town NPC."), + SDLK_UNKNOWN, + SpeakSelectedTownNpc, nullptr, IsTownNpcActionAllowed); options.Keymapper.AddAction( @@ -7141,10 +1971,10 @@ void InitKeymapActions() "SpeakNearestUnexploredSpace", N_("Nearest unexplored space"), N_("Speaks the nearest unexplored space."), - 'H', - SpeakNearestUnexploredTileKeyPressed, - nullptr, - CanPlayerTakeAction); + 'H', + SpeakNearestUnexploredTileKeyPressed, + nullptr, + CanPlayerTakeAction); options.Keymapper.AddAction( "TrackerPrevious", N_("Tracker previous"), @@ -7169,141 +1999,141 @@ void InitKeymapActions() TrackerHomeKeyPressed, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu() && !IsPlayerInStore() && !ChatLogFlag; }); - options.Keymapper.AddAction( - "KeyboardWalkNorth", - N_("Walk north"), - N_("Walk north (one tile)."), - SDLK_UP, - KeyboardWalkNorthKeyPressed); - options.Keymapper.AddAction( - "KeyboardWalkSouth", - N_("Walk south"), - N_("Walk south (one tile)."), - SDLK_DOWN, - KeyboardWalkSouthKeyPressed); - options.Keymapper.AddAction( - "KeyboardWalkEast", - N_("Walk east"), - N_("Walk east (one tile)."), - SDLK_RIGHT, - KeyboardWalkEastKeyPressed); - options.Keymapper.AddAction( - "KeyboardWalkWest", - N_("Walk west"), - N_("Walk west (one tile)."), - SDLK_LEFT, - KeyboardWalkWestKeyPressed); - options.Keymapper.AddAction( - "PrimaryAction", - N_("Primary action"), - N_("Attack monsters, talk to towners, lift and place inventory items."), - 'A', - PerformPrimaryActionAutoTarget, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Keymapper.AddAction( - "SecondaryAction", - N_("Secondary action"), - N_("Open chests, interact with doors, pick up items."), - 'D', - PerformSecondaryActionAutoTarget, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - options.Keymapper.AddAction( - "SpellAction", - N_("Spell action"), - N_("Cast the active spell."), - 'W', - PerformSpellActionAutoTarget, - nullptr, - []() { return CanPlayerTakeAction() && !InGameMenu(); }); - - 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."), - SDLK_UNKNOWN, - [] { - GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); - CalcViewportGeometry(); - }, - nullptr, - CanPlayerTakeAction); + options.Keymapper.AddAction( + "KeyboardWalkNorth", + N_("Walk north"), + N_("Walk north (one tile)."), + SDLK_UP, + KeyboardWalkNorthKeyPressed); + options.Keymapper.AddAction( + "KeyboardWalkSouth", + N_("Walk south"), + N_("Walk south (one tile)."), + SDLK_DOWN, + KeyboardWalkSouthKeyPressed); + options.Keymapper.AddAction( + "KeyboardWalkEast", + N_("Walk east"), + N_("Walk east (one tile)."), + SDLK_RIGHT, + KeyboardWalkEastKeyPressed); + options.Keymapper.AddAction( + "KeyboardWalkWest", + N_("Walk west"), + N_("Walk west (one tile)."), + SDLK_LEFT, + KeyboardWalkWestKeyPressed); + options.Keymapper.AddAction( + "PrimaryAction", + N_("Primary action"), + N_("Attack monsters, talk to towners, lift and place inventory items."), + 'A', + PerformPrimaryActionAutoTarget, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Keymapper.AddAction( + "SecondaryAction", + N_("Secondary action"), + N_("Open chests, interact with doors, pick up items."), + 'D', + PerformSecondaryActionAutoTarget, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + options.Keymapper.AddAction( + "SpellAction", + N_("Spell action"), + N_("Cast the active spell."), + 'W', + PerformSpellActionAutoTarget, + nullptr, + []() { return CanPlayerTakeAction() && !InGameMenu(); }); + + 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."), + SDLK_UNKNOWN, + [] { + GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); + CalcViewportGeometry(); + }, + nullptr, + CanPlayerTakeAction); options.Keymapper.AddAction( "SpeakPlayerHealthPercentage", N_("Health percentage"), @@ -7312,1487 +2142,1487 @@ void InitKeymapActions() SpeakPlayerHealthPercentageKeyPressed, nullptr, CanPlayerTakeAction); - options.Keymapper.AddAction( - "SpeakExperienceToNextLevel", - N_("Experience to level"), - N_("Speaks how much experience remains to reach the next level."), - 'X', - SpeakExperienceToNextLevelKeyPressed, - nullptr, - CanPlayerTakeAction); - options.Keymapper.AddAction( - "PauseGame", - N_("Pause Game"), - N_("Pauses the game."), - SDLK_UNKNOWN, - 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."), - SDLK_INSERT, - [] { - ToggleChatLog(); - }); - options.Keymapper.AddAction( - "SpeakCurrentLocation", - N_("Location"), - N_("Speaks the current dungeon and floor."), - 'L', - SpeakCurrentLocationKeyPressed, - nullptr, - CanPlayerTakeAction); - 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', - [] { + options.Keymapper.AddAction( + "SpeakExperienceToNextLevel", + N_("Experience to level"), + N_("Speaks how much experience remains to reach the next level."), + 'X', + SpeakExperienceToNextLevelKeyPressed, + nullptr, + CanPlayerTakeAction); + options.Keymapper.AddAction( + "PauseGame", + N_("Pause Game"), + N_("Pauses the game."), + SDLK_UNKNOWN, + 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."), + SDLK_INSERT, + [] { + ToggleChatLog(); + }); + options.Keymapper.AddAction( + "SpeakCurrentLocation", + N_("Location"), + N_("Speaks the current dungeon and floor."), + 'L', + SpeakCurrentLocationKeyPressed, + nullptr, + CanPlayerTakeAction); + 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) -{ - MousePosition = position; - if (ControlDevice != ControlTypes::KeyboardAndMouse) { - 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(); -#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()) - 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(); - 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(); - if (leveltype != DTYPE_TOWN) - SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true); - 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 + +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) +{ + MousePosition = position; + if (ControlDevice != ControlTypes::KeyboardAndMouse) { + 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(); +#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()) + 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(); + 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(); + if (leveltype != DTYPE_TOWN) + SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true); + 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/utils/accessibility_announcements.cpp b/Source/utils/accessibility_announcements.cpp new file mode 100644 index 000000000..0bdf593c7 --- /dev/null +++ b/Source/utils/accessibility_announcements.cpp @@ -0,0 +1,491 @@ +/** + * @file utils/accessibility_announcements.cpp + * + * Periodic accessibility announcements (low HP warning, durability, boss health, + * attackable monsters, interactable doors). + */ +#include "utils/accessibility_announcements.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef USE_SDL3 +#include +#else +#include +#endif + +#include "controls/plrctrls.h" +#include "engine/sound.h" +#include "gamemenu.h" +#include "inv.h" +#include "items.h" +#include "levels/gendung.h" +#include "monster.h" +#include "objects.h" +#include "player.h" +#include "utils/is_of.hpp" +#include "utils/language.h" +#include "utils/log.hpp" +#include "utils/screen_reader.hpp" +#include "utils/str_cat.hpp" +#include "utils/string_or_view.hpp" + +namespace devilution { + +#ifdef NOSOUND +void UpdatePlayerLowHpWarningSound() +{ +} +#else +namespace { + +std::unique_ptr PlayerLowHpWarningSound; +bool TriedLoadingPlayerLowHpWarningSound = false; + +TSnd *GetPlayerLowHpWarningSound() +{ + if (TriedLoadingPlayerLowHpWarningSound) + return PlayerLowHpWarningSound.get(); + TriedLoadingPlayerLowHpWarningSound = true; + + if (!gbSndInited) + return nullptr; + + PlayerLowHpWarningSound = std::make_unique(); + PlayerLowHpWarningSound->start_tc = SDL_GetTicks() - 80 - 1; + + // Support both the new "playerhaslowhp" name and the older underscore version. + if (PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0 + && PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0) { + LogWarn("Failed to load low HP warning sound from any of the expected paths."); + PlayerLowHpWarningSound = nullptr; + } + + return PlayerLowHpWarningSound.get(); +} + +void StopPlayerLowHpWarningSound() +{ + if (PlayerLowHpWarningSound != nullptr) + PlayerLowHpWarningSound->DSB.Stop(); +} + +[[nodiscard]] uint32_t LowHpIntervalMs(int hpPercent) +{ + // The sound starts at 50% HP (slow) and speeds up every 10% down to 0%. + if (hpPercent > 40) + return 1500; + if (hpPercent > 30) + return 1200; + if (hpPercent > 20) + return 900; + if (hpPercent > 10) + return 600; + return 300; +} + +} // namespace + +void UpdatePlayerLowHpWarningSound() +{ + static uint32_t LastWarningStartMs = 0; + + if (!gbSndInited || !gbSoundOn || MyPlayer == nullptr || InGameMenu()) { + StopPlayerLowHpWarningSound(); + LastWarningStartMs = 0; + return; + } + + // Stop immediately when dead. + if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { + StopPlayerLowHpWarningSound(); + LastWarningStartMs = 0; + return; + } + + const int maxHp = MyPlayer->_pMaxHP; + if (maxHp <= 0) { + StopPlayerLowHpWarningSound(); + LastWarningStartMs = 0; + return; + } + + const int hp = std::clamp(MyPlayer->_pHitPoints, 0, maxHp); + const int hpPercent = std::clamp(hp * 100 / maxHp, 0, 100); + + // Only play below (or equal to) 50% and above 0%. + if (hpPercent > 50 || hpPercent <= 0) { + StopPlayerLowHpWarningSound(); + LastWarningStartMs = 0; + return; + } + + TSnd *snd = GetPlayerLowHpWarningSound(); + if (snd == nullptr || !snd->DSB.IsLoaded()) + return; + + const uint32_t now = SDL_GetTicks(); + const uint32_t intervalMs = LowHpIntervalMs(hpPercent); + if (LastWarningStartMs == 0) + LastWarningStartMs = now - intervalMs; + if (now - LastWarningStartMs < intervalMs) + return; + + // Restart the cue even if it's already playing so the "tempo" is controlled by HP. + snd->DSB.Stop(); + snd_play_snd(snd, /*lVolume=*/0, /*lPan=*/0); + LastWarningStartMs = now; +} +#endif // NOSOUND + +namespace { + +[[nodiscard]] bool IsBossMonsterForHpAnnouncement(const Monster &monster) +{ + return monster.isUnique() || monster.ai == MonsterAIID::Diablo; +} + +} // namespace + +void UpdateLowDurabilityWarnings() +{ + static std::array WarnedSeeds {}; + static std::array HasWarned {}; + + if (MyPlayer == nullptr) + return; + if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) + return; + + std::vector newlyLow; + newlyLow.reserve(NUM_INVLOC); + + for (int slot = 0; slot < NUM_INVLOC; ++slot) { + const Item &item = MyPlayer->InvBody[slot]; + if (item.isEmpty() || item._iMaxDur <= 0 || item._iMaxDur == DUR_INDESTRUCTIBLE || item._iDurability == DUR_INDESTRUCTIBLE) { + HasWarned[slot] = false; + continue; + } + + const int maxDur = item._iMaxDur; + const int durability = item._iDurability; + if (durability <= 0) { + HasWarned[slot] = false; + continue; + } + + int threshold = std::max(2, maxDur / 10); + threshold = std::clamp(threshold, 1, maxDur); + + const bool isLow = durability <= threshold; + if (!isLow) { + HasWarned[slot] = false; + continue; + } + + if (HasWarned[slot] && WarnedSeeds[slot] == item._iSeed) + continue; + + HasWarned[slot] = true; + WarnedSeeds[slot] = item._iSeed; + + const StringOrView name = item.getName(); + if (!name.empty()) + newlyLow.emplace_back(name.str().data(), name.str().size()); + } + + if (newlyLow.empty()) + return; + + // Add ordinal numbers for duplicates (e.g. two rings with the same name). + for (size_t i = 0; i < newlyLow.size(); ++i) { + int total = 0; + for (size_t j = 0; j < newlyLow.size(); ++j) { + if (newlyLow[j] == newlyLow[i]) + ++total; + } + if (total <= 1) + continue; + + int ordinal = 1; + for (size_t j = 0; j < i; ++j) { + if (newlyLow[j] == newlyLow[i]) + ++ordinal; + } + newlyLow[i] = fmt::format("{} {}", newlyLow[i], ordinal); + } + + std::string joined; + for (size_t i = 0; i < newlyLow.size(); ++i) { + if (i != 0) + joined += ", "; + joined += newlyLow[i]; + } + + SpeakText(fmt::format(fmt::runtime(_("Low durability: {:s}")), joined), /*force=*/true); +} + +void UpdateBossHealthAnnouncements() +{ + static dungeon_type LastLevelType = DTYPE_NONE; + static int LastCurrLevel = -1; + static bool LastSetLevel = false; + static _setlevels LastSetLevelNum = SL_NONE; + static std::array LastAnnouncedBucket {}; + + if (MyPlayer == nullptr) + return; + if (leveltype == DTYPE_TOWN) + return; + + const bool levelChanged = LastLevelType != leveltype || LastCurrLevel != currlevel || LastSetLevel != setlevel || LastSetLevelNum != setlvlnum; + if (levelChanged) { + LastAnnouncedBucket.fill(-1); + LastLevelType = leveltype; + LastCurrLevel = currlevel; + LastSetLevel = setlevel; + LastSetLevelNum = setlvlnum; + } + + for (size_t monsterId = 0; monsterId < MaxMonsters; ++monsterId) { + if (LastAnnouncedBucket[monsterId] < 0) + continue; + + const Monster &monster = Monsters[monsterId]; + if (monster.isInvalid || monster.hitPoints <= 0 || !IsBossMonsterForHpAnnouncement(monster)) + LastAnnouncedBucket[monsterId] = -1; + } + + for (size_t i = 0; i < ActiveMonsterCount; i++) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; + + if (monster.isInvalid) + continue; + if ((monster.flags & MFLAG_HIDDEN) != 0) + continue; + if (!IsBossMonsterForHpAnnouncement(monster)) + continue; + if (monster.hitPoints <= 0 || monster.maxHitPoints <= 0) + continue; + + const int64_t hp = std::clamp(monster.hitPoints, 0, monster.maxHitPoints); + const int64_t maxHp = monster.maxHitPoints; + const int hpPercent = static_cast(std::clamp(hp * 100 / maxHp, 0, 100)); + const int bucket = ((hpPercent + 9) / 10) * 10; + + int8_t &lastBucket = LastAnnouncedBucket[monsterId]; + if (lastBucket < 0) { + lastBucket = static_cast(((hpPercent + 9) / 10) * 10); + continue; + } + + if (bucket >= lastBucket) + continue; + + lastBucket = static_cast(bucket); + SpeakText(fmt::format(fmt::runtime(_("{:s} health: {:d}%")), monster.name(), bucket), /*force=*/false); + } +} + +void UpdateAttackableMonsterAnnouncements() +{ + static std::optional LastAttackableMonsterId; + + if (MyPlayer == nullptr) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (leveltype == DTYPE_TOWN) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (InGameMenu() || invflag) { + LastAttackableMonsterId = std::nullopt; + return; + } + + const Player &player = *MyPlayer; + const Point playerPosition = player.position.tile; + + int bestRotations = 5; + std::optional bestId; + + for (size_t i = 0; i < ActiveMonsterCount; i++) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; + + if (monster.isInvalid) + continue; + if ((monster.flags & MFLAG_HIDDEN) != 0) + continue; + if (monster.hitPoints <= 0) + continue; + if (monster.isPlayerMinion()) + continue; + if (!monster.isPossibleToHit()) + continue; + + const Point monsterPosition = monster.position.tile; + if (playerPosition.WalkingDistance(monsterPosition) > 1) + continue; + + const int d1 = static_cast(player._pdir); + const int d2 = static_cast(GetDirection(playerPosition, monsterPosition)); + + int rotations = std::abs(d1 - d2); + if (rotations > 4) + rotations = 4 - (rotations % 4); + + if (!bestId || rotations < bestRotations || (rotations == bestRotations && monsterId < *bestId)) { + bestRotations = rotations; + bestId = monsterId; + } + } + + if (!bestId) { + LastAttackableMonsterId = std::nullopt; + return; + } + + if (LastAttackableMonsterId && *LastAttackableMonsterId == *bestId) + return; + + LastAttackableMonsterId = *bestId; + + const std::string_view name = Monsters[*bestId].name(); + if (!name.empty()) + SpeakText(name, /*force=*/true); +} + +StringOrView DoorLabelForSpeech(const Object &door) +{ + if (!door.isDoor()) + return door.name(); + + // Catacombs doors are grates, so differentiate them for the screen reader / tracker. + if (IsAnyOf(door._otype, _object_id::OBJ_L2LDOOR, _object_id::OBJ_L2RDOOR)) { + if (door._oVar4 == DOOR_OPEN) + return _("Open Grate Door"); + if (door._oVar4 == DOOR_CLOSED) + return _("Closed Grate Door"); + if (door._oVar4 == DOOR_BLOCKED) + return _("Blocked Grate Door"); + return _("Grate Door"); + } + + return door.name(); +} + +void UpdateInteractableDoorAnnouncements() +{ + static std::optional LastInteractableDoorId; + static std::optional LastInteractableDoorState; + + if (MyPlayer == nullptr) { + LastInteractableDoorId = std::nullopt; + LastInteractableDoorState = std::nullopt; + return; + } + if (leveltype == DTYPE_TOWN) { + LastInteractableDoorId = std::nullopt; + LastInteractableDoorState = std::nullopt; + return; + } + if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { + LastInteractableDoorId = std::nullopt; + LastInteractableDoorState = std::nullopt; + return; + } + if (InGameMenu() || invflag) { + LastInteractableDoorId = std::nullopt; + LastInteractableDoorState = std::nullopt; + return; + } + + const Player &player = *MyPlayer; + const Point playerPosition = player.position.tile; + + std::optional bestId; + int bestRotations = 5; + int bestDistance = 0; + + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + + const Point pos = playerPosition + Displacement { dx, dy }; + if (!InDungeonBounds(pos)) + continue; + + const int objectId = std::abs(dObject[pos.x][pos.y]) - 1; + if (objectId < 0 || objectId >= MAXOBJECTS) + continue; + + const Object &door = Objects[objectId]; + if (!door.isDoor() || !door.canInteractWith()) + continue; + + const int distance = playerPosition.WalkingDistance(door.position); + if (distance > 1) + continue; + + const int d1 = static_cast(player._pdir); + const int d2 = static_cast(GetDirection(playerPosition, door.position)); + + int rotations = std::abs(d1 - d2); + if (rotations > 4) + rotations = 4 - (rotations % 4); + + if (!bestId || rotations < bestRotations || (rotations == bestRotations && distance < bestDistance) + || (rotations == bestRotations && distance == bestDistance && objectId < *bestId)) { + bestRotations = rotations; + bestDistance = distance; + bestId = objectId; + } + } + } + + if (!bestId) { + LastInteractableDoorId = std::nullopt; + LastInteractableDoorState = std::nullopt; + return; + } + + const Object &door = Objects[*bestId]; + const int state = door._oVar4; + if (LastInteractableDoorId && LastInteractableDoorState && *LastInteractableDoorId == *bestId && *LastInteractableDoorState == state) + return; + + LastInteractableDoorId = *bestId; + LastInteractableDoorState = state; + + const StringOrView label = DoorLabelForSpeech(door); + if (!label.empty()) + SpeakText(label.str(), /*force=*/true); +} + +} // namespace devilution diff --git a/Source/utils/accessibility_announcements.hpp b/Source/utils/accessibility_announcements.hpp new file mode 100644 index 000000000..af8bdd75f --- /dev/null +++ b/Source/utils/accessibility_announcements.hpp @@ -0,0 +1,22 @@ +/** + * @file utils/accessibility_announcements.hpp + * + * Periodic accessibility announcements (low HP warning, durability, boss health, + * attackable monsters, interactable doors). + */ +#pragma once + +#include "utils/string_or_view.hpp" + +namespace devilution { + +struct Object; + +void UpdatePlayerLowHpWarningSound(); +void UpdateLowDurabilityWarnings(); +void UpdateBossHealthAnnouncements(); +void UpdateAttackableMonsterAnnouncements(); +StringOrView DoorLabelForSpeech(const Object &door); +void UpdateInteractableDoorAnnouncements(); + +} // namespace devilution diff --git a/Source/utils/navigation_speech.cpp b/Source/utils/navigation_speech.cpp new file mode 100644 index 000000000..1b26a7f70 --- /dev/null +++ b/Source/utils/navigation_speech.cpp @@ -0,0 +1,685 @@ +/** + * @file utils/navigation_speech.cpp + * + * Navigation speech: exit/stairs/portal/unexplored speech and keyboard walk keys. + */ +#include "utils/navigation_speech.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef USE_SDL3 +#include +#else +#include +#endif + +#include "automap.h" +#include "control/control.hpp" +#include "controls/accessibility_keys.hpp" +#include "controls/plrctrls.h" +#include "diablo.h" +#include "help.h" +#include "inv.h" +#include "levels/gendung.h" +#include "levels/setmaps.h" +#include "levels/tile_properties.hpp" +#include "levels/trigs.h" +#include "minitext.h" +#include "missiles.h" +#include "multi.h" +#include "player.h" +#include "portal.h" +#include "qol/chatlog.h" +#include "qol/stash.h" +#include "quests.h" +#include "stores.h" +#include "utils/language.h" +#include "utils/screen_reader.hpp" +#include "utils/str_cat.hpp" +#include "utils/sdl_compat.h" +#include "utils/walk_path_speech.hpp" + +namespace devilution { + +namespace { + +std::optional FindNearestUnexploredTile(Point startPosition) +{ + if (!InDungeonBounds(startPosition)) + return std::nullopt; + + std::array visited {}; + std::queue queue; + + const auto enqueue = [&](Point position) { + if (!InDungeonBounds(position)) + return; + + const size_t index = static_cast(position.x) + static_cast(position.y) * MAXDUNX; + if (visited[index]) + return; + + if (!IsTileWalkable(position, /*ignoreDoors=*/true)) + return; + + visited[index] = true; + queue.push(position); + }; + + enqueue(startPosition); + + constexpr std::array Neighbors = { + Direction::NorthEast, + Direction::SouthWest, + Direction::SouthEast, + Direction::NorthWest, + }; + + while (!queue.empty()) { + const Point position = queue.front(); + queue.pop(); + + if (!HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Explored)) + return position; + + for (const Direction dir : Neighbors) { + enqueue(position + dir); + } + } + + return std::nullopt; +} + +std::optional LockedTownDungeonTriggerIndex; + +std::optional FindDefaultTownDungeonTriggerIndex(const std::vector &candidates) +{ + for (const int index : candidates) { + if (trigs[index]._tmsg == WM_DIABNEXTLVL) + return index; + } + if (!candidates.empty()) + return candidates.front(); + return std::nullopt; +} + +std::optional FindLockedTownDungeonTriggerIndex(const std::vector &candidates) +{ + if (!LockedTownDungeonTriggerIndex) + return std::nullopt; + if (std::find(candidates.begin(), candidates.end(), *LockedTownDungeonTriggerIndex) != candidates.end()) + return *LockedTownDungeonTriggerIndex; + return std::nullopt; +} + +std::optional FindNextTownDungeonTriggerIndex(const std::vector &candidates, int current) +{ + if (candidates.empty()) + return std::nullopt; + + const auto it = std::find(candidates.begin(), candidates.end(), current); + if (it == candidates.end()) + return candidates.front(); + if (std::next(it) == candidates.end()) + return candidates.front(); + return *std::next(it); +} + +std::optional FindNearestTriggerIndexWithMessage(int message) +{ + if (numtrigs <= 0 || MyPlayer == nullptr) + return std::nullopt; + + const Point playerPosition = MyPlayer->position.future; + std::optional bestIndex; + int bestDistance = 0; + + for (int i = 0; i < numtrigs; ++i) { + if (trigs[i]._tmsg != message) + continue; + + const Point triggerPosition { trigs[i].position.x, trigs[i].position.y }; + const int distance = playerPosition.WalkingDistance(triggerPosition); + if (!bestIndex || distance < bestDistance) { + bestIndex = i; + bestDistance = distance; + } + } + + return bestIndex; +} + +std::optional FindNearestTownPortalOnCurrentLevel() +{ + if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) + return std::nullopt; + + const Point playerPosition = MyPlayer->position.future; + const int currentLevel = setlevel ? static_cast(setlvlnum) : currlevel; + + std::optional bestPosition; + int bestDistance = 0; + + for (int i = 0; i < MAXPORTAL; ++i) { + const Portal &portal = Portals[i]; + if (!portal.open) + continue; + if (portal.setlvl != setlevel) + continue; + if (portal.level != currentLevel) + continue; + + const int distance = playerPosition.WalkingDistance(portal.position); + if (!bestPosition || distance < bestDistance) { + bestPosition = portal.position; + bestDistance = distance; + } + } + + return bestPosition; +} + +struct TownPortalInTown { + int portalIndex; + Point position; + int distance; +}; + +std::optional FindNearestTownPortalInTown() +{ + if (MyPlayer == nullptr || leveltype != DTYPE_TOWN) + return std::nullopt; + + const Point playerPosition = MyPlayer->position.future; + + std::optional best; + int bestDistance = 0; + + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::TownPortal) + continue; + if (missile._misource < 0 || missile._misource >= MAXPORTAL) + continue; + if (!Portals[missile._misource].open) + continue; + + const Point portalPosition = missile.position.tile; + const int distance = playerPosition.WalkingDistance(portalPosition); + if (!best || distance < bestDistance) { + best = TownPortalInTown { + .portalIndex = missile._misource, + .position = portalPosition, + .distance = distance, + }; + bestDistance = distance; + } + } + + return best; +} + +struct QuestSetLevelEntrance { + _setlevels questLevel; + Point entrancePosition; + int distance; +}; + +std::optional FindNearestQuestSetLevelEntranceOnCurrentLevel() +{ + if (MyPlayer == nullptr || setlevel) + return std::nullopt; + + const Point playerPosition = MyPlayer->position.future; + std::optional best; + int bestDistance = 0; + + for (const Quest &quest : Quests) { + if (quest._qslvl == SL_NONE) + continue; + if (quest._qactive == QUEST_NOTAVAIL) + continue; + if (quest._qlevel != currlevel) + continue; + if (!InDungeonBounds(quest.position)) + continue; + + const int distance = playerPosition.WalkingDistance(quest.position); + if (!best || distance < bestDistance) { + best = QuestSetLevelEntrance { + .questLevel = quest._qslvl, + .entrancePosition = quest.position, + .distance = distance, + }; + bestDistance = distance; + } + } + + return best; +} + +void SpeakNearestStairsKeyPressed(int triggerMessage) +{ + if (!CanPlayerTakeAction()) + return; + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (leveltype == DTYPE_TOWN) { + SpeakText(_("Not in a dungeon."), true); + return; + } + if (MyPlayer == nullptr) + return; + + const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(triggerMessage); + if (!triggerIndex) { + SpeakText(_("No exits found."), true); + return; + } + + const TriggerStruct &trigger = trigs[*triggerIndex]; + const Point startPosition = MyPlayer->position.future; + const Point targetPosition { trigger.position.x, trigger.position.y }; + + std::string message; + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + if (!path) { + AppendDirectionalFallback(message, targetPosition - startPosition); + } else { + AppendKeyboardWalkPathForSpeech(message, *path); + } + + SpeakText(message, true); +} + +void KeyboardWalkKeyPressed(Direction direction) +{ + CancelAutoWalk(); + if (!IsKeyboardWalkAllowed()) + return; + + if (MyPlayer == nullptr) + return; + + NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, MyPlayer->position.future + direction); +} + +} // namespace + +std::string TriggerLabelForSpeech(const TriggerStruct &trigger) +{ + switch (trigger._tmsg) { + case WM_DIABNEXTLVL: + if (leveltype == DTYPE_TOWN) + return std::string { _("Cathedral entrance") }; + return std::string { _("Stairs down") }; + case WM_DIABPREVLVL: + return std::string { _("Stairs up") }; + case WM_DIABTOWNWARP: + switch (trigger._tlvl) { + case 5: + return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Catacombs")); + case 9: + return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Caves")); + case 13: + return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Hell")); + case 17: + return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Nest")); + case 21: + return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Crypt")); + default: + return fmt::format(fmt::runtime(_("Town warp to level {:d}")), trigger._tlvl); + } + case WM_DIABTWARPUP: + return std::string { _("Warp up") }; + case WM_DIABRETOWN: + return std::string { _("Return to town") }; + case WM_DIABWARPLVL: + return std::string { _("Warp") }; + case WM_DIABSETLVL: + return std::string { _("Set level") }; + case WM_DIABRTNLVL: + return std::string { _("Return level") }; + default: + return std::string { _("Exit") }; + } +} + +std::string TownPortalLabelForSpeech(const Portal &portal) +{ + if (portal.level <= 0) + return std::string { _("Town portal") }; + + if (portal.setlvl) { + const auto questLevel = static_cast<_setlevels>(portal.level); + const char *questLevelName = QuestLevelNames[questLevel]; + if (questLevelName == nullptr || questLevelName[0] == '\0') + return std::string { _("Town portal to set level") }; + + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a set/quest level name. */ "Town portal to {:s}")), _(questLevelName)); + } + + constexpr std::array DungeonStrs = { + N_("Town"), + N_("Cathedral"), + N_("Catacombs"), + N_("Caves"), + N_("Hell"), + N_("Nest"), + N_("Crypt"), + }; + std::string dungeonStr; + if (portal.ltype >= DTYPE_TOWN && portal.ltype <= DTYPE_LAST) { + dungeonStr = _(DungeonStrs[static_cast(portal.ltype)]); + } else { + dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); + } + + int floor = portal.level; + if (portal.ltype == DTYPE_CATACOMBS) + floor -= 4; + else if (portal.ltype == DTYPE_CAVES) + floor -= 8; + else if (portal.ltype == DTYPE_HELL) + floor -= 12; + else if (portal.ltype == DTYPE_NEST) + floor -= 16; + else if (portal.ltype == DTYPE_CRYPT) + floor -= 20; + + if (floor > 0) + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name and {:d} is a floor number. */ "Town portal to {:s} {:d}")), dungeonStr, floor); + + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name. */ "Town portal to {:s}")), dungeonStr); +} + +std::vector CollectTownDungeonTriggerIndices() +{ + std::vector result; + result.reserve(static_cast(std::max(0, numtrigs))); + + for (int i = 0; i < numtrigs; ++i) { + if (IsAnyOf(trigs[i]._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP)) + result.push_back(i); + } + + std::sort(result.begin(), result.end(), [](int a, int b) { + const TriggerStruct &ta = trigs[a]; + const TriggerStruct &tb = trigs[b]; + + const int kindA = ta._tmsg == WM_DIABNEXTLVL ? 0 : (ta._tmsg == WM_DIABTOWNWARP ? 1 : 2); + const int kindB = tb._tmsg == WM_DIABNEXTLVL ? 0 : (tb._tmsg == WM_DIABTOWNWARP ? 1 : 2); + if (kindA != kindB) + return kindA < kindB; + + if (ta._tmsg == WM_DIABTOWNWARP && tb._tmsg == WM_DIABTOWNWARP && ta._tlvl != tb._tlvl) + return ta._tlvl < tb._tlvl; + + return a < b; + }); + + return result; +} + +void SpeakNearestExitKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (MyPlayer == nullptr) + return; + + const Point startPosition = MyPlayer->position.future; + + const SDL_Keymod modState = SDL_GetModState(); + const bool seekQuestEntrance = (modState & SDL_KMOD_SHIFT) != 0; + const bool cycleTownDungeon = (modState & SDL_KMOD_CTRL) != 0; + + if (seekQuestEntrance) { + if (setlevel) { + const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABRTNLVL); + if (!triggerIndex) { + SpeakText(_("No quest exits found."), true); + return; + } + + const TriggerStruct &trigger = trigs[*triggerIndex]; + const Point targetPosition { trigger.position.x, trigger.position.y }; + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + std::string message = TriggerLabelForSpeech(trigger); + if (!message.empty()) + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + SpeakText(message, true); + return; + } + + if (const std::optional entrance = FindNearestQuestSetLevelEntranceOnCurrentLevel(); entrance) { + const Point targetPosition = entrance->entrancePosition; + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + + std::string message { _(QuestLevelNames[entrance->questLevel]) }; + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + SpeakText(message, true); + return; + } + + SpeakText(_("No quest entrances found."), true); + return; + } + + if (leveltype == DTYPE_TOWN) { + const std::vector dungeonCandidates = CollectTownDungeonTriggerIndices(); + if (dungeonCandidates.empty()) { + SpeakText(_("No exits found."), true); + return; + } + + if (cycleTownDungeon) { + if (dungeonCandidates.size() <= 1) { + SpeakText(_("No other dungeon entrances found."), true); + return; + } + + const int current = LockedTownDungeonTriggerIndex.value_or(-1); + const std::optional next = FindNextTownDungeonTriggerIndex(dungeonCandidates, current); + if (!next) { + SpeakText(_("No other dungeon entrances found."), true); + return; + } + + LockedTownDungeonTriggerIndex = *next; + const std::string label = TriggerLabelForSpeech(trigs[*next]); + if (!label.empty()) + SpeakText(label, true); + return; + } + + const int triggerIndex = FindLockedTownDungeonTriggerIndex(dungeonCandidates) + .value_or(FindDefaultTownDungeonTriggerIndex(dungeonCandidates).value_or(dungeonCandidates.front())); + LockedTownDungeonTriggerIndex = triggerIndex; + + const TriggerStruct &trigger = trigs[triggerIndex]; + const Point targetPosition { trigger.position.x, trigger.position.y }; + + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + std::string message = TriggerLabelForSpeech(trigger); + if (!message.empty()) + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + + SpeakText(message, true); + return; + } + + if (const std::optional portalPosition = FindNearestTownPortalOnCurrentLevel(); portalPosition) { + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *portalPosition); + std::string message { _("Return to town") }; + message.append(": "); + if (!path) + AppendDirectionalFallback(message, *portalPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + SpeakText(message, true); + return; + } + + const std::optional triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABPREVLVL); + if (!triggerIndex) { + SpeakText(_("No exits found."), true); + return; + } + + const TriggerStruct &trigger = trigs[*triggerIndex]; + const Point targetPosition { trigger.position.x, trigger.position.y }; + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + std::string message = TriggerLabelForSpeech(trigger); + if (!message.empty()) + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + SpeakText(message, true); +} + +void SpeakNearestTownPortalInTownKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (leveltype != DTYPE_TOWN) { + SpeakText(_("Not in town."), true); + return; + } + if (MyPlayer == nullptr) + return; + + const std::optional portal = FindNearestTownPortalInTown(); + if (!portal) { + SpeakText(_("No town portals found."), true); + return; + } + + const Point startPosition = MyPlayer->position.future; + const Point targetPosition = portal->position; + + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + + std::string message = TownPortalLabelForSpeech(Portals[portal->portalIndex]); + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + + SpeakText(message, true); +} + +void SpeakNearestStairsDownKeyPressed() +{ + SpeakNearestStairsKeyPressed(WM_DIABNEXTLVL); +} + +void SpeakNearestStairsUpKeyPressed() +{ + SpeakNearestStairsKeyPressed(WM_DIABPREVLVL); +} + +bool IsKeyboardWalkAllowed() +{ + return CanPlayerTakeAction() + && !InGameMenu() + && !IsPlayerInStore() + && !QuestLogIsOpen + && !HelpFlag + && !ChatLogFlag + && !ChatFlag + && !DropGoldFlag + && !IsStashOpen + && !IsWithdrawGoldOpen + && !AutomapActive + && !invflag + && !CharFlag + && !SpellbookFlag + && !SpellSelectFlag + && !qtextflag; +} + +void KeyboardWalkNorthKeyPressed() +{ + KeyboardWalkKeyPressed(Direction::NorthEast); +} + +void KeyboardWalkSouthKeyPressed() +{ + KeyboardWalkKeyPressed(Direction::SouthWest); +} + +void KeyboardWalkEastKeyPressed() +{ + KeyboardWalkKeyPressed(Direction::SouthEast); +} + +void KeyboardWalkWestKeyPressed() +{ + KeyboardWalkKeyPressed(Direction::NorthWest); +} + +void SpeakNearestUnexploredTileKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (leveltype == DTYPE_TOWN) { + SpeakText(_("Not in a dungeon."), true); + return; + } + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (MyPlayer == nullptr) + return; + + const Point startPosition = MyPlayer->position.future; + const std::optional target = FindNearestUnexploredTile(startPosition); + if (!target) { + SpeakText(_("No unexplored areas found."), true); + return; + } + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *target); + std::string message; + if (!path) + AppendDirectionalFallback(message, *target - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + + SpeakText(message, true); +} + +} // namespace devilution diff --git a/Source/utils/navigation_speech.hpp b/Source/utils/navigation_speech.hpp new file mode 100644 index 000000000..c833671a0 --- /dev/null +++ b/Source/utils/navigation_speech.hpp @@ -0,0 +1,31 @@ +/** + * @file utils/navigation_speech.hpp + * + * Navigation speech: exit/stairs/portal/unexplored speech and keyboard walk keys. + */ +#pragma once + +#include +#include + +namespace devilution { + +struct TriggerStruct; +struct Portal; + +std::string TriggerLabelForSpeech(const TriggerStruct &trigger); +std::string TownPortalLabelForSpeech(const Portal &portal); +std::vector CollectTownDungeonTriggerIndices(); + +void SpeakNearestExitKeyPressed(); +void SpeakNearestTownPortalInTownKeyPressed(); +void SpeakNearestStairsDownKeyPressed(); +void SpeakNearestStairsUpKeyPressed(); +void KeyboardWalkNorthKeyPressed(); +void KeyboardWalkSouthKeyPressed(); +void KeyboardWalkEastKeyPressed(); +void KeyboardWalkWestKeyPressed(); +void SpeakNearestUnexploredTileKeyPressed(); +bool IsKeyboardWalkAllowed(); + +} // namespace devilution diff --git a/Source/utils/walk_path_speech.cpp b/Source/utils/walk_path_speech.cpp new file mode 100644 index 000000000..105c329d2 --- /dev/null +++ b/Source/utils/walk_path_speech.cpp @@ -0,0 +1,604 @@ +/** + * @file utils/walk_path_speech.cpp + * + * Walk-path helpers, PosOk variants, and BFS pathfinding for accessibility speech. + */ +#include "utils/walk_path_speech.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "engine/path.h" +#include "levels/gendung.h" +#include "levels/tile_properties.hpp" +#include "monster.h" +#include "objects.h" +#include "player.h" +#include "utils/language.h" +#include "utils/str_cat.hpp" + +namespace devilution { + +Point NextPositionForWalkDirection(Point position, int8_t walkDir) +{ + switch (walkDir) { + case WALK_NE: + return { position.x, position.y - 1 }; + case WALK_NW: + return { position.x - 1, position.y }; + case WALK_SE: + return { position.x + 1, position.y }; + case WALK_SW: + return { position.x, position.y + 1 }; + case WALK_N: + return { position.x - 1, position.y - 1 }; + case WALK_E: + return { position.x + 1, position.y - 1 }; + case WALK_S: + return { position.x + 1, position.y + 1 }; + case WALK_W: + return { position.x - 1, position.y + 1 }; + default: + return position; + } +} + +Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps) +{ + Point position = start; + for (int i = 0; i < steps; ++i) { + position = NextPositionForWalkDirection(position, path[i]); + } + return position; +} + +int8_t OppositeWalkDirection(int8_t walkDir) +{ + switch (walkDir) { + case WALK_NE: + return WALK_SW; + case WALK_SW: + return WALK_NE; + case WALK_NW: + return WALK_SE; + case WALK_SE: + return WALK_NW; + case WALK_N: + return WALK_S; + case WALK_S: + return WALK_N; + case WALK_E: + return WALK_W; + case WALK_W: + return WALK_E; + default: + return WALK_NONE; + } +} + +bool PosOkPlayerIgnoreDoors(const Player &player, Point position) +{ + if (!InDungeonBounds(position)) + return false; + if (!IsTileWalkable(position, /*ignoreDoors=*/true)) + return false; + + Player *otherPlayer = PlayerAtPosition(position); + if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) + return false; + + if (dMonster[position.x][position.y] != 0) { + if (leveltype == DTYPE_TOWN) + return false; + if (dMonster[position.x][position.y] <= 0) + return false; + if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife()) + return false; + } + + return true; +} + +bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables) +{ + Object *object = FindObjectAtPosition(position); + if (object != nullptr) { + if (ignoreDoors && object->isDoor()) { + return true; + } + if (ignoreBreakables && object->_oSolidFlag && object->IsBreakable()) { + return true; + } + if (object->_oSolidFlag) { + return false; + } + } + + return IsTileNotSolid(position); +} + +bool PosOkPlayerIgnoreMonsters(const Player &player, Point position) +{ + if (!InDungeonBounds(position)) + return false; + if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/false, /*ignoreBreakables=*/false)) + return false; + + Player *otherPlayer = PlayerAtPosition(position); + if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) + return false; + + return true; +} + +bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position) +{ + if (!InDungeonBounds(position)) + return false; + if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/false)) + return false; + + Player *otherPlayer = PlayerAtPosition(position); + if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) + return false; + + return true; +} + +bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position) +{ + if (!InDungeonBounds(position)) + return false; + if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/true)) + return false; + + Player *otherPlayer = PlayerAtPosition(position); + if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) + return false; + + return true; +} + +namespace { + +using PosOkForSpeechFn = bool (*)(const Player &, Point); + +template +std::optional> FindKeyboardWalkPathForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, bool allowDestinationNonWalkable) +{ + if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) + return std::nullopt; + + if (startPosition == destinationPosition) + return std::vector {}; + + std::array visited {}; + std::array parentDir {}; + parentDir.fill(WALK_NONE); + + std::queue queue; + + const auto indexOf = [](Point position) -> size_t { + return static_cast(position.x) + static_cast(position.y) * MAXDUNX; + }; + + const auto enqueue = [&](Point current, int8_t dir) { + const Point next = NextPositionForWalkDirection(current, dir); + if (!InDungeonBounds(next)) + return; + + const size_t idx = indexOf(next); + if (visited[idx]) + return; + + const bool ok = posOk(player, next); + if (ok) { + if (!CanStep(current, next)) + return; + } else { + if (!allowDestinationNonWalkable || next != destinationPosition) + return; + } + + visited[idx] = true; + parentDir[idx] = dir; + queue.push(next); + }; + + visited[indexOf(startPosition)] = true; + queue.push(startPosition); + + const auto hasReachedDestination = [&]() -> bool { + return visited[indexOf(destinationPosition)]; + }; + + while (!queue.empty() && !hasReachedDestination()) { + const Point current = queue.front(); + queue.pop(); + + const Displacement delta = destinationPosition - current; + const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX; + const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY; + + std::array prioritizedDirs; + size_t prioritizedCount = 0; + + const auto addUniqueDir = [&](int8_t dir) { + if (dir == WALK_NONE) + return; + for (size_t i = 0; i < prioritizedCount; ++i) { + if (prioritizedDirs[i] == dir) + return; + } + prioritizedDirs[prioritizedCount++] = dir; + }; + + const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE); + const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE); + + if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) { + const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N); + addUniqueDir(diagDir); + } + + if (deltaAbsX >= deltaAbsY) { + addUniqueDir(xDir); + addUniqueDir(yDir); + } else { + addUniqueDir(yDir); + addUniqueDir(xDir); + } + for (const int8_t dir : walkDirections) { + addUniqueDir(dir); + } + + for (size_t i = 0; i < prioritizedCount; ++i) { + enqueue(current, prioritizedDirs[i]); + } + } + + if (!hasReachedDestination()) + return std::nullopt; + + std::vector path; + Point position = destinationPosition; + while (position != startPosition) { + const int8_t dir = parentDir[indexOf(position)]; + if (dir == WALK_NONE) + return std::nullopt; + + path.push_back(dir); + position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir)); + } + + std::reverse(path.begin(), path.end()); + return path; +} + +std::optional> FindKeyboardWalkPathForSpeechWithPosOk(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, bool allowDestinationNonWalkable) +{ + constexpr std::array AxisDirections = { + WALK_NE, + WALK_SW, + WALK_SE, + WALK_NW, + }; + + constexpr std::array AllDirections = { + WALK_NE, + WALK_SW, + WALK_SE, + WALK_NW, + WALK_N, + WALK_E, + WALK_S, + WALK_W, + }; + + if (const std::optional> axisPath = FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AxisDirections, /*allowDiagonalSteps=*/false, allowDestinationNonWalkable); axisPath) { + return axisPath; + } + + return FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AllDirections, /*allowDiagonalSteps=*/true, allowDestinationNonWalkable); +} + +template +std::optional> FindKeyboardWalkPathToClosestReachableForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, Point &closestPosition) +{ + if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) + return std::nullopt; + + if (startPosition == destinationPosition) { + closestPosition = destinationPosition; + return std::vector {}; + } + + std::array visited {}; + std::array parentDir {}; + std::array depth {}; + parentDir.fill(WALK_NONE); + depth.fill(0); + + std::queue queue; + + const auto indexOf = [](Point position) -> size_t { + return static_cast(position.x) + static_cast(position.y) * MAXDUNX; + }; + + const auto enqueue = [&](Point current, int8_t dir) { + const Point next = NextPositionForWalkDirection(current, dir); + if (!InDungeonBounds(next)) + return; + + const size_t nextIdx = indexOf(next); + if (visited[nextIdx]) + return; + + if (!posOk(player, next)) + return; + if (!CanStep(current, next)) + return; + + const size_t currentIdx = indexOf(current); + visited[nextIdx] = true; + parentDir[nextIdx] = dir; + depth[nextIdx] = static_cast(depth[currentIdx] + 1); + queue.push(next); + }; + + const size_t startIdx = indexOf(startPosition); + visited[startIdx] = true; + queue.push(startPosition); + + Point best = startPosition; + int bestDistance = startPosition.WalkingDistance(destinationPosition); + uint16_t bestDepth = 0; + + const auto considerBest = [&](Point position) { + const int distance = position.WalkingDistance(destinationPosition); + const uint16_t posDepth = depth[indexOf(position)]; + if (distance < bestDistance || (distance == bestDistance && posDepth < bestDepth)) { + best = position; + bestDistance = distance; + bestDepth = posDepth; + } + }; + + while (!queue.empty()) { + const Point current = queue.front(); + queue.pop(); + + considerBest(current); + + const Displacement delta = destinationPosition - current; + const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX; + const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY; + + std::array prioritizedDirs; + size_t prioritizedCount = 0; + + const auto addUniqueDir = [&](int8_t dir) { + if (dir == WALK_NONE) + return; + for (size_t i = 0; i < prioritizedCount; ++i) { + if (prioritizedDirs[i] == dir) + return; + } + prioritizedDirs[prioritizedCount++] = dir; + }; + + const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE); + const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE); + + if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) { + const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N); + addUniqueDir(diagDir); + } + + if (deltaAbsX >= deltaAbsY) { + addUniqueDir(xDir); + addUniqueDir(yDir); + } else { + addUniqueDir(yDir); + addUniqueDir(xDir); + } + for (const int8_t dir : walkDirections) { + addUniqueDir(dir); + } + + for (size_t i = 0; i < prioritizedCount; ++i) { + enqueue(current, prioritizedDirs[i]); + } + } + + closestPosition = best; + if (best == startPosition) + return std::vector {}; + + std::vector path; + Point position = best; + while (position != startPosition) { + const int8_t dir = parentDir[indexOf(position)]; + if (dir == WALK_NONE) + return std::nullopt; + + path.push_back(dir); + position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir)); + } + + std::reverse(path.begin(), path.end()); + return path; +} + +} // namespace + +std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) +{ + return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, allowDestinationNonWalkable); +} + +std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) +{ + return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayer, allowDestinationNonWalkable); +} + +std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) +{ + return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsAndMonsters, allowDestinationNonWalkable); +} + +std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) +{ + return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreMonsters, allowDestinationNonWalkable); +} + +std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) +{ + return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsMonstersAndBreakables, allowDestinationNonWalkable); +} + +std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition) +{ + constexpr std::array AxisDirections = { + WALK_NE, + WALK_SW, + WALK_SE, + WALK_NW, + }; + + constexpr std::array AllDirections = { + WALK_NE, + WALK_SW, + WALK_SE, + WALK_NW, + WALK_N, + WALK_E, + WALK_S, + WALK_W, + }; + + Point axisClosest; + const std::optional> axisPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AxisDirections, /*allowDiagonalSteps=*/false, axisClosest); + + Point diagClosest; + const std::optional> diagPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AllDirections, /*allowDiagonalSteps=*/true, diagClosest); + + if (!axisPath && !diagPath) + return std::nullopt; + if (!axisPath) { + closestPosition = diagClosest; + return diagPath; + } + if (!diagPath) { + closestPosition = axisClosest; + return axisPath; + } + + const int axisDistance = axisClosest.WalkingDistance(destinationPosition); + const int diagDistance = diagClosest.WalkingDistance(destinationPosition); + if (diagDistance < axisDistance) { + closestPosition = diagClosest; + return diagPath; + } + + closestPosition = axisClosest; + return axisPath; +} + +void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path) +{ + if (path.empty()) { + message.append(_("here")); + return; + } + + bool any = false; + const auto appendPart = [&](std::string_view label, int distance) { + if (distance == 0) + return; + if (any) + message.append(", "); + StrAppend(message, label, " ", distance); + any = true; + }; + + const auto labelForWalkDirection = [](int8_t dir) -> std::string_view { + switch (dir) { + case WALK_NE: + return _("north"); + case WALK_SW: + return _("south"); + case WALK_SE: + return _("east"); + case WALK_NW: + return _("west"); + case WALK_N: + return _("northwest"); + case WALK_E: + return _("northeast"); + case WALK_S: + return _("southeast"); + case WALK_W: + return _("southwest"); + default: + return {}; + } + }; + + int8_t currentDir = path.front(); + int runLength = 1; + for (size_t i = 1; i < path.size(); ++i) { + if (path[i] == currentDir) { + ++runLength; + continue; + } + + const std::string_view label = labelForWalkDirection(currentDir); + if (!label.empty()) + appendPart(label, runLength); + + currentDir = path[i]; + runLength = 1; + } + + const std::string_view label = labelForWalkDirection(currentDir); + if (!label.empty()) + appendPart(label, runLength); + + if (!any) + message.append(_("here")); +} + +void AppendDirectionalFallback(std::string &message, const Displacement &delta) +{ + bool any = false; + const auto appendPart = [&](std::string_view label, int distance) { + if (distance == 0) + return; + if (any) + message.append(", "); + StrAppend(message, label, " ", distance); + any = true; + }; + + if (delta.deltaY < 0) + appendPart(_("north"), -delta.deltaY); + else if (delta.deltaY > 0) + appendPart(_("south"), delta.deltaY); + + if (delta.deltaX > 0) + appendPart(_("east"), delta.deltaX); + else if (delta.deltaX < 0) + appendPart(_("west"), -delta.deltaX); + + if (!any) + message.append(_("here")); +} + +} // namespace devilution diff --git a/Source/utils/walk_path_speech.hpp b/Source/utils/walk_path_speech.hpp new file mode 100644 index 000000000..1188c946c --- /dev/null +++ b/Source/utils/walk_path_speech.hpp @@ -0,0 +1,44 @@ +/** + * @file utils/walk_path_speech.hpp + * + * Walk-path helpers, PosOk variants, and BFS pathfinding for accessibility speech. + */ +#pragma once + +#include +#include +#include +#include + +#include "engine/displacement.hpp" +#include "engine/point.hpp" + +namespace devilution { + +struct Player; + +// Walk direction helpers +Point NextPositionForWalkDirection(Point position, int8_t walkDir); +Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps); +int8_t OppositeWalkDirection(int8_t walkDir); + +// PosOk variants for pathfinding +bool PosOkPlayerIgnoreDoors(const Player &player, Point position); +bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables); +bool PosOkPlayerIgnoreMonsters(const Player &player, Point position); +bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position); +bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position); + +// BFS pathfinding for speech +std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition); + +// Speech formatting +void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path); +void AppendDirectionalFallback(std::string &message, const Displacement &delta); + +} // namespace devilution