Browse Source

Merge f62f809264 into 5a08031caf

pull/8486/merge
Yuri Pourre 4 days ago committed by GitHub
parent
commit
c7c8d97c95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Source/CMakeLists.txt
  2. 1327
      Source/accessibility/location_speech.cpp
  3. 49
      Source/accessibility/location_speech.hpp
  4. 490
      Source/accessibility/speech.cpp
  5. 17
      Source/accessibility/speech.hpp
  6. 358
      Source/accessibility/town_navigation.cpp
  7. 15
      Source/accessibility/town_navigation.hpp
  8. 1830
      Source/accessibility/tracker.cpp
  9. 39
      Source/accessibility/tracker.hpp
  10. 17
      Source/diablo.cpp
  11. 2
      Source/diablo.h
  12. 2
      Source/engine/sound_position.hpp

4
Source/CMakeLists.txt

@ -3,6 +3,10 @@ include(functions/devilutionx_library)
include(functions/genex)
set(libdevilutionx_SRCS
accessibility/location_speech.cpp
accessibility/speech.cpp
accessibility/town_navigation.cpp
accessibility/tracker.cpp
appfat.cpp
automap.cpp
capture.cpp

1327
Source/accessibility/location_speech.cpp

File diff suppressed because it is too large Load Diff

49
Source/accessibility/location_speech.hpp

@ -0,0 +1,49 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "engine/displacement.hpp"
#include "engine/point.hpp"
namespace devilution {
struct Player;
// Path-finding utilities for speech/navigation (BFS over the dungeon grid).
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition);
void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector<int8_t> &path);
void AppendDirectionalFallback(std::string &message, const Displacement &delta);
// Key handler that speaks the nearest unexplored tile direction.
void SpeakNearestUnexploredTileKeyPressed();
[[nodiscard]] std::string BuildCurrentLocationForSpeech();
// Key handlers for player status announcements.
void SpeakPlayerHealthPercentageKeyPressed();
void SpeakExperienceToNextLevelKeyPressed();
void SpeakCurrentLocationKeyPressed();
// Key handlers for navigation.
void SpeakNearestExitKeyPressed();
void SpeakNearestTownPortalInTownKeyPressed();
void SpeakNearestStairsDownKeyPressed();
void SpeakNearestStairsUpKeyPressed();
// Keyboard directional walk.
bool IsKeyboardWalkAllowed();
void KeyboardWalkNorthKeyPressed();
void KeyboardWalkSouthKeyPressed();
void KeyboardWalkEastKeyPressed();
void KeyboardWalkWestKeyPressed();
} // namespace devilution

490
Source/accessibility/speech.cpp

@ -0,0 +1,490 @@
#include "accessibility/speech.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#ifdef USE_SDL3
#include <SDL3/SDL_timer.h>
#else
#include <SDL.h>
#endif
#if !defined(USE_SDL3) && !defined(NOSOUND)
#include <Aulib/Stream.h>
#endif
#include <fmt/format.h>
#include "controls/plrctrls.h"
#include "engine/sound.h"
#include "inv.h"
#include "options.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/screen_reader.hpp"
#include "utils/str_cat.hpp"
namespace devilution {
#ifdef NOSOUND
void UpdatePlayerLowHpWarningSound()
{
}
#else
namespace {
std::unique_ptr<TSnd> PlayerLowHpWarningSound;
bool TriedLoadingPlayerLowHpWarningSound = false;
TSnd *GetPlayerLowHpWarningSound()
{
if (TriedLoadingPlayerLowHpWarningSound)
return PlayerLowHpWarningSound.get();
TriedLoadingPlayerLowHpWarningSound = true;
if (!gbSndInited)
return nullptr;
PlayerLowHpWarningSound = std::make_unique<TSnd>();
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, *GetOptions().Audio.soundVolume);
LastWarningStartMs = now;
}
#endif // NOSOUND
namespace {
[[nodiscard]] bool IsBossMonsterForHpAnnouncement(const Monster &monster)
{
return monster.isUnique() || monster.ai == MonsterAIID::Diablo;
}
} // namespace
void UpdateLowDurabilityWarnings()
{
static std::array<uint32_t, NUM_INVLOC> WarnedSeeds {};
static std::array<bool, NUM_INVLOC> HasWarned {};
if (MyPlayer == nullptr)
return;
if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife())
return;
std::vector<std::string> 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<int8_t, MaxMonsters> 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<int>(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<int64_t>(monster.hitPoints, 0, monster.maxHitPoints);
const int64_t maxHp = monster.maxHitPoints;
const int hpPercent = static_cast<int>(std::clamp<int64_t>(hp * 100 / maxHp, 0, 100));
const int bucket = ((hpPercent + 9) / 10) * 10;
int8_t &lastBucket = LastAnnouncedBucket[monsterId];
if (lastBucket < 0) {
lastBucket = static_cast<int8_t>(((hpPercent + 9) / 10) * 10);
continue;
}
if (bucket >= lastBucket)
continue;
lastBucket = static_cast<int8_t>(bucket);
SpeakText(fmt::format(fmt::runtime(_("{:s} health: {:d}%")), monster.name(), bucket), /*force=*/false);
}
}
void UpdateAttackableMonsterAnnouncements()
{
static std::optional<int> 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<int> bestId;
for (size_t i = 0; i < ActiveMonsterCount; i++) {
const int monsterId = static_cast<int>(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<int>(player._pdir);
const int d2 = static_cast<int>(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();
// Door state values are defined in `Source/objects.cpp` (DOOR_CLOSED=0, DOOR_OPEN=1, DOOR_BLOCKED=2).
constexpr int DoorClosed = 0;
constexpr int DoorOpen = 1;
constexpr int DoorBlocked = 2;
// 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 == DoorOpen)
return _("Open Grate Door");
if (door._oVar4 == DoorClosed)
return _("Closed Grate Door");
if (door._oVar4 == DoorBlocked)
return _("Blocked Grate Door");
return _("Grate Door");
}
return door.name();
}
void UpdateInteractableDoorAnnouncements()
{
static std::optional<int> LastInteractableDoorId;
static std::optional<int> 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<int> 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<int>(player._pdir);
const int d2 = static_cast<int>(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

17
Source/accessibility/speech.hpp

@ -0,0 +1,17 @@
#pragma once
#include "utils/string_or_view.hpp"
namespace devilution {
struct Object;
[[nodiscard]] StringOrView DoorLabelForSpeech(const Object &door);
void UpdatePlayerLowHpWarningSound();
void UpdateLowDurabilityWarnings();
void UpdateBossHealthAnnouncements();
void UpdateAttackableMonsterAnnouncements();
void UpdateInteractableDoorAnnouncements();
} // namespace devilution

358
Source/accessibility/town_navigation.cpp

@ -0,0 +1,358 @@
#include "accessibility/town_navigation.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <vector>
#include <fmt/format.h>
#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"
namespace devilution {
namespace {
std::vector<int> 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;
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<int>(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<int>(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<int>(it - TownNpcOrder.begin()) : 0;
const int size = static_cast<int>(TownNpcOrder.size());
int newIndex = (currentIndex + delta) % size;
if (newIndex < 0)
newIndex += size;
SelectedTownNpc = TownNpcOrder[static_cast<size_t>(newIndex)];
SpeakSelectedTownNpc();
}
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;
}
} // namespace
bool IsTownNpcActionAllowed()
{
return CanPlayerTakeAction()
&& leveltype == DTYPE_TOWN
&& !IsPlayerInStore()
&& !ChatLogFlag
&& !HelpFlag;
}
void SpeakSelectedTownNpc()
{
EnsureTownNpcOrder();
if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast<int>(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 CancelTownNpcAutoWalk()
{
AutoWalkTownNpcTarget = -1;
}
void SelectNextTownNpcKeyPressed()
{
SelectTownNpcRelative(+1);
}
void SelectPreviousTownNpcKeyPressed()
{
SelectTownNpcRelative(-1);
}
void GoToSelectedTownNpcKeyPressed()
{
if (!IsTownNpcActionAllowed())
return;
EnsureTownNpcOrder();
if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast<int>(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<int>(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<uint16_t>(townerIdx));
return;
}
constexpr size_t MaxAutoWalkPathLength = 512;
std::array<int8_t, MaxAutoWalkPathLength> 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<int>(MaxPathLengthPlayer)) {
const int townerIdx = AutoWalkTownNpcTarget;
AutoWalkTownNpcTarget = -1;
NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast<uint16_t>(townerIdx));
return;
}
const int segmentSteps = std::min(steps - 1, static_cast<int>(MaxPathLengthPlayer - 1));
const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps);
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint);
}
void ListTownNpcsKeyPressed()
{
if (leveltype != DTYPE_TOWN) {
ResetTownNpcSelection();
SpeakText(_("Not in town."), true);
return;
}
if (IsPlayerInStore())
return;
std::vector<const Towner *> townNpcs;
std::vector<const Towner *> 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<int>(cows.size()));
}
RefreshTownNpcOrder(true);
if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast<int>(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

15
Source/accessibility/town_navigation.hpp

@ -0,0 +1,15 @@
#pragma once
namespace devilution {
bool IsTownNpcActionAllowed();
void SpeakSelectedTownNpc();
void SelectNextTownNpcKeyPressed();
void SelectPreviousTownNpcKeyPressed();
void GoToSelectedTownNpcKeyPressed();
void ListTownNpcsKeyPressed();
void UpdateAutoWalkTownNpc();
void CancelTownNpcAutoWalk();
} // namespace devilution

1830
Source/accessibility/tracker.cpp

File diff suppressed because it is too large Load Diff

39
Source/accessibility/tracker.hpp

@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include "engine/point.hpp"
namespace devilution {
struct Player;
enum class TrackerTargetCategory : uint8_t {
Items,
Chests,
Doors,
Shrines,
Objects,
Breakables,
Monsters,
DeadBodies,
};
extern TrackerTargetCategory SelectedTrackerTargetCategory;
extern TrackerTargetCategory AutoWalkTrackerTargetCategory;
extern int AutoWalkTrackerTargetId;
// Position-check predicates used by both tracker and location_speech path-finding.
bool PosOkPlayerIgnoreDoors(const Player &player, Point position);
bool PosOkPlayerIgnoreMonsters(const Player &player, Point position);
bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position);
bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position);
void CycleTrackerTargetKeyPressed();
void NavigateToTrackerTargetKeyPressed();
void AutoWalkToTrackerTargetKeyPressed();
void UpdateAutoWalkTracker();
} // namespace devilution

17
Source/diablo.cpp

@ -24,6 +24,10 @@
#include <config.h>
#include "DiabloUI/selstart.h"
#include "accessibility/location_speech.hpp"
#include "accessibility/speech.hpp"
#include "accessibility/town_navigation.hpp"
#include "accessibility/tracker.hpp"
#include "appfat.h"
#include "automap.h"
#include "capture.h"
@ -1770,7 +1774,7 @@ bool IsGameRunning()
return PauseMode != 2;
}
bool CanPlayerTakeAction()
bool CanPlayerTakeActionImpl()
{
return !IsPlayerDead() && IsGameRunning();
}
@ -1802,6 +1806,17 @@ const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueCha
} // namespace
bool CanPlayerTakeAction()
{
return CanPlayerTakeActionImpl();
}
void CancelAutoWalk()
{
CancelTownNpcAutoWalk();
AutoWalkTrackerTargetId = -1;
}
void InitKeymapActions()
{
Options &options = GetOptions();

2
Source/diablo.h

@ -102,6 +102,8 @@ void DisableInputEventHandler(const SDL_Event &event, uint16_t modState);
tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir);
bool IsDiabloAlive(bool playSFX);
void PrintScreen(SDL_Keycode vkey);
bool CanPlayerTakeAction();
void CancelAutoWalk();
/**
* @param bStartup Process additional ticks before returning

2
Source/engine/sound_position.hpp

@ -6,4 +6,4 @@ namespace devilution {
bool CalculateSoundPosition(Point soundPosition, int *plVolume, int *plPan);
} // namespace devilution
} // namespace devilution
Loading…
Cancel
Save