From 849aa019bb3d49085e6a56a70026c1daf5e08b3c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 29 Jun 2024 13:45:23 -0600 Subject: [PATCH 01/86] Release build squashed --- .github/workflows/mac-universal.yml | 73 ++++++++++ .github/workflows/release.yml | 131 ++++++++++++++++++ .github/workflows/scripts/butler_push.sh | 28 ++++ .../workflows/scripts/linux/install-tgui.sh | 2 +- .../workflows/scripts/mac/make-universal.sh | 27 ++++ .github/workflows/scripts/mac/scons-build.sh | 2 +- .github/workflows/scripts/mac/sign-apps.sh | 79 +++++++++++ .itch.toml | 9 ++ SConstruct | 89 +++++++++--- rsrc/dialogs/welcome.xml | 13 +- src/fileio/fileio_scen.cpp | 28 +++- 11 files changed, 453 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/mac-universal.yml create mode 100644 .github/workflows/release.yml create mode 100755 .github/workflows/scripts/butler_push.sh create mode 100755 .github/workflows/scripts/mac/make-universal.sh create mode 100755 .github/workflows/scripts/mac/sign-apps.sh create mode 100644 .itch.toml diff --git a/.github/workflows/mac-universal.yml b/.github/workflows/mac-universal.yml new file mode 100644 index 000000000..874e2ccdf --- /dev/null +++ b/.github/workflows/mac-universal.yml @@ -0,0 +1,73 @@ +name: Mac universal build +on: + push: + branches: + - universal + release: + types: [published] +jobs: + build: + runs-on: macos-14 + env: + CONFIGURATION: ${{ matrix.configuration }} + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + PROD_MACOS_CERTIFICATE: '${{ secrets.PROD_MACOS_CERTIFICATE }}' + PROD_MACOS_CERTIFICATE_PWD: '${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}' + PROD_MACOS_CERTIFICATE_NAME: '${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}' + PROD_MACOS_CI_KEYCHAIN_PWD: '${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}' + PROD_MACOS_NOTARIZATION_APPLE_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}' + PROD_MACOS_NOTARIZATION_TEAM_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}' + PROD_MACOS_NOTARIZATION_PWD: '${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}' + BUILD_OS: macos + strategy: + fail-fast: false + matrix: + configuration: + - Release + - Debug + steps: + - uses: actions/checkout@v4 + + # Download macos-intel + - uses: robinraju/release-downloader@v1 + id: download + with: + latest: true + fileName: 'cboe-macos-intel-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'cboe-macos-intel-${{ matrix.configuration }}' + + # Download macos-silicon + - uses: robinraju/release-downloader@v1 + with: + latest: true + fileName: 'cboe-macos-silicon-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'cboe-macos-silicon-${{ matrix.configuration }}' + + - run: .github/workflows/scripts/mac/make-universal.sh + + # Skipping this for now because of issue nqnstudios#13 + - name: Codesign and notarize + run: 'SIGN="no" NOTARIZE="no" .github/workflows/scripts/mac/sign-apps.sh' + + - name: 'Tar files' + run: 'tar -cvf cboe-macos-universal-${{matrix.configuration}}.tar "build/Blades of Exile"' + + # Upload everything as artifact + - uses: actions/upload-artifact@v4 + with: + name: mac-universal-dependencies-${{matrix.configuration}} + path: cboe-macos-universal-${{matrix.configuration}}.tar + + # upload a release + - name: Github release + uses: softprops/action-gh-release@v2 + with: + files: cboe-macos-universal-${{ matrix.configuration }}.tar + tag_name: ${{ steps.download.outputs.tag_name }} + + - name: 'Itch.io release' + run: './.github/workflows/scripts/butler_push.sh' + shell: bash + if: ${{ matrix.configuration == 'Release' }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..fdfa824aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +on: + push: + branches: + - 'itch-edition' + tags: + - "v*.*.*" + pull_request: + branches: + - itch-edition +jobs: + release: + env: + ARCH: ${{ matrix.os.flag }} + MACOSX_DEPLOYMENT_TARGET: 10.15 + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + PROD_MACOS_CERTIFICATE: '${{ secrets.PROD_MACOS_CERTIFICATE }}' + PROD_MACOS_CERTIFICATE_PWD: '${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}' + PROD_MACOS_CERTIFICATE_NAME: '${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}' + PROD_MACOS_CI_KEYCHAIN_PWD: '${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}' + PROD_MACOS_NOTARIZATION_APPLE_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}' + PROD_MACOS_NOTARIZATION_TEAM_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}' + PROD_MACOS_NOTARIZATION_PWD: '${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}' + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + DEBUG_FLAG: ${{ matrix.configuration == 'Debug' && 'true' || 'false' }} + BUILD_OS: ${{ matrix.os.name }} + strategy: + fail-fast: false + matrix: + os: + - name: macos + suffix: '-intel' + flag: x86_64 + deps: macos-universal + version: 13 + scons-script: './.github/workflows/scripts/mac/scons-build.sh' + - name: macos + flag: arm64 + suffix: '-silicon' + deps: macos-universal + version: 14 + scons-script: './.github/workflows/scripts/mac/scons-build.sh' +# - name: ubuntu +# suffix: '' +# version: 22.04 +# scons-script: scons + - name: windows + suffix: '' + version: 2019 + scons-script: './.github/workflows/scripts/win/scons-build.bat' + configuration: + - Release + - Debug + runs-on: '${{ matrix.os.name }}-${{ matrix.os.version }}' + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: "core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');" + - name: checkout + uses: actions/checkout@v4 + with: + submodules: true + - name: Download dependency build + uses: robinraju/release-downloader@v1 + with: + repository: NQNStudios/cboe-dependencies + latest: true + fileName: 'dependencies-${{ matrix.os.deps || matrix.os.name }}-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'deps' + - name: Windows build dependencies + run: 'vcpkg install libxml2 && pip install scons' + if: ${{ matrix.os.name == 'windows' }} + - name: Mac build dependencies + run: brew install scons + if: ${{ matrix.os.name == 'macos' }} + - name: Linux build dependencies + run: sudo apt-get update && sudo apt-get install scons libxml2-utils libgl-dev libopenal-dev + if: ${{ matrix.os.name == 'ubuntu' }} + - name: Install TGUI + run: 'sudo ./.github/workflows/scripts/linux/install-tgui.sh' + if: ${{ matrix.os.name == 'ubuntu' }} + - name: Build + run: '${{ matrix.os.scons-script }} test=false debug=$DEBUG_FLAG' + shell: bash + + - name: Download fix-rpaths.py script + run: git clone https://gist.github.com/NQNStudios/7145bcf6621891f5176c8caa165d6b93 + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths game + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/Blades of Exile.app"' + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths scenario editor + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/BoE Scenario Editor.app"' + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths character editor + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/BoE Character Editor.app"' + if: ${{ matrix.os.name == 'macos' }} + + - run: cp .itch.toml "build/Blades of Exile/" + shell: bash + + - name: 'Tar unsigned files' + run: 'tar -cvf cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned.tar "Blades of Exile"' + working-directory: '${{ github.workspace }}/build' + - name: upload pre-signing artifact + uses: actions/upload-artifact@v4 + with: + name: cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned + path: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned.tar' + # Skipping this for now because of issue nqnstudios#13 + - name: Codesign and notarize + run: 'SIGN="no" NOTARIZE="no" ./.github/workflows/scripts/mac/sign-apps.sh' + if: ${{ matrix.os.name == 'macos' }} + - name: 'Tar files' + run: 'tar -cvf cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar "Blades of Exile"' + working-directory: '${{ github.workspace }}/build' + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }} + path: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar' + - name: Github release + uses: softprops/action-gh-release@v2 + with: + files: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar' + if: ${{ startsWith(github.ref, 'refs/tags/') }} + - name: 'Itch.io release' + run: './.github/workflows/scripts/butler_push.sh' + shell: bash + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.configuration == 'Release' && matrix.os.name != 'macos' }} diff --git a/.github/workflows/scripts/butler_push.sh b/.github/workflows/scripts/butler_push.sh new file mode 100755 index 000000000..5012a04f6 --- /dev/null +++ b/.github/workflows/scripts/butler_push.sh @@ -0,0 +1,28 @@ +#! /bin/bash + +butler_channel="" +butler_exe="" +release_dir="" +if [ "$BUILD_OS" = "ubuntu" ]; then + butler_channel=linux-amd64 + butler_exe=butler + release_dir="linux" +elif [ "$BUILD_OS" = "windows" ]; then + butler_channel=windows-amd64 + butler_exe=butler.exe + release_dir="windows" +elif [ "$BUILD_OS" = "macos" ]; then + butler_channel=darwin-amd64 + butler_exe=butler + release_dir="macos" +fi + +# -L follows redirects +# -O specifies output name +curl -L -o butler.zip https://broth.itch.ovh/butler/${butler_channel}/LATEST/archive/default +unzip butler.zip +# GNU unzip tends to not set the executable bit even though it's set in the .zip +chmod +x ${butler_exe} +# just a sanity check run (and also helpful in case you're sharing CI logs) +./${butler_exe} -V +./${butler_exe} push "build/Blades of Exile/" nqn/blades-of-exile:${release_dir} diff --git a/.github/workflows/scripts/linux/install-tgui.sh b/.github/workflows/scripts/linux/install-tgui.sh index 876bcdccb..9b32464df 100755 --- a/.github/workflows/scripts/linux/install-tgui.sh +++ b/.github/workflows/scripts/linux/install-tgui.sh @@ -3,7 +3,7 @@ git clone --depth 1 -b 0.9 https://github.com/texus/TGUI.git cd TGUI export CLICOLOR_FORCE=1 -cmake -D TGUI_CXX_STANDARD=14 . +SFML_DIR=../deps/lib/cmake/SFML cmake -D TGUI_CXX_STANDARD=14 -D SFML_DIR=../deps/lib/cmake/SFML . make cmake --install . cd .. # Probably not needed but... diff --git a/.github/workflows/scripts/mac/make-universal.sh b/.github/workflows/scripts/mac/make-universal.sh new file mode 100755 index 000000000..0675d9680 --- /dev/null +++ b/.github/workflows/scripts/mac/make-universal.sh @@ -0,0 +1,27 @@ +#! /bin/bash + +if [ -z "$CONFIGURATION" ]; then + CONFIGURATION=Release +fi + +INTEL=cboe-macos-intel-$CONFIGURATION +SILICON=cboe-macos-silicon-$CONFIGURATION + +combine() { + mkdir -p "build/Blades of Exile/$1.app/Contents/MacOS" + lipo -create "$SILICON/Blades of Exile/$1.app/Contents/MacOS/$1" "$INTEL/Blades of Exile/$1.app/Contents/MacOS/$1" -output "build/Blades of Exile/$1.app/Contents/MacOS/$1" + cp -r "$SILICON/Blades of Exile/$1.app/Contents/Frameworks" "build/Blades of Exile/$1.app/Contents/" + cp -r "$SILICON/Blades of Exile/$1.app/Contents/Resources" "build/Blades of Exile/$1.app/Contents/" + cp "$SILICON/Blades of Exile/$1.app/Contents/Info.plist" "build/Blades of Exile/$1.app/Contents/" + cp "$SILICON/Blades of Exile/$1.app/Contents/PkgInfo" "build/Blades of Exile/$1.app/Contents/" +} + +combine "Blades of Exile" +combine "BoE Scenario Editor" +combine "BoE Character Editor" + +cp -r "$SILICON/Blades of Exile/Blades of Exile Base" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/Blades of Exile Scenarios" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/data" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/docs" "build/Blades of Exile/" +cp "$SILICON/Blades of Exile/.itch.toml" "build/Blades of Exile/" \ No newline at end of file diff --git a/.github/workflows/scripts/mac/scons-build.sh b/.github/workflows/scripts/mac/scons-build.sh index 4cdf6d328..a130f4780 100755 --- a/.github/workflows/scripts/mac/scons-build.sh +++ b/.github/workflows/scripts/mac/scons-build.sh @@ -4,4 +4,4 @@ export CC="$(brew --prefix llvm)/bin/clang" export CXX="$(brew --prefix llvm)/bin/clang++" export SDKROOT="$(xcrun --show-sdk-path)" -scons CXXFLAGS="-I/usr/local/opt/zlib/include" LINKFLAGS="-L/usr/local/opt/zlib/lib" $@ +scons CXXFLAGS="-I/usr/local/opt/zlib/include -arch $ARCH" LINKFLAGS="-L/usr/local/opt/zlib/lib" $@ diff --git a/.github/workflows/scripts/mac/sign-apps.sh b/.github/workflows/scripts/mac/sign-apps.sh new file mode 100755 index 000000000..40ae904be --- /dev/null +++ b/.github/workflows/scripts/mac/sign-apps.sh @@ -0,0 +1,79 @@ +#! /bin/bash + +# CODE-SIGNING STEP +# Original Source: https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ +# Modified by NQNStudios + +# Turn our base64-encoded certificate back to a regular .p12 file + +echo $PROD_MACOS_CERTIFICATE | base64 --decode > certificate.p12 + +# We need to create a new keychain, otherwise using the certificate will prompt +# with a UI dialog asking for the certificate password, which we can't +# use in a headless CI environment + +security create-keychain -p "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain +security default-keychain -s build.keychain +security unlock-keychain -p "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain +security import certificate.p12 -k build.keychain -P "$PROD_MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain + +sign() { + if [ "$SIGN" = "no" ]; + then + return + fi + APP_PATH="build/Blades of Exile/$1.app" + + # We finally codesign our app bundle, specifying the Hardened runtime option + + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH"/Contents/Frameworks/*.dylib -v + (cd "$APP_PATH" && frameworks=Contents/Frameworks/*.framework/Versions/A/* && \ + for framework in $frameworks; do + if [ -f "$framework" ]; then + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$framework" -v + fi + done) + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH"/Contents/Frameworks/*.framework/Versions/A/Resources/Info.plist -v + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH/Contents/Info.plist" -v + + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH" -v + + # NOTARIZATION STEP + if [ "$NOTARIZE" = "no" ]; + then + return + fi + + # (same source) + + # Store the notarization credentials so that we can prevent a UI password dialog + # from blocking the CI + + echo "Create keychain profile" + xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + + # We can't notarize an app bundle directly, but we need to compress it as an archive. + # Therefore, we create a zip file containing our app bundle, so that we can send it to the + # notarization service + + echo "Creating temp notarization archive" + ditto -c -k --keepParent "$APP_PATH" "notarization.zip" + + # Here we send the notarization request to the Apple's Notarization service, waiting for the result. + # This typically takes a few seconds inside a CI environment, but it might take more depending on the App + # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if + # you're curious + + echo "Notarize app" + xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait + + # Finally, we need to "attach the staple" to our executable, which will allow our app to be + # validated by macOS even when an internet connection is not available. + echo "Attach staple" + xcrun stapler staple "$APP_PATH" || exit 1 +} + +sign "Blades of Exile" +sign "BoE Scenario Editor" +sign "BoE Character Editor" \ No newline at end of file diff --git a/.itch.toml b/.itch.toml new file mode 100644 index 000000000..2a0393fd1 --- /dev/null +++ b/.itch.toml @@ -0,0 +1,9 @@ +[[actions]] +name = "Play" +path = "Blades of Exile.exe" +platform = "windows" + +[[actions]] +name = "Play" +path = "Blades of Exile.app" +platform = "osx" \ No newline at end of file diff --git a/SConstruct b/SConstruct index 19a9fce5f..ed27f3911 100644 --- a/SConstruct +++ b/SConstruct @@ -232,13 +232,15 @@ elif platform == "win32": vcpkg_other_libs = list(filter(path.exists, vcpkg_other_libs)) vcpkg_other_bins = [path.join(d.get_abspath(), 'bin') for d in vcpkg_other_packages] vcpkg_other_bins = list(filter(path.exists, vcpkg_other_bins)) - - include_paths=[path.join(vcpkg_installed, 'include')] + vcpkg_other_includes + project_includes = [] + for (root, dirs, files) in os.walk('src'): + project_includes.append(path.join(os.getcwd(), root)) + include_paths=project_includes env.Append( - LINKFLAGS=['/SUBSYSTEM:WINDOWS','/ENTRY:mainCRTStartup',f'/MACHINE:X{arch_short}'], + LINKFLAGS=['/SUBSYSTEM:WINDOWS','/ENTRY:mainCRTStartup',f'/MACHINE:X{arch_short}', '/VERBOSE', '/NODEFAULTLIB:libboost_filesystem-vc142-mt-x64-1_85.lib'], CXXFLAGS=['/EHsc','/MD','/FIglobal.hpp'], CPPPATH=include_paths, - LIBPATH=[path.join(vcpkg_installed, 'lib')] + vcpkg_other_libs + vcpkg_other_bins, + LIBPATH=[], LIBS=Split(""" kernel32 user32 @@ -259,7 +261,10 @@ elif platform == "win32": def build_app_package(env, source, build_dir, info): env.Install(build_dir, source) elif platform == "posix": - env.Append(CXXFLAGS=["-std=c++14","-include","global.hpp"]) + env.Append( + CXXFLAGS=["-std=c++14","-include","global.hpp"], + #LINKFLAGS=["-rpath-link", "deps/lib/", "-rpath", "./"] + ) def build_app_package(env, source, build_dir, info): env.Install(build_dir, source) @@ -415,6 +420,7 @@ if not env.GetOption('clean'): if platform == 'posix': def check_tgui(conf, second_attempt=False): if conf.CheckLib('libtgui', language='C++'): + bundled_libs.append('tgui') return conf else: if second_attempt: @@ -496,7 +502,31 @@ Export("data_dir") SConscript(["rsrc/SConscript", "doc/SConscript"]) # Bundle required frameworks and libraries - +def handle_bundled_libs(extension, prefix=''): + target_dirs = ["#build/Blades of Exile", "#build/test"] + for lib in bundled_libs: + for lpath in env['LIBPATH']: + def check_path(src_file): + _dir = os.path.dirname(src_file) + print(f'checking {_dir} for {prefix}{lib}') + try: + print(os.listdir(_dir)) + except: + pass + if path.exists(src_file) and src_file != "/usr/lib/x86_64-linux-gnu/libz.so": + for targ in target_dirs: + for so in os.listdir(_dir): + if os.path.basename(src_file) in so: + print(f'found {path.join(_dir, so)}') + env.Install(targ, path.join(_dir, so)) + return True + return False + if check_path(path.join(lpath, prefix + lib + extension)): + break + elif check_path(path.join(lpath.replace('lib', 'bin'), prefix + lib + extension)): + break + elif check_path(path.join(lpath, 'x86_64-linux-gnu', prefix + lib + extension)): + break if platform == "darwin": app_targets = [] if 'game' in targets: @@ -538,20 +568,7 @@ elif platform == "win32": brotlidec brotlicommon """) - target_dirs = ["#build/Blades of Exile", "#build/test"] - for lib in bundled_libs: - for lpath in env['LIBPATH']: - src_file = path.join(lpath, lib + ".dll") - if path.exists(src_file): - for targ in target_dirs: - env.Install(targ, src_file) - break - elif 'lib' in lpath: - src_file = path.join(lpath.replace('lib', 'bin'), lib + ".dll") - if path.exists(src_file): - for targ in target_dirs: - env.Install(targ, src_file) - break + handle_bundled_libs(".dll") # Extra: Microsoft redistributable libraries installer if 'msvc' in env["TOOLS"]: if path.exists("deps/VCRedistInstall.exe"): @@ -559,11 +576,41 @@ elif platform == "win32": else: print("WARNING: Cannot find installer for the MSVC redistributable libraries for your version of Visual Studio.") print("Please download it from Microsoft's website and place it at:") - print(" deps/VCRedistInstall.exe") + print(" deps/VCRedistInstall.exe") # Create it so its lack doesn't cause makensis to break # (Because the installer is an optional component.) os.makedirs("build/Blades of Exile", exist_ok=True) open("build/Blades of Exile/VCRedistInstall.exe", 'w').close() +elif platform == "posix": + targets = [ + "Blades of Exile", + "BoE Character Editor", + "BoE Scenario Editor", + ] + def patchelf(): + to_patch = targets + [so for so in os.listdir("build/Blades of Exile") if '.so' in so] + for targ in to_patch: + subprocess.call(['patchelf', '--set-rpath', '.', targ], cwd='build/Blades of Exile') + atexit.register(patchelf) + bundled_libs += Split(""" + GL + X11 + stdc++ + Xrandr + Xcursor + udev + openal + vorbisenc + vorbisfile + vorbis + ogg + FLAC + freetype + GLdispatch + GLX + xcb + """) + handle_bundled_libs(".so", "lib") if env["package"]: if platform == "darwin": diff --git a/rsrc/dialogs/welcome.xml b/rsrc/dialogs/welcome.xml index 4413f472a..dd6aab382 100644 --- a/rsrc/dialogs/welcome.xml +++ b/rsrc/dialogs/welcome.xml @@ -9,10 +9,17 @@ Spiderweb Software, created in 1997 and later released as Free Open-Source Software. - - http://www.spidweb.com/blades/opensource.html + + You're playing an Itch Edition release of OpenBoE. The Itch Edition + was funded by many generous supporters through IndieGoGo, and is + currently funded by donations on Patreon. Click below to follow the project + and see the rewards for becoming a supporter! - + + http://patreon.com/blades_itch_edition + + + Three exciting original scenarios await you, filled with many hours of puzzles, fighting and adventure! In addition to these, hundreds more written by diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 030c8e3e7..877b6f581 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -69,8 +69,8 @@ static std::string get_file_error() { // Debug builds run from working directory "build/Blades of Exile" // should helpfully let you enter replay test scenarios. -// Support for multiple scenario directories will also help with how I plan -// to handle scenario packaging/distribution for my fork's release. +// The Itch Edition finds scenarios in the Itch client's install path, +// so scenarios can be distributed as Itch games. // - Nat std::vector all_scen_dirs() { std::vector scen_dirs = { scenDir }; @@ -87,6 +87,30 @@ std::vector all_scen_dirs() { } #endif + // Itch Edition: Search Itch client apps. + // But not recursively, because the player could have tons of games installed + // and that would search ALL of the installed files. + // In fact, try to disqualify folders ASAP by only allowing .boes, .txt, + // and Itch meta files + // (designers might want to ship a README.txt) + fs::path itch_apps_path = scenDir/".."/".."/"itch"/"apps"; + if(fs::is_directory(itch_apps_path)){ + for(fs::directory_iterator app_iter(itch_apps_path); app_iter != fs::directory_iterator(); app_iter++){ + fs::path app = *app_iter; + if(!fs::is_directory(app)) continue; + for(fs::directory_iterator file_iter(app); file_iter != fs::directory_iterator(); file_iter++){ + fs::path file = *file_iter; + if(file.extension() == ".boes"){ + scen_dirs.push_back(*app_iter); + break; + }else if(file.extension() == ".itch"){ + }else if(file.extension() != ".txt"){ + break; + } + } + } + } + return scen_dirs; } From 7d2f491b20c91d1c19e4d97bac12dd854c11195a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 1 Aug 2025 10:51:19 -0500 Subject: [PATCH 02/86] add as many custom scenarios as possible --- .gitmodules | 3 +++ proj/vs2013/Common/Common.vcxproj | 1 + proj/vs2017/Common/Common.vcxproj | 1 + rsrc/SConscript | 1 + rsrc/scenarios/custom | 1 + src/fileio/fileio_scen.cpp | 3 +++ 6 files changed, 10 insertions(+) create mode 160000 rsrc/scenarios/custom diff --git a/.gitmodules b/.gitmodules index 656289c2b..f8fa759e9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "deps/fix-rpaths"] path = deps/fix-rpaths url = https://gist.github.com/NQNStudios/7145bcf6621891f5176c8caa165d6b93 +[submodule "rsrc/scenarios/custom"] + path = rsrc/scenarios/custom + url = https://github.com/NQNStudios/cboe-scenarios diff --git a/proj/vs2013/Common/Common.vcxproj b/proj/vs2013/Common/Common.vcxproj index 2307fbff6..3db02b127 100644 --- a/proj/vs2013/Common/Common.vcxproj +++ b/proj/vs2013/Common/Common.vcxproj @@ -502,6 +502,7 @@ + diff --git a/proj/vs2017/Common/Common.vcxproj b/proj/vs2017/Common/Common.vcxproj index d2c0b4f87..082b7c0b9 100644 --- a/proj/vs2017/Common/Common.vcxproj +++ b/proj/vs2017/Common/Common.vcxproj @@ -561,6 +561,7 @@ + diff --git a/rsrc/SConscript b/rsrc/SConscript index e86015816..c01b3a7c2 100644 --- a/rsrc/SConscript +++ b/rsrc/SConscript @@ -57,6 +57,7 @@ scen_env.BuildScenario('#build/rsrc/scenarios/stealth.boes', 'scenarios/stealth/ scen_env.BuildScenario('#build/rsrc/scenarios/zakhazi.boes', 'scenarios/zakhazi/header.exs') env.Install(path.join(install_dir, "Blades of Exile Scenarios"), Glob("#build/rsrc/scenarios/*.boes")) +env.Install(path.join(install_dir, "Blades of Exile Scenarios"), "scenarios/custom") env.Install(path.join(install_dir, "Blades of Exile Base"), Glob("#build/rsrc/bases/*.boes")) # Validate dialogs diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom new file mode 160000 index 000000000..0a6259c37 --- /dev/null +++ b/rsrc/scenarios/custom @@ -0,0 +1 @@ +Subproject commit 0a6259c37fa3dd2c1fec7e5256f7b16b590cc790 diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 877b6f581..f0a41d0fe 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -79,6 +79,9 @@ std::vector all_scen_dirs() { scen_dirs.push_back(scen_dir); } + // Experimental: ship with the full scenario archive + scen_dirs.push_back(progDir/"Blades of Exile Scenarios/custom"); + #ifdef DEBUG fs::path replay_scenarios_dir = boost::filesystem::current_path(); replay_scenarios_dir = replay_scenarios_dir/".."/".."/"test"/"replays"/"scenarios"; From 7687c771d8e21bb0f6c0ef3db1c683ed58e028de Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 1 Aug 2025 11:33:22 -0500 Subject: [PATCH 03/86] loading scen headers, skip parts of legacy load that may error --- src/fileio/fileio_scen.cpp | 9 +++++---- src/scenario/scenario.cpp | 5 ++++- src/scenario/scenario.hpp | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index f0a41d0fe..6edb5e9bb 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -293,8 +293,11 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario return false; } port_item_list(&item_data); - scenario.import_legacy(temp_scenario); - scenario.import_legacy(item_data); + scenario.import_legacy(temp_scenario, load_type == eLoadScenario::ONLY_HEADER); + if(load_type == eLoadScenario::FULL){ + scenario.ter_types[23].fly_over = false; + scenario.import_legacy(item_data); + } // TODO: Consider skipping the fread and assignment when len is 0 scenario.special_items.resize(50); @@ -322,8 +325,6 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario fclose(file_id); - scenario.ter_types[23].fly_over = false; - scenario.scen_file = file_to_load; if(load_type == eLoadScenario::ONLY_HEADER) return true; load_spec_graphics_v1(scenario.scen_file); diff --git a/src/scenario/scenario.cpp b/src/scenario/scenario.cpp index 0337292ac..5f0df0578 100644 --- a/src/scenario/scenario.cpp +++ b/src/scenario/scenario.cpp @@ -231,7 +231,7 @@ cScenario::cItemStorage::cItemStorage() : ter_type(-1), property(0) { item_odds[i] = 0; } -void cScenario::import_legacy(legacy::scenario_data_type& old){ +void cScenario::import_legacy(legacy::scenario_data_type& old, bool header_only){ is_legacy = true; // TODO eventually the absence of feature flags here will replace is_legacy altogether feature_flags = {}; @@ -267,6 +267,9 @@ void cScenario::import_legacy(legacy::scenario_data_type& old){ rating = eContentRating(old.rating); // TODO: Is this used anywhere? uses_custom_graphics = old.uses_custom_graphics; + + if(header_only) return; + boats.resize(30); horses.resize(30); for(short i = 0; i < 30; i++) { diff --git a/src/scenario/scenario.hpp b/src/scenario/scenario.hpp index b61ca6ef7..3b7944784 100644 --- a/src/scenario/scenario.hpp +++ b/src/scenario/scenario.hpp @@ -148,7 +148,7 @@ class cScenario { towns.back()->init_start(); } - void import_legacy(legacy::scenario_data_type& old); + void import_legacy(legacy::scenario_data_type& old, bool header_only = false); void import_legacy(legacy::scen_item_data_type& old); void writeTo(cTagFile& file) const; void readFrom(const cTagFile& file); From 4f1d89ee92e8803dec7a679709723dedc54853c0 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 1 Aug 2025 11:53:48 -0500 Subject: [PATCH 04/86] Scenario picker handle names with first character numeric --- rsrc/dialogs/pick-scenario.xml | 1 + src/game/boe.dlgutil.cpp | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rsrc/dialogs/pick-scenario.xml b/rsrc/dialogs/pick-scenario.xml index ead9e7e95..d5ecbc9e5 100644 --- a/rsrc/dialogs/pick-scenario.xml +++ b/rsrc/dialogs/pick-scenario.xml @@ -75,6 +75,7 @@ + + + diff --git a/src/dialogxml/dialogs/btnpanel.cpp b/src/dialogxml/dialogs/btnpanel.cpp new file mode 100644 index 000000000..086398cb0 --- /dev/null +++ b/src/dialogxml/dialogs/btnpanel.cpp @@ -0,0 +1,111 @@ +#include "btnpanel.hpp" + +#include +#include + +#include +#include + +#include "dialogxml/widgets/field.hpp" +#include "dialogxml/dialogs/strdlog.hpp" +#include "fileio/resmgr/res_dialog.hpp" +#include "sounds.hpp" +#include "gfx/render_shapes.hpp" +#include "tools/cursors.hpp" + +cButtonPanel::cButtonPanel(cDialog* parent) + : dlg(*ResMgr::dialogs.get("tiny-button-panel"), parent) +{} + +cButtonPanel::cButtonPanel(const std::vector& strs, std::vector> click_handlers, std::string title, std::string ok_str, cDialog* parent) + : cButtonPanel(parent) +{ + setTitle(title); + if(!ok_str.empty()){ + dlg["done"].setText(ok_str); + } + strings = strs; + this->click_handlers = click_handlers; + attachHandlers(); +} + +void cButtonPanel::attachHandlers() { + using namespace std::placeholders; + dlg["left"].attachClickHandler(std::bind(&cButtonPanel::onLeft,this)); + dlg["right"].attachClickHandler(std::bind(&cButtonPanel::onRight,this)); + dlg["done"].attachClickHandler(std::bind(&cButtonPanel::onOkay,this,_1)); + dlg["cancel"].attachClickHandler(std::bind(&cButtonPanel::onCancel,this,_1)); + if(strings.size() <= per_page) { + dlg["left"].hide(); + dlg["right"].hide(); + } + // Attach click handler to the tiny buttons + for(int i = 0; i < per_page; ++i){ + short string_idx = page * per_page + i; + std::ostringstream sout; + sout << "button" << i + 1; + dlg[sout.str()].attachClickHandler([i, this](cDialog&,std::string,eKeyMod) -> bool { + click_handlers[i](*this); + return true; + }); + } +} + +cDialog* cButtonPanel::operator->() { + return &dlg; +} + +bool cButtonPanel::show() { + page = 0; + dlg.run(std::bind(&cButtonPanel::fillPage, this)); + return dlg.getResult(); +} + +void cButtonPanel::fillPage(){ + for(unsigned int i = 0; i < per_page; i++){ + short string_idx = page * per_page + i; + std::ostringstream sout; + sout << "button" << i + 1; + if(string_idx < strings.size()){ + dlg[sout.str()].setText(strings[string_idx]); + dlg[sout.str()].recalcRect(); + dlg[sout.str()].show(); + }else{ + dlg[sout.str()].hide(); + } + } +} + +bool cButtonPanel::onLeft(){ + if(page == 0) page = lastPage(); + else page--; + fillPage(); + return true; +} + +bool cButtonPanel::onRight(){ + if(page == lastPage()) page = 0; + else page++; + fillPage(); + return true; +} + +bool cButtonPanel::onCancel(cDialog& me){ + dlg.setResult(false); + me.toast(false); + return true; +} + +bool cButtonPanel::onOkay(cDialog& me){ + dlg.setResult(true); + me.toast(true); + return true; +} + +void cButtonPanel::setTitle(const std::string &title) { + if(!title.empty()) dlg["title"].setText(title); +} + +size_t cButtonPanel::lastPage() const { + return (strings.size() - 1) / per_page; +} \ No newline at end of file diff --git a/src/dialogxml/dialogs/btnpanel.hpp b/src/dialogxml/dialogs/btnpanel.hpp new file mode 100644 index 000000000..903172967 --- /dev/null +++ b/src/dialogxml/dialogs/btnpanel.hpp @@ -0,0 +1,47 @@ +#ifndef DIALOG_BTNPANEL_H +#define DIALOG_BTNPANEL_H + +#include +#include +#include +#include +#include "dialog.hpp" +#include "dialogxml/widgets/ledgroup.hpp" + +/// A dialog that presents a list of labeled tiny buttons, plus an OK and Cancel button. +/// The list may span several pages. +class cButtonPanel { + const size_t per_page = 5; + cDialog dlg; + bool onLeft(); + bool onRight(); + bool onCancel(cDialog& me); + bool onOkay(cDialog& me); + void attachHandlers(); + void fillPage(); + size_t lastPage() const; + std::vector strings; + std::vector> click_handlers; + size_t page, cur; + cButtonPanel(cDialog* parent); +public: + /// Initializes a dialog from a list of strings. + /// @param strs A list of all strings in the dialog. + /// @param click_handlers A list of click handlers for the strings + /// @param title The title to show in the dialog. + /// @param parent Optionally, a parent dialog. + explicit cButtonPanel(const std::vector& strs, std::vector> click_handlers, std::string title, std::string ok_str = "", cDialog* parent = nullptr); + /// Reference the cDialog powering this choice dialog, perhaps to customize details of it. + /// @return A pointer to the dialog. + cDialog* operator->(); + /// Show the dialog. + /// @return True if OK was pressed, false if Cancel pressed + bool show(); + /// Set the dialog's title. + /// @param title The new title. + void setTitle(const std::string& title); + /// Get the list of strings. + std::vector getStrings() const { return strings; } +}; + +#endif From 9bc963a761b5f2addd9d00fc4017ac8f5843100e Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 14:22:34 -0500 Subject: [PATCH 09/86] updates to scen archive --- rsrc/scenarios/custom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom index 0a6259c37..d17ffb61d 160000 --- a/rsrc/scenarios/custom +++ b/rsrc/scenarios/custom @@ -1 +1 @@ -Subproject commit 0a6259c37fa3dd2c1fec7e5256f7b16b590cc790 +Subproject commit d17ffb61da9b314980cd28962889dbd3e30dc5f3 From abeb8954ab6433eb37dc74e482035ad46fda9394 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 14:22:56 -0500 Subject: [PATCH 10/86] for legacy scenarios, help open readme or load prefab --- src/game/boe.dlgutil.cpp | 50 ++++++++++++++++++++++++++++++++++++++-- src/game/boe.fileio.cpp | 24 +++++++++++++++++++ src/game/boe.fileio.hpp | 3 +++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index 83491f690..ae1c77a42 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -28,6 +28,7 @@ #include "utility.hpp" #include "mathutil.hpp" #include "dialogxml/dialogs/strdlog.hpp" +#include "dialogxml/dialogs/btnpanel.hpp" #include "dialogxml/dialogs/choicedlog.hpp" #include "gfx/render_shapes.hpp" #include "tools/winutil.hpp" @@ -60,8 +61,10 @@ extern std::shared_ptr text_sbar,item_sbar,shop_sbar; extern std::shared_ptr done_btn, help_btn; extern bool map_visible; extern cUniverse univ; +extern cCustomGraphics spec_scen_g; extern std::map skill_max; extern void give_help_and_record(short help1, short help2, bool help_forced = false); +extern void post_load(); short sign_mode,person_graphic,store_person_graphic,store_sign_mode; long num_talk_entries; @@ -1825,12 +1828,55 @@ class cChooseScenario { prefab.prog_make_ver[1] = 0; prefab.prog_make_ver[2] = 0; me.setResult(prefab); + me.toast(true); } else { int scen_hit = which + (page - 1) * 3; if(scen_hit >= scen_headers.size()) return false; - me.setResult(scen_headers[scen_hit]); + + // Show text files, Offer to load prefab party + auto scen = scen_headers[scen_hit]; + std::vector files = extra_files(locate_scenario(scen.file)); + if(!files.empty()){ + std::vector choices; + std::vector> handlers; + + for(fs::path file : files){ + std::string ext = file.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + if(ext == ".sav"){ + choices.push_back("Load premade party: " + file.filename().string()); + handlers.push_back([file](cButtonPanel&) -> void { + if(!load_party(file, univ, spec_scen_g)) { + std::cout << "Failed to load save file: " << file << std::endl; + }else{ + finish_load_party(); + if(overall_mode != MODE_STARTUP) + post_load(); + } + }); + }else{ + choices.push_back("Open file: " + file.filename().string()); + handlers.push_back([file](cButtonPanel&) -> void { + launchURL("file://" + file.string()); + }); + } + } + + cButtonPanel panel(choices, handlers, scen.name, "Launch", &me); + dynamic_cast(panel->getControl("pic")).setPict(scen.intro_pic,PIC_SCEN); + if(panel.show()){ + // Launch pressed. + me.setResult(scen); + me.toast(true); + }; + } + // No extra files. Just launch + else{ + me.setResult(scen); + me.toast(true); + } + } - me.toast(true); return true; } diff --git a/src/game/boe.fileio.cpp b/src/game/boe.fileio.cpp index c303130a4..e4ec5de73 100644 --- a/src/game/boe.fileio.cpp +++ b/src/game/boe.fileio.cpp @@ -551,4 +551,28 @@ void try_auto_save(std::string reason) { ASB("Autosave: Save not completed"); } print_buf(); +} + +std::vector extra_extensions = {".sav", ".txt", ".rtf", ".htm", ".html"}; + +std::vector extra_files(fs::path scen_file) { + std::vector files; + + std::string scen_extension = scen_file.extension().string(); + std::transform(scen_extension.begin(), scen_extension.end(), scen_extension.begin(), tolower); + if(scen_extension != ".exs") return files; + + fs::path directory = scen_file.parent_path(); + + fs::recursive_directory_iterator file_iter(directory); + for(; file_iter != fs::recursive_directory_iterator(); file_iter++) { + fs::path file = *file_iter; + std::string extension = file.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); + if(std::find(extra_extensions.begin(), extra_extensions.end(), extension) != extra_extensions.end()){ + files.push_back(file); + } + } + + return files; } \ No newline at end of file diff --git a/src/game/boe.fileio.hpp b/src/game/boe.fileio.hpp index 4300a3d20..4016147b8 100644 --- a/src/game/boe.fileio.hpp +++ b/src/game/boe.fileio.hpp @@ -37,4 +37,7 @@ void try_auto_save(std::string reason); // Turn lower-case and strip articles from the front of a scenario title, for alphabetization std::string name_alphabetical(std::string scenario_name); +// Find extra files packaged with legacy scenarios (such as readme, prefab party): +std::vector extra_files(fs::path scen_file); + #endif From ffea37ae0354aa0489b4efcaf46ed72d030a377a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 15:47:02 -0500 Subject: [PATCH 11/86] blank out scen_name on legacy record if not in_scen --- src/fileio/fileio_party.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fileio/fileio_party.cpp b/src/fileio/fileio_party.cpp index f22a83fd8..716702146 100644 --- a/src/fileio/fileio_party.cpp +++ b/src/fileio/fileio_party.cpp @@ -284,6 +284,7 @@ bool load_party_v1(fs::path file_to_load, cUniverse& real_univ, bool town_restor univ.file = path; }else{ univ.party.scen_name = ""; + store_party.scen_name[0] = '\0'; } univ.party.import_legacy(store_party, univ); From eb212d493269f9c437793368ce9fed6bac9dc769 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 15:50:24 -0500 Subject: [PATCH 12/86] resize vector2d to have room for legacy load --- src/universe/party.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/universe/party.cpp b/src/universe/party.cpp index ba85429b5..a572597b0 100644 --- a/src/universe/party.cpp +++ b/src/universe/party.cpp @@ -274,10 +274,12 @@ void cParty::import_legacy(legacy::stored_items_list_type& old,short which_list) } void cParty::import_legacy(legacy::setup_save_type& old){ - for(int n = 0; n < 4; n++) + for(int n = 0; n < 4; n++){ + setup[n].resize(64, 64); for(int i = 0; i < 64; i++) for(int j = 0; j < 64; j++) setup[n][i][j] = old.setup[n][i][j]; + } } void cParty::cConvers::import_legacy(legacy::talk_save_type old, const cScenario& scenario){ From b086cd22eff58f568cd5281a675148cbbd3ed54f Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 15:50:43 -0500 Subject: [PATCH 13/86] bounds check when importing legacy journal entry --- src/universe/party.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/universe/party.cpp b/src/universe/party.cpp index a572597b0..7e3c23a44 100644 --- a/src/universe/party.cpp +++ b/src/universe/party.cpp @@ -283,7 +283,10 @@ void cParty::import_legacy(legacy::setup_save_type& old){ } void cParty::cConvers::import_legacy(legacy::talk_save_type old, const cScenario& scenario){ - who_said = scenario.towns[old.personality / 10]->talking.people[old.personality % 10].title; + size_t town = old.personality / 10; + size_t npc = old.personality % 10; + if(town >= scenario.towns.size()) return; + who_said = scenario.towns[town]->talking.people[npc].title; in_town = scenario.towns[old.town_num]->name; int strnums[2] = {old.str1, old.str2}; std::string* strs[2] = {&the_str1, &the_str2}; From 9ed4dd00e3e3d64b60db9478efdea1dfaed53c8c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 16:58:08 -0500 Subject: [PATCH 14/86] WIP fix loc_off_act_area --- src/game/boe.locutils.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index 259a4d3fd..061a90154 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -138,6 +138,7 @@ bool loc_off_world(location p1) { } bool loc_off_act_area(location p1) { + if(is_out()) return false; // How should this check be handled? if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && (p1.y > univ.town->in_town_rect.top) && (p1.y < univ.town->in_town_rect.bottom)) return false; From a1312e357f4eb1ab7f7c4493fcf8e462d144d132 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 17:37:11 -0500 Subject: [PATCH 15/86] Allow create new or load party after selecting scenario --- src/game/boe.dlgutil.cpp | 102 ++++++++++++++++++++++++--------------- src/game/boe.startup.cpp | 3 -- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index ae1c77a42..b1344d95f 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -65,6 +65,8 @@ extern cCustomGraphics spec_scen_g; extern std::map skill_max; extern void give_help_and_record(short help1, short help2, bool help_forced = false); extern void post_load(); +extern void start_new_game(bool force = false); +extern void do_load(); short sign_mode,person_graphic,store_person_graphic,store_sign_mode; long num_talk_entries; @@ -1815,6 +1817,7 @@ class cChooseScenario { bool doSelectScenario(int which) { int page = dynamic_cast(me["list"]).getPage(); + scen_header_type scen; if(page == 0) { scen_header_type prefab; switch(which) { @@ -1827,56 +1830,75 @@ class cChooseScenario { prefab.prog_make_ver[0] = 2; prefab.prog_make_ver[1] = 0; prefab.prog_make_ver[2] = 0; - me.setResult(prefab); - me.toast(true); + scen = prefab; } else { int scen_hit = which + (page - 1) * 3; if(scen_hit >= scen_headers.size()) return false; - - // Show text files, Offer to load prefab party - auto scen = scen_headers[scen_hit]; - std::vector files = extra_files(locate_scenario(scen.file)); - if(!files.empty()){ - std::vector choices; - std::vector> handlers; - - for(fs::path file : files){ - std::string ext = file.extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), tolower); - if(ext == ".sav"){ - choices.push_back("Load premade party: " + file.filename().string()); - handlers.push_back([file](cButtonPanel&) -> void { - if(!load_party(file, univ, spec_scen_g)) { - std::cout << "Failed to load save file: " << file << std::endl; - }else{ - finish_load_party(); - if(overall_mode != MODE_STARTUP) - post_load(); - } - }); + scen = scen_headers[scen_hit]; + } + std::vector choices; + std::vector> handlers; + // If no party is loaded, offer to load default or create new + if(!party_in_memory){ + choices.push_back("Create new party"); + handlers.push_back([](cButtonPanel& dlg) -> void { + start_new_game(); + if(party_in_memory){ + dlg->getControl("done").show(); + } + }); + choices.push_back("Load a party"); + handlers.push_back([](cButtonPanel& dlg) -> void { + do_load(); + if(party_in_memory){ + dlg->getControl("done").show(); + } + }); + } + + // Show text files, Offer to load prefab party + std::vector files = extra_files(locate_scenario(scen.file)); + for(fs::path file : files){ + std::string ext = file.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + if(ext == ".sav"){ + choices.push_back("Load premade party: " + file.filename().string()); + handlers.push_back([file](cButtonPanel& dlg) -> void { + if(!load_party(file, univ, spec_scen_g)) { + std::cout << "Failed to load save file: " << file << std::endl; }else{ - choices.push_back("Open file: " + file.filename().string()); - handlers.push_back([file](cButtonPanel&) -> void { - launchURL("file://" + file.string()); - }); + finish_load_party(); + if(overall_mode != MODE_STARTUP) + post_load(); + dlg->getControl("done").show(); } - } + }); + }else{ + choices.push_back("Open file: " + file.filename().string()); + handlers.push_back([file](cButtonPanel&) -> void { + launchURL("file://" + file.string()); + }); + } + } - cButtonPanel panel(choices, handlers, scen.name, "Launch", &me); - dynamic_cast(panel->getControl("pic")).setPict(scen.intro_pic,PIC_SCEN); - if(panel.show()){ - // Launch pressed. - me.setResult(scen); - me.toast(true); - }; + if(!choices.empty()){ + cButtonPanel panel(choices, handlers, scen.name, "Launch", &me); + if(!party_in_memory){ + panel->getControl("done").hide(); } - // No extra files. Just launch - else{ + dynamic_cast(panel->getControl("pic")).setPict(scen.intro_pic,PIC_SCEN); + if(panel.show()){ + // Launch pressed. me.setResult(scen); me.toast(true); - } - + }; } + // No extra files. Just launch + else{ + me.setResult(scen); + me.toast(true); + } + return true; } diff --git a/src/game/boe.startup.cpp b/src/game/boe.startup.cpp index f46c80f92..f1ad1f0f0 100644 --- a/src/game/boe.startup.cpp +++ b/src/game/boe.startup.cpp @@ -91,9 +91,6 @@ void handle_startup_button_click(eStartButton btn, eKeyMod mods) { force_party = true; start_new_game(true); - } else { - cChoiceDlog("need-party").show(); - break; } } From 113681686bc5b5688e2b9a46fd5f6b58eff004f0 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 2 Aug 2025 17:37:28 -0500 Subject: [PATCH 16/86] update scen archive --- rsrc/scenarios/custom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom index d17ffb61d..f20ba3b6a 160000 --- a/rsrc/scenarios/custom +++ b/rsrc/scenarios/custom @@ -1 +1 @@ -Subproject commit d17ffb61da9b314980cd28962889dbd3e30dc5f3 +Subproject commit f20ba3b6a0d2a5599a82124bc8e288bd1f2805cd From ba9e54cd945c9c49efed6580ea92b739ef99a9ff Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 10:08:34 -0500 Subject: [PATCH 17/86] Fix replaying scenario list with recursive scenario folder structure --- src/game/boe.fileio.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/game/boe.fileio.cpp b/src/game/boe.fileio.cpp index e4ec5de73..536f46713 100644 --- a/src/game/boe.fileio.cpp +++ b/src/game/boe.fileio.cpp @@ -360,11 +360,9 @@ std::vector build_scen_headers() { std::string scen_file; while(std::getline(in, scen_file)){ scen_header_type scen_head; - fs::path full_path; - for(fs::path scenDir : all_scen_dirs()){ - full_path = scenDir / scen_file; - if (fs::exists(full_path)) - break; + fs::path full_path = locate_scenario(scen_file, true); + if(full_path.empty()){ + LOG("Scenario missing! " + scen_file); } if(load_scenario_header(full_path, scen_head)){ scen_headers.push_back(scen_head); From d148577ebf86e78628bea62d0271dfcce592125c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 10:17:40 -0500 Subject: [PATCH 18/86] Fix replay which loads a prefab --- src/game/boe.dlgutil.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index b1344d95f..7bc8615cc 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -1864,6 +1864,11 @@ class cChooseScenario { if(ext == ".sav"){ choices.push_back("Load premade party: " + file.filename().string()); handlers.push_back([file](cButtonPanel& dlg) -> void { + if(replaying){ + // The next action encodes loading the prefab party, + // which is redundant here + pop_next_action(); + } if(!load_party(file, univ, spec_scen_g)) { std::cout << "Failed to load save file: " << file << std::endl; }else{ From 2011e577995df396b560b3c58643e4ffd2e98fd0 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 11:25:44 -0500 Subject: [PATCH 19/86] reuse RenderTexture --- src/game/boe.graphics.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 7d76df26b..b5209e973 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1186,7 +1186,12 @@ void place_trim(short q,short r,location where,ter_num_t ter_type) { } static void init_trim_mask(std::unique_ptr& mask, rectangle src_rect) { - sf::RenderTexture render; + static sf::RenderTexture render; + static bool init = false; + if(!init){ + render.create(28, 36); + init = true; + } rectangle dest_rect; dest_rect.top = src_rect.top % 36; dest_rect.bottom = (src_rect.bottom - 1) % 36 + 1; From e899df0dbffa574055b7ceaba3110d4b490351f1 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 11:35:11 -0500 Subject: [PATCH 20/86] Fix walkway trims. Fix #136 --- src/game/boe.graphics.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index b5209e973..52f50166a 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1197,11 +1197,10 @@ static void init_trim_mask(std::unique_ptr& mask, rectangle src_rec dest_rect.bottom = (src_rect.bottom - 1) % 36 + 1; dest_rect.left = src_rect.left % 28; dest_rect.right = (src_rect.right - 1) % 28 + 1; - std::tie(dest_rect.top, dest_rect.bottom) = std::make_tuple(36 - dest_rect.top, 36 - dest_rect.bottom); render.create(28, 36); render.clear(sf::Color::White); rect_draw_some_item(*ResMgr::graphics.get("trim"), src_rect, render, dest_rect); - render.display(); + // render.display(); // Using it as a mask, we don't need to flip mask.reset(new sf::Texture); mask->create(28, 36); mask->update(render.getTexture().copyToImage()); From 9fb72cb0103188dcfd93ce5cce8d9515dd9f23c1 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 12:33:31 -0500 Subject: [PATCH 21/86] delete obsolete branch which caused unintended roads to draw --- src/game/boe.graphics.cpp | 61 --------------------------------------- 1 file changed, 61 deletions(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 52f50166a..bc06a7069 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1372,67 +1372,6 @@ void place_road(short q,short r,location where,bool here) { to_rect.offset(13 + q * 28,13 + r * 36); rect_draw_some_item (roads_gworld, road_rects[0], terrain_screen_gworld(), to_rect); } - }else{ - // TODO: I suspect this branch is now irrelevant. - ter_num_t ter = coord_to_ter(where.x, where.y); - ter_num_t ref = coord_to_ter(where.x,where.y); - bool horz = false, vert = false; - eTrimType trim = eTrimType::NONE, vertTrim = eTrimType::NONE; - if(ref < univ.scenario.ter_types.size()) { - trim = univ.scenario.ter_types[ref].trim_type; - } - if(ter < univ.scenario.ter_types.size()) { - vertTrim = univ.scenario.ter_types[ter].trim_type; - } - if(where.y > 0) - ter = coord_to_ter(where.x,where.y - 1); - if((where.y == 0) || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - - if(((is_out()) && (where.x < 96)) || (!(is_out()) && (where.x < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x + 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.x == 96)) || (!(is_out()) && (where.x == univ.town->max_dim - 1)) - || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - - if(vert){ - if(((is_out()) && (where.y < 96)) || (!(is_out()) && (where.y < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x,where.y + 1); - eTrimType vertTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.y == 96)) || (!(is_out()) && (where.y == univ.town->max_dim - 1)) - || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - else vert = false; - } - - if(horz){ - if(where.x > 0) - ter = coord_to_ter(where.x - 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if((where.x == 0) || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - else horz = false; - } - - if(horz){ - to_rect = road_dest_rects[5]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[2], terrain_screen_gworld(), to_rect); - } - if(vert){ - to_rect = road_dest_rects[4]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[3], terrain_screen_gworld(), to_rect); - } } } From ef40b2171e5be69f18c046407822c4ef0b7ee6c5 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 18:22:01 -0500 Subject: [PATCH 22/86] apply substitutions when calculating line wrapping --- src/gfx/render_text.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index 869ac0704..a528945f7 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -108,10 +108,18 @@ static void push_snippets(size_t start, size_t end, text_params_t& options, size } while(start < upper_bound); } +std::map substitutions = { + {"–", "--"} +}; + break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextStyle style) { break_info_t break_info; if(str.empty()) return break_info; // Nothing to do! + for(auto it : substitutions){ + boost::replace_all(str, it.first, it.second); + } + sf::Text str_to_draw; style.applyTo(str_to_draw); short str_len = str.length(); @@ -241,10 +249,6 @@ std::string truncate_with_ellipsis(std::string str, const TextStyle& style, int return str; } -std::map substitutions = { - {"–", "--"} -}; - static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,text_params_t& options) { if(str.empty()) return; // Nothing to do! short line_height = options.style.lineHeight; From 7cc8d5b4775b064e2004f4b2c770ecac74850ae1 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 18:22:54 -0500 Subject: [PATCH 23/86] go to absurd lengths handling locales --- README.md | 2 +- SConstruct | 1 + proj/vs2013/vcpkg.json | 3 +- proj/vs2017/vcpkg.json | 3 +- src/fileio/fileio_scen.cpp | 80 +++++++++++++++++++++++++++++++++----- src/gfx/render_text.cpp | 6 ++- 6 files changed, 81 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 629580c1c..e9c500905 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ correct place to search. For example, if you installed Boost via Homebrew, you m add something like the following to the Project Build Settings, under Linking->Other Linking Flags: - -lboost_filesystem -lboost_system -L/usr/local/Cellar/boost/1.57.0/lib + -lboost_filesystem -lboost_system -lboost_locale -L/usr/local/Cellar/boost/1.57.0/lib Again with Homebrew, you may also need to add the following to Apple LLVM Custom Compiler Flags -> Other C++ Flags: diff --git a/SConstruct b/SConstruct index ed27f3911..4a2fcc12d 100644 --- a/SConstruct +++ b/SConstruct @@ -397,6 +397,7 @@ if not env.GetOption('clean'): check_header('boost/spirit/include/classic.hpp', 'Boost.Spirit.Classic') check_lib('boost_system', 'Boost.System', suffixes, boost_versions) check_lib('boost_filesystem', 'Boost.Filesystem', suffixes, boost_versions) + check_lib('boost_locale', 'Boost.Locale', suffixes, boost_versions) sfml_suffixes = ['-d'] check_lib('sfml-system', 'SFML-system', sfml_suffixes) check_lib('sfml-window', 'SFML-window', sfml_suffixes) diff --git a/proj/vs2013/vcpkg.json b/proj/vs2013/vcpkg.json index 7dc72c2e7..1b2b04d06 100644 --- a/proj/vs2013/vcpkg.json +++ b/proj/vs2013/vcpkg.json @@ -14,6 +14,7 @@ "boost-chrono", "boost-math", "boost-spirit", - "boost-process" + "boost-process", + "boost-locale" ] } \ No newline at end of file diff --git a/proj/vs2017/vcpkg.json b/proj/vs2017/vcpkg.json index 7dc72c2e7..1b2b04d06 100644 --- a/proj/vs2017/vcpkg.json +++ b/proj/vs2017/vcpkg.json @@ -14,6 +14,7 @@ "boost-chrono", "boost-math", "boost-spirit", - "boost-process" + "boost-process", + "boost-locale" ] } \ No newline at end of file diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 6edb5e9bb..704d98e99 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -9,10 +9,13 @@ #include "fileio.hpp" #include +#include #include #include #include #include +#include "tools/replay.hpp" +#include "dialogxml/dialogs/strchoice.hpp" #include "dialogxml/dialogs/strdlog.hpp" @@ -303,26 +306,83 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario scenario.special_items.resize(50); scenario.journal_strs.resize(50); scenario.spec_strs.resize(100); + + static std::vector encodings_to_try = {"Latin1", "Windows-1252", "MacRoman"}; + fs::path meta = file_to_load.parent_path() / "meta.xml"; + using namespace ticpp; + ticpp::Document meta_doc; + if(!fs::exists(meta)){ + ticpp::Element root_element("meta"); + meta_doc.InsertEndChild(root_element); + }else{ + meta_doc.LoadFile(meta.string()); + } + + auto info = info_from_action(*meta_doc.FirstChildElement()); + for(short i = 0; i < 270; i++) { len = (long) (temp_scenario.scen_str_len[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - if(i == 0) scenario.scen_name = temp_str; + + std::string decoded; + std::vector options; + if(info.find("encoding") != info.end()){ + std::string encoding = info["encoding"]; + decoded = boost::locale::conv::to_utf(temp_str, encoding); + }else{ + bool different = false; + for(std::string encoding : encodings_to_try){ + std::string enc = boost::locale::conv::to_utf(temp_str, encoding); + if(!options.empty() && enc != options.back()) different = true; + options.push_back(enc); + } + if(different){ + LOG_VALUE(file_to_load); + for(std::string enc : options){ + LOG_VALUE(enc); + } + int which = -1; + // Comment this out if you're not messing with the metadata: + // which = cStringChoice(options, "Which is best?").show(-1); + if(which != -1){ + info["encoding"] = encodings_to_try[which]; + decoded = options[which]; + } + else{ + decoded = options[0]; // temp! + } + }else{ + decoded = options.back(); + } + } + + if(i == 0) scenario.scen_name = decoded; else if(i == 1 || i == 2) - scenario.teaser_text[i-1] = temp_str; + scenario.teaser_text[i-1] = decoded; else if(i == 3) - scenario.contact_info[1] = temp_str; + scenario.contact_info[1] = decoded; else if(i >= 4 && i < 10) - scenario.intro_strs[i-4] = temp_str; + scenario.intro_strs[i-4] = decoded; else if(i >= 10 && i < 60) - scenario.journal_strs[i-10] = temp_str; + scenario.journal_strs[i-10] = decoded; else if(i >= 60 && i < 160) { - if(i % 2 == 0) scenario.special_items[(i-60)/2].name = temp_str; - else scenario.special_items[(i-60)/2].descr = temp_str; + if(i % 2 == 0) scenario.special_items[(i-60)/2].name = decoded; + else scenario.special_items[(i-60)/2].descr = decoded; } else if(i >= 260) continue; // These were never ever used, for some reason. - else scenario.spec_strs[i-160] = temp_str; - } - + else scenario.spec_strs[i-160] = decoded; + } + Element new_root("meta"); + ticpp::Document new_doc; + for(auto& p : info){ + Element next_child(p.first); + Text child_text(p.second); + next_child.InsertEndChild(child_text); + new_root.InsertEndChild(next_child); + } + new_doc.InsertEndChild(new_root); + new_doc.SaveFile(meta.string()); + fclose(file_id); scenario.scen_file = file_to_load; diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index a528945f7..a2445a0b2 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -109,7 +109,11 @@ static void push_snippets(size_t start, size_t end, text_params_t& options, size } std::map substitutions = { - {"–", "--"} + {"–", "--"}, + {"´", "'"}, + {"©", "(C)"}, + {"…", "..."}, + {"™", "TM"} }; break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextStyle style) { From f659d2fa399bcef4b8f7c6ead9f30ea02b383307 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 18:26:15 -0500 Subject: [PATCH 24/86] guessed encodings --- rsrc/scenarios/custom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom index f20ba3b6a..a3c512d97 160000 --- a/rsrc/scenarios/custom +++ b/rsrc/scenarios/custom @@ -1 +1 @@ -Subproject commit f20ba3b6a0d2a5599a82124bc8e288bd1f2805cd +Subproject commit a3c512d9762922163e870efe51f3f8e4cf9b8b9b From ae03eac912d7c5d2ab9a8814ac0439232be8ce8a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 20:12:47 -0500 Subject: [PATCH 25/86] run trimming and decoding on more scenario strings --- src/fileio/fileio_scen.cpp | 58 ++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 704d98e99..d96e691d6 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -46,8 +46,8 @@ void load_spec_graphics_v1(fs::path scen_file); void load_spec_graphics_v2(int num_sheets); // Load old scenarios (town talk is handled by the town loading function) static bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario load_type); -static bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario); -static bool load_town_v1(fs::path scen_file,short which_town,cTown& the_town,legacy::scenario_data_type& scenario,std::vector& shops); +static bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario, std::string encoding); +static bool load_town_v1(fs::path scen_file,short which_town,cTown& the_town,legacy::scenario_data_type& scenario,std::vector& shops, std::string encoding); // Load new scenarios static bool load_scenario_v2(fs::path file_to_load, cScenario& scenario, eLoadScenario load_type); // Some of these are non-static so that the test cases can access them. @@ -319,6 +319,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario } auto info = info_from_action(*meta_doc.FirstChildElement()); + std::string encoding = ""; for(short i = 0; i < 270; i++) { len = (long) (temp_scenario.scen_str_len[i]); @@ -328,7 +329,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario std::string decoded; std::vector options; if(info.find("encoding") != info.end()){ - std::string encoding = info["encoding"]; + encoding = info["encoding"]; decoded = boost::locale::conv::to_utf(temp_str, encoding); }else{ bool different = false; @@ -346,7 +347,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario // Comment this out if you're not messing with the metadata: // which = cStringChoice(options, "Which is best?").show(-1); if(which != -1){ - info["encoding"] = encodings_to_try[which]; + encoding = info["encoding"] = encodings_to_try[which]; decoded = options[which]; } else{ @@ -394,7 +395,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario for(int x = 0; x < temp_scenario.out_width; x++) { for(int y = 0; y < temp_scenario.out_height; y++) { scenario.outdoors[x][y] = new cOutdoors(scenario); - load_outdoors_v1(scenario.scen_file, loc(x,y), *scenario.outdoors[x][y], temp_scenario); + load_outdoors_v1(scenario.scen_file, loc(x,y), *scenario.outdoors[x][y], temp_scenario, encoding); } } @@ -409,7 +410,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario case 1: scenario.towns[i] = new cTown(scenario, AREA_MEDIUM); break; case 2: scenario.towns[i] = new cTown(scenario, AREA_SMALL); break; } - load_town_v1(scenario.scen_file, i, *scenario.towns[i], temp_scenario, shops); + load_town_v1(scenario.scen_file, i, *scenario.towns[i], temp_scenario, shops, encoding); } // Enable character creation in starting town scenario.towns[scenario.which_town_start]->has_tavern = true; @@ -2530,7 +2531,7 @@ static long get_town_offset(short which_town, legacy::scenario_data_type& scenar return len_to_jump; } -bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy::scenario_data_type& scenario, std::vector& shops) { +bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy::scenario_data_type& scenario, std::vector& shops, std::string encoding) { long len,len_to_jump = 0; char temp_str[256]; legacy::town_record_type store_town; @@ -2594,8 +2595,11 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: len = (long) (store_town.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - // Trim whitespace off of strings, which are fixed-width in legacy scenarios std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional if(i == 0) the_town.name = temp_str_trimmed; else if(i >= 1 && i < 17) @@ -2621,20 +2625,26 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: len = (long) (store_talk.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; + std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios + boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional if(i >= 0 && i < 10) - the_town.talking.people[i].title = temp_str; + the_town.talking.people[i].title = temp_str_trimmed; else if(i >= 10 && i < 20) - the_town.talking.people[i-10].look = temp_str; + the_town.talking.people[i-10].look = temp_str_trimmed; else if(i >= 20 && i < 30) - the_town.talking.people[i-20].name = temp_str; + the_town.talking.people[i-20].name = temp_str_trimmed; else if(i >= 30 && i < 40) - the_town.talking.people[i-30].job = temp_str; + the_town.talking.people[i-30].job = temp_str_trimmed; else if(i >= 160) - the_town.talking.people[i-160].dunno = temp_str; + the_town.talking.people[i-160].dunno = temp_str_trimmed; else { if(i % 2 == 0) - the_town.talking.talk_nodes[(i-40)/2].str1 = temp_str; - else the_town.talking.talk_nodes[(i-40)/2].str2 = temp_str; + the_town.talking.talk_nodes[(i-40)/2].str1 = temp_str_trimmed; + else the_town.talking.talk_nodes[(i-40)/2].str2 = temp_str_trimmed; } } @@ -2671,7 +2681,7 @@ static long get_outdoors_offset(location& which_out, legacy::scenario_data_type& } //mode -> 0 - primary load 1 - add to top 2 - right 3 - bottom 4 - left -bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario){ +bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario, std::string encoding){ long len,len_to_jump; char temp_str[256]; legacy::outdoor_record_type store_out; @@ -2707,13 +2717,19 @@ bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, len = (long) (store_out.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - if(i == 0) the_out.name = temp_str; - else if(i == 9) the_out.comment = temp_str; - else if(i < 9) the_out.area_desc[i-1].descr = temp_str; + std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios + boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional + if(i == 0) the_out.name = temp_str_trimmed; + else if(i == 9) the_out.comment = temp_str_trimmed; + else if(i < 9) the_out.area_desc[i-1].descr = temp_str_trimmed; else if(i >= 10 && i < 100) - the_out.spec_strs[i-10] = temp_str; + the_out.spec_strs[i-10] = temp_str_trimmed; else if(i >= 100 && i < 108) - the_out.sign_locs[i-100].text = temp_str; + the_out.sign_locs[i-100].text = temp_str_trimmed; } if(fclose(file_id) != 0) { From 2a845a79749df0057783dfe1ca67de513b2e4886 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 3 Aug 2025 20:24:49 -0500 Subject: [PATCH 26/86] fix word highlighting where substitutions are used --- src/game/boe.newgraph.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index b978b3a02..cd920e4f7 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -34,6 +34,7 @@ #include "tools/enum_map.hpp" #include "replay.hpp" #include +#include short monsters_faces[190] = { 0,1,2,3,4,5,6,7,8,9, @@ -994,6 +995,13 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col // First determine the offsets of clickable words. // The added spaces ensure that end-of-word boundaries are found std::string str = str_to_place + " |" + str_to_place2 + " "; + + // TODO use a font where we don't need this + extern std::map substitutions; + for(auto it : substitutions){ + boost::replace_all(str, it.first, it.second); + } + std::vector hilites; std::vector nodes; int wordStart = 0, wordEnd = 0; From b85633261da2fac5b00d32636123b7d39c2a22ec Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 03:48:10 -0500 Subject: [PATCH 27/86] Don't draw trims on top of characters in combat --- src/game/boe.graphics.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index bc06a7069..904ea2fe7 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1250,6 +1250,13 @@ void draw_trim(short q,short r,short which_trim,ter_num_t ground_ter) { } sf::Color test_color = {0,0,0}, store_color; + location targ; + targ.x = q; + targ.y = r; + if(supressing_some_spaces && (targ != ok_space[0]) && (targ != ok_space[1]) && + (targ != ok_space[2]) && (targ != ok_space[3])) + return; + unsigned short pic = univ.scenario.ter_types[ground_ter].picture; if(pic < 960){ int which_sheet = pic / 50; From 13ee6c9768903a468c60176293a6b5c25f27823a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 04:07:24 -0500 Subject: [PATCH 28/86] fix empty monsters in activate_groups() --- src/game/boe.monster.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index 52320bd4a..9d4882f07 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -1192,7 +1192,7 @@ void activate_monsters(short code,short /*attitude*/) { if(code == 0) return; for(short i = 0; i < univ.town->creatures.size(); i++) - if(univ.town->creatures[i].spec_enc_code == code) { + if(univ.town->creatures[i].number > 0 && univ.town->creatures[i].spec_enc_code == code) { cTownperson& monst = univ.town->creatures[i]; univ.town.monst.assign(i, monst, univ.scenario.scen_monsters[monst.number], univ.party.easy_mode, univ.difficulty_adjust()); univ.town.monst[i].spec_enc_code = 0; From 3c7f2f261842866185233993ab8c84de823f55be Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 04:38:09 -0500 Subject: [PATCH 29/86] handle MacRoman specially --- src/fileio/fileio_scen.cpp | 24 +++- src/fileio/mac_roman.hpp | 278 +++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 src/fileio/mac_roman.hpp diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index d96e691d6..d6f284a1b 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -30,6 +30,7 @@ #include "replay.hpp" #include "porting.hpp" +#include "fileio/mac_roman.hpp" #include "fileio/resmgr/res_image.hpp" #include "fileio/resmgr/res_sound.hpp" @@ -44,6 +45,7 @@ extern std::string last_load_file; void load_spec_graphics_v1(fs::path scen_file); void load_spec_graphics_v2(int num_sheets); +std::string decode_temp_str(std::string temp_str, std::string encoding); // Load old scenarios (town talk is handled by the town loading function) static bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario load_type); static bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario, std::string encoding); @@ -330,11 +332,11 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario std::vector options; if(info.find("encoding") != info.end()){ encoding = info["encoding"]; - decoded = boost::locale::conv::to_utf(temp_str, encoding); + decoded = decode_temp_str(temp_str, encoding); }else{ bool different = false; for(std::string encoding : encodings_to_try){ - std::string enc = boost::locale::conv::to_utf(temp_str, encoding); + std::string enc = decode_temp_str(temp_str, encoding); if(!options.empty() && enc != options.back()) different = true; options.push_back(enc); } @@ -2597,7 +2599,7 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: temp_str[len] = 0; std::string temp_str_trimmed = temp_str; if(!encoding.empty()){ - temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + temp_str_trimmed = decode_temp_str(temp_str, encoding); } // Trim whitespace off of strings, which are fixed-width in legacy scenarios boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional @@ -2627,7 +2629,7 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: temp_str[len] = 0; std::string temp_str_trimmed = temp_str; if(!encoding.empty()){ - temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + temp_str_trimmed = decode_temp_str(temp_str, encoding); } // Trim whitespace off of strings, which are fixed-width in legacy scenarios boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional @@ -2719,7 +2721,7 @@ bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, temp_str[len] = 0; std::string temp_str_trimmed = temp_str; if(!encoding.empty()){ - temp_str_trimmed = boost::locale::conv::to_utf(temp_str, encoding); + temp_str_trimmed = decode_temp_str(temp_str, encoding); } // Trim whitespace off of strings, which are fixed-width in legacy scenarios boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional @@ -2800,3 +2802,15 @@ void load_spec_graphics_v2(int num_sheets) { spec_scen_g.sheets[num_sheets] = &ResMgr::graphics.get(name); } } + +std::string decode_temp_str(std::string temp_str, std::string encoding) { + if(encoding == "MacRoman"){ + std::basic_string utf16; + std::basic_string utf8; + for(char byte : temp_str){ + utf16.push_back(gMORToUnicode[(unsigned char) byte]); + } + return boost::locale::conv::utf_to_utf(utf16); + } + return boost::locale::conv::to_utf(temp_str, encoding); +} \ No newline at end of file diff --git a/src/fileio/mac_roman.hpp b/src/fileio/mac_roman.hpp new file mode 100644 index 000000000..54b34921a --- /dev/null +++ b/src/fileio/mac_roman.hpp @@ -0,0 +1,278 @@ +// Source: https://github.com/fadden/nulib2/blob/master/nufxlib/Charset.c + +/* + * NuFX archive manipulation library + * Copyright (C) 2014 by Andy McFadden, All Rights Reserved. + * This is free software; you can redistribute it and/or modify it under the + * terms of the BSD License, see the file COPYING-LIB. + * + * Miscellaneous NufxLib utility functions. + */ + + /* + * Convert Mac OS Roman to Unicode. Mapping comes from: + * + * http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/ROMAN.TXT + * + * We use the "Control Pictures" block for the control characters + * (0x00-0x1f, 0x7f --> 0x2400-0x241f, 0x2421). This is a bit nicer + * than embedding control characters in filenames. + */ +static const uint16_t gMORToUnicode[256] = { + /*0x00*/ 0x2400, // [control] NULL + /*0x01*/ 0x2401, // [control] START OF HEADING + /*0x02*/ 0x2402, // [control] START OF TEXT + /*0x03*/ 0x2403, // [control] END OF TEXT + /*0x04*/ 0x2404, // [control] END OF TRANSMISSION + /*0x05*/ 0x2405, // [control] ENQUIRY + /*0x06*/ 0x2406, // [control] ACKNOWLEDGE + /*0x07*/ 0x2407, // [control] BELL + /*0x08*/ 0x2408, // [control] BACKSPACE + /*0x09*/ 0x2409, // [control] HORIZONTAL TABULATION + /*0x0a*/ 0x240a, // [control] LINE FEED + /*0x0b*/ 0x240b, // [control] VERTICAL TABULATION + /*0x0c*/ 0x240c, // [control] FORM FEED + /*0x0d*/ 0x240d, // [control] CARRIAGE RETURN + /*0x0e*/ 0x240e, // [control] SHIFT OUT + /*0x0f*/ 0x240f, // [control] SHIFT IN + /*0x10*/ 0x2410, // [control] DATA LINK ESCAPE + /*0x11*/ 0x2411, // [control] DEVICE CONTROL ONE + /*0x12*/ 0x2412, // [control] DEVICE CONTROL TWO + /*0x13*/ 0x2413, // [control] DEVICE CONTROL THREE + /*0x14*/ 0x2414, // [control] DEVICE CONTROL FOUR + /*0x15*/ 0x2415, // [control] NEGATIVE ACKNOWLEDGE + /*0x16*/ 0x2416, // [control] SYNCHRONOUS IDLE + /*0x17*/ 0x2417, // [control] END OF TRANSMISSION BLOCK + /*0x18*/ 0x2418, // [control] CANCEL + /*0x19*/ 0x2419, // [control] END OF MEDIUM + /*0x1a*/ 0x241a, // [control] SUBSTITUTE + /*0x1b*/ 0x241b, // [control] ESCAPE + /*0x1c*/ 0x241c, // [control] FILE SEPARATOR + /*0x1d*/ 0x241d, // [control] GROUP SEPARATOR + /*0x1e*/ 0x241e, // [control] RECORD SEPARATOR + /*0x1f*/ 0x241f, // [control] UNIT SEPARATOR + /*0x20*/ 0x0020, // SPACE + /*0x21*/ 0x0021, // EXCLAMATION MARK + /*0x22*/ 0x0022, // QUOTATION MARK + /*0x23*/ 0x0023, // NUMBER SIGN + /*0x24*/ 0x0024, // DOLLAR SIGN + /*0x25*/ 0x0025, // PERCENT SIGN + /*0x26*/ 0x0026, // AMPERSAND + /*0x27*/ 0x0027, // APOSTROPHE + /*0x28*/ 0x0028, // LEFT PARENTHESIS + /*0x29*/ 0x0029, // RIGHT PARENTHESIS + /*0x2A*/ 0x002A, // ASTERISK + /*0x2B*/ 0x002B, // PLUS SIGN + /*0x2C*/ 0x002C, // COMMA + /*0x2D*/ 0x002D, // HYPHEN-MINUS + /*0x2E*/ 0x002E, // FULL STOP + /*0x2F*/ 0x002F, // SOLIDUS + /*0x30*/ 0x0030, // DIGIT ZERO + /*0x31*/ 0x0031, // DIGIT ONE + /*0x32*/ 0x0032, // DIGIT TWO + /*0x33*/ 0x0033, // DIGIT THREE + /*0x34*/ 0x0034, // DIGIT FOUR + /*0x35*/ 0x0035, // DIGIT FIVE + /*0x36*/ 0x0036, // DIGIT SIX + /*0x37*/ 0x0037, // DIGIT SEVEN + /*0x38*/ 0x0038, // DIGIT EIGHT + /*0x39*/ 0x0039, // DIGIT NINE + /*0x3A*/ 0x003A, // COLON + /*0x3B*/ 0x003B, // SEMICOLON + /*0x3C*/ 0x003C, // LESS-THAN SIGN + /*0x3D*/ 0x003D, // EQUALS SIGN + /*0x3E*/ 0x003E, // GREATER-THAN SIGN + /*0x3F*/ 0x003F, // QUESTION MARK + /*0x40*/ 0x0040, // COMMERCIAL AT + /*0x41*/ 0x0041, // LATIN CAPITAL LETTER A + /*0x42*/ 0x0042, // LATIN CAPITAL LETTER B + /*0x43*/ 0x0043, // LATIN CAPITAL LETTER C + /*0x44*/ 0x0044, // LATIN CAPITAL LETTER D + /*0x45*/ 0x0045, // LATIN CAPITAL LETTER E + /*0x46*/ 0x0046, // LATIN CAPITAL LETTER F + /*0x47*/ 0x0047, // LATIN CAPITAL LETTER G + /*0x48*/ 0x0048, // LATIN CAPITAL LETTER H + /*0x49*/ 0x0049, // LATIN CAPITAL LETTER I + /*0x4A*/ 0x004A, // LATIN CAPITAL LETTER J + /*0x4B*/ 0x004B, // LATIN CAPITAL LETTER K + /*0x4C*/ 0x004C, // LATIN CAPITAL LETTER L + /*0x4D*/ 0x004D, // LATIN CAPITAL LETTER M + /*0x4E*/ 0x004E, // LATIN CAPITAL LETTER N + /*0x4F*/ 0x004F, // LATIN CAPITAL LETTER O + /*0x50*/ 0x0050, // LATIN CAPITAL LETTER P + /*0x51*/ 0x0051, // LATIN CAPITAL LETTER Q + /*0x52*/ 0x0052, // LATIN CAPITAL LETTER R + /*0x53*/ 0x0053, // LATIN CAPITAL LETTER S + /*0x54*/ 0x0054, // LATIN CAPITAL LETTER T + /*0x55*/ 0x0055, // LATIN CAPITAL LETTER U + /*0x56*/ 0x0056, // LATIN CAPITAL LETTER V + /*0x57*/ 0x0057, // LATIN CAPITAL LETTER W + /*0x58*/ 0x0058, // LATIN CAPITAL LETTER X + /*0x59*/ 0x0059, // LATIN CAPITAL LETTER Y + /*0x5A*/ 0x005A, // LATIN CAPITAL LETTER Z + /*0x5B*/ 0x005B, // LEFT SQUARE BRACKET + /*0x5C*/ 0x005C, // REVERSE SOLIDUS + /*0x5D*/ 0x005D, // RIGHT SQUARE BRACKET + /*0x5E*/ 0x005E, // CIRCUMFLEX ACCENT + /*0x5F*/ 0x005F, // LOW LINE + /*0x60*/ 0x0060, // GRAVE ACCENT + /*0x61*/ 0x0061, // LATIN SMALL LETTER A + /*0x62*/ 0x0062, // LATIN SMALL LETTER B + /*0x63*/ 0x0063, // LATIN SMALL LETTER C + /*0x64*/ 0x0064, // LATIN SMALL LETTER D + /*0x65*/ 0x0065, // LATIN SMALL LETTER E + /*0x66*/ 0x0066, // LATIN SMALL LETTER F + /*0x67*/ 0x0067, // LATIN SMALL LETTER G + /*0x68*/ 0x0068, // LATIN SMALL LETTER H + /*0x69*/ 0x0069, // LATIN SMALL LETTER I + /*0x6A*/ 0x006A, // LATIN SMALL LETTER J + /*0x6B*/ 0x006B, // LATIN SMALL LETTER K + /*0x6C*/ 0x006C, // LATIN SMALL LETTER L + /*0x6D*/ 0x006D, // LATIN SMALL LETTER M + /*0x6E*/ 0x006E, // LATIN SMALL LETTER N + /*0x6F*/ 0x006F, // LATIN SMALL LETTER O + /*0x70*/ 0x0070, // LATIN SMALL LETTER P + /*0x71*/ 0x0071, // LATIN SMALL LETTER Q + /*0x72*/ 0x0072, // LATIN SMALL LETTER R + /*0x73*/ 0x0073, // LATIN SMALL LETTER S + /*0x74*/ 0x0074, // LATIN SMALL LETTER T + /*0x75*/ 0x0075, // LATIN SMALL LETTER U + /*0x76*/ 0x0076, // LATIN SMALL LETTER V + /*0x77*/ 0x0077, // LATIN SMALL LETTER W + /*0x78*/ 0x0078, // LATIN SMALL LETTER X + /*0x79*/ 0x0079, // LATIN SMALL LETTER Y + /*0x7A*/ 0x007A, // LATIN SMALL LETTER Z + /*0x7B*/ 0x007B, // LEFT CURLY BRACKET + /*0x7C*/ 0x007C, // VERTICAL LINE + /*0x7D*/ 0x007D, // RIGHT CURLY BRACKET + /*0x7E*/ 0x007E, // TILDE + /*0x7f*/ 0x2421, // [control] DELETE + /*0x80*/ 0x00C4, // LATIN CAPITAL LETTER A WITH DIAERESIS + /*0x81*/ 0x00C5, // LATIN CAPITAL LETTER A WITH RING ABOVE + /*0x82*/ 0x00C7, // LATIN CAPITAL LETTER C WITH CEDILLA + /*0x83*/ 0x00C9, // LATIN CAPITAL LETTER E WITH ACUTE + /*0x84*/ 0x00D1, // LATIN CAPITAL LETTER N WITH TILDE + /*0x85*/ 0x00D6, // LATIN CAPITAL LETTER O WITH DIAERESIS + /*0x86*/ 0x00DC, // LATIN CAPITAL LETTER U WITH DIAERESIS + /*0x87*/ 0x00E1, // LATIN SMALL LETTER A WITH ACUTE + /*0x88*/ 0x00E0, // LATIN SMALL LETTER A WITH GRAVE + /*0x89*/ 0x00E2, // LATIN SMALL LETTER A WITH CIRCUMFLEX + /*0x8A*/ 0x00E4, // LATIN SMALL LETTER A WITH DIAERESIS + /*0x8B*/ 0x00E3, // LATIN SMALL LETTER A WITH TILDE + /*0x8C*/ 0x00E5, // LATIN SMALL LETTER A WITH RING ABOVE + /*0x8D*/ 0x00E7, // LATIN SMALL LETTER C WITH CEDILLA + /*0x8E*/ 0x00E9, // LATIN SMALL LETTER E WITH ACUTE + /*0x8F*/ 0x00E8, // LATIN SMALL LETTER E WITH GRAVE + /*0x90*/ 0x00EA, // LATIN SMALL LETTER E WITH CIRCUMFLEX + /*0x91*/ 0x00EB, // LATIN SMALL LETTER E WITH DIAERESIS + /*0x92*/ 0x00ED, // LATIN SMALL LETTER I WITH ACUTE + /*0x93*/ 0x00EC, // LATIN SMALL LETTER I WITH GRAVE + /*0x94*/ 0x00EE, // LATIN SMALL LETTER I WITH CIRCUMFLEX + /*0x95*/ 0x00EF, // LATIN SMALL LETTER I WITH DIAERESIS + /*0x96*/ 0x00F1, // LATIN SMALL LETTER N WITH TILDE + /*0x97*/ 0x00F3, // LATIN SMALL LETTER O WITH ACUTE + /*0x98*/ 0x00F2, // LATIN SMALL LETTER O WITH GRAVE + /*0x99*/ 0x00F4, // LATIN SMALL LETTER O WITH CIRCUMFLEX + /*0x9A*/ 0x00F6, // LATIN SMALL LETTER O WITH DIAERESIS + /*0x9B*/ 0x00F5, // LATIN SMALL LETTER O WITH TILDE + /*0x9C*/ 0x00FA, // LATIN SMALL LETTER U WITH ACUTE + /*0x9D*/ 0x00F9, // LATIN SMALL LETTER U WITH GRAVE + /*0x9E*/ 0x00FB, // LATIN SMALL LETTER U WITH CIRCUMFLEX + /*0x9F*/ 0x00FC, // LATIN SMALL LETTER U WITH DIAERESIS + /*0xA0*/ 0x2020, // DAGGER + /*0xA1*/ 0x00B0, // DEGREE SIGN + /*0xA2*/ 0x00A2, // CENT SIGN + /*0xA3*/ 0x00A3, // POUND SIGN + /*0xA4*/ 0x00A7, // SECTION SIGN + /*0xA5*/ 0x2022, // BULLET + /*0xA6*/ 0x00B6, // PILCROW SIGN + /*0xA7*/ 0x00DF, // LATIN SMALL LETTER SHARP S + /*0xA8*/ 0x00AE, // REGISTERED SIGN + /*0xA9*/ 0x00A9, // COPYRIGHT SIGN + /*0xAA*/ 0x2122, // TRADE MARK SIGN + /*0xAB*/ 0x00B4, // ACUTE ACCENT + /*0xAC*/ 0x00A8, // DIAERESIS + /*0xAD*/ 0x2260, // NOT EQUAL TO + /*0xAE*/ 0x00C6, // LATIN CAPITAL LETTER AE + /*0xAF*/ 0x00D8, // LATIN CAPITAL LETTER O WITH STROKE + /*0xB0*/ 0x221E, // INFINITY + /*0xB1*/ 0x00B1, // PLUS-MINUS SIGN + /*0xB2*/ 0x2264, // LESS-THAN OR EQUAL TO + /*0xB3*/ 0x2265, // GREATER-THAN OR EQUAL TO + /*0xB4*/ 0x00A5, // YEN SIGN + /*0xB5*/ 0x00B5, // MICRO SIGN + /*0xB6*/ 0x2202, // PARTIAL DIFFERENTIAL + /*0xB7*/ 0x2211, // N-ARY SUMMATION + /*0xB8*/ 0x220F, // N-ARY PRODUCT + /*0xB9*/ 0x03C0, // GREEK SMALL LETTER PI + /*0xBA*/ 0x222B, // INTEGRAL + /*0xBB*/ 0x00AA, // FEMININE ORDINAL INDICATOR + /*0xBC*/ 0x00BA, // MASCULINE ORDINAL INDICATOR + /*0xBD*/ 0x03A9, // GREEK CAPITAL LETTER OMEGA + /*0xBE*/ 0x00E6, // LATIN SMALL LETTER AE + /*0xBF*/ 0x00F8, // LATIN SMALL LETTER O WITH STROKE + /*0xC0*/ 0x00BF, // INVERTED QUESTION MARK + /*0xC1*/ 0x00A1, // INVERTED EXCLAMATION MARK + /*0xC2*/ 0x00AC, // NOT SIGN + /*0xC3*/ 0x221A, // SQUARE ROOT + /*0xC4*/ 0x0192, // LATIN SMALL LETTER F WITH HOOK + /*0xC5*/ 0x2248, // ALMOST EQUAL TO + /*0xC6*/ 0x2206, // INCREMENT + /*0xC7*/ 0x00AB, // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + /*0xC8*/ 0x00BB, // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /*0xC9*/ 0x2026, // HORIZONTAL ELLIPSIS + /*0xCA*/ 0x00A0, // NO-BREAK SPACE + /*0xCB*/ 0x00C0, // LATIN CAPITAL LETTER A WITH GRAVE + /*0xCC*/ 0x00C3, // LATIN CAPITAL LETTER A WITH TILDE + /*0xCD*/ 0x00D5, // LATIN CAPITAL LETTER O WITH TILDE + /*0xCE*/ 0x0152, // LATIN CAPITAL LIGATURE OE + /*0xCF*/ 0x0153, // LATIN SMALL LIGATURE OE + /*0xD0*/ 0x2013, // EN DASH + /*0xD1*/ 0x2014, // EM DASH + /*0xD2*/ 0x201C, // LEFT DOUBLE QUOTATION MARK + /*0xD3*/ 0x201D, // RIGHT DOUBLE QUOTATION MARK + /*0xD4*/ 0x2018, // LEFT SINGLE QUOTATION MARK + /*0xD5*/ 0x2019, // RIGHT SINGLE QUOTATION MARK + /*0xD6*/ 0x00F7, // DIVISION SIGN + /*0xD7*/ 0x25CA, // LOZENGE + /*0xD8*/ 0x00FF, // LATIN SMALL LETTER Y WITH DIAERESIS + /*0xD9*/ 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS + /*0xDA*/ 0x2044, // FRACTION SLASH + /*0xDB*/ 0x00A4, // CURRENCY SIGN (was EURO SIGN) + /*0xDC*/ 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + /*0xDD*/ 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /*0xDE*/ 0xFB01, // LATIN SMALL LIGATURE FI + /*0xDF*/ 0xFB02, // LATIN SMALL LIGATURE FL + /*0xE0*/ 0x2021, // DOUBLE DAGGER + /*0xE1*/ 0x00B7, // MIDDLE DOT + /*0xE2*/ 0x201A, // SINGLE LOW-9 QUOTATION MARK + /*0xE3*/ 0x201E, // DOUBLE LOW-9 QUOTATION MARK + /*0xE4*/ 0x2030, // PER MILLE SIGN + /*0xE5*/ 0x00C2, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + /*0xE6*/ 0x00CA, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + /*0xE7*/ 0x00C1, // LATIN CAPITAL LETTER A WITH ACUTE + /*0xE8*/ 0x00CB, // LATIN CAPITAL LETTER E WITH DIAERESIS + /*0xE9*/ 0x00C8, // LATIN CAPITAL LETTER E WITH GRAVE + /*0xEA*/ 0x00CD, // LATIN CAPITAL LETTER I WITH ACUTE + /*0xEB*/ 0x00CE, // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + /*0xEC*/ 0x00CF, // LATIN CAPITAL LETTER I WITH DIAERESIS + /*0xED*/ 0x00CC, // LATIN CAPITAL LETTER I WITH GRAVE + /*0xEE*/ 0x00D3, // LATIN CAPITAL LETTER O WITH ACUTE + /*0xEF*/ 0x00D4, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + /*0xF0*/ 0xF8FF, // Apple logo + /*0xF1*/ 0x00D2, // LATIN CAPITAL LETTER O WITH GRAVE + /*0xF2*/ 0x00DA, // LATIN CAPITAL LETTER U WITH ACUTE + /*0xF3*/ 0x00DB, // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + /*0xF4*/ 0x00D9, // LATIN CAPITAL LETTER U WITH GRAVE + /*0xF5*/ 0x0131, // LATIN SMALL LETTER DOTLESS I + /*0xF6*/ 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT + /*0xF7*/ 0x02DC, // SMALL TILDE + /*0xF8*/ 0x00AF, // MACRON + /*0xF9*/ 0x02D8, // BREVE + /*0xFA*/ 0x02D9, // DOT ABOVE + /*0xFB*/ 0x02DA, // RING ABOVE + /*0xFC*/ 0x00B8, // CEDILLA + /*0xFD*/ 0x02DD, // DOUBLE ACUTE ACCENT + /*0xFE*/ 0x02DB, // OGONEK + /*0xFF*/ 0x02C7 // CARON +}; \ No newline at end of file From 90b871818d98294a6f5c9cbcb8ee8797f78a83cd Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 11:21:37 -0500 Subject: [PATCH 30/86] fix monster field infliction logic. Fix #765 --- src/game/boe.monster.cpp | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index 9d4882f07..be4b46714 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -814,53 +814,44 @@ void monst_inflict_fields(short which_monst) { if(univ.town.monst[which_monst].is_alive()) { where_check.x = univ.town.monst[which_monst].cur_loc.x + i; where_check.y = univ.town.monst[which_monst].cur_loc.y + j; - // TODO: If the goal is to damage the monster by any fields it's on, why all the break statements? if(univ.town.is_quickfire(where_check.x,where_check.y)) { r1 = get_ran(2,1,8); damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } if(univ.town.is_blade_wall(where_check.x,where_check.y)) { r1 = get_ran(6,1,8); - if(have_radiate && which_radiate != eFieldType::WALL_BLADES) + if(!have_radiate || which_radiate != eFieldType::WALL_BLADES) damage_monst(*which_m,7,r1,eDamageType::WEAPON); - break; } if(univ.town.is_force_wall(where_check.x,where_check.y)) { r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FORCE) + if(!have_radiate || which_radiate != eFieldType::WALL_FORCE) damage_monst(*which_m,7,r1,eDamageType::MAGIC); - break; } if(univ.town.is_sleep_cloud(where_check.x,where_check.y)) { - if(have_radiate && which_radiate != eFieldType::CLOUD_SLEEP) + if(!have_radiate || which_radiate != eFieldType::CLOUD_SLEEP) which_m->sleep(eStatus::ASLEEP,3,0); - break; } if(univ.town.is_ice_wall(where_check.x,where_check.y)) { r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_ICE) + if(!have_radiate || which_radiate != eFieldType::WALL_ICE) damage_monst(*which_m,7,r1,eDamageType::COLD); - break; } if(univ.town.is_scloud(where_check.x,where_check.y)) { r1 = get_ran(1,2,3); - if(have_radiate && which_radiate != eFieldType::CLOUD_STINK) + if(!have_radiate || which_radiate != eFieldType::CLOUD_STINK) which_m->curse(r1); - break; } if(univ.town.is_web(where_check.x,where_check.y) && which_m->m_type != eRace::BUG) { which_m->spell_note(19); r1 = get_ran(1,2,3); which_m->web(r1); univ.town.set_web(where_check.x,where_check.y,false); - break; } if(univ.town.is_fire_wall(where_check.x,where_check.y)) { r1 = get_ran(2,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FIRE) + if(!have_radiate || which_radiate != eFieldType::WALL_FIRE) damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } if(univ.town.is_force_cage(where_check.x,where_check.y)) process_force_cage(where_check, univ.get_target_i(*which_m)); From 997895d032e06204d4635fddfdc8610be704b65f Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 11:30:25 -0500 Subject: [PATCH 31/86] big monsters don't take extra field damage from multiple of same --- src/game/boe.monster.cpp | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index be4b46714..549655db8 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -809,35 +809,50 @@ void monst_inflict_fields(short which_monst) { which_m = &univ.town.monst[which_monst]; bool have_radiate = which_m->abil[eMonstAbil::RADIATE].active; eFieldType which_radiate = which_m->abil[eMonstAbil::RADIATE].radiate.type; + // Judgment call: big monsters shouldn't only get damaged once per damage type if they're on + // multiple of the same field. (Except webs.) + bool quickfire = false; + bool blade_wall = false; + bool force_wall = false; + bool sleep_cloud = false; + bool ice_wall = false; + bool stink_cloud = false; + bool fire_wall = false; for(short i = 0; i < univ.town.monst[which_monst].x_width; i++) for(short j = 0; j < univ.town.monst[which_monst].y_width; j++) if(univ.town.monst[which_monst].is_alive()) { where_check.x = univ.town.monst[which_monst].cur_loc.x + i; where_check.y = univ.town.monst[which_monst].cur_loc.y + j; - if(univ.town.is_quickfire(where_check.x,where_check.y)) { + if(!quickfire && univ.town.is_quickfire(where_check.x,where_check.y)) { + quickfire = true; r1 = get_ran(2,1,8); damage_monst(*which_m,7,r1,eDamageType::FIRE); } - if(univ.town.is_blade_wall(where_check.x,where_check.y)) { + if(!blade_wall && univ.town.is_blade_wall(where_check.x,where_check.y)) { + blade_wall = true; r1 = get_ran(6,1,8); if(!have_radiate || which_radiate != eFieldType::WALL_BLADES) damage_monst(*which_m,7,r1,eDamageType::WEAPON); } - if(univ.town.is_force_wall(where_check.x,where_check.y)) { + if(!force_wall && univ.town.is_force_wall(where_check.x,where_check.y)) { + force_wall = true; r1 = get_ran(3,1,6); if(!have_radiate || which_radiate != eFieldType::WALL_FORCE) damage_monst(*which_m,7,r1,eDamageType::MAGIC); } - if(univ.town.is_sleep_cloud(where_check.x,where_check.y)) { + if(!sleep_cloud && univ.town.is_sleep_cloud(where_check.x,where_check.y)) { + sleep_cloud = true; if(!have_radiate || which_radiate != eFieldType::CLOUD_SLEEP) which_m->sleep(eStatus::ASLEEP,3,0); } - if(univ.town.is_ice_wall(where_check.x,where_check.y)) { + if(!ice_wall && univ.town.is_ice_wall(where_check.x,where_check.y)) { + ice_wall = true; r1 = get_ran(3,1,6); if(!have_radiate || which_radiate != eFieldType::WALL_ICE) damage_monst(*which_m,7,r1,eDamageType::COLD); } - if(univ.town.is_scloud(where_check.x,where_check.y)) { + if(!stink_cloud && univ.town.is_scloud(where_check.x,where_check.y)) { + stink_cloud = true; r1 = get_ran(1,2,3); if(!have_radiate || which_radiate != eFieldType::CLOUD_STINK) which_m->curse(r1); @@ -848,7 +863,8 @@ void monst_inflict_fields(short which_monst) { which_m->web(r1); univ.town.set_web(where_check.x,where_check.y,false); } - if(univ.town.is_fire_wall(where_check.x,where_check.y)) { + if(!fire_wall && univ.town.is_fire_wall(where_check.x,where_check.y)) { + fire_wall = true; r1 = get_ran(2,1,6); if(!have_radiate || which_radiate != eFieldType::WALL_FIRE) damage_monst(*which_m,7,r1,eDamageType::FIRE); From 5bbd0198689870645837b58c1bce6731fc6ba308 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 11:43:18 -0500 Subject: [PATCH 32/86] big monsters check whole footprint for safety. Fix #757 --- src/game/boe.monster.cpp | 34 +++++++++++++++++++++++----------- src/game/boe.monster.hpp | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index 549655db8..ef0289eb3 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -340,7 +340,7 @@ bool monst_hate_spot(short which_m,location *good_loc) { hate_spot = true; } if(hate_spot) { - prospect = find_clear_spot(loc,1); + prospect = find_clear_spot(loc,1,univ.town.monst[which_m].x_width,univ.town.monst[which_m].y_width); if(prospect.x > 0) { *good_loc = prospect; return true; @@ -707,10 +707,12 @@ bool try_move(short i,location start,short x,short y) { } bool combat_move_monster(short which,location destination) { - if(!monst_can_be_there(destination,which)) + if(!monst_can_be_there(destination,which)){ return false; - else if(!monst_check_special_terrain(destination,2,which)) + } + else if(!monst_check_special_terrain(destination,2,which)){ return false; + } else { univ.town.monst[which].direction = set_direction(univ.town.monst[which].cur_loc, destination); univ.town.monst[which].cur_loc = destination; @@ -727,12 +729,26 @@ bool combat_move_monster(short which,location destination) { // Looks at all spaces within 2, looking for a spot which is clear of nastiness and beings // returns {0,0} if none found // TODO: NO WAIT IT DOESN'T LOOK AT ALL SPACES!!! -// TODO: THIS MAKES NO ADJUSTMENTS FOR BIG MONSTERS!!! //mode; // 0 - normal 1 - prefer adjacent space -location find_clear_spot(location from_where,short mode) { +location find_clear_spot(location from_where,short mode,short x_width,short y_width) { location loc,store_loc; short num_tries = 0,r1; - + auto is_clear = [from_where](location loc) -> bool { + return !loc_off_act_area(loc) && !is_blocked(loc) + && can_see_light(from_where,loc,combat_obscurity) == 0 + && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) + && (!(is_town()) || (loc != univ.party.town_loc)) + && (!univ.town.is_summon_safe(loc.x, loc.y)); + }; + auto all_is_clear = [from_where, x_width, y_width, is_clear](location loc) -> bool { + for(int x = 0; x < x_width; ++x){ + for(int y = 0; y < y_width; ++y){ + if(!is_clear({loc.x + x, loc.y + y})) + return false; + } + } + return true; + }; while(num_tries < 75) { num_tries++; loc = from_where; @@ -740,11 +756,7 @@ location find_clear_spot(location from_where,short mode) { loc.x = loc.x + r1; r1 = get_ran(1,-2,2); loc.y = loc.y + r1; - if(!loc_off_act_area(loc) && !is_blocked(loc) - && can_see_light(from_where,loc,combat_obscurity) == 0 - && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) - && (!(is_town()) || (loc != univ.party.town_loc)) - && (!univ.town.is_summon_safe(loc.x, loc.y))) { + if(all_is_clear(loc)) { if((mode == 0) || ((mode == 1) && (adjacent(from_where,loc)))) return loc; else store_loc = loc; diff --git a/src/game/boe.monster.hpp b/src/game/boe.monster.hpp index be3fb0656..8b68f8a2f 100644 --- a/src/game/boe.monster.hpp +++ b/src/game/boe.monster.hpp @@ -24,7 +24,7 @@ bool seek_party(short i,location l1,location l2); bool flee_party(short i,location l1,location l2); bool try_move(short i,location start,short x,short y); bool combat_move_monster(short which,location destination); -location find_clear_spot(location from_where,short mode); +location find_clear_spot(location from_where,short mode,short x_width = 1, short y_width = 1); location random_shift(location start); bool outdoor_move_monster(short num,location dest); bool town_move_monster(short num,location dest); From bb343340d8cd6a558c05ee7ecf395f27bfbb90ae Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 13:01:28 -0500 Subject: [PATCH 33/86] more encodings --- rsrc/scenarios/custom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom index a3c512d97..948f4cddc 160000 --- a/rsrc/scenarios/custom +++ b/rsrc/scenarios/custom @@ -1 +1 @@ -Subproject commit a3c512d9762922163e870efe51f3f8e4cf9b8b9b +Subproject commit 948f4cddcf8c55cf45c9aeaa0491cd37a60ff1a8 From 7bba161dd92dcc93cae00cbe5684147c6bc8f55c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 14:44:19 -0500 Subject: [PATCH 34/86] highlight targetable squares with enemy/ally attitude. fix #779 --- src/game/boe.graphics.cpp | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 904ea2fe7..0e2a4954c 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1635,6 +1635,36 @@ void draw_targeting_line() { extern bool targeting_line_visible; if(targeting_line_visible && ((overall_mode == MODE_SPELL_TARGET) || (overall_mode == MODE_FIRING) || (overall_mode == MODE_THROWING) || (overall_mode == MODE_FANCY_TARGET) || ((overall_mode == MODE_TOWN_TARGET) && (current_pat[4][4] != 0)))) { + + // Highlight targetable squares including ally/enemy attitude + for(short q = 0; q < 9; q++) { + for(short r = 0; r < 9; r++) { + which_space = center; + which_space.x += q - 4; + which_space.y += r - 4; + + sf::Color frame_color(0, 0, 0, 0); + if((can_see_light(from_loc,which_space,sight_obscurity) < 5) + && (dist(from_loc,which_space) <= current_spell_range)){ + frame_color = sf::Color(255, 255, 255, 127); + + iLiving* targ = univ.target_there(which_space, TARG_PC); + if(targ != nullptr){ + frame_color = sf::Color(0, 255, 0, 127); + } + targ = univ.target_there(which_space, TARG_MONST); + if(targ != nullptr){ + frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); + } + + target_rect.left = 13 + 28 * q + 19; + target_rect.right = target_rect.left + 28; + target_rect.top = 13 + 36 * r + 7; + target_rect.bottom = target_rect.top + 36; + frame_rect(mainPtr(), target_rect, frame_color); + } + } + } if(mouse_to_terrain_coords(which_space, false)) { int xBound = (short) (from_loc.x - center.x + 4); @@ -1662,7 +1692,8 @@ void draw_targeting_line() { target_rect.right = target_rect.left + 28; target_rect.top = 13 + 36 * j + 7; target_rect.bottom = target_rect.top + 36; - frame_rect(mainPtr(), target_rect, sf::Color::White); + static sf::Color target_color(255, 0, 0, 255); + frame_rect(mainPtr(), target_rect, target_color); target_rect.inset(-5,-5); // Now place number of shots left, if drawing center of target From 4b12053508ad3a22d1fa11b748c0d927f2968068 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 15:24:22 -0500 Subject: [PATCH 35/86] don't highlight invisible monster for targeting --- src/game/boe.graphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 0e2a4954c..759d35792 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1653,7 +1653,7 @@ void draw_targeting_line() { frame_color = sf::Color(0, 255, 0, 127); } targ = univ.target_there(which_space, TARG_MONST); - if(targ != nullptr){ + if(targ != nullptr && !(dynamic_cast(targ))->invisible){ frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); } From 5aada2c7df2108714b3fefd29aefff0df3ce48c4 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 15:42:44 -0500 Subject: [PATCH 36/86] frame friendlies/monsters during combat --- src/game/boe.graphutil.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 1b432dc95..dc5b2b74f 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -199,6 +199,16 @@ void draw_monsters() { sf::Texture& monst_gworld = *ResMgr::graphics.get("monst" + std::to_string(1 + which_sheet)); Draw_Some_Item(monst_gworld, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } + + if(is_combat()){ + // Frame the monster + rectangle target_rect; + target_rect.left = 13 + 28 * where_draw.x; + target_rect.right = target_rect.left + 28; + target_rect.top = 13 + 36 * where_draw.y; + target_rect.bottom = target_rect.top + 36; + frame_rect(terrain_screen_gworld(), target_rect, monst.is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127)); + } } } } From 0cd06c791ded438c5bacdc50af7b7661ee44e368 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 16:04:45 -0500 Subject: [PATCH 37/86] don't show misleading frame in targeting mode --- src/game/boe.graphutil.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index dc5b2b74f..025785a14 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -200,7 +200,8 @@ void draw_monsters() { Draw_Some_Item(monst_gworld, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } - if(is_combat()){ + extern bool targeting_line_visible; + if(is_combat() && !targeting_line_visible){ // Frame the monster rectangle target_rect; target_rect.left = 13 + 28 * where_draw.x; From 761edf0e7ca49c05efd997cbf4b308e38e566229 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 18:12:55 -0500 Subject: [PATCH 38/86] shrink talk text when it doesn't fit --- src/game/boe.dlgutil.cpp | 13 +++++++------ src/game/boe.newgraph.cpp | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index 7bc8615cc..acd06706f 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -664,15 +664,16 @@ std::vector preset_words = { "Ask About...", }; +std::vector preset_word_locs = { + {4, 366}, {70, 366}, {136, 366}, + {4, 389}, {70, 389}, {121, 389}, + {210, 389}, {190, 366}, + {4, 343} +}; + static void reset_talk_words() { // first initialise talk_words here talk_words.clear(); - static const std::vector preset_word_locs = { - {4, 366}, {70, 366}, {136, 366}, - {4, 389}, {70, 389}, {121, 389}, - {210, 389}, {190, 366}, - {4, 343} - }; TextStyle style; style.font = FONT_DUNGEON; style.pointSize = TALK_WORD_SIZE; diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index cd920e4f7..cdafe7d59 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -1021,6 +1021,16 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col } } + // If the text will overflow onto the preset talk words, shrink it + extern std::vector preset_word_locs; + auto break_info = calculate_line_wrapping(word_place_rect, str, style); + short lines = break_info.size(); + short height = lines * (style.lineHeight+1); + if(height >= word_place_rect.height()){ + short overflow = height - preset_word_locs.back().y; + style.lineHeight -= ceil(overflow / (float) lines); + } + std::vector word_rects = draw_string_hilite(talk_gworld(), word_place_rect, str, style, hilites, color ? CUSTOM_WORD_ON : CUSTOM_WORD_OFF); if(!talk_end_forced) { From faae4d473fb510ec7f9d0ba7de77bf03d6d5a7bf Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 18:13:12 -0500 Subject: [PATCH 39/86] update scen archive --- rsrc/scenarios/custom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom index 948f4cddc..1f33aa6bd 160000 --- a/rsrc/scenarios/custom +++ b/rsrc/scenarios/custom @@ -1 +1 @@ -Subproject commit 948f4cddcf8c55cf45c9aeaa0491cd37a60ff1a8 +Subproject commit 1f33aa6bd49486cadcb3aa521d86fe8694179851 From 0f9538c8c8f4958b4778f9c358e6861d5249f0ce Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 21:34:19 -0500 Subject: [PATCH 40/86] make boats and horses exist in legacy scenarios --- src/game/boe.town.cpp | 3 ++- src/scenario/vehicle.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 7f46d89ad..e2001773b 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -491,7 +491,7 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { monst.targ_loc.y = 0; } - // check horses + // check boats for(short i = 0; i < univ.party.boats.size(); i++) { if(univ.scenario.boats[i].which_town >= 0 && univ.scenario.boats[i].loc.x >= 0) { if(!univ.party.boats[i].exists) { @@ -500,6 +500,7 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { } } } + // check horses for(short i = 0; i < univ.party.horses.size(); i++) { if(univ.scenario.horses[i].which_town >= 0 && univ.scenario.horses[i].loc.x >= 0) { if(!univ.party.horses[i].exists) { diff --git a/src/scenario/vehicle.cpp b/src/scenario/vehicle.cpp index 7a2d0c3b5..5716bf3e8 100644 --- a/src/scenario/vehicle.cpp +++ b/src/scenario/vehicle.cpp @@ -25,7 +25,7 @@ cVehicle::cVehicle() : void cVehicle::import_legacy(legacy::horse_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.horse_loc.x; @@ -40,7 +40,7 @@ void cVehicle::import_legacy(legacy::horse_record_type& old){ void cVehicle::import_legacy(legacy::boat_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.boat_loc.x; From 65b503980d6203d6e26078f3e980562f6df7ac4c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 4 Aug 2025 21:41:10 -0500 Subject: [PATCH 41/86] remove redundant boat/horse initialization --- src/game/boe.town.cpp | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index e2001773b..46a0cfbb9 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -491,25 +491,6 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { monst.targ_loc.y = 0; } - // check boats - for(short i = 0; i < univ.party.boats.size(); i++) { - if(univ.scenario.boats[i].which_town >= 0 && univ.scenario.boats[i].loc.x >= 0) { - if(!univ.party.boats[i].exists) { - univ.party.boats[i] = univ.scenario.boats[i]; - univ.party.boats[i].exists = true; - } - } - } - // check horses - for(short i = 0; i < univ.party.horses.size(); i++) { - if(univ.scenario.horses[i].which_town >= 0 && univ.scenario.horses[i].loc.x >= 0) { - if(!univ.party.horses[i].exists) { - univ.party.horses[i] = univ.scenario.horses[i]; - univ.party.horses[i].exists = true; - } - } - } - clear_map(); reset_item_max(); town_force = 200; From df63388da5284378575c4e3254f0957f8e4dc9d1 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Tue, 5 Aug 2025 07:13:33 -0500 Subject: [PATCH 42/86] combat highlighting handle big monsters --- src/game/boe.graphics.cpp | 23 +++++++++++++++++++---- src/game/boe.graphutil.cpp | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 759d35792..c414e200e 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "boe.global.hpp" @@ -1637,13 +1638,17 @@ void draw_targeting_line() { || ((overall_mode == MODE_TOWN_TARGET) && (current_pat[4][4] != 0)))) { // Highlight targetable squares including ally/enemy attitude + std::set big_monst_there; for(short q = 0; q < 9; q++) { for(short r = 0; r < 9; r++) { which_space = center; which_space.x += q - 4; which_space.y += r - 4; + if(big_monst_there.count(which_space)) continue; sf::Color frame_color(0, 0, 0, 0); + int x_width = 1; + int y_width = 1; if((can_see_light(from_loc,which_space,sight_obscurity) < 5) && (dist(from_loc,which_space) <= current_spell_range)){ frame_color = sf::Color(255, 255, 255, 127); @@ -1653,14 +1658,24 @@ void draw_targeting_line() { frame_color = sf::Color(0, 255, 0, 127); } targ = univ.target_there(which_space, TARG_MONST); - if(targ != nullptr && !(dynamic_cast(targ))->invisible){ - frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); + if(targ != nullptr){ + cMonster* monst = dynamic_cast(targ); + if(!monst->invisible){ + frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); + x_width = monst->x_width; + y_width = monst->y_width; + for(int i = 0; i < x_width; ++i){ + for(int j = 0; j < y_width; ++j){ + big_monst_there.insert(loc(which_space.x + i, which_space.y + j)); + } + } + } } target_rect.left = 13 + 28 * q + 19; - target_rect.right = target_rect.left + 28; + target_rect.right = target_rect.left + 28 * x_width; target_rect.top = 13 + 36 * r + 7; - target_rect.bottom = target_rect.top + 36; + target_rect.bottom = target_rect.top + 36 * y_width; frame_rect(mainPtr(), target_rect, frame_color); } } diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 025785a14..96b717c85 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -205,9 +205,9 @@ void draw_monsters() { // Frame the monster rectangle target_rect; target_rect.left = 13 + 28 * where_draw.x; - target_rect.right = target_rect.left + 28; + target_rect.right = target_rect.left + 28 * monst.x_width; target_rect.top = 13 + 36 * where_draw.y; - target_rect.bottom = target_rect.top + 36; + target_rect.bottom = target_rect.top + 36 * monst.y_width; frame_rect(terrain_screen_gworld(), target_rect, monst.is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127)); } } From f2a45bd1fb2acc0470143e19058f264c160f4b21 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Tue, 5 Aug 2025 10:18:57 -0500 Subject: [PATCH 43/86] better handling for out-of-sight ranged/boom anim --- src/game/boe.newgraph.cpp | 40 +++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index cdafe7d59..2f029713e 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -127,10 +127,7 @@ terrain_screen_rects_t terrain_screen_rects() { to.offset(current_terrain_ul); rectangle in_frame = to; - in_frame.top += 11; - in_frame.left += 11; - in_frame.bottom -= 11; - in_frame.right -= 11; + in_frame.inset(14, 14); return {from, to, in_frame}; } @@ -360,10 +357,20 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { return; } - // Eliminate missiles traveling 0 distance + bool can_see_origin = party_can_see(missile_origin) < 6; + bool missile_flying = false; for(short i = 0; i < 30; i++) { - if((store_missiles[i].missile_type >= 0) && (missile_origin == store_missiles[i].dest)) - store_missiles[i].missile_type = -1; + if((store_missiles[i].missile_type >= 0)){ + // Eliminate missiles traveling 0 distance + if(missile_origin == store_missiles[i].dest){ + store_missiles[i].missile_type = -1; + } + // Eliminate missiles whose whole arc is out of view + if(!can_see_origin && party_can_see(store_missiles[i].dest) == 6){ + store_missiles[i].missile_type = -1; + missile_flying = true; + } + } } std::vector missile_targets; @@ -376,7 +383,11 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { missile_targets.push_back(store_missiles[i].dest); } - if(missile_targets.empty()) return; + if(missile_targets.empty()){ + if(missile_flying) + play_sound(-1 * sound_num); + return; + } if(missile_targets.size() == 1){ tracking_missile = missile_indices[0]; @@ -387,9 +398,14 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { tracking_missile = missile_indices[closest_point_idx(missile_targets, camera_dest)]; } - // Start the camera as close as possible to containing the origin and the camera destination - // on the same screen - center = between_anchor_points(missile_origin, camera_dest); + if(can_see_origin){ + // Start the camera as close as possible to containing the origin and the camera destination + // on the same screen + center = between_anchor_points(missile_origin, camera_dest); + }else{ + // Can't see the origin, so showing the whole flight path when we don't have to is kind of a spoiler. + center = camera_dest; + } // make terrain_template contain current terrain all nicely draw_terrain(1); @@ -576,7 +592,7 @@ void do_explosion_anim(short /*sound_num*/,short special_draw, short snd) { draw_terrain(1); if(special_draw != 2) { auto ter_rects = terrain_screen_rects(); - rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to); + rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to, ter_rects.in_frame); } TextStyle style; From c703c8544b1f42b6dcf559bb2c96abdbb04a9280 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Tue, 5 Aug 2025 10:55:02 -0500 Subject: [PATCH 44/86] shortcut to get all gold in reach --- rsrc/dialogs/get-items.xml | 2 ++ src/game/boe.items.cpp | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/rsrc/dialogs/get-items.xml b/rsrc/dialogs/get-items.xml index 53c053514..c5c26d2c1 100644 --- a/rsrc/dialogs/get-items.xml +++ b/rsrc/dialogs/get-items.xml @@ -4,6 +4,8 @@ + Get all gold diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index 4e9cc41c5..049ee77e3 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -395,6 +395,16 @@ static void put_item_graphics(cDialog& me, size_t& first_item_shown, short& curr me["down"].hide(); else me["down"].show(); + me["gold"].hide(); + me["gold-label"].hide(); + for(cItem* item : item_array){ + if(item->variety == eItemType::GOLD){ + me["gold"].show(); + me["gold-label"].show(); + break; + } + } + for(short i = 0; i < ITEMS_IN_WINDOW; i++) { std::ostringstream sout; sout << "item" << i + 1; @@ -460,7 +470,22 @@ static bool display_item_event_filter(cDialog& me, std::string id, size_t& first first_item_shown += ITEMS_IN_WINDOW; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } - } else if(id.substr(0,2) == "pc") { + } else if(id == "gold"){ + // Get all gold + for(int i = item_array.size() - 1; i >= 0; --i){ + item = *item_array[i]; + if(item.variety != eItemType::GOLD) continue; + if(item.item_level > 3000) + item.item_level = 3000; + set_item_flag(&item); + give_gold(item.item_level,false); + *item_array[i] = cItem(); + item_array.erase(item_array.begin() + i); + } + play_sound(39); // formerly force_play_sound + put_item_graphics(me, first_item_shown, current_getting_pc, item_array); + } + else if(id.substr(0,2) == "pc") { current_getting_pc = id[2] - '1'; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } else { @@ -561,7 +586,7 @@ bool show_get_items(std::string titleText, std::vector& itemRefs, short cDialog itemDialog(*ResMgr::dialogs.get("get-items")); auto handler = std::bind(display_item_event_filter, _1, _2, std::ref(first_item), std::ref(pc_getting), std::ref(itemRefs), overload); - itemDialog.attachClickHandlers(handler, {"done", "up", "down"}); + itemDialog.attachClickHandlers(handler, {"done", "up", "down", "gold"}); itemDialog.attachClickHandlers(handler, {"pc1", "pc2", "pc3", "pc4", "pc5", "pc6"}); itemDialog.setResult(false); cTextMsg& title = dynamic_cast(itemDialog["title"]); From fc8259533fe40181e660e50bff89f7e998009f9a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 10:26:21 -0500 Subject: [PATCH 45/86] fix party disappearing in huge town --- src/game/boe.graphutil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 96b717c85..99d89443f 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -459,7 +459,7 @@ void draw_party_symbol(location center) { return; if(!univ.party.is_alive()) return; - if((is_town()) && (univ.party.town_loc.x > 70)) + if((is_town()) && (univ.party.town_loc.x > (univ.scenario.is_legacy ? 70 : LOC_UNUSED))) return; if(is_town() && center != univ.party.town_loc) { From 4fb08daae5945c60de47d4690b400dbffba7ddb3 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 11:41:02 -0500 Subject: [PATCH 46/86] Refactor minimap rendering and fix tiny town crash --- src/game/boe.items.cpp | 4 +- src/game/boe.town.cpp | 142 ++++++++++++++++++++++++----------------- src/game/boe.town.hpp | 1 + 3 files changed, 87 insertions(+), 60 deletions(-) diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index 049ee77e3..6ffaba6a2 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -683,6 +683,8 @@ short get_num_of_items(short max_num) { return minmax(0,max_num,numPanel.getResult()); } +// extern declaration doesn't work here?? +const short view_max_dim = 40; void init_mini_map() { double map_scale = get_ui_scale_map(); if (map_scale < 0.1) map_scale = 1.0; @@ -702,7 +704,7 @@ void init_mini_map() { makeFrontWindow(mainPtr(), mini_map()); // Create and initialize map gworld - if(!map_gworld().create(384, 384)) { + if(!map_gworld().create(view_max_dim * 6, view_max_dim * 6)) { play_sound(2); throw std::string("Failed to initialized automap!"); } else { diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 46a0cfbb9..acc15dab6 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1283,75 +1283,57 @@ void clear_map() { draw_map(true); } +const short view_max_dim = 40; +rectangle minimap_view_rect() { + rectangle view_rect; + short total_size, party_loc_x, party_loc_y; + + if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || + ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || + ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS))) { + total_size = 48; + party_loc_x = univ.party.loc_in_sec.x; + party_loc_y = univ.party.loc_in_sec.y; + } + else { + total_size = univ.town->max_dim; + party_loc_x = univ.party.town_loc.x; + party_loc_y = univ.party.town_loc.y; + } + if(total_size <= view_max_dim){ + view_rect = {0, 0, total_size, total_size}; + }else{ + short scroll_max = total_size - view_max_dim; + view_rect.left = minmax(0, scroll_max, party_loc_x - view_max_dim / 2); + view_rect.top = minmax(0, scroll_max, party_loc_y - view_max_dim / 2); + view_rect.right = view_rect.left + view_max_dim; + view_rect.bottom = view_rect.top + view_max_dim; + } + return view_rect; +} + void draw_map(bool need_refresh) { if(!map_visible) return; pic_num_t pic; - rectangle the_rect,map_world_rect = {0,0,384,384}; + rectangle the_rect; + + rectangle map_world_rect(map_gworld()); location where; location kludge; rectangle draw_rect,orig_draw_rect = {0,0,6,6},ter_temp_from,base_source_rect = {0,0,12,12}; rectangle dlogpicrect = {6,6,42,42}; bool draw_pcs = true,out_mode; - rectangle view_rect= {0,0,48,48},tiny_rect = {0,0,32,32}, - redraw_rect = {0,0,48,48},big_rect = {0,0,64,64}; // Rectangle visible in view screen - rectangle area_to_draw_from,area_to_draw_on = {29,47,269,287}; + rectangle area_to_draw_on = {29,47,269,287}; ter_num_t what_ter; bool expl; - short total_size = 48; // if full redraw, use this to figure out everything rectangle custom_from; town_map_adj.x = 0; town_map_adj.y = 0; - // view rect is rect that is visible, redraw rect is area to redraw now - // area_to_draw_from is final draw from rect - // area_to_draw_on is final draw to rect - // extern short store_pre_shop_mode,store_pre_talk_mode; - if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || - ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || - ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS))) { - view_rect.left = minmax(0,8,univ.party.loc_in_sec.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,8,univ.party.loc_in_sec.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = view_rect; - } - else { - total_size = univ.town->max_dim; - switch(total_size) { - case 64: - view_rect.left = minmax(0,24,univ.party.town_loc.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,24,univ.party.town_loc.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = big_rect; - break; - case 48: - view_rect.left = minmax(0,8,univ.party.town_loc.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,8,univ.party.town_loc.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = view_rect; - break; - case 32: - view_rect = tiny_rect; - redraw_rect = view_rect; - break; - } - } - if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || - ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || - ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS)) || - is_town() || is_combat()) { - area_to_draw_from = view_rect; - area_to_draw_from.width() = 40; - area_to_draw_from.height() = 40; - area_to_draw_from.left *= 6; - area_to_draw_from.right *= 6; - area_to_draw_from.top *= 6; - area_to_draw_from.bottom *= 6; - } + // We use view_rect as a camera to show only as much of the map as can fit in the window + rectangle view_rect = minimap_view_rect(); if(is_combat()) draw_pcs = false; @@ -1376,7 +1358,7 @@ void draw_map(bool need_refresh) { // Now, if shopping or talking, just don't touch anything. if((overall_mode == MODE_SHOPPING) || (overall_mode == MODE_TALKING)) - redraw_rect.right = -1; + view_rect.right = -1; // Otherwise, clear to black first: else fill_rect(map_gworld(), map_world_rect, sf::Color::Black); @@ -1388,12 +1370,11 @@ void draw_map(bool need_refresh) { out_mode = true; else out_mode = false; - // TODO: It could be possible to draw the entire map here and then only refresh if a spot actually changes terrain type sf::Texture& small_ter_gworld = *ResMgr::graphics.get("termap"); - for(where.x = redraw_rect.left; where.x < redraw_rect.right; where.x++) - for(where.y = redraw_rect.top; where.y < redraw_rect.bottom; where.y++) { + for(where.x = view_rect.left; where.x < view_rect.right; where.x++) + for(where.y = view_rect.top; where.y < view_rect.bottom; where.y++) { draw_rect = orig_draw_rect; - draw_rect.offset(6 * where.x, 6 * where.y); + draw_rect.offset(6 * (where.x - view_rect.left), 6 * (where.y - view_rect.top)); if(out_mode) what_ter = univ.out[where.x + 48 * univ.party.i_w_c.x][where.y + 48 * univ.party.i_w_c.y]; @@ -1405,6 +1386,7 @@ void draw_map(bool need_refresh) { expl = univ.out.out_e[where.x + 48 * univ.party.i_w_c.x][where.y + 48 * univ.party.i_w_c.y]; else expl = is_explored(where.x,where.y); + // Draw tile if(expl != 0) { pic = univ.scenario.ter_types[what_ter].map_pic; bool drawLargeIcon = false; @@ -1453,6 +1435,48 @@ void draw_map(bool need_refresh) { draw_rect.inset(1,1); rect_draw_some_item(*ResMgr::graphics.get("trim"),{8,112,12,116},map_gworld(),draw_rect); } + + // Draw discovered edges of rooms/areas + const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; + location a; + location b; + for(info_rect_t area : area_desc){ + location adjusted_where = where; + adjusted_where.x -= view_rect.left; + adjusted_where.y -= view_rect.top; + if(where.y == area.top && where.x >= area.left && where.x <= area.right){ + a = orig_draw_rect.topLeft(); + a.x += 6 * adjusted_where.x; + a.y += 6 * adjusted_where.y; + b = a; + b.x += 6; + draw_line(map_gworld(), a, b, 1, Colours::RED); + } + if(where.x == area.left && where.y >= area.top && where.y <= area.bottom){ + a = orig_draw_rect.topLeft(); + a.x += 6 * adjusted_where.x; + a.y += 6 * adjusted_where.y; + b = a; + b.y += 6; + draw_line(map_gworld(), a, b, 1, Colours::RED); + } + if(where.y == area.bottom && where.x >= area.left && where.x <= area.right){ + a = orig_draw_rect.topLeft(); + a.x += 6 * adjusted_where.x; + a.y += 6 * (adjusted_where.y + 1) - 1; + b = a; + b.x += 6; + draw_line(map_gworld(), a, b, 1, Colours::RED); + } + if(where.x == area.right && where.y >= area.top && where.y <= area.bottom){ + a = orig_draw_rect.topLeft(); + a.x += 6 * (adjusted_where.x + 1) - 1; + a.y += 6 * adjusted_where.y; + b = a; + b.y += 6; + draw_line(map_gworld(), a, b, 1, Colours::RED); + } + } } } @@ -1483,7 +1507,7 @@ void draw_map(bool need_refresh) { win_draw_string(mini_map(), map_bar_rect,"(Hit Escape to close.)",eTextMode::WRAP,style); if(canMap) { - rect_draw_some_item(map_gworld().getTexture(),area_to_draw_from,mini_map(),area_to_draw_on); + rect_draw_some_item(map_gworld().getTexture(),the_rect,mini_map(),area_to_draw_on); // Now place PCs and monsters if(draw_pcs) { diff --git a/src/game/boe.town.hpp b/src/game/boe.town.hpp index 9467ad2c7..8bb6429c6 100644 --- a/src/game/boe.town.hpp +++ b/src/game/boe.town.hpp @@ -28,6 +28,7 @@ void erase_completed_specials(cArea& sector, std::function clear void erase_out_specials(); bool does_location_have_special(cOutdoors& sector, location loc, eTerSpec type); void clear_map(); +rectangle minimap_view_rect(); void draw_map(bool need_refresh); bool is_door(location destination); void display_map(); From b64f5fc814a8270782c411235259acec3ea84822 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 12:21:39 -0500 Subject: [PATCH 47/86] messy map tooltip demo --- src/game/boe.items.cpp | 8 +++++--- src/game/boe.main.cpp | 24 +++++++++++++++++++++++- src/game/boe.town.cpp | 5 ++++- src/game/boe.town.hpp | 2 +- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index 6ffaba6a2..bb7a75337 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -41,7 +41,7 @@ extern short d_rect_index[80]; extern bool map_visible; extern cUniverse univ; -extern void draw_map(bool need_refresh); +extern void draw_map(bool need_refresh, std::string tooltip_text = ""); short selected; @@ -685,17 +685,19 @@ short get_num_of_items(short max_num) { // extern declaration doesn't work here?? const short view_max_dim = 40; +const short map_window_width = 296; +const short map_window_height = 307; void init_mini_map() { double map_scale = get_ui_scale_map(); if (map_scale < 0.1) map_scale = 1.0; if (mini_map().isOpen()) mini_map().close(); - mini_map().create(sf::VideoMode(map_scale*296,map_scale*277), "Map", sf::Style::Titlebar | sf::Style::Close); + mini_map().create(sf::VideoMode(map_scale*map_window_width,map_scale*map_window_height), "Map", sf::Style::Titlebar | sf::Style::Close); // TODO why is 52,62 the default position, anyway? int map_x = get_int_pref("MapWindowX", 52); int map_y = get_int_pref("MapWindowY", 62); mini_map().setPosition(sf::Vector2i(map_x,map_y)); sf::View view; - view.reset(sf::FloatRect(0, 0, map_scale*296,map_scale*277)); + view.reset(sf::FloatRect(0, 0, map_scale*map_window_width,map_scale*map_window_height)); view.setViewport(sf::FloatRect(0, 0, map_scale, map_scale)); mini_map().setView(view); mini_map().setVisible(false); diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 9eb4aa3f8..5faaa4db8 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -92,6 +92,8 @@ boost::optional scen_arg_out_sec, scen_arg_loc; extern std::string last_load_file; std::string help_text_rsrc = "help"; +std::string last_tooltip_text = ""; + /* // Example feature flags: { @@ -1444,6 +1446,26 @@ void handle_one_minimap_event(const sf::Event& event) { map_window_lost_focus = true; }else if(event.type == sf::Event::MouseMoved){ check_window_moved(mini_map(), last_map_x, last_map_y, "MapWindow"); + + // Check if something interesting is hovered + location tile = minimap_view_rect().topLeft(); + int x = event.mouseMove.x; + int y = event.mouseMove.y; + tile.x += (x - (47 * get_ui_scale_map())) / get_ui_scale_map() / 6; + tile.y += (y - (29 * get_ui_scale_map())) / get_ui_scale_map() / 6; + + std::string tooltip_text = ""; + if(is_explored(tile.x, tile.y)){ + const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; + for(info_rect_t area : area_desc){ + if(area.contains(tile)){ + tooltip_text += area.descr + " |"; + } + } + } + if(tooltip_text != last_tooltip_text) + draw_map(false, tooltip_text); + last_tooltip_text = tooltip_text; }else if(event.type == sf::Event::KeyPressed){ switch(event.key.code){ case sf::Keyboard::Escape: @@ -1491,7 +1513,7 @@ void update_everything() { void redraw_everything() { redraw_screen(REFRESH_ALL); - if(map_visible) draw_map(false); + if(map_visible) draw_map(false, last_tooltip_text); } void Mouse_Pressed(const sf::Event& event, cFramerateLimiter& fps_limiter) { diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index acc15dab6..b3860ba38 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -62,6 +62,7 @@ location town_force_loc; bool shop_button_active[12]; rectangle map_title_rect = {3,50,15,300}; rectangle map_bar_rect = {15,50,27,300}; +rectangle map_tooltip_rect = {270,50,307,350}; void force_town_enter(short which_town,location where_start) { town_force = which_town; @@ -1312,7 +1313,7 @@ rectangle minimap_view_rect() { return view_rect; } -void draw_map(bool need_refresh) { +void draw_map(bool need_refresh, std::string tooltip_text) { if(!map_visible) return; pic_num_t pic; rectangle the_rect; @@ -1542,6 +1543,8 @@ void draw_map(bool need_refresh) { } } + win_draw_string(mini_map(), map_tooltip_rect,tooltip_text,eTextMode::WRAP,style); + mini_map().setActive(false); mini_map().display(); diff --git a/src/game/boe.town.hpp b/src/game/boe.town.hpp index 8bb6429c6..5916f7be1 100644 --- a/src/game/boe.town.hpp +++ b/src/game/boe.town.hpp @@ -29,7 +29,7 @@ void erase_out_specials(); bool does_location_have_special(cOutdoors& sector, location loc, eTerSpec type); void clear_map(); rectangle minimap_view_rect(); -void draw_map(bool need_refresh); +void draw_map(bool need_refresh, std::string tooltip_text = ""); bool is_door(location destination); void display_map(); void check_done(); From a5cc323f00126324ed9e625ff9c90fbb7d2c7539 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 13:45:40 -0500 Subject: [PATCH 48/86] draw and give tooltips for more map features --- src/game/boe.locutils.cpp | 12 ++++++++ src/game/boe.locutils.hpp | 3 ++ src/game/boe.main.cpp | 31 ++++++++++++++++++++ src/game/boe.town.cpp | 59 ++++++++++++++++++++++++++------------- src/gfx/render_shapes.hpp | 1 + 5 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index 061a90154..f01f4348c 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -600,3 +600,15 @@ void alter_space(short i,short j,ter_num_t ter) { univ.town->set_up_lights(); } } + +bool vehicle_is_here(const cVehicle& vehicle) { + if(!vehicle.exists) return false; + if(is_out()){ + location party_sector = univ.party.outdoor_corner; + party_sector.x += univ.party.i_w_c.x; + party_sector.y += univ.party.i_w_c.y; + return vehicle.which_town == 200 && vehicle.sector == party_sector; + }else{ + return vehicle.which_town == univ.party.town_num; + } +} \ No newline at end of file diff --git a/src/game/boe.locutils.hpp b/src/game/boe.locutils.hpp index ecf6e0c5d..01f147137 100644 --- a/src/game/boe.locutils.hpp +++ b/src/game/boe.locutils.hpp @@ -1,6 +1,7 @@ #include #include "location.hpp" +#include "vehicle.hpp" bool is_explored(short i,short j); void make_explored(short i,short j); @@ -43,3 +44,5 @@ location push_loc(location from_where,location to_where); bool spot_impassable(short i,short j); void swap_ter(short i,short j,ter_num_t ter1,ter_num_t ter2); void alter_space(short i,short j,ter_num_t ter); +// Whether a vehicle is in the current town/outdoor section shown on the map +bool vehicle_is_here(const cVehicle& vehicle); \ No newline at end of file diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 5faaa4db8..28d59b796 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1456,12 +1456,43 @@ void handle_one_minimap_event(const sf::Event& event) { std::string tooltip_text = ""; if(is_explored(tile.x, tile.y)){ + // Area rectangle hovered const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; for(info_rect_t area : area_desc){ if(area.contains(tile)){ tooltip_text += area.descr + " |"; } } + + // Sign hovered + const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; + for(sign_loc_t sign : sign_locs){ + if(sign == tile){ + tooltip_text += "Sign: " + sign.text + " |"; + } + } + // Town entrance hovered + if(is_out()){ + const std::vector& city_locs = univ.out->city_locs; + for(spec_loc_t city : city_locs){ + if(city == tile){ + tooltip_text += univ.scenario.towns[city.spec]->name + " |"; + } + } + } + // Vehicle hovered + for(auto& boat : univ.party.boats) { + if(!vehicle_is_here(boat)) continue; + if(boat.loc == tile){ + tooltip_text += (boat.property ? "Boat (Not Yours)" : "Your Boat"); + } + } + for(auto& horse : univ.party.horses) { + if(!vehicle_is_here(horse)) continue; + if(horse.loc == tile){ + tooltip_text += (horse.property ? "Horses (Not Yours)" : "Your Horses"); + } + } } if(tooltip_text != last_tooltip_text) draw_map(false, tooltip_text); diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index b3860ba38..92af62f44 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -62,7 +62,7 @@ location town_force_loc; bool shop_button_active[12]; rectangle map_title_rect = {3,50,15,300}; rectangle map_bar_rect = {15,50,27,300}; -rectangle map_tooltip_rect = {270,50,307,350}; +rectangle map_tooltip_rect = {270,50,307,300}; void force_town_enter(short which_town,location where_start) { town_force = which_town; @@ -1510,37 +1510,56 @@ void draw_map(bool need_refresh, std::string tooltip_text) { if(canMap) { rect_draw_some_item(map_gworld().getTexture(),the_rect,mini_map(),area_to_draw_on); + auto mark_loc = [view_rect, &draw_rect, area_to_draw_on](location where, sf::Color inner, sf::Color outer) -> void { + if((is_explored(where.x,where.y)) && + ((where.x >= view_rect.left) && (where.x < view_rect.right) + && where.y >= view_rect.top && where.y < view_rect.bottom)){ + draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); + draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); + draw_rect.right = draw_rect.left + 6; + draw_rect.bottom = draw_rect.top + 6; + + fill_rect(mini_map(), draw_rect, inner); + frame_circle(mini_map(), draw_rect, outer); + } + }; + // Now place PCs and monsters if(draw_pcs) { if((is_town()) && (univ.party.status[ePartyStatus::DETECT_LIFE] > 0)) for(short i = 0; i < univ.town.monst.size(); i++) if(univ.town.monst[i].is_alive()) { where = univ.town.monst[i].cur_loc; - if((is_explored(where.x,where.y)) && - ((where.x >= view_rect.left) && (where.x < view_rect.right) - && where.y >= view_rect.top && where.y < view_rect.bottom)){ - - draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); - draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); - draw_rect.right = draw_rect.left + 6; - draw_rect.bottom = draw_rect.top + 6; - - fill_rect(mini_map(), draw_rect, Colours::GREEN); - frame_circle(mini_map(), draw_rect, Colours::BLUE); - } + mark_loc(where, Colours::GREEN, Colours::BLUE); } if((overall_mode != MODE_SHOPPING) && (overall_mode != MODE_TALKING)) { where = (is_town()) ? univ.party.town_loc : global_to_local(univ.party.out_loc); - draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); - draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); - draw_rect.right = draw_rect.left + 6; - draw_rect.bottom = draw_rect.top + 6; - fill_rect(mini_map(), draw_rect, Colours::RED); - frame_circle(mini_map(), draw_rect, sf::Color::Black); - + mark_loc(where, Colours::RED, Colours::BLACK); } } + + // Draw signs + const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; + for(sign_loc_t sign : sign_locs){ + mark_loc(sign, Colours::TRANSPARENT, Colours::YELLOW); + } + // Draw town entrances + if(is_out()){ + const std::vector& city_locs = univ.out->city_locs; + for(spec_loc_t city : city_locs){ + mark_loc(city, Colours::TRANSPARENT, Colours::GREEN); + } + } + // Draw vehicles + for(auto& boat : univ.party.boats) { + if(!vehicle_is_here(boat)) continue; + mark_loc(boat.loc, Colours::MAROON, Colours::BLACK); + } + for(auto& horse : univ.party.horses) { + if(!vehicle_is_here(horse)) continue; + mark_loc(horse.loc, Colours::MAROON, Colours::BLACK); + } } win_draw_string(mini_map(), map_tooltip_rect,tooltip_text,eTextMode::WRAP,style); diff --git a/src/gfx/render_shapes.hpp b/src/gfx/render_shapes.hpp index d56610aed..3ae03c845 100644 --- a/src/gfx/render_shapes.hpp +++ b/src/gfx/render_shapes.hpp @@ -58,6 +58,7 @@ namespace Colours { const sf::Color YELLOW { 0xff, 0xff, 0x31}; const sf::Color ORANGE { 0xff, 0x80, 0x00}; const sf::Color LIGHT_BLUE { 0xad, 0xd8, 0xe6 }; // Spell points on dark background + const sf::Color TRANSPARENT { 0x00, 0x00, 0x00, 0x00 }; // Text colours for shopping / talking // TODO: The Windows version appears to use completely different colours? const sf::Color SHADOW { 0x00, 0x00, 0x68}; // formerly c[3] QD colour = {0,0,26623} (shop/character name shadow, shop subtitle) From 0d3b939f616a684fda11087ebe72b93b6941ca0a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 14:14:22 -0500 Subject: [PATCH 49/86] fix place/edit special node tool --- src/scenedit/scen.actions.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scenedit/scen.actions.cpp b/src/scenedit/scen.actions.cpp index 91e96bd71..afe429652 100644 --- a/src/scenedit/scen.actions.cpp +++ b/src/scenedit/scen.actions.cpp @@ -2888,6 +2888,7 @@ void place_edit_special(location loc) { bool is_new = false; if(i == specials.size()){ specials.emplace_back(-1,-1,-1); + get_current_area()->specials.emplace_back(); is_new = true; } if(specials[i].spec < 0) { From 218caf1aa2c29bea7e52599e52a4bda25b8fa092 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 14:14:39 -0500 Subject: [PATCH 50/86] record 2-string dialogs in one element --- src/game/boe.infodlg.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/game/boe.infodlg.cpp b/src/game/boe.infodlg.cpp index 059bd0cdd..d1a9d69bc 100644 --- a/src/game/boe.infodlg.cpp +++ b/src/game/boe.infodlg.cpp @@ -707,12 +707,12 @@ void cStringRecorder::operator()(cDialog& me) { play_sound(0); std::string str1, str2; univ.get_strs(str1, str2, spec_type, label1, label2); - if(univ.party.record(note_type, str1, location)){ + std::string combined = str1; + if(!str2.empty()) + combined += " ||" + str2; + if(univ.party.record(note_type, combined, location)){ give_help(58,0,me); ASB("Added to encounter notes."); } - - if(!str2.empty()) - univ.party.record(note_type, str2, location); } From d17715f71feaacb9fb11ec6e790d689d81ed2512 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 15:17:56 -0500 Subject: [PATCH 51/86] combine 2-string dialog records into 1 element --- src/game/boe.infodlg.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/boe.infodlg.cpp b/src/game/boe.infodlg.cpp index d1a9d69bc..3acd03a15 100644 --- a/src/game/boe.infodlg.cpp +++ b/src/game/boe.infodlg.cpp @@ -707,10 +707,10 @@ void cStringRecorder::operator()(cDialog& me) { play_sound(0); std::string str1, str2; univ.get_strs(str1, str2, spec_type, label1, label2); - std::string combined = str1; + // Combine str1 and str2 in one journal entry: if(!str2.empty()) - combined += " ||" + str2; - if(univ.party.record(note_type, combined, location)){ + str1 += " ||" + str2; + if(univ.party.record(note_type, str1, location)){ give_help(58,0,me); ASB("Added to encounter notes."); } From 23106c6954e7bc6aba42f48cf31e3a5f6e14a514 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 15:29:01 -0500 Subject: [PATCH 52/86] very messy support for recording 6-string dialog --- rsrc/dialogs/adventure-notes.xml | 18 ++++++---- src/dialogxml/dialogs/3choice.cpp | 51 ++++++++++++++++++++++++++-- src/dialogxml/dialogs/3choice.hpp | 17 +++++++++- src/dialogxml/dialogs/choicedlog.hpp | 2 +- src/game/boe.infodlg.cpp | 6 ++++ src/game/boe.specials.cpp | 10 +++--- 6 files changed, 89 insertions(+), 15 deletions(-) diff --git a/rsrc/dialogs/adventure-notes.xml b/rsrc/dialogs/adventure-notes.xml index 16a0800a4..2dea6c55a 100644 --- a/rsrc/dialogs/adventure-notes.xml +++ b/rsrc/dialogs/adventure-notes.xml @@ -4,16 +4,22 @@ - - + + + diff --git a/src/dialogxml/dialogs/3choice.cpp b/src/dialogxml/dialogs/3choice.cpp index fe45e1dfd..7ca629502 100644 --- a/src/dialogxml/dialogs/3choice.cpp +++ b/src/dialogxml/dialogs/3choice.cpp @@ -155,6 +155,13 @@ void cThreeChoice::init_buttons(cBasicButtonType btn1, cBasicButtonType btn2, cB me->add(btn, cur_btn_rect, sout.str()); cur_btn_rect.right = cur_btn_rect.left - 4; } + // Add a record button and hide it + cButton* record = new cButton(*me); + record->setText("Record"); + record->attachClickHandler(std::bind(&cThreeChoice::onRecord, this, _2)); + cur_btn_rect = {buttons_top,10,buttons_top + 23,buttons_right}; + me->add(record, cur_btn_rect, "record"); + record->hide(); } void cThreeChoice::init_pict(pic_num_t pic){ @@ -174,7 +181,7 @@ std::string cThreeChoice::show(){ return "**ERROR**"; // shouldn't be reached } -short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons,bool anim_pict,short anim_loops, int anim_fps, cDialog* parent) { +short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons,bool anim_pict,short anim_loops, int anim_fps, cUniverse* univ, cDialog* parent) { set_cursor(sword_curs); std::vector vec(strs.begin(), strs.end()); @@ -185,6 +192,28 @@ short custom_choice_dialog(std::array& strs,short pic_num,ePicTy if(anim_pict) setup_dialog_pict_anim(*(customDialog.operator->()), "pict", anim_loops, anim_fps); + if(univ != nullptr){ + customDialog.setRecordHandler([vec, univ](cDialog& dlg) -> void { + std::string combined; + for(std::string msg : vec){ + if(!msg.empty()){ + if(!combined.empty()){ + combined += " ||"; + } + combined += msg; + } + } + // It's not necessarily a NOTE_SCEN and location shouldn't be empty, + // but these things aren't actually shown to the party in the encounter notes dialog. + if(univ->party.record(NOTE_SCEN, combined, "")){ + give_help(58,0,dlg); + // TODO ASB is only in game source, but this function is in common source so the scenario editor can preview dialogs :( + /* + add_string_to_buf("Added to encounter notes."); + */ + } + }); + } std::string item_hit = customDialog.show(); for(int i = 0; i < 3; i++) { @@ -220,5 +249,23 @@ short once_dialog(cUniverse& univ, cSpecial& spec, eSpecCtxType cur_type, cDialo showError("Dialog box ended up with no buttons."); return -1; } - return custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, parent); + return custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ, parent); +} + +cThreeChoice& cThreeChoice::setRecordHandler(record_callback_t rec){ + if(rec == nullptr){ + hasRecord = false; + dlg["record"].hide(); + }else{ + hasRecord = true; + rec_f = rec; + dlg["record"].show(); + } + return *this; +} + +bool cThreeChoice::onRecord(std::string id){ + if(hasRecord) rec_f(dlg); + else dlg[id].hide(); + return hasRecord; } \ No newline at end of file diff --git a/src/dialogxml/dialogs/3choice.hpp b/src/dialogxml/dialogs/3choice.hpp index 281222be0..612b8c7dd 100644 --- a/src/dialogxml/dialogs/3choice.hpp +++ b/src/dialogxml/dialogs/3choice.hpp @@ -37,6 +37,9 @@ namespace {cBasicButtonType null_btn = boost::none;} extern bbtt basic_buttons[71]; #endif +/// The signature of a record handler for cThreeChoice. +typedef std::function record_callback_t; + /// A choice dialog with several strings and up to three buttons. /// This is the class used for dialogs generated by special nodes. /// It generates the dialog dynamically from the given input. @@ -48,6 +51,9 @@ class cThreeChoice : public cChoiceDlog { void init_buttons(cBasicButtonType btn1, cBasicButtonType btn2, cBasicButtonType btn3); void init_pict(pic_num_t pic); const ePicType type; + record_callback_t rec_f; + bool hasRecord; + bool onRecord(std::string id); public: /// Create a dialog with just one button. /// @param strings A list of the strings to place in the dialog. @@ -70,12 +76,21 @@ class cThreeChoice : public cChoiceDlog { /// @param t The type of the icon. /// @param parent Optionally, a parent dialog. cThreeChoice(std::vector& strings, std::array& buttons, pic_num_t pic, ePicType t, cDialog* parent = nullptr); + /// Set a record handler. + /// @param rec The handler. + /// @return This object, for method-call chaining. + /// @note Only one record handler can be set at a time. To remove it, set it to null. + /// @note The presence of the Record button is determined entirely by the presence of a record handler. + /// + /// A record handler should take one parameter, which is a reference to the dialog. + /// (That's the cDialog, not the cThreeChoice.) It should return void. + cThreeChoice& setRecordHandler(record_callback_t rec); /// @copydoc cChoiceDlog::show() /// @note The unique key in this case is the label specified in the button specification. std::string show(); }; -short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons, bool anim_pict = false, short anim_loops = -1, int anim_fps = -1, cDialog* parent = nullptr); +short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons, bool anim_pict = false, short anim_loops = -1, int anim_fps = -1, cUniverse* univ = nullptr, cDialog* parent = nullptr); short once_dialog(cUniverse& univ, cSpecial& spec, eSpecCtxType cur_type, cDialog* parent = nullptr); #endif diff --git a/src/dialogxml/dialogs/choicedlog.hpp b/src/dialogxml/dialogs/choicedlog.hpp index 318cca912..37e60c424 100644 --- a/src/dialogxml/dialogs/choicedlog.hpp +++ b/src/dialogxml/dialogs/choicedlog.hpp @@ -17,8 +17,8 @@ /// This class loads a definition from a file, so there can be any amount of other stuff in the dialog, /// and the buttons could be arranged in any fashion you want. class cChoiceDlog { - cDialog dlg; protected: + cDialog dlg; /// The click handler for the dialog's buttons. /// @param me A reference to the current dialog. /// @param id The unique key of the clicked control. diff --git a/src/game/boe.infodlg.cpp b/src/game/boe.infodlg.cpp index 3acd03a15..008bf7402 100644 --- a/src/game/boe.infodlg.cpp +++ b/src/game/boe.infodlg.cpp @@ -512,10 +512,14 @@ static bool adventure_notes_event_filter(cDialog& me, std::string item_hit, eKey std::string n = boost::lexical_cast(i + 1); if(univ.party.special_notes.size() > store_page_on * 3+i) { me["str" + n].setText(univ.party.special_notes[store_page_on * 3+i].the_str); + me["str" + n].recalcRect(); + me["pane" + n].recalcRect(); me["del" + n].show(); } else { me["str" + n].setText(""); + me["str" + n].recalcRect(); + me["pane" + n].recalcRect(); me["del" + n].hide(); } } @@ -544,6 +548,8 @@ void adventure_notes() { std::string n = boost::lexical_cast(i + 1); if(univ.party.special_notes.size() > i) { encNotes["str" + n].setText(univ.party.special_notes[i].the_str); + encNotes["str" + n].recalcRect(); + encNotes["pane" + n].recalcRect(); encNotes["del" + n].show(); } else encNotes["del" + n].hide(); diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 97036a6ed..32b8aaafa 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -2637,7 +2637,7 @@ void oneshot_spec(const runtime_state& ctx) { univ.get_strs(strs, ctx.cur_spec_type, spec.m1); // Leave / Take buttons[0] = 9; buttons[1] = 19; - dlg_res = custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c); + dlg_res = custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ); if(dlg_res == 1) {set_sd = false; ctx.next_spec = -1;} else { store_i = univ.scenario.get_stored_item(spec.ex1a); @@ -2677,7 +2677,7 @@ void oneshot_spec(const runtime_state& ctx) { if((spec.m1 >= 0) || (spec.m2 >= 0)) { univ.get_strs(strs[0],strs[1], ctx.cur_spec_type, spec.m1, spec.m2); buttons[0] = 3; buttons[1] = 2; - dlg_res = custom_choice_dialog(strs,spec.pic,ePicType(spec.pictype),buttons, true, spec.ex1c, spec.ex2c); + dlg_res = custom_choice_dialog(strs,spec.pic,ePicType(spec.pictype),buttons, true, spec.ex1c, spec.ex2c, &univ); // TODO: Make custom_choice_dialog return string? } else dlg_res = cChoiceDlog("basic-trap",{"yes","no"}).show() == "no"; @@ -4006,7 +4006,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 9; buttons[1] = 35; - if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) + if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ) == 1) ctx.next_spec = -1; else { int x = univ.party.get_ptr(10), y = univ.party.get_ptr(11); @@ -4037,7 +4037,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs, ctx.cur_spec_type,spec.m1); buttons[0] = 9; buttons[1] = 8; - if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) { + if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ) == 1) { ctx.next_spec = -1; if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; @@ -4069,7 +4069,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 20; buttons[1] = 24; - int i = spec.ex2b == 1 ? 2 : custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons); + int i = spec.ex2b == 1 ? 2 : custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, false, -1, -1, &univ); *ctx.ret_a = 1; if(i == 1) { ctx.next_spec = -1; From ba8b893c20dd7cba26209ae7873f1ef81b95ad66 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 6 Aug 2025 15:43:02 -0500 Subject: [PATCH 53/86] TRANSPARENT->EMPTY makes it compile on Windows --- src/game/boe.town.cpp | 4 ++-- src/gfx/render_shapes.hpp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 92af62f44..51b34415a 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1542,13 +1542,13 @@ void draw_map(bool need_refresh, std::string tooltip_text) { // Draw signs const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; for(sign_loc_t sign : sign_locs){ - mark_loc(sign, Colours::TRANSPARENT, Colours::YELLOW); + mark_loc(sign, Colours::EMPTY, Colours::YELLOW); } // Draw town entrances if(is_out()){ const std::vector& city_locs = univ.out->city_locs; for(spec_loc_t city : city_locs){ - mark_loc(city, Colours::TRANSPARENT, Colours::GREEN); + mark_loc(city, Colours::EMPTY, Colours::GREEN); } } // Draw vehicles diff --git a/src/gfx/render_shapes.hpp b/src/gfx/render_shapes.hpp index 3ae03c845..623935354 100644 --- a/src/gfx/render_shapes.hpp +++ b/src/gfx/render_shapes.hpp @@ -58,7 +58,8 @@ namespace Colours { const sf::Color YELLOW { 0xff, 0xff, 0x31}; const sf::Color ORANGE { 0xff, 0x80, 0x00}; const sf::Color LIGHT_BLUE { 0xad, 0xd8, 0xe6 }; // Spell points on dark background - const sf::Color TRANSPARENT { 0x00, 0x00, 0x00, 0x00 }; + // On Windows, TRANSPARENT is macro-defined as something...? + const sf::Color EMPTY { 0x00, 0x00, 0x00, 0x00 }; // Text colours for shopping / talking // TODO: The Windows version appears to use completely different colours? const sf::Color SHADOW { 0x00, 0x00, 0x68}; // formerly c[3] QD colour = {0,0,26623} (shop/character name shadow, shop subtitle) From 8341bd46f9aaae2faaec37444b1d652bc6adbef4 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 7 Aug 2025 09:17:23 -0500 Subject: [PATCH 54/86] better way of only showing known extent of area rectangle --- src/game/boe.town.cpp | 64 +++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 51b34415a..0e41df50a 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "boe.global.hpp" @@ -1371,6 +1372,10 @@ void draw_map(bool need_refresh, std::string tooltip_text) { out_mode = true; else out_mode = false; + // While iterating the map, calculate the discovered extent of rooms/areas + const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; + std::vector known_area_rects(area_desc.size(), {std::numeric_limits::max(),std::numeric_limits::max(),std::numeric_limits::min(),std::numeric_limits::min()}); + sf::Texture& small_ter_gworld = *ResMgr::graphics.get("termap"); for(where.x = view_rect.left; where.x < view_rect.right; where.x++) for(where.y = view_rect.top; where.y < view_rect.bottom; where.y++) { @@ -1437,50 +1442,31 @@ void draw_map(bool need_refresh, std::string tooltip_text) { rect_draw_some_item(*ResMgr::graphics.get("trim"),{8,112,12,116},map_gworld(),draw_rect); } - // Draw discovered edges of rooms/areas - const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; - location a; - location b; - for(info_rect_t area : area_desc){ - location adjusted_where = where; - adjusted_where.x -= view_rect.left; - adjusted_where.y -= view_rect.top; - if(where.y == area.top && where.x >= area.left && where.x <= area.right){ - a = orig_draw_rect.topLeft(); - a.x += 6 * adjusted_where.x; - a.y += 6 * adjusted_where.y; - b = a; - b.x += 6; - draw_line(map_gworld(), a, b, 1, Colours::RED); - } - if(where.x == area.left && where.y >= area.top && where.y <= area.bottom){ - a = orig_draw_rect.topLeft(); - a.x += 6 * adjusted_where.x; - a.y += 6 * adjusted_where.y; - b = a; - b.y += 6; - draw_line(map_gworld(), a, b, 1, Colours::RED); - } - if(where.y == area.bottom && where.x >= area.left && where.x <= area.right){ - a = orig_draw_rect.topLeft(); - a.x += 6 * adjusted_where.x; - a.y += 6 * (adjusted_where.y + 1) - 1; - b = a; - b.x += 6; - draw_line(map_gworld(), a, b, 1, Colours::RED); - } - if(where.x == area.right && where.y >= area.top && where.y <= area.bottom){ - a = orig_draw_rect.topLeft(); - a.x += 6 * (adjusted_where.x + 1) - 1; - a.y += 6 * adjusted_where.y; - b = a; - b.y += 6; - draw_line(map_gworld(), a, b, 1, Colours::RED); + for(int i = 0; i < area_desc.size(); ++i){ + info_rect_t area = area_desc[i]; + rectangle& known_bounds = known_area_rects[i]; + // tile is in an area rectangle. see if it extends the party's known bounds of the area + if(area.contains(where)){ + if(where.x < known_bounds.left) known_bounds.left = where.x; + if(where.y < known_bounds.top) known_bounds.top = where.y; + if(where.x + 1 > known_bounds.right) known_bounds.right = where.x + 1; + if(where.y + 1 > known_bounds.bottom) known_bounds.bottom = where.y + 1; } } } } + // Draw known extent of area rectangles + for(const rectangle& known_bounds : known_area_rects){ + if(!known_bounds.empty()){ + draw_rect.top = 6 * (known_bounds.top - view_rect.top); + draw_rect.left = 6 * (known_bounds.left - view_rect.left); + draw_rect.bottom = 6 * (known_bounds.bottom - view_rect.top); + draw_rect.right = 6 * (known_bounds.right - view_rect.left); + frame_rect(map_gworld(), draw_rect, Colours::RED); + } + } + map_gworld().display(); // this stops flickering if the display time is too long glFlush(); From 18f1c3b9239fc0ed979b9926c7cef05845935f51 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 7 Aug 2025 09:23:21 -0500 Subject: [PATCH 55/86] Tooltip text when party icon highlighted --- src/game/boe.main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 28d59b796..171430c03 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1485,14 +1485,20 @@ void handle_one_minimap_event(const sf::Event& event) { if(!vehicle_is_here(boat)) continue; if(boat.loc == tile){ tooltip_text += (boat.property ? "Boat (Not Yours)" : "Your Boat"); + tooltip_text += " |"; } } for(auto& horse : univ.party.horses) { if(!vehicle_is_here(horse)) continue; if(horse.loc == tile){ tooltip_text += (horse.property ? "Horses (Not Yours)" : "Your Horses"); + tooltip_text += " |"; } } + // Party hovered + if(tile == (is_out() ? univ.party.loc_in_sec : univ.party.town_loc)){ + tooltip_text += "Your Party"; + } } if(tooltip_text != last_tooltip_text) draw_map(false, tooltip_text); From 660fdb584a06c1e5528a3a5f170e070a72183825 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 7 Aug 2025 10:41:22 -0500 Subject: [PATCH 56/86] specify vehicle.hpp directory --- src/game/boe.locutils.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.locutils.hpp b/src/game/boe.locutils.hpp index 01f147137..c61cdfd9c 100644 --- a/src/game/boe.locutils.hpp +++ b/src/game/boe.locutils.hpp @@ -1,7 +1,7 @@ #include #include "location.hpp" -#include "vehicle.hpp" +#include "scenario/vehicle.hpp" bool is_explored(short i,short j); void make_explored(short i,short j); From db293d321e662a01d74c87f214dae882d94959fa Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 7 Aug 2025 15:20:05 -0500 Subject: [PATCH 57/86] add btnpanel sources to xcode12 --- proj/xc12/BoE.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proj/xc12/BoE.xcodeproj/project.pbxproj b/proj/xc12/BoE.xcodeproj/project.pbxproj index 3642036e6..9e5c93470 100755 --- a/proj/xc12/BoE.xcodeproj/project.pbxproj +++ b/proj/xc12/BoE.xcodeproj/project.pbxproj @@ -66,6 +66,8 @@ 413AAF672D38A4A5002E9BF1 /* living.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 914698FB1A7362D900F20F5E /* living.cpp */; }; 413FE08F2CECFAFF000D97DC /* winutil.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 413FE08E2CECFAFF000D97DC /* winutil.cpp */; }; 415EEEB02D5534A500B47408 /* prefs.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 415EEEAF2D5534A500B47408 /* prefs.cpp */; }; + 419889802E4541D10080B0FE /* btnpanel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4198897F2E4541D10080B0FE /* btnpanel.cpp */; }; + 419889812E4541D10080B0FE /* btnpanel.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 4198897E2E4541D10080B0FE /* btnpanel.hpp */; }; 41E550542DEB8C2A00A7DF52 /* scen.undo.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 41E550532DEB8C2A00A7DF52 /* scen.undo.cpp */; }; 91034D211B225E4A008F01C1 /* scen.appleevents.mm in Sources */ = {isa = PBXBuildFile; fileRef = 91034D201B225E49008F01C1 /* scen.appleevents.mm */; }; 911A14031B8FAFC600900FD9 /* town_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C2A6EC1B8FA91400346948 /* town_read.cpp */; }; @@ -638,6 +640,8 @@ 41342CE92DFB872400E66BEB /* quest.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = quest.cpp; sourceTree = ""; }; 413FE08E2CECFAFF000D97DC /* winutil.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = winutil.cpp; sourceTree = ""; }; 415EEEAF2D5534A500B47408 /* prefs.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = prefs.cpp; sourceTree = ""; }; + 4198897E2E4541D10080B0FE /* btnpanel.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = btnpanel.hpp; sourceTree = ""; }; + 4198897F2E4541D10080B0FE /* btnpanel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = btnpanel.cpp; sourceTree = ""; }; 41E550522DEB8C1400A7DF52 /* scen.undo.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = scen.undo.hpp; sourceTree = ""; }; 41E550532DEB8C2A00A7DF52 /* scen.undo.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = scen.undo.cpp; sourceTree = ""; }; 91034D201B225E49008F01C1 /* scen.appleevents.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = scen.appleevents.mm; sourceTree = ""; }; @@ -1155,6 +1159,8 @@ 91E128F41BC2077700C8BE1D /* pictchoice.hpp */, 91E128F51BC2077700C8BE1D /* strchoice.hpp */, 91E128F61BC2077700C8BE1D /* strdlog.hpp */, + 4198897E2E4541D10080B0FE /* btnpanel.hpp */, + 4198897F2E4541D10080B0FE /* btnpanel.cpp */, ); path = dialogs; sourceTree = ""; @@ -1751,6 +1757,7 @@ 9149924C25913E3F00B5BE97 /* container.hpp in Headers */, 9149924E25913E3F00B5BE97 /* led.hpp in Headers */, 9170C5102D717F24009B6E7C /* scen.locpicker.hpp in Headers */, + 419889812E4541D10080B0FE /* btnpanel.hpp in Headers */, 9143044B2970EDC1003A3967 /* keymods.hpp in Headers */, 9149925025913E3F00B5BE97 /* ledgroup.hpp in Headers */, 915473CE2C800AB000EB1C94 /* enchant.hpp in Headers */, @@ -2202,6 +2209,7 @@ 91E128F11BC2076B00C8BE1D /* strdlog.cpp in Sources */, 91CE248A1EA12866005BDCE4 /* utility.cpp in Sources */, 91CE248C1EA12A96005BDCE4 /* render_text.cpp in Sources */, + 419889802E4541D10080B0FE /* btnpanel.cpp in Sources */, 91CE248E1EA12AA3005BDCE4 /* render_shapes.cpp in Sources */, 91A2480E2969CFD200B8D90F /* res_dialog.cpp in Sources */, 91CE24921EA12ABD005BDCE4 /* gfxsheets.cpp in Sources */, From 72aafe9faf823277fe8f0ba36a9cbf6b5ba7f39c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 8 Aug 2025 20:39:59 -0500 Subject: [PATCH 58/86] fix loc_off_act_area for outdoors --- src/game/boe.locutils.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index f01f4348c..bf0778f7b 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -138,8 +138,9 @@ bool loc_off_world(location p1) { } bool loc_off_act_area(location p1) { - if(is_out()) return false; // How should this check be handled? - if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && + if(is_out() && univ.out->is_on_map(p1)) + return false; + else if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && (p1.y > univ.town->in_town_rect.top) && (p1.y < univ.town->in_town_rect.bottom)) return false; return true; From dde1488cfa8d34c2779e0aa082cb8bbee550cb99 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 8 Aug 2025 20:40:25 -0500 Subject: [PATCH 59/86] fix crash when out-of-bounds 'tile' hovered in minimap --- src/game/boe.main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 171430c03..f44d1a2df 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1455,7 +1455,10 @@ void handle_one_minimap_event(const sf::Event& event) { tile.y += (y - (29 * get_ui_scale_map())) / get_ui_scale_map() / 6; std::string tooltip_text = ""; - if(is_explored(tile.x, tile.y)){ + bool is_on_map; + if(is_out()) is_on_map = tile.x >= 0 && tile.y >= 0 && tile.x < 48 && tile.y < 48; + else is_on_map = univ.town->is_on_map(tile); + if(is_on_map && is_explored(tile.x, tile.y)){ // Area rectangle hovered const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; for(info_rect_t area : area_desc){ From 32b9ce643ba1b03c1f5d2ed262f948d9e30f56c8 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 8 Aug 2025 20:40:41 -0500 Subject: [PATCH 60/86] fix outdoor positioning of circle markers --- src/game/boe.town.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 0e41df50a..a8ccb5a21 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1497,7 +1497,9 @@ void draw_map(bool need_refresh, std::string tooltip_text) { rect_draw_some_item(map_gworld().getTexture(),the_rect,mini_map(),area_to_draw_on); auto mark_loc = [view_rect, &draw_rect, area_to_draw_on](location where, sf::Color inner, sf::Color outer) -> void { - if((is_explored(where.x,where.y)) && + location real_where = where; + if(is_out()) real_where = local_to_global(where); + if((is_explored(real_where.x,real_where.y)) && ((where.x >= view_rect.left) && (where.x < view_rect.right) && where.y >= view_rect.top && where.y < view_rect.bottom)){ draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); From e4ae1a70ee4ece505ba7c3276087ada82d7c582d Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 8 Aug 2025 20:43:31 -0500 Subject: [PATCH 61/86] fix hovering map features outdoors --- src/game/boe.main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index f44d1a2df..51be89abe 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1458,7 +1458,9 @@ void handle_one_minimap_event(const sf::Event& event) { bool is_on_map; if(is_out()) is_on_map = tile.x >= 0 && tile.y >= 0 && tile.x < 48 && tile.y < 48; else is_on_map = univ.town->is_on_map(tile); - if(is_on_map && is_explored(tile.x, tile.y)){ + location real_tile = tile; + if(is_out()) real_tile = local_to_global(tile); + if(is_on_map && is_explored(real_tile.x, real_tile.y)){ // Area rectangle hovered const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; for(info_rect_t area : area_desc){ From e5c81f2eb214fa05410b6d15e19454c885004b7b Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 11:09:08 -0500 Subject: [PATCH 62/86] move is_sign to boe.locutils --- src/game/boe.actions.cpp | 8 -------- src/game/boe.actions.hpp | 1 - src/game/boe.locutils.cpp | 6 +++++- src/game/boe.locutils.hpp | 1 + 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/game/boe.actions.cpp b/src/game/boe.actions.cpp index 77368b150..4f0a3bad8 100644 --- a/src/game/boe.actions.cpp +++ b/src/game/boe.actions.cpp @@ -692,7 +692,6 @@ void handle_look(location destination, bool right_button, eKeyMod mods, bool& ne if(adj_town_look(destination)) need_redraw = true; // TODO: This would be the place to call OUT_LOOK special - // TODO: Do we really need an is_sign function? if(is_sign(ter_looked_at)) { print_buf(); need_reprint = false; @@ -4273,13 +4272,6 @@ short count_walls(location loc) { // TODO: Generalize this function return answer; } -bool is_sign(ter_num_t ter) { - - if(univ.scenario.ter_types[ter].special == eTerSpec::IS_A_SIGN) - return true; - return false; -} - bool check_for_interrupt(std::string confirm_dialog){ using kb = sf::Keyboard; bool interrupt = false; diff --git a/src/game/boe.actions.hpp b/src/game/boe.actions.hpp index b722c90dc..bf8b26bef 100644 --- a/src/game/boe.actions.hpp +++ b/src/game/boe.actions.hpp @@ -54,7 +54,6 @@ short nearest_monster(); void setup_outdoors(location where); short get_outdoor_num(); short count_walls(location loc); -bool is_sign(ter_num_t ter); bool check_for_interrupt(std::string confirm_dialog = "confirm-interrupt-special"); void handle_startup_button_click(eStartButton btn, eKeyMod mods); diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index bf0778f7b..bc3bf4369 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -612,4 +612,8 @@ bool vehicle_is_here(const cVehicle& vehicle) { }else{ return vehicle.which_town == univ.party.town_num; } -} \ No newline at end of file +} + +bool is_sign(ter_num_t ter) { + return univ.scenario.ter_types[ter].special == eTerSpec::IS_A_SIGN; +} diff --git a/src/game/boe.locutils.hpp b/src/game/boe.locutils.hpp index c61cdfd9c..3adcfb3af 100644 --- a/src/game/boe.locutils.hpp +++ b/src/game/boe.locutils.hpp @@ -36,6 +36,7 @@ bool outd_is_special(location to_check); bool impassable(ter_num_t terrain_to_check); short get_blockage(ter_num_t terrain_type); short light_radius(); +bool is_sign(ter_num_t ter); bool pt_in_light(location from_where,location to_where) ;// Assumes, of course, in town or combat bool combat_pt_in_light(location to_where); bool party_sees_a_monst(); // Returns true is a hostile monster is in sight. From 28dbf97eeef18c593510dd48ccf3e595c5b9d40a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 11:09:41 -0500 Subject: [PATCH 63/86] don't tooltip or annotate hidden towns/junk sign locs --- src/game/boe.main.cpp | 10 ++++++++-- src/game/boe.town.cpp | 13 ++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 51be89abe..bcda41e15 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1473,7 +1473,10 @@ void handle_one_minimap_event(const sf::Event& event) { const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; for(sign_loc_t sign : sign_locs){ if(sign == tile){ - tooltip_text += "Sign: " + sign.text + " |"; + // make sure the terrain is a sign + ter_num_t ter = is_out() ? univ.out[real_tile] : univ.town->terrain(real_tile.x, real_tile.y); + if(is_sign(ter)) + tooltip_text += "Sign: " + sign.text + " |"; } } // Town entrance hovered @@ -1481,7 +1484,10 @@ void handle_one_minimap_event(const sf::Event& event) { const std::vector& city_locs = univ.out->city_locs; for(spec_loc_t city : city_locs){ if(city == tile){ - tooltip_text += univ.scenario.towns[city.spec]->name + " |"; + // don't tooltip hidden towns + ter_num_t ter = is_out() ? univ.out[real_tile] : univ.town->terrain(real_tile.x, real_tile.y); + if(univ.scenario.ter_types[ter].special == eTerSpec::TOWN_ENTRANCE) + tooltip_text += univ.scenario.towns[city.spec]->name + " |"; } } } diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index a8ccb5a21..a16eaea8c 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1496,12 +1496,18 @@ void draw_map(bool need_refresh, std::string tooltip_text) { if(canMap) { rect_draw_some_item(map_gworld().getTexture(),the_rect,mini_map(),area_to_draw_on); - auto mark_loc = [view_rect, &draw_rect, area_to_draw_on](location where, sf::Color inner, sf::Color outer) -> void { + auto mark_loc = [view_rect, &draw_rect, area_to_draw_on](location where, sf::Color inner, sf::Color outer, eTerSpec if_spec = eTerSpec::NONE) -> void { location real_where = where; if(is_out()) real_where = local_to_global(where); if((is_explored(real_where.x,real_where.y)) && ((where.x >= view_rect.left) && (where.x < view_rect.right) && where.y >= view_rect.top && where.y < view_rect.bottom)){ + // if if_spec is specified, make sure the terrain on the spot has the given special + if(if_spec != eTerSpec::NONE){ + ter_num_t ter = is_out() ? univ.out[real_where] : univ.town->terrain(real_where.x, real_where.y); + if(univ.scenario.ter_types[ter].special != if_spec) return; + } + draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); draw_rect.right = draw_rect.left + 6; @@ -1530,13 +1536,14 @@ void draw_map(bool need_refresh, std::string tooltip_text) { // Draw signs const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; for(sign_loc_t sign : sign_locs){ - mark_loc(sign, Colours::EMPTY, Colours::YELLOW); + mark_loc(sign, Colours::EMPTY, Colours::YELLOW, eTerSpec::IS_A_SIGN); } // Draw town entrances if(is_out()){ const std::vector& city_locs = univ.out->city_locs; + // TODO don't draw hidden ones for(spec_loc_t city : city_locs){ - mark_loc(city, Colours::EMPTY, Colours::GREEN); + mark_loc(city, Colours::EMPTY, Colours::GREEN, eTerSpec::TOWN_ENTRANCE); } } // Draw vehicles From 4f5645f033f6c37931020dc3333784ccd7c44a7c Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 11:44:22 -0500 Subject: [PATCH 64/86] Clip big monster combat frame at window edge --- src/game/boe.graphutil.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 99d89443f..3e3e1a064 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -208,6 +208,11 @@ void draw_monsters() { target_rect.right = target_rect.left + 28 * monst.x_width; target_rect.top = 13 + 36 * where_draw.y; target_rect.bottom = target_rect.top + 36 * monst.y_width; + // clip the frame at the window boundaries + target_rect.top = max(13, target_rect.top); + target_rect.left = max(13, target_rect.left); + target_rect.bottom = min(target_rect.bottom, terrain_screen_gworld().getSize().y - 13); + target_rect.right = min(target_rect.right, terrain_screen_gworld().getSize().x - 13); frame_rect(terrain_screen_gworld(), target_rect, monst.is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127)); } } From edf7c28163ad003901b8f0292592a4b0d232a266 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 13:03:14 -0500 Subject: [PATCH 65/86] targeting grid fix big monster handling --- src/game/boe.graphics.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index c414e200e..6c7e90526 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1649,6 +1649,8 @@ void draw_targeting_line() { sf::Color frame_color(0, 0, 0, 0); int x_width = 1; int y_width = 1; + short frame_q = q; + short frame_r = r; if((can_see_light(from_loc,which_space,sight_obscurity) < 5) && (dist(from_loc,which_space) <= current_spell_range)){ frame_color = sf::Color(255, 255, 255, 127); @@ -1659,23 +1661,36 @@ void draw_targeting_line() { } targ = univ.target_there(which_space, TARG_MONST); if(targ != nullptr){ - cMonster* monst = dynamic_cast(targ); + cCreature* monst = dynamic_cast(targ); if(!monst->invisible){ frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); + // Frame needs to be positioned at the monster's top left corner + frame_q -= (which_space.x - monst->cur_loc.x); + frame_r -= (which_space.y - monst->cur_loc.y); x_width = monst->x_width; y_width = monst->y_width; for(int i = 0; i < x_width; ++i){ for(int j = 0; j < y_width; ++j){ - big_monst_there.insert(loc(which_space.x + i, which_space.y + j)); + big_monst_there.insert(loc(monst->cur_loc.x + i, monst->cur_loc.y + j)); } } } } - target_rect.left = 13 + 28 * q + 19; + const int ter_screen_left = 19 + 13; // 13 is the white frame + const int ter_screen_top = 7 + 13; // 13 is the white frame + + target_rect.left = ter_screen_left + 28 * frame_q; target_rect.right = target_rect.left + 28 * x_width; - target_rect.top = 13 + 36 * r + 7; + target_rect.top = ter_screen_top + 36 * frame_r; target_rect.bottom = target_rect.top + 36 * y_width; + + // Clip the target rect to the window bounds + target_rect.left = max(ter_screen_left, target_rect.left); + target_rect.top = max(ter_screen_top, target_rect.top); + target_rect.right = min(ter_screen_left + 28 * 9, target_rect.right); + target_rect.bottom = min(ter_screen_top + 36 * 9, target_rect.bottom); + frame_rect(mainPtr(), target_rect, frame_color); } } From 7ff53365e0a0e8ac67b6b681600fc5194829d54a Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 13:52:52 -0500 Subject: [PATCH 66/86] fix monsters rendering backwards? fix #785 --- src/game/boe.graphutil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 3e3e1a064..280ddeece 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -192,7 +192,7 @@ void draw_monsters() { Draw_Some_Item(*src_gw, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } else { pic_num_t this_monst = monst.picture_num; - int pic_mode = (monst.direction) < 4 ? 0 : 1; + int pic_mode = (monst.direction) >= 4 ? 0 : 1; pic_mode += (combat_posing_monster == i + 100) ? 10 : 0; source_rect = get_monster_template_rect(this_monst, pic_mode, k); int which_sheet = (m_pic_index[this_monst].i+k) / 20; From 87e0b4508985862c132d78002f7b6f7170cca1d3 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 15:28:36 -0500 Subject: [PATCH 67/86] feature flag keep doors easier in old scenarios --- src/game/boe.party.cpp | 6 +++--- src/game/boe.town.cpp | 4 ++-- src/scenedit/scen.core.cpp | 2 ++ src/universe/universe.cpp | 4 ++++ src/universe/universe.hpp | 1 + 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/game/boe.party.cpp b/src/game/boe.party.cpp index 95262d51b..2f3e20bf7 100644 --- a/src/game/boe.party.cpp +++ b/src/game/boe.party.cpp @@ -1384,10 +1384,10 @@ void cast_town_spell(location where) { case eSpell::UNLOCK: // TODO: Is the unlock spell supposed to have a max range? if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE){ - if(univ.scenario.ter_types[ter].flag2 == 10) + if(univ.scenario.ter_types[ter].flag2 == 10){ r1 = 10000; - else{ - r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town->difficulty; + }else{ + r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town.door_diff_adjust(); r1 += univ.scenario.ter_types[ter].flag2 * 7; } if(r1 < (135 - combat_percent[min(19,level)])) { diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index a16eaea8c..a2a048c30 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1144,7 +1144,7 @@ void pick_lock(location where,short pc_num) { if(r1 < 75) will_break = true; - r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town->difficulty * 7 + r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town.door_diff_adjust() * 7 - 5 * univ.party[pc_num].skill(eSkill::LOCKPICKING) - which_item->abil_strength * 7; // Nimble? @@ -1177,7 +1177,7 @@ void bash_door(location where,short pc_num) { short r1,unlock_adjust; terrain = univ.town->terrain(where.x,where.y); - r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town->difficulty * 4; + r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town.door_diff_adjust() * 4; if(univ.scenario.ter_types[terrain].special != eTerSpec::UNLOCKABLE) { add_string_to_buf(" Wrong terrain type."); diff --git a/src/scenedit/scen.core.cpp b/src/scenedit/scen.core.cpp index 641b9c662..c01d01f1c 100644 --- a/src/scenedit/scen.core.cpp +++ b/src/scenedit/scen.core.cpp @@ -3344,6 +3344,8 @@ bool build_scenario() { scenario.feature_flags = { {"scenario-meta-format", "V2"}, {"resurrection-balm", "required"}, + // Town difficulty, due to a bug, used to not be added to door difficulty + {"door-town-difficulty", "fixed"} }; fs::path basePath = progDir/"Blades of Exile Base"/(grass ? "bladbase.boes" : "cavebase.boes"); diff --git a/src/universe/universe.cpp b/src/universe/universe.cpp index 9ba324289..327df733f 100644 --- a/src/universe/universe.cpp +++ b/src/universe/universe.cpp @@ -116,6 +116,10 @@ const cTown& cCurTown::operator * () const { return *record(); } +int cCurTown::door_diff_adjust() { + return univ.scenario.has_feature_flag("door-town-difficulty") ? arena->difficulty : 0; +} + void cCurTown::place_preset_fields() { // Initialize barriers, etc. Note non-sfx gets forgotten if this is a town recently visited. fields.resize(record()->max_dim, record()->max_dim); diff --git a/src/universe/universe.hpp b/src/universe/universe.hpp index a40246766..17e9f8af7 100644 --- a/src/universe/universe.hpp +++ b/src/universe/universe.hpp @@ -52,6 +52,7 @@ class cCurTown { void import_legacy(unsigned char(& old_sfx)[64][64], unsigned char(& old_misc_i)[64][64]); void import_legacy(legacy::big_tr_type& old); + int door_diff_adjust(); cTown* operator -> (); cTown& operator * (); const cTown* operator -> () const; From c41d71c70fe6b5de7fd954698f41265ebbc49c7b Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 15:46:00 -0500 Subject: [PATCH 68/86] Fix a case of minimap area rect spoilers --- src/game/boe.locutils.cpp | 18 ++++++++++-------- src/game/boe.locutils.hpp | 2 +- src/game/boe.town.cpp | 4 +++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index bc3bf4369..a27cdfbe0 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -260,14 +260,14 @@ void update_explored(const location dest) { // All purpose function to check is spot is free for travel into. -bool is_blocked(location to_check) { +bool is_blocked(location to_check, bool count_party) { if(is_out()) { if(!univ.out.is_on_map(to_check.x, to_check.y)) return true; if(impassable(univ.out[to_check.x][to_check.y])) { return true; } - if(to_check == univ.party.out_loc) + if(count_party && to_check == univ.party.out_loc) return true; for(short i = 0; i < univ.party.out_c.size(); i++) if((univ.party.out_c[i].exists)) @@ -294,12 +294,14 @@ bool is_blocked(location to_check) { // Note: The purpose of the above check is to avoid portals. // Party there? - if(is_town() && to_check == univ.party.town_loc) - return true; - if(is_combat()) { - for(short i = 0; i < 6; i++) { - if(univ.party[i].main_status == eMainStatus::ALIVE && to_check == univ.party[i].combat_pos) { - return true; + if(count_party){ + if(is_town() && to_check == univ.party.town_loc) + return true; + if(is_combat()) { + for(short i = 0; i < 6; i++) { + if(univ.party[i].main_status == eMainStatus::ALIVE && to_check == univ.party[i].combat_pos) { + return true; + } } } } diff --git a/src/game/boe.locutils.hpp b/src/game/boe.locutils.hpp index 3adcfb3af..6778042a2 100644 --- a/src/game/boe.locutils.hpp +++ b/src/game/boe.locutils.hpp @@ -24,7 +24,7 @@ short combat_obscurity(short x,short y); ter_num_t coord_to_ter(short x,short y); bool is_container(location loc); void update_explored(location dest); -bool is_blocked(location to_check); +bool is_blocked(location to_check, bool count_party = true); bool monst_can_be_there(location loc,short m_num); bool monst_adjacent(location loc,short m_num); bool monst_can_see(short m_num,location l); diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index a2a048c30..82c353730 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1443,10 +1443,12 @@ void draw_map(bool need_refresh, std::string tooltip_text) { } for(int i = 0; i < area_desc.size(); ++i){ + location real_where = where; + if(is_out()) real_where = local_to_global(where); info_rect_t area = area_desc[i]; rectangle& known_bounds = known_area_rects[i]; // tile is in an area rectangle. see if it extends the party's known bounds of the area - if(area.contains(where)){ + if(area.contains(where) && !is_blocked(real_where, false)){ if(where.x < known_bounds.left) known_bounds.left = where.x; if(where.y < known_bounds.top) known_bounds.top = where.y; if(where.x + 1 > known_bounds.right) known_bounds.right = where.x + 1; From 5ac0fea02ecf05c87d8c48020e57bf2866b1df51 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 16:24:32 -0500 Subject: [PATCH 69/86] fix outdoor monster facing --- src/game/boe.graphutil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 280ddeece..2c8e58b65 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -142,7 +142,7 @@ void draw_monsters() { } if(picture_wanted < 1000) { for(short k = 0; k < width * height; k++) { - source_rect = get_monster_template_rect(picture_wanted,(enc.direction < 4) ? 0 : 1,k); + source_rect = get_monster_template_rect(picture_wanted,(enc.direction >= 4) ? 0 : 1,k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); int which_sheet = m_pic_index[picture_wanted].i / 20; From c9c629c14babbccb91988baa417f91589a42ec63 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 16:29:03 -0500 Subject: [PATCH 70/86] fix outdoor custom monster facing --- src/game/boe.graphutil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 2c8e58b65..b6e1d7cae 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -134,7 +134,7 @@ void draw_monsters() { for(short k = 0; k < width * height; k++) { std::shared_ptr src_gw; graf_pos_ref(src_gw, source_rect) = spec_scen_g.find_graphic(picture_wanted % 1000 + - ((enc.direction < 4) ? 0 : (width * height)) + k); + ((enc.direction >= 4) ? 0 : (width * height)) + k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); rect_draw_some_item(*src_gw, source_rect, terrain_screen_gworld(),to_rect, sf::BlendAlpha); From 725c5d9a2b3d274d55811b6738af73e2c265b299 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 18:49:05 -0500 Subject: [PATCH 71/86] Get all gold don't steal without consequences --- src/game/boe.items.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index bb7a75337..dfe59ea54 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -398,7 +398,7 @@ static void put_item_graphics(cDialog& me, size_t& first_item_shown, short& curr me["gold"].hide(); me["gold-label"].hide(); for(cItem* item : item_array){ - if(item->variety == eItemType::GOLD){ + if(item->variety == eItemType::GOLD && !item->property){ me["gold"].show(); me["gold-label"].show(); break; @@ -474,7 +474,7 @@ static bool display_item_event_filter(cDialog& me, std::string id, size_t& first // Get all gold for(int i = item_array.size() - 1; i >= 0; --i){ item = *item_array[i]; - if(item.variety != eItemType::GOLD) continue; + if(item.variety != eItemType::GOLD || item.property) continue; if(item.item_level > 3000) item.item_level = 3000; set_item_flag(&item); From fc463df66b0f87d586b6f150b6f5646544b95b97 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 18:57:47 -0500 Subject: [PATCH 72/86] Draw roads with brown dots. Fix #782 --- src/game/boe.town.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 82c353730..dd046eb60 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1439,7 +1439,7 @@ void draw_map(bool need_refresh, std::string tooltip_text) { if(is_out() ? univ.out->roads[where.x][where.y] : univ.town.is_road(where.x,where.y)) { draw_rect.inset(1,1); - rect_draw_some_item(*ResMgr::graphics.get("trim"),{8,112,12,116},map_gworld(),draw_rect); + rect_draw_some_item(*ResMgr::graphics.get("fields"),{80,28,84,32},map_gworld(),draw_rect); } for(int i = 0; i < area_desc.size(); ++i){ From 36c56f4b9a901f42aaef175195f60369ec04aba8 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 9 Aug 2025 20:27:21 -0500 Subject: [PATCH 73/86] ignore uninitialized Rectangle 1-8 --- src/game/boe.main.cpp | 1 + src/game/boe.text.cpp | 20 +++++++------------- src/game/boe.town.cpp | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index bcda41e15..fa3057efe 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -1464,6 +1464,7 @@ void handle_one_minimap_event(const sf::Event& event) { // Area rectangle hovered const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; for(info_rect_t area : area_desc){ + if(area.empty()) continue; if(area.contains(tile)){ tooltip_text += area.descr + " |"; } diff --git a/src/game/boe.text.cpp b/src/game/boe.text.cpp index 2aa7a3ac8..c3d86fcbc 100644 --- a/src/game/boe.text.cpp +++ b/src/game/boe.text.cpp @@ -1261,19 +1261,13 @@ std::string get_location(cUniverse* specific_univ) { std::string loc_str = ""; location loc = outdoors ? global_to_local(specific_univ->party.out_loc) : specific_univ->party.town_loc; - if(outdoors) { - loc_str = specific_univ->out->name; - for(short i = 0; i < specific_univ->out->area_desc.size(); i++) - if(loc.in(specific_univ->out->area_desc[i])) { - loc_str = specific_univ->out->area_desc[i].descr; - } - } - if(town){ - loc_str = specific_univ->town->name; - for(short i = 0; i < specific_univ->town->area_desc.size(); i++) - if(loc.in(specific_univ->town->area_desc[i])) { - loc_str = specific_univ->town->area_desc[i].descr; - } + const std::vector& area_desc = (outdoors ? specific_univ->out->area_desc : specific_univ->town->area_desc); + + loc_str = outdoors ? specific_univ->out->name : specific_univ->town->name; + for(const info_rect_t& area : area_desc){ + if(!area.empty() && loc.in(area)) { + loc_str = area.descr; + } } return loc_str; } diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index dd046eb60..abc944746 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1448,7 +1448,7 @@ void draw_map(bool need_refresh, std::string tooltip_text) { info_rect_t area = area_desc[i]; rectangle& known_bounds = known_area_rects[i]; // tile is in an area rectangle. see if it extends the party's known bounds of the area - if(area.contains(where) && !is_blocked(real_where, false)){ + if(!area.empty() && area.contains(where) && !is_blocked(real_where, false)){ if(where.x < known_bounds.left) known_bounds.left = where.x; if(where.y < known_bounds.top) known_bounds.top = where.y; if(where.x + 1 > known_bounds.right) known_bounds.right = where.x + 1; From 12db2a5c4e338f7b59d470ba502e8b43a33d93da Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sun, 10 Aug 2025 13:01:23 -0500 Subject: [PATCH 74/86] ignore empty encoding element --- src/fileio/fileio_scen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index d6f284a1b..06078ff04 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -330,7 +330,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario std::string decoded; std::vector options; - if(info.find("encoding") != info.end()){ + if(info.find("encoding") != info.end() && !info["encoding"].empty()){ encoding = info["encoding"]; decoded = decode_temp_str(temp_str, encoding); }else{ From 29fec09816185518d12a73ccde93d4b37d5d4295 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 11 Aug 2025 12:15:52 -0500 Subject: [PATCH 75/86] fix grammar in special node description --- rsrc/strings/specials-text-town.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsrc/strings/specials-text-town.txt b/rsrc/strings/specials-text-town.txt index db809e25a..b016130ca 100644 --- a/rsrc/strings/specials-text-town.txt +++ b/rsrc/strings/specials-text-town.txt @@ -353,7 +353,7 @@ Number of town to place party in Skip dialog and always change level? Trigger Limitations Special to Call in New Town -A dialog comes up text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. +A dialog comes up with text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. -------------------- Relocate Outdoors Unused From f5200b6ad86e081569b5f820ad19cf834d9461d4 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 11 Aug 2025 12:33:34 -0500 Subject: [PATCH 76/86] When shifting to town entrance, show outdoor loc str Fix #693 --- rsrc/dialogs/shift-town-entrance.xml | 5 ++--- src/scenario/area.hpp | 11 +++++++++++ src/scenedit/scen.actions.cpp | 9 ++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/rsrc/dialogs/shift-town-entrance.xml b/rsrc/dialogs/shift-town-entrance.xml index c28ff69eb..65ae29df0 100644 --- a/rsrc/dialogs/shift-town-entrance.xml +++ b/rsrc/dialogs/shift-town-entrance.xml @@ -5,8 +5,7 @@ - - Shift to this town's entrance in this outdoor section? + + Shift to this town's entrance in outdoor section {sec} at {loc} ({loc_str})? - diff --git a/src/scenario/area.hpp b/src/scenario/area.hpp index 8308235d1..7d308087b 100644 --- a/src/scenario/area.hpp +++ b/src/scenario/area.hpp @@ -54,6 +54,17 @@ class cArea { bool is_on_map(location loc) const { return loc.x < max_dim && loc.y < max_dim && loc.x >= 0 && loc.y >= 0; } + + std::string loc_str(location where) { + std::string str = name; + for(info_rect_t rect : area_desc){ + if(!rect.empty() && rect.contains(where)){ + str += ": " + rect.descr; + break; + } + } + return str; + } }; #endif diff --git a/src/scenedit/scen.actions.cpp b/src/scenedit/scen.actions.cpp index afe429652..e4e6ae80d 100644 --- a/src/scenedit/scen.actions.cpp +++ b/src/scenedit/scen.actions.cpp @@ -2335,7 +2335,10 @@ void handle_editor_screen_shift(int dx, int dy) { if(town_entrances.size() == 1){ town_entrance_t only_entrance = town_entrances[0]; cChoiceDlog shift_prompt("shift-town-entrance", {"yes", "no"}); - shift_prompt->getControl("out-sec").setText(boost::lexical_cast(only_entrance.out_sec)); + cControl& text = shift_prompt->getControl("prompt"); + text.replaceText("{sec}", boost::lexical_cast(only_entrance.out_sec)); + text.replaceText("{loc}", boost::lexical_cast(only_entrance.loc)); + text.replaceText("{loc_str}", scenario.outdoors[only_entrance.out_sec.x][only_entrance.out_sec.y]->loc_str(only_entrance.loc)); if(shift_prompt.show() == "yes"){ set_current_out(only_entrance.out_sec, true); @@ -2350,9 +2353,9 @@ void handle_editor_screen_shift(int dx, int dy) { std::vector entrance_strings; for(town_entrance_t entrance : town_entrances){ std::ostringstream sstr; - sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc; + sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc + << " (" <loc_str(entrance.loc) << ")"; entrance_strings.push_back(sstr.str()); - } cStringChoice dlog(entrance_strings, "Shift to one of this town's entrances in the outdoors?"); size_t choice = dlog.show(-1); From 3b5ca5033f685320fb9a5bd032d861b3d3807ce1 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 11 Aug 2025 12:34:50 -0500 Subject: [PATCH 77/86] starting with stairways, show more info in edit special list --- src/scenario/special.cpp | 16 ++++++++++++++++ src/scenario/special.hpp | 2 ++ src/scenedit/scen.graphics.cpp | 6 +++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/scenario/special.cpp b/src/scenario/special.cpp index 05bb0d5af..631ae258d 100644 --- a/src/scenario/special.cpp +++ b/src/scenario/special.cpp @@ -21,6 +21,7 @@ #include "skills_traits.hpp" #include "damage.hpp" #include "fields.hpp" +#include "scenario.hpp" bool cTimer::is_valid() const { if(time < 0) return false; @@ -529,6 +530,21 @@ void cSpecial::import_legacy(legacy::special_node_type& old){ } } +// In the editor node list, be as helpful as possible about what the specific node instance does +std::string cSpecial::editor_hint(const cScenario& scenario) const { + std::string hint = (*type).name(); + + switch(type){ + case eSpecType::TOWN_STAIR: + case eSpecType::TOWN_GENERIC_STAIR: + hint += " to "; + if(ex2a < scenario.towns.size()) hint += scenario.towns[ex2a]->loc_str(loc(ex1a, ex1b)); + else hint += "INVALID TOWN"; + default: break; + } + return hint; +} + static eSpecCat getNodeCategory(eSpecType node) { for(int i = 0; i <= int(eSpecCat::OUTDOOR); i++) { eSpecCat cat = eSpecCat(i); diff --git a/src/scenario/special.hpp b/src/scenario/special.hpp index 34683ed14..6f9427a09 100644 --- a/src/scenario/special.hpp +++ b/src/scenario/special.hpp @@ -16,6 +16,7 @@ #include "dialogxml/widgets/pictypes.hpp" namespace legacy { struct special_node_type; }; +class cScenario; static const short SDF_COMPLETE = 250; @@ -108,6 +109,7 @@ class cSpecial { return true; } bool operator!=(const cSpecial& other) const { return !(*this == other); } + std::string editor_hint(const cScenario& scenario) const; }; enum class eSpecCtxType { diff --git a/src/scenedit/scen.graphics.cpp b/src/scenedit/scen.graphics.cpp index 40f5bad0c..f0d1a1a59 100644 --- a/src/scenedit/scen.graphics.cpp +++ b/src/scenedit/scen.graphics.cpp @@ -535,15 +535,15 @@ static void apply_mode_buttons() { std::ostringstream strb; switch(mode) { case 0: - strb << i << " - " << (*scenario.scen_specials[i].type).name(); + strb << i << " - " << scenario.scen_specials[i].editor_hint(scenario); set_rb(i,RB_SCEN_SPEC, i, strb.str()); break; case 1: - strb << i << " - " << (*current_terrain->specials[i].type).name(); + strb << i << " - " << current_terrain->specials[i].editor_hint(scenario); set_rb(i,RB_OUT_SPEC, i, strb.str()); break; case 2: - strb << i << " - " << (*town->specials[i].type).name(); + strb << i << " - " << town->specials[i].editor_hint(scenario); set_rb(i,RB_TOWN_SPEC, i, strb.str()); break; } From bbbb59d2c1f1b15028d71789bf588bf7f7f5d08d Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 11 Aug 2025 13:21:23 -0500 Subject: [PATCH 78/86] queue_special() return whether special was queued --- src/game/boe.specials.cpp | 5 +++-- src/game/boe.specials.hpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 32b8aaafa..6da53b9b5 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -1986,8 +1986,8 @@ void special_increase_age(long length, bool queue) { draw_terrain(0); } -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { - if(spec < 0) return; +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { + if(spec < 0) return false; pending_special_type queued_special; queued_special.spec = spec; queued_special.where = spec_loc; @@ -1999,6 +1999,7 @@ void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, loca run_special(queued_special, nullptr, nullptr, nullptr); else special_queue.push(queued_special); + return true; } void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { diff --git a/src/game/boe.specials.hpp b/src/game/boe.specials.hpp index 32523b9f3..203f35d37 100644 --- a/src/game/boe.specials.hpp +++ b/src/game/boe.specials.hpp @@ -20,7 +20,7 @@ void teleport_party(short x,short y,short mode); bool run_stone_circle(short which); void change_level(short town_num,short x,short y); void push_things(); -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_spec, location spec_loc, short* a = nullptr, short* b = nullptr, bool* redraw = nullptr); void run_special(pending_special_type spec, short* a, short* b, bool* redraw); From def607d0c9951d33bd17d7675967799545c93f81 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 11 Aug 2025 13:22:41 -0500 Subject: [PATCH 79/86] town entry autosave wait for specials to run. fix #781 --- src/game/boe.specials.cpp | 6 ++++++ src/game/boe.town.cpp | 13 +++++++++---- src/game/boe.town.hpp | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 6da53b9b5..4d50f64f8 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -2009,6 +2009,8 @@ void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { univ.party.age = std::max(univ.party.age, store_time); } +extern bool need_enter_town_autosave; + // This is the big painful one, the main special engine entry point // which_mode - says when it was called // which_type - where the special is stored (town, out, scenario) @@ -2166,6 +2168,10 @@ void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_ erase_out_specials(); else erase_town_specials(); special_in_progress = false; + if(which_mode == eSpecCtx::ENTER_TOWN && need_enter_town_autosave){ + try_auto_save("EnterTown"); + need_enter_town_autosave = false; + } // TODO: Should find a way to do this that doesn't risk stack overflow if(ctx.next_spec == -1 && !special_queue.empty()) { diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index abc944746..f4fdd3fa1 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -70,6 +70,8 @@ void force_town_enter(short which_town,location where_start) { town_force_loc = where_start; } +bool need_enter_town_autosave = false; + //short entry_dir; // if 9, go to forced void start_town_mode(short which_town, short entry_dir, bool debug_enter) { short town_number; @@ -335,8 +337,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { if(no_thrash.count(&monst) == 0) monst.active = eCreatureStatus::DEAD; } + bool specials_queued = false; if(!debug_enter) - handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); + specials_queued = handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); // Flush excess doomguards and viscous goos for(short i = 0; i < univ.town.monst.size(); i++) @@ -500,7 +503,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { // ... except it actually doesn't, because the town enter special is only queued, not run immediately. draw_terrain(1); - try_auto_save("EnterTown"); + // If special nodes still need to be called, we can't do the autosave yet. + if(specials_queued) need_enter_town_autosave = true; + else try_auto_save("EnterTown"); } @@ -627,8 +632,8 @@ location end_town_mode(bool switching_level,location destination, bool debug_lea return to_return; } -void handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { - queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); +bool handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { + return queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); } void handle_leave_town_specials(short /*town_number*/, short which_spec,location /*start_loc*/) { diff --git a/src/game/boe.town.hpp b/src/game/boe.town.hpp index 5916f7be1..437edde39 100644 --- a/src/game/boe.town.hpp +++ b/src/game/boe.town.hpp @@ -6,7 +6,7 @@ void force_town_enter(short which_town,location where_start); void start_town_mode(short which_town, short entry_dir, bool debug_enter = false); location end_town_mode(bool switching_level,location destination,bool debug_leave=false); // returns new party location void handle_leave_town_specials(short town_number, short which_spec,location start_loc) ; -void handle_town_specials(short town_number, bool town_dead,location start_loc) ; +bool handle_town_specials(short town_number, bool town_dead,location start_loc) ; bool abil_exists(eItemAbil abil); void start_town_combat(eDirection direction); From b64f2f42a10b880613788be063142ec07fb5311e Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 31 Jul 2025 14:37:20 -0500 Subject: [PATCH 80/86] add donor to credits --- pkg/credits/Funding.txt | 1 + rsrc/dialogs/about-boe.xml | 9 +++++---- rsrc/graphics/startanim.png | Bin 24905 -> 25086 bytes 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/credits/Funding.txt b/pkg/credits/Funding.txt index aca5479d8..905a2baf7 100644 --- a/pkg/credits/Funding.txt +++ b/pkg/credits/Funding.txt @@ -25,6 +25,7 @@ Confirmed: - K L - Laura Nelson - Mariann Krizsan +- Maryanne Wachter - Mike Lapinsky - Nathan Rickey - Nick Chaimov diff --git a/rsrc/dialogs/about-boe.xml b/rsrc/dialogs/about-boe.xml index f290fba02..ca7bc931a 100644 --- a/rsrc/dialogs/about-boe.xml +++ b/rsrc/dialogs/about-boe.xml @@ -18,11 +18,11 @@ ORIGINAL GAME:





- OPEN SOURCE CREDITS:











































































+ OPEN SOURCE CREDITS:












































































SCENARIO FIXES AND UPDATES:

- +
Concept, Design, Programming:
Graphics:
@@ -34,11 +34,11 @@ Graphics:






Consulting:



Testing and Troubleshooting:

- Funding:













































+ Funding:
















































Bandit Busywork:
- +
Jeff Vogel
Andrew Hunter
@@ -99,6 +99,7 @@ K L
Laura Nelson
Mariann Krizsan
+ Maryanne Wachter
Mike Lapinsky
Nathan Rickey
Nick Chaimov
diff --git a/rsrc/graphics/startanim.png b/rsrc/graphics/startanim.png index e4443d9a8181159680d516f2b316fcbfd71c81df..b10cb955fbc59aabd068fd33ea258189d1864f3f 100644 GIT binary patch delta 2874 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 delta 2681 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 From a60a39f65809f4889a8cd00029bdcded61c903d4 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Fri, 15 Aug 2025 09:43:36 -0500 Subject: [PATCH 81/86] add name to funding list --- pkg/credits/Funding.txt | 1 + rsrc/dialogs/about-boe.xml | 9 +++++---- rsrc/graphics/startanim.png | Bin 25086 -> 25115 bytes 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/credits/Funding.txt b/pkg/credits/Funding.txt index 905a2baf7..a5ef8089d 100644 --- a/pkg/credits/Funding.txt +++ b/pkg/credits/Funding.txt @@ -10,6 +10,7 @@ Confirmed: - Bret Rodabaugh - Dylan Nugent - Evan Mulrooney +- Glen Chudley - Jake Harrelson - Jared Forcinito - Jeff Potter diff --git a/rsrc/dialogs/about-boe.xml b/rsrc/dialogs/about-boe.xml index ca7bc931a..b703a0de1 100644 --- a/rsrc/dialogs/about-boe.xml +++ b/rsrc/dialogs/about-boe.xml @@ -18,11 +18,11 @@ ORIGINAL GAME:





- OPEN SOURCE CREDITS:












































































+ OPEN SOURCE CREDITS:













































































SCENARIO FIXES AND UPDATES:

- +
Concept, Design, Programming:
Graphics:
@@ -34,11 +34,11 @@ Graphics:






Consulting:



Testing and Troubleshooting:

- Funding:














































+ Funding:

















































Bandit Busywork:
- +
Jeff Vogel
Andrew Hunter
@@ -84,6 +84,7 @@ Bret Rodabaugh
Dylan Nugent
Evan Mulrooney
+ Glen Chudley
Jake Harrelson
Jared Forcinito
Jeff Potter
diff --git a/rsrc/graphics/startanim.png b/rsrc/graphics/startanim.png index b10cb955fbc59aabd068fd33ea258189d1864f3f..247de61ebbe305e7d7e4c27320eacbf7b42d4343 100644 GIT binary patch delta 21679 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 From 26377b38f56d4a91e9f47bb7dad53c0ed2875e58 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 16 Aug 2025 11:40:11 -0500 Subject: [PATCH 82/86] print success chance when fail breaking door or barrier --- src/game/boe.party.cpp | 24 ++++++++++++++++-------- src/game/boe.town.cpp | 38 ++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/game/boe.party.cpp b/src/game/boe.party.cpp index 2f3e20bf7..da23ecdfc 100644 --- a/src/game/boe.party.cpp +++ b/src/game/boe.party.cpp @@ -1384,13 +1384,17 @@ void cast_town_spell(location where) { case eSpell::UNLOCK: // TODO: Is the unlock spell supposed to have a max range? if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE){ + short success_chance = 0; + short total_modifier = 0; + short min_fail_roll = (135 - combat_percent[min(19,level)]); if(univ.scenario.ter_types[ter].flag2 == 10){ r1 = 10000; }else{ - r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town.door_diff_adjust(); - r1 += univ.scenario.ter_types[ter].flag2 * 7; + total_modifier = -5 * adj + 5 * univ.town.door_diff_adjust() + univ.scenario.ter_types[ter].flag2 * 7; + r1 = get_ran(1,1,100) + total_modifier; + success_chance = minmax(min_fail_roll - 1 - total_modifier, 0, 100); } - if(r1 < (135 - combat_percent[min(19,level)])) { + if(r1 < min_fail_roll) { add_string_to_buf(" Door unlocked."); play_sound(9); univ.town->terrain(where.x,where.y) = univ.scenario.ter_types[ter].flag1; @@ -1398,7 +1402,7 @@ void cast_town_spell(location where) { } else { play_sound(41); - add_string_to_buf(" Didn't work."); + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); } }else add_string_to_buf(" Wrong terrain type."); @@ -1406,10 +1410,13 @@ void cast_town_spell(location where) { case eSpell::DISPEL_BARRIER: if((univ.town.is_fire_barr(where.x,where.y)) || (univ.town.is_force_barr(where.x,where.y))) { - r1 = get_ran(1,1,100) - 5 * adj + 5 * (univ.town->difficulty / 10) + 25 * univ.town->strong_barriers; + short total_modifier = -5 * adj + 5 * (univ.town->difficulty / 10) + 25 * univ.town->strong_barriers; if(univ.town.is_fire_barr(where.x,where.y)) - r1 -= 8; - if(r1 < (120 - combat_percent[min(19,level)])) { + total_modifier -= 8; + short min_fail_roll = (120 - combat_percent[min(19,level)]); + short success_chance = minmax(min_fail_roll - 1 - total_modifier, 0, 100); + r1 = get_ran(1,1,100) + total_modifier; + if(r1 < min_fail_roll) { add_string_to_buf(" Barrier broken."); univ.town.set_fire_barr(where.x,where.y,false); univ.town.set_force_barr(where.x,where.y,false); @@ -1420,8 +1427,9 @@ void cast_town_spell(location where) { else { store = get_ran(1,0,1); (void) store; // TODO: Why does it even do this? + play_sound(41); - add_string_to_buf(" Didn't work."); + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); } } else if(univ.town.is_force_cage(where.x,where.y)) { add_string_to_buf(" Cage broken."); diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index f4fdd3fa1..95e7e5435 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -1144,23 +1144,30 @@ void pick_lock(location where,short pc_num) { return; } + // Roll to determine if the pick breaks r1 = get_ran(1,1,100) + which_item->abil_strength * 7; - if(r1 < 75) will_break = true; - r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town.door_diff_adjust() * 7 + // Roll to determine success or fail + short total_modifier = -5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town.door_diff_adjust() * 7 - 5 * univ.party[pc_num].skill(eSkill::LOCKPICKING) - which_item->abil_strength * 7; - // Nimble? if(univ.party[pc_num].traits[eTrait::NIMBLE]) - r1 -= 8; - + total_modifier -= 8; if(univ.party[pc_num].has_abil_equip(eItemAbil::THIEVING)) - r1 = r1 - 12; + total_modifier -= 12; + + r1 = get_ran(1,1,100) + total_modifier; + unlock_adjust = univ.scenario.ter_types[terrain].flag2; - if((unlock_adjust >= 5) || (r1 > (unlock_adjust * 15 + 30))) { - add_string_to_buf(" Didn't work."); + short success_chance = 0; + short max_success_roll = (unlock_adjust * 15 + 30); + if(unlock_adjust < 5){ + success_chance = minmax(max_success_roll - total_modifier, 0, 100); + } + if((unlock_adjust >= 5) || (r1 > max_success_roll)) { + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); if(will_break) { add_string_to_buf(" Pick breaks."); univ.party[pc_num].remove_charge(which_item.slot); @@ -1182,16 +1189,23 @@ void bash_door(location where,short pc_num) { short r1,unlock_adjust; terrain = univ.town->terrain(where.x,where.y); - r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town.door_diff_adjust() * 4; - + if(univ.scenario.ter_types[terrain].special != eTerSpec::UNLOCKABLE) { add_string_to_buf(" Wrong terrain type."); return; } + short total_modifier = -15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town.door_diff_adjust() * 4; unlock_adjust = univ.scenario.ter_types[terrain].flag2; - if(unlock_adjust >= 5 || r1 > (unlock_adjust * 15 + 40) || univ.scenario.ter_types[terrain].flag3 != 1) { - add_string_to_buf(" Didn't work."); + short max_success_roll = (unlock_adjust * 15 + 40); + + r1 = get_ran(1,1,100) + total_modifier; + bool success_chance = 0; + if(unlock_adjust < 5 && univ.scenario.ter_types[terrain].flag3 == 1){ + success_chance = minmax(max_success_roll - total_modifier, 0, 100); + } + if(unlock_adjust >= 5 || r1 > max_success_roll || univ.scenario.ter_types[terrain].flag3 != 1) { + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); damage_pc(univ.party[pc_num],get_ran(1,1,4),eDamageType::SPECIAL,eRace::UNKNOWN); } else { From 3808a82d5046bbe348d0c62869924049af6598db Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Sat, 16 Aug 2025 14:12:29 -0500 Subject: [PATCH 83/86] has_class require_charges allow item w/ 0 max charges --- src/universe/pc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/universe/pc.cpp b/src/universe/pc.cpp index 3bd86faa7..5587976fa 100644 --- a/src/universe/pc.cpp +++ b/src/universe/pc.cpp @@ -846,7 +846,7 @@ const cInvenSlot cPlayer::has_class_equip(unsigned int item_class) const { cInvenSlot cPlayer::has_class(unsigned int item_class, bool require_charges) { return find_item_matching([item_class, require_charges](int, const cItem& item) { - return item.special_class == item_class && (!require_charges || item.charges > 0); + return item.special_class == item_class && (!require_charges || item.charges > 0 || item.max_charges == 0); }); } From 6dfb49e73edbf5c00f314cb6ce1650fbe3c043df Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 18 Aug 2025 11:45:12 -0500 Subject: [PATCH 84/86] Fix rendering UTF-8 text --- src/game/boe.text.cpp | 2 +- src/gfx/render_text.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/game/boe.text.cpp b/src/game/boe.text.cpp index c3d86fcbc..f579ba7b1 100644 --- a/src/game/boe.text.cpp +++ b/src/game/boe.text.cpp @@ -1129,7 +1129,7 @@ void print_buf () { line_style.applyTo(text, get_ui_scale()); // A spacing factor of 1.0 within multiline messages doesn't actually line up with other single buffer lines text.setLineSpacing(0.85); - text.setString(message); + text.setString(sf::String::fromUtf8(message.begin(), message.end())); text.setPosition(moveTo); draw_scale_aware_text(text_area_gworld(), text); } diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index a2445a0b2..8a7e2702c 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -129,7 +129,7 @@ break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextS short str_len = str.length(); unsigned short last_line_break = 0,last_word_break = 0; - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); // Even if the text is only one line, break_info is required for calculating word boundaries. // So we can't skip the rest of this. @@ -267,7 +267,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st for(auto it : substitutions){ boost::replace_all(str, it.first, it.second); } - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); short total_width = str_to_draw.getLocalBounds().width; options.style.applyTo(str_to_draw, get_ui_scale()); @@ -341,7 +341,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st } for(auto& snippet : options.snippets) { - str_to_draw.setString(snippet.text); + str_to_draw.setString(sf::String::fromUtf8(snippet.text.begin(), snippet.text.end())); str_to_draw.setPosition(snippet.at); if(snippet.hilited) { rectangle bounds = str_to_draw.getGlobalBounds(); @@ -413,7 +413,7 @@ size_t string_length(std::string str, const TextStyle& style, short* height){ sf::Text text; style.applyTo(text); - text.setString(str); + text.setString(sf::String::fromUtf8(str.begin(), str.end())); total_width = text.getLocalBounds().width; if(strings_to_cache.count(str)){ location measurement; From 76d601dc4f091c8f30f2f9bb66abc4dc6abafd4b Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 18 Aug 2025 11:48:32 -0500 Subject: [PATCH 85/86] substitutions no longer needed --- src/game/boe.newgraph.cpp | 6 ------ src/gfx/render_text.cpp | 15 --------------- 2 files changed, 21 deletions(-) diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index 2f029713e..01d0fd07f 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -1012,12 +1012,6 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col // The added spaces ensure that end-of-word boundaries are found std::string str = str_to_place + " |" + str_to_place2 + " "; - // TODO use a font where we don't need this - extern std::map substitutions; - for(auto it : substitutions){ - boost::replace_all(str, it.first, it.second); - } - std::vector hilites; std::vector nodes; int wordStart = 0, wordEnd = 0; diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index 8a7e2702c..c374d30ff 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -108,22 +108,10 @@ static void push_snippets(size_t start, size_t end, text_params_t& options, size } while(start < upper_bound); } -std::map substitutions = { - {"–", "--"}, - {"´", "'"}, - {"©", "(C)"}, - {"…", "..."}, - {"™", "TM"} -}; - break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextStyle style) { break_info_t break_info; if(str.empty()) return break_info; // Nothing to do! - for(auto it : substitutions){ - boost::replace_all(str, it.first, it.second); - } - sf::Text str_to_draw; style.applyTo(str_to_draw); short str_len = str.length(); @@ -264,9 +252,6 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st // TODO: Why the heck are we drawing a whole line higher than requested!? adjust_y -= str_to_draw.getLocalBounds().height; - for(auto it : substitutions){ - boost::replace_all(str, it.first, it.second); - } str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); short total_width = str_to_draw.getLocalBounds().width; From a1a89d58129a994aa2da88b3b7a028ded812670b Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Mon, 18 Aug 2025 14:04:12 -0500 Subject: [PATCH 86/86] try allowing implicit fallthrough in xcode --- proj/xc12/BoE.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proj/xc12/BoE.xcodeproj/project.pbxproj b/proj/xc12/BoE.xcodeproj/project.pbxproj index 9e5c93470..11ed9d86b 100755 --- a/proj/xc12/BoE.xcodeproj/project.pbxproj +++ b/proj/xc12/BoE.xcodeproj/project.pbxproj @@ -2506,7 +2506,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Debug; @@ -2620,7 +2620,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Release;