Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
42c9067
Fix Polish translations by removing duplicate entries
AJenbo Jan 26, 2026
857e4fb
Merge pull request #2 from AJenbo/patch-1
mojsior Jan 27, 2026
91caa77
Add Ctrl+E town dungeon selection
mojsior Jan 27, 2026
61908a6
Add town portal navigation hotkey (P)
mojsior Jan 27, 2026
1aebd94
Fix keymapping migration for town portal hotkey
mojsior Jan 27, 2026
7aab154
Update README keybinds (E/Ctrl+E, P)
mojsior Jan 27, 2026
9e62d39
access: tracker quest exit, Shift+T, door types
mojsior Jan 28, 2026
4fab16d
Normalize line endings to CRLF in diablo.cpp and plrctrls.cpp
ATGillespie25 Jan 29, 2026
e160626
Add auto-walk to tracker target (M key)
ATGillespie25 Jan 29, 2026
6b38cdb
Fix auto-walk tracker bugs and improve UX
ATGillespie25 Jan 29, 2026
233386b
Fix auto-walk cancellation not stopping player movement
ATGillespie25 Jan 29, 2026
e6f0c9a
Merge pull request #7 from ATGillespie25/fix/auto-walk-tracker
mojsior Jan 29, 2026
f74b27e
access: town item tracking, item cues in town, dead bodies tracker
mojsior Jan 29, 2026
c5c8752
docs: update tracker keybind notes
mojsior Jan 29, 2026
e31ece4
update auto-walk in dungeons to detect closed doors
ATGillespie25 Jan 30, 2026
88b4338
fix archway door position in auto-walk path detection
ATGillespie25 Jan 30, 2026
8f20d7e
access: fix inventory navigation for multi-tile items, add position s…
ChipsAhoiMcCoy Jan 31, 2026
b84ae82
access: improve belt navigation for accessibility
ChipsAhoiMcCoy Jan 31, 2026
b49c22a
access: simplify belt navigation return behavior
ChipsAhoiMcCoy Jan 31, 2026
4155a6e
Merge pull request #3 from ChipsAhoiMcCoy/inventory-fixes-and-tweaks
mojsior Jan 31, 2026
4edfe9e
Merge pull request #2 from ATGillespie25/fix/auto-walk-door-detection
mojsior Jan 31, 2026
89f8d5e
access: extract accessibility subsystems into dedicated modules
ATGillespie25 Feb 2, 2026
0ea5705
access: wire extracted modules into diablo.cpp and build system
ATGillespie25 Feb 2, 2026
e55ce90
access: fix incorrect and misleading comments
ATGillespie25 Feb 2, 2026
9799f41
access: move SelectedTrackerTargetCategory to anonymous namespace
ATGillespie25 Feb 2, 2026
4db589c
access: add null-safety checks for MyPlayer in accessibility code
ATGillespie25 Feb 2, 2026
91e4ab4
access: log warning when low HP sound fails to load
ATGillespie25 Feb 2, 2026
9d3ecd7
access: add null-safety check for MyPlayer in UpdateAutoWalkTownNpc
ATGillespie25 Feb 2, 2026
cd9b828
access: provide spoken feedback on null-player early returns in town …
ATGillespie25 Feb 2, 2026
02389df
tools: add build_release.ps1
mojsior Feb 2, 2026
3382c0e
access: tracker exits + stairs speech fix
mojsior Feb 2, 2026
ff82e24
access: speak class + difficulty descriptions
mojsior Feb 2, 2026
ad0e396
docs: update tracker keybinds
mojsior Feb 2, 2026
d1aeb39
access: speak class + difficulty descriptions
ATGillespie25 Feb 2, 2026
f7da793
access: export navigation speech helpers for use by tracker
ATGillespie25 Feb 2, 2026
4a2eef7
access: add 6 new tracker categories and unified cycling infrastructure
ATGillespie25 Feb 2, 2026
63b36db
access: speak mana percentage with Shift+health key
ATGillespie25 Feb 2, 2026
0750821
access: overhaul tracker keybindings in InitKeymapActions
ATGillespie25 Feb 2, 2026
25e0a18
Merge upstream/master, resolve diablo.cpp conflicts with our extracte…
ATGillespie25 Feb 2, 2026
facef18
Merge pull request #4 from ATGillespie25/refactor/seperate-accessibil…
mojsior Feb 4, 2026
9b69503
access: remove old town NPC tracker code (fixes #7)
ATGillespie25 Feb 4, 2026
d349948
access: detect alternative dungeon exits in tracker (fixes #6)
ATGillespie25 Feb 5, 2026
065bf6f
Merge pull request #9 from ATGillespie25/fix/remove-old-town-npc-tracker
mojsior Feb 5, 2026
cd4db81
Merge pull request #10 from ATGillespie25/fix/alternative-dungeon-exits
mojsior Feb 5, 2026
9b75e9b
access: tracker: increase range + fix empty list speech
mojsior Feb 5, 2026
f0523cc
access: tracker selection lists all targets
mojsior Feb 5, 2026
fc45320
feat(android): Implement native accessibility support with TalkBack i…
azurejoga Feb 5, 2026
7ee96e4
feat(android): Implement native accessibility support with TalkBack i…
azurejoga Feb 5, 2026
e4431d5
Merge branch 'diablo-access-testing' of https://github.com/mojsior/di…
azurejoga Feb 5, 2026
2fb4bab
exclud retroarch
azurejoga Feb 5, 2026
eff6150
access: unique stats, item quality, Lazarus exit, stash focus
mojsior Feb 9, 2026
4655359
access: keyboard stand ground
mojsior Feb 9, 2026
62be784
access: speak monster type
mojsior Feb 9, 2026
7d515f8
access: item usability and weapon speed
mojsior Feb 9, 2026
e7712fc
access: fix stash arrow-key navigation
mojsior Feb 9, 2026
4ba0743
access: fix Tab stash focus switching
mojsior Feb 9, 2026
2408e2d
access: migrate TAB binding for stash focus
mojsior Feb 9, 2026
76f7196
access: toggle stash focus based on cursor
mojsior Feb 9, 2026
2713cea
access: pl translation for stash focus
mojsior Feb 9, 2026
efc88a7
access: keep arrow navigation within stash
mojsior Feb 9, 2026
d6bfc89
access: speak stash items on arrow navigation
mojsior Feb 9, 2026
2965149
Accessibility: add Lazarus object tracking and audio/speech toggles
mojsior Feb 17, 2026
e4b5df0
Merge branch 'access' into release
AJenbo Feb 17, 2026
5fc3711
Accessibility fixes for spells/stash/tracker; release 1.4.0
mojsior Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ comparer-config.toml

#ignore cmake cache
/build-*/
/build/releases/
.vscode/tasks.json

# Extra files in the source distribution (see make_src_dist.py)
Expand Down
2 changes: 2 additions & 0 deletions CMake/Dependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ add_subdirectory(3rdParty/sol2)
if(SCREEN_READER_INTEGRATION)
if(WIN32)
add_subdirectory(3rdParty/tolk)
elseif(ANDROID)
# Android uses native accessibility API, no external dependency needed
else()
find_package(Speechd REQUIRED)
endif()
Expand Down
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@ Keybinds are configurable, but these are the defaults most players will use:

## Navigation / tracking

- `T` - cycle tracker target (items / chests / doors / shrines / objects / breakables / monsters).
- `N` - tracker directions to the nearest target (speaks target name + directions).
- `Shift`+`N` - cycle to the next target in the current tracker category (speaks name only; duplicates get ordinal numbers).
- `Ctrl`+`N` - clear the tracker target.
- `Ctrl`+`PageUp` / `Ctrl`+`PageDown` - previous / next tracker category.
- `PageUp` / `PageDown` - select previous / next target in the current category (speaks name; duplicates get ordinal numbers).
- `Home` - tracker directions to the selected target (speaks target name + directions).
- `Ctrl`+`Home` - clear the selected tracker target.
- `Shift`+`Home` - auto-walk to the selected target (press again to cancel).
- `H` - speak nearest unexplored space.
- `E` - speak nearest exit (hold `Shift` for quest entrances).
- `,` - speak nearest stairs up.
- `.` - speak nearest stairs down.
- `L` - speak current dungeon + floor.
- `Z` - speak player health percentage.
- `Z` - speak player health percentage (hold `Shift` for mana).
- `X` - speak experience remaining to next level.

## Keyboard controls
Expand All @@ -41,10 +39,7 @@ Keybinds are configurable, but these are the defaults most players will use:

## Town NPCs

- `F4` - list town NPCs.
- `PageUp` / `PageDown` - select previous / next town NPC.
- `End` - speak selected town NPC.
- `Home` - walk to selected town NPC (town only).
Town NPCs are available via the tracker category list.

# How to Install

Expand Down
55 changes: 31 additions & 24 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +52,7 @@ set(libdevilutionx_SRCS
controls/menu_controls.cpp
controls/modifier_hints.cpp
controls/plrctrls.cpp
controls/tracker.cpp

DiabloUI/button.cpp
DiabloUI/credits.cpp
Expand Down Expand Up @@ -160,13 +162,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.
Expand Down Expand Up @@ -739,31 +744,31 @@ 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
libdevilutionx_options
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()
Expand Down Expand Up @@ -996,6 +1001,8 @@ if(SCREEN_READER_INTEGRATION)
if(WIN32)
target_compile_definitions(libdevilutionx PRIVATE Tolk)
target_link_libraries(libdevilutionx PUBLIC Tolk)
elseif(ANDROID)
# Android uses native accessibility API, no external library needed
else()
target_include_directories(libdevilutionx PUBLIC ${Speechd_INCLUDE_DIRS})
target_link_libraries(libdevilutionx PUBLIC speechd)
Expand Down
93 changes: 53 additions & 40 deletions Source/DiabloUI/diabloui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,16 @@ void (*gfnFullscreen)();
bool (*gfnListYesNo)();
std::vector<UiItemBase *> gUiItems;
UiList *gUiList = nullptr;
bool UiItemsWraps;

std::optional<TextInputState> UiTextInputState;
bool allowEmptyTextInput = false;

constexpr Uint32 ListDoubleClickTimeMs = 500;
std::size_t lastListClickIndex = static_cast<std::size_t>(-1);
Uint32 lastListClickTicks = 0;
bool UiItemsWraps;

std::optional<TextInputState> UiTextInputState;
bool allowEmptyTextInput = false;

std::optional<std::string> UiSpokenTextOverride;

constexpr Uint32 ListDoubleClickTimeMs = 500;
std::size_t lastListClickIndex = static_cast<std::size_t>(-1);
Uint32 lastListClickTicks = 0;

struct ScrollBarState {
bool upArrowPressed;
Expand Down Expand Up @@ -155,14 +157,20 @@ std::string FormatSpokenText(const StringOrView &format, const std::vector<DrawS
return formatted;
}

void SpeakListItem(std::size_t index, bool force = false)
{
if (gUiList == nullptr || index > SelectedItemMax)
return;

const UiListItem *pItem = gUiList->GetItem(index);
if (pItem == nullptr)
return;
void SpeakListItem(std::size_t index, bool force = false)
{
if (gUiList == nullptr || index > SelectedItemMax)
return;

if (UiSpokenTextOverride) {
SpeakText(*UiSpokenTextOverride, force);
UiSpokenTextOverride = std::nullopt;
return;
}

const UiListItem *pItem = gUiList->GetItem(index);
if (pItem == nullptr)
return;

std::string text = FormatSpokenText(pItem->m_text, pItem->args);

Expand All @@ -180,10 +188,10 @@ void SpeakListItem(std::size_t index, bool force = false)

if (!text.empty())
SpeakText(text, force);
}
void AdjustListOffset(std::size_t itemIndex)
{
}

void AdjustListOffset(std::size_t itemIndex)
{
if (itemIndex >= listOffset + ListViewportSize)
listOffset = itemIndex - (ListViewportSize - 1);
if (itemIndex < listOffset)
Expand Down Expand Up @@ -232,13 +240,18 @@ void UiUpdateFadePalette()
SystemPaletteUpdated();
if (IsHardwareCursor()) ReinitializeHardwareCursor();
}

} // namespace

bool IsTextInputActive()
{
return UiTextInputState.has_value();
}

} // namespace

void UiSetSpokenTextOverride(std::string text)
{
UiSpokenTextOverride = std::move(text);
}

bool IsTextInputActive()
{
return UiTextInputState.has_value();
}

void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector<std::unique_ptr<UiItemBase>> &items, bool itemsWraps, void (*fnFullscreen)(), bool (*fnYesNo)(), size_t selectedItem /*= 0*/)
{
Expand Down Expand Up @@ -365,19 +378,19 @@ void UiFocus(std::size_t itemIndex, bool checkUp, bool ignoreItemsWraps = false)
}
pItem = gUiList->GetItem(itemIndex);
}
SpeakListItem(itemIndex);
if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement))
AdjustListOffset(itemIndex + 1);
AdjustListOffset(itemIndex);
SelectedItem = itemIndex;
UiPlayMoveSound();
if (gfnListFocus != nullptr)
gfnListFocus(itemIndex);
}
if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement))
AdjustListOffset(itemIndex + 1);
AdjustListOffset(itemIndex);

SelectedItem = itemIndex;

UiPlayMoveSound();

if (gfnListFocus != nullptr)
gfnListFocus(itemIndex);

SpeakListItem(itemIndex);
}

void UiFocusUp()
{
Expand Down
39 changes: 22 additions & 17 deletions Source/DiabloUI/diabloui.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>

#ifdef USE_SDL3
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_surface.h>
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>

#ifdef USE_SDL3
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_surface.h>
#else
#include <SDL.h>
#endif
Expand Down Expand Up @@ -109,13 +111,16 @@ bool UiLoadBlackBackground();
void LoadBackgroundArt(const char *pszFile, int frames = 1);
void UiAddBackground(std::vector<std::unique_ptr<UiItemBase>> *vecDialog);
void UiAddLogo(std::vector<std::unique_ptr<UiItemBase>> *vecDialog, int y = GetUIRectangle().position.y);
void UiFocusNavigationSelect();
void UiFocusNavigationEsc();
void UiFocusNavigationYesNo();

void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector<std::unique_ptr<UiItemBase>> &items, bool wraps = false, void (*fnFullscreen)() = nullptr, bool (*fnYesNo)() = nullptr, size_t selectedItem = 0);
void UiRenderListItems();
void UiInitList_clear();
void UiFocusNavigationSelect();
void UiFocusNavigationEsc();
void UiFocusNavigationYesNo();

/** Overrides what the screen reader will speak for the next focused list item. */
void UiSetSpokenTextOverride(std::string text);

void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector<std::unique_ptr<UiItemBase>> &items, bool wraps = false, void (*fnFullscreen)() = nullptr, bool (*fnYesNo)() = nullptr, size_t selectedItem = 0);
void UiRenderListItems();
void UiInitList_clear();

void UiClearScreen();
void UiPollAndRender(std::optional<tl::function_ref<bool(SDL_Event &)>> eventHandler = std::nullopt);
Expand Down
Loading