Initial commit

This commit is contained in:
Alexander Batalov 2023-02-09 22:13:05 +03:00
commit 7520167b33
300 changed files with 145631 additions and 0 deletions

2
.clang-format Normal file
View File

@ -0,0 +1,2 @@
BasedOnStyle: WebKit
AllowShortIfStatementsOnASingleLine: WithoutElse

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_white_space = true

24
.gitattributes vendored Normal file
View File

@ -0,0 +1,24 @@
# Force LF
*.c text eol=lf
*.cc text eol=lf
*.cmake text eol=lf
*.gradle text eol=lf
*.h text eol=lf
*.java text eol=lf
*.json text eol=lf
*.md text eol=lf
*.plist text eol=lf
*.pro text eol=lf
*.properties text eol=lf
*.xml text eol=lf
*.yml text eol=lf
.clang-format text eol=lf
.editorconfig text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf
gradlew text eol=lf
CMakeLists.txt text eol=lf
LICENSE text eol=lf
# Force CRLF
*.bat text eol=crlf

271
.github/workflows/ci-build.yml vendored Normal file
View File

@ -0,0 +1,271 @@
name: Build
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
defaults:
run:
shell: bash
jobs:
static-analysis:
name: Static analysis
runs-on: ubuntu-latest
steps:
- name: Install
run: |
sudo apt update
sudo apt install cppcheck
- name: Clone
uses: actions/checkout@v3
- name: cppcheck
run: cppcheck --std=c++17 src/
android:
name: Android
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 11
cache: gradle
- name: Cache cmake build
uses: actions/cache@v3
with:
path: os/android/app/.cxx
key: android-cmake-v1
- name: Setup signing config
if: env.KEYSTORE_FILE_BASE64 != '' && env.KEYSTORE_PROPERTIES_FILE_BASE64 != ''
run: |
cd os/android
echo "$KEYSTORE_FILE_BASE64" | base64 --decode > debug.keystore
echo "$KEYSTORE_PROPERTIES_FILE_BASE64" | base64 --decode > debug-keystore.properties
env:
KEYSTORE_FILE_BASE64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_FILE_BASE64 }}
KEYSTORE_PROPERTIES_FILE_BASE64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_PROPERTIES_FILE_BASE64 }}
- name: Build
run: |
cd os/android
./gradlew assembleDebug
- name: Upload
uses: actions/upload-artifact@v3
with:
name: fallout-ce-debug.apk
path: os/android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 7
ios:
name: iOS
runs-on: macos-11
steps:
- name: Clone
uses: actions/checkout@v3
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: ios-cmake-v1
- name: Configure
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D CMAKE_TOOLCHAIN_FILE=cmake/toolchain/ios.toolchain.cmake \
-D ENABLE_BITCODE=0 \
-D PLATFORM=OS64 \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(sysctl -n hw.physicalcpu) \
--target package \
# EOL
# TODO: Should be a part of packaging.
- name: Prepare for uploading
run: |
cp build/fallout-ce.zip build/fallout-ce.ipa
- name: Upload
uses: actions/upload-artifact@v3
with:
name: fallout-ce.ipa
path: build/fallout-ce.ipa
retention-days: 7
linux:
name: Linux (${{ matrix.arch }})
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
arch:
- x86
- x64
steps:
- name: Clone
uses: actions/checkout@v3
- name: Dependencies (x86)
if: matrix.arch == 'x86'
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install --allow-downgrades libpcre2-8-0=10.34-7
sudo apt install g++-multilib libsdl2-dev:i386 zlib1g-dev:i386
- name: Dependencies (x64)
if: matrix.arch == 'x64'
run: |
sudo apt update
sudo apt install libsdl2-dev zlib1g-dev
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: linux-${{ matrix.arch }}-cmake-v1
- name: Configure (x86)
if: matrix.arch == 'x86'
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D CMAKE_TOOLCHAIN_FILE=cmake/toolchain/Linux32.cmake \
# EOL
- name: Configure (x64)
if: matrix.arch == 'x64'
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(nproc) \
# EOL
- name: Upload
uses: actions/upload-artifact@v3
with:
name: fallout-ce-linux-${{ matrix.arch }}
path: build/fallout-ce
retention-days: 7
macos:
name: macOS
runs-on: macos-11
steps:
- name: Clone
uses: actions/checkout@v3
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: macos-cmake-v3
- name: Configure
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(sysctl -n hw.physicalcpu) \
--target package \
# EOL
- name: Upload
uses: actions/upload-artifact@v3
with:
name: fallout-ce-macos.dmg
path: build/fallout-ce.dmg
retention-days: 7
windows:
name: Windows (${{ matrix.arch }})
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
include:
- arch: x86
generator-platform: Win32
- arch: x64
generator-platform: x64
steps:
- name: Clone
uses: actions/checkout@v3
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: windows-${{ matrix.arch }}-cmake-v1
- name: Configure
run: |
cmake \
-B build \
-G "Visual Studio 16 2019" \
-A ${{ matrix.generator-platform }} \
# EOL
- name: Build
run: |
cmake \
--build build \
--config RelWithDebInfo \
# EOL
- name: Upload
uses: actions/upload-artifact@v3
with:
name: fallout-ce-windows-${{ matrix.arch }}
path: build/RelWithDebInfo/fallout-ce.exe
retention-days: 7

280
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,280 @@
name: Release
on:
release:
types:
- published
defaults:
run:
shell: bash
jobs:
android:
name: Android
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 11
cache: gradle
- name: Cache cmake build
uses: actions/cache@v3
with:
path: os/android/app/.cxx
key: android-cmake-v1
- name: Setup signing config
if: env.KEYSTORE_FILE_BASE64 != '' && env.KEYSTORE_PROPERTIES_FILE_BASE64 != ''
run: |
cd os/android
echo "$KEYSTORE_FILE_BASE64" | base64 --decode > release.keystore
echo "$KEYSTORE_PROPERTIES_FILE_BASE64" | base64 --decode > release-keystore.properties
env:
KEYSTORE_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_FILE_BASE64 }}
KEYSTORE_PROPERTIES_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PROPERTIES_FILE_BASE64 }}
- name: Build
run: |
cd os/android
./gradlew assembleRelease
- name: Upload
run: |
cd os/android/app/build/outputs/apk/release
cp app-release.apk fallout-ce-android.apk
gh release upload ${{ github.ref_name }} fallout-ce-android.apk
rm fallout-ce-android.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ios:
name: iOS
runs-on: macos-11
steps:
- name: Clone
uses: actions/checkout@v3
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: ios-cmake-v1
- name: Configure
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D CMAKE_TOOLCHAIN_FILE=cmake/toolchain/ios.toolchain.cmake \
-D ENABLE_BITCODE=0 \
-D PLATFORM=OS64 \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(sysctl -n hw.physicalcpu) \
--target package \
# EOL
- name: Upload
run: |
cd build
cp fallout-ce.zip fallout-ce-ios.ipa
gh release upload ${{ github.ref_name }} fallout-ce-ios.ipa
rm fallout-ce-ios.ipa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linux:
name: Linux (${{ matrix.arch }})
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
arch:
- x86
- x64
steps:
- name: Clone
uses: actions/checkout@v3
- name: Dependencies (x86)
if: matrix.arch == 'x86'
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install --allow-downgrades libpcre2-8-0=10.34-7
sudo apt install g++-multilib libsdl2-dev:i386 zlib1g-dev:i386
- name: Dependencies (x64)
if: matrix.arch == 'x64'
run: |
sudo apt update
sudo apt install libsdl2-dev zlib1g-dev
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: linux-${{ matrix.arch }}-cmake-v1
- name: Configure (x86)
if: matrix.arch == 'x86'
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D CMAKE_TOOLCHAIN_FILE=cmake/toolchain/Linux32.cmake \
# EOL
- name: Configure (x64)
if: matrix.arch == 'x64'
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(nproc) \
# EOL
- name: Upload
run: |
cd build
tar -czvf fallout-ce-linux-${{ matrix.arch }}.tar.gz fallout-ce
gh release upload ${{ github.ref_name }} fallout-ce-linux-${{ matrix.arch }}.tar.gz
rm fallout-ce-linux-${{ matrix.arch }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
macos:
name: macOS
runs-on: macos-11
steps:
- name: Clone
uses: actions/checkout@v3
- name: Import code signing certificates
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_FILE_BASE64 }}
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_PASSWORD }}
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: macos-cmake-v3
- name: Configure
run: |
cmake \
-B build \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D CPACK_BUNDLE_APPLE_CERT_APP="${{ secrets.APPLE_DEVELOPER_CERTIFICATE_IDENTITY }}" \
# EOL
- name: Build
run: |
cmake \
--build build \
-j $(sysctl -n hw.physicalcpu) \
--target package \
# EOL
- name: Notarize
run: |
brew install mitchellh/gon/gon
cat << EOF > config.json
{
"notarize": {
"path": "build/fallout-ce.dmg",
"bundle_id": "$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" build/fallout-ce.app/Contents/Info.plist)",
"staple": true
}
}
EOF
gon config.json
rm config.json
env:
AC_USERNAME: ${{ secrets.APPLE_DEVELOPER_AC_USERNAME }}
AC_PASSWORD: ${{ secrets.APPLE_DEVELOPER_AC_PASSWORD }}
- name: Upload
run: |
cd build
cp fallout-ce.dmg fallout-ce-macos.dmg
gh release upload ${{ github.ref_name }} fallout-ce-macos.dmg
rm fallout-ce-macos.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
windows:
name: Windows (${{ matrix.arch }})
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
include:
- arch: x86
generator-platform: Win32
- arch: x64
generator-platform: x64
steps:
- name: Clone
uses: actions/checkout@v3
- name: Cache cmake build
uses: actions/cache@v3
with:
path: build
key: windows-${{ matrix.arch }}-cmake-v1
- name: Configure
run: |
cmake \
-B build \
-G "Visual Studio 16 2019" \
-A ${{ matrix.generator-platform }} \
# EOL
- name: Build
run: |
cmake \
--build build \
--config RelWithDebInfo \
# EOL
- name: Upload
run: |
cd build/RelWithDebInfo
7z a fallout-ce-windows-${{ matrix.arch }}.zip fallout-ce.exe
gh release upload ${{ github.ref_name }} fallout-ce-windows-${{ matrix.arch }}.zip
rm fallout-ce-windows-${{ matrix.arch }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

393
.gitignore vendored Normal file
View File

@ -0,0 +1,393 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Dd]ebug-Remote/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
.idea/
*.sln.iml
# CMake
/out
/build

358
CMakeLists.txt Normal file
View File

@ -0,0 +1,358 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(EXECUTABLE_NAME fallout-ce)
if (APPLE)
if(IOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11" CACHE STRING "")
set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "")
else()
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.11" CACHE STRING "")
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "")
endif()
endif()
project(${EXECUTABLE_NAME})
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
if (ANDROID)
add_library(${EXECUTABLE_NAME} SHARED)
else()
add_executable(${EXECUTABLE_NAME} WIN32 MACOSX_BUNDLE)
endif()
target_include_directories(${EXECUTABLE_NAME} PUBLIC src)
target_sources(${EXECUTABLE_NAME} PUBLIC
"src/game/actions.cc"
"src/game/actions.h"
"src/game/amutex.cc"
"src/game/amutex.h"
"src/game/anim.cc"
"src/game/anim.h"
"src/game/art.cc"
"src/game/art.h"
"src/game/automap.cc"
"src/game/automap.h"
"src/game/bmpdlog.cc"
"src/game/bmpdlog.h"
"src/game/cache.cc"
"src/game/cache.h"
"src/game/combat_defs.h"
"src/game/combat.cc"
"src/game/combat.h"
"src/game/combatai.cc"
"src/game/combatai.h"
"src/game/config.cc"
"src/game/config.h"
"src/game/counter.cc"
"src/game/counter.h"
"src/game/credits.cc"
"src/game/credits.h"
"src/game/critter.cc"
"src/game/critter.h"
"src/game/cycle.cc"
"src/game/cycle.h"
"src/game/display.cc"
"src/game/display.h"
"src/game/editor.cc"
"src/game/editor.h"
"src/game/elevator.cc"
"src/game/elevator.h"
"src/game/endgame.cc"
"src/game/endgame.h"
"src/game/fontmgr.cc"
"src/game/fontmgr.h"
"src/game/game_vars.h"
"src/game/game.cc"
"src/game/game.h"
"src/game/gconfig.cc"
"src/game/gconfig.h"
"src/game/gdebug.cc"
"src/game/gdebug.h"
"src/game/gdialog.cc"
"src/game/gdialog.h"
"src/game/gmemory.cc"
"src/game/gmemory.h"
"src/game/gmouse.cc"
"src/game/gmouse.h"
"src/game/gmovie.cc"
"src/game/gmovie.h"
"src/game/graphlib.cc"
"src/game/graphlib.h"
"src/game/gsound.cc"
"src/game/gsound.h"
"src/game/heap.cc"
"src/game/heap.h"
"src/game/intface.cc"
"src/game/intface.h"
"src/game/inventry.cc"
"src/game/inventry.h"
"src/game/item.cc"
"src/game/item.h"
"src/game/light.cc"
"src/game/light.h"
"src/game/lip_sync.cc"
"src/game/lip_sync.h"
"src/game/loadsave.cc"
"src/game/loadsave.h"
"src/game/main.cc"
"src/game/main.h"
"src/game/mainmenu.cc"
"src/game/mainmenu.h"
"src/game/map_defs.h"
"src/game/map.cc"
"src/game/map.h"
"src/game/message.cc"
"src/game/message.h"
"src/game/moviefx.cc"
"src/game/moviefx.h"
"src/game/object_types.h"
"src/game/object.cc"
"src/game/object.h"
"src/game/options.cc"
"src/game/options.h"
"src/game/palette.cc"
"src/game/palette.h"
"src/game/party.cc"
"src/game/party.h"
"src/game/perk_defs.h"
"src/game/perk.cc"
"src/game/perk.h"
"src/game/pipboy.cc"
"src/game/pipboy.h"
"src/game/protinst.cc"
"src/game/protinst.h"
"src/game/proto_types.h"
"src/game/proto.cc"
"src/game/proto.h"
"src/game/queue.cc"
"src/game/queue.h"
"src/game/reaction.cc"
"src/game/reaction.h"
"src/game/roll.cc"
"src/game/roll.h"
"src/game/scripts.cc"
"src/game/scripts.h"
"src/game/select.cc"
"src/game/select.h"
"src/game/selfrun.cc"
"src/game/selfrun.h"
"src/game/sfxcache.cc"
"src/game/sfxcache.h"
"src/game/sfxlist.cc"
"src/game/sfxlist.h"
"src/game/skill_defs.h"
"src/game/skill.cc"
"src/game/skill.h"
"src/game/skilldex.cc"
"src/game/skilldex.h"
"src/game/stat_defs.h"
"src/game/stat.cc"
"src/game/stat.h"
"src/game/textobj.cc"
"src/game/textobj.h"
"src/game/tile.cc"
"src/game/tile.h"
"src/game/trait.cc"
"src/game/trait.h"
"src/game/version.cc"
"src/game/version.h"
"src/game/wordwrap.cc"
"src/game/wordwrap.h"
"src/game/worldmap_walkmask.cc"
"src/game/worldmap_walkmask.h"
"src/game/worldmap.cc"
"src/game/worldmap.h"
"src/int/audio.cc"
"src/int/audio.h"
"src/int/audiof.cc"
"src/int/audiof.h"
"src/int/datafile.cc"
"src/int/datafile.h"
"src/int/dialog.cc"
"src/int/dialog.h"
"src/int/export.cc"
"src/int/export.h"
"src/int/intlib.cc"
"src/int/intlib.h"
"src/int/intrpret.cc"
"src/int/intrpret.h"
"src/int/memdbg.cc"
"src/int/memdbg.h"
"src/int/mousemgr.cc"
"src/int/mousemgr.h"
"src/int/movie.cc"
"src/int/movie.h"
"src/int/nevs.cc"
"src/int/nevs.h"
"src/int/pcx.cc"
"src/int/pcx.h"
"src/int/region.cc"
"src/int/region.h"
"src/int/share1.cc"
"src/int/share1.h"
"src/int/sound.cc"
"src/int/sound.h"
"src/int/support/intextra.cc"
"src/int/support/intextra.h"
"src/int/widget.cc"
"src/int/widget.h"
"src/int/window.cc"
"src/int/window.h"
"src/plib/assoc/assoc.cc"
"src/plib/assoc/assoc.h"
"src/plib/color/color.cc"
"src/plib/color/color.h"
"src/plib/db/db.cc"
"src/plib/db/db.h"
"src/plib/db/lzss.cc"
"src/plib/db/lzss.h"
"src/plib/gnw/button.cc"
"src/plib/gnw/button.h"
"src/plib/gnw/debug.cc"
"src/plib/gnw/debug.h"
"src/plib/gnw/dxinput.cc"
"src/plib/gnw/dxinput.h"
"src/plib/gnw/grbuf.cc"
"src/plib/gnw/grbuf.h"
"src/plib/gnw/input.cc"
"src/plib/gnw/input.h"
"src/plib/gnw/gnw_types.h"
"src/plib/gnw/gnw.cc"
"src/plib/gnw/gnw.h"
"src/plib/gnw/intrface.cc"
"src/plib/gnw/intrface.h"
"src/plib/gnw/kb.cc"
"src/plib/gnw/kb.h"
"src/plib/gnw/memory.cc"
"src/plib/gnw/memory.h"
"src/plib/gnw/mmx.cc"
"src/plib/gnw/mmx.h"
"src/plib/gnw/mouse.cc"
"src/plib/gnw/mouse.h"
"src/plib/gnw/rect.cc"
"src/plib/gnw/rect.h"
"src/plib/gnw/svga_types.h"
"src/plib/gnw/svga.cc"
"src/plib/gnw/svga.h"
"src/plib/gnw/text.cc"
"src/plib/gnw/text.h"
"src/plib/gnw/vcr.cc"
"src/plib/gnw/vcr.h"
"src/plib/gnw/winmain.cc"
"src/plib/gnw/winmain.h"
"src/movie_lib.cc"
"src/movie_lib.h"
"src/sound_decoder.cc"
"src/sound_decoder.h"
)
target_sources(${EXECUTABLE_NAME} PUBLIC
"src/audio_engine.cc"
"src/audio_engine.h"
"src/fps_limiter.cc"
"src/fps_limiter.h"
"src/platform_compat.cc"
"src/platform_compat.h"
"src/pointer_registry.cc"
"src/pointer_registry.h"
)
if(IOS)
target_sources(${EXECUTABLE_NAME} PUBLIC
"src/platform/ios/paths.h"
"src/platform/ios/paths.mm"
)
endif()
if(WIN32)
target_compile_definitions(${EXECUTABLE_NAME} PUBLIC
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_WARNINGS
NOMINMAX
WIN32_LEAN_AND_MEAN
)
endif()
if(WIN32)
target_link_libraries(${EXECUTABLE_NAME}
winmm
)
endif()
if (WIN32)
target_sources(${EXECUTABLE_NAME} PUBLIC
"os/windows/fallout-ce.ico"
"os/windows/fallout-ce.rc"
)
endif()
if(APPLE)
target_sources(${EXECUTABLE_NAME} PUBLIC "os/macos/fallout-ce.icns")
set_source_files_properties("os/macos/fallout-ce.icns" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
if(IOS)
target_sources(${EXECUTABLE_NAME} PUBLIC "os/ios/LaunchScreen.storyboard")
set_source_files_properties("os/ios/LaunchScreen.storyboard" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
set_target_properties(${EXECUTABLE_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/os/ios/Info.plist")
set_target_properties(${EXECUTABLE_NAME} PROPERTIES XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2")
else()
set_target_properties(${EXECUTABLE_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/os/macos/Info.plist")
endif()
set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.alexbatalov.fallout-ce")
set(MACOSX_BUNDLE_BUNDLE_NAME "${EXECUTABLE_NAME}")
set(MACOSX_BUNDLE_ICON_FILE "fallout-ce.icns")
set(MACOSX_BUNDLE_DISPLAY_NAME "Fallout")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0.0")
set(MACOSX_BUNDLE_BUNDLE_VERSION "1.0.0")
endif()
if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Linux")
add_subdirectory("third_party/sdl2")
else()
find_package(SDL2)
endif()
add_subdirectory("third_party/fpattern")
target_link_libraries(${EXECUTABLE_NAME} ${FPATTERN_LIBRARY})
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${FPATTERN_INCLUDE_DIR})
target_link_libraries(${EXECUTABLE_NAME} ${SDL2_LIBRARIES})
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${SDL2_INCLUDE_DIRS})
if(APPLE)
if(IOS)
install(TARGETS ${EXECUTABLE_NAME} DESTINATION "Payload")
set(CPACK_GENERATOR "ZIP")
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)
set(CPACK_PACKAGE_FILE_NAME "fallout-ce")
else()
install(TARGETS ${EXECUTABLE_NAME} DESTINATION .)
install(CODE "
include(BundleUtilities)
fixup_bundle(${CMAKE_BINARY_DIR}/${MACOSX_BUNDLE_BUNDLE_NAME}.app \"\" \"\")
"
COMPONENT Runtime)
if (CPACK_BUNDLE_APPLE_CERT_APP)
install(CODE "
execute_process(COMMAND codesign --deep --force --options runtime --sign \"${CPACK_BUNDLE_APPLE_CERT_APP}\" ${CMAKE_BINARY_DIR}/${MACOSX_BUNDLE_BUNDLE_NAME}.app)
"
COMPONENT Runtime)
endif()
set(CPACK_GENERATOR "DragNDrop")
set(CPACK_DMG_DISABLE_APPLICATIONS_SYMLINK ON)
set(CPACK_PACKAGE_FILE_NAME "fallout-ce")
endif()
include(CPack)
endif()

50
CMakeSettings.json Normal file
View File

@ -0,0 +1,50 @@
{
"configurations": [
{
"name": "x86-Debug",
"generator": "Visual Studio 16 2019",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x86" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
},
{
"name": "x86-Release",
"generator": "Visual Studio 16 2019",
"configurationType": "Release",
"inheritEnvironments": [ "msvc_x86" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
},
{
"name": "x64-Debug",
"generator": "Visual Studio 16 2019 Win64",
"configurationType": "Debug",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x64_x64" ],
"variables": []
},
{
"name": "x64-Release",
"generator": "Visual Studio 16 2019 Win64",
"configurationType": "Release",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x64_x64" ],
"variables": []
}
]
}

51
LICENSE.md Normal file
View File

@ -0,0 +1,51 @@
# Sustainable Use License
Version 1.0
## Acceptance
By using the software, you agree to all of the terms and conditions below.
## Copyright License
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
## Limitations
You may use or modify the software only for your own internal business purposes or for non-commercial or personal use. You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensors trademarks is subject to applicable law.
## Patents
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software.
## No Other Rights
These terms do not imply any licenses other than those expressly granted in these terms.
## Termination
If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
## No Liability
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
## Definitions
The "licensor" is the entity offering these terms.
The "software" is the software the licensor makes available under these terms, including any portion of it.
"You" refers to the individual or entity agreeing to these terms.
"Your company" is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
"Your license" is the license granted to you for the software under these terms.
"Use" means anything you do with the software requiring your license.
"Trademark" means trademarks, service marks, and similar rights.

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# Fallout Community Edition
Fallout Community Edition is a fully working open source re-implementation of Fallout, with the same original gameplay, engine bugfixes, and some quality of life improvements, that works (mostly) hassle-free on multiple platforms.
## Installation
You must own the game to play. Purchase your copy on [GOG](https://www.gog.com/game/fallout) or [Steam](https://store.steampowered.com/app/38400). Download latest release or build from source.
### Windows
Download and copy `fallout-ce.exe` to your `Fallout` folder. It serves as a drop-in replacement for `falloutw.exe`.
### Linux
- Use Windows installation as a base - it contains data assets needed to play. Copy `Fallout` folder somewhere, for example `/home/john/Desktop/Fallout`.
- Download and copy `fallout-ce` to this folder.
- Install [SDL2](https://libsdl.org/download-2.0.php):
```console
$ sudo apt install libsdl2-2.0-0
```
- Run `./fallout-ce`.
### macOS
> **NOTE**: macOS 10.11 (El Capitan) or higher is required. Runs natively on Intel-based Macs and Apple Silicon.
- Use Windows installation as a base - it contains data assets needed to play. Copy `Fallout` folder somewhere, for example `/Applications/Fallout`.
- Alternatively you can use Fallout from MacPlay/The Omni Group as a base - you need to extract game assets from the original bundle. Mount CD/DMG, right click `Fallout` -> `Show Package Contents`, navigate to `Contents/Resources`. Copy `GameData` folder somewhere, for example `/Applications/Fallout`.
- Download and copy `fallout-ce.app` to this folder.
- Run `fallout-ce.app`.
### Android
> **NOTE**: Fallout was designed with mouse in mind. There are many controls that require precise cursor positioning, which is not possible with fingers. When playing on Android you'll use fingers to move mouse cursor, not a character, or a map. Double tap to "click" left mouse button in the current cursor position, triple tap to "click" right mouse button. It might feel awkward at first, but it's super handy - you can play with just a thumb. This is not set in stone and might change in the future.
- Use Windows installation as a base - it contains data assets needed to play. Copy `Fallout` folder to your device, for example to `Downloads`. You need `master.dat`, `critter.dat`, and `data` folder.
- Download `fallout-ce.apk` and copy it to your device. Open it with file explorer, follow instructions (install from unknown source).
- When you run the game for the first time it will immediately present file picker. Select the folder from the first step. Wait until this data is copied. A loading dialog will appear, just wait for about 30 seconds. The game will start automatically.
### iOS
> **NOTE**: See Android note on controls.
- Download `fallout-ce.ipa`. Use sideloading applications ([AltStore](https://altstore.io/) or [Sideloadly](https://sideloadly.io/)) to install it to your device. Alternatively you can always build from source with your own signing certificate.
- Run the game once. You'll see error message saying "Couldn't find/load text fonts". This step is needed for iOS to expose the game via File Sharing feature.
- Use Finder (macOS Catalina and later) or iTunes (Windows and macOS Mojave or earlier) to copy `master.dat`, `critter.dat`, and `data` folder to "Fallout" app ([how-to](https://support.apple.com/HT210598)).
## Contributing
Here is a couple of current goals. Open up an issue if you have suggestion or feature request.
- **Update to v1.2**. This project is based on Reference Edition which implements v1.1 released in November 1997. There is a newer v1.2 released in March 1998 which at least contains important multilingual support.
- **Backport some Fallout 2 features**. Fallout 2 (with some Sfall additions) added many great improvements and quality of life enhancements to the original Fallout engine. Many deserve to be backported to Fallout 1. Keep in mind this is a different game, with slightly different gameplay balance (which is a fragile thing on its own).
## License
The source code is this repository is available under the [Sustainable Use License](LICENSE.md).

View File

@ -0,0 +1,4 @@
set( CMAKE_SYSTEM_NAME "Linux" )
set( CMAKE_SYSTEM_PROCESSOR "i386" )
set( CMAKE_C_FLAGS "-m32" )
set( CMAKE_CXX_FLAGS "-m32" )

File diff suppressed because it is too large Load Diff

13
os/android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*.iml
.gradle
/.idea/assetWizardSettings.xml
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/navEditor.xml
/.idea/workspace.xml
/local.properties
/debug-keystore.properties
/debug.keystore
/release-keystore.properties
/release.keystore

5
os/android/app/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/.cxx
/build
# TODO: Cleanup root .gitignore
!/src/debug

100
os/android/app/build.gradle Normal file
View File

@ -0,0 +1,100 @@
plugins {
id 'com.android.application'
}
android {
compileSdk 32
defaultConfig {
applicationId 'com.alexbatalov.falloutce'
minSdk 21
targetSdk 32
versionCode 3
versionName '1.0.0'
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
// TODO: Remove once format issues are resolved.
cppFlags '-Wno-format-security'
// Specify target library explicitly as there is a shared zlib,
// that we don't need to be linked in.
targets 'fallout-ce'
}
}
}
signingConfigs {
// Override default debug signing config to make sure every CI runner
// uses the same key for painless updates.
def debugKeystorePropertiesFile = rootProject.file('debug-keystore.properties')
if (debugKeystorePropertiesFile.exists()) {
def debugKeystoreProperties = new Properties()
debugKeystoreProperties.load(new FileInputStream(debugKeystorePropertiesFile))
debug {
storeFile rootProject.file(debugKeystoreProperties.getProperty('storeFile'))
storePassword debugKeystoreProperties.getProperty('storePassword')
keyAlias debugKeystoreProperties.getProperty('keyAlias')
keyPassword debugKeystoreProperties.getProperty('keyPassword')
}
}
def releaseKeystoreProperties = new Properties()
def releaseKeystorePropertiesFile = rootProject.file('release-keystore.properties')
if (releaseKeystorePropertiesFile.exists()) {
releaseKeystoreProperties.load(new FileInputStream(releaseKeystorePropertiesFile))
release {
storeFile rootProject.file(releaseKeystoreProperties.getProperty('storeFile'))
storePassword releaseKeystoreProperties.getProperty('storePassword')
keyAlias releaseKeystoreProperties.getProperty('keyAlias')
keyPassword releaseKeystoreProperties.getProperty('keyPassword')
}
}
}
buildTypes {
debug {
// Prevents signing keys clashes between debug and release versions
// for painless development.
applicationIdSuffix '.debug'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// Release signing config is optional and might not be present in CI
// builds, hence `findByName`.
signingConfig signingConfigs.findByName('release')
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
applicationVariants.all { variant ->
tasks["merge${variant.name.capitalize()}Assets"]
.dependsOn("externalNativeBuild${variant.name.capitalize()}")
}
if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) {
sourceSets.main {
jniLibs.srcDir 'libs'
}
externalNativeBuild {
cmake {
path '../../../CMakeLists.txt'
}
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.documentfile:documentfile:1.0.1'
}

21
os/android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Fallout (Debug)</string>
</resources>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.alexbatalov.falloutce"
android:versionCode="1"
android:versionName="1.0.0"
android:installLocation="auto">
<!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" />
<!-- Touchscreen support -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- Game controller support -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- External mouse input events -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false" />
<!-- Audio recording support -->
<!-- if you want to capture audio, uncomment this. -->
<!-- <uses-feature
android:name="android.hardware.microphone"
android:required="false" /> -->
<!-- Allow downloading to the external storage on Android 5.1 and older -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" />
<!-- Allow access to Bluetooth devices -->
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> -->
<!-- Allow access to the vibrator -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- if you want to capture audio, uncomment this. -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
<!-- Create a Java class extending SDLActivity and place it in a
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
in the XML below.
An example Java class can be found in README-android.md
-->
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true" >
<!-- Example of setting SDL hints from AndroidManifest.xml:
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
-->
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:alwaysRetainTaskState="true"
android:launchMode="singleInstance"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:exported="true"
android:screenOrientation="landscape"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Let Android know that we can handle some USB devices and should receive this event -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<!-- Drop file event -->
<!--
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
-->
</activity>
<activity android:name=".ImportActivity"
android:theme="@style/AppTheme">
</activity>
</application>
</manifest>

View File

@ -0,0 +1,56 @@
package com.alexbatalov.falloutce;
import android.content.ContentResolver;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FileUtils {
static boolean copyRecursively(ContentResolver contentResolver, DocumentFile src, File dest) {
final DocumentFile[] documentFiles = src.listFiles();
for (final DocumentFile documentFile : documentFiles) {
if (documentFile.isFile()) {
if (!copyFile(contentResolver, documentFile, new File(dest, documentFile.getName()))) {
return false;
}
} else if (documentFile.isDirectory()) {
final File subdirectory = new File(dest, documentFile.getName());
if (!subdirectory.exists()) {
subdirectory.mkdir();
}
if (!copyRecursively(contentResolver, documentFile, subdirectory)) {
return false;
}
}
}
return true;
}
private static boolean copyFile(ContentResolver contentResolver, DocumentFile src, File dest) {
try {
final InputStream inputStream = contentResolver.openInputStream(src.getUri());
final OutputStream outputStream = new FileOutputStream(dest);
final byte[] buffer = new byte[16384];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
}

View File

@ -0,0 +1,74 @@
package com.alexbatalov.falloutce;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
public class ImportActivity extends Activity {
private static final int IMPORT_REQUEST_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, IMPORT_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == IMPORT_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
final Uri treeUri = resultData.getData();
if (treeUri != null) {
final DocumentFile treeDocument = DocumentFile.fromTreeUri(this, treeUri);
if (treeDocument != null) {
copyFiles(treeDocument);
return;
}
}
}
finish();
} else {
super.onActivityResult(requestCode, resultCode, resultData);
}
}
private void copyFiles(DocumentFile treeDocument) {
ProgressDialog dialog = createProgressDialog();
dialog.show();
new Thread(() -> {
ContentResolver contentResolver = getContentResolver();
File externalFilesDir = getExternalFilesDir(null);
FileUtils.copyRecursively(contentResolver, treeDocument, externalFilesDir);
startMainActivity();
dialog.dismiss();
finish();
}).start();
}
private void startMainActivity() {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
private ProgressDialog createProgressDialog() {
ProgressDialog progressDialog = new ProgressDialog(this,
android.R.style.Theme_Material_Light_Dialog);
progressDialog.setTitle(R.string.loading_dialog_title);
progressDialog.setMessage(getString(R.string.loading_dialog_message));
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setCancelable(false);
return progressDialog;
}
}

View File

@ -0,0 +1,50 @@
package com.alexbatalov.falloutce;
import android.content.Intent;
import android.os.Bundle;
import org.libsdl.app.SDLActivity;
import java.io.File;
public class MainActivity extends SDLActivity {
private boolean noExit = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final File externalFilesDir = getExternalFilesDir(null);
final File configFile = new File(externalFilesDir, "fallout.cfg");
if (!configFile.exists()) {
final File masterDatFile = new File(externalFilesDir, "master.dat");
final File critterDatFile = new File(externalFilesDir, "critter.dat");
if (!masterDatFile.exists() || !critterDatFile.exists()) {
final Intent intent = new Intent(this, ImportActivity.class);
startActivity(intent);
noExit = true;
finish();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!noExit) {
// Needed to make sure libc calls exit handlers, which releases
// in-game resources.
System.exit(0);
}
}
@Override
protected String[] getLibraries() {
return new String[]{
"fallout-ce",
};
}
}

View File

@ -0,0 +1,22 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int sendFeatureReport(byte[] report);
public int sendOutputReport(byte[] report);
public boolean getFeatureReport(byte[] report);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}

View File

@ -0,0 +1,650 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.UUID;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
}
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
public String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
public BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
public void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristic)) {
Log.v(TAG, "Found input characteristic");
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
// Run in main thread
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
});
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
queueGattOperation(op);
}
public void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
public void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
}
else {
probeService(this);
}
}
}
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
setRegistered();
}
}
finishCurrentGattOperation();
}
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(inputCharacteristic)) {
boolean hasWrittenInputDescriptor = true;
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
if (reportChr != null) {
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
gatt.writeCharacteristic(reportChr);
}
}
finishCurrentGattOperation();
}
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
// We don't have an easy way to query from the Bluetooth device, but we know what it is
final int D0G_BLE2_PID = 0x1106;
return D0G_BLE2_PID;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
}
@Override
public int sendOutputReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
@Override
public boolean getFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
//Log.v(TAG, "getFeatureReport");
readCharacteristic(reportCharacteristic);
return true;
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}

View File

@ -0,0 +1,679 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
public static HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
public static void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.commit();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
public Context getContext() {
return mContext;
}
public int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.commit();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
mContext.registerReceiver(mUsbBroadcast, filter);
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x045e, // Microsoft
0x0738, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x2dc8, /* 8BitDo */
0x2e24, // Hyperkin
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT <= 30 &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
mContext.registerReceiver(mBluetoothBroadcast, filter);
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
public void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
public boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
public boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
public boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public int sendOutputReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendOutputReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public int sendFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public boolean getFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.getFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
}

View File

@ -0,0 +1,309 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
}
public String getIdentifier() {
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
if (Build.VERSION.SDK_INT >= 21) {
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21) {
result = mDevice.getManufacturerName();
}
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21) {
result = mDevice.getProductName();
}
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
public String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
}
@Override
public int sendOutputReport(byte[] report) {
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (r != report.length) {
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
}
return r;
}
@Override
public boolean getFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceFeatureReport(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}

View File

@ -0,0 +1,85 @@
package org.libsdl.app;
import android.content.Context;
import java.lang.Class;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
public static void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
public static void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
public static void setContext(Context context) {
mContext = context;
}
public static Context getContext() {
return mContext;
}
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = mContext.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Context mContext;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,394 @@
package org.libsdl.app;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
public class SDLAudioManager
{
protected static final String TAG = "SDLAudio";
protected static AudioTrack mAudioTrack;
protected static AudioRecord mAudioRecord;
public static void initialize() {
mAudioTrack = null;
mAudioRecord = null;
}
// Audio
protected static String getAudioFormatString(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return "8-bit";
case AudioFormat.ENCODING_PCM_16BIT:
return "16-bit";
case AudioFormat.ENCODING_PCM_FLOAT:
return "float";
default:
return Integer.toString(audioFormat);
}
}
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
int channelConfig;
int sampleSize;
int frameSize;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
/* On older devices let's use known good settings */
if (Build.VERSION.SDK_INT < 21) {
if (desiredChannels > 2) {
desiredChannels = 2;
}
}
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
if (Build.VERSION.SDK_INT < 22) {
if (sampleRate < 8000) {
sampleRate = 8000;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
}
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
int minSDKVersion = (isCapture ? 23 : 21);
if (Build.VERSION.SDK_INT < minSDKVersion) {
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
}
}
switch (audioFormat)
{
case AudioFormat.ENCODING_PCM_8BIT:
sampleSize = 1;
break;
case AudioFormat.ENCODING_PCM_16BIT:
sampleSize = 2;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
sampleSize = 4;
break;
default:
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
sampleSize = 2;
break;
}
if (isCapture) {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_IN_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
}
} else {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 3:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 7:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
break;
case 8:
if (Build.VERSION.SDK_INT >= 23) {
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else {
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
desiredChannels = 6;
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
}
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
}
/*
Log.v(TAG, "Speaker configuration (and order of channels):");
if ((channelConfig & 0x00000004) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
}
if ((channelConfig & 0x00000008) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
}
if ((channelConfig & 0x00000010) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
}
if ((channelConfig & 0x00000020) != 0) {
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
}
if ((channelConfig & 0x00000040) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
}
if ((channelConfig & 0x00000080) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
}
if ((channelConfig & 0x00000100) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
}
if ((channelConfig & 0x00000200) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
}
if ((channelConfig & 0x00000400) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
}
if ((channelConfig & 0x00000800) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
}
if ((channelConfig & 0x00001000) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
}
*/
}
frameSize = (sampleSize * desiredChannels);
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
int minBufferSize;
if (isCapture) {
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
} else {
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
}
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
int[] results = new int[4];
if (isCapture) {
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize);
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Failed during initialization of AudioRecord");
mAudioRecord.release();
mAudioRecord = null;
return null;
}
mAudioRecord.startRecording();
}
results[0] = mAudioRecord.getSampleRate();
results[1] = mAudioRecord.getAudioFormat();
results[2] = mAudioRecord.getChannelCount();
} else {
if (mAudioTrack == null) {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
/* Try again, with safer values */
Log.e(TAG, "Failed during initialization of Audio Track");
mAudioTrack.release();
mAudioTrack = null;
return null;
}
mAudioTrack.play();
}
results[0] = mAudioTrack.getSampleRate();
results[1] = mAudioTrack.getAudioFormat();
results[2] = mAudioTrack.getChannelCount();
}
results[3] = desiredFrames;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
return results;
}
/**
* This method is called by SDL using JNI.
*/
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteFloatBuffer(float[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(float)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteShortBuffer(short[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(short)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteByteBuffer(byte[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length; ) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(byte)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
}
/** This method is called by SDL using JNI. */
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
/** This method is called by SDL using JNI. */
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static void audioClose() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
/** This method is called by SDL using JNI. */
public static void captureClose() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
/** This method is called by SDL using JNI. */
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
try {
/* Set thread name */
if (iscapture) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
public static native int nativeSetupJNI();
}

View File

@ -0,0 +1,788 @@
package org.libsdl.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public class SDLControllerManager
{
public static native int nativeSetupJNI();
public static native int nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
boolean is_accelerometer, int button_mask,
int naxes, int nhats, int nballs);
public static native int nativeRemoveJoystick(int device_id);
public static native int nativeAddHaptic(int device_id, String name);
public static native int nativeRemoveHaptic(int device_id);
public static native int onNativePadDown(int device_id, int keycode);
public static native int onNativePadUp(int device_id, int keycode);
public static native void onNativeJoy(int device_id, int axis,
float value);
public static native void onNativeHat(int device_id, int hat_id,
int x, int y);
protected static SDLJoystickHandler mJoystickHandler;
protected static SDLHapticHandler mHapticHandler;
private static final String TAG = "SDLControllerManager";
public static void initialize() {
if (mJoystickHandler == null) {
if (Build.VERSION.SDK_INT >= 19) {
mJoystickHandler = new SDLJoystickHandler_API19();
} else {
mJoystickHandler = new SDLJoystickHandler_API16();
}
}
if (mHapticHandler == null) {
if (Build.VERSION.SDK_INT >= 26) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
}
}
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
public static boolean handleJoystickMotionEvent(MotionEvent event) {
return mJoystickHandler.handleMotionEvent(event);
}
/**
* This method is called by SDL using JNI.
*/
public static void pollInputDevices() {
mJoystickHandler.pollInputDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void pollHapticDevices() {
mHapticHandler.pollHapticDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticStop(int device_id)
{
mHapticHandler.stop(device_id);
}
// Check if a given device is considered a possible SDL joystick
public static boolean isDeviceSDLJoystick(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
// We cannot use InputDevice.isVirtual before API 16, so let's accept
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
if ((device == null) || (deviceId < 0)) {
return false;
}
int sources = device.getSources();
/* This is called for every button press, so let's not spam the logs */
/*
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
}
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
}
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
}
*/
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
);
}
}
class SDLJoystickHandler {
/**
* Handles given MotionEvent.
* @param event the event to be handled.
* @return if given event was processed.
*/
public boolean handleMotionEvent(MotionEvent event) {
return false;
}
/**
* Handles adding and removing of input devices.
*/
public void pollInputDevices() {
}
}
/* Actual joystick functionality available for API >= 12 devices */
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
static class SDLJoystick {
public int device_id;
public String name;
public String desc;
public ArrayList<InputDevice.MotionRange> axes;
public ArrayList<InputDevice.MotionRange> hats;
}
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
return arg0Axis - arg1Axis;
}
}
private final ArrayList<SDLJoystick> mJoysticks;
public SDLJoystickHandler_API16() {
mJoysticks = new ArrayList<SDLJoystick>();
}
@Override
public void pollInputDevices() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int device_id : deviceIds) {
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null) {
InputDevice joystickDevice = InputDevice.getDevice(device_id);
joystick = new SDLJoystick();
joystick.device_id = device_id;
joystick.name = joystickDevice.getName();
joystick.desc = getJoystickDescriptor(joystickDevice);
joystick.axes = new ArrayList<InputDevice.MotionRange>();
joystick.hats = new ArrayList<InputDevice.MotionRange>();
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joystick.hats.add(range);
} else {
joystick.axes.add(range);
}
}
}
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice), false,
getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLJoystick joystick : mJoysticks) {
int device_id = joystick.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveJoystick(device_id);
for (int i = 0; i < mJoysticks.size(); i++) {
if (mJoysticks.get(i).device_id == device_id) {
mJoysticks.remove(i);
break;
}
}
}
}
}
protected SDLJoystick getJoystick(int device_id) {
for (SDLJoystick joystick : mJoysticks) {
if (joystick.device_id == device_id) {
return joystick;
}
}
return null;
}
@Override
public boolean handleMotionEvent(MotionEvent event) {
int actionPointerIndex = event.getActionIndex();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_MOVE) {
SDLJoystick joystick = getJoystick(event.getDeviceId());
if (joystick != null) {
for (int i = 0; i < joystick.axes.size(); i++) {
InputDevice.MotionRange range = joystick.axes.get(i);
/* Normalize the value to -1...1 */
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
}
for (int i = 0; i < joystick.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
}
}
}
return true;
}
public String getJoystickDescriptor(InputDevice joystickDevice) {
String desc = joystickDevice.getDescriptor();
if (desc != null && !desc.isEmpty()) {
return desc;
}
return joystickDevice.getName();
}
public int getProductId(InputDevice joystickDevice) {
return 0;
}
public int getVendorId(InputDevice joystickDevice) {
return 0;
}
public int getButtonMask(InputDevice joystickDevice) {
return -1;
}
}
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
@Override
public int getProductId(InputDevice joystickDevice) {
return joystickDevice.getProductId();
}
@Override
public int getVendorId(InputDevice joystickDevice) {
return joystickDevice.getVendorId();
}
@Override
public int getButtonMask(InputDevice joystickDevice) {
int button_mask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_MENU,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 6), // MENU -> START
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] has_keys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (has_keys[i]) {
button_mask |= masks[i];
}
}
return button_mask;
}
}
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
if (intensity == 0.0f) {
stop(device_id);
return;
}
int vibeValue = Math.round(intensity * 255);
if (vibeValue > 255) {
vibeValue = 255;
}
if (vibeValue < 1) {
stop(device_id);
return;
}
try {
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
haptic.vib.vibrate(length);
}
}
}
}
class SDLHapticHandler {
static class SDLHaptic {
public int device_id;
public String name;
public Vibrator vib;
}
private final ArrayList<SDLHaptic> mHaptics;
public SDLHapticHandler() {
mHaptics = new ArrayList<SDLHaptic>();
}
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.vibrate(length);
}
}
public void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.cancel();
}
}
public void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
int[] deviceIds = InputDevice.getDeviceIds();
// It helps processing the device ids in reverse order
// For example, in the case of the XBox 360 wireless dongle,
// so the first controller seen by SDL matches what the receiver
// considers to be the first controller
for (int i = deviceIds.length - 1; i > -1; i--) {
SDLHaptic haptic = getHaptic(deviceIds[i]);
if (haptic == null) {
InputDevice device = InputDevice.getDevice(deviceIds[i]);
Vibrator vib = device.getVibrator();
if (vib.hasVibrator()) {
haptic = new SDLHaptic();
haptic.device_id = deviceIds[i];
haptic.name = device.getName();
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
hasVibratorService = vib.hasVibrator();
if (hasVibratorService) {
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
if (haptic == null) {
haptic = new SDLHaptic();
haptic.device_id = deviceId_VIBRATOR_SERVICE;
haptic.name = "VIBRATOR_SERVICE";
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
} // else: don't remove the vibrator if it is still present
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveHaptic(device_id);
for (int i = 0; i < mHaptics.size(); i++) {
if (mHaptics.get(i).device_id == device_id) {
mHaptics.remove(i);
break;
}
}
}
}
}
protected SDLHaptic getHaptic(int device_id) {
for (SDLHaptic haptic : mHaptics) {
if (haptic.device_id == device_id) {
return haptic;
}
}
return null;
}
}
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
// Generic Motion (mouse hover, joystick...) events go here
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
public boolean supportsRelativeMouse() {
return false;
}
public boolean inRelativeMode() {
return false;
}
public boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
public void reclaimRelativeMouseModeIfNeeded()
{
}
public float getEventX(MotionEvent event) {
return event.getX(0);
}
public float getEventY(MotionEvent event) {
return event.getY(0);
}
}
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
// Handle relative mouse mode
if (mRelativeModeEnabled) {
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_HOVER_MOVE) {
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
}
}
}
// Event was not managed, call SDLGenericMotionListener_API12 method
return super.onGenericMotion(v, event);
}
@Override
public boolean supportsRelativeMouse() {
return true;
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
mRelativeModeEnabled = enabled;
return true;
}
@Override
public float getEventX(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
} else {
return event.getX(0);
}
}
@Override
public float getEventY(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
} else {
return event.getY(0);
}
}
}
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
// DeX desktop mouse cursor is a separate non-standard input type.
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
case InputDevice.SOURCE_MOUSE_RELATIVE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
@Override
public boolean supportsRelativeMouse() {
return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27));
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) {
if (enabled) {
SDLActivity.getContentView().requestPointerCapture();
} else {
SDLActivity.getContentView().releasePointerCapture();
}
mRelativeModeEnabled = enabled;
return true;
} else {
return false;
}
}
@Override
public void reclaimRelativeMouseModeIfNeeded()
{
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
SDLActivity.getContentView().requestPointerCapture();
}
}
@Override
public float getEventX(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getX(0);
}
@Override
public float getEventY(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getY(0);
}
}

View File

@ -0,0 +1,405 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
public boolean mIsSurfaceReady;
// Startup
public SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
public void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
public void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
public Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
try
{
if (Build.VERSION.SDK_INT >= 17) {
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
}
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip in MultiWindow.
if (skip) {
if (Build.VERSION.SDK_INT >= 24) {
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
Log.v("SDL", "Don't skip in Multi-Window");
skip = false;
}
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerFingerId;
int i = -1;
float x,y,p;
/*
* Prevent id to be -1, since it's used in SDL internal for synthetic events
* Appears when using Android emulator, eg:
* adb shell input mouse tap 100 100
* adb shell input touchscreen tap 100 100
*/
if (touchDevId < 0) {
touchDevId -= 1;
}
// 12290 = Samsung DeX mode desktop mouse
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
// 0x2 = SOURCE_CLASS_POINTER
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
int mouseButton = 1;
try {
Object object = event.getClass().getMethod("getButtonState").invoke(event);
if (object != null) {
mouseButton = (Integer) object;
}
} catch(Exception ignored) {
}
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event);
y = motionListener.getEventY(event);
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
} else {
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// Primary pointer up/down, the index is always zero
i = 0;
/* fallthrough */
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
// Non primary pointer up/down
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
}
}
return true;
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newOrientation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
break;
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
break;
}
if (newOrientation != SDLActivity.mCurrentOrientation) {
SDLActivity.mCurrentOrientation = newOrientation;
SDLActivity.onNativeOrientationChanged(newOrientation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Captured pointer events for API 26.
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(0);
y = event.getY(0);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
return false;
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@ -0,0 +1,5 @@
<resources>
<string name="app_name">Fallout</string>
<string name="loading_dialog_title">PLEASE STAND BY</string>
<string name="loading_dialog_message">Copying files…</string>
</resources>

View File

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

9
os/android/build.gradle Normal file
View File

@ -0,0 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Mon Jul 25 15:30:57 PST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

185
os/android/gradlew vendored Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
os/android/gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Fallout Community Edition"
include ':app'

56
os/ios/Info.plist Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${MACOSX_BUNDLE_DISPLAY_NAME}</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.role-playing-games</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

36
os/macos/Info.plist Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>${MACOSX_BUNDLE_DISPLAY_NAME}</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>LSMinimumSystemVersion</key>
<string>10.11</string>
<key>SDL_FILESYSTEM_BASE_DIR_TYPE</key>
<string>parent</string>
</dict>
</plist>

BIN
os/macos/fallout-ce.icns Normal file

Binary file not shown.

BIN
os/windows/fallout-ce.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

1
os/windows/fallout-ce.rc Normal file
View File

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "fallout-ce.ico"

491
src/audio_engine.cc Normal file
View File

@ -0,0 +1,491 @@
#include "audio_engine.h"
#include <string.h>
#include <mutex>
#include <SDL.h>
namespace fallout {
#define AUDIO_ENGINE_SOUND_BUFFERS 8
struct AudioEngineSoundBuffer {
bool active;
unsigned int size;
int bitsPerSample;
int channels;
int rate;
void* data;
int volume;
bool playing;
bool looping;
unsigned int pos;
SDL_AudioStream* stream;
std::recursive_mutex mutex;
};
extern bool GNW95_isActive;
static bool soundBufferIsValid(int soundBufferIndex);
static void audioEngineMixin(void* userData, Uint8* stream, int length);
static SDL_AudioSpec gAudioEngineSpec;
static SDL_AudioDeviceID gAudioEngineDeviceId = -1;
static AudioEngineSoundBuffer gAudioEngineSoundBuffers[AUDIO_ENGINE_SOUND_BUFFERS];
static bool audioEngineIsInitialized()
{
return gAudioEngineDeviceId != -1;
}
static bool soundBufferIsValid(int soundBufferIndex)
{
return soundBufferIndex >= 0 && soundBufferIndex < AUDIO_ENGINE_SOUND_BUFFERS;
}
static void audioEngineMixin(void* userData, Uint8* stream, int length)
{
memset(stream, gAudioEngineSpec.silence, length);
if (!GNW95_isActive) {
return;
}
for (int index = 0; index < AUDIO_ENGINE_SOUND_BUFFERS; index++) {
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[index]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (soundBuffer->active && soundBuffer->playing) {
int srcFrameSize = soundBuffer->bitsPerSample / 8 * soundBuffer->channels;
unsigned char buffer[1024];
int pos = 0;
while (pos < length) {
int remaining = length - pos;
if (remaining > sizeof(buffer)) {
remaining = sizeof(buffer);
}
// TODO: Make something better than frame-by-frame convertion.
SDL_AudioStreamPut(soundBuffer->stream, (unsigned char*)soundBuffer->data + soundBuffer->pos, srcFrameSize);
soundBuffer->pos += srcFrameSize;
int bytesRead = SDL_AudioStreamGet(soundBuffer->stream, buffer, remaining);
if (bytesRead == -1) {
break;
}
SDL_MixAudioFormat(stream + pos, buffer, gAudioEngineSpec.format, bytesRead, soundBuffer->volume);
if (soundBuffer->pos >= soundBuffer->size) {
if (soundBuffer->looping) {
soundBuffer->pos %= soundBuffer->size;
} else {
soundBuffer->playing = false;
break;
}
}
pos += bytesRead;
}
}
}
}
bool audioEngineInit()
{
if (SDL_InitSubSystem(SDL_INIT_AUDIO) == -1) {
return false;
}
SDL_AudioSpec desiredSpec;
desiredSpec.freq = 22050;
desiredSpec.format = AUDIO_S16;
desiredSpec.channels = 2;
desiredSpec.samples = 1024;
desiredSpec.callback = audioEngineMixin;
gAudioEngineDeviceId = SDL_OpenAudioDevice(NULL, 0, &desiredSpec, &gAudioEngineSpec, SDL_AUDIO_ALLOW_ANY_CHANGE);
if (gAudioEngineDeviceId == -1) {
return false;
}
SDL_PauseAudioDevice(gAudioEngineDeviceId, 0);
return true;
}
void audioEngineExit()
{
if (audioEngineIsInitialized()) {
SDL_CloseAudioDevice(gAudioEngineDeviceId);
gAudioEngineDeviceId = -1;
}
if (SDL_WasInit(SDL_INIT_AUDIO)) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
void audioEnginePause()
{
if (audioEngineIsInitialized()) {
SDL_PauseAudioDevice(gAudioEngineDeviceId, 1);
}
}
void audioEngineResume()
{
if (audioEngineIsInitialized()) {
SDL_PauseAudioDevice(gAudioEngineDeviceId, 0);
}
}
int audioEngineCreateSoundBuffer(unsigned int size, int bitsPerSample, int channels, int rate)
{
if (!audioEngineIsInitialized()) {
return -1;
}
for (int index = 0; index < AUDIO_ENGINE_SOUND_BUFFERS; index++) {
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[index]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
soundBuffer->active = true;
soundBuffer->size = size;
soundBuffer->bitsPerSample = bitsPerSample;
soundBuffer->channels = channels;
soundBuffer->rate = rate;
soundBuffer->volume = SDL_MIX_MAXVOLUME;
soundBuffer->playing = false;
soundBuffer->looping = false;
soundBuffer->pos = 0;
soundBuffer->data = malloc(size);
soundBuffer->stream = SDL_NewAudioStream(bitsPerSample == 16 ? AUDIO_S16 : AUDIO_S8, channels, rate, gAudioEngineSpec.format, gAudioEngineSpec.channels, gAudioEngineSpec.freq);
return index;
}
}
return -1;
}
bool audioEngineSoundBufferRelease(int soundBufferIndex)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
soundBuffer->active = false;
free(soundBuffer->data);
soundBuffer->data = NULL;
SDL_FreeAudioStream(soundBuffer->stream);
soundBuffer->stream = NULL;
return true;
}
bool audioEngineSoundBufferSetVolume(int soundBufferIndex, int volume)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
soundBuffer->volume = volume;
return true;
}
bool audioEngineSoundBufferGetVolume(int soundBufferIndex, int* volumePtr)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
*volumePtr = soundBuffer->volume;
return true;
}
bool audioEngineSoundBufferSetPan(int soundBufferIndex, int pan)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
// NOTE: Audio engine does not support sound panning. I'm not sure it's
// even needed. For now this value is silently ignored.
return true;
}
bool audioEngineSoundBufferPlay(int soundBufferIndex, unsigned int flags)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
soundBuffer->playing = true;
if ((flags & AUDIO_ENGINE_SOUND_BUFFER_PLAY_LOOPING) != 0) {
soundBuffer->looping = true;
}
return true;
}
bool audioEngineSoundBufferStop(int soundBufferIndex)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
soundBuffer->playing = false;
return true;
}
bool audioEngineSoundBufferGetCurrentPosition(int soundBufferIndex, unsigned int* readPosPtr, unsigned int* writePosPtr)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
if (readPosPtr != NULL) {
*readPosPtr = soundBuffer->pos;
}
if (writePosPtr != NULL) {
*writePosPtr = soundBuffer->pos;
if (soundBuffer->playing) {
// 15 ms lead
// See: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/mt708925(v=vs.85)#remarks
*writePosPtr += soundBuffer->rate / 150;
*writePosPtr %= soundBuffer->size;
}
}
return true;
}
bool audioEngineSoundBufferSetCurrentPosition(int soundBufferIndex, unsigned int pos)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
soundBuffer->pos = pos % soundBuffer->size;
return true;
}
bool audioEngineSoundBufferLock(int soundBufferIndex, unsigned int writePos, unsigned int writeBytes, void** audioPtr1, unsigned int* audioBytes1, void** audioPtr2, unsigned int* audioBytes2, unsigned int flags)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
if (audioBytes1 == NULL) {
return false;
}
if ((flags & AUDIO_ENGINE_SOUND_BUFFER_LOCK_FROM_WRITE_POS) != 0) {
if (!audioEngineSoundBufferGetCurrentPosition(soundBufferIndex, NULL, &writePos)) {
return false;
}
}
if ((flags & AUDIO_ENGINE_SOUND_BUFFER_LOCK_ENTIRE_BUFFER) != 0) {
writeBytes = soundBuffer->size;
}
if (writePos + writeBytes <= soundBuffer->size) {
*(unsigned char**)audioPtr1 = (unsigned char*)soundBuffer->data + writePos;
*audioBytes1 = writeBytes;
if (audioPtr2 != NULL) {
*audioPtr2 = NULL;
}
if (audioBytes2 != NULL) {
*audioBytes2 = 0;
}
} else {
unsigned int remainder = writePos + writeBytes - soundBuffer->size;
*(unsigned char**)audioPtr1 = (unsigned char*)soundBuffer->data + writePos;
*audioBytes1 = soundBuffer->size - writePos;
if (audioPtr2 != NULL) {
*(unsigned char**)audioPtr2 = (unsigned char*)soundBuffer->data;
}
if (audioBytes2 != NULL) {
*audioBytes2 = writeBytes - (soundBuffer->size - writePos);
}
}
// TODO: Mark range as locked.
return true;
}
bool audioEngineSoundBufferUnlock(int soundBufferIndex, void* audioPtr1, unsigned int audioBytes1, void* audioPtr2, unsigned int audioBytes2)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
// TODO: Mark range as unlocked.
return true;
}
bool audioEngineSoundBufferGetStatus(int soundBufferIndex, unsigned int* statusPtr)
{
if (!audioEngineIsInitialized()) {
return false;
}
if (!soundBufferIsValid(soundBufferIndex)) {
return false;
}
AudioEngineSoundBuffer* soundBuffer = &(gAudioEngineSoundBuffers[soundBufferIndex]);
std::lock_guard<std::recursive_mutex> lock(soundBuffer->mutex);
if (!soundBuffer->active) {
return false;
}
if (statusPtr == NULL) {
return false;
}
*statusPtr = 0;
if (soundBuffer->playing) {
*statusPtr |= AUDIO_ENGINE_SOUND_BUFFER_STATUS_PLAYING;
if (soundBuffer->looping) {
*statusPtr |= AUDIO_ENGINE_SOUND_BUFFER_STATUS_LOOPING;
}
}
return true;
}
} // namespace fallout

33
src/audio_engine.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef FALLOUT_AUDIO_ENGINE_H_
#define FALLOUT_AUDIO_ENGINE_H_
namespace fallout {
#define AUDIO_ENGINE_SOUND_BUFFER_LOCK_FROM_WRITE_POS 0x00000001
#define AUDIO_ENGINE_SOUND_BUFFER_LOCK_ENTIRE_BUFFER 0x00000002
#define AUDIO_ENGINE_SOUND_BUFFER_PLAY_LOOPING 0x00000001
#define AUDIO_ENGINE_SOUND_BUFFER_STATUS_PLAYING 0x00000001
#define AUDIO_ENGINE_SOUND_BUFFER_STATUS_LOOPING 0x00000004
bool audioEngineInit();
void audioEngineExit();
void audioEnginePause();
void audioEngineResume();
int audioEngineCreateSoundBuffer(unsigned int size, int bitsPerSample, int channels, int rate);
bool audioEngineSoundBufferRelease(int soundBufferIndex);
bool audioEngineSoundBufferSetVolume(int soundBufferIndex, int volume);
bool audioEngineSoundBufferGetVolume(int soundBufferIndex, int* volumePtr);
bool audioEngineSoundBufferSetPan(int soundBufferIndex, int pan);
bool audioEngineSoundBufferPlay(int soundBufferIndex, unsigned int flags);
bool audioEngineSoundBufferStop(int soundBufferIndex);
bool audioEngineSoundBufferGetCurrentPosition(int soundBufferIndex, unsigned int* readPosPtr, unsigned int* writePosPtr);
bool audioEngineSoundBufferSetCurrentPosition(int soundBufferIndex, unsigned int pos);
bool audioEngineSoundBufferLock(int soundBufferIndex, unsigned int writePos, unsigned int writeBytes, void** audioPtr1, unsigned int* audioBytes1, void** audioPtr2, unsigned int* audioBytes2, unsigned int flags);
bool audioEngineSoundBufferUnlock(int soundBufferIndex, void* audioPtr1, unsigned int audioBytes1, void* audioPtr2, unsigned int audioBytes2);
bool audioEngineSoundBufferGetStatus(int soundBufferIndex, unsigned int* status);
} // namespace fallout
#endif /* FALLOUT_AUDIO_ENGINE_H_ */

25
src/fps_limiter.cc Normal file
View File

@ -0,0 +1,25 @@
#include "fps_limiter.h"
#include <SDL.h>
namespace fallout {
FpsLimiter::FpsLimiter(unsigned int fps)
: _fps(fps)
, _ticks(0)
{
}
void FpsLimiter::mark()
{
_ticks = SDL_GetTicks();
}
void FpsLimiter::throttle() const
{
if (1000 / _fps > SDL_GetTicks() - _ticks) {
SDL_Delay(1000 / _fps - (SDL_GetTicks() - _ticks));
}
}
} // namespace fallout

19
src/fps_limiter.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef FALLOUT_FPS_LIMITER_H_
#define FALLOUT_FPS_LIMITER_H_
namespace fallout {
class FpsLimiter {
public:
FpsLimiter(unsigned int fps = 60);
void mark();
void throttle() const;
private:
const unsigned int _fps;
unsigned int _ticks;
};
} // namespace fallout
#endif /* FALLOUT_FPS_LIMITER_H_ */

1960
src/game/actions.cc Normal file

File diff suppressed because it is too large Load Diff

43
src/game/actions.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef FALLOUT_GAME_ACTIONS_H_
#define FALLOUT_GAME_ACTIONS_H_
#include "game/combat_defs.h"
#include "game/object_types.h"
#include "game/proto_types.h"
namespace fallout {
extern unsigned int rotation;
extern int obj_fid;
extern int obj_pid_old;
void switch_dude();
int action_knockback(Object* obj, int* anim, int maxDistance, int rotation, int delay);
int action_blood(Object* obj, int anim, int delay);
void show_damage_to_object(Object* a1, int damage, int flags, Object* weapon, bool isFallingBack, int knockbackDistance, int knockbackRotation, int a8, Object* a9, int a10);
int show_damage_target(Attack* attack);
int show_damage_extras(Attack* attack);
void show_damage(Attack* attack, int a2, int a3);
int action_attack(Attack* attack);
int use_an_object(Object* item);
int a_use_obj(Object* a1, Object* a2, Object* a3);
int action_use_an_item_on_object(Object* a1, Object* a2, Object* a3);
int action_use_an_object(Object* a1, Object* a2);
int get_an_object(Object* item);
int action_get_an_object(Object* critter, Object* item);
int action_loot_container(Object* critter, Object* container);
int action_skill_use(int a1);
int action_use_skill_in_combat_error(Object* critter);
int action_use_skill_on(Object* a1, Object* a2, int skill);
Object* pick_object(int objectType, bool a2);
int pick_hex();
bool is_hit_from_front(Object* a1, Object* a2);
bool can_see(Object* a1, Object* a2);
int pick_fall(Object* obj, int anim);
int action_explode(int tile, int elevation, int minDamage, int maxDamage, Object* a5, bool a6);
int action_talk_to(Object* a1, Object* a2);
void action_dmg(int tile, int elevation, int minDamage, int maxDamage, int damageType, bool animated, bool bypassArmor);
} // namespace fallout
#endif /* FALLOUT_GAME_ACTIONS_H_ */

38
src/game/amutex.cc Normal file
View File

@ -0,0 +1,38 @@
#include "game/amutex.h"
#ifdef _WIN32
#include <windows.h>
#endif
namespace fallout {
#ifdef _WIN32
// 0x540010
static HANDLE autorun_mutex;
#endif
// 0x413450
bool autorun_mutex_create()
{
#ifdef _WIN32
autorun_mutex = CreateMutexA(NULL, FALSE, "InterplayGenericAutorunMutex");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(autorun_mutex);
return false;
}
#endif
return true;
}
// 0x413490
void autorun_mutex_destroy()
{
#ifdef _WIN32
if (autorun_mutex != NULL) {
CloseHandle(autorun_mutex);
}
#endif
}
} // namespace fallout

11
src/game/amutex.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef FALLOUT_GAME_AMUTEX_H_
#define FALLOUT_GAME_AMUTEX_H_
namespace fallout {
bool autorun_mutex_create();
void autorun_mutex_destroy();
} // namespace fallout
#endif /* FALLOUT_GAME_AMUTEX_H_ */

3355
src/game/anim.cc Normal file

File diff suppressed because it is too large Load Diff

171
src/game/anim.h Normal file
View File

@ -0,0 +1,171 @@
#ifndef FALLOUT_GAME_ANIMATION_H_
#define FALLOUT_GAME_ANIMATION_H_
#include "game/object_types.h"
namespace fallout {
typedef enum AnimationRequestOptions {
ANIMATION_REQUEST_UNRESERVED = 0x01,
ANIMATION_REQUEST_RESERVED = 0x02,
ANIMATION_REQUEST_NO_STAND = 0x04,
ANIMATION_REQUEST_0x100 = 0x100,
ANIMATION_REQUEST_INSIGNIFICANT = 0x200,
} AnimationRequestOptions;
// Basic animations: 0-19
// Knockdown and death: 20-35
// Change positions: 36-37
// Weapon: 38-47
// Single-frame death animations (the last frame of knockdown and death animations): 48-63
typedef enum AnimationType {
ANIM_STAND = 0,
ANIM_WALK = 1,
ANIM_JUMP_BEGIN = 2,
ANIM_JUMP_END = 3,
ANIM_CLIMB_LADDER = 4,
ANIM_FALLING = 5,
ANIM_UP_STAIRS_RIGHT = 6,
ANIM_UP_STAIRS_LEFT = 7,
ANIM_DOWN_STAIRS_RIGHT = 8,
ANIM_DOWN_STAIRS_LEFT = 9,
ANIM_MAGIC_HANDS_GROUND = 10,
ANIM_MAGIC_HANDS_MIDDLE = 11,
ANIM_MAGIC_HANDS_UP = 12,
ANIM_DODGE_ANIM = 13,
ANIM_HIT_FROM_FRONT = 14,
ANIM_HIT_FROM_BACK = 15,
ANIM_THROW_PUNCH = 16,
ANIM_KICK_LEG = 17,
ANIM_THROW_ANIM = 18,
ANIM_RUNNING = 19,
ANIM_FALL_BACK = 20,
ANIM_FALL_FRONT = 21,
ANIM_BAD_LANDING = 22,
ANIM_BIG_HOLE = 23,
ANIM_CHARRED_BODY = 24,
ANIM_CHUNKS_OF_FLESH = 25,
ANIM_DANCING_AUTOFIRE = 26,
ANIM_ELECTRIFY = 27,
ANIM_SLICED_IN_HALF = 28,
ANIM_BURNED_TO_NOTHING = 29,
ANIM_ELECTRIFIED_TO_NOTHING = 30,
ANIM_EXPLODED_TO_NOTHING = 31,
ANIM_MELTED_TO_NOTHING = 32,
ANIM_FIRE_DANCE = 33,
ANIM_FALL_BACK_BLOOD = 34,
ANIM_FALL_FRONT_BLOOD = 35,
ANIM_PRONE_TO_STANDING = 36,
ANIM_BACK_TO_STANDING = 37,
ANIM_TAKE_OUT = 38,
ANIM_PUT_AWAY = 39,
ANIM_PARRY_ANIM = 40,
ANIM_THRUST_ANIM = 41,
ANIM_SWING_ANIM = 42,
ANIM_POINT = 43,
ANIM_UNPOINT = 44,
ANIM_FIRE_SINGLE = 45,
ANIM_FIRE_BURST = 46,
ANIM_FIRE_CONTINUOUS = 47,
ANIM_FALL_BACK_SF = 48,
ANIM_FALL_FRONT_SF = 49,
ANIM_BAD_LANDING_SF = 50,
ANIM_BIG_HOLE_SF = 51,
ANIM_CHARRED_BODY_SF = 52,
ANIM_CHUNKS_OF_FLESH_SF = 53,
ANIM_DANCING_AUTOFIRE_SF = 54,
ANIM_ELECTRIFY_SF = 55,
ANIM_SLICED_IN_HALF_SF = 56,
ANIM_BURNED_TO_NOTHING_SF = 57,
ANIM_ELECTRIFIED_TO_NOTHING_SF = 58,
ANIM_EXPLODED_TO_NOTHING_SF = 59,
ANIM_MELTED_TO_NOTHING_SF = 60,
ANIM_FIRE_DANCE_SF = 61,
ANIM_FALL_BACK_BLOOD_SF = 62,
ANIM_FALL_FRONT_BLOOD_SF = 63,
ANIM_CALLED_SHOT_PIC = 64,
ANIM_COUNT = 65,
FIRST_KNOCKDOWN_AND_DEATH_ANIM = ANIM_FALL_BACK,
LAST_KNOCKDOWN_AND_DEATH_ANIM = ANIM_FALL_FRONT_BLOOD,
FIRST_SF_DEATH_ANIM = ANIM_FALL_BACK_SF,
LAST_SF_DEATH_ANIM = ANIM_FALL_FRONT_BLOOD_SF,
} AnimationType;
#define FID_ANIM_TYPE(value) ((value) & 0xFF0000) >> 16
// Signature of animation callback accepting 2 parameters.
typedef int AnimationCallback(void*, void*);
// Signature of animation callback accepting 3 parameters.
typedef int AnimationCallback3(void*, void*, void*);
typedef Object* PathBuilderCallback(Object* object, int tile, int elevation);
typedef struct StraightPathNode {
int tile;
int elevation;
int x;
int y;
} StraightPathNode;
void anim_init();
void anim_reset();
void anim_exit();
int register_begin(int a1);
int register_priority(int a1);
int register_clear(Object* a1);
int register_end();
int check_registry(Object* obj);
int anim_busy(Object* a1);
int register_object_move_to_object(Object* owner, Object* destination, int actionPoints, int delay);
int register_object_run_to_object(Object* owner, Object* destination, int actionPoints, int delay);
int register_object_move_to_tile(Object* owner, int tile, int elevation, int actionPoints, int delay);
int register_object_run_to_tile(Object* owner, int tile, int elevation, int actionPoints, int delay);
int register_object_move_straight_to_tile(Object* object, int tile, int elevation, int anim, int delay);
int register_object_animate_and_move_straight(Object* owner, int tile, int elev, int anim, int delay);
int register_object_move_on_stairs(Object* owner, Object* stairs, int delay);
int register_object_check_falling(Object* owner, int delay);
int register_object_animate(Object* owner, int anim, int delay);
int register_object_animate_reverse(Object* owner, int anim, int delay);
int register_object_animate_and_hide(Object* owner, int anim, int delay);
int register_object_turn_towards(Object* owner, int tile);
int register_object_inc_rotation(Object* owner);
int register_object_dec_rotation(Object* owner);
int register_object_erase(Object* object);
int register_object_must_erase(Object* object);
int register_object_call(void* a1, void* a2, AnimationCallback* proc, int delay);
int register_object_call3(void* a1, void* a2, void* a3, AnimationCallback3* proc, int delay);
int register_object_must_call(void* a1, void* a2, AnimationCallback* proc, int delay);
int register_object_fset(Object* object, int flag, int delay);
int register_object_funset(Object* object, int flag, int delay);
int register_object_flatten(Object* object, int delay);
int register_object_change_fid(Object* owner, int fid, int delay);
int register_object_take_out(Object* owner, int weaponAnimationCode, int delay);
int register_object_light(Object* owner, int lightDistance, int delay);
int register_object_outline(Object* object, bool outline, int delay);
int register_object_play_sfx(Object* owner, const char* soundEffectName, int delay);
int register_object_animate_forever(Object* owner, int anim, int delay);
int register_ping(int a1, int a2);
int make_path(Object* object, int from, int to, unsigned char* a4, int a5);
int make_path_func(Object* object, int from, int to, unsigned char* rotations, int a5, PathBuilderCallback* callback);
int idist(int a1, int a2, int a3, int a4);
int EST(int tile1, int tile2);
int make_straight_path(Object* a1, int from, int to, StraightPathNode* pathNodes, Object** a5, int a6);
int make_straight_path_func(Object* a1, int from, int to, StraightPathNode* pathNodes, Object** a5, int a6, PathBuilderCallback* callback);
int anim_move_on_stairs(Object* obj, int tile, int elevation, int anim, int animationSequenceIndex);
int check_for_falling(Object* obj, int anim, int a3);
void object_animate();
int check_move(int* a1);
int dude_move(int a1);
int dude_run(int a1);
void dude_fidget();
void dude_stand(Object* obj, int rotation, int fid);
void dude_standup(Object* a1);
int anim_hide(Object* object, int animationSequenceIndex);
int anim_change_fid(Object* obj, int animationSequenceIndex, int fid);
void anim_stop();
unsigned int compute_tpf(Object* object, int fid);
} // namespace fallout
#endif /* FALLOUT_GAME_ANIMATION_H_ */

1219
src/game/art.cc Normal file

File diff suppressed because it is too large Load Diff

159
src/game/art.h Normal file
View File

@ -0,0 +1,159 @@
#ifndef FALLOUT_GAME_ART_H_
#define FALLOUT_GAME_ART_H_
#include "game/cache.h"
#include "game/heap.h"
#include "game/object_types.h"
#include "game/proto_types.h"
namespace fallout {
typedef enum Head {
HEAD_INVALID,
HEAD_MARCUS,
HEAD_MYRON,
HEAD_ELDER,
HEAD_LYNETTE,
HEAD_HAROLD,
HEAD_TANDI,
HEAD_COM_OFFICER,
HEAD_SULIK,
HEAD_PRESIDENT,
HEAD_HAKUNIN,
HEAD_BOSS,
HEAD_DYING_HAKUNIN,
HEAD_COUNT,
} Head;
typedef enum HeadAnimation {
HEAD_ANIMATION_VERY_GOOD_REACTION = 0,
FIDGET_GOOD = 1,
HEAD_ANIMATION_GOOD_TO_NEUTRAL = 2,
HEAD_ANIMATION_NEUTRAL_TO_GOOD = 3,
FIDGET_NEUTRAL = 4,
HEAD_ANIMATION_NEUTRAL_TO_BAD = 5,
HEAD_ANIMATION_BAD_TO_NEUTRAL = 6,
FIDGET_BAD = 7,
HEAD_ANIMATION_VERY_BAD_REACTION = 8,
HEAD_ANIMATION_GOOD_PHONEMES = 9,
HEAD_ANIMATION_NEUTRAL_PHONEMES = 10,
HEAD_ANIMATION_BAD_PHONEMES = 11,
} HeadAnimation;
typedef enum Background {
BACKGROUND_0,
BACKGROUND_1,
BACKGROUND_2,
BACKGROUND_HUB,
BACKGROUND_NECROPOLIS,
BACKGROUND_BROTHERHOOD,
BACKGROUND_MILITARY_BASE,
BACKGROUND_JUNK_TOWN,
BACKGROUND_CATHEDRAL,
BACKGROUND_SHADY_SANDS,
BACKGROUND_VAULT,
BACKGROUND_MASTER,
BACKGROUND_FOLLOWER,
BACKGROUND_RAIDERS,
BACKGROUND_CAVE,
BACKGROUND_ENCLAVE,
BACKGROUND_WASTELAND,
BACKGROUND_BOSS,
BACKGROUND_PRESIDENT,
BACKGROUND_TENT,
BACKGROUND_ADOBE,
BACKGROUND_COUNT,
} Background;
typedef struct Art {
int field_0;
short framesPerSecond;
short actionFrame;
short frameCount;
short xOffsets[6];
short yOffsets[6];
int dataOffsets[6];
int padding[6];
int dataSize;
} Art;
typedef struct ArtFrame {
short width;
short height;
int size;
short x;
short y;
} ArtFrame;
typedef struct HeadDescription {
int goodFidgetCount;
int neutralFidgetCount;
int badFidgetCount;
} HeadDescription;
typedef enum WeaponAnimation {
WEAPON_ANIMATION_NONE,
WEAPON_ANIMATION_KNIFE, // d
WEAPON_ANIMATION_CLUB, // e
WEAPON_ANIMATION_HAMMER, // f
WEAPON_ANIMATION_SPEAR, // g
WEAPON_ANIMATION_PISTOL, // h
WEAPON_ANIMATION_SMG, // i
WEAPON_ANIMATION_SHOTGUN, // j
WEAPON_ANIMATION_LASER_RIFLE, // k
WEAPON_ANIMATION_MINIGUN, // l
WEAPON_ANIMATION_LAUNCHER, // m
WEAPON_ANIMATION_COUNT,
} WeaponAnimation;
extern int art_vault_guy_num;
extern int art_vault_person_nums[GENDER_COUNT];
extern int art_mapper_blank_tile;
extern Cache art_cache;
extern HeadDescription* head_info;
int art_init();
void art_reset();
void art_exit();
char* art_dir(int objectType);
int art_get_disable(int objectType);
void art_toggle_disable(int objectType);
int art_total(int objectType);
int art_head_fidgets(int headFid);
void scale_art(int fid, unsigned char* dest, int width, int height, int pitch);
Art* art_ptr_lock(int fid, CacheEntry** cache_entry);
unsigned char* art_ptr_lock_data(int fid, int frame, int direction, CacheEntry** out_cache_entry);
unsigned char* art_lock(int fid, CacheEntry** out_cache_entry, int* widthPtr, int* heightPtr);
int art_ptr_unlock(CacheEntry* cache_entry);
int art_discard(int fid);
int art_flush();
int art_get_base_name(int objectType, int a2, char* a3);
int art_get_code(int a1, int a2, char* a3, char* a4);
char* art_get_name(int a1);
int art_read_lst(const char* path, char** artListPtr, int* artListSizePtr);
int art_frame_fps(Art* art);
int art_frame_action_frame(Art* art);
int art_frame_max_frame(Art* art);
int art_frame_width(Art* art, int frame, int direction);
int art_frame_length(Art* art, int frame, int direction);
int art_frame_width_length(Art* art, int frame, int direction, int* out_width, int* out_height);
int art_frame_hot(Art* art, int frame, int direction, int* a4, int* a5);
int art_frame_offset(Art* art, int rotation, int* out_offset_x, int* out_offset_y);
unsigned char* art_frame_data(Art* art, int frame, int direction);
ArtFrame* frame_ptr(Art* art, int frame, int direction);
bool art_exists(int fid);
bool art_fid_valid(int fid);
int art_alias_num(int a1);
int art_alias_fid(int fid);
int art_data_size(int a1, int* out_size);
int art_data_load(int a1, int* a2, unsigned char* data);
void art_data_free(void* ptr);
int art_id(int objectType, int frmId, int animType, int a4, int rotation);
Art* load_frame(const char* path);
int load_frame_into(const char* path, unsigned char* data);
int save_frame(const char* path, unsigned char* data);
} // namespace fallout
#endif /* FALLOUT_GAME_ART_H_ */

1078
src/game/automap.cc Normal file

File diff suppressed because it is too large Load Diff

48
src/game/automap.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef FALLOUT_GAME_AUTOMAP_H_
#define FALLOUT_GAME_AUTOMAP_H_
#include "game/map_defs.h"
#include "plib/db/db.h"
namespace fallout {
#define AUTOMAP_DB "AUTOMAP.DB"
#define AUTOMAP_TMP "AUTOMAP.TMP"
// The number of map entries that is stored in automap.db.
#define AUTOMAP_MAP_COUNT 66
typedef struct AutomapHeader {
unsigned char version;
// The size of entire automap database (including header itself).
int dataSize;
// Offsets from the beginning of the automap database file into
// entries data.
//
// These offsets are specified for every map/elevation combination. A value
// of 0 specifies that there is no data for appropriate map/elevation
// combination.
int offsets[AUTOMAP_MAP_COUNT][ELEVATION_COUNT];
} AutomapHeader;
typedef struct AutomapEntry {
int dataSize;
unsigned char isCompressed;
} AutomapEntry;
int automap_init();
int automap_reset();
void automap_exit();
int automap_load(DB_FILE* stream);
int automap_save(DB_FILE* stream);
void automap(bool isInGame, bool isUsingScanner);
int draw_top_down_map_pipboy(int win, int map, int elevation);
int automap_pip_save();
int YesWriteIndex(int mapIndex, int elevation);
int ReadAMList(AutomapHeader** automapHeaderPtr);
} // namespace fallout
#endif /* FALLOUT_GAME_AUTOMAP_H_ */

1411
src/game/bmpdlog.cc Normal file

File diff suppressed because it is too large Load Diff

53
src/game/bmpdlog.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef FALLOUT_GAME_BMPDLOG_H_
#define FALLOUT_GAME_BMPDLOG_H_
namespace fallout {
typedef enum DialogBoxOptions {
DIALOG_BOX_LARGE = 0x01,
DIALOG_BOX_MEDIUM = 0x02,
DIALOG_BOX_NO_HORIZONTAL_CENTERING = 0x04,
DIALOG_BOX_NO_VERTICAL_CENTERING = 0x08,
DIALOG_BOX_YES_NO = 0x10,
DIALOG_BOX_0x20 = 0x20,
} DialogBoxOptions;
typedef enum DialogType {
DIALOG_TYPE_MEDIUM,
DIALOG_TYPE_LARGE,
DIALOG_TYPE_COUNT,
} DialogType;
typedef enum FileDialogFrm {
FILE_DIALOG_FRM_BACKGROUND,
FILE_DIALOG_FRM_LITTLE_RED_BUTTON_NORMAL,
FILE_DIALOG_FRM_LITTLE_RED_BUTTON_PRESSED,
FILE_DIALOG_FRM_SCROLL_DOWN_ARROW_NORMAL,
FILE_DIALOG_FRM_SCROLL_DOWN_ARROW_PRESSED,
FILE_DIALOG_FRM_SCROLL_UP_ARROW_NORMAL,
FILE_DIALOG_FRM_SCROLL_UP_ARROW_PRESSED,
FILE_DIALOG_FRM_COUNT,
} FileDialogFrm;
typedef enum FileDialogScrollDirection {
FILE_DIALOG_SCROLL_DIRECTION_NONE,
FILE_DIALOG_SCROLL_DIRECTION_UP,
FILE_DIALOG_SCROLL_DIRECTION_DOWN,
} FileDialogScrollDirection;
extern int dbox[DIALOG_TYPE_COUNT];
extern int ytable[DIALOG_TYPE_COUNT];
extern int xtable[DIALOG_TYPE_COUNT];
extern int doneY[DIALOG_TYPE_COUNT];
extern int doneX[DIALOG_TYPE_COUNT];
extern int dblines[DIALOG_TYPE_COUNT];
extern int flgids[FILE_DIALOG_FRM_COUNT];
extern int flgids2[FILE_DIALOG_FRM_COUNT];
int dialog_out(const char* title, const char** body, int bodyLength, int x, int y, int titleColor, const char* a8, int bodyColor, int flags);
int file_dialog(char* title, char** fileList, char* dest, int fileListLength, int x, int y, int flags);
int save_file_dialog(char* title, char** fileList, char* dest, int fileListLength, int x, int y, int flags);
} // namespace fallout
#endif /* FALLOUT_GAME_BMPDLOG_H_ */

779
src/game/cache.cc Normal file
View File

@ -0,0 +1,779 @@
#include "game/cache.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "int/sound.h"
#include "plib/gnw/debug.h"
#include "plib/gnw/memory.h"
namespace fallout {
static bool cache_add(Cache* cache, int key, int* indexPtr);
static bool cache_insert(Cache* cache, CacheEntry* cacheEntry, int index);
static int cache_find(Cache* cache, int key, int* indexPtr);
static int cache_create_item(CacheEntry** cacheEntryPtr);
static bool cache_init_item(CacheEntry* cacheEntry);
static bool cache_destroy_item(Cache* cache, CacheEntry* cacheEntry);
static bool cache_unlock_all(Cache* cache);
static bool cache_reset_counter(Cache* cache);
static bool cache_make_room(Cache* cache, int size);
static bool cache_purge(Cache* cache);
static bool cache_resize_array(Cache* cache, int newCapacity);
static int cache_compare_make_room(const void* a1, const void* a2);
static int cache_compare_reset_counter(const void* a1, const void* a2);
// 0x4FEC7C
static int lock_sound_ticker = 0;
// 0x41E9C0
bool cache_init(Cache* cache, CacheSizeProc* sizeProc, CacheReadProc* readProc, CacheFreeProc* freeProc, int maxSize)
{
if (!heap_init(&(cache->heap), maxSize)) {
return false;
}
cache->size = 0;
cache->maxSize = maxSize;
cache->entriesLength = 0;
cache->entriesCapacity = CACHE_ENTRIES_INITIAL_CAPACITY;
cache->hits = 0;
cache->entries = (CacheEntry**)mem_malloc(sizeof(*cache->entries) * cache->entriesCapacity);
cache->sizeProc = sizeProc;
cache->readProc = readProc;
cache->freeProc = freeProc;
if (cache->entries == NULL) {
return false;
}
memset(cache->entries, 0, sizeof(*cache->entries) * cache->entriesCapacity);
return true;
}
// 0x41EA50
bool cache_exit(Cache* cache)
{
if (cache == NULL) {
return false;
}
cache_unlock_all(cache);
cache_flush(cache);
heap_exit(&(cache->heap));
cache->size = 0;
cache->maxSize = 0;
cache->entriesLength = 0;
cache->entriesCapacity = 0;
cache->hits = 0;
if (cache->entries != NULL) {
mem_free(cache->entries);
cache->entries = NULL;
}
cache->sizeProc = NULL;
cache->readProc = NULL;
cache->freeProc = NULL;
return true;
}
// 0x41EAC0
int cache_query(Cache* cache, int key)
{
int index;
if (cache == NULL) {
return 0;
}
if (cache_find(cache, key, &index) != 2) {
return 0;
}
return 1;
}
// 0x41EAE8
bool cache_lock(Cache* cache, int key, void** data, CacheEntry** cacheEntryPtr)
{
if (cache == NULL || data == NULL || cacheEntryPtr == NULL) {
return false;
}
*cacheEntryPtr = NULL;
int index;
int rc = cache_find(cache, key, &index);
if (rc == 2) {
// Use existing cache entry.
CacheEntry* cacheEntry = cache->entries[index];
cacheEntry->hits++;
} else if (rc == 3) {
// New cache entry is required.
if (cache->entriesLength >= INT_MAX) {
return false;
}
if (!cache_add(cache, key, &index)) {
return false;
}
lock_sound_ticker %= 4;
if (lock_sound_ticker == 0) {
soundUpdate();
}
} else {
return false;
}
CacheEntry* cacheEntry = cache->entries[index];
if (cacheEntry->referenceCount == 0) {
if (!heap_lock(&(cache->heap), cacheEntry->heapHandleIndex, &(cacheEntry->data))) {
return false;
}
}
cacheEntry->referenceCount++;
cache->hits++;
cacheEntry->mru = cache->hits;
if (cache->hits == UINT_MAX) {
cache_reset_counter(cache);
}
*data = cacheEntry->data;
*cacheEntryPtr = cacheEntry;
return true;
}
// 0x41EDB8
bool cache_unlock(Cache* cache, CacheEntry* cacheEntry)
{
if (cache == NULL || cacheEntry == NULL) {
return false;
}
if (cacheEntry->referenceCount == 0) {
return false;
}
cacheEntry->referenceCount--;
if (cacheEntry->referenceCount == 0) {
heap_unlock(&(cache->heap), cacheEntry->heapHandleIndex);
}
return true;
}
// 0x41EDEC
int cache_discard(Cache* cache, int key)
{
int index;
CacheEntry* cacheEntry;
if (cache == NULL) {
return 0;
}
if (cache_find(cache, key, &index) != 2) {
return 0;
}
cacheEntry = cache->entries[index];
if (cacheEntry->referenceCount != 0) {
return 0;
}
cacheEntry->flags |= CACHE_ENTRY_MARKED_FOR_EVICTION;
cache_purge(cache);
return 1;
}
// 0x41EE2C
bool cache_flush(Cache* cache)
{
if (cache == NULL) {
return false;
}
// Loop thru cache entries and mark those with no references for eviction.
for (int index = 0; index < cache->entriesLength; index++) {
CacheEntry* cacheEntry = cache->entries[index];
if (cacheEntry->referenceCount == 0) {
cacheEntry->flags |= CACHE_ENTRY_MARKED_FOR_EVICTION;
}
}
// Sweep cache entries marked earlier.
cache_purge(cache);
// Shrink cache entries array if it's too big.
int optimalCapacity = cache->entriesLength + CACHE_ENTRIES_GROW_CAPACITY;
if (optimalCapacity < cache->entriesCapacity) {
cache_resize_array(cache, optimalCapacity);
}
return true;
}
// 0x41EE84
int cache_size(Cache* cache, int* sizePtr)
{
if (cache == NULL) {
return 0;
}
if (sizePtr == NULL) {
return 0;
}
*sizePtr = cache->size;
return 1;
}
// 0x41EE9C
bool cache_stats(Cache* cache, char* dest, size_t size)
{
if (cache == NULL || dest == NULL) {
return false;
}
snprintf(dest, size, "Cache stats are disabled.%s", "\n");
return true;
}
// 0x41EEC0
int cache_create_list(Cache* cache, unsigned int a2, int** tagsPtr, int* tagsLengthPtr)
{
int cacheItemIndex;
int tagIndex;
if (cache == NULL) {
return 0;
}
if (tagsPtr == NULL) {
return 0;
}
if (tagsLengthPtr == NULL) {
return 0;
}
*tagsLengthPtr = 0;
switch (a2) {
case CACHE_LIST_REQUEST_TYPE_ALL_ITEMS:
*tagsPtr = (int*)mem_malloc(sizeof(*tagsPtr) * cache->entriesLength);
if (*tagsPtr == NULL) {
return 0;
}
for (cacheItemIndex = 0; cacheItemIndex < cache->entriesLength; cacheItemIndex++) {
(*tagsPtr)[cacheItemIndex] = cache->entries[cacheItemIndex]->key;
}
*tagsLengthPtr = cache->entriesLength;
break;
case CACHE_LIST_REQUEST_TYPE_LOCKED_ITEMS:
for (cacheItemIndex = 0; cacheItemIndex < cache->entriesLength; cacheItemIndex++) {
if (cache->entries[cacheItemIndex]->referenceCount != 0) {
(*tagsLengthPtr)++;
}
}
*tagsPtr = (int*)mem_malloc(sizeof(*tagsPtr) * (*tagsLengthPtr));
if (*tagsPtr == NULL) {
return 0;
}
tagIndex = 0;
for (cacheItemIndex = 0; cacheItemIndex < cache->entriesLength; cacheItemIndex++) {
if (cache->entries[cacheItemIndex]->referenceCount != 0) {
if (tagIndex < *tagsLengthPtr) {
(*tagsPtr)[tagIndex++] = cache->entries[cacheItemIndex]->key;
}
}
}
break;
case CACHE_LIST_REQUEST_TYPE_UNLOCKED_ITEMS:
for (cacheItemIndex = 0; cacheItemIndex < cache->entriesLength; cacheItemIndex++) {
if (cache->entries[cacheItemIndex]->referenceCount == 0) {
(*tagsLengthPtr)++;
}
}
*tagsPtr = (int*)mem_malloc(sizeof(*tagsPtr) * (*tagsLengthPtr));
if (*tagsPtr == NULL) {
return 0;
}
tagIndex = 0;
for (cacheItemIndex = 0; cacheItemIndex < cache->entriesLength; cacheItemIndex++) {
if (cache->entries[cacheItemIndex]->referenceCount == 0) {
if (tagIndex < *tagsLengthPtr) {
(*tagsPtr)[tagIndex++] = cache->entries[cacheItemIndex]->key;
}
}
}
break;
}
return 1;
}
// 0x41F084
int cache_destroy_list(int** tagsPtr)
{
if (tagsPtr == NULL) {
return 0;
}
if (*tagsPtr == NULL) {
return 0;
}
mem_free(*tagsPtr);
*tagsPtr = NULL;
return 1;
}
// Fetches entry for the specified key into the cache.
//
// 0x41F0AC
static bool cache_add(Cache* cache, int key, int* indexPtr)
{
CacheEntry* cacheEntry;
// NOTE: Uninline.
if (cache_create_item(&cacheEntry) != 1) {
return 0;
}
do {
int size;
if (cache->sizeProc(key, &size) != 0) {
break;
}
if (!cache_make_room(cache, size)) {
break;
}
bool allocated = false;
int cacheEntrySize = size;
for (int attempt = 0; attempt < 10; attempt++) {
if (heap_allocate(&(cache->heap), &(cacheEntry->heapHandleIndex), size, 1)) {
allocated = true;
break;
}
cacheEntrySize = (int)((double)cacheEntrySize + (double)size * 0.25);
if (cacheEntrySize > cache->maxSize) {
break;
}
if (!cache_make_room(cache, cacheEntrySize)) {
break;
}
}
if (!allocated) {
cache_flush(cache);
allocated = true;
if (!heap_allocate(&(cache->heap), &(cacheEntry->heapHandleIndex), size, 1)) {
if (!heap_allocate(&(cache->heap), &(cacheEntry->heapHandleIndex), size, 0)) {
allocated = false;
}
}
}
if (!allocated) {
break;
}
do {
if (!heap_lock(&(cache->heap), cacheEntry->heapHandleIndex, &(cacheEntry->data))) {
break;
}
if (cache->readProc(key, &size, cacheEntry->data) != 0) {
break;
}
heap_unlock(&(cache->heap), cacheEntry->heapHandleIndex);
cacheEntry->size = size;
cacheEntry->key = key;
bool isNewKey = true;
if (*indexPtr < cache->entriesLength) {
if (key < cache->entries[*indexPtr]->key) {
if (*indexPtr == 0 || key > cache->entries[*indexPtr - 1]->key) {
isNewKey = false;
}
}
}
if (isNewKey) {
if (cache_find(cache, key, indexPtr) != 3) {
break;
}
}
if (!cache_insert(cache, cacheEntry, *indexPtr)) {
break;
}
return true;
} while (0);
heap_unlock(&(cache->heap), cacheEntry->heapHandleIndex);
} while (0);
// NOTE: Uninline.
cache_destroy_item(cache, cacheEntry);
return false;
}
// 0x41F2E8
static bool cache_insert(Cache* cache, CacheEntry* cacheEntry, int index)
{
// Ensure cache have enough space for new entry.
if (cache->entriesLength == cache->entriesCapacity - 1) {
if (!cache_resize_array(cache, cache->entriesCapacity + CACHE_ENTRIES_GROW_CAPACITY)) {
return false;
}
}
// Move entries below insertion point.
memmove(&(cache->entries[index + 1]), &(cache->entries[index]), sizeof(*cache->entries) * (cache->entriesLength - index));
cache->entries[index] = cacheEntry;
cache->entriesLength++;
cache->size += cacheEntry->size;
return true;
}
// Finds index for given key.
//
// Returns 2 if entry already exists in cache, or 3 if entry does not exist. In
// this case indexPtr represents insertion point.
//
// 0x41F354
static int cache_find(Cache* cache, int key, int* indexPtr)
{
int length = cache->entriesLength;
if (length == 0) {
*indexPtr = 0;
return 3;
}
int r = length - 1;
int l = 0;
int mid;
int cmp;
do {
mid = (l + r) / 2;
cmp = key - cache->entries[mid]->key;
if (cmp == 0) {
*indexPtr = mid;
return 2;
}
if (cmp > 0) {
l = l + 1;
} else {
r = r - 1;
}
} while (r >= l);
if (cmp < 0) {
*indexPtr = mid;
} else {
*indexPtr = mid + 1;
}
return 3;
}
// 0x41F3C0
static int cache_create_item(CacheEntry** cacheEntryPtr)
{
*cacheEntryPtr = (CacheEntry*)mem_malloc(sizeof(**cacheEntryPtr));
// FIXME: Wrong check, should be *cacheEntryPtr != NULL.
if (cacheEntryPtr != NULL) {
// NOTE: Uninline.
return cache_init_item(*cacheEntryPtr);
}
return 0;
}
// 0x41F408
static bool cache_init_item(CacheEntry* cacheEntry)
{
cacheEntry->key = 0;
cacheEntry->size = 0;
cacheEntry->data = NULL;
cacheEntry->referenceCount = 0;
cacheEntry->hits = 0;
cacheEntry->flags = 0;
cacheEntry->mru = 0;
return true;
}
// 0x41F440
static bool cache_destroy_item(Cache* cache, CacheEntry* cacheEntry)
{
if (cacheEntry->data != NULL) {
heap_deallocate(&(cache->heap), &(cacheEntry->heapHandleIndex));
}
mem_free(cacheEntry);
return true;
}
// 0x41F464
static bool cache_unlock_all(Cache* cache)
{
Heap* heap = &(cache->heap);
for (int index = 0; index < cache->entriesLength; index++) {
CacheEntry* cacheEntry = cache->entries[index];
// NOTE: Original code is slightly different. For unknown reason it uses
// inner loop to decrement `referenceCount` one by one. Probably using
// some inlined function.
if (cacheEntry->referenceCount != 0) {
heap_unlock(heap, cacheEntry->heapHandleIndex);
cacheEntry->referenceCount = 0;
}
}
return true;
}
// 0x41F4D4
static bool cache_reset_counter(Cache* cache)
{
if (cache == NULL) {
return false;
}
CacheEntry** entries = (CacheEntry**)mem_malloc(sizeof(*entries) * cache->entriesLength);
if (entries == NULL) {
return false;
}
memcpy(entries, cache->entries, sizeof(*entries) * cache->entriesLength);
qsort(entries, cache->entriesLength, sizeof(*entries), cache_compare_reset_counter);
for (int index = 0; index < cache->entriesLength; index++) {
CacheEntry* cacheEntry = entries[index];
cacheEntry->mru = index;
}
cache->hits = cache->entriesLength;
// FIXME: Obviously leak `entries`.
return true;
}
// Prepare cache for storing new entry with the specified size.
//
// 0x41F54C
static bool cache_make_room(Cache* cache, int size)
{
if (size > cache->maxSize) {
// The entry of given size is too big for caching, no matter what.
return false;
}
if (cache->maxSize - cache->size >= size) {
// There is space available for entry of given size, there is no need to
// evict anything.
return true;
}
CacheEntry** entries = (CacheEntry**)mem_malloc(sizeof(*entries) * cache->entriesLength);
if (entries != NULL) {
memcpy(entries, cache->entries, sizeof(*entries) * cache->entriesLength);
qsort(entries, cache->entriesLength, sizeof(*entries), cache_compare_make_room);
// The sweeping threshold is 20% of cache size plus size for the new
// entry. Once the threshold is reached the marking process stops.
int threshold = size + (int)((double)cache->size * 0.2);
int accum = 0;
int index;
for (index = 0; index < cache->entriesLength; index++) {
CacheEntry* entry = entries[index];
if (entry->referenceCount == 0) {
if (entry->size >= threshold) {
entry->flags |= CACHE_ENTRY_MARKED_FOR_EVICTION;
// We've just found one huge entry, there is no point to
// mark individual smaller entries in the code path below,
// reset the accumulator to skip it entirely.
accum = 0;
break;
} else {
accum += entry->size;
if (accum >= threshold) {
break;
}
}
}
}
if (accum != 0) {
// The loop below assumes index to be positioned on the entry, where
// accumulator stopped. If we've reached the end, reposition
// it to the last entry.
if (index == cache->entriesLength) {
index -= 1;
}
// Loop backwards from the point we've stopped and mark all
// unreferenced entries for sweeping.
for (; index >= 0; index--) {
CacheEntry* entry = entries[index];
if (entry->referenceCount == 0) {
entry->flags |= CACHE_ENTRY_MARKED_FOR_EVICTION;
}
}
}
mem_free(entries);
}
cache_purge(cache);
if (cache->maxSize - cache->size >= size) {
return true;
}
return false;
}
// 0x41F69C
static bool cache_purge(Cache* cache)
{
for (int index = 0; index < cache->entriesLength; index++) {
CacheEntry* cacheEntry = cache->entries[index];
if ((cacheEntry->flags & CACHE_ENTRY_MARKED_FOR_EVICTION) != 0) {
if (cacheEntry->referenceCount != 0) {
// Entry was marked for eviction but still has references,
// unmark it.
cacheEntry->flags &= ~CACHE_ENTRY_MARKED_FOR_EVICTION;
} else {
int cacheEntrySize = cacheEntry->size;
// NOTE: Uninline.
cache_destroy_item(cache, cacheEntry);
// Move entries up.
memmove(&(cache->entries[index]), &(cache->entries[index + 1]), sizeof(*cache->entries) * ((cache->entriesLength - index) - 1));
cache->entriesLength--;
cache->size -= cacheEntrySize;
// The entry was removed, compensate index.
index--;
}
}
}
return true;
}
// 0x41F740
static bool cache_resize_array(Cache* cache, int newCapacity)
{
if (newCapacity < cache->entriesLength) {
return false;
}
CacheEntry** entries = (CacheEntry**)mem_realloc(cache->entries, sizeof(*cache->entries) * newCapacity);
if (entries == NULL) {
return false;
}
cache->entries = entries;
cache->entriesCapacity = newCapacity;
return true;
}
// 0x41F774
static int cache_compare_make_room(const void* a1, const void* a2)
{
CacheEntry* v1 = *(CacheEntry**)a1;
CacheEntry* v2 = *(CacheEntry**)a2;
if (v1->referenceCount != 0 && v2->referenceCount == 0) {
return 1;
}
if (v2->referenceCount != 0 && v1->referenceCount == 0) {
return -1;
}
if (v1->hits < v2->hits) {
return -1;
} else if (v1->hits > v2->hits) {
return 1;
}
if (v1->mru < v2->mru) {
return -1;
} else if (v1->mru > v2->mru) {
return 1;
}
return 0;
}
// 0x41F7E8
static int cache_compare_reset_counter(const void* a1, const void* a2)
{
CacheEntry* v1 = *(CacheEntry**)a1;
CacheEntry* v2 = *(CacheEntry**)a2;
if (v1->mru < v2->mru) {
return 1;
} else if (v1->mru > v2->mru) {
return -1;
} else {
return 0;
}
}
} // namespace fallout

92
src/game/cache.h Normal file
View File

@ -0,0 +1,92 @@
#ifndef FALLOUT_GAME_CACHE_H_
#define FALLOUT_GAME_CACHE_H_
#include <stddef.h>
#include "game/heap.h"
namespace fallout {
#define INVALID_CACHE_ENTRY ((CacheEntry*)-1)
// The initial number of cache entries in new cache.
#define CACHE_ENTRIES_INITIAL_CAPACITY 100
// The number of cache entries added when cache capacity is reached.
#define CACHE_ENTRIES_GROW_CAPACITY 50
typedef enum CacheEntryFlags {
// Specifies that cache entry has no references as should be evicted during
// the next sweep operation.
CACHE_ENTRY_MARKED_FOR_EVICTION = 0x01,
} CacheEntryFlags;
typedef enum CacheListRequestType {
CACHE_LIST_REQUEST_TYPE_ALL_ITEMS = 0,
CACHE_LIST_REQUEST_TYPE_LOCKED_ITEMS = 1,
CACHE_LIST_REQUEST_TYPE_UNLOCKED_ITEMS = 2,
} CacheListRequestType;
typedef int CacheSizeProc(int key, int* sizePtr);
typedef int CacheReadProc(int key, int* sizePtr, unsigned char* buffer);
typedef void CacheFreeProc(void* ptr);
typedef struct CacheEntry {
int key;
int size;
unsigned char* data;
unsigned int referenceCount;
// Total number of hits that this cache entry received during it's
// lifetime.
unsigned int hits;
unsigned int flags;
// The most recent hit in terms of cache hit counter. Used to track most
// recently used entries in eviction strategy.
unsigned int mru;
int heapHandleIndex;
} CacheEntry;
typedef struct Cache {
// Current size of entries in cache.
int size;
// Maximum size of entries in cache.
int maxSize;
// The length of `entries` array.
int entriesLength;
// The capacity of `entries` array.
int entriesCapacity;
// Total number of hits during cache lifetime.
unsigned int hits;
// List of cache entries.
CacheEntry** entries;
CacheSizeProc* sizeProc;
CacheReadProc* readProc;
CacheFreeProc* freeProc;
Heap heap;
} Cache;
bool cache_init(Cache* cache, CacheSizeProc* sizeProc, CacheReadProc* readProc, CacheFreeProc* freeProc, int maxSize);
bool cache_exit(Cache* cache);
int cache_query(Cache* cache, int key);
bool cache_lock(Cache* cache, int key, void** data, CacheEntry** cacheEntryPtr);
bool cache_unlock(Cache* cache, CacheEntry* cacheEntry);
int cache_discard(Cache* cache, int key);
bool cache_flush(Cache* cache);
int cache_size(Cache* cache, int* sizePtr);
bool cache_stats(Cache* cache, char* dest, size_t size);
int cache_create_list(Cache* cache, unsigned int a2, int** tagsPtr, int* tagsLengthPtr);
int cache_destroy_list(int** tagsPtr);
} // namespace fallout
#endif /* FALLOUT_GAME_CACHE_H_ */

4760
src/game/combat.cc Normal file

File diff suppressed because it is too large Load Diff

68
src/game/combat.h Normal file
View File

@ -0,0 +1,68 @@
#ifndef FALLOUT_GAME_COMBAT_H_
#define FALLOUT_GAME_COMBAT_H_
#include "game/anim.h"
#include "game/combat_defs.h"
#include "game/message.h"
#include "game/object_types.h"
#include "game/party.h"
#include "game/proto_types.h"
#include "plib/db/db.h"
namespace fallout {
extern unsigned int combat_state;
extern STRUCT_664980* gcsd;
extern bool combat_call_display;
extern int cf_table[WEAPON_CRITICAL_FAILURE_TYPE_COUNT][WEAPON_CRITICAL_FAILURE_EFFECT_COUNT];
extern MessageList combat_message_file;
extern Object* combat_turn_obj;
extern int combat_exps;
extern int combat_free_move;
int combat_init();
void combat_reset();
void combat_exit();
int find_cid(int start, int cid, Object** critterList, int critterListLength);
int combat_load(DB_FILE* stream);
int combat_save(DB_FILE* stream);
Object* combat_whose_turn();
void combat_data_init(Object* obj);
void combat_over_from_load();
void combat_give_exps(int exp_points);
int combat_in_range(Object* critter);
void combat_end();
void combat_turn_run();
void combat_end_turn();
void combat(STRUCT_664980* attack);
void combat_ctd_init(Attack* attack, Object* attacker, Object* defender, int hitMode, int hitLocation);
int combat_attack(Object* a1, Object* a2, int hitMode, int location);
int combat_bullet_start(const Object* a1, const Object* a2);
void compute_explosion_on_extras(Attack* attack, int a2, bool isGrenade, int a4);
int determine_to_hit(Object* a1, Object* a2, int hitLocation, int hitMode);
int determine_to_hit_no_range(Object* a1, Object* a2, int hitLocation, int hitMode);
void death_checks(Attack* attack);
void apply_damage(Attack* attack, bool animated);
void combat_display(Attack* attack);
void combat_anim_begin();
void combat_anim_finished();
int combat_check_bad_shot(Object* attacker, Object* defender, int hitMode, bool aiming);
bool combat_to_hit(Object* target, int* accuracy);
void combat_attack_this(Object* a1);
void combat_outline_on();
void combat_outline_off();
void combat_highlight_change();
bool combat_is_shot_blocked(Object* a1, int from, int to, Object* a4, int* a5);
int combat_player_knocked_out_by();
int combat_explode_scenery(Object* a1, Object* a2);
void combat_delete_critter(Object* obj);
static inline bool isInCombat()
{
return (combat_state & COMBAT_STATE_0x01) != 0;
}
} // namespace fallout
#endif /* FALLOUT_GAME_COMBAT_H_ */

160
src/game/combat_defs.h Normal file
View File

@ -0,0 +1,160 @@
#ifndef FALLOUT_GAME_COMBAT_DEFS_H_
#define FALLOUT_GAME_COMBAT_DEFS_H_
#include "game/object_types.h"
namespace fallout {
#define EXPLOSION_TARGET_COUNT (6)
#define CRTICIAL_EFFECT_COUNT (6)
#define WEAPON_CRITICAL_FAILURE_TYPE_COUNT (7)
#define WEAPON_CRITICAL_FAILURE_EFFECT_COUNT (5)
typedef enum CombatState {
COMBAT_STATE_0x01 = 0x01,
COMBAT_STATE_0x02 = 0x02,
COMBAT_STATE_0x08 = 0x08,
} CombatState;
typedef enum HitMode {
HIT_MODE_LEFT_WEAPON_PRIMARY = 0,
HIT_MODE_LEFT_WEAPON_SECONDARY = 1,
HIT_MODE_RIGHT_WEAPON_PRIMARY = 2,
HIT_MODE_RIGHT_WEAPON_SECONDARY = 3,
HIT_MODE_PUNCH = 4,
HIT_MODE_KICK = 5,
HIT_MODE_LEFT_WEAPON_RELOAD = 6,
HIT_MODE_RIGHT_WEAPON_RELOAD = 7,
// Punch Level 2
HIT_MODE_STRONG_PUNCH = 8,
// Punch Level 3
HIT_MODE_HAMMER_PUNCH = 9,
// Punch Level 4 aka 'Lightning Punch'
HIT_MODE_HAYMAKER = 10,
// Punch Level 5 aka 'Chop Punch'
HIT_MODE_JAB = 11,
// Punch Level 6 aka 'Dragon Punch'
HIT_MODE_PALM_STRIKE = 12,
// Punch Level 7 aka 'Force Punch'
HIT_MODE_PIERCING_STRIKE = 13,
// Kick Level 2
HIT_MODE_STRONG_KICK = 14,
// Kick Level 3
HIT_MODE_SNAP_KICK = 15,
// Kick Level 4 aka 'Roundhouse Kick'
HIT_MODE_POWER_KICK = 16,
// Kick Level 5
HIT_MODE_HIP_KICK = 17,
// Kick Level 6 aka 'Jump Kick'
HIT_MODE_HOOK_KICK = 18,
// Kick Level 7 aka 'Death Blossom Kick'
HIT_MODE_PIERCING_KICK = 19,
HIT_MODE_COUNT,
FIRST_ADVANCED_PUNCH_HIT_MODE = HIT_MODE_STRONG_PUNCH,
LAST_ADVANCED_PUNCH_HIT_MODE = HIT_MODE_PIERCING_STRIKE,
FIRST_ADVANCED_KICK_HIT_MODE = HIT_MODE_STRONG_KICK,
LAST_ADVANCED_KICK_HIT_MODE = HIT_MODE_PIERCING_KICK,
FIRST_ADVANCED_UNARMED_HIT_MODE = FIRST_ADVANCED_PUNCH_HIT_MODE,
LAST_ADVANCED_UNARMED_HIT_MODE = LAST_ADVANCED_KICK_HIT_MODE,
} HitMode;
typedef enum HitLocation {
HIT_LOCATION_HEAD,
HIT_LOCATION_LEFT_ARM,
HIT_LOCATION_RIGHT_ARM,
HIT_LOCATION_TORSO,
HIT_LOCATION_RIGHT_LEG,
HIT_LOCATION_LEFT_LEG,
HIT_LOCATION_EYES,
HIT_LOCATION_GROIN,
HIT_LOCATION_UNCALLED,
HIT_LOCATION_COUNT,
HIT_LOCATION_SPECIFIC_COUNT = HIT_LOCATION_COUNT - 1,
} HitLocation;
typedef struct STRUCT_664980 {
Object* attacker;
Object* defender;
int actionPointsBonus;
int accuracyBonus;
int damageBonus;
int minDamage;
int maxDamage;
int field_1C; // probably bool, indicating field_20 and field_24 used
int field_20; // flags on attacker
int field_24; // flags on defender
} STRUCT_664980;
typedef struct Attack {
Object* attacker;
int hitMode;
Object* weapon;
int attackHitLocation;
int attackerDamage;
int attackerFlags;
int ammoQuantity;
int criticalMessageId;
Object* defender;
int tile;
int defenderHitLocation;
int defenderDamage;
int defenderFlags;
int defenderKnockback;
Object* oops;
int extrasLength;
Object* extras[EXPLOSION_TARGET_COUNT];
int extrasHitLocation[EXPLOSION_TARGET_COUNT];
int extrasDamage[EXPLOSION_TARGET_COUNT];
int extrasFlags[EXPLOSION_TARGET_COUNT];
int extrasKnockback[EXPLOSION_TARGET_COUNT];
} Attack;
// Provides metadata about critical hit effect.
typedef struct CriticalHitDescription {
int damageMultiplier;
// Damage flags that will be applied to defender.
int flags;
// Stat to check to upgrade this critical hit to massive critical hit or
// -1 if there is no massive critical hit.
int massiveCriticalStat;
// Bonus/penalty to massive critical stat.
int massiveCriticalStatModifier;
// Additional damage flags if this critical hit become massive critical.
int massiveCriticalFlags;
int messageId;
int massiveCriticalMessageId;
} CriticalHitDescription;
typedef enum CombatBadShot {
COMBAT_BAD_SHOT_OK = 0,
COMBAT_BAD_SHOT_NO_AMMO = 1,
COMBAT_BAD_SHOT_OUT_OF_RANGE = 2,
COMBAT_BAD_SHOT_NOT_ENOUGH_AP = 3,
COMBAT_BAD_SHOT_ALREADY_DEAD = 4,
COMBAT_BAD_SHOT_AIM_BLOCKED = 5,
COMBAT_BAD_SHOT_ARM_CRIPPLED = 6,
COMBAT_BAD_SHOT_BOTH_ARMS_CRIPPLED = 7,
} CombatBadShot;
} // namespace fallout
#endif /* FALLOUT_GAME_COMBAT_DEFS_H_ */

1708
src/game/combatai.cc Normal file

File diff suppressed because it is too large Load Diff

69
src/game/combatai.h Normal file
View File

@ -0,0 +1,69 @@
#ifndef FALLOUT_GAME_COMBATAI_H_
#define FALLOUT_GAME_COMBATAI_H_
#include <stddef.h>
#include "game/combat_defs.h"
#include "game/message.h"
#include "game/object_types.h"
#include "game/party.h"
#include "plib/db/db.h"
namespace fallout {
typedef enum AiMessageType {
AI_MESSAGE_TYPE_RUN,
AI_MESSAGE_TYPE_MOVE,
AI_MESSAGE_TYPE_ATTACK,
AI_MESSAGE_TYPE_MISS,
AI_MESSAGE_TYPE_HIT,
} AiMessageType;
typedef struct AiPacket {
char* name;
int packet_num;
int max_dist;
int min_to_hit;
int min_hp;
int aggression;
int hurt_too_much;
int secondary_freq;
int called_freq;
int font;
int color;
int outline_color;
int chance;
int run_start;
int move_start;
int attack_start;
int miss_start;
int hit_start[HIT_LOCATION_SPECIFIC_COUNT];
int last_msg;
} AiPacket;
int combat_ai_init();
void combat_ai_reset();
int combat_ai_exit();
int combat_ai_load(DB_FILE* stream);
int combat_ai_save(DB_FILE* stream);
int combat_ai_num();
char* combat_ai_name(int packetNum);
Object* ai_danger_source(Object* critter);
Object* ai_search_inven(Object* critter, int check_action_points);
void combat_ai_begin(int critters_count, Object** critters);
void combat_ai_over();
Object* combat_ai(Object* critter, Object* target);
bool combatai_want_to_join(Object* critter);
bool combatai_want_to_stop(Object* critter);
int combatai_switch_team(Object* critter, int team);
int combatai_msg(Object* critter, Attack* attack, int message_type, int delay);
Object* combat_ai_random_target(Attack* attack);
void combatai_check_retaliation(Object* critter, Object* candidate);
bool is_within_perception(Object* critter1, Object* critter2);
void combatai_refresh_messages();
void combatai_notify_onlookers(Object* critter);
void combatai_delete_critter(Object* critter);
} // namespace fallout
#endif /* FALLOUT_GAME_COMBATAI_H_ */

534
src/game/config.cc Normal file
View File

@ -0,0 +1,534 @@
#include "game/config.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform_compat.h"
#include "plib/db/db.h"
#include "plib/gnw/memory.h"
namespace fallout {
#define CONFIG_FILE_MAX_LINE_LENGTH 256
// The initial number of sections (or key-value) pairs in the config.
#define CONFIG_INITIAL_CAPACITY 10
static bool config_parse_line(Config* config, char* string);
static bool config_split_line(char* string, char* key, char* value);
static bool config_add_section(Config* config, const char* sectionKey);
static bool config_strip_white_space(char* string);
// 0x426540
bool config_init(Config* config)
{
if (config == NULL) {
return false;
}
if (assoc_init(config, CONFIG_INITIAL_CAPACITY, sizeof(ConfigSection), NULL) != 0) {
return false;
}
return true;
}
// 0x42656C
void config_exit(Config* config)
{
if (config == NULL) {
return;
}
for (int sectionIndex = 0; sectionIndex < config->size; sectionIndex++) {
assoc_pair* sectionEntry = &(config->list[sectionIndex]);
ConfigSection* section = (ConfigSection*)sectionEntry->data;
for (int keyValueIndex = 0; keyValueIndex < section->size; keyValueIndex++) {
assoc_pair* keyValueEntry = &(section->list[keyValueIndex]);
char** value = (char**)keyValueEntry->data;
mem_free(*value);
*value = NULL;
}
assoc_free(section);
}
assoc_free(config);
}
// Parses command line argments and adds them into the config.
//
// The expected format of [argv] elements are "[section]key=value", otherwise
// the element is silently ignored.
//
// NOTE: This function trims whitespace in key-value pair, but not in section.
// I don't know if this is intentional or it's bug.
//
// 0x4265D0
bool config_cmd_line_parse(Config* config, int argc, char** argv)
{
if (config == NULL) {
return false;
}
for (int arg = 0; arg < argc; arg++) {
char* pch;
char* string = argv[arg];
// Find opening bracket.
pch = strchr(string, '[');
if (pch == NULL) {
continue;
}
char* sectionKey = pch + 1;
// Find closing bracket.
pch = strchr(sectionKey, ']');
if (pch == NULL) {
continue;
}
*pch = '\0';
char key[260];
char value[260];
if (config_split_line(pch + 1, key, value)) {
if (!config_set_string(config, sectionKey, key, value)) {
*pch = ']';
return false;
}
}
*pch = ']';
}
return true;
}
// 0x4266E0
bool config_get_string(Config* config, const char* sectionKey, const char* key, char** valuePtr)
{
if (config == NULL || sectionKey == NULL || key == NULL || valuePtr == NULL) {
return false;
}
int sectionIndex = assoc_search(config, sectionKey);
if (sectionIndex == -1) {
return false;
}
assoc_pair* sectionEntry = &(config->list[sectionIndex]);
ConfigSection* section = (ConfigSection*)sectionEntry->data;
int index = assoc_search(section, key);
if (index == -1) {
return false;
}
assoc_pair* keyValueEntry = &(section->list[index]);
*valuePtr = *(char**)keyValueEntry->data;
return true;
}
// 0x426728
bool config_set_string(Config* config, const char* sectionKey, const char* key, const char* value)
{
if (config == NULL || sectionKey == NULL || key == NULL || value == NULL) {
return false;
}
int sectionIndex = assoc_search(config, sectionKey);
if (sectionIndex == -1) {
if (!config_add_section(config, sectionKey)) {
return false;
}
sectionIndex = assoc_search(config, sectionKey);
}
assoc_pair* sectionEntry = &(config->list[sectionIndex]);
ConfigSection* section = (ConfigSection*)sectionEntry->data;
int index = assoc_search(section, key);
if (index != -1) {
assoc_pair* keyValueEntry = &(section->list[index]);
char** existingValue = (char**)keyValueEntry->data;
mem_free(*existingValue);
*existingValue = NULL;
assoc_delete(section, key);
}
char* valueCopy = mem_strdup(value);
if (valueCopy == NULL) {
return false;
}
if (assoc_insert(section, key, &valueCopy) == -1) {
mem_free(valueCopy);
return false;
}
return true;
}
// 0x4267DC
bool config_get_value(Config* config, const char* sectionKey, const char* key, int* valuePtr)
{
if (valuePtr == NULL) {
return false;
}
char* stringValue;
if (!config_get_string(config, sectionKey, key, &stringValue)) {
return false;
}
*valuePtr = atoi(stringValue);
return true;
}
// 0x426810
bool config_get_values(Config* config, const char* sectionKey, const char* key, int* arr, int count)
{
if (arr == NULL || count < 2) {
return false;
}
char* string;
if (!config_get_string(config, sectionKey, key, &string)) {
return false;
}
char temp[CONFIG_FILE_MAX_LINE_LENGTH];
string = strncpy(temp, string, CONFIG_FILE_MAX_LINE_LENGTH - 1);
while (1) {
char* pch = strchr(string, ',');
if (pch == NULL) {
break;
}
count--;
if (count == 0) {
break;
}
*pch = '\0';
*arr++ = atoi(string);
string = pch + 1;
}
if (count <= 1) {
*arr = atoi(string);
return true;
}
return false;
}
// 0x4268E0
bool config_set_value(Config* config, const char* sectionKey, const char* key, int value)
{
char stringValue[20];
compat_itoa(value, stringValue, 10);
return config_set_string(config, sectionKey, key, stringValue);
}
// Reads .INI file into config.
//
// 0x426A00
bool config_load(Config* config, const char* filePath, bool isDb)
{
if (config == NULL || filePath == NULL) {
return false;
}
char string[CONFIG_FILE_MAX_LINE_LENGTH];
if (isDb) {
DB_FILE* stream = db_fopen(filePath, "rb");
if (stream != NULL) {
while (db_fgets(string, sizeof(string), stream) != NULL) {
config_parse_line(config, string);
}
db_fclose(stream);
}
} else {
FILE* stream = fopen(filePath, "rt");
if (stream != NULL) {
while (fgets(string, sizeof(string), stream) != NULL) {
config_parse_line(config, string);
}
fclose(stream);
}
// FIXME: This function returns `true` even if the file was not actually
// read. I'm pretty sure it's bug.
}
return true;
}
// Writes config into .INI file.
//
// 0x426AA4
bool config_save(Config* config, const char* filePath, bool isDb)
{
if (config == NULL || filePath == NULL) {
return false;
}
if (isDb) {
DB_FILE* stream = db_fopen(filePath, "wt");
if (stream == NULL) {
return false;
}
for (int sectionIndex = 0; sectionIndex < config->size; sectionIndex++) {
assoc_pair* sectionEntry = &(config->list[sectionIndex]);
db_fprintf(stream, "[%s]\n", sectionEntry->name);
ConfigSection* section = (ConfigSection*)sectionEntry->data;
for (int index = 0; index < section->size; index++) {
assoc_pair* keyValueEntry = &(section->list[index]);
db_fprintf(stream, "%s=%s\n", keyValueEntry->name, *(char**)keyValueEntry->data);
}
db_fprintf(stream, "\n");
}
db_fclose(stream);
} else {
FILE* stream = fopen(filePath, "wt");
if (stream == NULL) {
return false;
}
for (int sectionIndex = 0; sectionIndex < config->size; sectionIndex++) {
assoc_pair* sectionEntry = &(config->list[sectionIndex]);
fprintf(stream, "[%s]\n", sectionEntry->name);
ConfigSection* section = (ConfigSection*)sectionEntry->data;
for (int index = 0; index < section->size; index++) {
assoc_pair* keyValueEntry = &(section->list[index]);
fprintf(stream, "%s=%s\n", keyValueEntry->name, *(char**)keyValueEntry->data);
}
fprintf(stream, "\n");
}
fclose(stream);
}
return true;
}
// Parses a line from .INI file into config.
//
// A line either contains a "[section]" section key or "key=value" pair. In the
// first case section key is not added to config immediately, instead it is
// stored in |section| for later usage. This prevents empty
// sections in the config.
//
// In case of key-value pair it pretty straight forward - it adds key-value
// pair into previously read section key stored in |section|.
//
// Returns `true` when a section was parsed or key-value pair was parsed and
// added to the config, or `false` otherwise.
//
// 0x426C3C
static bool config_parse_line(Config* config, char* string)
{
// 0x504C28
static char section[CONFIG_FILE_MAX_LINE_LENGTH] = "unknown";
char* pch;
// Find comment marker and truncate the string.
pch = strchr(string, ';');
if (pch != NULL) {
*pch = '\0';
}
// Find opening bracket.
pch = strchr(string, '[');
if (pch != NULL) {
char* sectionKey = pch + 1;
// Find closing bracket.
pch = strchr(sectionKey, ']');
if (pch != NULL) {
*pch = '\0';
strcpy(section, sectionKey);
return config_strip_white_space(section);
}
}
char key[260];
char value[260];
if (!config_split_line(string, key, value)) {
return false;
}
return config_set_string(config, section, key, value);
}
// Splits "key=value" pair from [string] and copy appropriate parts into [key]
// and [value] respectively.
//
// Both key and value are trimmed.
//
// 0x426D14
static bool config_split_line(char* string, char* key, char* value)
{
if (string == NULL || key == NULL || value == NULL) {
return false;
}
// Find equals character.
char* pch = strchr(string, '=');
if (pch == NULL) {
return false;
}
*pch = '\0';
strcpy(key, string);
strcpy(value, pch + 1);
*pch = '=';
config_strip_white_space(key);
config_strip_white_space(value);
return true;
}
// Ensures the config has a section with specified key.
//
// Return `true` if section exists or it was successfully added, or `false`
// otherwise.
//
// 0x426DB8
static bool config_add_section(Config* config, const char* sectionKey)
{
if (config == NULL || sectionKey == NULL) {
return false;
}
if (assoc_search(config, sectionKey) != -1) {
// Section already exists, no need to do anything.
return true;
}
ConfigSection section;
if (assoc_init(&section, CONFIG_INITIAL_CAPACITY, sizeof(char**), NULL) == -1) {
return false;
}
if (assoc_insert(config, sectionKey, &section) == -1) {
return false;
}
return true;
}
// Removes leading and trailing whitespace from the specified string.
//
// 0x426E18
static bool config_strip_white_space(char* string)
{
if (string == NULL) {
return false;
}
int length = strlen(string);
if (length == 0) {
return true;
}
// Starting from the end of the string, loop while it's a whitespace and
// decrement string length.
char* pch = string + length - 1;
while (length != 0 && isspace(*pch)) {
length--;
pch--;
}
// pch now points to the last non-whitespace character.
pch[1] = '\0';
// Starting from the beginning of the string loop while it's a whitespace
// and decrement string length.
pch = string;
while (isspace(*pch)) {
pch++;
length--;
}
// pch now points for to the first non-whitespace character.
memmove(string, pch, length + 1);
return true;
}
// 0x426E98
bool config_get_double(Config* config, const char* sectionKey, const char* key, double* valuePtr)
{
if (valuePtr == NULL) {
return false;
}
char* stringValue;
if (!config_get_string(config, sectionKey, key, &stringValue)) {
return false;
}
*valuePtr = strtod(stringValue, NULL);
return true;
}
// 0x426ECC
bool config_set_double(Config* config, const char* sectionKey, const char* key, double value)
{
char stringValue[32];
snprintf(stringValue, sizeof(stringValue), "%.6f", value);
return config_set_string(config, sectionKey, key, stringValue);
}
// NOTE: Boolean-typed variant of [config_get_value].
bool configGetBool(Config* config, const char* sectionKey, const char* key, bool* valuePtr)
{
if (valuePtr == NULL) {
return false;
}
int integerValue;
if (!config_get_value(config, sectionKey, key, &integerValue)) {
return false;
}
*valuePtr = integerValue != 0;
return true;
}
// NOTE: Boolean-typed variant of [configGetInt].
bool configSetBool(Config* config, const char* sectionKey, const char* key, bool value)
{
return config_set_value(config, sectionKey, key, value ? 1 : 0);
}
} // namespace fallout

39
src/game/config.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef FALLOUT_GAME_CONFIG_H_
#define FALLOUT_GAME_CONFIG_H_
#include "plib/assoc/assoc.h"
namespace fallout {
// A representation of .INI file.
//
// It's implemented as a [assoc_array] whos keys are section names of .INI file,
// and it's values are [ConfigSection] structs.
typedef assoc_array Config;
// Representation of .INI section.
//
// It's implemented as a [assoc_array] whos keys are names of .INI file
// key-pair values, and it's values are pointers to strings (char**).
typedef assoc_array ConfigSection;
bool config_init(Config* config);
void config_exit(Config* config);
bool config_cmd_line_parse(Config* config, int argc, char** argv);
bool config_get_string(Config* config, const char* sectionKey, const char* key, char** valuePtr);
bool config_set_string(Config* config, const char* sectionKey, const char* key, const char* value);
bool config_get_value(Config* config, const char* sectionKey, const char* key, int* valuePtr);
bool config_get_values(Config* config, const char* section, const char* key, int* arr, int count);
bool config_set_value(Config* config, const char* sectionKey, const char* key, int value);
bool config_load(Config* config, const char* filePath, bool isDb);
bool config_save(Config* config, const char* filePath, bool isDb);
bool config_get_double(Config* config, const char* sectionKey, const char* key, double* valuePtr);
bool config_set_double(Config* config, const char* sectionKey, const char* key, double value);
// TODO: Remove.
bool configGetBool(Config* config, const char* sectionKey, const char* key, bool* valuePtr);
bool configSetBool(Config* config, const char* sectionKey, const char* key, bool value);
} // namespace fallout
#endif /* FALLOUT_GAME_CONFIG_H_ */

61
src/game/counter.cc Normal file
View File

@ -0,0 +1,61 @@
#include "game/counter.h"
#include <time.h>
#include "plib/gnw/debug.h"
#include "plib/gnw/input.h"
namespace fallout {
static void counter();
// 0x504D28
static int counter_is_on = 0;
// 0x504D2C
static unsigned char count = 0;
// 0x504D30
static clock_t last_time = 0;
// 0x504D34
static CounterOutputFunc* counter_output_func;
// 0x426F10
void counter_on(CounterOutputFunc* outputFunc)
{
if (!counter_is_on) {
debug_printf("Turning on counter...\n");
add_bk_process(counter);
counter_output_func = outputFunc;
counter_is_on = 1;
last_time = clock();
}
}
// 0x426F54
void counter_off()
{
if (counter_is_on) {
remove_bk_process(counter);
counter_is_on = 0;
}
}
// 0x426F74
static void counter()
{
// 0x56BED0
static clock_t this_time;
count++;
if (count == 0) {
this_time = clock();
if (counter_output_func != NULL) {
counter_output_func(256.0 / (this_time - last_time) / 100.0);
}
last_time = this_time;
}
}
} // namespace fallout

13
src/game/counter.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef FALLOUT_GAME_COUNTER_H_
#define FALLOUT_GAME_COUNTER_H_
namespace fallout {
typedef void(CounterOutputFunc)(double a1);
void counter_on(CounterOutputFunc* outputFunc);
void counter_off();
} // namespace fallout
#endif /* FALLOUT_GAME_COUNTER_H_ */

334
src/game/credits.cc Normal file
View File

@ -0,0 +1,334 @@
#include "game/credits.h"
#include <string.h>
#include "game/art.h"
#include "game/cycle.h"
#include "game/gconfig.h"
#include "game/gmouse.h"
#include "game/message.h"
#include "game/palette.h"
#include "int/sound.h"
#include "platform_compat.h"
#include "plib/color/color.h"
#include "plib/db/db.h"
#include "plib/gnw/debug.h"
#include "plib/gnw/gnw.h"
#include "plib/gnw/grbuf.h"
#include "plib/gnw/input.h"
#include "plib/gnw/memory.h"
#include "plib/gnw/svga.h"
#include "plib/gnw/text.h"
namespace fallout {
#define CREDITS_WINDOW_SCROLLING_DELAY 38
static bool credits_get_next_line(char* dest, int* font, int* color);
// 0x56BEE0
static DB_FILE* credits_file;
// 0x56BEE4
static int name_color;
// 0x56BEE8
static int title_font;
// 0x56BEEC
static int name_font;
// 0x56BEF0
static int title_color;
// 0x426FE0
void credits(const char* filePath, int backgroundFid, bool useReversedStyle)
{
int oldFont = text_curr();
loadColorTable("color.pal");
if (useReversedStyle) {
title_color = colorTable[18917];
name_font = 103;
title_font = 104;
name_color = colorTable[13673];
} else {
title_color = colorTable[13673];
name_font = 104;
title_font = 103;
name_color = colorTable[18917];
}
soundUpdate();
char localizedPath[COMPAT_MAX_PATH];
if (message_make_path(localizedPath, sizeof(localizedPath), filePath)) {
credits_file = db_fopen(localizedPath, "rt");
if (credits_file != NULL) {
soundUpdate();
cycle_disable();
gmouse_set_cursor(MOUSE_CURSOR_NONE);
bool cursorWasHidden = mouse_hidden();
if (cursorWasHidden) {
mouse_show();
}
int windowWidth = screenGetWidth();
int windowHeight = screenGetHeight();
int window = win_add(0, 0, windowWidth, windowHeight, colorTable[0], 20);
soundUpdate();
if (window != -1) {
unsigned char* windowBuffer = win_get_buf(window);
if (windowBuffer != NULL) {
unsigned char* backgroundBuffer = (unsigned char*)mem_malloc(windowWidth * windowHeight);
if (backgroundBuffer) {
soundUpdate();
memset(backgroundBuffer, colorTable[0], windowWidth * windowHeight);
if (backgroundFid != -1) {
CacheEntry* backgroundFrmHandle;
Art* frm = art_ptr_lock(backgroundFid, &backgroundFrmHandle);
if (frm != NULL) {
int width = art_frame_width(frm, 0, 0);
int height = art_frame_length(frm, 0, 0);
unsigned char* backgroundFrmData = art_frame_data(frm, 0, 0);
buf_to_buf(backgroundFrmData,
width,
height,
width,
backgroundBuffer + windowWidth * ((windowHeight - height) / 2) + (windowWidth - width) / 2,
windowWidth);
art_ptr_unlock(backgroundFrmHandle);
}
}
unsigned char* intermediateBuffer = (unsigned char*)mem_malloc(windowWidth * windowHeight);
if (intermediateBuffer != NULL) {
memset(intermediateBuffer, 0, windowWidth * windowHeight);
text_font(title_font);
int titleFontLineHeight = text_height();
text_font(name_font);
int nameFontLineHeight = text_height();
int lineHeight = nameFontLineHeight + (titleFontLineHeight >= nameFontLineHeight ? titleFontLineHeight - nameFontLineHeight : 0);
int stringBufferSize = windowWidth * lineHeight;
unsigned char* stringBuffer = (unsigned char*)mem_malloc(stringBufferSize);
if (stringBuffer != NULL) {
const char* boom = "boom";
int exploding_head_frame = 0;
int exploding_head_cycle = 0;
int violence_level = 0;
config_get_value(&game_config, GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_VIOLENCE_LEVEL_KEY, &violence_level);
buf_to_buf(backgroundBuffer,
windowWidth,
windowHeight,
windowWidth,
windowBuffer,
windowWidth);
win_draw(window);
palette_fade_to(cmap);
unsigned char* v40 = intermediateBuffer + windowWidth * windowHeight - windowWidth;
char str[260];
int font;
int color;
unsigned int tick = 0;
bool stop = false;
while (credits_get_next_line(str, &font, &color)) {
text_font(font);
int v19 = text_width(str);
if (v19 >= windowWidth) {
continue;
}
memset(stringBuffer, 0, stringBufferSize);
text_to_buf(stringBuffer, str, windowWidth, windowWidth, color);
unsigned char* dest = intermediateBuffer + windowWidth * windowHeight - windowWidth + (windowWidth - v19) / 2;
unsigned char* src = stringBuffer;
for (int index = 0; index < lineHeight; index++) {
sharedFpsLimiter.mark();
int input = get_input();
if (input != -1) {
if (input != *boom) {
stop = true;
break;
}
boom++;
if (*boom == '\0') {
exploding_head_frame = 1;
boom = "boom";
}
}
memmove(intermediateBuffer, intermediateBuffer + windowWidth, windowWidth * windowHeight - windowWidth);
memcpy(dest, src, v19);
buf_to_buf(backgroundBuffer,
windowWidth,
windowHeight,
windowWidth,
windowBuffer,
windowWidth);
trans_buf_to_buf(intermediateBuffer,
windowWidth,
windowHeight,
windowWidth,
windowBuffer,
windowWidth);
if (violence_level != VIOLENCE_LEVEL_NONE) {
if (exploding_head_frame != 0) {
CacheEntry* exploding_head_key;
int exploding_head_fid = art_id(OBJ_TYPE_INTERFACE, 39, 0, 0, 0);
Art* exploding_head_frm = art_ptr_lock(exploding_head_fid, &exploding_head_key);
if (exploding_head_frm != NULL && exploding_head_frame - 1 < art_frame_max_frame(exploding_head_frm)) {
int width = art_frame_width(exploding_head_frm, exploding_head_frame - 1, 0);
int height = art_frame_length(exploding_head_frm, exploding_head_frame - 1, 0);
unsigned char* logoData = art_frame_data(exploding_head_frm, exploding_head_frame - 1, 0);
trans_buf_to_buf(logoData,
width,
height,
width,
windowBuffer + windowWidth * (windowHeight - height) + (windowWidth - width) / 2,
windowWidth);
art_ptr_unlock(exploding_head_key);
if (exploding_head_cycle) {
exploding_head_frame++;
}
exploding_head_cycle = 1 - exploding_head_cycle;
} else {
exploding_head_frame = 0;
}
}
}
while (elapsed_time(tick) < CREDITS_WINDOW_SCROLLING_DELAY) {
}
tick = get_time();
win_draw(window);
src += windowWidth;
sharedFpsLimiter.throttle();
renderPresent();
}
if (stop) {
break;
}
}
if (!stop) {
for (int index = 0; index < windowHeight; index++) {
sharedFpsLimiter.mark();
if (get_input() != -1) {
break;
}
memmove(intermediateBuffer, intermediateBuffer + windowWidth, windowWidth * windowHeight - windowWidth);
memset(intermediateBuffer + windowWidth * windowHeight - windowWidth, 0, windowWidth);
buf_to_buf(backgroundBuffer,
windowWidth,
windowHeight,
windowWidth,
windowBuffer,
windowWidth);
trans_buf_to_buf(intermediateBuffer,
windowWidth,
windowHeight,
windowWidth,
windowBuffer,
windowWidth);
while (elapsed_time(tick) < CREDITS_WINDOW_SCROLLING_DELAY) {
}
tick = get_time();
win_draw(window);
sharedFpsLimiter.throttle();
renderPresent();
}
}
mem_free(stringBuffer);
}
mem_free(intermediateBuffer);
}
mem_free(backgroundBuffer);
}
}
soundUpdate();
palette_fade_to(black_palette);
soundUpdate();
win_delete(window);
}
if (cursorWasHidden) {
mouse_hide();
}
gmouse_set_cursor(MOUSE_CURSOR_ARROW);
cycle_enable();
db_fclose(credits_file);
}
}
text_font(oldFont);
}
// 0x42777C
static bool credits_get_next_line(char* dest, int* font, int* color)
{
char string[256];
while (db_fgets(string, 256, credits_file)) {
char* pch;
if (string[0] == ';') {
continue;
} else if (string[0] == '@') {
*font = title_font;
*color = title_color;
pch = string + 1;
} else if (string[0] == '#') {
*font = name_font;
*color = colorTable[17969];
pch = string + 1;
} else {
*font = name_font;
*color = name_color;
pch = string;
}
strcpy(dest, pch);
return true;
}
return false;
}
} // namespace fallout

10
src/game/credits.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef FALLOUT_GAME_CREDITS_H_
#define FALLOUT_GAME_CREDITS_H_
namespace fallout {
void credits(const char* path, int fid, bool useReversedStyle);
} // namespace fallout
#endif /* FALLOUT_GAME_CREDITS_H_ */

1246
src/game/critter.cc Normal file

File diff suppressed because it is too large Load Diff

111
src/game/critter.h Normal file
View File

@ -0,0 +1,111 @@
#ifndef FALLOUT_GAME_CRITTER_H_
#define FALLOUT_GAME_CRITTER_H_
#include "game/object_types.h"
#include "game/proto_types.h"
#include "plib/db/db.h"
namespace fallout {
// Maximum length of dude's name length.
#define DUDE_NAME_MAX_LENGTH 32
// The number of effects caused by radiation.
//
// A radiation effect is an identifier and does not have it's own name. It's
// stat is specified in `rad_stat`, and it's amount is specified
// in `rad_bonus` for every `RadiationLevel`.
#define RADIATION_EFFECT_COUNT 8
// Radiation levels.
//
// The names of levels are taken from Fallout 3, comments from Fallout 2.
typedef enum RadiationLevel {
// Very nauseous.
RADIATION_LEVEL_NONE,
// Slightly fatigued.
RADIATION_LEVEL_MINOR,
// Vomiting does not stop.
RADIATION_LEVEL_ADVANCED,
// Hair is falling out.
RADIATION_LEVEL_CRITICAL,
// Skin is falling off.
RADIATION_LEVEL_DEADLY,
// Intense agony.
RADIATION_LEVEL_FATAL,
// The number of radiation levels.
RADIATION_LEVEL_COUNT,
} RadiationLevel;
typedef enum PcFlags {
PC_FLAG_SNEAKING = 0,
PC_FLAG_LEVEL_UP_AVAILABLE = 3,
PC_FLAG_ADDICTED = 4,
} PcFlags;
extern int rad_stat[RADIATION_EFFECT_COUNT];
extern int rad_bonus[RADIATION_LEVEL_COUNT][RADIATION_EFFECT_COUNT];
int critter_init();
void critter_reset();
void critter_exit();
int critter_load(DB_FILE* stream);
int critter_save(DB_FILE* stream);
char* critter_name(Object* critter);
void critter_copy(CritterProtoData* dest, CritterProtoData* src);
int critter_pc_set_name(const char* name);
void critter_pc_reset_name();
int critter_get_hits(Object* critter);
int critter_adjust_hits(Object* critter, int amount);
int critter_get_poison(Object* critter);
int critter_adjust_poison(Object* obj, int amount);
int critter_check_poison(Object* obj, void* data);
int critter_get_rads(Object* critter);
int critter_adjust_rads(Object* obj, int amount);
int critter_check_rads(Object* critter);
int critter_process_rads(Object* obj, void* data);
int critter_load_rads(DB_FILE* stream, void** data);
int critter_save_rads(DB_FILE* stream, void* data);
int critter_kill_count_inc(int critter_type);
int critter_kill_count(int critter_type);
int critter_kill_count_load(DB_FILE* stream);
int critter_kill_count_save(DB_FILE* stream);
int critter_kill_count_type(Object* critter);
char* critter_kill_name(int critter_type);
char* critter_kill_info(int critter_type);
int critter_heal_hours(Object* critter, int hours);
void critter_kill(Object* critter, int anim, bool refresh_window);
int critter_kill_exps(Object* critter);
bool critter_is_active(Object* critter);
bool critter_is_dead(Object* critter);
bool critter_is_crippled(Object* critter);
bool critter_is_prone(Object* critter);
int critter_body_type(Object* critter);
int critter_load_data(CritterProtoData* critter_data, const char* path);
int pc_load_data(const char* path);
int critter_read_data(DB_FILE* stream, CritterProtoData* critter_data);
int critter_save_data(CritterProtoData* critter_data, const char* path);
int pc_save_data(const char* path);
int critter_write_data(DB_FILE* stream, CritterProtoData* critter_data);
void pc_flag_off(int pc_flag);
void pc_flag_on(int pc_flag);
void pc_flag_toggle(int pc_flag);
bool is_pc_flag(int pc_flag);
int critter_sneak_check(Object* obj, void* data);
int critter_sneak_clear(Object* obj, void* data);
bool is_pc_sneak_working();
int critter_wake_up(Object* obj, void* data);
int critter_wake_clear(Object* obj, void* data);
int critter_set_who_hit_me(Object* critter, Object* who_hit_me);
bool critter_can_obj_dude_rest();
int critter_compute_ap_from_distance(Object* critter, int distance);
} // namespace fallout
#endif /* FALLOUT_GAME_CRITTER_H_ */

343
src/game/cycle.cc Normal file
View File

@ -0,0 +1,343 @@
#include "game/cycle.h"
#include "game/gconfig.h"
#include "game/palette.h"
#include "plib/color/color.h"
#include "plib/gnw/input.h"
namespace fallout {
#define COLOR_CYCLE_PERIOD_SLOW 200U
#define COLOR_CYCLE_PERIOD_MEDIUM 142U
#define COLOR_CYCLE_PERIOD_FAST 100U
#define COLOR_CYCLE_PERIOD_VERY_FAST 33U
static void cycle_colors();
// 0x504E3C
static int cycle_speed_factor = 1;
// 0x504E40
unsigned char slime[12] = {
// clang-format off
0, 108, 0,
11, 115, 7,
27, 123, 15,
43, 131, 27,
// clang-format on
};
// 0x504E4C
unsigned char shoreline[18] = {
// clang-format off
83, 63, 43,
75, 59, 43,
67, 55, 39,
63, 51, 39,
55, 47, 35,
51, 43, 35,
// clang-format on
};
// 0x504E5E
unsigned char fire_slow[15] = {
// clang-format off
255, 0, 0,
215, 0, 0,
147, 43, 11,
255, 119, 0,
255, 59, 0,
// clang-format on
};
// 0x504E6D
unsigned char fire_fast[15] = {
// clang-format off
71, 0, 0,
123, 0, 0,
179, 0, 0,
123, 0, 0,
71, 0, 0,
// clang-format on
};
// 0x504E7C
unsigned char monitors[15] = {
// clang-format off
107, 107, 111,
99, 103, 127,
87, 107, 143,
0, 147, 163,
107, 187, 255,
// clang-format on
};
// 0x504E8C
static bool cycle_initialized = false;
// 0x504E90
static bool cycle_enabled = false;
// 0x56BF60
static unsigned int last_cycle_fast;
// 0x56BF64
static unsigned int last_cycle_slow;
// 0x56BF68
static unsigned int last_cycle_medium;
// 0x56BF6C
static unsigned int last_cycle_very_fast;
// 0x428D60
void cycle_init()
{
bool colorCycling;
int index;
int cycleSpeedFactor;
if (cycle_initialized) {
return;
}
if (!configGetBool(&game_config, GAME_CONFIG_SYSTEM_KEY, GAME_CONFIG_COLOR_CYCLING_KEY, &colorCycling)) {
colorCycling = true;
}
if (!colorCycling) {
return;
}
for (index = 0; index < 12; index++) {
slime[index] >>= 2;
}
for (index = 0; index < 18; index++) {
shoreline[index] >>= 2;
}
for (index = 0; index < 15; index++) {
fire_slow[index] >>= 2;
}
for (index = 0; index < 15; index++) {
fire_fast[index] >>= 2;
}
for (index = 0; index < 15; index++) {
monitors[index] >>= 2;
}
add_bk_process(cycle_colors);
cycle_initialized = true;
cycle_enabled = true;
if (!config_get_value(&game_config, GAME_CONFIG_SYSTEM_KEY, GAME_CONFIG_CYCLE_SPEED_FACTOR_KEY, &cycleSpeedFactor)) {
cycleSpeedFactor = 1;
}
change_cycle_speed(cycleSpeedFactor);
}
// 0x428EAC
void cycle_reset()
{
if (cycle_initialized) {
last_cycle_slow = 0;
last_cycle_medium = 0;
last_cycle_fast = 0;
last_cycle_very_fast = 0;
add_bk_process(cycle_colors);
cycle_enabled = true;
}
}
// 0x428EEC
void cycle_exit()
{
if (cycle_initialized) {
remove_bk_process(cycle_colors);
cycle_initialized = false;
cycle_enabled = false;
}
}
// 0x428F10
void cycle_disable()
{
cycle_enabled = false;
}
// 0x428F1C
void cycle_enable()
{
cycle_enabled = true;
}
// 0x428F28
bool cycle_is_enabled()
{
return cycle_enabled;
}
// 0x428F5C
static void cycle_colors()
{
// 0x504E94
static int slime_start = 0;
// 0x504E98
static int shoreline_start = 0;
// 0x504E9C
static int fire_slow_start = 0;
// 0x504EA0
static int fire_fast_start = 0;
// 0x504EA4
static int monitors_start = 0;
// 0x504EA8
static unsigned char bobber_red = 0;
// 0x504EA9
static signed char bobber_diff = -4;
if (!cycle_enabled) {
return;
}
bool changed = false;
unsigned char* palette = getSystemPalette();
unsigned int time = get_time();
if (elapsed_tocks(time, last_cycle_slow) >= COLOR_CYCLE_PERIOD_SLOW * cycle_speed_factor) {
changed = true;
last_cycle_slow = time;
int paletteIndex = 229 * 3;
for (int index = slime_start; index < 12; index++) {
palette[paletteIndex++] = slime[index];
}
for (int index = 0; index < slime_start; index++) {
palette[paletteIndex++] = slime[index];
}
slime_start -= 3;
if (slime_start < 0) {
slime_start = 9;
}
paletteIndex = 248 * 3;
for (int index = shoreline_start; index < 18; index++) {
palette[paletteIndex++] = shoreline[index];
}
for (int index = 0; index < shoreline_start; index++) {
palette[paletteIndex++] = shoreline[index];
}
shoreline_start -= 3;
if (shoreline_start < 0) {
shoreline_start = 15;
}
paletteIndex = 238 * 3;
for (int index = fire_slow_start; index < 15; index++) {
palette[paletteIndex++] = fire_slow[index];
}
for (int index = 0; index < fire_slow_start; index++) {
palette[paletteIndex++] = fire_slow[index];
}
fire_slow_start -= 3;
if (fire_slow_start < 0) {
fire_slow_start = 12;
}
}
if (elapsed_tocks(time, last_cycle_medium) >= COLOR_CYCLE_PERIOD_MEDIUM * cycle_speed_factor) {
changed = true;
last_cycle_medium = time;
int paletteIndex = 243 * 3;
for (int index = fire_fast_start; index < 15; index++) {
palette[paletteIndex++] = fire_fast[index];
}
for (int index = 0; index < fire_fast_start; index++) {
palette[paletteIndex++] = fire_fast[index];
}
fire_fast_start -= 3;
if (fire_fast_start < 0) {
fire_fast_start = 12;
}
}
if (elapsed_tocks(time, last_cycle_fast) >= COLOR_CYCLE_PERIOD_FAST * cycle_speed_factor) {
changed = true;
last_cycle_fast = time;
int paletteIndex = 233 * 3;
for (int index = monitors_start; index < 15; index++) {
palette[paletteIndex++] = monitors[index];
}
for (int index = 0; index < monitors_start; index++) {
palette[paletteIndex++] = monitors[index];
}
monitors_start -= 3;
if (monitors_start < 0) {
monitors_start = 12;
}
}
if (elapsed_tocks(time, last_cycle_very_fast) >= COLOR_CYCLE_PERIOD_VERY_FAST * cycle_speed_factor) {
changed = true;
last_cycle_very_fast = time;
if (bobber_red == 0 || bobber_red == 60) {
bobber_diff = -bobber_diff;
}
bobber_red += bobber_diff;
int paletteIndex = 254 * 3;
palette[paletteIndex++] = bobber_red;
palette[paletteIndex++] = 0;
palette[paletteIndex++] = 0;
}
if (changed) {
palette_set_entries(palette + 229 * 3, 229, 255);
}
}
// 0x428F30
void change_cycle_speed(int value)
{
cycle_speed_factor = value;
config_set_value(&game_config, GAME_CONFIG_SYSTEM_KEY, GAME_CONFIG_CYCLE_SPEED_FACTOR_KEY, value);
}
// 0x428F54
int get_cycle_speed()
{
return cycle_speed_factor;
}
} // namespace fallout

23
src/game/cycle.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef FALLOUT_GAME_CYCLE_H_
#define FALLOUT_GAME_CYCLE_H_
namespace fallout {
extern unsigned char slime[12];
extern unsigned char shoreline[18];
extern unsigned char fire_slow[15];
extern unsigned char fire_fast[15];
extern unsigned char monitors[15];
void cycle_init();
void cycle_reset();
void cycle_exit();
void cycle_disable();
void cycle_enable();
bool cycle_is_enabled();
void change_cycle_speed(int value);
int get_cycle_speed();
} // namespace fallout
#endif /* FALLOUT_GAME_CYCLE_H_ */

391
src/game/display.cc Normal file
View File

@ -0,0 +1,391 @@
#include "game/display.h"
#include <string.h>
#include "game/art.h"
#include "game/combat.h"
#include "game/gmouse.h"
#include "game/gsound.h"
#include "game/intface.h"
#include "plib/color/color.h"
#include "plib/gnw/button.h"
#include "plib/gnw/gnw.h"
#include "plib/gnw/grbuf.h"
#include "plib/gnw/input.h"
#include "plib/gnw/memory.h"
#include "plib/gnw/rect.h"
#include "plib/gnw/text.h"
namespace fallout {
// The maximum number of lines display monitor can hold. Once this value
// is reached earlier messages are thrown away.
#define DISPLAY_MONITOR_LINES_CAPACITY 100
// The maximum length of a string in display monitor (in characters).
#define DISPLAY_MONITOR_LINE_LENGTH 80
#define DISPLAY_MONITOR_X 23
#define DISPLAY_MONITOR_Y 24
#define DISPLAY_MONITOR_WIDTH 167
#define DISPLAY_MONITOR_HEIGHT 60
#define DISPLAY_MONITOR_HALF_HEIGHT (DISPLAY_MONITOR_HEIGHT / 2)
#define DISPLAY_MONITOR_FONT 101
#define DISPLAY_MONITOR_BEEP_DELAY 500U
// 0x504F0C
static bool disp_init = false;
// The rectangle that display monitor occupies in the main interface window.
//
// 0x504F10
static Rect disp_rect = {
DISPLAY_MONITOR_X,
DISPLAY_MONITOR_Y,
DISPLAY_MONITOR_X + DISPLAY_MONITOR_WIDTH - 1,
DISPLAY_MONITOR_Y + DISPLAY_MONITOR_HEIGHT - 1,
};
// 0x504F20
static int dn_bid = -1;
// 0x504F24
static int up_bid = -1;
// 0x56C38C
static char disp_str[DISPLAY_MONITOR_LINES_CAPACITY][DISPLAY_MONITOR_LINE_LENGTH];
// 0x56E2CC
static unsigned char* disp_buf;
// 0x56E2D0
static int max_disp_ptr;
// 0x56E2D4
static bool display_enabled;
// 0x56E2D8
static int disp_curr;
// 0x56E2DC
static int intface_full_wid;
// 0x56E2DE0
static int max_ptr;
// 0x56E2DE4
static int disp_start;
// 0x42BBE0
int display_init()
{
if (!disp_init) {
int oldFont = text_curr();
text_font(DISPLAY_MONITOR_FONT);
max_ptr = DISPLAY_MONITOR_LINES_CAPACITY;
max_disp_ptr = DISPLAY_MONITOR_HEIGHT / text_height();
disp_start = 0;
disp_curr = 0;
text_font(oldFont);
disp_buf = (unsigned char*)mem_malloc(DISPLAY_MONITOR_WIDTH * DISPLAY_MONITOR_HEIGHT);
if (disp_buf == NULL) {
return -1;
}
CacheEntry* backgroundFrmHandle;
int backgroundFid = art_id(OBJ_TYPE_INTERFACE, 16, 0, 0, 0);
Art* backgroundFrm = art_ptr_lock(backgroundFid, &backgroundFrmHandle);
if (backgroundFrm == NULL) {
mem_free(disp_buf);
return -1;
}
unsigned char* backgroundFrmData = art_frame_data(backgroundFrm, 0, 0);
intface_full_wid = art_frame_width(backgroundFrm, 0, 0);
buf_to_buf(backgroundFrmData + intface_full_wid * DISPLAY_MONITOR_Y + DISPLAY_MONITOR_X,
DISPLAY_MONITOR_WIDTH,
DISPLAY_MONITOR_HEIGHT,
intface_full_wid,
disp_buf,
DISPLAY_MONITOR_WIDTH);
art_ptr_unlock(backgroundFrmHandle);
up_bid = win_register_button(interfaceWindow,
DISPLAY_MONITOR_X,
DISPLAY_MONITOR_Y,
DISPLAY_MONITOR_WIDTH,
DISPLAY_MONITOR_HALF_HEIGHT,
-1,
-1,
-1,
-1,
NULL,
NULL,
NULL,
0);
if (up_bid != -1) {
win_register_button_func(up_bid,
display_arrow_up,
display_arrow_restore,
display_scroll_up,
NULL);
}
dn_bid = win_register_button(interfaceWindow,
DISPLAY_MONITOR_X,
DISPLAY_MONITOR_Y + DISPLAY_MONITOR_HALF_HEIGHT,
DISPLAY_MONITOR_WIDTH,
DISPLAY_MONITOR_HEIGHT - DISPLAY_MONITOR_HALF_HEIGHT,
-1,
-1,
-1,
-1,
NULL,
NULL,
NULL,
0);
if (dn_bid != -1) {
win_register_button_func(dn_bid,
display_arrow_down,
display_arrow_restore,
display_scroll_down,
NULL);
}
display_enabled = true;
disp_init = true;
// NOTE: Uninline.
display_clear();
}
return 0;
}
// 0x42BDD0
int display_reset()
{
// NOTE: Uninline.
display_clear();
return 0;
}
// 0x42BE1C
void display_exit()
{
if (disp_init) {
mem_free(disp_buf);
disp_init = false;
}
}
// 0x42BE3C
void display_print(char* str)
{
// 0x56E2E8
static unsigned int lastTime;
if (!disp_init) {
return;
}
int oldFont = text_curr();
text_font(DISPLAY_MONITOR_FONT);
char knob = '\x95';
char knobString[2];
knobString[0] = knob;
knobString[1] = '\0';
int knobWidth = text_width(knobString);
if (!isInCombat()) {
unsigned int now = get_bk_time();
if (elapsed_tocks(now, lastTime) >= DISPLAY_MONITOR_BEEP_DELAY) {
lastTime = now;
gsound_play_sfx_file("monitor");
}
}
// TODO: Refactor these two loops.
char* v1 = NULL;
while (true) {
while (text_width(str) < DISPLAY_MONITOR_WIDTH - max_disp_ptr - knobWidth) {
char* temp = disp_str[disp_start];
int length;
if (knob != '\0') {
*temp++ = knob;
length = DISPLAY_MONITOR_LINE_LENGTH - 2;
knob = '\0';
knobWidth = 0;
} else {
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
}
strncpy(temp, str, length);
disp_str[disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
disp_start = (disp_start + 1) % max_ptr;
if (v1 == NULL) {
text_font(oldFont);
disp_curr = disp_start;
display_redraw();
return;
}
str = v1 + 1;
*v1 = ' ';
v1 = NULL;
}
char* space = strrchr(str, ' ');
if (space == NULL) {
break;
}
if (v1 != NULL) {
*v1 = ' ';
}
v1 = space;
*space = '\0';
}
char* temp = disp_str[disp_start];
int length;
if (knob != '\0') {
temp++;
disp_str[disp_start][0] = knob;
length = DISPLAY_MONITOR_LINE_LENGTH - 2;
knob = '\0';
} else {
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
}
strncpy(temp, str, length);
disp_str[disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
disp_start = (disp_start + 1) % max_ptr;
text_font(oldFont);
disp_curr = disp_start;
display_redraw();
}
// 0x42BFF4
void display_clear()
{
int index;
if (disp_init) {
for (index = 0; index < max_ptr; index++) {
disp_str[index][0] = '\0';
}
disp_start = 0;
disp_curr = 0;
display_redraw();
}
}
// 0x42C040
void display_redraw()
{
if (!disp_init) {
return;
}
unsigned char* buf = win_get_buf(interfaceWindow);
if (buf == NULL) {
return;
}
buf += intface_full_wid * DISPLAY_MONITOR_Y + DISPLAY_MONITOR_X;
buf_to_buf(disp_buf,
DISPLAY_MONITOR_WIDTH,
DISPLAY_MONITOR_HEIGHT,
DISPLAY_MONITOR_WIDTH,
buf,
intface_full_wid);
int oldFont = text_curr();
text_font(DISPLAY_MONITOR_FONT);
for (int index = 0; index < max_disp_ptr; index++) {
int stringIndex = (disp_curr + max_ptr + index - max_disp_ptr) % max_ptr;
text_to_buf(buf + index * intface_full_wid * text_height(), disp_str[stringIndex], DISPLAY_MONITOR_WIDTH, intface_full_wid, colorTable[992]);
// Even though the display monitor is rectangular, it's graphic is not.
// To give a feel of depth it's covered by some metal canopy and
// considered inclined outwards. This way earlier messages appear a
// little bit far from player's perspective. To implement this small
// detail the destination buffer is incremented by 1.
buf++;
}
win_draw_rect(interfaceWindow, &disp_rect);
text_font(oldFont);
}
// 0x42C138
void display_scroll_up(int btn, int keyCode)
{
if ((max_ptr + disp_curr - 1) % max_ptr != disp_start) {
disp_curr = (max_ptr + disp_curr - 1) % max_ptr;
display_redraw();
}
}
// 0x42C164
void display_scroll_down(int btn, int keyCode)
{
if (disp_curr != disp_start) {
disp_curr = (disp_curr + 1) % max_ptr;
display_redraw();
}
}
// 0x42C190
void display_arrow_up(int btn, int keyCode)
{
gmouse_set_cursor(MOUSE_CURSOR_SMALL_ARROW_UP);
}
// 0x42C19C
void display_arrow_down(int btn, int keyCode)
{
gmouse_set_cursor(MOUSE_CURSOR_SMALL_ARROW_DOWN);
}
// 0x42C1A8
void display_arrow_restore(int btn, int keyCode)
{
gmouse_set_cursor(MOUSE_CURSOR_ARROW);
}
// 0x42C1B4
void display_disable()
{
if (display_enabled) {
win_disable_button(dn_bid);
win_disable_button(up_bid);
display_enabled = false;
}
}
// 0x42C1DC
void display_enable()
{
if (!display_enabled) {
win_enable_button(dn_bid);
win_enable_button(up_bid);
display_enabled = true;
}
}
} // namespace fallout

22
src/game/display.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef FALLOUT_GAME_DISPLAY_H_
#define FALLOUT_GAME_DISPLAY_H_
namespace fallout {
int display_init();
int display_reset();
void display_exit();
void display_print(char* string);
void display_clear();
void display_redraw();
void display_scroll_up(int btn, int keyCode);
void display_scroll_down(int btn, int keyCode);
void display_arrow_up(int btn, int keyCode);
void display_arrow_down(int btn, int keyCode);
void display_arrow_restore(int btn, int keyCode);
void display_disable();
void display_enable();
} // namespace fallout
#endif /* FALLOUT_GAME_DISPLAY_H_ */

6212
src/game/editor.cc Normal file

File diff suppressed because it is too large Load Diff

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