Browse Source

Merge branch 'master' into fix-delta-items

pull/8341/head
Yuri Pourre 2 months ago committed by GitHub
parent
commit
6ec805cea9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/3ds.yml
  2. 4
      .github/workflows/Android.yml
  3. 6
      .github/workflows/Linux_aarch64.yml
  4. 6
      .github/workflows/Linux_x86.yml
  5. 6
      .github/workflows/Linux_x86_64.yml
  6. 4
      .github/workflows/Linux_x86_64_SDL1.yml
  7. 4
      .github/workflows/Linux_x86_64_SDL3_test.yml
  8. 2
      .github/workflows/Linux_x86_64_test.yml
  9. 2
      .github/workflows/PS4.yml
  10. 2
      .github/workflows/PS5.yml
  11. 2
      .github/workflows/Windows9x_MinGW.yml
  12. 2
      .github/workflows/Windows_MSVC_x64.yml
  13. 2
      .github/workflows/Windows_MinGW_x64.yml
  14. 2
      .github/workflows/Windows_MinGW_x86.yml
  15. 2
      .github/workflows/Windows_XP_32bit.yml
  16. 2
      .github/workflows/amiga-m68k.yml
  17. 4
      .github/workflows/iOS.yml
  18. 8
      .github/workflows/macOS_arm64.yml
  19. 8
      .github/workflows/macOS_x86_64.yml
  20. 4
      .github/workflows/miyoo_mini_release.yml
  21. 4
      .github/workflows/opendingux_release.yml
  22. 2
      .github/workflows/retrofw_release.yml
  23. 2
      .github/workflows/s390x_qemu_big_endian_tests.yml
  24. 4
      .github/workflows/src_dist_release.yml
  25. 2
      .github/workflows/switch.yml
  26. 2
      .github/workflows/vita.yml
  27. 2
      .github/workflows/xbox_nxdk.yml
  28. 2
      .github/workflows/xbox_one.yml
  29. 4
      3rdParty/SDL3/CMakeLists.txt
  30. 4
      3rdParty/SDL3_mixer/CMakeLists.txt
  31. 2
      3rdParty/bzip2/CMakeLists.txt
  32. 11
      CMake/Assets.cmake
  33. 10
      CMake/Tests.cmake
  34. 2
      README.md
  35. 29
      Source/CMakeLists.txt
  36. 2
      Source/DiabloUI/diabloui.cpp
  37. 2
      Source/DiabloUI/hero/selhero.cpp
  38. 14
      Source/DiabloUI/selstart.h
  39. 117
      Source/automap.cpp
  40. 2108
      Source/control.cpp
  41. 145
      Source/control/control.hpp
  42. 319
      Source/control/control_chat.cpp
  43. 51
      Source/control/control_chat.hpp
  44. 280
      Source/control/control_chat_commands.cpp
  45. 9
      Source/control/control_chat_commands.hpp
  46. 149
      Source/control/control_flasks.cpp
  47. 12
      Source/control/control_flasks.hpp
  48. 141
      Source/control/control_gold.cpp
  49. 425
      Source/control/control_infobox.cpp
  50. 832
      Source/control/control_panel.cpp
  51. 18
      Source/control/control_panel.hpp
  52. 2
      Source/controls/controller_motion.cpp
  53. 2
      Source/controls/keymapper.cpp
  54. 2
      Source/controls/modifier_hints.cpp
  55. 4
      Source/controls/plrctrls.cpp
  56. 2
      Source/controls/touch/event_handlers.cpp
  57. 2
      Source/controls/touch/gamepad.cpp
  58. 2
      Source/controls/touch/renderers.cpp
  59. 2
      Source/cursor.cpp
  60. 2
      Source/dead.cpp
  61. 12
      Source/diablo.cpp
  62. 2
      Source/diablo.h
  63. 2
      Source/discord/discord.cpp
  64. 2
      Source/doom.cpp
  65. 5
      Source/dvlnet/base.cpp
  66. 7
      Source/engine/demomode.cpp
  67. 810
      Source/engine/random.hpp
  68. 78
      Source/engine/render/scrollrt.cpp
  69. 148
      Source/engine/render/text_render.cpp
  70. 2
      Source/engine/render/text_render.hpp
  71. 2
      Source/engine/sound.cpp
  72. 2
      Source/gmenu.cpp
  73. 2
      Source/interfac.cpp
  74. 14
      Source/items.cpp
  75. 2
      Source/items.h
  76. 2
      Source/items/validation.cpp
  77. 2
      Source/levels/drlg_l3.cpp
  78. 2
      Source/levels/drlg_l4.cpp
  79. 2
      Source/levels/setmaps.cpp
  80. 2
      Source/levels/themes.h
  81. 2
      Source/levels/trigs.cpp
  82. 4
      Source/loadsave.cpp
  83. 14
      Source/lua/lua_event.hpp
  84. 47
      Source/lua/lua_global.cpp
  85. 7
      Source/lua/lua_global.hpp
  86. 21
      Source/lua/modules/audio.cpp
  87. 2
      Source/lua/modules/dev/monsters.cpp
  88. 2
      Source/lua/modules/dev/player/spells.cpp
  89. 24
      Source/lua/modules/floatingnumbers.cpp
  90. 9
      Source/lua/modules/floatingnumbers.hpp
  91. 13
      Source/lua/modules/items.cpp
  92. 18
      Source/lua/modules/items.hpp
  93. 20
      Source/lua/modules/monsters.cpp
  94. 18
      Source/lua/modules/monsters.hpp
  95. 84
      Source/lua/modules/player.cpp
  96. 46
      Source/lua/modules/render.cpp
  97. 25
      Source/lua/modules/system.cpp
  98. 9
      Source/lua/modules/system.hpp
  99. 6
      Source/minitext.cpp
  100. 2
      Source/minitext.h
  101. Some files were not shown because too many files have changed in this diff Show More

4
.github/workflows/3ds.yml

@ -58,14 +58,14 @@ jobs:
- name: Upload 3dsx Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx.3dsx
path: ./build/devilutionx.3dsx
- name: Upload cia Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx.cia
path: ./build/devilutionx.cia

4
.github/workflows/Android.yml

@ -41,7 +41,7 @@ jobs:
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "cmake;3.31.0"
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: android-project/app/.cxx
key: ${{ github.workflow }}-v5-${{ github.sha }}
@ -54,7 +54,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-debug.apk
path: android-project/app/build/outputs/apk/debug/app-debug.apk

6
.github/workflows/Linux_aarch64.yml

@ -37,7 +37,7 @@ jobs:
run: Packaging/nix/debian-cross-aarch64-prep.sh
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v8-${{ github.sha }}
@ -66,7 +66,7 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-aarch64-linux-gnu.tar.xz
path: devilutionx-aarch64-linux-gnu.tar.xz
@ -74,7 +74,7 @@ jobs:
# AppImage cross-packaging is not implemented yet.
# - name: Upload AppImage
# if: ${{ !env.ACT }}
# uses: actions/upload-artifact@v5
# uses: actions/upload-artifact@v6
# with:
# name: devilutionx-aarch64-linux-gnu.appimage
# path: devilutionx-aarch64-linux-gnu.appimage

6
.github/workflows/Linux_x86.yml

@ -37,7 +37,7 @@ jobs:
run: Packaging/nix/debian-cross-i386-prep.sh
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v7-${{ github.sha }}
@ -67,7 +67,7 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-i386-linux-gnu.tar.xz
path: devilutionx-i386-linux-gnu.tar.xz
@ -75,7 +75,7 @@ jobs:
# AppImage cross-packaging is not implemented yet.
# - name: Upload AppImage
# if: ${{ !env.ACT }}
# uses: actions/upload-artifact@v5
# uses: actions/upload-artifact@v6
# with:
# name: devilutionx-i386-linux-gnu.appimage
# path: devilutionx-i386-linux-gnu.appimage

6
.github/workflows/Linux_x86_64.yml

@ -37,7 +37,7 @@ jobs:
run: Packaging/nix/debian-host-prep.sh
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v7-${{ github.sha }}
@ -65,14 +65,14 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-x86_64-linux-gnu.tar.xz
path: devilutionx-x86_64-linux-gnu.tar.xz
- name: Upload AppImage
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-x86_64-linux-gnu.appimage
path: devilutionx-x86_64-linux-gnu.appimage

4
.github/workflows/Linux_x86_64_SDL1.yml

@ -32,7 +32,7 @@ jobs:
sudo apt-get install -y cmake file g++ git libfmt-dev libsdl1.2-dev libsodium-dev libpng-dev libbz2-dev rpm smpq
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v3-${{ github.sha }}
@ -54,7 +54,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx_linux_x86_64_SDL1.tar.xz
path: devilutionx.tar.xz

4
.github/workflows/Linux_x86_64_SDL3_test.yml

@ -30,10 +30,10 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake curl g++ git libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsodium-dev libpng-dev libbz2-dev wget
sudo apt-get install -y cmake curl g++ git libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsodium-dev libpng-dev libbz2-dev libasound2-dev libxcursor-dev libxi-dev libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev wget
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v4-${{ github.sha }}

2
.github/workflows/Linux_x86_64_test.yml

@ -33,7 +33,7 @@ jobs:
sudo apt-get install -y cmake curl g++ git lcov libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsdl2-dev libsodium-dev libpng-dev libbz2-dev wget
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v3-${{ github.sha }}

2
.github/workflows/PS4.yml

@ -53,7 +53,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-ps4.pkg
path: build-ps4/devilutionx.pkg

2
.github/workflows/PS5.yml

@ -48,7 +48,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-ps5.zip
path: build-ps5/devilutionx-ps5.zip

2
.github/workflows/Windows9x_MinGW.yml

@ -53,7 +53,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
path: devilutionx-win9x.zip

2
.github/workflows/Windows_MSVC_x64.yml

@ -61,7 +61,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx.exe
path: |

2
.github/workflows/Windows_MinGW_x64.yml

@ -44,7 +44,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx_x64.zip
path: build/devilutionx.zip

2
.github/workflows/Windows_MinGW_x86.yml

@ -44,7 +44,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx_x86.zip
path: build/devilutionx.zip

2
.github/workflows/Windows_XP_32bit.yml

@ -42,7 +42,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
path: devilutionx-windows-xp-32bit.zip

2
.github/workflows/amiga-m68k.yml

@ -50,7 +50,7 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx_m68k
path: ./build/devilutionx

4
.github/workflows/iOS.yml

@ -36,7 +36,7 @@ jobs:
fetch-depth: 0
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v5-${{ github.sha }}
@ -61,7 +61,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-iOS.ipa
path: build/devilutionx-iOS.ipa

8
.github/workflows/macOS_arm64.yml

@ -35,11 +35,11 @@ jobs:
run: brew bundle install
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v1-${{ github.sha }}
restore-keys: ${{ github.workflow }}-v1-
key: ${{ github.workflow }}-v2-${{ github.sha }}
restore-keys: ${{ github.workflow }}-v2-
- name: Clean previous DMG
working-directory: ${{github.workspace}}
@ -58,7 +58,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-arm64-macOS.dmg
path: build/devilutionx-arm64-macOS.dmg

8
.github/workflows/macOS_x86_64.yml

@ -35,11 +35,11 @@ jobs:
run: brew bundle install
- name: Cache CMake build folder
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: build
key: ${{ github.workflow }}-v3-${{ github.sha }}
restore-keys: ${{ github.workflow }}-v3-
key: ${{ github.workflow }}-v4-${{ github.sha }}
restore-keys: ${{ github.workflow }}-v4-
- name: Clean previous DMG
working-directory: ${{github.workspace}}
@ -58,7 +58,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-x86_64-macOS.dmg
path: build/devilutionx-x86_64-macOS.dmg

4
.github/workflows/miyoo_mini_release.yml

@ -31,14 +31,14 @@ jobs:
- name: Upload-OnionOS-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-miyoo-mini-onion-os.zip
path: build-miyoo-mini/devilutionx-miyoo-mini-onion-os.zip
- name: Upload-miniUI-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-miyoo-mini-miniui.zip
path: build-miyoo-mini/devilutionx-miyoo-mini-miniui.zip

4
.github/workflows/opendingux_release.yml

@ -39,7 +39,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-rg350.opk.zip
path: build-rg350/devilutionx-rg350.opk
@ -77,7 +77,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-lepus.opk.zip
path: build-lepus/devilutionx-lepus.opk

2
.github/workflows/retrofw_release.yml

@ -39,7 +39,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-retrofw.opk.zip
path: build-retrofw/devilutionx-retrofw.opk

2
.github/workflows/s390x_qemu_big_endian_tests.yml

@ -23,7 +23,7 @@ jobs:
fetch-depth: 0
- name: Cache .ccache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: .ccache
key: ${{ github.workflow }}-v1-${{ github.sha }}

4
.github/workflows/src_dist_release.yml

@ -32,7 +32,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-src.tar.xz
path: devilutionx-src.tar.xz
@ -63,7 +63,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-src-fully-vendored.tar.xz
path: devilutionx-src-fully-vendored.tar.xz

2
.github/workflows/switch.yml

@ -44,7 +44,7 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx.nro
path: ./build/devilutionx.nro

2
.github/workflows/vita.yml

@ -53,7 +53,7 @@ jobs:
- name: Upload Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx.vpk
path: ./build/devilutionx.vpk

2
.github/workflows/xbox_nxdk.yml

@ -57,7 +57,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-xbox
path: build-xbox/pkg/

2
.github/workflows/xbox_one.yml

@ -68,7 +68,7 @@ jobs:
- name: Upload-Package
if: ${{ !env.ACT }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: devilutionx-xbox-one-series
if-no-files-found: error

4
3rdParty/SDL3/CMakeLists.txt vendored

@ -16,7 +16,7 @@ include(functions/FetchContent_ExcludeFromAll_backport)
include(FetchContent)
FetchContent_Declare(SDL3
URL https://github.com/libsdl-org/SDL/archive/02c4478f9364ac9ba8c0b157c01e47c2f059552c.tar.gz
URL_HASH SHA256=1c8e9369ef6c67b6ca4102cc957846966815de56924b24a8a28feeac344a506a
URL https://github.com/libsdl-org/SDL/archive/f173fd28f04cb64ae054d6a97edb5d33925f539b.tar.gz
URL_HASH SHA256=f7501d84c1a7f168567c002f4e1db4f220c4a34c51f7fa7d199962d0ed5fb42c
)
FetchContent_MakeAvailable_ExcludeFromAll(SDL3)

4
3rdParty/SDL3_mixer/CMakeLists.txt vendored

@ -27,7 +27,7 @@ set(SDLMIXER_VORBIS_TREMOR OFF)
set(SDLMIXER_WAVPACK OFF)
FetchContent_Declare_ExcludeFromAll(SDL_mixer
URL https://github.com/libsdl-org/SDL_mixer/archive/4b4b05949208ad0a49832ed34f59beeae6d6c2da.tar.gz
URL_HASH SHA256=744bbe25e127121a87b070f9211794b38e71db7e6ea497757bf45ac85525a905
URL https://github.com/libsdl-org/SDL_mixer/archive/7d37755016f0952c32c9483c556d8608da7ee82f.tar.gz
URL_HASH SHA256=2fa63f1eb623e3acd0012a461771eb93332e2026205f9487da3a3a75bc790111
)
FetchContent_MakeAvailable_ExcludeFromAll(SDL_mixer)

2
3rdParty/bzip2/CMakeLists.txt vendored

@ -3,7 +3,7 @@ include(functions/FetchContent_ExcludeFromAll_backport)
include(FetchContent)
FetchContent_Declare_ExcludeFromAll(bzip2
GIT_REPOSITORY https://sourceware.org/git/bzip2
GIT_REPOSITORY https://gitlab.com/bzip2/bzip2
GIT_TAG bzip2-1.0.8
)
FetchContent_MakeAvailable_ExcludeFromAll(bzip2)

11
CMake/Assets.cmake

@ -5,7 +5,7 @@ if(NOT DEFINED DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY)
set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets")
endif()
set(devilutionx_langs bg cs da de el es et fi fr hr hu it ja ko pl pt_BR ro ru uk sv tr zh_CN zh_TW)
set(devilutionx_langs be bg cs da de el es et fi fr hr hu it ja ko pl pt_BR ro ru uk sv tr zh_CN zh_TW)
if(USE_GETTEXT_FROM_VCPKG)
# vcpkg doesn't add its own tools directory to the search path
list(APPEND Gettext_ROOT ${CMAKE_CURRENT_BINARY_DIR}/vcpkg_installed/${VCPKG_TARGET_TRIPLET}/tools/gettext/bin)
@ -72,6 +72,7 @@ set(devilutionx_assets
fonts/12-02.clx
fonts/12-03.clx
fonts/12-04.clx
fonts/12-05.clx
fonts/12-1f4.clx
fonts/12-1f6.clx
fonts/12-1f9.clx
@ -90,6 +91,7 @@ set(devilutionx_assets
fonts/24-02.clx
fonts/24-03.clx
fonts/24-04.clx
fonts/24-05.clx
fonts/24-1f4.clx
fonts/24-1f6.clx
fonts/24-1f9.clx
@ -101,6 +103,7 @@ set(devilutionx_assets
fonts/30-02.clx
fonts/30-03.clx
fonts/30-04.clx
fonts/30-05.clx
fonts/30-20.clx
fonts/30-e0.clx
fonts/42-00.clx
@ -108,12 +111,14 @@ set(devilutionx_assets
fonts/42-02.clx
fonts/42-03.clx
fonts/42-04.clx
fonts/42-05.clx
fonts/42-20.clx
fonts/46-00.clx
fonts/46-01.clx
fonts/46-02.clx
fonts/46-03.clx
fonts/46-04.clx
fonts/46-05.clx
fonts/46-20.clx
fonts/black.trn
fonts/blue.trn
@ -153,7 +158,10 @@ set(devilutionx_assets
lua_internal/get_lua_function_signature.lua
lua/devilutionx/events.lua
lua/inspect.lua
lua/mods/adria_refills_mana/init.lua
lua/mods/clock/init.lua
"lua/mods/Floating Numbers - Damage/init.lua"
"lua/mods/Floating Numbers - XP/init.lua"
lua/repl_prelude.lua
plrgfx/warrior/whu/whufm.trn
plrgfx/warrior/whu/whulm.trn
@ -280,3 +288,4 @@ else()
add_dependencies(libdevilutionx devilutionx_copied_assets)
endif()
endif()

10
CMake/Tests.cmake

@ -170,6 +170,16 @@ if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG AND
kerning_fit_spacing__align_right.png
vertical_overflow.png
vertical_overflow-colors.png
cursor-start.png
cursor-middle.png
cursor-end.png
multiline_cursor-end_first_line.png
multiline_cursor-start_second_line.png
multiline_cursor-middle_second_line.png
multiline_cursor-end_second_line.png
highlight-partial.png
highlight-full.png
multiline_highlight.png
SRC_PREFIX test/fixtures/text_render_integration_test/
OUTPUT_DIR "${DEVILUTIONX_TEST_FIXTURES_OUTPUT_DIRECTORY}/text_render_integration_test"
OUTPUT_VARIABLE _text_render_integration_test_fixtures

2
README.md

@ -52,9 +52,9 @@ If you want to help test the latest development version (make sure to back up yo
[![Linux x86](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86.yml?query=branch%3Amaster)
[![Linux x86_64 SDL1](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64_SDL1.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64_SDL1.yml?query=branch%3Amaster)
[![macOS x86_64](https://github.com/diasurgical/devilutionX/actions/workflows/macOS_x86_64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/macOS_x86_64.yml?query=branch%3Amaster)
[![Windows MSVC x64](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml?query=branch%3Amaster)
[![Windows MinGW x64](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x64.yml?query=branch%3Amaster)
[![Windows MinGW x86](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x86.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x86.yml?query=branch%3Amaster)
[![Windows MSVC x64](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml?query=branch%3Amaster)
[![Android](https://github.com/diasurgical/devilutionX/actions/workflows/Android.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Android.yml?query=branch%3Amaster)
[![iOS](https://github.com/diasurgical/devilutionX/actions/workflows/iOS.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/iOS.yml?query=branch%3Amaster)
[![PS4](https://github.com/diasurgical/devilutionX/actions/workflows/PS4.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/PS4.yml?query=branch%3Amaster)

29
Source/CMakeLists.txt

@ -6,7 +6,6 @@ set(libdevilutionx_SRCS
appfat.cpp
automap.cpp
capture.cpp
control.cpp
cursor.cpp
dead.cpp
debug.cpp
@ -22,7 +21,6 @@ set(libdevilutionx_SRCS
loadsave.cpp
menu.cpp
minitext.cpp
misdat.cpp
missiles.cpp
movie.cpp
msg.cpp
@ -32,12 +30,17 @@ set(libdevilutionx_SRCS
portal.cpp
restrict.cpp
sync.cpp
textdat.cpp
tmsg.cpp
townerdat.cpp
towners.cpp
track.cpp
control/control_chat.cpp
control/control_chat_commands.cpp
control/control_flasks.cpp
control/control_gold.cpp
control/control_infobox.cpp
control/control_panel.cpp
controls/axis_direction.cpp
controls/controller_motion.cpp
controls/controller.cpp
@ -114,12 +117,14 @@ set(libdevilutionx_SRCS
lua/modules/dev/quests.cpp
lua/modules/dev/search.cpp
lua/modules/dev/towners.cpp
lua/modules/floatingnumbers.cpp
lua/modules/i18n.cpp
lua/modules/items.cpp
lua/modules/log.cpp
lua/modules/monsters.cpp
lua/modules/player.cpp
lua/modules/render.cpp
lua/modules/system.cpp
lua/modules/towners.cpp
lua/repl.cpp
@ -150,6 +155,11 @@ set(libdevilutionx_SRCS
storm/storm_net.cpp
storm/storm_svid.cpp
tables/misdat.cpp
tables/textdat.cpp
tables/townerdat.cpp
utils/display.cpp
utils/language.cpp
utils/sdl_bilinear_scale.cpp
@ -480,7 +490,7 @@ target_link_dependencies(libdevilutionx_logged_fstream PUBLIC
)
add_devilutionx_object_library(libdevilutionx_items
itemdat.cpp
tables/itemdat.cpp
items.cpp
)
target_link_dependencies(libdevilutionx_items PUBLIC
@ -517,7 +527,7 @@ target_link_libraries(libdevilutionx_log INTERFACE
target_sources(libdevilutionx_log INTERFACE $<TARGET_OBJECTS:libdevilutionx_strings>)
add_devilutionx_object_library(libdevilutionx_level_objects
objdat.cpp
tables/objdat.cpp
objects.cpp
)
target_link_dependencies(libdevilutionx_level_objects PUBLIC
@ -534,7 +544,7 @@ target_link_dependencies(libdevilutionx_level_objects PUBLIC
)
add_devilutionx_object_library(libdevilutionx_monster
monstdat.cpp
tables/monstdat.cpp
monster.cpp
)
target_link_dependencies(libdevilutionx_monster
@ -638,13 +648,14 @@ endif()
add_devilutionx_object_library(libdevilutionx_player
player.cpp
playerdat.cpp
tables/playerdat.cpp
)
target_link_dependencies(libdevilutionx_player
PUBLIC
DevilutionX::SDL
fmt::fmt
magic_enum::magic_enum
sol2::sol2
tl
unordered_dense::unordered_dense
libdevilutionx_game_mode
@ -670,7 +681,7 @@ add_devilutionx_object_library(libdevilutionx_quick_messages
)
add_devilutionx_object_library(libdevilutionx_spells
spelldat.cpp
tables/spelldat.cpp
spells.cpp
)
target_link_dependencies(libdevilutionx_spells PUBLIC

2
Source/DiabloUI/diabloui.cpp

@ -50,8 +50,8 @@
#include "init.hpp"
#include "options.h"
#include "player.h"
#include "playerdat.hpp"
#include "sound_effect_enums.h"
#include "tables/playerdat.hpp"
#include "utils/algorithm/container.hpp"
#include "utils/display.h"
#include "utils/enum_traits.h"

2
Source/DiabloUI/hero/selhero.cpp

@ -33,7 +33,7 @@
#include "levels/gendung.h"
#include "options.h"
#include "pfile.h"
#include "playerdat.hpp"
#include "tables/playerdat.hpp"
#include "utils/enum_traits.h"
#include "utils/language.h"
#include "utils/sdl_geometry.h"

14
Source/DiabloUI/selstart.h

@ -1,7 +1,7 @@
#pragma once
namespace devilution {
void UiSelStartUpGameOption();
} // namespace devilution
#pragma once
namespace devilution {
void UiSelStartUpGameOption();
} // namespace devilution

117
Source/automap.cpp

@ -10,7 +10,7 @@
#include <fmt/format.h>
#include "control.h"
#include "control/control.hpp"
#include "engine/load_file.hpp"
#include "engine/palette.h"
#include "engine/render/automap_render.hpp"
@ -1411,54 +1411,59 @@ void DrawAutomapText(const Surface &out)
{
Point linePosition { 8, 8 };
auto advanceLine = [&](int numLines = 1) {
linePosition.y += 15 * numLines;
};
auto drawStringAndAdvanceLine = [&](std::string_view text, TextRenderOptions opts = {}, int numLines = 1) {
DrawString(out, text, linePosition, opts);
advanceLine(numLines);
};
if (*GetOptions().Graphics.showFPS) {
linePosition.y += 15;
advanceLine();
}
if (gbIsMultiplayer) {
if (GameName != "0.0.0.0" && !IsLoopback) {
std::string description = std::string(_("Game: "));
description.append(GameName);
DrawString(out, description, linePosition);
linePosition.y += 15;
drawStringAndAdvanceLine(description);
}
std::string description;
if (IsLoopback) {
description = std::string(_("Offline Game"));
} else if (!PublicGame) {
} else if (PublicGame) {
description = std::string(_("Public Game"));
} else {
description = std::string(_("Password: "));
description.append(GamePassword);
} else {
description = std::string(_("Public Game"));
}
DrawString(out, description, linePosition);
linePosition.y += 15;
drawStringAndAdvanceLine(description);
}
if (setlevel) {
DrawString(out, _(QuestLevelNames[setlvlnum]), linePosition);
return;
}
std::string description;
switch (leveltype) {
case DTYPE_NEST:
description = fmt::format(fmt::runtime(_("Level: Nest {:d}")), currlevel - 16);
break;
case DTYPE_CRYPT:
description = fmt::format(fmt::runtime(_("Level: Crypt {:d}")), currlevel - 20);
break;
case DTYPE_TOWN:
description = std::string(_("Town"));
break;
default:
description = fmt::format(fmt::runtime(_("Level: {:d}")), currlevel);
break;
drawStringAndAdvanceLine(_(QuestLevelNames[setlvlnum]));
} else {
std::string description;
switch (leveltype) {
case DTYPE_NEST:
description = fmt::format(fmt::runtime(_("Level: Nest {:d}")), currlevel - 16);
break;
case DTYPE_CRYPT:
description = fmt::format(fmt::runtime(_("Level: Crypt {:d}")), currlevel - 20);
break;
case DTYPE_TOWN:
description = std::string(_("Town"));
break;
default:
description = fmt::format(fmt::runtime(_("Level: {:d}")), currlevel);
break;
}
drawStringAndAdvanceLine(description);
}
DrawString(out, description, linePosition);
linePosition.y += 15;
std::string_view difficulty;
switch (sgGameInitInfo.nDifficulty) {
case DIFF_NORMAL:
@ -1472,41 +1477,29 @@ void DrawAutomapText(const Surface &out)
break;
}
const std::string difficultyString = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Game Difficulty. */ "Difficulty: {:s}")), difficulty);
DrawString(out, difficultyString, linePosition);
const std::string description = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Game Difficulty. */ "Difficulty: {:s}")), difficulty);
drawStringAndAdvanceLine(description);
#ifdef _DEBUG
const TextRenderOptions debugTextOptions {
.flags = UiFlags::ColorOrange,
};
linePosition.y += 45;
if (DebugGodMode) {
linePosition.y += 15;
DrawString(out, "God Mode", linePosition, debugTextOptions);
}
if (DebugInvisible) {
linePosition.y += 15;
DrawString(out, "Invisible", linePosition, debugTextOptions);
}
if (DisableLighting) {
linePosition.y += 15;
DrawString(out, "Fullbright", linePosition, debugTextOptions);
}
if (DebugVision) {
linePosition.y += 15;
DrawString(out, "Draw Vision", linePosition, debugTextOptions);
}
if (DebugPath) {
linePosition.y += 15;
DrawString(out, "Draw Path", linePosition, debugTextOptions);
}
if (DebugGrid) {
linePosition.y += 15;
DrawString(out, "Draw Grid", linePosition, debugTextOptions);
}
if (DebugScrollViewEnabled) {
linePosition.y += 15;
DrawString(out, "Scroll View", linePosition, debugTextOptions);
if (DebugGodMode || DebugInvisible || DisableLighting || DebugVision || DebugPath || DebugGrid || DebugScrollViewEnabled) {
const TextRenderOptions disabled {
.flags = UiFlags::ColorBlack,
};
const TextRenderOptions enabled {
.flags = UiFlags::ColorOrange,
};
advanceLine();
drawStringAndAdvanceLine("Debug toggles:");
drawStringAndAdvanceLine("Player:");
drawStringAndAdvanceLine("God Mode", DebugGodMode ? enabled : disabled);
drawStringAndAdvanceLine("Invisible", DebugInvisible ? enabled : disabled);
drawStringAndAdvanceLine("Display:");
drawStringAndAdvanceLine("Fullbright", DisableLighting ? enabled : disabled);
drawStringAndAdvanceLine("Draw Vision", DebugVision ? enabled : disabled);
drawStringAndAdvanceLine("Draw Path", DebugPath ? enabled : disabled);
drawStringAndAdvanceLine("Draw Grid", DebugGrid ? enabled : disabled);
drawStringAndAdvanceLine("Scroll View", DebugScrollViewEnabled ? enabled : disabled);
}
#endif
}

2108
Source/control.cpp

File diff suppressed because it is too large Load Diff

145
Source/control.h → Source/control/control.hpp

@ -1,8 +1,3 @@
/**
* @file control.h
*
* Interface of the character and main control panels
*/
#pragma once
#include <cstddef>
@ -33,8 +28,8 @@
#include "engine/render/text_render.hpp"
#include "engine/size.hpp"
#include "panels/ui_panels.hpp"
#include "spelldat.h"
#include "spells.h"
#include "tables/spelldat.h"
#include "utils/attributes.h"
#include "utils/string_or_view.hpp"
#include "utils/ui_fwd.h"
@ -45,34 +40,37 @@ constexpr Size SidePanelSize { 320, 352 };
constexpr Rectangle InfoBoxRect = { { 177, 46 }, { 288, 64 } };
extern bool DropGoldFlag;
extern TextInputCursorState GoldDropCursor;
extern char GoldDropText[21];
extern bool CharPanelButton[4];
extern bool LevelButtonDown;
extern bool CharPanelButtonActive;
extern UiFlags InfoColor;
extern int SpellbookTab;
extern bool ChatFlag;
extern bool SpellbookFlag;
extern bool CharFlag;
extern UiFlags InfoColor;
extern StringOrView InfoString;
extern StringOrView FloatingInfoString;
extern bool MainPanelFlag;
extern Rectangle MainPanelButtonRect[8];
extern Rectangle CharPanelButtonRect[4];
extern bool MainPanelButtonDown;
extern bool SpellSelectFlag;
const Rectangle &GetMainPanel();
const Rectangle &GetLeftPanel();
const Rectangle &GetRightPanel();
bool IsLeftPanelOpen();
bool IsRightPanelOpen();
extern bool LevelButtonDown;
extern std::optional<OwnedSurface> BottomBuffer;
extern OptionalOwnedClxSpriteList GoldBoxBuffer;
extern Rectangle MainPanelButtonRect[8];
extern bool MainPanelFlag;
extern bool ChatFlag;
extern bool SpellbookFlag;
extern bool CharFlag;
extern bool SpellSelectFlag;
[[nodiscard]] const Rectangle &GetMainPanel();
[[nodiscard]] const Rectangle &GetLeftPanel();
[[nodiscard]] const Rectangle &GetRightPanel();
bool IsLeftPanelOpen();
bool IsRightPanelOpen();
void CalculatePanelAreas();
bool IsChatAvailable();
/**
* @brief Moves the mouse to the first attribute "+" button.
@ -85,7 +83,7 @@ void ToggleCharPanel();
/**
* @brief Check if the UI can cover the game area entirely
*/
inline bool CanPanelsCoverView()
[[nodiscard]] inline bool CanPanelsCoverView()
{
const Rectangle &mainPanel = GetMainPanel();
return GetScreenWidth() <= mainPanel.size.width && GetScreenHeight() <= SidePanelSize.height + mainPanel.size.height;
@ -96,46 +94,6 @@ void AddInfoBoxString(std::string &&str, bool floatingBox = false);
void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition);
Point GetPanelPosition(UiPanels panel, Point offset = { 0, 0 });
/**
* Draws the top dome of the life flask (that part that protrudes out of the control panel).
* The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and
* the filled flask cel is drawn from that level to the top of the control panel if required.
*/
void DrawLifeFlaskUpper(const Surface &out);
/**
* Controls the drawing of the area of the life flask within the control panel.
* First sets the fill amount then draws the empty flask cel portion then the filled
* flask portion.
*/
void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Draws the top dome of the mana flask (that part that protrudes out of the control panel).
* The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and
* the filled flask cel is drawn from that level to the top of the control panel if required.
*/
void DrawManaFlaskUpper(const Surface &out);
/**
* Controls the drawing of the area of the mana flask within the control panel.
*/
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Controls drawing of current / max values (health, mana) within the control panel.
*/
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue);
/**
* @brief calls on the active player object to update HP/Mana percentage variables
*
* This is used to ensure that DrawFlaskAbovePanel routines display an accurate representation of the players health/mana
*
* @see Player::UpdateHitPointPercentage() and Player::UpdateManaPercentage()
*/
void UpdateLifeManaPercent();
tl::expected<void, std::string> InitMainPanel();
void DrawMainPanel(const Surface &out);
@ -186,21 +144,66 @@ void DrawDurIcon(const Surface &out);
void RedBack(const Surface &out);
void DrawDeathText(const Surface &out);
void DrawSpellBook(const Surface &out);
void DrawGoldSplit(const Surface &out);
void control_drop_gold(SDL_Keycode vkey);
extern Rectangle CharPanelButtonRect[4];
bool CheckKeypress(SDL_Keycode vkey);
void DiabloHotkeyMsg(uint32_t dwMsg);
void DrawChatBox(const Surface &out);
bool CheckMuteButton();
void CheckMuteButtonUp();
void TypeChatMessage();
void ResetChat();
bool IsChatActive();
bool IsChatAvailable();
bool HandleTalkTextInputEvent(const SDL_Event &event);
bool CheckKeypress(SDL_Keycode vkey);
void DiabloHotkeyMsg(uint32_t dwMsg);
/**
* Draws the top dome of the life flask (that part that protrudes out of the control panel).
* The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and
* the filled flask cel is drawn from that level to the top of the control panel if required.
*/
void DrawLifeFlaskUpper(const Surface &out);
/**
* Controls the drawing of the area of the life flask within the control panel.
* First sets the fill amount then draws the empty flask cel portion then the filled
* flask portion.
*/
void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Draws the top dome of the mana flask (that part that protrudes out of the control panel).
* The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and
* the filled flask cel is drawn from that level to the top of the control panel if required.
*/
void DrawManaFlaskUpper(const Surface &out);
/**
* Controls the drawing of the area of the mana flask within the control panel.
*/
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Controls drawing of current / max values (health, mana) within the control panel.
*/
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue);
/**
* @brief calls on the active player object to update HP/Mana percentage variables
*
* This is used to ensure that DrawFlaskAbovePanel routines display an accurate representation of the players health/mana
*
* @see Player::UpdateHitPointPercentage() and Player::UpdateManaPercentage()
*/
void UpdateLifeManaPercent();
extern bool DropGoldFlag;
void DrawGoldSplit(const Surface &out);
void control_drop_gold(SDL_Keycode vkey);
void OpenGoldDrop(int8_t invIndex, int max);
void CloseGoldDrop();
int GetGoldDropMax();
bool HandleGoldDropTextInputEvent(const SDL_Event &event);
extern Rectangle CharPanelButtonRect[4];
} // namespace devilution

319
Source/control/control_chat.cpp

@ -0,0 +1,319 @@
#include "control_chat.hpp"
#include "control.hpp"
#include "control_panel.hpp"
#include "control/control_chat_commands.hpp"
#include "engine/backbuffer_state.hpp"
#include "engine/render/clx_render.hpp"
#include "options.h"
#include "panels/console.hpp"
#include "panels/mainpanel.hpp"
#include "quick_messages.hpp"
#include "utils/display.h"
#include "utils/sdl_compat.h"
#include "utils/str_cat.hpp"
namespace devilution {
std::optional<TextInputState> ChatInputState;
char TalkMessage[MAX_SEND_STR_LEN];
bool TalkButtonsDown[3];
int sgbPlrTalkTbl;
bool WhisperList[MAX_PLRS];
OptionalOwnedClxSpriteList talkButtons;
namespace {
char TalkSave[8][MAX_SEND_STR_LEN];
uint8_t TalkSaveIndex;
uint8_t NextTalkSave;
TextInputCursorState ChatCursor;
int MuteButtons = 3;
int MuteButtonPadding = 2;
Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } };
void ResetChatMessage()
{
if (CheckChatCommand(TalkMessage))
return;
uint32_t pmask = 0;
for (size_t i = 0; i < Players.size(); i++) {
if (WhisperList[i])
pmask |= 1 << i;
}
NetSendCmdString(pmask, TalkMessage);
}
void ControlPressEnter()
{
if (TalkMessage[0] != 0) {
ResetChatMessage();
uint8_t i = 0;
for (; i < 8; i++) {
if (strcmp(TalkSave[i], TalkMessage) == 0)
break;
}
if (i >= 8) {
strcpy(TalkSave[NextTalkSave], TalkMessage);
NextTalkSave++;
NextTalkSave &= 7;
} else {
uint8_t talkSave = NextTalkSave - 1;
talkSave &= 7;
if (i != talkSave) {
strcpy(TalkSave[i], TalkSave[talkSave]);
*BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0';
}
}
TalkMessage[0] = '\0';
TalkSaveIndex = NextTalkSave;
}
ResetChat();
}
void ControlUpDown(int v)
{
for (int i = 0; i < 8; i++) {
TalkSaveIndex = (v + TalkSaveIndex) & 7;
if (TalkSave[TalkSaveIndex][0] != 0) {
ChatInputState->assign(TalkSave[TalkSaveIndex]);
return;
}
}
}
} // namespace
void DrawChatBox(const Surface &out)
{
if (!ChatFlag)
return;
const Point mainPanelPosition = GetMainPanel().position;
DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 });
int off = 0;
for (int i = 293; i > 283; off++, i--) {
DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 });
}
DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 });
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 });
for (int i = 0; i < 10; i++) {
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 });
}
DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 });
int x = mainPanelPosition.x + 200;
const int y = mainPanelPosition.y + 10;
const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } },
{
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor,
.lineHeight = 13,
.cursorPosition = static_cast<int>(ChatCursor.position),
.highlightRange = { static_cast<int>(ChatCursor.selection.begin), static_cast<int>(ChatCursor.selection.end) },
});
ChatInputState->truncate(len);
x += 46;
int talkBtn = 0;
for (size_t i = 0; i < Players.size(); i++) {
Player &player = Players[i];
if (&player == MyPlayer)
continue;
const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed;
const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn };
if (WhisperList[i]) {
// the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held
if (TalkButtonsDown[talkBtn]) {
const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest.
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]);
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
// the first button be treated the same as the other two further down the panel.
RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 });
}
} else {
unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest.
if (TalkButtonsDown[talkBtn])
spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively)
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]);
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
// the first button be treated the same as the other two further down the panel.
RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 });
}
if (player.plractive) {
DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, { .flags = color });
}
talkBtn++;
}
}
bool CheckMuteButton()
{
if (!ChatFlag)
return false;
Rectangle buttons = MuteButtonRect;
SetPanelObjectPosition(UiPanels::Main, buttons);
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding);
if (!buttons.contains(MousePosition))
return false;
for (bool &talkButtonDown : TalkButtonsDown) {
talkButtonDown = false;
}
const Point mainPanelPosition = GetMainPanel().position;
TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true;
return true;
}
void CheckMuteButtonUp()
{
if (!ChatFlag)
return;
for (bool &talkButtonDown : TalkButtonsDown)
talkButtonDown = false;
Rectangle buttons = MuteButtonRect;
SetPanelObjectPosition(UiPanels::Main, buttons);
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding);
if (!buttons.contains(MousePosition))
return;
int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding);
size_t playerId = 0;
for (; playerId < Players.size() && off != -1; ++playerId) {
if (playerId != MyPlayerId)
off--;
}
if (playerId > 0 && playerId <= Players.size())
WhisperList[playerId - 1] = !WhisperList[playerId - 1];
}
void TypeChatMessage()
{
if (!IsChatAvailable())
return;
ChatFlag = true;
TalkMessage[0] = '\0';
ChatInputState.emplace(TextInputState::Options {
.value = TalkMessage,
.cursor = &ChatCursor,
.maxLength = sizeof(TalkMessage) - 1 });
for (bool &talkButtonDown : TalkButtonsDown) {
talkButtonDown = false;
}
sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight;
RedrawEverything();
TalkSaveIndex = NextTalkSave;
SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27);
SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0);
SDLC_StartTextInput(ghMainWnd);
}
void ResetChat()
{
ChatFlag = false;
SDLC_StopTextInput(ghMainWnd);
ChatCursor = {};
ChatInputState = std::nullopt;
sgbPlrTalkTbl = 0;
RedrawEverything();
}
bool IsChatActive()
{
if (!IsChatAvailable())
return false;
if (!ChatFlag)
return false;
return true;
}
bool CheckKeypress(SDL_Keycode vkey)
{
if (!IsChatAvailable())
return false;
if (!ChatFlag)
return false;
switch (vkey) {
case SDLK_ESCAPE:
ResetChat();
return true;
case SDLK_RETURN:
case SDLK_KP_ENTER:
ControlPressEnter();
return true;
case SDLK_DOWN:
ControlUpDown(1);
return true;
case SDLK_UP:
ControlUpDown(-1);
return true;
default:
return vkey >= SDLK_SPACE && vkey <= SDLK_Z;
}
}
void DiabloHotkeyMsg(uint32_t dwMsg)
{
assert(dwMsg < QuickMessages.size());
#ifdef _DEBUG
constexpr std::string_view LuaPrefix = "/lua ";
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) {
if (!msg.starts_with(LuaPrefix)) continue;
InitConsole();
RunInConsole(std::string_view(msg).substr(LuaPrefix.size()));
}
#endif
if (!IsChatAvailable()) {
return;
}
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) {
#ifdef _DEBUG
if (msg.starts_with(LuaPrefix)) continue;
#endif
char charMsg[MAX_SEND_STR_LEN];
CopyUtf8(charMsg, msg, sizeof(charMsg));
NetSendCmdString(0xFFFFFF, charMsg);
}
}
bool IsChatAvailable()
{
return gbIsMultiplayer;
}
bool HandleTalkTextInputEvent(const SDL_Event &event)
{
return HandleInputEvent(event, ChatInputState);
}
} // namespace devilution

51
Source/control/control_chat.hpp

@ -0,0 +1,51 @@
#pragma once
#include <optional>
#include <string_view>
#include "DiabloUI/text_input.hpp"
#include "engine/clx_sprite.hpp"
#include "msg.h"
#include "multi.h"
#ifdef USE_SDL3
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_rect.h>
#else
#include <SDL.h>
#ifdef USE_SDL1
#include "utils/sdl2_to_1_2_backports.h"
#endif
#endif
namespace devilution {
extern OptionalOwnedClxSpriteList talkButtons;
extern std::optional<TextInputState> ChatInputState;
extern char TalkMessage[MAX_SEND_STR_LEN];
extern bool TalkButtonsDown[3];
extern int sgbPlrTalkTbl;
extern bool WhisperList[MAX_PLRS];
bool CheckChatCommand(std::string_view text);
template <typename InputStateType>
bool HandleInputEvent(const SDL_Event &event, std::optional<InputStateType> &inputState)
{
if (!inputState) {
return false; // No input state to handle
}
if constexpr (std::is_same_v<InputStateType, TextInputState>) {
return HandleTextInputEvent(event, *inputState);
} else if constexpr (std::is_same_v<InputStateType, NumberInputState>) {
return HandleNumberInputEvent(event, *inputState);
}
return false; // Unknown input state type
}
} // namespace devilution

280
Source/control/control_chat_commands.cpp

@ -0,0 +1,280 @@
#include "control_chat_commands.hpp"
#include "control.hpp"
#include "diablo_msg.hpp"
#include "engine/backbuffer_state.hpp"
#include "inv.h"
#include "levels/setmaps.h"
#include "storm/storm_net.hpp"
#include "utils/algorithm/container.hpp"
#include "utils/log.hpp"
#include "utils/parse_int.hpp"
#include "utils/str_case.hpp"
#include "utils/str_cat.hpp"
#ifdef _DEBUG
#include "debug.h"
#endif
namespace devilution {
namespace {
struct TextCmdItem {
const std::string text;
const std::string description;
const std::string requiredParameter;
std::string (*actionProc)(const std::string_view);
};
extern std::vector<TextCmdItem> TextCmdList;
std::string TextCmdHelp(const std::string_view parameter)
{
if (parameter.empty()) {
std::string ret;
StrAppend(ret, _("Available Commands:"));
for (const TextCmdItem &textCmd : TextCmdList) {
StrAppend(ret, " ", _(textCmd.text));
}
return ret;
}
auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return elem.text == parameter; });
if (textCmdIterator == TextCmdList.end())
return StrCat(_("Command "), parameter, _(" is unknown."));
auto &textCmdItem = *textCmdIterator;
if (textCmdItem.requiredParameter.empty())
return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed."));
return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter));
}
void AppendArenaOverview(std::string &ret)
{
for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) {
StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")");
}
}
std::string TextCmdArena(const std::string_view parameter)
{
std::string ret;
if (!gbIsMultiplayer) {
StrAppend(ret, _("Arenas are only supported in multiplayer."));
return ret;
}
if (parameter.empty()) {
StrAppend(ret, _("What arena do you want to visit?"));
AppendArenaOverview(ret);
return ret;
}
const ParseIntResult<int> parsedParam = ParseInt<int>(parameter, /*min=*/0);
const _setlevels arenaLevel = parsedParam.has_value() ? static_cast<_setlevels>(parsedParam.value() - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE;
if (!IsArenaLevel(arenaLevel)) {
StrAppend(ret, _("Invalid arena-number. Valid numbers are:"));
AppendArenaOverview(ret);
return ret;
}
if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) {
StrAppend(ret, _("To enter a arena, you need to be in town or another arena."));
return ret;
}
setlvltype = GetArenaLevelType(arenaLevel);
StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel);
return ret;
}
std::string TextCmdArenaPot(const std::string_view parameter)
{
std::string ret;
if (!gbIsMultiplayer) {
StrAppend(ret, _("Arenas are only supported in multiplayer."));
return ret;
}
const int numPots = ParseInt<int>(parameter, /*min=*/1).value_or(1);
Player &myPlayer = *MyPlayer;
for (int potNumber = numPots; potNumber > 0; potNumber--) {
Item item {};
InitializeItem(item, IDI_ARENAPOT);
GenerateNewSeed(item);
item.updateRequiredStatsCacheForPlayer(myPlayer);
if (!AutoPlaceItemInBelt(myPlayer, item, true, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) {
break; // inventory is full
}
}
return ret;
}
std::string TextCmdInspect(const std::string_view parameter)
{
std::string ret;
if (!gbIsMultiplayer) {
StrAppend(ret, _("Inspecting only supported in multiplayer."));
return ret;
}
if (parameter.empty()) {
StrAppend(ret, _("Stopped inspecting players."));
InspectPlayer = MyPlayer;
return ret;
}
const std::string param = AsciiStrToLower(parameter);
auto it = c_find_if(Players, [&param](const Player &player) {
return AsciiStrToLower(player._pName) == param;
});
if (it == Players.end()) {
it = c_find_if(Players, [&param](const Player &player) {
return AsciiStrToLower(player._pName).find(param) != std::string::npos;
});
}
if (it == Players.end()) {
StrAppend(ret, _("No players found with such a name"));
return ret;
}
Player &player = *it;
InspectPlayer = &player;
StrAppend(ret, _("Inspecting player: "));
StrAppend(ret, player._pName);
OpenCharPanel();
if (!SpellbookFlag)
invflag = true;
RedrawEverything();
return ret;
}
bool IsQuestEnabled(const Quest &quest)
{
switch (quest._qidx) {
case Q_FARMER:
return gbIsHellfire && !sgGameInitInfo.bCowQuest;
case Q_JERSEY:
return gbIsHellfire && sgGameInitInfo.bCowQuest;
case Q_GIRL:
return gbIsHellfire && sgGameInitInfo.bTheoQuest;
case Q_CORNSTN:
return gbIsHellfire && !gbIsMultiplayer;
case Q_GRAVE:
case Q_DEFILER:
case Q_NAKRUL:
return gbIsHellfire;
case Q_TRADER:
return false;
default:
return quest._qactive != QUEST_NOTAVAIL;
}
}
std::string TextCmdLevelSeed(const std::string_view parameter)
{
const std::string_view levelType = setlevel ? "set level" : "dungeon level";
char gameId[] = {
static_cast<char>((sgGameInitInfo.programid >> 24) & 0xFF),
static_cast<char>((sgGameInitInfo.programid >> 16) & 0xFF),
static_cast<char>((sgGameInitInfo.programid >> 8) & 0xFF),
static_cast<char>(sgGameInitInfo.programid & 0xFF),
'\0'
};
const std::string_view mode = gbIsMultiplayer ? "MP" : "SP";
const std::string_view questPool = UseMultiplayerQuests() ? "MP" : "Full";
uint32_t questFlags = 0;
for (const Quest &quest : Quests) {
questFlags <<= 1;
if (IsQuestEnabled(quest))
questFlags |= 1;
}
return StrCat(
"Seedinfo for ", levelType, " ", currlevel, "\n",
"seed: ", DungeonSeeds[currlevel], "\n",
#ifdef _DEBUG
"Mid1: ", glMid1Seed[currlevel], "\n",
"Mid2: ", glMid2Seed[currlevel], "\n",
"Mid3: ", glMid3Seed[currlevel], "\n",
"End: ", glEndSeed[currlevel], "\n",
#endif
"\n",
gameId, " ", mode, "\n",
questPool, " quests: ", questFlags, "\n",
"Storybook: ", DungeonSeeds[16]);
}
std::string TextCmdPing(const std::string_view parameter)
{
std::string ret;
const std::string param = AsciiStrToLower(parameter);
auto it = c_find_if(Players, [&param](const Player &player) {
return AsciiStrToLower(player._pName) == param;
});
if (it == Players.end()) {
it = c_find_if(Players, [&param](const Player &player) {
return AsciiStrToLower(player._pName).find(param) != std::string::npos;
});
}
if (it == Players.end()) {
StrAppend(ret, _("No players found with such a name"));
return ret;
}
Player &player = *it;
DvlNetLatencies latencies = DvlNet_GetLatencies(player.getId());
StrAppend(ret, fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Latency statistics for {:s}:")), player.name()));
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency));
if (latencies.providerLatency) {
if (latencies.isRelayed && *latencies.isRelayed) {
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency));
} else {
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency));
}
}
return ret;
}
std::vector<TextCmdItem> TextCmdList = {
{ "/help", N_("Prints help overview or help for a specific command."), N_("[command]"), &TextCmdHelp },
{ "/arena", N_("Enter a PvP Arena."), N_("<arena-number>"), &TextCmdArena },
{ "/arenapot", N_("Gives Arena Potions."), N_("<number>"), &TextCmdArenaPot },
{ "/inspect", N_("Inspects stats and equipment of another player."), N_("<player name>"), &TextCmdInspect },
{ "/seedinfo", N_("Show seed infos for current level."), "", &TextCmdLevelSeed },
{ "/ping", N_("Show latency statistics for another player."), N_("<player name>"), &TextCmdPing },
};
} // namespace
bool CheckChatCommand(const std::string_view text)
{
if (text.size() < 1 || text[0] != '/')
return false;
auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); });
if (textCmdIterator == TextCmdList.end()) {
InitDiabloMsg(StrCat(_("Command "), "\"", text, "\"", _(" is unknown.")));
return true;
}
const TextCmdItem &textCmd = *textCmdIterator;
std::string_view parameter = "";
if (text.length() > (textCmd.text.length() + 1))
parameter = text.substr(textCmd.text.length() + 1);
const std::string result = textCmd.actionProc(parameter);
if (result != "")
InitDiabloMsg(result);
return true;
}
} // namespace devilution

9
Source/control/control_chat_commands.hpp

@ -0,0 +1,9 @@
#pragma once
#include <string_view>
namespace devilution {
bool CheckChatCommand(std::string_view text);
} // namespace devilution

149
Source/control/control_flasks.cpp

@ -0,0 +1,149 @@
#include "control_flasks.hpp"
#include "control.hpp"
#include "engine/surface.hpp"
#include "utils/str_cat.hpp"
namespace devilution {
std::optional<OwnedSurface> pLifeBuff;
std::optional<OwnedSurface> pManaBuff;
namespace {
Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } };
Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } };
/**
* Draws the dome of the flask that protrudes above the panel top line.
* It draws a rectangle of fixed width 59 and height 'h' from the source buffer
* into the target buffer.
* @param out The target buffer.
* @param celBuf Buffer of the flask cel.
* @param targetPosition Target buffer coordinate.
*/
void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition)
{
out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition);
}
/**
* @brief Draws the part of the life/mana flasks protruding above the bottom panel
* @see DrawFlaskLower()
* @param out The display region to draw to
* @param sourceBuffer A sprite representing the appropriate background/empty flask style
* @param offset X coordinate offset for where the flask should be drawn
* @param fillPer How full the flask is (a value from 0 to 81)
*/
void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer)
{
const Rectangle &rect = FlaskTopRect;
const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height);
const int filledRows = rect.size.height - emptyRows;
// Draw the empty part of the flask
DrawFlaskAbovePanel(out,
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height),
GetMainPanel().position + Displacement { offset, -rect.size.height });
// Draw the filled part of the flask over the empty part
if (filledRows > 0) {
DrawFlaskAbovePanel(out,
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows),
GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows });
}
}
/**
* Draws a section of the empty flask cel on top of the panel to create the illusion
* of the flask getting empty. This function takes a cel and draws a
* horizontal stripe of height (max-min) onto the given buffer.
* @param out Target buffer.
* @param celBuf Buffer of the flask cel.
* @param targetPosition Target buffer coordinate.
*/
void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition)
{
out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition);
}
/**
* @brief Draws the part of the life/mana flasks inside the bottom panel
* @see DrawFlaskUpper()
* @param out The display region to draw to
* @param sourceBuffer A sprite representing the appropriate background/empty flask style
* @param offset X coordinate offset for where the flask should be drawn
* @param fillPer How full the flask is (a value from 0 to 80)
* @param drawFilledPortion Indicates whether to draw the filled portion of the flask
*/
void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion)
{
const Rectangle &rect = FlaskBottomRect;
const int filledRows = std::clamp(fillPer, 0, rect.size.height);
const int emptyRows = rect.size.height - filledRows;
// Draw the empty part of the flask
if (emptyRows > 0) {
DrawFlaskOnPanel(out,
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows),
GetMainPanel().position + Displacement { offset, 0 });
}
// Draw the filled part of the flask
if (drawFilledPortion && filledRows > 0) {
DrawFlaskOnPanel(out,
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows),
GetMainPanel().position + Displacement { offset, emptyRows });
}
}
} // namespace
void DrawLifeFlaskUpper(const Surface &out)
{
constexpr int LifeFlaskUpperOffset = 107;
DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, MyPlayer->_pHPPer);
}
void DrawManaFlaskUpper(const Surface &out)
{
constexpr int ManaFlaskUpperOffset = 475;
DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer);
}
void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion)
{
constexpr int LifeFlaskLowerOffset = 96;
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion);
}
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion)
{
constexpr int ManaFlaskLowerOffset = 464;
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion);
}
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue)
{
const UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed);
auto drawStringWithShadow = [out, color](std::string_view text, Point pos) {
DrawString(out, text, pos + Displacement { -1, -1 },
{ .flags = UiFlags::ColorBlack | UiFlags::KerningFitSpacing, .spacing = 0 });
DrawString(out, text, pos,
{ .flags = color | UiFlags::KerningFitSpacing, .spacing = 0 });
};
const std::string currText = StrCat(currValue);
drawStringWithShadow(currText, pos - Displacement { GetLineWidth(currText, GameFont12) + 1, 0 });
drawStringWithShadow("/", pos);
drawStringWithShadow(StrCat(maxValue), pos + Displacement { GetLineWidth("/", GameFont12) + 1, 0 });
}
void UpdateLifeManaPercent()
{
MyPlayer->UpdateManaPercentage();
MyPlayer->UpdateHitPointPercentage();
}
} // namespace devilution

12
Source/control/control_flasks.hpp

@ -0,0 +1,12 @@
#pragma once
#include <optional>
#include "engine/surface.hpp"
namespace devilution {
extern std::optional<OwnedSurface> pLifeBuff;
extern std::optional<OwnedSurface> pManaBuff;
} // namespace devilution

141
Source/control/control_gold.cpp

@ -0,0 +1,141 @@
#include "control.hpp"
#include "control_chat.hpp"
#include "DiabloUI/text_input.hpp"
#include "engine/render/clx_render.hpp"
#include "inv.h"
#include "utils/display.h"
#include "utils/format_int.hpp"
#include "utils/log.hpp"
#include "utils/sdl_compat.h"
namespace devilution {
bool DropGoldFlag;
TextInputCursorState GoldDropCursor;
char GoldDropText[21];
namespace {
int8_t GoldDropInvIndex;
std::optional<NumberInputState> GoldDropInputState;
void RemoveGold(Player &player, int goldIndex, int amount)
{
const int gi = goldIndex - INVITEM_INV_FIRST;
player.InvList[gi]._ivalue -= amount;
if (player.InvList[gi]._ivalue > 0) {
SetPlrHandGoldCurs(player.InvList[gi]);
NetSyncInvItem(player, gi);
} else {
player.RemoveInvItem(gi);
}
MakeGoldStack(player.HoldItem, amount);
NewCursor(player.HoldItem);
player._pGold = CalculateGold(player);
}
int GetGoldDropMax()
{
return GoldDropInputState->max();
}
} // namespace
void DrawGoldSplit(const Surface &out)
{
const int dialogX = 30;
ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*GoldBoxBuffer)[0]);
const std::string_view amountText = GoldDropText;
const TextInputCursorState &cursor = GoldDropCursor;
const int max = GetGoldDropMax();
const std::string description = fmt::format(
fmt::runtime(ngettext(
/* TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold.*/
"You have {:s} gold piece. How many do you want to remove?",
"You have {:s} gold pieces. How many do you want to remove?",
max)),
FormatInteger(max));
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words
const std::string wrapped = WordWrapString(description, 200);
// The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount.
// Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left
// for the text entered by the player.
DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } },
{ .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 });
// Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we
// use the Point form of DrawString.
DrawString(out, amountText, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }),
{
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor,
.cursorPosition = static_cast<int>(cursor.position),
.highlightRange = { static_cast<int>(cursor.selection.begin), static_cast<int>(cursor.selection.end) },
});
}
void control_drop_gold(SDL_Keycode vkey)
{
Player &myPlayer = *MyPlayer;
if (myPlayer.hasNoLife()) {
CloseGoldDrop();
return;
}
switch (vkey) {
case SDLK_RETURN:
case SDLK_KP_ENTER:
if (const int value = GoldDropInputState->value(); value != 0) {
RemoveGold(myPlayer, GoldDropInvIndex, value);
}
CloseGoldDrop();
break;
case SDLK_ESCAPE:
CloseGoldDrop();
break;
default:
break;
}
}
void OpenGoldDrop(int8_t invIndex, int max)
{
DropGoldFlag = true;
GoldDropInvIndex = invIndex;
GoldDropText[0] = '\0';
GoldDropInputState.emplace(NumberInputState::Options {
.textOptions {
.value = GoldDropText,
.cursor = &GoldDropCursor,
.maxLength = sizeof(GoldDropText) - 1,
},
.min = 0,
.max = max,
});
SDLC_StartTextInput(ghMainWnd);
}
void CloseGoldDrop()
{
if (!DropGoldFlag)
return;
SDLC_StopTextInput(ghMainWnd);
DropGoldFlag = false;
GoldDropInputState = std::nullopt;
GoldDropInvIndex = 0;
}
bool HandleGoldDropTextInputEvent(const SDL_Event &event)
{
return HandleInputEvent(event, GoldDropInputState);
}
} // namespace devilution

425
Source/control/control_infobox.cpp

@ -0,0 +1,425 @@
#include "control.hpp"
#include "control_panel.hpp"
#include "engine/render/primitive_render.hpp"
#include "inv.h"
#include "levels/trigs.h"
#include "panels/partypanel.hpp"
#include "qol/stash.h"
#include "qol/xpbar.h"
#include "towners.h"
#include "utils/algorithm/container.hpp"
#include "utils/format_int.hpp"
#include "utils/log.hpp"
#include "utils/screen_reader.hpp"
#include "utils/str_cat.hpp"
#include "utils/str_split.hpp"
namespace devilution {
StringOrView InfoString;
StringOrView FloatingInfoString;
namespace {
void PrintInfo(const Surface &out)
{
if (ChatFlag)
return;
const int space[] = { 18, 12, 6, 3, 0 };
Rectangle infoBox = InfoBoxRect;
SetPanelObjectPosition(UiPanels::Main, infoBox);
const auto newLineCount = static_cast<int>(c_count(InfoString.str(), '\n'));
const int spaceIndex = std::min(4, newLineCount);
const int spacing = space[spaceIndex];
const int lineHeight = 12 + spacing;
// Adjusting the line height to add spacing between lines
// will also add additional space beneath the last line
// which throws off the vertical centering
infoBox.position.y += spacing / 2;
SpeakText(InfoString);
DrawString(out, InfoString, infoBox,
{
.flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing,
.spacing = 2,
.lineHeight = lineHeight,
});
}
Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing)
{
// Calculate the width and height of the floating info box
const std::string txt = std::string(FloatingInfoString);
auto lines = SplitByChar(txt, '\n');
const GameFontTables font = GameFont12;
int maxW = 0;
for (const auto &line : lines) {
const int w = GetLineWidth(line, font, textSpacing, nullptr);
maxW = std::max(maxW, w);
}
const auto lineCount = 1 + static_cast<int>(c_count(FloatingInfoString.str(), '\n'));
const int totalH = lineCount * lineHeight;
const Player &player = *InspectPlayer;
// 1) Equipment (Rect position)
if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) {
const int slot = pcursinvitem - INVITEM_HEAD;
static constexpr Point equipLocal[] = {
{ 133, 59 },
{ 48, 205 },
{ 249, 205 },
{ 205, 60 },
{ 17, 160 },
{ 248, 160 },
{ 133, 160 },
};
Point itemPosition = equipLocal[slot];
auto &item = player.InvBody[slot];
const Size frame = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM);
if (slot == INVLOC_HAND_LEFT) {
itemPosition.x += frame.width == InventorySlotSizeInPixels.width
? InventorySlotSizeInPixels.width
: 0;
itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height
? 0
: -InventorySlotSizeInPixels.height;
} else if (slot == INVLOC_HAND_RIGHT) {
itemPosition.x += frame.width == InventorySlotSizeInPixels.width
? (InventorySlotSizeInPixels.width - 1)
: 1;
itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height
? 0
: -InventorySlotSizeInPixels.height;
}
itemPosition.y++; // Align position to bottom left of the item graphic
itemPosition.x += frame.width / 2; // Align position to center of the item graphic
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition);
return { { screen.x, screen.y }, { maxW, totalH } };
}
// 2) Inventory grid (Rect position)
if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) {
const int itemIdx = pcursinvitem - INVITEM_INV_FIRST;
for (int j = 0; j < InventoryGridCells; ++j) {
if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) {
const Item &it = player.InvList[itemIdx];
Point itemPosition = InvRect[j + SLOTXY_INV_FIRST].position;
itemPosition.x += GetInventorySize(it).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition);
return { { screen.x, screen.y }, { maxW, totalH } };
}
}
}
// 3) Belt (Rect position)
if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) {
const int itemIdx = pcursinvitem - INVITEM_BELT_FIRST;
for (int i = 0; i < MaxBeltItems; ++i) {
if (player.SpdList[i].isEmpty())
continue;
if (i != itemIdx)
continue;
const Item &item = player.SpdList[i];
Point itemPosition = InvRect[i + SLOTXY_BELT_FIRST].position;
itemPosition.x += GetInventorySize(item).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
const Point screen = GetMainPanel().position + Displacement { itemPosition.x, itemPosition.y };
return { { screen.x, screen.y }, { maxW, totalH } };
}
}
// 4) Stash (Rect position)
if (pcursstashitem != StashStruct::EmptyCell) {
for (auto slot : StashGridRange) {
auto itemId = Stash.GetItemIdAtPosition(slot);
if (itemId == StashStruct::EmptyCell)
continue;
if (itemId != pcursstashitem)
continue;
const Item &item = Stash.stashList[itemId];
Point itemPosition = GetStashSlotCoord(slot);
const Size itemGridSize = GetInventorySize(item);
itemPosition.y += itemGridSize.height * (InventorySlotSizeInPixels.height + 1) - 1; // Align position to bottom left of the item graphic
itemPosition.x += itemGridSize.width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
return { { itemPosition.x, itemPosition.y }, { maxW, totalH } };
}
}
return { { 0, 0 }, { 0, 0 } };
}
int GetHoverSpriteHeight()
{
if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) {
auto &it = (*InspectPlayer).InvBody[pcursinvitem - INVITEM_HEAD];
return GetInvItemSize(it._iCurs + CURSOR_FIRSTITEM).height + 1;
}
if (pcursinvitem >= INVITEM_INV_FIRST
&& pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) {
const int idx = pcursinvitem - INVITEM_INV_FIRST;
auto &it = (*InspectPlayer).InvList[idx];
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1)
- InventorySlotSizeInPixels.height;
}
if (pcursinvitem >= INVITEM_BELT_FIRST
&& pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) {
const int idx = pcursinvitem - INVITEM_BELT_FIRST;
auto &it = (*InspectPlayer).SpdList[idx];
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1)
- InventorySlotSizeInPixels.height - 1;
}
if (pcursstashitem != StashStruct::EmptyCell) {
auto &it = Stash.stashList[pcursstashitem];
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1);
}
return InventorySlotSizeInPixels.height;
}
int ClampAboveOrBelow(int anchorY, int spriteH, int boxH, int pad, int linePad)
{
const int yAbove = anchorY - spriteH - boxH - pad;
const int yBelow = anchorY + linePad / 2 + pad;
return (yAbove >= 0) ? yAbove : yBelow;
}
void PrintFloatingInfo(const Surface &out)
{
if (ChatFlag)
return;
if (FloatingInfoString.empty())
return;
const int verticalSpacing = 3;
const int lineHeight = 12 + verticalSpacing;
const int textSpacing = 2;
const int hPadding = 5;
const int vPadding = 4;
Rectangle floatingInfoBox = GetFloatingInfoRect(lineHeight, textSpacing);
// Prevent the floating info box from going off-screen horizontally
floatingInfoBox.position.x = std::clamp(floatingInfoBox.position.x, hPadding, GetScreenWidth() - (floatingInfoBox.size.width + hPadding));
const int spriteH = GetHoverSpriteHeight();
const int anchorY = floatingInfoBox.position.y;
// Prevent the floating info box from going off-screen vertically
floatingInfoBox.position.y = ClampAboveOrBelow(anchorY, spriteH, floatingInfoBox.size.height, vPadding, verticalSpacing);
SpeakText(FloatingInfoString);
for (int i = 0; i < 3; i++)
DrawHalfTransparentRectTo(out, floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding, floatingInfoBox.size.width + hPadding * 2, floatingInfoBox.size.height + vPadding * 2);
DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x - hPadding - 1, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10);
DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x + hPadding + floatingInfoBox.size.width, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10);
DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10);
DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y + vPadding + floatingInfoBox.size.height }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10);
DrawString(out, FloatingInfoString, floatingInfoBox,
{
.flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter,
.spacing = textSpacing,
.lineHeight = lineHeight,
});
}
} // namespace
void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/)
{
StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString;
if (infoString.empty())
infoString = str;
else
infoString = StrCat(infoString, "\n", str);
}
void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/)
{
StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString;
if (infoString.empty())
infoString = std::move(str);
else
infoString = StrCat(infoString, "\n", str);
}
void CheckPanelInfo()
{
MainPanelFlag = false;
InfoString = StringOrView {};
FloatingInfoString = StringOrView {};
const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons;
for (int i = 0; i < totalButtons; i++) {
Rectangle button = MainPanelButtonRect[i];
SetPanelObjectPosition(UiPanels::Main, button);
if (button.contains(MousePosition)) {
if (i != 7) {
InfoString = _(PanBtnStr[i]);
} else {
if (MyPlayer->friendlyMode)
InfoString = _("Player friendly");
else
InfoString = _("Player attack");
}
if (PanBtnHotKey[i] != nullptr) {
AddInfoBoxString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i])));
}
InfoColor = UiFlags::ColorWhite;
MainPanelFlag = true;
}
}
Rectangle spellSelectButton = SpellButtonRect;
SetPanelObjectPosition(UiPanels::Main, spellSelectButton);
if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) {
InfoString = _("Select current spell button");
InfoColor = UiFlags::ColorWhite;
MainPanelFlag = true;
AddInfoBoxString(_("Hotkey: 's'"));
const Player &myPlayer = *MyPlayer;
const SpellID spellId = myPlayer._pRSpell;
if (IsValidSpell(spellId)) {
switch (myPlayer._pRSplType) {
case SpellType::Skill:
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText)));
break;
case SpellType::Spell: {
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText)));
const int spellLevel = myPlayer.GetSpellLevel(spellId);
AddInfoBoxString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel));
} break;
case SpellType::Scroll: {
AddInfoBoxString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText)));
const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) {
return item.isScrollOf(spellId);
});
AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount));
} break;
case SpellType::Charges:
AddInfoBoxString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText)));
AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges));
break;
case SpellType::Invalid:
break;
}
}
}
Rectangle belt = BeltRect;
SetPanelObjectPosition(UiPanels::Main, belt);
if (belt.contains(MousePosition))
pcursinvitem = CheckInvHLight();
if (CheckXPBarInfo())
MainPanelFlag = true;
}
void DrawInfoBox(const Surface &out)
{
DrawPanelBox(out, MakeSdlRect(InfoBoxRect.position.x, InfoBoxRect.position.y + PanelPaddingHeight, InfoBoxRect.size.width, InfoBoxRect.size.height), GetMainPanel().position + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y });
if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) {
InfoString = StringOrView {};
InfoColor = UiFlags::ColorWhite;
}
const Player &myPlayer = *MyPlayer;
if (SpellSelectFlag || trigflag || pcurs == CURSOR_HOURGLASS) {
InfoColor = UiFlags::ColorWhite;
} else if (!myPlayer.HoldItem.isEmpty()) {
if (myPlayer.HoldItem._itype == ItemType::Gold) {
const int nGold = myPlayer.HoldItem._ivalue;
InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold));
} else if (!myPlayer.CanUseItem(myPlayer.HoldItem)) {
InfoString = _("Requirements not met");
} else {
InfoString = myPlayer.HoldItem.getName();
InfoColor = myPlayer.HoldItem.getTextColor();
}
} else {
if (pcursitem != -1)
GetItemStr(Items[pcursitem]);
else if (ObjectUnderCursor != nullptr)
GetObjectStr(*ObjectUnderCursor);
if (pcursmonst != -1) {
if (leveltype != DTYPE_TOWN) {
const Monster &monster = Monsters[pcursmonst];
InfoColor = UiFlags::ColorWhite;
InfoString = monster.name();
if (monster.isUnique()) {
InfoColor = UiFlags::ColorWhitegold;
PrintUniqueHistory();
} else {
PrintMonstHistory(monster.type().type);
}
} else if (pcursitem == -1) {
InfoString = std::string_view(Towners[pcursmonst].name);
}
}
if (PlayerUnderCursor != nullptr) {
InfoColor = UiFlags::ColorWhitegold;
const auto &target = *PlayerUnderCursor;
InfoString = std::string_view(target._pName);
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), target.getClassName(), target.getCharacterLevel()));
AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6));
}
if (PortraitIdUnderCursor != -1) {
InfoColor = UiFlags::ColorWhitegold;
auto &target = Players[PortraitIdUnderCursor];
InfoString = std::string_view(target._pName);
AddInfoBoxString(_("Right click to inspect"));
}
}
if (!InfoString.empty())
PrintInfo(out);
}
void DrawFloatingInfoBox(const Surface &out)
{
if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) {
FloatingInfoString = StringOrView {};
InfoColor = UiFlags::ColorWhite;
}
if (!FloatingInfoString.empty())
PrintFloatingInfo(out);
}
} // namespace devilution

832
Source/control/control_panel.cpp

@ -0,0 +1,832 @@
#include "control_panel.hpp"
#include "control.hpp"
#include "control_chat.hpp"
#include "control_flasks.hpp"
#include "automap.h"
#include "controls/control_mode.hpp"
#include "controls/modifier_hints.h"
#include "diablo_msg.hpp"
#include "engine/backbuffer_state.hpp"
#include "engine/load_cel.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/trn.hpp"
#include "gamemenu.h"
#include "headless_mode.hpp"
#include "minitext.h"
#include "options.h"
#include "panels/charpanel.hpp"
#include "panels/mainpanel.hpp"
#include "panels/partypanel.hpp"
#include "panels/spell_book.hpp"
#include "panels/spell_icons.hpp"
#include "panels/spell_list.hpp"
#include "pfile.h"
#include "qol/stash.h"
#include "stores.h"
#include "utils/sdl_compat.h"
namespace devilution {
bool CharPanelButton[4];
bool LevelButtonDown;
bool CharPanelButtonActive;
UiFlags InfoColor;
int SpellbookTab;
bool ChatFlag;
bool SpellbookFlag;
bool CharFlag;
bool MainPanelFlag;
bool MainPanelButtonDown;
bool SpellSelectFlag;
Rectangle MainPanel;
Rectangle LeftPanel;
Rectangle RightPanel;
std::optional<OwnedSurface> BottomBuffer;
OptionalOwnedClxSpriteList GoldBoxBuffer;
const Rectangle &GetMainPanel()
{
return MainPanel;
}
const Rectangle &GetLeftPanel()
{
return LeftPanel;
}
const Rectangle &GetRightPanel()
{
return RightPanel;
}
bool IsLeftPanelOpen()
{
return CharFlag || QuestLogIsOpen || IsStashOpen;
}
bool IsRightPanelOpen()
{
return invflag || SpellbookFlag;
}
constexpr Size IncrementAttributeButtonSize { 41, 22 };
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */
Rectangle CharPanelButtonRect[4] = {
{ { 137, 138 }, IncrementAttributeButtonSize },
{ { 137, 166 }, IncrementAttributeButtonSize },
{ { 137, 195 }, IncrementAttributeButtonSize },
{ { 137, 223 }, IncrementAttributeButtonSize }
};
constexpr Size WidePanelButtonSize { 71, 20 };
constexpr Size PanelButtonSize { 33, 32 };
/** Positions of panel buttons. */
Rectangle MainPanelButtonRect[8] = {
// clang-format off
{ { 9, 9 }, WidePanelButtonSize }, // char button
{ { 9, 35 }, WidePanelButtonSize }, // quests button
{ { 9, 75 }, WidePanelButtonSize }, // map button
{ { 9, 101 }, WidePanelButtonSize }, // menu button
{ { 560, 9 }, WidePanelButtonSize }, // inv button
{ { 560, 35 }, WidePanelButtonSize }, // spells button
{ { 87, 91 }, PanelButtonSize }, // chat button
{ { 527, 91 }, PanelButtonSize }, // friendly fire button
// clang-format on
};
Rectangle LevelButtonRect = { { 40, -39 }, { 41, 22 } };
constexpr int BeltItems = 8;
constexpr Size BeltSize { (INV_SLOT_SIZE_PX + 1) * BeltItems, INV_SLOT_SIZE_PX };
Rectangle BeltRect { { 205, 5 }, BeltSize };
Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } };
int PanelPaddingHeight = 16;
/** Maps from panel_button_id to panel button description. */
const char *const PanBtnStr[8] = {
N_("Character Information"),
N_("Quests log"),
N_("Automap"),
N_("Main Menu"),
N_("Inventory"),
N_("Spell book"),
N_("Send Message"),
"" // Player attack
};
/** Maps from panel_button_id to hotkey name. */
const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr };
int TotalSpMainPanelButtons = 6;
int TotalMpMainPanelButtons = 8;
namespace {
OptionalOwnedClxSpriteList pDurIcons;
OptionalOwnedClxSpriteList multiButtons;
OptionalOwnedClxSpriteList pMainPanelButtons;
enum panel_button_id : uint8_t {
PanelButtonCharinfo,
PanelButtonFirst = PanelButtonCharinfo,
PanelButtonQlog,
PanelButtonAutomap,
PanelButtonMainmenu,
PanelButtonInventory,
PanelButtonSpellbook,
PanelButtonSendmsg,
PanelButtonFriendly,
PanelButtonLast = PanelButtonFriendly,
};
bool MainPanelButtons[PanelButtonLast + 1];
void SetMainPanelButtonDown(int btnId)
{
MainPanelButtons[btnId] = true;
RedrawComponent(PanelDrawComponent::ControlButtons);
MainPanelButtonDown = true;
}
void SetMainPanelButtonUp()
{
RedrawComponent(PanelDrawComponent::ControlButtons);
MainPanelButtonDown = false;
}
int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute)
{
const int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute);
return std::min(remainingStatPoints, pointsToReachCap);
}
int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c)
{
const int durabilityThresholdGold = 5;
const int durabilityThresholdRed = 2;
if (pItem.isEmpty())
return x;
if (pItem._iDurability > durabilityThresholdGold)
return x;
if (c == 0) {
switch (pItem._itype) {
case ItemType::Sword:
c = 1;
break;
case ItemType::Axe:
c = 5;
break;
case ItemType::Bow:
c = 6;
break;
case ItemType::Mace:
c = 4;
break;
case ItemType::Staff:
c = 7;
break;
case ItemType::Shield:
default:
c = 0;
break;
}
}
// Calculate how much of the icon should be gold and red
const int height = (*pDurIcons)[c].height(); // Height of durability icon CEL
int partition = 0;
if (pItem._iDurability > durabilityThresholdRed) {
const int current = pItem._iDurability - durabilityThresholdRed;
partition = (height * current) / (durabilityThresholdGold - durabilityThresholdRed);
}
// Draw icon
const int y = -17 + GetMainPanel().position.y;
if (partition > 0) {
const Surface stenciledBuffer = out.subregionY(y - partition, partition);
ClxDraw(stenciledBuffer, { x, partition }, (*pDurIcons)[c + 8]); // Gold icon
}
if (partition != height) {
const Surface stenciledBuffer = out.subregionY(y - height, height - partition);
ClxDraw(stenciledBuffer, { x, height }, (*pDurIcons)[c]); // Red icon
}
return x - (*pDurIcons)[c].height() - 8; // Add in spacing for the next durability icon
}
bool IsLevelUpButtonVisible()
{
if (SpellSelectFlag || CharFlag || MyPlayer->_pStatPts == 0) {
return false;
}
if (ControlMode == ControlTypes::VirtualGamepad) {
return false;
}
if (IsPlayerInStore() || IsStashOpen) {
return false;
}
if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) {
return false;
}
return true;
}
} // namespace
void CalculatePanelAreas()
{
constexpr Size MainPanelSize { 640, 128 };
MainPanel = {
{ (gnScreenWidth - MainPanelSize.width) / 2, gnScreenHeight - MainPanelSize.height },
MainPanelSize
};
LeftPanel = {
{ 0, 0 },
SidePanelSize
};
RightPanel = {
{ 0, 0 },
SidePanelSize
};
if (ControlMode == ControlTypes::VirtualGamepad) {
LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width;
} else {
if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > MainPanel.size.width) {
LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - MainPanel.size.width) / 2;
}
}
LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - MainPanel.size.height) / 2;
if (ControlMode == ControlTypes::VirtualGamepad) {
RightPanel.position.x = gnScreenWidth / 2;
} else {
RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x;
}
RightPanel.position.y = LeftPanel.position.y;
gnViewportHeight = gnScreenHeight;
if (gnScreenWidth <= MainPanel.size.width) {
// Part of the screen is fully obscured by the UI
gnViewportHeight -= MainPanel.size.height;
}
}
void FocusOnCharInfo()
{
const Player &myPlayer = *MyPlayer;
if (invflag || myPlayer._pStatPts <= 0)
return;
// Find the first incrementable stat.
int stat = -1;
for (auto attribute : enum_values<CharacterAttribute>()) {
if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute))
continue;
stat = static_cast<int>(attribute);
}
if (stat == -1)
return;
SetCursorPos(CharPanelButtonRect[stat].Center());
}
void OpenCharPanel()
{
QuestLogIsOpen = false;
CloseGoldWithdraw();
CloseStash();
CharFlag = true;
}
void CloseCharPanel()
{
CharFlag = false;
if (IsInspectingPlayer()) {
InspectPlayer = MyPlayer;
RedrawEverything();
if (InspectingFromPartyPanel)
InspectingFromPartyPanel = false;
else
InitDiabloMsg(_("Stopped inspecting players."));
}
}
void ToggleCharPanel()
{
if (CharFlag)
CloseCharPanel();
else
OpenCharPanel();
}
Point GetPanelPosition(UiPanels panel, Point offset)
{
const Displacement displacement { offset.x, offset.y };
switch (panel) {
case UiPanels::Main:
return GetMainPanel().position + displacement;
case UiPanels::Quest:
case UiPanels::Character:
case UiPanels::Stash:
return GetLeftPanel().position + displacement;
case UiPanels::Spell:
case UiPanels::Inventory:
return GetRightPanel().position + displacement;
default:
return GetMainPanel().position + displacement;
}
}
void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition)
{
out.BlitFrom(*BottomBuffer, srcRect, targetPosition);
}
tl::expected<void, std::string> InitMainPanel()
{
if (!HeadlessMode) {
BottomBuffer.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + PanelPaddingHeight) * (IsChatAvailable() ? 2 : 1));
pManaBuff.emplace(88, 88);
pLifeBuff.emplace(88, 88);
RETURN_IF_ERROR(LoadPartyPanel());
RETURN_IF_ERROR(LoadCharPanel());
RETURN_IF_ERROR(LoadLargeSpellIcons());
{
ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\panel8", GetMainPanel().size.width));
ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) - 1 }, sprite[0]);
}
{
const Point bulbsPosition { 0, 87 };
ASSIGN_OR_RETURN(const OwnedClxSpriteList statusPanel, LoadCelWithStatus("ctrlpan\\p8bulbs", 88));
ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]);
ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]);
}
}
ChatFlag = false;
ChatInputState = std::nullopt;
if (IsChatAvailable()) {
if (!HeadlessMode) {
{
ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\talkpanl", GetMainPanel().size.width));
ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) * 2 - 1 }, sprite[0]);
}
multiButtons = LoadCel("ctrlpan\\p8but2", 33);
talkButtons = LoadCel("ctrlpan\\talkbutt", 61);
}
sgbPlrTalkTbl = 0;
TalkMessage[0] = '\0';
for (bool &whisper : WhisperList)
whisper = true;
for (bool &talkButtonDown : TalkButtonsDown)
talkButtonDown = false;
}
MainPanelFlag = false;
LevelButtonDown = false;
if (!HeadlessMode) {
RETURN_IF_ERROR(LoadMainPanel());
ASSIGN_OR_RETURN(pMainPanelButtons, LoadCelWithStatus("ctrlpan\\panel8bu", 71));
static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 };
ASSIGN_OR_RETURN(pChrButtons, LoadCelWithStatus("data\\charbut", CharButtonsFrameWidths));
}
ResetMainPanelButtons();
if (!HeadlessMode)
pDurIcons = LoadCel("items\\duricons", 32);
for (bool &buttonEnabled : CharPanelButton)
buttonEnabled = false;
CharPanelButtonActive = false;
InfoString = StringOrView {};
FloatingInfoString = StringOrView {};
RedrawComponent(PanelDrawComponent::Health);
RedrawComponent(PanelDrawComponent::Mana);
CloseCharPanel();
SpellSelectFlag = false;
SpellbookTab = 0;
SpellbookFlag = false;
if (!HeadlessMode) {
InitSpellBook();
ASSIGN_OR_RETURN(pQLogCel, LoadCelWithStatus("data\\quest", static_cast<uint16_t>(SidePanelSize.width)));
ASSIGN_OR_RETURN(GoldBoxBuffer, LoadCelWithStatus("ctrlpan\\golddrop", 261));
}
CloseGoldDrop();
CalculatePanelAreas();
if (!HeadlessMode)
InitModifierHints();
return {};
}
void DrawMainPanel(const Surface &out)
{
DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + PanelPaddingHeight, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position);
DrawInfoBox(out);
}
void DrawMainPanelButtons(const Surface &out)
{
const Point mainPanelPosition = GetMainPanel().position;
for (int i = 0; i < TotalSpMainPanelButtons; i++) {
if (!MainPanelButtons[i]) {
DrawPanelBox(out, MakeSdlRect(MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y + PanelPaddingHeight, MainPanelButtonRect[i].size.width, MainPanelButtonRect[i].size.height + 1), mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y });
} else {
const Point position = mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y };
RenderClxSprite(out, (*pMainPanelButtons)[i], position);
RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, 0 });
}
}
if (IsChatAvailable()) {
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonSendmsg] ? 1 : 0], mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonSendmsg].position.x, MainPanelButtonRect[PanelButtonSendmsg].position.y });
const Point friendlyButtonPosition = mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonFriendly].position.x, MainPanelButtonRect[PanelButtonFriendly].position.y };
if (MyPlayer->friendlyMode)
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 3 : 2], friendlyButtonPosition);
else
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 5 : 4], friendlyButtonPosition);
}
}
void ResetMainPanelButtons()
{
for (bool &panelButton : MainPanelButtons)
panelButton = false;
SetMainPanelButtonUp();
}
void CheckMainPanelButton()
{
const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons;
for (int i = 0; i < totalButtons; i++) {
Rectangle button = MainPanelButtonRect[i];
SetPanelObjectPosition(UiPanels::Main, button);
if (button.contains(MousePosition)) {
SetMainPanelButtonDown(i);
}
}
Rectangle spellSelectButton = SpellButtonRect;
SetPanelObjectPosition(UiPanels::Main, spellSelectButton);
if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) {
if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) {
Player &myPlayer = *MyPlayer;
myPlayer._pRSpell = SpellID::Invalid;
myPlayer._pRSplType = SpellType::Invalid;
RedrawEverything();
return;
}
DoSpeedBook();
gamemenu_off();
}
}
void CheckMainPanelButtonDead()
{
Rectangle menuButton = MainPanelButtonRect[PanelButtonMainmenu];
SetPanelObjectPosition(UiPanels::Main, menuButton);
if (menuButton.contains(MousePosition)) {
SetMainPanelButtonDown(PanelButtonMainmenu);
return;
}
Rectangle chatButton = MainPanelButtonRect[PanelButtonSendmsg];
SetPanelObjectPosition(UiPanels::Main, chatButton);
if (chatButton.contains(MousePosition)) {
SetMainPanelButtonDown(PanelButtonSendmsg);
}
}
void DoAutoMap()
{
if (!AutomapActive)
StartAutomap();
else
AutomapActive = false;
}
void CycleAutomapType()
{
if (!AutomapActive) {
StartAutomap();
return;
}
const AutomapType newType { static_cast<std::underlying_type_t<AutomapType>>(
(static_cast<unsigned>(GetAutomapType()) + 1) % enum_size<AutomapType>::value) };
SetAutomapType(newType);
if (newType == AutomapType::FIRST) {
AutomapActive = false;
}
}
void CheckMainPanelButtonUp()
{
bool gamemenuOff = true;
SetMainPanelButtonUp();
for (int i = PanelButtonFirst; i <= PanelButtonLast; i++) {
if (!MainPanelButtons[i])
continue;
MainPanelButtons[i] = false;
Rectangle button = MainPanelButtonRect[i];
SetPanelObjectPosition(UiPanels::Main, button);
if (!button.contains(MousePosition))
continue;
switch (i) {
case PanelButtonCharinfo:
ToggleCharPanel();
break;
case PanelButtonQlog:
CloseCharPanel();
CloseGoldWithdraw();
CloseStash();
if (!QuestLogIsOpen)
StartQuestlog();
else
QuestLogIsOpen = false;
break;
case PanelButtonAutomap:
DoAutoMap();
break;
case PanelButtonMainmenu:
if (MyPlayerIsDead) {
if (!gbIsMultiplayer) {
if (gbValidSaveFile)
gamemenu_load_game(false);
else
gamemenu_exit_game(false);
} else {
NetSendCmd(true, CMD_RETOWN);
}
break;
} else if (MyPlayer->hasNoLife()) {
break;
}
qtextflag = false;
gamemenu_handle_previous();
gamemenuOff = false;
break;
case PanelButtonInventory:
SpellbookFlag = false;
CloseGoldWithdraw();
CloseStash();
invflag = !invflag;
CloseGoldDrop();
break;
case PanelButtonSpellbook:
CloseInventory();
CloseGoldDrop();
SpellbookFlag = !SpellbookFlag;
break;
case PanelButtonSendmsg:
if (ChatFlag)
ResetChat();
else
TypeChatMessage();
break;
case PanelButtonFriendly:
// Toggle friendly Mode
NetSendCmd(true, CMD_FRIENDLYMODE);
break;
}
}
if (gamemenuOff)
gamemenu_off();
}
void FreeControlPan()
{
BottomBuffer = std::nullopt;
pManaBuff = std::nullopt;
pLifeBuff = std::nullopt;
FreeLargeSpellIcons();
FreeSpellBook();
pMainPanelButtons = std::nullopt;
multiButtons = std::nullopt;
talkButtons = std::nullopt;
pChrButtons = std::nullopt;
pDurIcons = std::nullopt;
pQLogCel = std::nullopt;
GoldBoxBuffer = std::nullopt;
FreeMainPanel();
FreePartyPanel();
FreeCharPanel();
FreeModifierHints();
}
void CheckLevelButton()
{
if (!IsLevelUpButtonVisible()) {
return;
}
Rectangle button = LevelButtonRect;
SetPanelObjectPosition(UiPanels::Main, button);
if (!LevelButtonDown && button.contains(MousePosition))
LevelButtonDown = true;
}
void CheckLevelButtonUp()
{
Rectangle button = LevelButtonRect;
SetPanelObjectPosition(UiPanels::Main, button);
if (button.contains(MousePosition)) {
OpenCharPanel();
}
LevelButtonDown = false;
}
void DrawLevelButton(const Surface &out)
{
if (IsLevelUpButtonVisible()) {
const int nCel = LevelButtonDown ? 2 : 1;
DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, LevelButtonRect.position.y - 23 }, { 120, 0 } },
{ .flags = UiFlags::ColorWhite | UiFlags::AlignCenter | UiFlags::KerningFitSpacing });
RenderClxSprite(out, (*pChrButtons)[nCel], GetMainPanel().position + Displacement { LevelButtonRect.position.x, LevelButtonRect.position.y });
}
}
void CheckChrBtns()
{
const Player &myPlayer = *MyPlayer;
if (myPlayer._pmode == PM_DEATH)
return;
if (CharPanelButtonActive || myPlayer._pStatPts == 0)
return;
for (auto attribute : enum_values<CharacterAttribute>()) {
if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute))
continue;
auto buttonId = static_cast<size_t>(attribute);
Rectangle button = CharPanelButtonRect[buttonId];
SetPanelObjectPosition(UiPanels::Character, button);
if (button.contains(MousePosition)) {
CharPanelButton[buttonId] = true;
CharPanelButtonActive = true;
}
}
}
void ReleaseChrBtns(bool addAllStatPoints)
{
const Player &myPlayer = *MyPlayer;
if (myPlayer._pmode == PM_DEATH)
return;
CharPanelButtonActive = false;
for (auto attribute : enum_values<CharacterAttribute>()) {
auto buttonId = static_cast<size_t>(attribute);
if (!CharPanelButton[buttonId])
continue;
CharPanelButton[buttonId] = false;
Rectangle button = CharPanelButtonRect[buttonId];
SetPanelObjectPosition(UiPanels::Character, button);
if (button.contains(MousePosition)) {
Player &myPlayer = *MyPlayer;
int statPointsToAdd = 1;
if (addAllStatPoints)
statPointsToAdd = CapStatPointsToAdd(myPlayer._pStatPts, myPlayer, attribute);
switch (attribute) {
case CharacterAttribute::Strength:
NetSendCmdParam1(true, CMD_ADDSTR, statPointsToAdd);
myPlayer._pStatPts -= statPointsToAdd;
break;
case CharacterAttribute::Magic:
NetSendCmdParam1(true, CMD_ADDMAG, statPointsToAdd);
myPlayer._pStatPts -= statPointsToAdd;
break;
case CharacterAttribute::Dexterity:
NetSendCmdParam1(true, CMD_ADDDEX, statPointsToAdd);
myPlayer._pStatPts -= statPointsToAdd;
break;
case CharacterAttribute::Vitality:
NetSendCmdParam1(true, CMD_ADDVIT, statPointsToAdd);
myPlayer._pStatPts -= statPointsToAdd;
break;
}
}
}
}
void DrawDurIcon(const Surface &out)
{
const bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16;
const bool hasRoomUnderPanels = MainPanel.position.y - (RightPanel.position.y + RightPanel.size.height) >= 16 + 32 + 16;
if (!hasRoomBetweenPanels && !hasRoomUnderPanels) {
if (IsLeftPanelOpen() && IsRightPanelOpen())
return;
}
int x = MainPanel.position.x + MainPanel.size.width - 32 - 16;
if (!hasRoomUnderPanels) {
if (IsRightPanelOpen() && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x)
x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x;
}
Player &myPlayer = *MyPlayer;
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 3);
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 2);
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_LEFT], x, 0);
DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_RIGHT], x, 0);
}
void RedBack(const Surface &out)
{
uint8_t *dst = out.begin();
uint8_t *tbl = GetPauseTRN();
for (int h = gnViewportHeight; h != 0; h--, dst += out.pitch() - gnScreenWidth) {
for (int w = gnScreenWidth; w != 0; w--) {
if (leveltype != DTYPE_HELL || *dst >= 32)
*dst = tbl[*dst];
dst++;
}
}
}
void DrawDeathText(const Surface &out)
{
const TextRenderOptions largeTextOptions {
.flags = UiFlags::FontSize42 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter,
.spacing = 2
};
const TextRenderOptions smallTextOptions {
.flags = UiFlags::FontSize30 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter,
.spacing = 2
};
std::string text;
const int verticalPadding = 42;
Point linePosition { 0, gnScreenHeight / 2 - (verticalPadding * 2) };
text = _("You have died");
DrawString(out, text, linePosition, largeTextOptions);
linePosition.y += verticalPadding;
std::string buttonText;
switch (ControlMode) {
case ControlTypes::KeyboardAndMouse:
buttonText = _("ESC");
break;
case ControlTypes::Gamepad:
buttonText = ToString(GamepadType, ControllerButton_BUTTON_START);
break;
case ControlTypes::VirtualGamepad:
buttonText = _("Menu Button");
break;
default:
break;
}
if (!gbIsMultiplayer) {
if (gbValidSaveFile)
text = fmt::format(fmt::runtime(_("Press {} to load last save.")), buttonText);
else
text = fmt::format(fmt::runtime(_("Press {} to return to Main Menu.")), buttonText);
} else {
text = fmt::format(fmt::runtime(_("Press {} to restart in town.")), buttonText);
}
DrawString(out, text, linePosition, smallTextOptions);
}
void SetPanelObjectPosition(UiPanels panel, Rectangle &button)
{
button.position = GetPanelPosition(panel, button.position);
}
} // namespace devilution

18
Source/control/control_panel.hpp

@ -0,0 +1,18 @@
#pragma once
#include "engine/rectangle.hpp"
#include "panels/ui_panels.hpp"
namespace devilution {
extern int TotalSpMainPanelButtons;
extern int TotalMpMainPanelButtons;
extern int PanelPaddingHeight;
extern const char *const PanBtnStr[8];
extern const char *const PanBtnHotKey[8];
extern Rectangle SpellButtonRect;
extern Rectangle BeltRect;
void SetPanelObjectPosition(UiPanels panel, Rectangle &button);
} // namespace devilution

2
Source/controls/controller_motion.cpp

@ -9,7 +9,7 @@
#include <SDL.h>
#endif
#include "control.h"
#include "control/control.hpp"
#include "controls/control_mode.hpp"
#include "controls/controller.h"
#ifndef USE_SDL1

2
Source/controls/keymapper.cpp

@ -12,7 +12,7 @@
#endif
#endif
#include "control.h"
#include "control/control.hpp"
#include "options.h"
#include "utils/is_of.hpp"

2
Source/controls/modifier_hints.cpp

@ -4,7 +4,7 @@
#include <cstdint>
#include "DiabloUI/ui_flags.hpp"
#include "control.h"
#include "control/control.hpp"
#include "controls/controller_motion.h"
#include "controls/game_controls.h"
#include "controls/plrctrls.h"

4
Source/controls/plrctrls.cpp

@ -18,7 +18,7 @@
#endif
#include "automap.h"
#include "control.h"
#include "control/control.hpp"
#include "controls/controller_motion.h"
#ifndef USE_SDL1
#include "controls/devices/game_controller.h"
@ -539,7 +539,7 @@ void Interact()
return;
}
if (leveltype != DTYPE_TOWN && PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) {
if (leveltype != DTYPE_TOWN && PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) {
NetSendCmdParam1(true, myPlayer.UsesRangedWeapon() ? CMD_RATTACKPID : CMD_ATTACKPID, PlayerUnderCursor->getId());
LastPlayerAction = PlayerActionType::AttackPlayerTarget;
return;

2
Source/controls/touch/event_handlers.cpp

@ -7,7 +7,7 @@
#include <SDL.h>
#endif
#include "control.h"
#include "control/control.hpp"
#include "controls/plrctrls.h"
#include "cursor.h"
#include "diablo.h"

2
Source/controls/touch/gamepad.cpp

@ -6,7 +6,7 @@
#include <SDL.h>
#endif
#include "control.h"
#include "control/control.hpp"
#include "controls/touch/event_handlers.h"
#include "controls/touch/gamepad.h"
#include "quests.h"

2
Source/controls/touch/renderers.cpp

@ -6,7 +6,7 @@
#include <SDL.h>
#endif
#include "control.h"
#include "control/control.hpp"
#include "cursor.h"
#include "diablo.h"
#include "doom.h"

2
Source/cursor.cpp

@ -21,7 +21,7 @@
#include <fmt/format.h>
#include "DiabloUI/diabloui.h"
#include "control.h"
#include "control/control.hpp"
#include "controls/control_mode.hpp"
#include "controls/plrctrls.h"
#include "doom.h"

2
Source/dead.cpp

@ -11,8 +11,8 @@
#include "headless_mode.hpp"
#include "levels/gendung.h"
#include "lighting.h"
#include "misdat.h"
#include "monster.h"
#include "tables/misdat.h"
namespace devilution {

12
Source/diablo.cpp

@ -27,7 +27,7 @@
#include "appfat.h"
#include "automap.h"
#include "capture.h"
#include "control.h"
#include "control/control.hpp"
#include "cursor.h"
#include "dead.h"
#ifdef _DEBUG
@ -76,7 +76,6 @@
#include "menu.h"
#include "minitext.h"
#include "missiles.h"
#include "monstdat.h"
#include "movie.h"
#include "multi.h"
#include "nthread.h"
@ -88,7 +87,6 @@
#include "panels/spell_book.hpp"
#include "panels/spell_list.hpp"
#include "pfile.h"
#include "playerdat.hpp"
#include "plrmsg.h"
#include "qol/chatlog.h"
#include "qol/floatingnumbers.h"
@ -101,6 +99,8 @@
#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"
@ -281,7 +281,7 @@ void LeftMouseCmd(bool bShift)
LastPlayerAction = PlayerActionType::AttackMonsterTarget;
NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst);
}
} else if (PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) {
} else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) {
LastPlayerAction = PlayerActionType::AttackPlayerTarget;
NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId());
}
@ -301,7 +301,7 @@ void LeftMouseCmd(bool bShift)
} else if (pcursmonst != -1) {
LastPlayerAction = PlayerActionType::AttackMonsterTarget;
NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst);
} else if (PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) {
} else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) {
LastPlayerAction = PlayerActionType::AttackPlayerTarget;
NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId());
}
@ -2853,7 +2853,7 @@ bool TryIconCurs()
NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), static_cast<uint16_t>(sd), spellFrom);
} else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) {
NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);
} else if (PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) {
} else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) {
NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);
} else {
NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);

2
Source/diablo.h

@ -19,7 +19,7 @@
#endif
#ifdef _DEBUG
#include "monstdat.h"
#include "tables/monstdat.h"
#endif
#include "levels/gendung.h"
#include "utils/attributes.h"

2
Source/discord/discord.cpp

@ -23,7 +23,7 @@
#include "multi.h"
#include "panels/charpanel.hpp"
#include "player.h"
#include "playerdat.hpp"
#include "tables/playerdat.hpp"
#include "utils/language.h"
#include "utils/str_cat.hpp"

2
Source/doom.cpp

@ -7,7 +7,7 @@
#include <optional>
#include "control.h"
#include "control/control.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/load_cel.hpp"
#include "engine/render/clx_render.hpp"

5
Source/dvlnet/base.cpp

@ -155,6 +155,10 @@ tl::expected<void, PacketError> base::HandleDisconnect(packet &pkt)
tl::expected<void, PacketError> base::HandleEchoRequest(packet &pkt)
{
// If we have already left the game,
// there is no need to respond to echoes
if (plr_self == PLR_BROADCAST) return {};
return pkt.Time()
.and_then([&](cookie_t &&pktTime) {
return pktfty->make_packet<PT_ECHO_REPLY>(plr_self, pkt.Source(), pktTime);
@ -168,6 +172,7 @@ tl::expected<void, PacketError> base::HandleEchoReply(packet &pkt)
{
const uint32_t now = SDL_GetTicks();
plr_t src = pkt.Source();
if (src >= MAX_PLRS) return {};
return pkt.Time().transform([&](cookie_t &&pktTime) {
PlayerState &playerState = playerStateTable_[src];
playerState.roundTripLatency = now - pktTime;

7
Source/engine/demomode.cpp

@ -127,7 +127,6 @@ struct {
bool autoElixirPickup = false;
bool autoOilPickup = false;
bool autoPickupInTown = false;
bool adriaRefillsMana = false;
bool autoEquipWeapons = false;
bool autoEquipArmor = false;
bool autoEquipHelms = false;
@ -166,7 +165,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le
DemoSettings.autoElixirPickup = ReadByte(in) != 0;
DemoSettings.autoOilPickup = ReadByte(in) != 0;
DemoSettings.autoPickupInTown = ReadByte(in) != 0;
DemoSettings.adriaRefillsMana = ReadByte(in) != 0;
(void)ReadByte(in); // adriaRefillsMana (removed feature, kept for backward compatibility)
DemoSettings.autoEquipWeapons = ReadByte(in) != 0;
DemoSettings.autoEquipArmor = ReadByte(in) != 0;
DemoSettings.autoEquipHelms = ReadByte(in) != 0;
@ -195,7 +194,6 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le
{ _("Auto Elixir Pickup"), DemoSettings.autoGoldPickup },
{ _("Auto Oil Pickup"), DemoSettings.autoOilPickup },
{ _("Auto Pickup in Town"), DemoSettings.autoPickupInTown },
{ _("Adria Refills Mana"), DemoSettings.adriaRefillsMana },
{ _("Auto Equip Weapons"), DemoSettings.autoEquipWeapons },
{ _("Auto Equip Armor"), DemoSettings.autoEquipArmor },
{ _("Auto Equip Helms"), DemoSettings.autoEquipHelms },
@ -231,7 +229,7 @@ void WriteSettings(FILE *out)
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoElixirPickup));
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoOilPickup));
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoPickupInTown));
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.adriaRefillsMana));
WriteByte(out, 0); // adriaRefillsMana (removed feature, kept for backward compatibility)
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoEquipWeapons));
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoEquipArmor));
WriteByte(out, static_cast<uint8_t>(*options.Gameplay.autoEquipHelms));
@ -650,7 +648,6 @@ void OverrideOptions()
options.Gameplay.autoElixirPickup.SetValue(DemoSettings.autoElixirPickup);
options.Gameplay.autoOilPickup.SetValue(DemoSettings.autoOilPickup);
options.Gameplay.autoPickupInTown.SetValue(DemoSettings.autoPickupInTown);
options.Gameplay.adriaRefillsMana.SetValue(DemoSettings.adriaRefillsMana);
options.Gameplay.autoEquipWeapons.SetValue(DemoSettings.autoEquipWeapons);
options.Gameplay.autoEquipArmor.SetValue(DemoSettings.autoEquipArmor);
options.Gameplay.autoEquipHelms.SetValue(DemoSettings.autoEquipHelms);

810
Source/engine/random.hpp

@ -1,405 +1,405 @@
/**
* @file random.hpp
*
* Contains convenience functions for random number generation
*
* This includes specific engine/distribution functions for logic that needs to be compatible with the base game.
*/
#pragma once
#include <algorithm>
#include <cstdint>
#include <initializer_list>
#include <random>
namespace devilution {
class DiabloGenerator {
private:
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> lcg;
public:
/**
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state
*/
DiabloGenerator(uint32_t seed)
{
lcg.seed(seed);
}
/**
* @brief Advance the global RandomNumberEngine state by the specified number of rounds
*
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated.
* @param count How many values to discard
*/
void discardRandomValues(unsigned count)
{
lcg.discard(count);
}
/**
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG
*
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31.
*
* This function is only used when the base game wants to store the seed used to generate an item or level, however
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game.
*
* @return A random number in the range [0,2^31) or -2^31
*/
int32_t advanceRndSeed()
{
const int32_t seed = static_cast<int32_t>(lcg());
// since abs(INT_MIN) is undefined behavior, handle this value specially
return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed);
}
/**
* @brief Generates a random integer less than the given limit using the vanilla RNG
*
* If v is not a positive number this function returns 0 without calling the RNG.
*
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value
* less than 32768 for limits in that range.
*
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed()
*
* @see AdvanceRndSeed()
* @param v The upper limit for the return value
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1]
*/
int32_t generateRnd(int32_t v)
{
if (v <= 0)
return 0;
if (v <= 0x7FFF) // use the high bits to correct for LCG bias
return (advanceRndSeed() >> 16) % v;
return advanceRndSeed() % v;
}
/**
* @brief Generates a random boolean value using the vanilla RNG
*
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2
* represents a 50/50 chance.
*
* @param frequency odds of returning a true value
* @return A random boolean value
*/
bool flipCoin(unsigned frequency)
{
// Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned.
return generateRnd(static_cast<int32_t>(frequency)) == 0;
}
/**
* @brief Picks one of the elements in the list randomly.
*
* @param values The values to pick from
* @return A random value from the 'values' list.
*/
template <typename T>
const T pickRandomlyAmong(const std::initializer_list<T> &values)
{
const auto index { std::max<int32_t>(generateRnd(static_cast<int32_t>(values.size())), 0) };
return *(values.begin() + index);
}
/**
* @brief Generates a random non-negative integer
*
* Effectively the same as GenerateRnd but will never return a negative value
* @param v upper limit for the return value
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v)
*/
inline int32_t randomIntLessThan(int32_t v)
{
return std::max<int32_t>(generateRnd(v), 0);
}
/**
* @brief Randomly chooses a value somewhere within the given range
* @param min lower limit, minimum possible value
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range
* @param halfOpen whether to use the limits as a half-open range or not
* @return a randomly selected integer
*/
inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false)
{
return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min;
}
};
// Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008
// https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68
// and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix32 {
uint32_t state;
public:
SplitMix32(uint32_t state)
: state(state)
{
}
uint32_t next()
{
uint32_t z = (state += 0x9e3779b9);
z = (z ^ (z >> 16)) * 0x85ebca6b;
z = (z ^ (z >> 13)) * 0xc2b2ae35;
return z ^ (z >> 16);
}
void generate(uint32_t *begin, const uint32_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};
// Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix64 {
uint64_t state;
public:
SplitMix64(uint64_t state)
: state(state)
{
}
uint64_t next()
{
uint64_t z = (state += 0x9e3779b97f4a7c15);
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
return z ^ (z >> 31);
}
void generate(uint64_t *begin, const uint64_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};
/** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */
class xoshiro128plusplus {
public:
typedef uint32_t state[4];
xoshiro128plusplus() { seed(); }
xoshiro128plusplus(const state &s) { copy(this->s, s); }
xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); }
xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); }
uint32_t next();
/* This is the jump function for the generator. It is equivalent
to 2^64 calls to next(); it can be used to generate 2^64
non-overlapping subsequences for parallel computations. */
void jump()
{
static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b };
uint32_t s0 = 0;
uint32_t s1 = 0;
uint32_t s2 = 0;
uint32_t s3 = 0;
for (const uint32_t entry : JUMP)
for (int b = 0; b < 32; b++) {
if (entry & UINT32_C(1) << b) {
s0 ^= s[0];
s1 ^= s[1];
s2 ^= s[2];
s3 ^= s[3];
}
next();
}
s[0] = s0;
s[1] = s1;
s[2] = s2;
s[3] = s3;
}
void save(state &s) const
{
copy(s, this->s);
}
private:
state s;
void seed(uint64_t value)
{
uint64_t seeds[2];
SplitMix64 seedSequence { value };
seedSequence.generate(seeds, seeds + 2);
s[0] = static_cast<uint32_t>(seeds[0] >> 32);
s[1] = static_cast<uint32_t>(seeds[0]);
s[2] = static_cast<uint32_t>(seeds[1] >> 32);
s[3] = static_cast<uint32_t>(seeds[1]);
}
void seed(uint32_t value)
{
SplitMix32 seedSequence { value };
seedSequence.generate(s, s + 4);
}
void seed()
{
seed(timeSeed());
#if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0))
static std::random_device rd;
std::uniform_int_distribution<uint32_t> dist;
for (uint32_t &cell : s)
cell ^= dist(rd);
#endif
}
static uint64_t timeSeed();
static void copy(state &dst, const state &src);
};
/**
* @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions
*/
xoshiro128plusplus ReserveSeedSequence();
/**
* @brief Advances the global seed generator state and returns the new value
*/
uint32_t GenerateSeed();
/**
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state
*/
void SetRndSeed(uint32_t seed);
/**
* @brief Returns the current state of the RandomNumberEngine used by the base game
*
* This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged
* due to the poor randomness and bugs in the implementation that need to be retained for compatibility.
*
* @return The current engine state
*/
uint32_t GetLCGEngineState();
/**
* @brief Advance the global RandomNumberEngine state by the specified number of rounds
*
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated.
* @param count How many values to discard
*/
void DiscardRandomValues(unsigned count);
/**
* @brief Advances the global RandomNumberEngine state and returns the new value
*/
uint32_t GenerateRandomNumber();
/**
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG
*
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31.
*
* This function is only used when the base game wants to store the seed used to generate an item or level, however
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game.
*
* @return A random number in the range [0,2^31) or -2^31
*/
[[nodiscard]] int32_t AdvanceRndSeed();
/**
* @brief Generates a random integer less than the given limit using the vanilla RNG
*
* If v is not a positive number this function returns 0 without calling the RNG.
*
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value
* less than 32768 for limits in that range.
*
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed()
*
* @see AdvanceRndSeed()
* @param v The upper limit for the return value
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1]
*/
int32_t GenerateRnd(int32_t v);
/**
* @brief Generates a random boolean value using the vanilla RNG
*
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2
* represents a 50/50 chance.
*
* @param frequency odds of returning a true value
* @return A random boolean value
*/
bool FlipCoin(unsigned frequency = 2);
/**
* @brief Picks one of the elements in the list randomly.
*
* @param values The values to pick from
* @return A random value from the 'values' list.
*/
template <typename T>
const T PickRandomlyAmong(const std::initializer_list<T> &values)
{
const auto index { std::max<int32_t>(GenerateRnd(static_cast<int32_t>(values.size())), 0) };
return *(values.begin() + index);
}
/**
* @brief Generates a random non-negative integer
*
* Effectively the same as GenerateRnd but will never return a negative value
* @param v upper limit for the return value
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v)
*/
inline int32_t RandomIntLessThan(int32_t v)
{
return std::max<int32_t>(GenerateRnd(v), 0);
}
/**
* @brief Randomly chooses a value somewhere within the given range
* @param min lower limit, minimum possible value
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range
* @param halfOpen whether to use the limits as a half-open range or not
* @return a randomly selected integer
*/
inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false)
{
return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min;
}
} // namespace devilution
/**
* @file random.hpp
*
* Contains convenience functions for random number generation
*
* This includes specific engine/distribution functions for logic that needs to be compatible with the base game.
*/
#pragma once
#include <algorithm>
#include <cstdint>
#include <initializer_list>
#include <random>
namespace devilution {
class DiabloGenerator {
private:
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> lcg;
public:
/**
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state
*/
DiabloGenerator(uint32_t seed)
{
lcg.seed(seed);
}
/**
* @brief Advance the global RandomNumberEngine state by the specified number of rounds
*
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated.
* @param count How many values to discard
*/
void discardRandomValues(unsigned count)
{
lcg.discard(count);
}
/**
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG
*
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31.
*
* This function is only used when the base game wants to store the seed used to generate an item or level, however
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game.
*
* @return A random number in the range [0,2^31) or -2^31
*/
int32_t advanceRndSeed()
{
const int32_t seed = static_cast<int32_t>(lcg());
// since abs(INT_MIN) is undefined behavior, handle this value specially
return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed);
}
/**
* @brief Generates a random integer less than the given limit using the vanilla RNG
*
* If v is not a positive number this function returns 0 without calling the RNG.
*
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value
* less than 32768 for limits in that range.
*
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed()
*
* @see AdvanceRndSeed()
* @param v The upper limit for the return value
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1]
*/
int32_t generateRnd(int32_t v)
{
if (v <= 0)
return 0;
if (v <= 0x7FFF) // use the high bits to correct for LCG bias
return (advanceRndSeed() >> 16) % v;
return advanceRndSeed() % v;
}
/**
* @brief Generates a random boolean value using the vanilla RNG
*
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2
* represents a 50/50 chance.
*
* @param frequency odds of returning a true value
* @return A random boolean value
*/
bool flipCoin(unsigned frequency)
{
// Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned.
return generateRnd(static_cast<int32_t>(frequency)) == 0;
}
/**
* @brief Picks one of the elements in the list randomly.
*
* @param values The values to pick from
* @return A random value from the 'values' list.
*/
template <typename T>
const T pickRandomlyAmong(const std::initializer_list<T> &values)
{
const auto index { std::max<int32_t>(generateRnd(static_cast<int32_t>(values.size())), 0) };
return *(values.begin() + index);
}
/**
* @brief Generates a random non-negative integer
*
* Effectively the same as GenerateRnd but will never return a negative value
* @param v upper limit for the return value
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v)
*/
inline int32_t randomIntLessThan(int32_t v)
{
return std::max<int32_t>(generateRnd(v), 0);
}
/**
* @brief Randomly chooses a value somewhere within the given range
* @param min lower limit, minimum possible value
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range
* @param halfOpen whether to use the limits as a half-open range or not
* @return a randomly selected integer
*/
inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false)
{
return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min;
}
};
// Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008
// https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68
// and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix32 {
uint32_t state;
public:
SplitMix32(uint32_t state)
: state(state)
{
}
uint32_t next()
{
uint32_t z = (state += 0x9e3779b9);
z = (z ^ (z >> 16)) * 0x85ebca6b;
z = (z ^ (z >> 13)) * 0xc2b2ae35;
return z ^ (z >> 16);
}
void generate(uint32_t *begin, const uint32_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};
// Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
//
// See also:
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
// https://doi.org/10.1145/2714064.2660195
class SplitMix64 {
uint64_t state;
public:
SplitMix64(uint64_t state)
: state(state)
{
}
uint64_t next()
{
uint64_t z = (state += 0x9e3779b97f4a7c15);
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
return z ^ (z >> 31);
}
void generate(uint64_t *begin, const uint64_t *end)
{
while (begin != end) {
*begin = next();
++begin;
}
}
};
/** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */
class xoshiro128plusplus {
public:
typedef uint32_t state[4];
xoshiro128plusplus() { seed(); }
xoshiro128plusplus(const state &s) { copy(this->s, s); }
xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); }
xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); }
uint32_t next();
/* This is the jump function for the generator. It is equivalent
to 2^64 calls to next(); it can be used to generate 2^64
non-overlapping subsequences for parallel computations. */
void jump()
{
static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b };
uint32_t s0 = 0;
uint32_t s1 = 0;
uint32_t s2 = 0;
uint32_t s3 = 0;
for (const uint32_t entry : JUMP)
for (int b = 0; b < 32; b++) {
if (entry & UINT32_C(1) << b) {
s0 ^= s[0];
s1 ^= s[1];
s2 ^= s[2];
s3 ^= s[3];
}
next();
}
s[0] = s0;
s[1] = s1;
s[2] = s2;
s[3] = s3;
}
void save(state &s) const
{
copy(s, this->s);
}
private:
state s;
void seed(uint64_t value)
{
uint64_t seeds[2];
SplitMix64 seedSequence { value };
seedSequence.generate(seeds, seeds + 2);
s[0] = static_cast<uint32_t>(seeds[0] >> 32);
s[1] = static_cast<uint32_t>(seeds[0]);
s[2] = static_cast<uint32_t>(seeds[1] >> 32);
s[3] = static_cast<uint32_t>(seeds[1]);
}
void seed(uint32_t value)
{
SplitMix32 seedSequence { value };
seedSequence.generate(s, s + 4);
}
void seed()
{
seed(timeSeed());
#if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0))
static std::random_device rd;
std::uniform_int_distribution<uint32_t> dist;
for (uint32_t &cell : s)
cell ^= dist(rd);
#endif
}
static uint64_t timeSeed();
static void copy(state &dst, const state &src);
};
/**
* @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions
*/
xoshiro128plusplus ReserveSeedSequence();
/**
* @brief Advances the global seed generator state and returns the new value
*/
uint32_t GenerateSeed();
/**
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed
* @param seed New engine state
*/
void SetRndSeed(uint32_t seed);
/**
* @brief Returns the current state of the RandomNumberEngine used by the base game
*
* This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged
* due to the poor randomness and bugs in the implementation that need to be retained for compatibility.
*
* @return The current engine state
*/
uint32_t GetLCGEngineState();
/**
* @brief Advance the global RandomNumberEngine state by the specified number of rounds
*
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated.
* @param count How many values to discard
*/
void DiscardRandomValues(unsigned count);
/**
* @brief Advances the global RandomNumberEngine state and returns the new value
*/
uint32_t GenerateRandomNumber();
/**
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG
*
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31.
*
* This function is only used when the base game wants to store the seed used to generate an item or level, however
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game.
*
* @return A random number in the range [0,2^31) or -2^31
*/
[[nodiscard]] int32_t AdvanceRndSeed();
/**
* @brief Generates a random integer less than the given limit using the vanilla RNG
*
* If v is not a positive number this function returns 0 without calling the RNG.
*
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value
* less than 32768 for limits in that range.
*
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed()
*
* @see AdvanceRndSeed()
* @param v The upper limit for the return value
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1]
*/
int32_t GenerateRnd(int32_t v);
/**
* @brief Generates a random boolean value using the vanilla RNG
*
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2
* represents a 50/50 chance.
*
* @param frequency odds of returning a true value
* @return A random boolean value
*/
bool FlipCoin(unsigned frequency = 2);
/**
* @brief Picks one of the elements in the list randomly.
*
* @param values The values to pick from
* @return A random value from the 'values' list.
*/
template <typename T>
const T PickRandomlyAmong(const std::initializer_list<T> &values)
{
const auto index { std::max<int32_t>(GenerateRnd(static_cast<int32_t>(values.size())), 0) };
return *(values.begin() + index);
}
/**
* @brief Generates a random non-negative integer
*
* Effectively the same as GenerateRnd but will never return a negative value
* @param v upper limit for the return value
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v)
*/
inline int32_t RandomIntLessThan(int32_t v)
{
return std::max<int32_t>(GenerateRnd(v), 0);
}
/**
* @brief Randomly chooses a value somewhere within the given range
* @param min lower limit, minimum possible value
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range
* @param halfOpen whether to use the limits as a half-open range or not
* @return a randomly selected integer
*/
inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false)
{
return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min;
}
} // namespace devilution

78
Source/engine/render/scrollrt.cpp

@ -928,10 +928,8 @@ void DrawFloor(const Surface &out, const Lightmap &lightmap, Point tilePosition,
{
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) {
if (!InDungeonBounds(tilePosition)) {
world_draw_black_tile(out, targetBufferPosition.x, targetBufferPosition.y);
if (!InDungeonBounds(tilePosition))
continue;
}
if (IsFloor(tilePosition)) {
DrawFloorTile(out, lightmap, tilePosition, targetBufferPosition);
}
@ -1018,6 +1016,77 @@ void DrawTileContent(const Surface &out, const Lightmap &lightmap, Point tilePos
}
}
void DrawDirtTile(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition)
{
// This should be the *top-left* of the 2×2 dirt pattern in the actual dungeon.
// You might need to tweak these to where your dirt patch actually lives.
constexpr Point base { 0, 0 };
// Decide which of the 4 tiles of the 2×2 block to use,
// based on where this OOB tile is in the world grid.
const int ox = (tilePosition.x & 1); // 0 or 1
const int oy = (tilePosition.y & 1); // 0 or 1
Point sample {
base.x + ox,
base.y + oy,
};
// Safety: clamp in case tilePosition is wildly outside and base+offset ever escapes
sample.x = std::clamp(sample.x, 0, MAXDUNX - 1);
sample.y = std::clamp(sample.y, 0, MAXDUNY - 1);
if (!InDungeonBounds(sample) || dPiece[sample.x][sample.y] == 0) {
// Failsafe: if our sample somehow isn't valid, fall back to black
world_draw_black_tile(out, targetBufferPosition.x, targetBufferPosition.y);
return;
}
const int lightTableIndex = dLight[sample.x][sample.y];
// Let the normal dungeon tile renderer compose the full tile
DrawCell(out, lightmap, sample, targetBufferPosition, lightTableIndex);
}
/**
* @brief Render a row of tiles
* @param out Buffer to render to
* @param lightmap Per-pixel light buffer
* @param tilePosition dPiece coordinates
* @param targetBufferPosition Target buffer coordinates
* @param rows Number of rows
* @param columns Tile in a row
*/
void DrawOOB(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns)
{
for (int i = 0; i < rows + 5; i++) { // 5 extra rows needed to make sure everything gets rendered at the bottom half of the screen
for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) {
if (!InDungeonBounds(tilePosition)) {
if (leveltype == DTYPE_TOWN) {
world_draw_black_tile(out, targetBufferPosition.x, targetBufferPosition.y);
} else {
DrawDirtTile(out, lightmap, tilePosition, targetBufferPosition);
}
}
}
// Return to start of row
tilePosition += Displacement(Direction::West) * columns;
targetBufferPosition.x -= columns * TILE_WIDTH;
// Jump to next row
targetBufferPosition.y += TILE_HEIGHT / 2;
if ((i & 1) != 0) {
tilePosition.x++;
columns--;
targetBufferPosition.x += TILE_WIDTH / 2;
} else {
tilePosition.y++;
columns++;
targetBufferPosition.x -= TILE_WIDTH / 2;
}
}
}
/**
* @brief Scale up the top left part of the buffer 2x.
*/
@ -1183,6 +1252,7 @@ void DrawGame(const Surface &fullOut, Point position, Displacement offset)
DrawFloor(out, lightmap, position, Point {} + offset, rows, columns);
DrawTileContent(out, lightmap, position, Point {} + offset, rows, columns);
DrawOOB(out, lightmap, position, Point {} + offset, rows, columns);
if (*GetOptions().Graphics.zoom) {
Zoom(fullOut.subregionY(0, gnViewportHeight));
@ -1746,7 +1816,7 @@ void DrawAndBlit()
const Rectangle &mainPanel = GetMainPanel();
if (gnScreenWidth > mainPanel.size.width || IsRedrawEverything() || *GetOptions().Gameplay.enableFloatingNumbers != FloatingNumbers::Off) {
if (gnScreenWidth > mainPanel.size.width || IsRedrawEverything()) {
drawHealth = true;
drawMana = true;
drawControlButtons = true;

148
Source/engine/render/text_render.cpp

@ -437,6 +437,89 @@ int GetLineStartX(UiFlags flags, const Rectangle &rect, int lineWidth)
return rect.position.x;
}
void DrawLine(
const Surface &out,
std::string_view text,
Point characterPosition,
Rectangle rect,
UiFlags flags,
int curSpacing,
GameFontTables size,
text_color color,
bool outline,
const TextRenderOptions &opts,
size_t lineStartPos,
int totalWidth)
{
CurrentFont currentFont;
std::string_view lineCopy = text;
size_t currentPos = 0;
size_t cpLen;
const auto maybeDrawCursor = [&]() {
const auto byteIndex = static_cast<int>(lineStartPos + currentPos);
Point position = characterPosition;
if (opts.cursorPosition == byteIndex) {
if (GetAnimationFrame(2, 500) != 0 || opts.cursorStatic) {
FontStack baseFont = LoadFont(size, color, 0);
if (baseFont.has_value()) {
DrawFont(out, position, baseFont.glyph('|'), color, outline);
}
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
}
}
};
// Start from the beginning of the line
characterPosition.x = GetLineStartX(flags, rect, totalWidth);
while (!lineCopy.empty()) {
char32_t c = DecodeFirstUtf8CodePoint(lineCopy, &cpLen);
if (c == Utf8DecodeError) break;
if (c == ZWSP) {
currentPos += cpLen;
lineCopy.remove_prefix(cpLen);
continue;
}
if (!currentFont.load(size, color, c)) {
c = U'?';
if (!currentFont.load(size, color, c)) {
app_fatal("Missing fonts");
}
}
const uint8_t frame = c & 0xFF;
const ClxSprite glyph = currentFont.glyph(frame);
const int charWidth = glyph.width();
const auto byteIndex = static_cast<int>(lineStartPos + currentPos);
// Draw highlight
if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) {
const bool lastInRange = static_cast<int>(byteIndex + cpLen) == opts.highlightRange.end;
FillRect(out, characterPosition.x, characterPosition.y,
glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(),
opts.highlightColor);
}
DrawFont(out, characterPosition, glyph, color, outline);
maybeDrawCursor();
// Move to the next position
characterPosition.x += charWidth + curSpacing;
currentPos += cpLen;
lineCopy.remove_prefix(cpLen);
}
assert(currentPos == text.size());
maybeDrawCursor();
}
uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point &characterPosition,
int lineWidth, int charactersInLine, int rightMargin, int bottomMargin, GameFontTables size, text_color color, bool outline,
TextRenderOptions &opts)
@ -455,19 +538,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
std::string_view remaining = text;
size_t cpLen;
const auto maybeDrawCursor = [&]() {
if (opts.cursorPosition == static_cast<int>(text.size() - remaining.size())) {
Point position = characterPosition;
MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight);
if (GetAnimationFrame(2, 500) != 0) {
FontStack baseFont = LoadFont(size, color, 0);
if (baseFont.has_value()) {
DrawFont(out, position, baseFont.glyph('|'), color, outline);
}
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
}
// Track line boundaries
size_t lineStartPos = 0;
size_t lineEndPos = 0;
const auto drawLine = [&]() {
std::string_view lineText = text.substr(lineStartPos, lineEndPos - lineStartPos);
if (!lineText.empty()) {
DrawLine(
out,
lineText,
characterPosition,
rect,
opts.flags,
curSpacing,
size,
color,
outline,
opts,
lineStartPos,
lineWidth);
}
};
@ -487,8 +577,10 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
const uint8_t frame = next & 0xFF;
const uint16_t width = currentFont.glyph(frame).width();
if (next == U'\n' || characterPosition.x + width > rightMargin) {
if (next == '\n')
maybeDrawCursor();
lineEndPos = text.size() - remaining.size();
drawLine();
const int nextLineY = characterPosition.y + opts.lineHeight;
if (nextLineY >= bottomMargin)
break;
@ -506,26 +598,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
}
characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth);
// Start a new line
lineStartPos = next == U'\n' ? (text.size() - remaining.size() + cpLen) : (text.size() - remaining.size());
lineEndPos = lineStartPos;
if (next == U'\n')
continue;
}
const ClxSprite glyph = currentFont.glyph(frame);
const auto byteIndex = static_cast<int>(text.size() - remaining.size());
// Draw highlight
if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) {
const bool lastInRange = static_cast<int>(byteIndex + cpLen) == opts.highlightRange.end;
FillRect(out, characterPosition.x, characterPosition.y,
glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(),
opts.highlightColor);
}
// Update end position as we add characters
lineEndPos = text.size() - remaining.size() + cpLen;
DrawFont(out, characterPosition, glyph, color, outline);
maybeDrawCursor();
// Update position for the next character
characterPosition.x += width + curSpacing;
}
maybeDrawCursor();
// Draw any remaining characters in the last line
if (lineStartPos < lineEndPos) {
drawLine();
}
return static_cast<uint32_t>(remaining.data() - text.data());
}

2
Source/engine/render/text_render.hpp

@ -155,6 +155,8 @@ struct TextRenderOptions {
/** @brief If a cursor is rendered, the surface coordinates are saved here. */
std::optional<Point> *renderedCursorPositionOut = nullptr;
bool cursorStatic = false;
};
/**

2
Source/engine/sound.cpp

@ -348,8 +348,6 @@ void music_start(_music_id nTrack)
}
music.SetVolume(*GetOptions().Audio.musicVolume, VOLUME_MIN, VOLUME_MAX);
if (!diablo_is_focused())
music_mute();
if (!music.Play(/*numIterations=*/0)) {
LogError(LogCategory::Audio, "Aulib::Stream::play (from music_start): {}", SDL_GetError());
music_stop();

2
Source/gmenu.cpp

@ -20,7 +20,7 @@
#include "DiabloUI/ui_flags.hpp"
#include "appfat.h"
#include "control.h"
#include "control/control.hpp"
#include "controls/axis_direction.h"
#include "controls/controller_motion.h"
#include "engine/clx_sprite.hpp"

2
Source/interfac.cpp

@ -22,7 +22,7 @@
#include <expected.hpp>
#include "control.h"
#include "control/control.hpp"
#include "controls/input.h"
#include "engine/clx_sprite.hpp"
#include "engine/dx.h"

14
Source/items.cpp

@ -28,7 +28,7 @@
#include <fmt/core.h>
#include "DiabloUI/ui_flags.hpp"
#include "control.h"
#include "control/control.hpp"
#include "controls/control_mode.hpp"
#include "controls/controller_buttons.h"
#include "cursor.h"
@ -52,7 +52,6 @@
#include "headless_mode.hpp"
#include "inv.h"
#include "inv_iterators.hpp"
#include "itemdat.h"
#include "items/validation.h"
#include "levels/gendung.h"
#include "levels/gendung_defs.hpp"
@ -60,25 +59,26 @@
#include "levels/town.h"
#include "lighting.h"
#include "minitext.h"
#include "monstdat.h"
#include "monster.h"
#include "msg.h"
#include "multi.h"
#include "objdat.h"
#include "objects.h"
#include "options.h"
#include "pack.h"
#include "panels/info_box.hpp"
#include "panels/ui_panels.hpp"
#include "player.h"
#include "playerdat.hpp"
#include "qol/stash.h"
#include "quests.h"
#include "sound_effect_enums.h"
#include "spelldat.h"
#include "spells.h"
#include "stores.h"
#include "textdat.h"
#include "tables/itemdat.h"
#include "tables/monstdat.h"
#include "tables/objdat.h"
#include "tables/playerdat.hpp"
#include "tables/spelldat.h"
#include "tables/textdat.h"
#include "utils/enum_traits.h"
#include "utils/format_int.hpp"
#include "utils/is_of.hpp"

2
Source/items.h

@ -13,9 +13,9 @@
#include "engine/animationinfo.h"
#include "engine/point.hpp"
#include "engine/surface.hpp"
#include "itemdat.h"
#include "levels/dun_tile.hpp"
#include "monster.h"
#include "tables/itemdat.h"
#include "utils/is_of.hpp"
#include "utils/string_or_view.hpp"

2
Source/items/validation.cpp

@ -9,10 +9,10 @@
#include <cstdint>
#include "items.h"
#include "monstdat.h"
#include "msg.h"
#include "player.h"
#include "spells.h"
#include "tables/monstdat.h"
#include "utils/endian_swap.hpp"
#include "utils/is_of.hpp"

2
Source/levels/drlg_l3.cpp

@ -10,10 +10,10 @@
#include "levels/setmaps.h"
#include "lighting.h"
#include "monster.h"
#include "objdat.h"
#include "objects.h"
#include "player.h"
#include "quests.h"
#include "tables/objdat.h"
#include "utils/is_of.hpp"
namespace devilution {

2
Source/levels/drlg_l4.cpp

@ -12,8 +12,8 @@
#include "levels/gendung.h"
#include "monster.h"
#include "multi.h"
#include "objdat.h"
#include "player.h"
#include "tables/objdat.h"
#include "utils/is_of.hpp"
namespace devilution {

2
Source/levels/setmaps.cpp

@ -14,9 +14,9 @@
#include "levels/gendung.h"
#include "levels/trigs.h"
#include "msg.h"
#include "objdat.h"
#include "objects.h"
#include "quests.h"
#include "tables/objdat.h"
#include "utils/language.h"
namespace devilution {

2
Source/levels/themes.h

@ -8,7 +8,7 @@
#include <cstdint>
#include "levels/gendung.h"
#include "objdat.h"
#include "tables/objdat.h"
namespace devilution {

2
Source/levels/trigs.cpp

@ -10,7 +10,7 @@
#include <fmt/format.h>
#include "control.h"
#include "control/control.hpp"
#include "controls/control_mode.hpp"
#include "controls/plrctrls.h"
#include "cursor.h"

4
Source/loadsave.cpp

@ -17,7 +17,7 @@
#include "automap.h"
#include "codec.h"
#include "control.h"
#include "control/control.hpp"
#include "cursor.h"
#include "dead.h"
#include "doom.h"
@ -33,10 +33,10 @@
#include "monsters/validation.hpp"
#include "mpq/mpq_common.hpp"
#include "pfile.h"
#include "playerdat.hpp"
#include "plrmsg.h"
#include "qol/stash.h"
#include "stores.h"
#include "tables/playerdat.hpp"
#include "utils/algorithm/container.hpp"
#include "utils/endian_read.hpp"
#include "utils/endian_swap.hpp"

14
Source/lua/lua_event.hpp

@ -0,0 +1,14 @@
#pragma once
#include <string_view>
namespace devilution {
/**
* @brief Triggers a Lua event by name.
* This is a minimal header for code that only needs to trigger events.
*/
void LuaEvent(std::string_view name);
void LuaEvent(std::string_view name, std::string_view arg);
} // namespace devilution

47
Source/lua/lua_global.cpp

@ -13,6 +13,7 @@
#include "effects.h"
#include "engine/assets.hpp"
#include "lua/modules/audio.hpp"
#include "lua/modules/floatingnumbers.hpp"
#include "lua/modules/hellfire.hpp"
#include "lua/modules/i18n.hpp"
#include "lua/modules/items.hpp"
@ -20,8 +21,11 @@
#include "lua/modules/monsters.hpp"
#include "lua/modules/player.hpp"
#include "lua/modules/render.hpp"
#include "lua/modules/system.hpp"
#include "lua/modules/towners.hpp"
#include "monster.h"
#include "options.h"
#include "player.h"
#include "plrmsg.h"
#include "utils/console.h"
#include "utils/log.hpp"
@ -201,6 +205,9 @@ sol::environment CreateLuaSandbox()
sandbox["require"] = lua["requireGen"](sandbox, CurrentLuaState->commonPackages, LuaLoadScriptFromAssets);
// Expose commonly used enums globally for mods
sandbox["SfxID"] = lua["SfxID"];
return sandbox;
}
@ -283,6 +290,8 @@ void LuaInitialize()
"devilutionx.render", LuaRenderModule(lua),
"devilutionx.towners", LuaTownersModule(lua),
"devilutionx.hellfire", LuaHellfireModule(lua),
"devilutionx.system", LuaSystemModule(lua),
"devilutionx.floatingnumbers", LuaFloatingNumbersModule(lua),
"devilutionx.message", [](std::string_view text) { EventPlrMsg(text, UiFlags::ColorRed); },
// This package is loaded without a sandbox:
"inspect", RunScript(/*env=*/std::nullopt, "inspect", /*optional=*/false));
@ -305,7 +314,43 @@ void LuaShutdown()
CurrentLuaState = std::nullopt;
}
template <typename... Args>
void CallLuaEvent(std::string_view name, Args &&...args)
{
if (!CurrentLuaState.has_value()) {
return;
}
const auto trigger = CurrentLuaState->events.traverse_get<std::optional<sol::object>>(name, "trigger");
if (!trigger.has_value() || !trigger->is<sol::protected_function>()) {
LogError("events.{}.trigger is not a function", name);
return;
}
const sol::protected_function fn = trigger->as<sol::protected_function>();
SafeCallResult(fn(std::forward<Args>(args)...), /*optional=*/true);
}
void LuaEvent(std::string_view name)
{
CallLuaEvent(name);
}
void LuaEvent(std::string_view name, const Player *player, int arg1, int arg2)
{
CallLuaEvent(name, player, arg1, arg2);
}
void LuaEvent(std::string_view name, const Monster *monster, int arg1, int arg2)
{
CallLuaEvent(name, monster, arg1, arg2);
}
void LuaEvent(std::string_view name, const Player *player, uint32_t arg1)
{
CallLuaEvent(name, player, arg1);
}
void LuaEvent(std::string_view name, std::string_view arg)
{
if (!CurrentLuaState.has_value()) {
return;
@ -317,7 +362,7 @@ void LuaEvent(std::string_view name)
return;
}
const sol::protected_function fn = trigger->as<sol::protected_function>();
SafeCallResult(fn(), /*optional=*/true);
SafeCallResult(fn(arg), /*optional=*/true);
}
sol::state &GetLuaState()

7
Source/lua/lua_global.hpp

@ -8,10 +8,17 @@
namespace devilution {
struct Player;
struct Monster;
void LuaInitialize();
void LuaReloadActiveMods();
void LuaShutdown();
void LuaEvent(std::string_view name);
void LuaEvent(std::string_view name, std::string_view arg);
void LuaEvent(std::string_view name, const Player *player, int arg1, int arg2);
void LuaEvent(std::string_view name, const Monster *monster, int arg1, int arg2);
void LuaEvent(std::string_view name, const Player *player, uint32_t arg1);
sol::state &GetLuaState();
sol::environment CreateLuaSandbox();
sol::object SafeCallResult(sol::protected_function_result result, bool optional);

21
Source/lua/modules/audio.cpp

@ -1,9 +1,11 @@
#include "lua/modules/audio.hpp"
#include <magic_enum/magic_enum.hpp>
#include <sol/sol.hpp>
#include "effects.h"
#include "lua/metadoc.hpp"
#include "sound_effect_enums.h"
namespace devilution {
@ -14,10 +16,27 @@ bool IsValidSfx(int16_t psfx)
return psfx >= 0 && psfx <= static_cast<int16_t>(SfxID::LAST);
}
void RegisterSfxIDEnum(sol::state_view &lua)
{
constexpr auto enumValues = magic_enum::enum_values<SfxID>();
sol::table enumTable = lua.create_table();
for (const auto enumValue : enumValues) {
const std::string_view name = magic_enum::enum_name(enumValue);
if (!name.empty() && name != "LAST" && name != "None") {
enumTable[name] = static_cast<int16_t>(enumValue);
}
}
// Add LAST and None explicitly
enumTable["LAST"] = static_cast<int16_t>(SfxID::LAST);
enumTable["None"] = static_cast<int16_t>(SfxID::None);
lua["SfxID"] = enumTable;
}
} // namespace
sol::table LuaAudioModule(sol::state_view &lua)
{
RegisterSfxIDEnum(lua);
sol::table table = lua.create_table();
LuaSetDocFn(table,
"playSfx", "(id: number)",
@ -25,6 +44,8 @@ sol::table LuaAudioModule(sol::state_view &lua)
LuaSetDocFn(table,
"playSfxLoc", "(id: number, x: number, y: number)",
[](int16_t psfx, int x, int y) { if (IsValidSfx(psfx)) PlaySfxLoc(static_cast<SfxID>(psfx), { x, y }); });
// Expose SfxID enum through the module table
table["SfxID"] = lua["SfxID"];
return table;
}

2
Source/lua/modules/dev/monsters.cpp

@ -11,9 +11,9 @@
#include "levels/tile_properties.hpp"
#include "lighting.h"
#include "lua/metadoc.hpp"
#include "monstdat.h"
#include "monster.h"
#include "player.h"
#include "tables/monstdat.h"
#include "utils/str_case.hpp"
#include "utils/str_cat.hpp"

2
Source/lua/modules/dev/player/spells.cpp

@ -8,8 +8,8 @@
#include "lua/metadoc.hpp"
#include "msg.h"
#include "spelldat.h"
#include "spells.h"
#include "tables/spelldat.h"
#include "utils/str_cat.hpp"
namespace devilution {

24
Source/lua/modules/floatingnumbers.cpp

@ -0,0 +1,24 @@
#include "lua/modules/floatingnumbers.hpp"
#include <sol/sol.hpp>
#include "engine/point.hpp"
#include "lua/metadoc.hpp"
#include "qol/floatingnumbers.h"
namespace devilution {
sol::table LuaFloatingNumbersModule(sol::state_view &lua)
{
sol::table table = lua.create_table();
LuaSetDocFn(table, "add", "(text: string, pos: Point, style: UiFlags, id: integer = 0, reverseDirection: boolean = false)",
"Add a floating number",
[](const std::string &text, Point pos, UiFlags style, std::optional<int> id, std::optional<bool> reverseDirection) {
AddFloatingNumber(pos, { 0, 0 }, text, style, id.value_or(0), reverseDirection.value_or(false));
});
return table;
}
} // namespace devilution

9
Source/lua/modules/floatingnumbers.hpp

@ -0,0 +1,9 @@
#pragma once
#include <sol/forward.hpp>
namespace devilution {
sol::table LuaFloatingNumbersModule(sol::state_view &lua);
} // namespace devilution

13
Source/lua/modules/items.cpp

@ -6,10 +6,10 @@
#include <sol/sol.hpp>
#include "data/file.hpp"
#include "itemdat.h"
#include "items.h"
#include "lua/metadoc.hpp"
#include "player.h"
#include "tables/itemdat.h"
#include "utils/utf8.hpp"
namespace devilution {
@ -485,6 +485,17 @@ sol::table LuaItemModule(sol::state_view &lua)
LuaSetDocFn(table, "addItemDataFromTsv", "(path: string, baseMappingId: number)", AddItemDataFromTsv);
LuaSetDocFn(table, "addUniqueItemDataFromTsv", "(path: string, baseMappingId: number)", AddUniqueItemDataFromTsv);
// Expose enums through the module table
table["ItemIndex"] = lua["ItemIndex"];
table["ItemType"] = lua["ItemType"];
table["ItemClass"] = lua["ItemClass"];
table["ItemEquipType"] = lua["ItemEquipType"];
table["ItemMiscID"] = lua["ItemMiscID"];
table["SpellID"] = lua["SpellID"];
table["ItemEffectType"] = lua["ItemEffectType"];
table["ItemSpecialEffect"] = lua["ItemSpecialEffect"];
table["ItemSpecialEffectHf"] = lua["ItemSpecialEffectHf"];
return table;
}

18
Source/lua/modules/items.hpp

@ -1,9 +1,9 @@
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaItemModule(sol::state_view &lua);
} // namespace devilution
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaItemModule(sol::state_view &lua);
} // namespace devilution

20
Source/lua/modules/monsters.cpp

@ -6,8 +6,10 @@
#include <sol/sol.hpp>
#include "data/file.hpp"
#include "engine/point.hpp"
#include "lua/metadoc.hpp"
#include "monstdat.h"
#include "monster.h"
#include "tables/monstdat.h"
#include "utils/language.h"
#include "utils/str_split.hpp"
@ -27,10 +29,26 @@ void AddUniqueMonsterDataFromTsv(const std::string_view path)
LoadUniqueMonstDatFromFile(dataFile, path);
}
void InitMonsterUserType(sol::state_view &lua)
{
sol::usertype<Monster> monsterType = lua.new_usertype<Monster>(sol::no_constructor);
LuaSetDocReadonlyProperty(monsterType, "position", "Point",
"Monster's current position (readonly)",
[](const Monster &monster) {
return Point { monster.position.tile };
});
LuaSetDocReadonlyProperty(monsterType, "id", "integer",
"Monster's unique ID (readonly)",
[](const Monster &monster) {
return static_cast<int>(reinterpret_cast<uintptr_t>(&monster));
});
}
} // namespace
sol::table LuaMonstersModule(sol::state_view &lua)
{
InitMonsterUserType(lua);
sol::table table = lua.create_table();
LuaSetDocFn(table, "addMonsterDataFromTsv", "(path: string)", AddMonsterDataFromTsv);
LuaSetDocFn(table, "addUniqueMonsterDataFromTsv", "(path: string)", AddUniqueMonsterDataFromTsv);

18
Source/lua/modules/monsters.hpp

@ -1,9 +1,9 @@
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaMonstersModule(sol::state_view &lua);
} // namespace devilution
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaMonstersModule(sol::state_view &lua);
} // namespace devilution

84
Source/lua/modules/player.cpp

@ -4,18 +4,34 @@
#include <sol/sol.hpp>
#include "effects.h"
#include "engine/backbuffer_state.hpp"
#include "engine/point.hpp"
#include "engine/random.hpp"
#include "inv.h"
#include "items.h"
#include "lua/metadoc.hpp"
#include "player.h"
namespace devilution {
namespace {
void InitPlayerUserType(sol::state_view &lua)
{
sol::usertype<Player> playerType = lua.new_usertype<Player>(sol::no_constructor);
LuaSetDocReadonlyProperty(playerType, "name", "string",
"Player's name (readonly)",
&Player::name);
LuaSetDocReadonlyProperty(playerType, "id", "integer",
"Player's unique ID (readonly)",
[](const Player &player) {
return static_cast<int>(reinterpret_cast<uintptr_t>(&player));
});
LuaSetDocReadonlyProperty(playerType, "position", "Point",
"Player's current position (readonly)",
[](const Player &player) -> Point {
return Point { player.position.tile };
});
LuaSetDocFn(playerType, "addExperience", "(experience: integer, monsterLevel: integer = nil)",
"Adds experience to this player based on the current game mode",
[](Player &player, uint32_t experience, std::optional<int> monsterLevel) {
@ -28,6 +44,73 @@ void InitPlayerUserType(sol::state_view &lua)
LuaSetDocProperty(playerType, "characterLevel", "number",
"Character level (writeable)",
&Player::getCharacterLevel, &Player::setCharacterLevel);
LuaSetDocFn(playerType, "addItem", "(itemId: integer, count: integer = 1)",
"Add an item to the player's inventory",
[](Player &player, int itemId, std::optional<int> count) -> bool {
const _item_indexes itemIndex = static_cast<_item_indexes>(itemId);
const int itemCount = count.value_or(1);
for (int i = 0; i < itemCount; i++) {
Item tempItem {};
SetupAllItems(player, tempItem, itemIndex, AdvanceRndSeed(), 1, 1, true, false);
if (!AutoPlaceItemInInventory(player, tempItem, true)) {
return false;
}
}
CalcPlrInv(player, true);
return true;
});
LuaSetDocFn(playerType, "hasItem", "(itemId: integer)",
"Check if the player has an item with the given ID",
[](const Player &player, int itemId) -> bool {
return HasInventoryOrBeltItemWithId(player, static_cast<_item_indexes>(itemId));
});
LuaSetDocFn(playerType, "removeItem", "(itemId: integer, count: integer = 1)",
"Remove an item from the player's inventory",
[](Player &player, int itemId, std::optional<int> count) -> int {
const _item_indexes targetId = static_cast<_item_indexes>(itemId);
const int itemCount = count.value_or(1);
int removed = 0;
// Remove from inventory
for (int i = player._pNumInv - 1; i >= 0 && removed < itemCount; i--) {
if (player.InvList[i].IDidx == targetId) {
player.RemoveInvItem(i);
removed++;
}
}
// Remove from belt if needed
for (int i = MaxBeltItems - 1; i >= 0 && removed < itemCount; i--) {
if (!player.SpdList[i].isEmpty() && player.SpdList[i].IDidx == targetId) {
player.RemoveSpdBarItem(i);
removed++;
}
}
if (removed > 0) {
CalcPlrInv(player, true);
}
return removed;
});
LuaSetDocFn(playerType, "restoreFullLife", "()",
"Restore player's HP to maximum",
[](Player &player) {
player._pHitPoints = player._pMaxHP;
player._pHPBase = player._pMaxHPBase;
});
LuaSetDocFn(playerType, "restoreFullMana", "()",
"Restore player's mana to maximum",
[](Player &player) {
player._pMana = player._pMaxMana;
player._pManaBase = player._pMaxManaBase;
});
LuaSetDocReadonlyProperty(playerType, "mana", "number",
"Current mana (readonly)",
[](Player &player) { return player._pMana >> 6; });
LuaSetDocReadonlyProperty(playerType, "maxMana", "number",
"Maximum mana (readonly)",
[](Player &player) { return player._pMaxMana >> 6; });
}
} // namespace
@ -45,6 +128,7 @@ sol::table LuaPlayerModule(sol::state_view &lua)
[](int x, int y) {
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, Point { x, y });
});
return table;
}

46
Source/lua/modules/render.cpp

@ -2,6 +2,7 @@
#include <sol/sol.hpp>
#include "DiabloUI/ui_flags.hpp"
#include "engine/dx.h"
#include "engine/render/text_render.hpp"
#include "lua/metadoc.hpp"
@ -19,6 +20,51 @@ sol::table LuaRenderModule(sol::state_view &lua)
"Returns the screen width", []() { return gnScreenWidth; });
LuaSetDocFn(table, "screen_height", "()",
"Returns the screen height", []() { return gnScreenHeight; });
auto uiFlags = lua.create_table();
uiFlags["None"] = UiFlags::None;
uiFlags["FontSize12"] = UiFlags::FontSize12;
uiFlags["FontSize24"] = UiFlags::FontSize24;
uiFlags["FontSize30"] = UiFlags::FontSize30;
uiFlags["FontSize42"] = UiFlags::FontSize42;
uiFlags["FontSize46"] = UiFlags::FontSize46;
uiFlags["FontSizeDialog"] = UiFlags::FontSizeDialog;
uiFlags["ColorUiGold"] = UiFlags::ColorUiGold;
uiFlags["ColorUiSilver"] = UiFlags::ColorUiSilver;
uiFlags["ColorUiGoldDark"] = UiFlags::ColorUiGoldDark;
uiFlags["ColorUiSilverDark"] = UiFlags::ColorUiSilverDark;
uiFlags["ColorDialogWhite"] = UiFlags::ColorDialogWhite;
uiFlags["ColorDialogYellow"] = UiFlags::ColorDialogYellow;
uiFlags["ColorDialogRed"] = UiFlags::ColorDialogRed;
uiFlags["ColorYellow"] = UiFlags::ColorYellow;
uiFlags["ColorGold"] = UiFlags::ColorGold;
uiFlags["ColorBlack"] = UiFlags::ColorBlack;
uiFlags["ColorWhite"] = UiFlags::ColorWhite;
uiFlags["ColorWhitegold"] = UiFlags::ColorWhitegold;
uiFlags["ColorRed"] = UiFlags::ColorRed;
uiFlags["ColorBlue"] = UiFlags::ColorBlue;
uiFlags["ColorOrange"] = UiFlags::ColorOrange;
uiFlags["ColorButtonface"] = UiFlags::ColorButtonface;
uiFlags["ColorButtonpushed"] = UiFlags::ColorButtonpushed;
uiFlags["AlignCenter"] = UiFlags::AlignCenter;
uiFlags["AlignRight"] = UiFlags::AlignRight;
uiFlags["VerticalCenter"] = UiFlags::VerticalCenter;
uiFlags["KerningFitSpacing"] = UiFlags::KerningFitSpacing;
uiFlags["ElementDisabled"] = UiFlags::ElementDisabled;
uiFlags["ElementHidden"] = UiFlags::ElementHidden;
uiFlags["PentaCursor"] = UiFlags::PentaCursor;
uiFlags["Outlined"] = UiFlags::Outlined;
uiFlags["NeedsNextElement"] = UiFlags::NeedsNextElement;
table["UiFlags"] = uiFlags;
return table;
}

25
Source/lua/modules/system.cpp

@ -0,0 +1,25 @@
#include "lua/modules/system.hpp"
#include <sol/sol.hpp>
#ifdef USE_SDL3
#include <SDL3/SDL_timer.h>
#else
#include <SDL.h>
#endif
#include "lua/metadoc.hpp"
namespace devilution {
sol::table LuaSystemModule(sol::state_view &lua)
{
sol::table table = lua.create_table();
LuaSetDocFn(table, "get_ticks", "() -> integer", "Returns the number of milliseconds since the game started.",
[]() { return static_cast<int>(SDL_GetTicks()); });
return table;
}
} // namespace devilution

9
Source/lua/modules/system.hpp

@ -0,0 +1,9 @@
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaSystemModule(sol::state_view &lua);
} // namespace devilution

6
Source/minitext.cpp

@ -10,15 +10,15 @@
#include <vector>
#include "DiabloUI/ui_flags.hpp"
#include "control.h"
#include "control/control.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/dx.h"
#include "engine/load_cel.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/render/primitive_render.hpp"
#include "engine/render/text_render.hpp"
#include "playerdat.hpp"
#include "textdat.h"
#include "tables/playerdat.hpp"
#include "tables/textdat.h"
#include "utils/language.h"
#include "utils/timer.hpp"

2
Source/minitext.h

@ -6,7 +6,7 @@
#pragma once
#include "engine/surface.hpp"
#include "textdat.h"
#include "tables/textdat.h"
namespace devilution {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save