Vendor SpaceCadetPinball, build for Apple Silicon

Reverse-engineered reimplementation of 3D Pinball for Windows - Space Cadet
from k4zmu2a/SpaceCadetPinball (MIT), upstream cb9b7b8, vendored rather than
submoduled so local patches are just edits.

- CMakeLists: stop hard-setting CMAKE_OSX_ARCHITECTURES so the documented -D
  override works; brew's SDL2 is arm64-only and universal linking failed.
- gitignore original .DAT/.MID assets, which are not ours to redistribute.
- CLAUDE.md: build line, upstream deviation, and where game data goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-27 19:05:12 +10:00
commit b671975742
175 changed files with 102024 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

23
.github/workflows/ReleaseBuilds.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Make Release Builds
on: [workflow_dispatch]
jobs:
build-macos:
runs-on: macos-12
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: master
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 13
- run: bash build-mac-app.sh
- uses: actions/upload-artifact@v4
with:
name: mac-build
path: SpaceCadetPinball-*-mac.dmg

308
.gitignore vendored Normal file
View File

@ -0,0 +1,308 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
ARM/
ARM64/
win32/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.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
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# 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
# TODO: 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
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# 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
# 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
node_modules/
orleans.codegen.cs
# 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
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
/Ida
/Export
/DrMem
/Doc private
# Windows and macOS local libraries
/Libs
#CMake generated
out/
/cmake-build-debug
# Windows XP stuff
DebugWinXp/
ReleaseWinXp/
### CMake ###
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
### CMake Patch ###
# External projects
*-prefix/
### Ninja ###
.ninja_deps
.ninja_log
build.ninja
# Build directory
/build
# Original Microsoft/Maxis game assets — never commit these, they are not ours to redistribute
*.DAT
*.dat
*.MID
*.mid

20
BuildForWindows.ps1 Normal file
View File

@ -0,0 +1,20 @@
#Run this script from Developer Command Prompt for VS *
$artefacts = ".\bin\Release\SpaceCadetPinball.exe", ".\bin\Release\SDL2.dll", ".\bin\Release\SDL2_mixer.dll"
#X86 build
Remove-Item -Path .\build\CMakeCache.txt -ErrorAction SilentlyContinue
cmake -S . -B build -A Win32 -DCMAKE_WIN32_EXECUTABLE:BOOL=1
cmake --build build --config Release
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx86Win.zip" -Force
#X64 build
Remove-Item -Path .\build\CMakeCache.txt
cmake -S . -B build -A x64 -DCMAKE_WIN32_EXECUTABLE:BOOL=1
cmake --build build --config Release
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx64Win.zip" -Force
#86 XP build, requires special XP MSVC toolset
Remove-Item -Path .\build\CMakeCache.txt
cmake -S . -B build -A Win32 -DCMAKE_WIN32_EXECUTABLE:BOOL=1 -T v141_xp
cmake --build build --config Release
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx86WinXP.zip" -Force

42
CLAUDE.md Normal file
View File

@ -0,0 +1,42 @@
# macrosoft3dpinball
`3D Pinball for Windows Space Cadet` on macOS. Vendored copy of
[k4zmu2a/SpaceCadetPinball](https://github.com/k4zmu2a/SpaceCadetPinball) (MIT) at upstream
commit `cb9b7b8`, not a submodule — patch it in place, pull upstream by re-vendoring.
## Build (Apple Silicon)
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_PREFIX_PATH="$(brew --prefix sdl2);$(brew --prefix sdl2_mixer)"
cmake --build build -j8
```
Binary lands in `bin/SpaceCadetPinball`. Needs `brew install sdl2 sdl2_mixer`.
Do **not** use upstream `build-mac-app.sh` — it downloads SDL2 `.framework` DMGs and builds a
signed universal `.app`+`.dmg`. Unnecessary here; brew SDL2 is already installed.
## Gotchas
- `CMakeLists.txt` line ~27 upstream hard-set `CMAKE_OSX_ARCHITECTURES "arm64;x86_64"`, which
silently overrode the `-D` flag the README tells you to pass. Universal linking then fails
because brew SDL2 is arm64-only. Patched to only set the default when not given on the
command line. Re-apply this when re-vendoring upstream.
- Homebrew resolves `sdl2` to `sdl2-compat` (SDL2 API on top of SDL3). Links and runs fine.
- Deployment target is 10.11, so the link emits "built for newer version 26.0" warnings against
the brew dylibs. Harmless.
## Game data (not in this repo)
The engine ships no assets. It needs the original data files, which are Microsoft's and are
**not redistributable** — supply them from your own media (Windows XP or earlier install disc
for `PINBALL.DAT`; the retail `Full Tilt! Pinball` CD for `CADET.DAT`). `.DAT`/`.MID` are
gitignored so they never land in a commit.
Drop them in any of these; first match wins in the order `CADET.DAT`, `PINBALL.DAT`, `DEMO.DAT`:
- the working directory, or `bin/` next to the binary
- `~/Library/Application Support/SpaceCadetPinball/` ← preferred, keeps the repo clean
Without them the binary exits with "Could not load game data" and lists these paths.

250
CMakeLists.txt Normal file
View File

@ -0,0 +1,250 @@
cmake_minimum_required(VERSION 3.0...3.5)
project(SpaceCadetPinball)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/bin)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMakeModules")
# On Windows, set paths to SDL-devel packages here
if(WIN32)
list(APPEND SDL2_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs/SDL2")
list(APPEND SDL2_MIXER_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs/SDL2_mixer")
endif()
# Link mingw libs static
if(MINGW)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static")
endif()
if(APPLE)
set(MACOSX_RPATH)
set(CMAKE_BUILD_WITH_INSTALL_RPATH true)
set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.11")
# Universal by default, but let -DCMAKE_OSX_ARCHITECTURES win (brew SDL2 is single-arch).
if(NOT DEFINED CACHE{CMAKE_OSX_ARCHITECTURES})
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
endif()
list(APPEND SDL2_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs")
list(APPEND SDL2_MIXER_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs")
endif()
# SDL2main is not needed
set(SDL2_BUILDING_LIBRARY ON)
find_package(SDL2 REQUIRED)
FIND_PACKAGE(SDL2_mixer REQUIRED)
include_directories(${SDL2_INCLUDE_DIR} ${SDL2_MIXER_INCLUDE_DIR})
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
foreach(dir ${dirs})
message(STATUS "Include dir='${dir}'")
endforeach()
set(SOURCE_FILES
SpaceCadetPinball/control.cpp
SpaceCadetPinball/control.h
SpaceCadetPinball/EmbeddedData.cpp
SpaceCadetPinball/EmbeddedData.h
SpaceCadetPinball/font_selection.cpp
SpaceCadetPinball/font_selection.h
SpaceCadetPinball/fullscrn.cpp
SpaceCadetPinball/fullscrn.h
SpaceCadetPinball/gdrv.cpp
SpaceCadetPinball/gdrv.h
SpaceCadetPinball/GroupData.cpp
SpaceCadetPinball/GroupData.h
SpaceCadetPinball/high_score.cpp
SpaceCadetPinball/high_score.h
SpaceCadetPinball/loader.cpp
SpaceCadetPinball/loader.h
SpaceCadetPinball/maths.cpp
SpaceCadetPinball/maths.h
SpaceCadetPinball/midi.cpp
SpaceCadetPinball/midi.h
SpaceCadetPinball/nudge.cpp
SpaceCadetPinball/nudge.h
SpaceCadetPinball/options.cpp
SpaceCadetPinball/options.h
SpaceCadetPinball/partman.cpp
SpaceCadetPinball/partman.h
SpaceCadetPinball/pb.cpp
SpaceCadetPinball/pb.h
SpaceCadetPinball/pch.h
SpaceCadetPinball/proj.cpp
SpaceCadetPinball/proj.h
SpaceCadetPinball/render.cpp
SpaceCadetPinball/render.h
SpaceCadetPinball/score.cpp
SpaceCadetPinball/score.h
SpaceCadetPinball/Sound.cpp
SpaceCadetPinball/Sound.h
SpaceCadetPinball/SpaceCadetPinball.cpp
SpaceCadetPinball/TBall.cpp
SpaceCadetPinball/TBall.h
SpaceCadetPinball/TBlocker.cpp
SpaceCadetPinball/TBlocker.h
SpaceCadetPinball/TBumper.cpp
SpaceCadetPinball/TBumper.h
SpaceCadetPinball/TCircle.cpp
SpaceCadetPinball/TCircle.h
SpaceCadetPinball/TCollisionComponent.cpp
SpaceCadetPinball/TCollisionComponent.h
SpaceCadetPinball/TComponentGroup.cpp
SpaceCadetPinball/TComponentGroup.h
SpaceCadetPinball/TDemo.cpp
SpaceCadetPinball/TDemo.h
SpaceCadetPinball/TDrain.cpp
SpaceCadetPinball/TDrain.h
SpaceCadetPinball/TEdgeBox.h
SpaceCadetPinball/TEdgeManager.cpp
SpaceCadetPinball/TEdgeManager.h
SpaceCadetPinball/TEdgeSegment.cpp
SpaceCadetPinball/TEdgeSegment.h
SpaceCadetPinball/TFlagSpinner.cpp
SpaceCadetPinball/TFlagSpinner.h
SpaceCadetPinball/TFlipper.cpp
SpaceCadetPinball/TFlipper.h
SpaceCadetPinball/TFlipperEdge.cpp
SpaceCadetPinball/TFlipperEdge.h
SpaceCadetPinball/TGate.cpp
SpaceCadetPinball/TGate.h
SpaceCadetPinball/THole.cpp
SpaceCadetPinball/THole.h
SpaceCadetPinball/timer.cpp
SpaceCadetPinball/timer.h
SpaceCadetPinball/TKickback.cpp
SpaceCadetPinball/TKickback.h
SpaceCadetPinball/TKickout.cpp
SpaceCadetPinball/TKickout.h
SpaceCadetPinball/TLight.cpp
SpaceCadetPinball/TLight.h
SpaceCadetPinball/TLightBargraph.cpp
SpaceCadetPinball/TLightBargraph.h
SpaceCadetPinball/TLightGroup.cpp
SpaceCadetPinball/TLightGroup.h
SpaceCadetPinball/TLightRollover.cpp
SpaceCadetPinball/TLightRollover.h
SpaceCadetPinball/TLine.cpp
SpaceCadetPinball/TLine.h
SpaceCadetPinball/TOneway.cpp
SpaceCadetPinball/TOneway.h
SpaceCadetPinball/TPinballComponent.cpp
SpaceCadetPinball/TPinballComponent.h
SpaceCadetPinball/TPinballTable.cpp
SpaceCadetPinball/TPinballTable.h
SpaceCadetPinball/TPlunger.cpp
SpaceCadetPinball/TPlunger.h
SpaceCadetPinball/TPopupTarget.cpp
SpaceCadetPinball/TPopupTarget.h
SpaceCadetPinball/TRamp.cpp
SpaceCadetPinball/TRamp.h
SpaceCadetPinball/translations.cpp
SpaceCadetPinball/translations.h
SpaceCadetPinball/TRollover.cpp
SpaceCadetPinball/TRollover.h
SpaceCadetPinball/TSink.cpp
SpaceCadetPinball/TSink.h
SpaceCadetPinball/TSoloTarget.cpp
SpaceCadetPinball/TSoloTarget.h
SpaceCadetPinball/TSound.cpp
SpaceCadetPinball/TSound.h
SpaceCadetPinball/TTableLayer.cpp
SpaceCadetPinball/TTableLayer.h
SpaceCadetPinball/TTextBox.cpp
SpaceCadetPinball/TTextBox.h
SpaceCadetPinball/TTextBoxMessage.cpp
SpaceCadetPinball/TTextBoxMessage.h
SpaceCadetPinball/TTimer.cpp
SpaceCadetPinball/TTimer.h
SpaceCadetPinball/TTripwire.cpp
SpaceCadetPinball/TTripwire.h
SpaceCadetPinball/TWall.cpp
SpaceCadetPinball/TWall.h
SpaceCadetPinball/winmain.cpp
SpaceCadetPinball/winmain.h
SpaceCadetPinball/zdrv.cpp
SpaceCadetPinball/zdrv.h
SpaceCadetPinball/imconfig.h
SpaceCadetPinball/imgui_internal.h
SpaceCadetPinball/imgui.cpp
SpaceCadetPinball/imgui.h
SpaceCadetPinball/imgui_sdl.cpp
SpaceCadetPinball/imgui_sdl.h
SpaceCadetPinball/imgui_draw.cpp
SpaceCadetPinball/imgui_widgets.cpp
SpaceCadetPinball/imgui_tables.cpp
SpaceCadetPinball/imgui_demo.cpp
SpaceCadetPinball/imgui_impl_sdl.cpp
SpaceCadetPinball/imgui_impl_sdl.h
SpaceCadetPinball/imgui_impl_sdlrenderer.h
SpaceCadetPinball/imgui_impl_sdlrenderer.cpp
SpaceCadetPinball/imstb_textedit.h
SpaceCadetPinball/imstb_rectpack.h
SpaceCadetPinball/imstb_truetype.h
SpaceCadetPinball/DebugOverlay.cpp
SpaceCadetPinball/DebugOverlay.h
)
# On Windows, include resource file with the icon
if(WIN32)
set_source_files_properties(SpaceCadetPinball/SpaceCadetPinball.rc LANGUAGE RC)
list(APPEND SOURCE_FILES SpaceCadetPinball/SpaceCadetPinball.rc)
endif(WIN32)
add_executable(SpaceCadetPinball ${SOURCE_FILES})
# Skip pch on foreign code
set_source_files_properties(
SpaceCadetPinball/imgui.cpp
SpaceCadetPinball/imgui_sdl.cpp
SpaceCadetPinball/imgui_draw.cpp
SpaceCadetPinball/imgui_widgets.cpp
SpaceCadetPinball/imgui_tables.cpp
SpaceCadetPinball/imgui_demo.cpp
SpaceCadetPinball/imgui_impl_sdl.cpp
SpaceCadetPinball/imgui_impl_sdlrenderer.cpp
PROPERTIES SKIP_PRECOMPILE_HEADERS 1
)
if(${CMAKE_VERSION} VERSION_GREATER "3.16.0" OR ${CMAKE_VERSION} VERSION_EQUAL "3.16.0")
target_precompile_headers(SpaceCadetPinball
PUBLIC
SpaceCadetPinball/pch.h
)
endif()
target_link_libraries(SpaceCadetPinball ${SDL2_LIBRARY} ${SDL2_MIXER_LIBRARY})
# On Windows, copy DLL to output
if(WIN32)
list(GET SDL2_LIBRARY -1 SDL2_DLL_PATH)
list(GET SDL2_MIXER_LIBRARY -1 SDL2_MIXER_DLL_PATH)
get_filename_component(SDL2_DLL_PATH ${SDL2_DLL_PATH} DIRECTORY)
get_filename_component(SDL2_MIXER_DLL_PATH ${SDL2_MIXER_DLL_PATH} DIRECTORY)
if(MINGW)
string(REGEX REPLACE "lib$" "bin" SDL2_DLL_PATH ${SDL2_DLL_PATH})
string(REGEX REPLACE "lib$" "bin" SDL2_MIXER_DLL_PATH ${SDL2_MIXER_DLL_PATH})
endif()
message(STATUS "copy paths='${SDL2_DLL_PATH}' '${SDL2_MIXER_DLL_PATH}'")
add_custom_command(TARGET SpaceCadetPinball POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_DLL_PATH}/SDL2.dll" $<TARGET_FILE_DIR:SpaceCadetPinball>
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_MIXER_DLL_PATH}/SDL2_mixer.dll" $<TARGET_FILE_DIR:SpaceCadetPinball>
)
endif()
if(UNIX AND NOT APPLE)
include(GNUInstallDirs)
install(TARGETS "${PROJECT_NAME}" RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "${CMAKE_SOURCE_DIR}/Platform/Linux/${PROJECT_NAME}.desktop" DESTINATION "share/applications")
install(FILES "${CMAKE_SOURCE_DIR}/Platform/Linux/${PROJECT_NAME}.metainfo.xml" DESTINATION "share/metainfo")
foreach(S 16 32 48 128 192)
install(FILES "${CMAKE_SOURCE_DIR}/${PROJECT_NAME}/Icon_${S}x${S}.png" DESTINATION
"share/icons/hicolor/${S}x${S}/apps" RENAME "${PROJECT_NAME}.png")
endforeach(S)
endif()

173
CMakeModules/FindSDL2.cmake Normal file
View File

@ -0,0 +1,173 @@
# This module defines
# SDL2_LIBRARY, the name of the library to link against
# SDL2_FOUND, if false, do not try to link to SDL2
# SDL2_INCLUDE_DIR, where to find SDL.h
#
# This module responds to the the flag:
# SDL2_BUILDING_LIBRARY
# If this is defined, then no SDL2main will be linked in because
# only applications need main().
# Otherwise, it is assumed you are building an application and this
# module will attempt to locate and set the the proper link flags
# as part of the returned SDL2_LIBRARY variable.
#
# Don't forget to include SDLmain.h and SDLmain.m your project for the
# OS X framework based version. (Other versions link to -lSDL2main which
# this module will try to find on your behalf.) Also for OS X, this
# module will automatically add the -framework Cocoa on your behalf.
#
#
# Additional Note: If you see an empty SDL2_LIBRARY_TEMP in your configuration
# and no SDL2_LIBRARY, it means CMake did not find your SDL2 library
# (SDL2.dll, libsdl2.so, SDL2.framework, etc).
# Set SDL2_LIBRARY_TEMP to point to your SDL2 library, and configure again.
# Similarly, if you see an empty SDL2MAIN_LIBRARY, you should set this value
# as appropriate. These values are used to generate the final SDL2_LIBRARY
# variable, but when these values are unset, SDL2_LIBRARY does not get created.
#
#
# $SDL2DIR is an environment variable that would
# correspond to the ./configure --prefix=$SDL2DIR
# used in building SDL2.
# l.e.galup 9-20-02
#
# Modified by Eric Wing.
# Added code to assist with automated building by using environmental variables
# and providing a more controlled/consistent search behavior.
# Added new modifications to recognize OS X frameworks and
# additional Unix paths (FreeBSD, etc).
# Also corrected the header search path to follow "proper" SDL guidelines.
# Added a search for SDL2main which is needed by some platforms.
# Added a search for threads which is needed by some platforms.
# Added needed compile switches for MinGW.
#
# On OSX, this will prefer the Framework version (if found) over others.
# People will have to manually change the cache values of
# SDL2_LIBRARY to override this selection or set the CMake environment
# CMAKE_INCLUDE_PATH to modify the search paths.
#
# Note that the header path has changed from SDL2/SDL.h to just SDL.h
# This needed to change because "proper" SDL convention
# is #include "SDL.h", not <SDL2/SDL.h>. This is done for portability
# reasons because not all systems place things in SDL2/ (see FreeBSD).
#=============================================================================
# Copyright 2003-2009 Kitware, Inc.
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# (To distribute this file outside of CMake, substitute the full
# License text for the above reference.)
# message("<FindSDL2.cmake>")
SET(SDL2_SEARCH_PATHS
~/Library/Frameworks
/Library/Frameworks
/usr/local
/usr
/sw # Fink
/opt/local # DarwinPorts
/opt/csw # Blastwave
/opt
${SDL2_PATH}
)
FIND_PATH(SDL2_INCLUDE_DIR SDL.h
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES include/SDL2 include SDL2
PATHS ${SDL2_SEARCH_PATHS}
)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(PATH_SUFFIXES lib64 lib/x64 lib)
else()
set(PATH_SUFFIXES lib/x86 lib)
endif()
FIND_LIBRARY(SDL2_LIBRARY_TEMP
NAMES SDL2
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES ${PATH_SUFFIXES}
PATHS ${SDL2_SEARCH_PATHS}
)
IF(NOT SDL2_BUILDING_LIBRARY)
IF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
# Non-OS X framework versions expect you to also dynamically link to
# SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms
# seem to provide SDL2main for compatibility even though they don't
# necessarily need it.
FIND_LIBRARY(SDL2MAIN_LIBRARY
NAMES SDL2main
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES ${PATH_SUFFIXES}
PATHS ${SDL2_SEARCH_PATHS}
)
ENDIF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
ENDIF(NOT SDL2_BUILDING_LIBRARY)
# SDL2 may require threads on your system.
# The Apple build may not need an explicit flag because one of the
# frameworks may already provide it.
# But for non-OSX systems, I will use the CMake Threads package.
IF(NOT APPLE)
FIND_PACKAGE(Threads)
ENDIF(NOT APPLE)
# MinGW needs an additional link flag, -mwindows
# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -mwindows
IF(MINGW)
SET(MINGW32_LIBRARY mingw32 "-mwindows" CACHE STRING "mwindows for MinGW")
ENDIF(MINGW)
IF(SDL2_LIBRARY_TEMP)
# For SDL2main
IF(NOT SDL2_BUILDING_LIBRARY)
IF(SDL2MAIN_LIBRARY)
SET(SDL2_LIBRARY_TEMP ${SDL2MAIN_LIBRARY} ${SDL2_LIBRARY_TEMP})
ENDIF(SDL2MAIN_LIBRARY)
ENDIF(NOT SDL2_BUILDING_LIBRARY)
# For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
# CMake doesn't display the -framework Cocoa string in the UI even
# though it actually is there if I modify a pre-used variable.
# I think it has something to do with the CACHE STRING.
# So I use a temporary variable until the end so I can set the
# "real" variable in one-shot.
IF(APPLE)
SET(SDL2_LIBRARY_TEMP ${SDL2_LIBRARY_TEMP} "-framework Cocoa")
ENDIF(APPLE)
# For threads, as mentioned Apple doesn't need this.
# In fact, there seems to be a problem if I used the Threads package
# and try using this line, so I'm just skipping it entirely for OS X.
IF(NOT APPLE)
SET(SDL2_LIBRARY_TEMP ${CMAKE_THREAD_LIBS_INIT} ${SDL2_LIBRARY_TEMP})
ENDIF(NOT APPLE)
# For MinGW library
IF(MINGW)
SET(SDL2_LIBRARY_TEMP ${MINGW32_LIBRARY} ${SDL2_LIBRARY_TEMP})
ENDIF(MINGW)
# Set the final string here so the GUI reflects the final state.
SET(SDL2_LIBRARY ${SDL2_LIBRARY_TEMP} CACHE STRING "Where the SDL2 Library can be found")
# Set the temp variable to INTERNAL so it is not seen in the CMake GUI
SET(SDL2_LIBRARY_TEMP "${SDL2_LIBRARY_TEMP}" CACHE INTERNAL "")
ENDIF(SDL2_LIBRARY_TEMP)
# message("</FindSDL2.cmake>")
INCLUDE(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2 REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR)

View File

@ -0,0 +1,100 @@
# Locate SDL_MIXER library
#
# This module defines:
#
# ::
#
# SDL2_MIXER_LIBRARIES, the name of the library to link against
# SDL2_MIXER_INCLUDE_DIRS, where to find the headers
# SDL2_MIXER_FOUND, if false, do not try to link against
# SDL2_MIXER_VERSION_STRING - human-readable string containing the version of SDL_MIXER
#
#
#
# For backward compatibility the following variables are also set:
#
# ::
#
# SDLMIXER_LIBRARY (same value as SDL2_MIXER_LIBRARIES)
# SDLMIXER_INCLUDE_DIR (same value as SDL2_MIXER_INCLUDE_DIRS)
# SDLMIXER_FOUND (same value as SDL2_MIXER_FOUND)
#
#
#
# $SDLDIR is an environment variable that would correspond to the
# ./configure --prefix=$SDLDIR used in building SDL.
#
# Created by Eric Wing. This was influenced by the FindSDL.cmake
# module, but with modifications to recognize OS X frameworks and
# additional Unix paths (FreeBSD, etc).
#=============================================================================
# Copyright 2005-2009 Kitware, Inc.
# Copyright 2012 Benjamin Eikel
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# (To distribute this file outside of CMake, substitute the full
# License text for the above reference.)
find_path(SDL2_MIXER_INCLUDE_DIR SDL_mixer.h
HINTS
ENV SDL2MIXERDIR
ENV SDL2DIR
PATH_SUFFIXES SDL2
# path suffixes to search inside ENV{SDLDIR}
include/SDL2 include
PATHS ${SDL2_MIXER_PATH}
)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(VC_LIB_PATH_SUFFIX lib/x64)
else()
set(VC_LIB_PATH_SUFFIX lib/x86)
endif()
find_library(SDL2_MIXER_LIBRARY
NAMES SDL2_mixer
HINTS
ENV SDL2MIXERDIR
ENV SDL2DIR
PATH_SUFFIXES lib bin ${VC_LIB_PATH_SUFFIX}
PATHS ${SDL2_MIXER_PATH}
)
if(SDL2_MIXER_INCLUDE_DIR AND EXISTS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h")
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_MIXER_MAJOR_VERSION[ \t]+[0-9]+$")
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_MIXER_MINOR_VERSION[ \t]+[0-9]+$")
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_MIXER_PATCHLEVEL[ \t]+[0-9]+$")
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_MAJOR "${SDL2_MIXER_VERSION_MAJOR_LINE}")
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_MINOR "${SDL2_MIXER_VERSION_MINOR_LINE}")
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_PATCH "${SDL2_MIXER_VERSION_PATCH_LINE}")
set(SDL2_MIXER_VERSION_STRING ${SDL2_MIXER_VERSION_MAJOR}.${SDL2_MIXER_VERSION_MINOR}.${SDL2_MIXER_VERSION_PATCH})
unset(SDL2_MIXER_VERSION_MAJOR_LINE)
unset(SDL2_MIXER_VERSION_MINOR_LINE)
unset(SDL2_MIXER_VERSION_PATCH_LINE)
unset(SDL2_MIXER_VERSION_MAJOR)
unset(SDL2_MIXER_VERSION_MINOR)
unset(SDL2_MIXER_VERSION_PATCH)
endif()
set(SDL2_MIXER_LIBRARIES ${SDL2_MIXER_LIBRARY})
set(SDL2_MIXER_INCLUDE_DIRS ${SDL2_MIXER_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2_mixer
REQUIRED_VARS SDL2_MIXER_LIBRARIES SDL2_MIXER_INCLUDE_DIRS
VERSION_VAR SDL2_MIXER_VERSION_STRING)
# for backward compatibility
set(SDLMIXER_LIBRARY ${SDL2_MIXER_LIBRARIES})
set(SDLMIXER_INCLUDE_DIR ${SDL2_MIXER_INCLUDE_DIRS})
set(SDLMIXER_FOUND ${SDL2_MIXER_FOUND})
mark_as_advanced(SDL2_MIXER_LIBRARY SDL2_MIXER_INCLUDE_DIR)

74
CMakeSettings.json Normal file
View File

@ -0,0 +1,74 @@
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"addressSanitizerEnabled": false
},
{
"name": "x86-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x86" ],
"addressSanitizerEnabled": false
},
{
"name": "x86-Release",
"generator": "Ninja",
"configurationType": "Release",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x86" ]
},
{
"name": "x64-Release",
"generator": "Ninja",
"configurationType": "Release",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x64_x64" ]
},
{
"name": "x86-Release-WinXP",
"generator": "Visual Studio 16 2019",
"configurationType": "Release",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1 -T v141_xp",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x86" ]
},
{
"name": "WSL-Clang-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeExecutable": "cmake",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "linux_clang_x64" ],
"wslPath": "${defaultWSLPath}"
}
]
}

11
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,11 @@
# Issues
Dont forget to mention game version, which release or branch it came from.\
Source port issues are handled in their respective repositories.
# Pull request
No source ports in main repository.\
I have no way to test and maintain most of them.\
The best I can do is to add a link.
There is no guarantee that any particular PR will be accepted.\
If you are unsure, ask first, make PR second.

5084
Doc/.dat dump.txt Normal file

File diff suppressed because it is too large Load Diff

133
Doc/.dat file format.txt Normal file
View File

@ -0,0 +1,133 @@
+-----------------------------------------------------+
| 3D Pinball for Windows ("PARTOUT") .DAT file format |
| AdrienTD |
+-----------------------------------------------------+
//-- Header --//
+0x00: File signature BYTE*21
+0x15: App name BYTE*50
+0x47: Description BYTE*100
+0xAB: File size DWORD
+0xAF: Number of files/groups WORD
+0xB1: Size of body DWORD
+0xB5: Unknown (0) WORD
In 3D Pinball, the file signature is "PARTOUT(4.0)RESOURCE". The rest is
filled with 0.
//-- Body --//
The body is constitued of groups. Every group begins with a byte that tells
how many entries are in the group.
Every entry begins with a byte that specifies the type of the entry. A DWORD that
tells the size of the coming data follows the byte. Then there's some data for the
entry. However, if the type is 0, the type byte is followed by a short value,
and that's all.
Existing types:
Type Meaning/comments
---------------------------------------------------
0 ?, does not have the 32bits size value, but a 16bits value (see above).
1 8 bpp bitmap
3 Group name
5 Palette (1 color is 1 DWORD, only present 1 time in PINBALL.DAT
with a data size of 1024 bytes for 256 colors. Some colors are 0
because their indexes are reserved by Windows.)
9 String (content)
10 Array of 16bits integer values
11 Array of 32bits floating point values (collision box, ...)
12 16 bpp bitmap (zMap)
//-- 8bpp bitmap data header --//
+0: Resolution BYTE 0=640x480, 1=800x600, 2=1024x768, -1=Load in all resolutions
+1: Width WORD
+3: Height WORD
+5: X position WORD
+7 Y position WORD
+9: Size of bitmap DWORD
+13: Flags BYTE bit0=Raw bmp align; bit1=DibBitmap, raw when 0; bit2=Spliced bitmap (aka skipline), combines bmp and zMap in RLE-like way
+14: Bitmap data BYTE*(DWORD@+9)
//-- 16bpp zMap data header --//
+0: Width WORD
+2: Height WORD
+4: Pitch/2 WORD
+6: Unknown (0) DWORD
+10: Unknown (0) WORD
+12: Unknown (80) WORD
+14: Bitmap data BYTE*(DWORD@+9)
//-- 16bpp zMap data header full tilt --//
+0: Resolution BYTE 0=640x480, 1=800x600, 2=1024x768, -1=Load in all resolutions
+1: Width WORD
+3: Height WORD
+5: Pitch/2 WORD
+7: Unknown (0) DWORD
+11: Unknown (0) WORD
+13: Unknown (80) WORD
+15: Bitmap data BYTE*(DWORD@+9)
//-- Pinball 3D remarkable groups --//
-- table_size
Entry type 10 contains 2 16-bit integers: first is width, second is height.
-- table_objects
In entry type 10, the first integer is unknown, but then comes a series of
16-bits integer pairs. Every pair represents an object of the table. The
first integer is the type of object, and the second is the number of
a group/resource in this data file.
Types of object (3D Pinball Space Cadet for Windows):
1000: ?
1001: Plunger
1002: Light (lite###)
1003: Left flipper
1004: Right flipper
1005: Bumper
1006: Yellow target
1007: Drain (no bmp)
1010: ? (no bmp)
1011: Bloc (dot between the flippers)
1012: kout (no bmp) (?)
1013: Gate
1014: Kicker
1015: Roll
1016: One way (no bmp) (?)
1017: Sink (no bmp) (?)
1018: Flag
1019: Red target
1020: Roll (the green circle that makes a weird sound when rolling on it)
1021: Ramp (no bmp)
1022: Ramp hole (no bmp)
1023: Demo (no bmp)
1024: Trip (no bmp)
1026: Lights (no bmp, list?)
1028: Bumpers list (one for attack, one for launch)
1029: kout (no bmp) (?, similar to 1012?)
1030: Fuel bargraph (list?)
1031: Sound
1033: Text box (score, ball counter, player number)
//-- Information --//
There's an article of the format at http://rewiki.regengedanken.de/.
I haven't read it completely, but some informations helped me a bit.
Another thing: There's nearly nothing about hacking the game on the web (even
though the Space Cadet game bundled with Windows is very popular).
What I've only seen when doing a (Google) web search is the 'hidden test'
cheat code. [as of 2017 or even less]
//-- Contact --//
Any questions? Contact me at [see my GitHub profile @AdrienTD for contacts] .
AdrienTD :)

12193
Doc/.pdb dump.txt Normal file

File diff suppressed because it is too large Load Diff

BIN
Doc/FuncStats.xlsx Normal file

Binary file not shown.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Andrey Muzychenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

25
Platform/Linux/BUILD.md Normal file
View File

@ -0,0 +1,25 @@
# Building
Install devel packages for `SDL2` and `SDL2_mixer`.\
Compile with CMake; tested with GCC 10, Clang 11.\
To cross-compile for Windows, install a 64-bit version of mingw and its `SDL2` and `SDL2_mixer` distributions, then use the `mingwcc.cmake` toolchain.
```bash
# On Debian and Ubuntu
sudo apt install cmake build-essential ninja-build libsdl2-dev libsdl2-mixer-dev libsdl2-mixer-2.0-0 libsdl2-2.0-0 fluidsynth
# On Fedora
sudo dnf install cmake ninja-build SDL2 SDL2-devel SDL2_mixer SDL2_mixer-devel fluidsynth fluidsynth-libs mscore-fonts g++
# Build
cmake -GNinja .
ninja
```
**If you can't hear the background music**: You need to export the following variable before launching the game like this:
`export SDL_SOUNDFONT=/path/to/your/soundfont`
Fluidsynth will install a default soundfont in the following path:
On Fedora: `/usr/share/soundfonts/FluidR3_GM.sf2`
On Ubuntu/Debian: `/usr/share/sounds/sf2/FluidR3_GM.sf2`

View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Name=Space Cadet Pinball
Comment=Decompilation of 3D Pinball for Windows - Space Cadet
Exec=SpaceCadetPinball
StartupNotify=true
Terminal=false
Icon=SpaceCadetPinball
Type=Application
Categories=Game;ArcadeGame;

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.github.k4zmu2a.spacecadetpinball</id>
<name>Space Cadet Pinball</name>
<display_name>Space Cadet Pinball</display_name>
<summary>Game engine for Space Cadet Pinball</summary>
<url type="homepage">https://github.com/k4zmu2a/SpaceCadetPinball</url>
<url type="bugtracker">https://github.com/k4zmu2a/SpaceCadetPinball/issues</url>
<description>
<p>
Reverse engineering of '3D Pinball for Windows - Space Cadet', a game bundled with Windows.
</p>
</description>
<categories>
<category>Game</category>
<category>ArcadeGame</category>
</categories>
<launchable type="desktop-id">SpaceCadetPinball.desktop</launchable>
<provides>
<binary>SpaceCadetPinball</binary>
</provides>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/k4zmu2a/SpaceCadetPinball/master/Screenshots/screenshot-1.jpg</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/k4zmu2a/SpaceCadetPinball/master/Screenshots/screenshot-2.jpg</image>
</screenshot>
</screenshots>
<releases>
<release version="2.1.0" date="2023-10-16">
<description>
<p>Feature release focusing on FT compatibility.</p>
<p>Main highlights:</p>
<ul>
<li>FT: collision system changes, table control changes, multiball, three MIDI tracks.</li>
<li>Work in progress: localization support. Depends on external font selection for most languages.</li>
<li>HW accelerated ImGui renderer.</li>
<li>Experimental stereo sound support.</li>
<li>UX improvements.</li>
<li>Bug fixes.</li>
</ul>
</description>
</release>
<release version="2.0.1" date="2021-12-29">
<description>
<p>First bug fix release of cross-platform port.</p>
<p>Main highlights:</p>
<ul>
<li>Improved Linux support: install target, icons, desktop shortcut, alternative data paths.</li>
<li>Improved game controller support.</li>
<li>High precision sleep mode for ~0 frame time jitter.</li>
<li>Integer image scaling mode.</li>
<li>3DPB &lt;-&gt; FT data switch option, for easier data-set changes when both are available.</li>
<li>FT demo data support.</li>
<li>Bug fixes.</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2021-10-30">
<description>
<p>First release of cross-platform port.</p>
<p>Main highlights:</p>
<ul>
<li>Windows dependencies removed.</li>
<li>Graphics and sounds rendered with SDL.</li>
<li>Dear ImGui GUI.</li>
<li>CMake build script, compiles on a lot of platforms.</li>
<li>A number of 3rd party source ports.</li>
</ul>
<p>All features of v1.x are present, except for text translations.</p>
<p>
Development and releases of v1.x will continue, dont confuse the two.
All source ports are technically v2.0RC, not v1.x.
</p>
<p>There are no Linux or macOS binaries attached, compile them yourself.</p>
<p>
This release was made better with the help of contributors.
I thank them for their interest in the project.
</p>
</description>
</release>
</releases>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Andrey Muzychenko</developer_name>
<project_license>MIT</project_license>
<keywords>
<keyword>pinball</keyword>
<keyword>space</keyword>
<keyword>cadet</keyword>
</keywords>
<content_rating type="oars-1.1" />
</component>

36
Platform/macOS/Info.plist Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//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>CFBundleExecutable</key>
<string>SpaceCadetPinball</string>
<key>CFBundleIconFile</key>
<string>SpaceCadetPinball.icns</string>
<key>CFBundleIdentifier</key>
<string>com.github.k4zmu2a.spacecadetpinball</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string></string>
<key>CFBundleName</key>
<string>SpaceCadetPinball</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>CHANGEME_SW_VERSION</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.11</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

Binary file not shown.

123
README.md Normal file
View File

@ -0,0 +1,123 @@
<!-- markdownlint-disable-file MD033 -->
# SpaceCadetPinball
## Summary
Reverse engineering of `3D Pinball for Windows - Space Cadet`, a game bundled with Windows.
## How to play
Place compiled executable into a folder containing original game resources (not included).\
Supports data files from Windows and Full Tilt versions of the game.
## Known source ports
| Platform | Author | URL |
| ------------------ | --------------- | ---------------------------------------------------------------------------------------------------------- |
| PS Vita | Axiom | <https://github.com/suicvne/SpaceCadetPinball_Vita> |
| Emscripten | alula | <https://github.com/alula/SpaceCadetPinball> <br> Play online: <https://alula.github.io/SpaceCadetPinball> |
| Nintendo Switch | averne | <https://github.com/averne/SpaceCadetPinball-NX> |
| webOS TV | mariotaku | <https://github.com/webosbrew/SpaceCadetPinball> |
| Android (WIP) | Iscle | https://github.com/Iscle/SpaceCadetPinball |
| Nintendo Wii | MaikelChan | https://github.com/MaikelChan/SpaceCadetPinball |
| Nintendo 3DS | MaikelChan | https://github.com/MaikelChan/SpaceCadetPinball/tree/3ds |
| Nintendo DS | Headshotnoby | https://github.com/headshot2017/3dpinball-nds |
| Nintendo Wii U | IntriguingTiles | https://github.com/IntriguingTiles/SpaceCadetPinball-WiiU |
| PlayStation 2 | Headshotnoby | https://github.com/headshot2017/3dpinball-ps2 |
| Sega Dreamcast | Headshotnoby | https://github.com/headshot2017/3dpinball-dc |
| MorphOS | BeWorld | https://www.morphos-storage.net/?id=1688897 |
| AmigaOS 4 | rjd324 | http://aminet.net/package/game/actio/spacecadetpinball-aos4 |
| Android (WIP) | fexed | https://github.com/fexed/Pinball-on-Android |
Platforms covered by this project: desktop Windows, Linux and macOS.
<br>
<br>
<br>
<br>
<br>
<br>
## Source
* `pinball.exe` from `Windows XP` (SHA-1 `2A5B525E0F631BB6107639E2A69DF15986FB0D05`) and its public PDB
* `CADET.EXE` 32bit version from `Full Tilt! Pinball` (SHA-1 `3F7B5699074B83FD713657CD94671F2156DBEDC4`)
## Tools used
`Ghidra`, `Ida`, `Visual Studio`
## What was done
* All structures were populated, globals and locals named.
* All subs were decompiled, C pseudo code was converted to compilable C++. Loose (namespace?) subs were assigned to classes.
## Compiling
Project uses `C++11` and depends on `SDL2` libs.
### On Windows
Download and unpack devel packages for `SDL2` and `SDL2_mixer`.\
Set paths to them in `CMakeLists.txt`, see suggested placement in `/Libs`.\
Compile with Visual Studio; tested with 2019.
### On Linux
Install devel packages for `SDL2` and `SDL2_mixer`.\
Compile with CMake; tested with GCC 10, Clang 11.\
To cross-compile for Windows, install a 64-bit version of mingw and its `SDL2` and `SDL2_mixer` distributions, then use the `mingwcc.cmake` toolchain.
[![Packaging status](https://repology.org/badge/tiny-repos/spacecadetpinball.svg)](https://repology.org/project/spacecadetpinball/versions)
Some distributions provide a package in their repository. You can use those for easier dependency management and updates.
This project is available as Flatpak on [Flathub](https://flathub.org/apps/details/com.github.k4zmu2a.spacecadetpinball).
### On macOS
Install XCode (or at least Xcode Command Line Tools with `xcode-select --install`) and CMake.
**HomeBrew**
You can easily install the build artifact by using `brew`.
```sh
brew tap draftbrew/tap
brew install --no-quarantine space-cadet-pinball
```
Be aware that the flag `--no-quarantime` will disable macOS's Gatekeeper during installation.
**Manual compilation:**
* **Homebrew**: Install the `SDL2`, `SDL2_mixer` homebrew packages.
* **MacPorts**: Install the `libSDL2`, `libSDL2_mixer` macports packages.
Compile with CMake. Ensure that `CMAKE_OSX_ARCHITECTURES` variable is set for either `x86_64` Apple Intel or `arm64` for Apple Silicon.
Tested with: macOS Big Sur (Intel) with Xcode 13 & macOS Montery Beta (Apple Silicon) with Xcode 13.
**Automated compilation:**
Run the `build-mac-app.sh` script from the root of the repository. The app will be available in a DMG file named `SpaceCadetPinball-<version>-mac.dmg`.
Tested with: macOS Ventura (Apple Silicon) with Xcode Command Line Tools 14 & macOS Big Sur on GitHub Runner (Intel) with XCode 13.
## Plans
* ~~Decompile original game~~
* ~~Resizable window, scaled graphics~~
* ~~Loader for high-res sprites from CADET.DAT~~
* ~~Cross-platform port using SDL2, SDL2_mixer, ImGui~~
* Full Tilt Cadet features
* Localization support
* Maybe: Support for the other two tables - Dragon and Pirate
* Maybe: Game data editor
## On 64-bit bug that killed the game
I did not find it, decompiled game worked in x64 mode on the first try.\
It was either lost in decompilation or introduced in x64 port/not present in x86 build.\
Based on public description of the bug (no ball collision), I guess that the bug was in `TEdgeManager::TestGridBox`

View File

@ -0,0 +1,358 @@
#include "pch.h"
#include "DebugOverlay.h"
#include "loader.h"
#include "maths.h"
#include "proj.h"
#include "winmain.h"
#include "TFlipperEdge.h"
#include "TFlipper.h"
#include "pb.h"
#include "TLine.h"
#include "TCircle.h"
#include "TPinballTable.h"
#include "TEdgeBox.h"
#include "TTableLayer.h"
#include "TBall.h"
#include "render.h"
#include "options.h"
#include "Sound.h"
gdrv_bitmap8* DebugOverlay::dbScreen = nullptr;
static int SDL_RenderDrawCircle(SDL_Renderer* renderer, int x, int y, int radius)
{
SDL_Point points[256];
int pointCount = 0;
int offsetx, offsety, d;
int status;
offsetx = 0;
offsety = radius;
d = radius - 1;
status = 0;
while (offsety >= offsetx)
{
if (pointCount + 8 > 256)
{
status = SDL_RenderDrawPoints(renderer, points, pointCount);
pointCount = 0;
if (status < 0) {
status = -1;
break;
}
}
points[pointCount++] = { x + offsetx, y + offsety };
points[pointCount++] = { x + offsety, y + offsetx };
points[pointCount++] = { x - offsetx, y + offsety };
points[pointCount++] = { x - offsety, y + offsetx };
points[pointCount++] = { x + offsetx, y - offsety };
points[pointCount++] = { x + offsety, y - offsetx };
points[pointCount++] = { x - offsetx, y - offsety };
points[pointCount++] = { x - offsety, y - offsetx };
if (d >= 2 * offsetx) {
d -= 2 * offsetx + 1;
offsetx += 1;
}
else if (d < 2 * (radius - offsety)) {
d += 2 * offsety - 1;
offsety -= 1;
}
else {
d += 2 * (offsety - offsetx - 1);
offsety -= 1;
offsetx += 1;
}
}
if (pointCount > 0)
status = SDL_RenderDrawPoints(renderer, points, pointCount);
return status;
}
void DebugOverlay::UnInit()
{
delete dbScreen;
dbScreen = nullptr;
}
void DebugOverlay::DrawOverlay()
{
if (dbScreen == nullptr)
{
dbScreen = new gdrv_bitmap8(render::vscreen->Width, render::vscreen->Height, false, false);
dbScreen->CreateTexture("nearest", SDL_TEXTUREACCESS_TARGET);
SDL_SetTextureBlendMode(dbScreen->Texture, SDL_BLENDMODE_BLEND);
}
// Setup overlay rendering
Uint8 initialR, initialG, initialB, initialA;
auto initialRenderTarget = SDL_GetRenderTarget(winmain::Renderer);
SDL_GetRenderDrawColor(winmain::Renderer, &initialR, &initialG, &initialB, &initialA);
SDL_SetRenderTarget(winmain::Renderer, dbScreen->Texture);
SDL_SetRenderDrawColor(winmain::Renderer, 0, 0, 0, 0);
SDL_RenderClear(winmain::Renderer);
// Draw EdgeManager box grid
if (options::Options.DebugOverlayGrid)
DrawBoxGrid();
// Draw bounding boxes around sprites
if (options::Options.DebugOverlaySprites)
DrawAllSprites();
// Draw all edges registered in TCollisionComponent.EdgeList + flippers
if (options::Options.DebugOverlayAllEdges)
DrawAllEdges();
// Draw ball collision info
if (options::Options.DebugOverlayBallPosition || options::Options.DebugOverlayBallEdges)
DrawBallInfo();
// Draw positions associated with currently playing sound channels
if (options::Options.DebugOverlaySounds)
DrawSoundPositions();
// Draw ball depth cutoff steps that determine sprite size.
if (options::Options.DebugOverlayBallDepthGrid)
DrawBallDepthSteps();
// Draw AABB of collision components
if (options::Options.DebugOverlayAabb)
DrawComponentAabb();
// Restore render target
SDL_SetRenderTarget(winmain::Renderer, initialRenderTarget);
SDL_SetRenderDrawColor(winmain::Renderer,
initialR, initialG, initialB, initialA);
// Copy overlay with alpha blending
SDL_BlendMode blendMode;
SDL_GetRenderDrawBlendMode(winmain::Renderer, &blendMode);
SDL_SetRenderDrawBlendMode(winmain::Renderer, SDL_BLENDMODE_BLEND);
SDL_RenderCopy(winmain::Renderer, dbScreen->Texture, nullptr, &render::DestinationRect);
SDL_SetRenderDrawBlendMode(winmain::Renderer, blendMode);
}
void DebugOverlay::DrawBoxGrid()
{
auto& edgeMan = *TTableLayer::edge_manager;
SDL_SetRenderDrawColor(winmain::Renderer, 0, 255, 0, 255);
for (int x = 0; x <= edgeMan.MaxBoxX; x++)
{
vector2 boxPt{ x * edgeMan.AdvanceX + edgeMan.MinX , edgeMan.MinY };
auto pt1 = proj::xform_to_2d(boxPt);
boxPt.Y = edgeMan.MaxBoxY * edgeMan.AdvanceY + edgeMan.MinY;
auto pt2 = proj::xform_to_2d(boxPt);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
}
for (int y = 0; y <= edgeMan.MaxBoxY; y++)
{
vector2 boxPt{ edgeMan.MinX, y * edgeMan.AdvanceY + edgeMan.MinY };
auto pt1 = proj::xform_to_2d(boxPt);
boxPt.X = edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX;
auto pt2 = proj::xform_to_2d(boxPt);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
}
}
void DebugOverlay::DrawAllEdges()
{
SDL_SetRenderDrawColor(winmain::Renderer, 0, 200, 200, 255);
for (auto cmp : pb::MainTable->ComponentList)
{
auto collCmp = dynamic_cast<TCollisionComponent*>(cmp);
if (collCmp)
{
for (auto edge : collCmp->EdgeList)
{
DrawEdge(edge);
}
}
auto flip = dynamic_cast<TFlipper*>(cmp);
if (flip)
{
DrawEdge(flip->FlipperEdge);
}
}
}
void DebugOverlay::DrawBallInfo()
{
auto& edgeMan = *TTableLayer::edge_manager;
for (auto ball : pb::MainTable->BallList)
{
if (ball->ActiveFlag)
{
vector2 ballPosition = { ball->Position.X, ball->Position.Y };
if (options::Options.DebugOverlayBallEdges)
{
SDL_SetRenderDrawColor(winmain::Renderer, 255, 0, 0, 255);
auto x = edgeMan.box_x(ballPosition.X), y = edgeMan.box_y(ballPosition.Y);
auto& box = edgeMan.BoxArray[x + y * edgeMan.MaxBoxX];
for (auto edge : box.EdgeList)
{
DrawEdge(edge);
}
}
if (options::Options.DebugOverlayBallPosition)
{
SDL_SetRenderDrawColor(winmain::Renderer, 0, 0, 255, 255);
auto pt1 = proj::xform_to_2d(ballPosition);
vector2 radVec1 = { 0, ballPosition.Y }, radVec2 = { ball->Radius, ballPosition.Y };
auto radVec1I = proj::xform_to_2d(radVec1), radVec2I = proj::xform_to_2d(radVec2);
auto radI = std::sqrt(maths::magnitudeSq(vector2i{ radVec1I.X - radVec2I.X ,radVec1I.Y - radVec2I.Y }));
SDL_RenderDrawCircle(winmain::Renderer, pt1.X, pt1.Y, static_cast<int>(std::round(radI)));
auto nextPos = ballPosition;
maths::vector_add(nextPos, maths::vector_mul(ball->Direction, ball->Speed / 10.0f));
auto pt2 = proj::xform_to_2d(nextPos);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
}
}
}
}
void DebugOverlay::DrawAllSprites()
{
SDL_SetRenderDrawColor(winmain::Renderer, 200, 200, 0, 255);
for (auto cmp : pb::MainTable->ComponentList)
{
if (cmp->RenderSprite)
{
auto& bmpR = cmp->RenderSprite->BmpRect;
if (bmpR.Width != 0 && bmpR.Height != 0)
{
SDL_Rect rect{ bmpR.XPosition, bmpR.YPosition, bmpR.Width, bmpR.Height };
SDL_RenderDrawRect(winmain::Renderer, &rect);
}
}
}
}
void DebugOverlay::DrawSoundPositions()
{
auto& edgeMan = *TTableLayer::edge_manager;
SDL_SetRenderDrawColor(winmain::Renderer, 200, 0, 200, 255);
for (auto& posNorm : Sound::Channels)
{
auto pos3D = edgeMan.DeNormalizeBox(posNorm.Position);
auto pos2D = proj::xform_to_2d(pos3D);
SDL_RenderDrawCircle(winmain::Renderer, pos2D.X, pos2D.Y, 7);
}
}
void DebugOverlay::DrawBallDepthSteps()
{
auto& edgeMan = *TTableLayer::edge_manager;
SDL_SetRenderDrawColor(winmain::Renderer, 200, 100, 0, 255);
for (auto ball : pb::MainTable->BallList)
{
auto visualCount = loader::query_visual_states(ball->GroupIndex);
for (auto index = 0; index < visualCount; ++index)
{
auto depthPt = reinterpret_cast<vector3*>(loader::query_float_attribute(ball->GroupIndex, index, 501));
auto pt = proj::xform_to_2d(*depthPt);
// Snap X coordinate to edge box sides
auto x1 = proj::xform_to_2d(vector2{edgeMan.MinX, depthPt->Y}).X;
auto x2 = proj::xform_to_2d(vector2{edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX, depthPt->Y}).X;
auto ff = proj::xform_to_2d(vector2{ edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX, depthPt->Y });
SDL_RenderDrawLine(winmain::Renderer, x1, pt.Y, x2, pt.Y);
}
break;
}
}
void DebugOverlay::DrawComponentAabb()
{
SDL_SetRenderDrawColor(winmain::Renderer, 0, 50, 200, 255);
for (auto cmp : pb::MainTable->ComponentList)
{
auto collCmp = dynamic_cast<TCollisionComponent*>(cmp);
if (collCmp)
{
const auto& aabb = collCmp->AABB;
auto pt1 = proj::xform_to_2d(vector2{ aabb.XMax, aabb.YMax });
auto pt2 = proj::xform_to_2d(vector2{ aabb.XMin, aabb.YMin });
SDL_Rect rect{ pt2.X,pt2.Y, pt1.X - pt2.X , pt1.Y - pt2.Y };
SDL_RenderDrawRect(winmain::Renderer, &rect);
}
}
}
void DebugOverlay::DrawCicleType(circle_type& circle)
{
vector2 linePt{ circle.Center.X + sqrt(circle.RadiusSq), circle.Center.Y };
auto pt1 = proj::xform_to_2d(circle.Center);
auto pt2 = proj::xform_to_2d(linePt);
auto radius = abs(pt2.X - pt1.X);
SDL_RenderDrawCircle(winmain::Renderer, pt1.X, pt1.Y, radius);
}
void DebugOverlay::DrawLineType(line_type& line)
{
auto pt1 = proj::xform_to_2d(line.Origin);
auto pt2 = proj::xform_to_2d(line.End);
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
}
void DebugOverlay::DrawEdge(TEdgeSegment* edge)
{
if (options::Options.DebugOverlayCollisionMask)
{
TBall* refBall = nullptr;
for (auto ball : pb::MainTable->BallList)
{
if (ball->ActiveFlag)
{
refBall = ball;
break;
}
}
if (refBall != nullptr && (refBall->CollisionMask & edge->CollisionGroup) == 0)
return;
}
auto line = dynamic_cast<TLine*>(edge);
if (line)
{
DrawLineType(line->Line);
return;
}
auto circle = dynamic_cast<TCircle*>(edge);
if (circle)
{
DrawCicleType(circle->Circle);
return;
}
auto flip = dynamic_cast<TFlipperEdge*>(edge);
if (flip)
{
if (flip->ControlPointDirtyFlag)
flip->set_control_points(flip->CurrentAngle);
DrawLineType(flip->LineA);
DrawLineType(flip->LineB);
DrawCicleType(flip->circlebase);
DrawCicleType(flip->circleT1);
}
}

View File

@ -0,0 +1,26 @@
#pragma once
struct gdrv_bitmap8;
struct circle_type;
struct line_type;
class TEdgeSegment;
class DebugOverlay
{
public:
static void UnInit();
static void DrawOverlay();
private:
static gdrv_bitmap8* dbScreen;
static void DrawCicleType(circle_type& circle);
static void DrawLineType(line_type& line);
static void DrawEdge(TEdgeSegment* edge);
static void DrawBoxGrid();
static void DrawAllEdges();
static void DrawBallInfo();
static void DrawAllSprites();
static void DrawSoundPositions();
static void DrawBallDepthSteps();
static void DrawComponentAabb();
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
#pragma once
class EmbeddedData
{
public:
static const char PB_MSGFT_bin_compressed_data_base85[5380 + 1];
static const unsigned int SDL_GameControllerDB_compressed_data[63392 / 4];
static const unsigned int SDL_GameControllerDB_compressed_size;
};

View File

@ -0,0 +1,361 @@
#include "pch.h"
#include "GroupData.h"
#include "EmbeddedData.h"
#include "fullscrn.h"
#include "gdrv.h"
#include "pb.h"
#include "zdrv.h"
EntryData::~EntryData()
{
if (Buffer)
{
if (EntryType == FieldTypes::Bitmap8bit)
{
delete reinterpret_cast<gdrv_bitmap8*>(Buffer);
}
else if (EntryType == FieldTypes::Bitmap16bit)
{
delete reinterpret_cast<zmap_header_type*>(Buffer);
}
else
delete[] Buffer;
}
}
GroupData::GroupData(int groupId)
{
GroupId = groupId;
}
void GroupData::AddEntry(EntryData* entry)
{
auto addEntry = true;
switch (entry->EntryType)
{
case FieldTypes::GroupName:
GroupName = entry->Buffer;
break;
case FieldTypes::Bitmap8bit:
{
auto srcBmp = reinterpret_cast<gdrv_bitmap8*>(entry->Buffer);
if (srcBmp->BitmapType == BitmapTypes::Spliced)
{
// Get rid of spliced bitmap early on, to simplify render pipeline
auto bmp = new gdrv_bitmap8(srcBmp->Width, srcBmp->Height, true);
auto zMap = new zmap_header_type(srcBmp->Width, srcBmp->Height, srcBmp->Width);
SplitSplicedBitmap(*srcBmp, *bmp, *zMap);
NeedsSort = true;
addEntry = false;
AddEntry(new EntryData(FieldTypes::Bitmap8bit, reinterpret_cast<char*>(bmp)));
AddEntry(new EntryData(FieldTypes::Bitmap16bit, reinterpret_cast<char*>(zMap)));
delete entry;
}
else
{
SetBitmap(srcBmp);
}
break;
}
case FieldTypes::Bitmap16bit:
{
SetZMap(reinterpret_cast<zmap_header_type*>(entry->Buffer));
break;
}
default: break;
}
if (addEntry)
Entries.push_back(entry);
}
void GroupData::FinalizeGroup()
{
if (NeedsSort)
{
// Entries within a group are sorted by EntryType, in ascending order.
// Dat files follow this rule, zMaps inserted in the middle break it.
NeedsSort = false;
std::sort(Entries.begin(), Entries.end(), [](const EntryData* lhs, const EntryData* rhs)
{
return lhs->EntryType < rhs->EntryType;
});
Entries.shrink_to_fit();
}
}
gdrv_bitmap8* GroupData::GetBitmap(int resolution) const
{
return Bitmaps[resolution];
}
zmap_header_type* GroupData::GetZMap(int resolution) const
{
return ZMaps[resolution];
}
void GroupData::SplitSplicedBitmap(const gdrv_bitmap8& srcBmp, gdrv_bitmap8& bmp, zmap_header_type& zMap)
{
assertm(srcBmp.BitmapType == BitmapTypes::Spliced, "GroupData: wrong bitmap type");
std::memset(bmp.IndexedBmpPtr, 0xff, bmp.Stride * bmp.Height);
bmp.XPosition = srcBmp.XPosition;
bmp.YPosition = srcBmp.YPosition;
bmp.Resolution = srcBmp.Resolution;
zdrv::fill(&zMap, zMap.Width, zMap.Height, 0, 0, 0xFFFF);
zMap.Resolution = srcBmp.Resolution;
auto tableWidth = fullscrn::resolution_array[srcBmp.Resolution].TableWidth;
auto src = reinterpret_cast<uint16_t*>(srcBmp.IndexedBmpPtr);
auto srcChar = reinterpret_cast<char**>(&src);
for (int dstInd = 0;;)
{
auto stride = static_cast<int16_t>(*src++);
if (stride < 0)
break;
// Stride is in terms of dst stride, hardcoded to match vScreen width in current resolution
if (stride > bmp.Width)
{
stride += bmp.Width - tableWidth;
assertm(stride >= 0, "Spliced bitmap: negative computed stride");
}
dstInd += stride;
for (auto count = *src++; count; count--)
{
auto depth = *src++;
bmp.IndexedBmpPtr[dstInd] = **srcChar;
zMap.ZPtr1[dstInd] = depth;
(*srcChar)++;
dstInd++;
}
}
}
void GroupData::SetBitmap(gdrv_bitmap8* bmp)
{
assertm(Bitmaps[bmp->Resolution] == nullptr, "GroupData: bitmap override");
Bitmaps[bmp->Resolution] = bmp;
auto zMap = ZMaps[bmp->Resolution];
if (zMap)
{
assertm(bmp->Width == zMap->Width && bmp->Height == zMap->Height,
"GroupData: mismatched bitmap/zMap dimensions");
}
}
void GroupData::SetZMap(zmap_header_type* zMap)
{
// Flip zMap to match with flipped non-indexed bitmaps
zdrv::FlipZMapHorizontally(*zMap);
assertm(ZMaps[zMap->Resolution] == nullptr, "GroupData: zMap override");
ZMaps[zMap->Resolution] = zMap;
auto bmp = Bitmaps[zMap->Resolution];
if (bmp)
{
assertm(bmp->Width == zMap->Width && bmp->Height == zMap->Height,
"GroupData: mismatched bitmap/zMap dimensions");
}
}
DatFile::~DatFile()
{
for (auto group : Groups)
{
if (!group)
continue;
for (const auto entry : group->GetEntries())
{
delete entry;
}
delete group;
}
}
char* DatFile::field(int groupIndex, FieldTypes targetEntryType)
{
assertm(targetEntryType != FieldTypes::Bitmap8bit && targetEntryType != FieldTypes::Bitmap16bit,
"partman: Use specific get for bitmaps");
auto group = Groups[groupIndex];
for (const auto entry : group->GetEntries())
{
if (entry->EntryType == targetEntryType)
{
return entry->Buffer;
}
if (entry->EntryType > targetEntryType)
break;
}
return nullptr;
}
char* DatFile::field_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN)
{
assertm(targetEntryType != FieldTypes::Bitmap8bit && targetEntryType != FieldTypes::Bitmap16bit,
"partman: Use specific get for bitmaps");
auto group = Groups[groupIndex];
auto skipCount = 0;
for (const auto entry : group->GetEntries())
{
if (entry->EntryType > targetEntryType)
break;
if (entry->EntryType == targetEntryType)
if (skipCount++ == skipFirstN)
return entry->Buffer;
}
return nullptr;
}
int DatFile::field_size_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN)
{
auto group = Groups[groupIndex];
auto skipCount = 0;
for (const auto entry : group->GetEntries())
{
if (entry->EntryType > targetEntryType)
return 0;
if (entry->EntryType == targetEntryType)
if (skipCount++ == skipFirstN)
return entry->FieldSize;
}
return 0;
}
int DatFile::field_size(int groupIndex, FieldTypes targetEntryType)
{
return field_size_nth(groupIndex, targetEntryType, 0);
}
int DatFile::record_labeled(LPCSTR targetGroupName)
{
auto targetLength = strlen(targetGroupName);
for (int groupIndex = static_cast<int>(Groups.size()) - 1; groupIndex >= 0; --groupIndex)
{
auto groupName = field(groupIndex, FieldTypes::GroupName);
if (!groupName)
continue;
auto index = 0u;
for (; index < targetLength; index++)
if (targetGroupName[index] != groupName[index])
break;
if (index == targetLength && !targetGroupName[index] && !groupName[index])
return groupIndex;
}
return -1;
}
char* DatFile::field_labeled(LPCSTR lpString, FieldTypes fieldType)
{
auto groupIndex = record_labeled(lpString);
return groupIndex < 0 ? nullptr : field(groupIndex, fieldType);
}
gdrv_bitmap8* DatFile::GetBitmap(int groupIndex)
{
auto group = Groups[groupIndex];
return group->GetBitmap(fullscrn::GetResolution());
}
zmap_header_type* DatFile::GetZMap(int groupIndex)
{
auto group = Groups[groupIndex];
return group->GetZMap(fullscrn::GetResolution());
}
void DatFile::Finalize()
{
if (!pb::FullTiltMode)
{
int groupIndex = record_labeled("pbmsg_ft");
assertm(groupIndex < 0, "DatFile: pbmsg_ft is already in .dat");
// Load 3DPB font into dat to simplify pipeline
auto rcData = reinterpret_cast<MsgFont*>(ImFontAtlas::DecompressCompressedBase85Data(
EmbeddedData::PB_MSGFT_bin_compressed_data_base85));
AddMsgFont(rcData, "pbmsg_ft");
IM_FREE(rcData);
// PINBALL2.MID is an alternative font provided in 3DPB data
// Scaled down because it is too large for top text box
/*auto file = pb::make_path_name("PINBALL2.MID");
auto fileHandle = fopenu(file.c_str(), "rb");
fseek(fileHandle, 0, SEEK_END);
auto fileSize = static_cast<uint32_t>(ftell(fileHandle));
auto rcData = reinterpret_cast<MsgFont*>(new uint8_t[fileSize]);
fseek(fileHandle, 0, SEEK_SET);
fread(rcData, 1, fileSize, fileHandle);
fclose(fileHandle);
auto groupId = Groups.back()->GroupId + 1u;
AddMsgFont(rcData, "pbmsg_ft");
delete[] rcData;
for (auto i = groupId; i < Groups.size(); i++)
Groups[i]->GetBitmap(0)->ScaleIndexed(0.84f, 0.84f);*/
}
for (auto group : Groups)
{
group->FinalizeGroup();
}
}
void DatFile::AddMsgFont(MsgFont* font, const std::string& fontName)
{
auto groupId = Groups.back()->GroupId + 1;
auto ptrToData = reinterpret_cast<char*>(font->Data);
for (auto charInd = 32; charInd < 128; charInd++, groupId++)
{
auto curChar = reinterpret_cast<MsgFontChar*>(ptrToData);
assertm(curChar->Width == font->CharWidths[charInd], "Score: mismatched font width");
ptrToData += curChar->Width * font->Height + 1;
auto bmp = new gdrv_bitmap8(curChar->Width, font->Height, true);
auto srcPtr = curChar->Data;
auto dstPtr = &bmp->IndexedBmpPtr[bmp->Stride * (bmp->Height - 1)];
for (auto y = 0; y < font->Height; ++y)
{
memcpy(dstPtr, srcPtr, curChar->Width);
srcPtr += curChar->Width;
dstPtr -= bmp->Stride;
}
auto group = new GroupData(groupId);
group->AddEntry(new EntryData(FieldTypes::Bitmap8bit, reinterpret_cast<char*>(bmp)));
if (charInd == 32)
{
// First font group holds font name and gap width
auto groupName = new char[fontName.length() + 1];
strcpy(groupName, fontName.c_str());
group->AddEntry(new EntryData(FieldTypes::GroupName, groupName));
auto gaps = new char[2];
*reinterpret_cast<int16_t*>(gaps) = font->GapWidth;
group->AddEntry(new EntryData(FieldTypes::ShortArray, gaps));
}
else
{
auto groupName = new char[30];
sprintf(groupName, "char %d='%c'", charInd, charInd);
group->AddEntry(new EntryData(FieldTypes::GroupName, groupName));
}
Groups.push_back(group);
}
}

View File

@ -0,0 +1,128 @@
#pragma once
struct zmap_header_type;
struct gdrv_bitmap8;
enum class FieldTypes : int16_t
{
// One 16 bit signed integer
ShortValue = 0,
// Sprite bitmap, 8bpp, indexed color
Bitmap8bit = 1,
Unknown2 = 2,
// Group name, char[]. Not all groups have names.
GroupName = 3,
Unknown4 = 4,
// Palette, contains 256 RBGA 4-byte colors.
Palette = 5,
Unknown6 = 6,
Unknown7 = 7,
Unknown8 = 8,
// String, char[]
String = 9,
// Array of 16 bit signed integers
ShortArray = 10,
// Array of 32 bit floats
FloatArray = 11,
// Sprite depth map, 16bpp, unsigned
Bitmap16bit = 12,
};
struct EntryData
{
EntryData() = default;
EntryData(FieldTypes entryType, char* buffer): EntryType(entryType), FieldSize(-1), Buffer(buffer)
{
}
~EntryData();
FieldTypes EntryType{};
int FieldSize{};
char* Buffer{};
};
#pragma pack(push, 1)
struct MsgFontChar
{
uint8_t Width;
char Data[1];
};
struct MsgFont
{
int16_t GapWidth;
int16_t Unknown1;
int16_t Height;
uint8_t CharWidths[128];
MsgFontChar Data[1];
};
#pragma pack(pop)
static_assert(sizeof(MsgFont) == 136, "Wrong size of MsgFont");
class GroupData
{
public:
int GroupId;
std::string GroupName;
GroupData(int groupId);
void AddEntry(EntryData* entry);
void FinalizeGroup();
const std::vector<EntryData*>& GetEntries() const { return Entries; }
const EntryData* GetEntry(size_t index) const { return Entries[index]; }
size_t EntryCount() const { return Entries.size(); }
void ReserveEntries(size_t count) { Entries.reserve(count); }
gdrv_bitmap8* GetBitmap(int resolution) const;
zmap_header_type* GetZMap(int resolution) const;
private:
std::vector<EntryData*> Entries;
gdrv_bitmap8* Bitmaps[3]{};
zmap_header_type* ZMaps[3]{};
bool NeedsSort = false;
static void SplitSplicedBitmap(const gdrv_bitmap8& srcBmp, gdrv_bitmap8& bmp, zmap_header_type& zMap);
void SetBitmap(gdrv_bitmap8* bmp);
void SetZMap(zmap_header_type* zMap);
};
class DatFile
{
public:
std::string AppName;
std::string Description;
std::vector<GroupData*> Groups;
~DatFile();
char* field_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN);
char* field(int groupIndex, FieldTypes entryType);
int field_size_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN);
int field_size(int groupIndex, FieldTypes targetEntryType);
int record_labeled(LPCSTR targetGroupName);
char* field_labeled(LPCSTR lpString, FieldTypes fieldType);
gdrv_bitmap8* GetBitmap(int groupIndex);
zmap_header_type* GetZMap(int groupIndex);
void Finalize();
private:
void AddMsgFont(MsgFont* font, const std::string& fontName);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="ColorRgba">
<DisplayString>{{ Color:{Color} }}</DisplayString>
<Expand>
<Item Name="[Alpha]" ExcludeView="simple">Color>>alphaOffset &amp; 255</Item>
<Item Name="[Red]" ExcludeView="simple">Color>>redOffset &amp; 255</Item>
<Item Name="[Green]" ExcludeView="simple">Color>>greenOffset &amp; 255</Item>
<Item Name="[Blue]" ExcludeView="simple">Color>>blueOffset &amp; 255</Item>
<Item Name="[Color]" ExcludeView="simple">Color</Item>
</Expand>
</Type>
<Type Name="vector3">
<DisplayString>{{ X={X} Y={Y} Z={Z} }}</DisplayString>
</Type>
</AutoVisualizer>

Binary file not shown.

154
SpaceCadetPinball/Sound.cpp Normal file
View File

@ -0,0 +1,154 @@
#include "pch.h"
#include "options.h"
#include "Sound.h"
#include "maths.h"
int Sound::num_channels;
bool Sound::enabled_flag = false;
std::vector<ChannelInfo> Sound::Channels{};
int Sound::Volume = MIX_MAX_VOLUME;
bool Sound::MixOpen = false;
void Sound::Init(bool mixOpen, int channels, bool enableFlag, int volume)
{
MixOpen = mixOpen;
Volume = volume;
SetChannels(channels);
Enable(enableFlag);
}
void Sound::Enable(bool enableFlag)
{
enabled_flag = enableFlag;
if (MixOpen && !enableFlag)
Mix_HaltChannel(-1);
}
void Sound::Activate()
{
if (MixOpen)
Mix_Resume(-1);
}
void Sound::Deactivate()
{
if (MixOpen)
Mix_Pause(-1);
}
void Sound::Close()
{
Enable(false);
Channels.clear();
}
void Sound::PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent* soundSource, const char* info)
{
if (MixOpen && wavePtr && enabled_flag)
{
if (Mix_Playing(-1) == num_channels)
{
auto cmp = [](const ChannelInfo& a, const ChannelInfo& b)
{
return a.TimeStamp < b.TimeStamp;
};
auto min = std::min_element(Channels.begin(), Channels.end(), cmp);
auto oldestChannel = static_cast<int>(std::distance(Channels.begin(), min));
Mix_HaltChannel(oldestChannel);
}
auto channel = Mix_PlayChannel(-1, wavePtr, 0);
if (channel != -1)
{
Channels[channel].TimeStamp = time;
if (options::Options.SoundStereo)
{
// Positional audio uses collision grid 2D coordinates normalized to [0, 1]
// Point (0, 0) is bottom left table corner; point (1, 1) is top right table corner.
// Z is defined as: 0 at table level, positive axis goes up from table surface.
// Get the source sound position.
// Sound without position are assumed to be at the center top of the table.
vector3 soundPos{};
if (soundSource)
{
auto soundPos2D = soundSource->get_coordinates();
soundPos = {soundPos2D.X, soundPos2D.Y, 0.0f};
}
else
{
soundPos = {0.5f, 1.0f, 0.0f};
}
Channels[channel].Position = soundPos;
// Listener is positioned at the bottom center of the table,
// at 0.5 height, so roughly a table half - length.
vector3 playerPos = {0.5f, 0.0f, 0.5f};
auto soundDir = maths::vector_sub(soundPos, playerPos);
// Find sound angle from positive Y axis in clockwise direction with atan2
// Remap atan2 output from (-Pi, Pi] to [0, 2 * Pi)
auto angle = fmodf(atan2(soundDir.X, soundDir.Y) + Pi * 2, Pi * 2);
auto angleDeg = angle * 180.0f / Pi;
auto angleSdl = static_cast<Sint16>(angleDeg);
// Distance from listener to the sound position is roughly in the [0, ~1.22] range.
// Remap to [0, 122] by multiplying by 100 and cast to an integer.
auto distance = static_cast<Uint8>(100.0f * maths::magnitude(soundDir));
// Mix_SetPosition expects an angle in (Sint16)degrees, where
// angle 0 is due north, and rotates clockwise as the value increases.
// Mix_SetPosition expects a (Uint8)distance from 0 (near) to 255 (far).
Mix_SetPosition(channel, angleSdl, distance);
// Output position of each sound emitted so we can verify
// the sanity of the implementation.
/*printf("X: %3.3f Y: %3.3f Angle: %3.3f Distance: %3d, Object: %s\n",
soundPos.X,
soundPos.Y,
angleDeg,
distance,
info
);*/
}
}
}
}
Mix_Chunk* Sound::LoadWaveFile(const std::string& lpName)
{
if (!MixOpen)
return nullptr;
auto wavFile = fopenu(lpName.c_str(), "r");
if (!wavFile)
return nullptr;
fclose(wavFile);
return Mix_LoadWAV(lpName.c_str());
}
void Sound::FreeSound(Mix_Chunk* wave)
{
if (MixOpen && wave)
Mix_FreeChunk(wave);
}
void Sound::SetChannels(int channels)
{
if (channels <= 0)
channels = 8;
num_channels = channels;
Channels.resize(num_channels);
if (MixOpen)
Mix_AllocateChannels(num_channels);
SetVolume(Volume);
}
void Sound::SetVolume(int volume)
{
Volume = volume;
if (MixOpen)
Mix_Volume(-1, volume);
}

31
SpaceCadetPinball/Sound.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include "maths.h"
#include "TPinballComponent.h"
struct ChannelInfo
{
int TimeStamp;
vector2 Position;
};
class Sound
{
public:
static std::vector<ChannelInfo> Channels;
static void Init(bool mixOpen, int channels, bool enableFlag, int volume);
static void Enable(bool enableFlag);
static void Activate();
static void Deactivate();
static void Close();
static void PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent *soundSource, const char* info);
static Mix_Chunk* LoadWaveFile(const std::string& lpName);
static void FreeSound(Mix_Chunk* wave);
static void SetChannels(int channels);
static void SetVolume(int volume);
private:
static int num_channels;
static bool enabled_flag;
static int Volume;
static bool MixOpen;
};

View File

@ -0,0 +1,55 @@
#include "pch.h"
#include "winmain.h"
int main(int argc, char* argv[])
{
std::string cmdLine;
for (int i = 1; i < argc; i++)
{
if (i > 1)
cmdLine += " ";
cmdLine += argv[i];
}
return winmain::WinMain(cmdLine.c_str());
}
#if _WIN32
#include <windows.h>
// Windows subsystem main
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
return winmain::WinMain(lpCmdLine);
}
// fopen to _wfopen adapter, for UTF-8 paths
FILE* fopenu(const char* path, const char* opt)
{
wchar_t* wideArgs[2]{};
for (auto& arg : wideArgs)
{
auto src = wideArgs[0] ? opt : path;
auto length = MultiByteToWideChar(CP_UTF8, 0, src, -1, nullptr, 0);
arg = new wchar_t[length];
MultiByteToWideChar(CP_UTF8, 0, src, -1, arg, length);
}
auto fileHandle = _wfopen(wideArgs[0], wideArgs[1]);
for (auto arg : wideArgs)
delete[] arg;
return fileHandle;
}
#endif
// Run program: Ctrl + F5 or Debug > Start Without Debugging menu
// Debug program: F5 or Debug > Start Debugging menu
// Tips for Getting Started:
// 1. Use the Solution Explorer window to add/manage files
// 2. Use the Team Explorer window to connect to source control
// 3. Use the Output window to see build output and other messages
// 4. Use the Error List window to view errors
// 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project
// 6. In the future, to open this project again, go to File > Open > Project and select the .sln file

View File

@ -0,0 +1 @@
ICON_1 ICON "Icon_1.ico"

197
SpaceCadetPinball/TBall.cpp Normal file
View File

@ -0,0 +1,197 @@
#include "pch.h"
#include "TBall.h"
#include "fullscrn.h"
#include "loader.h"
#include "maths.h"
#include "pb.h"
#include "proj.h"
#include "render.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TBall::TBall(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false),
TEdgeSegment(this, &ActiveFlag, 0)
{
visualStruct visual{};
char ballGroupName[10]{"ball"};
RayMaxDistance = 0.0;
ActiveFlag = 1;
CollisionComp = nullptr;
EdgeCollisionCount = 0;
TimeDelta = 0.0;
CollisionFlag = 0;
Speed = 0.0;
Direction.Y = 0.0;
Direction.X = 0.0;
ListBitmap = new std::vector<SpriteData>();
if (groupIndex == -1)
{
HasGroupFlag = false;
Position = {0, 0, 0};
CollisionMask = 1;
}
else
{
HasGroupFlag = true;
loader::query_visual(groupIndex, 0, &visual);
CollisionMask = visual.CollisionGroup;
auto floatArr = loader::query_float_attribute(groupIndex, 0, 408);
Position = {floatArr[0], floatArr[1], floatArr[3]};
}
/*Full tilt: ball is ballN, where N[0,2] resolution*/
groupIndex = loader::query_handle(ballGroupName);
if (groupIndex < 0)
{
ballGroupName[4] = '0' + fullscrn::GetResolution();
groupIndex = loader::query_handle(ballGroupName);
}
Radius = *loader::query_float_attribute(groupIndex, 0, 500);
auto visualCount = loader::query_visual_states(groupIndex);
for (auto index = 0; index < visualCount; ++index)
{
loader::query_visual(groupIndex, index, &visual);
ListBitmap->push_back(visual.Bitmap);
auto visVec = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, index, 501));
auto zDepth = proj::z_distance(*visVec);
VisualZArray[index] = zDepth;
}
RenderSprite = new render_sprite(VisualTypes::Ball, nullptr, nullptr, 0, 0, nullptr);
PinballTable->CollisionCompOffset = Radius;
Position.Z = Radius;
GroupIndex = groupIndex;
}
void TBall::Repaint()
{
if (CollisionFlag)
{
Position.Z =
CollisionOffset.X * Position.X +
CollisionOffset.Y * Position.Y +
Radius + CollisionOffset.Z;
}
auto pos2D = proj::xform_to_2d(Position);
auto zDepth = proj::z_distance(Position);
auto index = 0u;
for (; index < ListBitmap->size() - 1; ++index)
{
if (VisualZArray[index] <= zDepth) break;
}
SpriteSetBall(index, pos2D, zDepth);
}
void TBall::not_again(TEdgeSegment* edge)
{
if (EdgeCollisionCount < 16)
{
Collisions[EdgeCollisionCount] = edge;
++EdgeCollisionCount;
}
else
{
for (int i = 0; i < 8; i++)
Collisions[i] = Collisions[i + 8];
Collisions[8] = edge;
EdgeCollisionCount = 9;
}
EdgeCollisionResetFlag = true;
}
bool TBall::already_hit(const TEdgeSegment& edge) const
{
for (int i = 0; i < EdgeCollisionCount; i++)
{
if (Collisions[i] == &edge)
return true;
}
return false;
}
int TBall::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset)
{
SpriteSetBall(-1, {0, 0}, 0.0f);
Position.X = 0.0;
CollisionComp = nullptr;
Position.Y = 0.0;
ActiveFlag = 0;
CollisionFlag = 0;
CollisionMask = 1;
Direction.Y = 0.0;
Position.Z = Radius;
Direction.X = 0.0;
Speed = 0.0;
RayMaxDistance = 0.0;
}
return 0;
}
void TBall::throw_ball(vector3* direction, float angleMult, float speedMult1, float speedMult2)
{
CollisionComp = nullptr;
Direction = *direction;
float rnd = RandFloat();
float angle = (1.0f - (rnd + rnd)) * angleMult;
maths::RotateVector(Direction, angle);
rnd = RandFloat();
Speed = (1.0f - (rnd + rnd)) * (speedMult1 * speedMult2) + speedMult1;
}
void TBall::EdgeCollision(TBall* ball, float distance)
{
ball->CollisionDisabledFlag = true;
ball->Position.X += ball->Direction.X * distance;
ball->Position.Y += ball->Direction.Y * distance;
ball->Direction.X *= ball->Speed;
ball->Direction.Y *= ball->Speed;
Direction.X *= Speed;
Direction.Y *= Speed;
// AB - vector from ball to this, BA - from this to ball; collision direction
vector2 AB{ball->Position.X - Position.X, ball->Position.Y - Position.Y};
maths::normalize_2d(AB);
vector2 BA{-AB.X, -AB.Y};
// Projection = difference between ball directions and collision direction
auto projAB = -maths::DotProduct(ball->Direction, AB);
auto projBA = -maths::DotProduct(Direction, BA);
vector2 delta{AB.X * projAB - BA.X * projBA, AB.Y * projAB - BA.Y * projBA};
ball->Direction.X += delta.X;
ball->Direction.Y += delta.Y;
ball->Speed = maths::normalize_2d(ball->Direction);
Direction.X -= delta.X;
Direction.Y -= delta.Y;
Speed = maths::normalize_2d(Direction);
}
float TBall::FindCollisionDistance(const ray_type& ray)
{
// Original inherits TCircle and aliases position.
const circle_type ballCircle{{Position.X, Position.Y}, Radius * Radius * 4.0f};
return maths::ray_intersect_circle(ray, ballCircle);
}
vector2 TBall::get_coordinates()
{
return TTableLayer::edge_manager->NormalizeBox(Position);
}
void TBall::Disable()
{
ActiveFlag = false;
CollisionDisabledFlag = true;
SpriteSet(-1);
}

41
SpaceCadetPinball/TBall.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include "maths.h"
#include "TCollisionComponent.h"
#include "TEdgeSegment.h"
class TBall : public TCollisionComponent, public TEdgeSegment
{
public :
TBall(TPinballTable* table, int groupIndex);
void Repaint();
void not_again(TEdgeSegment* edge);
bool already_hit(const TEdgeSegment& edge) const;
int Message(MessageCode code, float value) override;
vector2 get_coordinates() override;
void Disable();
void throw_ball(vector3* direction, float angleMult, float speedMult1, float speedMult2);
void place_in_grid(RectF* aabb) override {}
void EdgeCollision(TBall* ball, float distance) override;
float FindCollisionDistance(const ray_type& ray) override;
vector3 Position{};
vector3 PrevPosition{};
vector3 Direction{};
float Speed;
float RayMaxDistance;
float TimeDelta;
vector2 RampFieldForce{};
TCollisionComponent* CollisionComp;
int CollisionMask;
TEdgeSegment* Collisions[16]{};
int EdgeCollisionCount;
bool EdgeCollisionResetFlag{};
vector3 CollisionOffset{};
int CollisionFlag;
float Radius;
bool HasGroupFlag;
int StuckCounter = 0;
int LastActiveTime{};
float VisualZArray[50]{};
bool CollisionDisabledFlag{};
};

View File

@ -0,0 +1,72 @@
#include "pch.h"
#include "TBlocker.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "timer.h"
TBlocker::TBlocker(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
visualStruct visual{};
loader::query_visual(groupIndex, 0, &visual);
SoundIndex4 = visual.SoundIndex4;
SoundIndex3 = visual.SoundIndex3;
InitialDuration = 55;
ExtendedDuration = 5;
Threshold = 1000000000.0f;
Timer = 0;
MessageField = 0;
ActiveFlag = 0;
SpriteSet(-1);
}
int TBlocker::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::SetTiltLock:
case MessageCode::PlayerChanged:
case MessageCode::Reset:
case MessageCode::TBlockerDisable:
if (Timer)
{
timer::kill(Timer);
Timer = 0;
}
MessageField = 0;
ActiveFlag = 0;
SpriteSet(-1);
if (code == MessageCode::TBlockerDisable)
loader::play_sound(SoundIndex3, this, "TBlocker1");
break;
case MessageCode::TBlockerEnable:
ActiveFlag = 1;
loader::play_sound(SoundIndex4, this, "TBlocker2");
SpriteSet(0);
if (Timer)
timer::kill(Timer);
Timer = 0;
if (value >= 0)
Timer = timer::set(value, this, TimerExpired);
break;
case MessageCode::TBlockerRestartTimeout:
if (Timer)
timer::kill(Timer);
Timer = timer::set(std::max(value, 0.0f), this, TimerExpired);
break;
default:
break;
}
return 0;
}
void TBlocker::TimerExpired(int timerId, void* caller)
{
auto blocker = static_cast<TBlocker*>(caller);
blocker->Timer = 0;
control::handler(MessageCode::ControlTimerExpired, blocker);
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "TCollisionComponent.h"
class TBlocker :
public TCollisionComponent
{
public:
TBlocker(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
static void TimerExpired(int timerId, void* caller);
int InitialDuration;
int ExtendedDuration;
int Timer;
int SoundIndex4;
int SoundIndex3;
};

View File

@ -0,0 +1,123 @@
#include "pch.h"
#include "TBumper.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "timer.h"
#include "TPinballTable.h"
TBumper::TBumper(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
visualStruct visual{};
BmpIndex = 0;
Timer = 0;
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
loader::query_visual(groupIndex, 0, &visual);
SoundIndex4 = visual.SoundIndex4;
SoundIndex3 = visual.SoundIndex3;
OriginalThreshold = Threshold;
}
int TBumper::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TBumperSetBmpIndex:
{
auto nextBmp = static_cast<int>(floor(value));
auto maxBmp = static_cast<int>(ListBitmap->size()) - 1;
if (2 * nextBmp > maxBmp)
nextBmp = maxBmp / 2;
if (nextBmp < 0)
nextBmp = 0;
if (nextBmp != BmpIndex)
{
if (nextBmp >= BmpIndex)
loader::play_sound(SoundIndex4, this, "TBumper1");
if (nextBmp < BmpIndex)
loader::play_sound(SoundIndex3, this, "TBumper2");
BmpIndex = nextBmp;
Fire();
control::handler(MessageCode::TBumperSetBmpIndex, this);
}
break;
}
case MessageCode::TBumperIncBmpIndex:
{
auto nextBmp = BmpIndex + 1;
auto maxBmp = static_cast<int>(ListBitmap->size()) - 1;
if (2 * nextBmp > maxBmp)
nextBmp = maxBmp / 2;
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(nextBmp));
break;
}
case MessageCode::TBumperDecBmpIndex:
{
auto nextBmp = BmpIndex - 1;
if (nextBmp < 0)
nextBmp = 0;
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(nextBmp));
break;
}
case MessageCode::PlayerChanged:
{
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
playerPtr->BmpIndex = BmpIndex;
playerPtr->MessageField = MessageField;
playerPtr = &PlayerData[static_cast<int>(floor(value))];
BmpIndex = playerPtr->BmpIndex;
MessageField = playerPtr->MessageField;
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(BmpIndex));
break;
}
case MessageCode::Reset:
{
if (Timer)
{
timer::kill(Timer);
TimerExpired(Timer, this);
}
BmpIndex = 0;
MessageField = 0;
for (auto& playerPtr : PlayerData)
{
playerPtr.BmpIndex = 0;
playerPtr.MessageField = 0;
}
TimerExpired(0, this);
break;
}
default:
break;
}
return 0;
}
void TBumper::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (DefaultCollision(ball, nextPosition, direction))
{
Fire();
control::handler(MessageCode::ControlCollision, this);
}
}
void TBumper::TimerExpired(int timerId, void* caller)
{
auto bump = static_cast<TBumper*>(caller);
bump->SpriteSet(bump->BmpIndex * 2);
bump->Timer = 0;
bump->Threshold = bump->OriginalThreshold;
}
void TBumper::Fire()
{
SpriteSet(2 * BmpIndex + 1);
Timer = timer::set(TimerTime, this, TimerExpired);
Threshold = 1000000000.0;
}

View File

@ -0,0 +1,30 @@
#pragma once
#include "TCollisionComponent.h"
struct TBumper_player_backup
{
int MessageField;
int BmpIndex;
};
class TBumper :
public TCollisionComponent
{
public:
TBumper(TPinballTable* table, int groupIndex);
~TBumper() override = default;
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
void Fire();
static void TimerExpired(int timerId, void* caller);
int BmpIndex;
int Timer;
float TimerTime;
float OriginalThreshold;
int SoundIndex4;
int SoundIndex3;
TBumper_player_backup PlayerData[4]{};
};

View File

@ -0,0 +1,44 @@
#include "pch.h"
#include "TCircle.h"
#include "TBall.h"
#include "TCollisionComponent.h"
#include "TTableLayer.h"
TCircle::TCircle(TCollisionComponent* collComp, char* activeFlag, unsigned collisionGroup, vector2* center,
float radius): TEdgeSegment(collComp, activeFlag, collisionGroup)
{
Circle.RadiusSq = radius * radius;
Circle.Center = *center;
}
float TCircle::FindCollisionDistance(const ray_type& ray)
{
return maths::ray_intersect_circle(ray, Circle);
}
void TCircle::EdgeCollision(TBall* ball, float distance)
{
vector2 direction{}, nextPosition{};
nextPosition.X = distance * ball->Direction.X + ball->Position.X;
nextPosition.Y = distance * ball->Direction.Y + ball->Position.Y;
direction.X = nextPosition.X - Circle.Center.X;
direction.Y = nextPosition.Y - Circle.Center.Y;
maths::normalize_2d(direction);
CollisionComponent->Collision(ball, &nextPosition, &direction, distance, this);
}
void TCircle::place_in_grid(RectF* aabb)
{
if(aabb)
{
const auto radius = sqrt(Circle.RadiusSq);
aabb->Merge({
Circle.Center.X + radius, Circle.Center.Y + radius,
Circle.Center.X - radius, Circle.Center.Y - radius
});
}
TTableLayer::edges_insert_circle(&Circle, this, nullptr);
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "maths.h"
#include "TEdgeSegment.h"
class TCircle :
public TEdgeSegment
{
public:
circle_type Circle{};
TCircle(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup, vector2* center,
float radius);
float FindCollisionDistance(const ray_type& ray) override;
void EdgeCollision(TBall* ball, float distance) override;
void place_in_grid(RectF* aabb) override;
};

View File

@ -0,0 +1,103 @@
#include "pch.h"
#include "TCollisionComponent.h"
#include "loader.h"
#include "maths.h"
#include "TEdgeSegment.h"
#include "TPinballTable.h"
#include "TBall.h"
TCollisionComponent::TCollisionComponent(TPinballTable* table, int groupIndex, bool createWall) :
TPinballComponent(table, groupIndex, true)
{
visualStruct visual{};
ActiveFlag = 1;
AABB = { -10000, -10000, 10000, 10000 };
if (GroupName != nullptr)
UnusedBaseFlag = 1;
if (groupIndex <= 0)
{
loader::default_vsi(&visual);
}
else
{
loader::query_visual(groupIndex, 0, &visual);
if (createWall)
{
float offset = table->CollisionCompOffset;
float* floatArr = loader::query_float_attribute(groupIndex, 0, 600);
TEdgeSegment::install_wall(floatArr, this, &ActiveFlag, visual.CollisionGroup, offset, 0);
}
}
Threshold = visual.Kicker.Threshold;
Elasticity = visual.Elasticity;
Smoothness = visual.Smoothness;
Boost = visual.Kicker.Boost;
HardHitSoundId = visual.Kicker.HardHitSoundId;
SoftHitSoundId = visual.SoftHitSoundId;
GroupIndex = groupIndex;
}
TCollisionComponent::~TCollisionComponent()
{
for (auto edge : EdgeList)
delete edge;
}
void TCollisionComponent::port_draw()
{
for (auto edge : EdgeList)
edge->port_draw();
}
bool TCollisionComponent::DefaultCollision(TBall* ball, vector2* nextPosition, vector2* direction)
{
if (PinballTable->TiltLockFlag)
{
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
return false;
}
bool collision = false;
auto projSpeed = maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, Threshold, Boost);
if (projSpeed > Threshold)
{
loader::play_sound(HardHitSoundId, ball, "TCollisionComponent1");
collision = true;
}
else if (projSpeed > 0.2f)
loader::play_sound(SoftHitSoundId, ball, "TCollisionComponent2");
return collision;
}
void TCollisionComponent::Collision(TBall* ball, vector2* nextPosition, vector2* direction,
float distance, TEdgeSegment* edge)
{
if (PinballTable->TiltLockFlag)
{
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
return;
}
auto projSpeed = maths::basic_collision(
ball,
nextPosition,
direction,
Elasticity,
Smoothness,
Threshold,
Boost);
if (projSpeed > Threshold)
loader::play_sound(HardHitSoundId, ball, "TCollisionComponent3");
else if (projSpeed > 0.2f)
loader::play_sound(SoftHitSoundId, ball, "TCollisionComponent4");
}
int TCollisionComponent::FieldEffect(TBall* ball, vector2* vecDst)
{
return 0;
}

View File

@ -0,0 +1,28 @@
#pragma once
#include "maths.h"
#include "TPinballComponent.h"
struct vector2;
class TEdgeSegment;
class TBall;
class TCollisionComponent : public TPinballComponent
{
public:
std::vector<TEdgeSegment*> EdgeList;
float Elasticity;
float Smoothness;
float Boost;
float Threshold;
int SoftHitSoundId;
int HardHitSoundId;
RectF AABB;
TCollisionComponent(TPinballTable* table, int groupIndex, bool createWall);
~TCollisionComponent() override;
void port_draw() override;
virtual void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge);
virtual int FieldEffect(TBall* ball, vector2* vecDst);
bool DefaultCollision(TBall* ball, vector2* nextPosition, vector2* direction);
};

View File

@ -0,0 +1,64 @@
#include "pch.h"
#include "TComponentGroup.h"
#include "control.h"
#include "loader.h"
#include "timer.h"
#include "TPinballTable.h"
TComponentGroup::TComponentGroup(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, false)
{
Timer = 0;
if (groupIndex > 0)
{
int attrCount;
auto shortArr = loader::query_iattribute(groupIndex, 1027, &attrCount);
auto shortArrPtr = shortArr;
for (auto index = 0; index < attrCount; ++index, ++shortArrPtr)
{
auto component = table->find_component(*shortArrPtr);
if (component)
List.push_back(component);
}
}
}
TComponentGroup::~TComponentGroup()
{
if (Timer)
{
timer::kill(Timer);
Timer = 0;
}
}
int TComponentGroup::Message(MessageCode code, float value)
{
if (code == MessageCode::TComponentGroupResetNotifyTimer)
{
if (this->Timer)
{
timer::kill(this->Timer);
this->Timer = 0;
}
if (value > 0.0f)
this->Timer = timer::set(value, this, NotifyTimerExpired);
}
else if (code < MessageCode::Pause || (code > MessageCode::SetTiltLock &&
code != MessageCode::PlayerChanged && code != MessageCode::GameOver))
{
for (auto component : List)
{
component->Message(code, value);
}
}
return 0;
}
void TComponentGroup::NotifyTimerExpired(int timerId, void* caller)
{
auto compGroup = static_cast<TComponentGroup*>(caller);
compGroup->Timer = 0;
control::handler(MessageCode::ControlNotifyTimerExpired, compGroup);
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "TPinballComponent.h"
class TComponentGroup :
public TPinballComponent
{
public:
TComponentGroup(TPinballTable* table, int groupIndex);
~TComponentGroup() override;
int Message(MessageCode code, float value) override;
static void NotifyTimerExpired(int timerId, void* caller);
std::vector<TPinballComponent*> List;
int Timer;
};

203
SpaceCadetPinball/TDemo.cpp Normal file
View File

@ -0,0 +1,203 @@
#include "pch.h"
#include "TDemo.h"
#include "loader.h"
#include "pb.h"
#include "TEdgeSegment.h"
#include "timer.h"
#include "TPinballTable.h"
#include "TBall.h"
TDemo::TDemo(TPinballTable* table, int groupIndex)
: TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
RestartGameTimer = 0;
PlungerFlag = 0;
FlipLeftTimer = 0;
FlipRightTimer = 0;
MessageField = 0;
UnusedBaseFlag = 0;
ActiveFlag = 0;
FlipRightFlag = 0;
FlipLeftFlag = 0;
table->Demo = this;
auto floatArr1 = loader::query_float_attribute(groupIndex, 0, 407);
if (floatArr1)
{
FlipTimerTime1 = floatArr1[0];
FlipTimerTime2 = floatArr1[1];
UnFlipTimerTime1 = floatArr1[2];
UnFlipTimerTime2 = floatArr1[3];
}
else
{
FlipTimerTime1 = 0.2f;
FlipTimerTime2 = 0.1f;
UnFlipTimerTime1 = 0.2f;
UnFlipTimerTime2 = 0.1f;
}
loader::query_visual(groupIndex, 0, &visual);
auto v5 = loader::query_float_attribute(groupIndex, 0, 1400);
Edge1 = TEdgeSegment::install_wall(v5, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1400);
auto v6 = loader::query_float_attribute(groupIndex, 0, 1401);
TEdgeSegment::install_wall(v6, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1401);
auto v7 = loader::query_float_attribute(groupIndex, 0, 1402);
Edge2 = TEdgeSegment::install_wall(v7, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1402);
auto v8 = loader::query_float_attribute(groupIndex, 0, 1403);
TEdgeSegment::install_wall(v8, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1403);
auto v9 = loader::query_float_attribute(groupIndex, 0, 1404);
Edge3 = TEdgeSegment::install_wall(v9, this, &ActiveFlag, visual.CollisionGroup, table->CollisionCompOffset, 1404);
}
int TDemo::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::NewGame:
if (RestartGameTimer)
timer::kill(RestartGameTimer);
RestartGameTimer = 0;
break;
case MessageCode::GameOver:
if (RestartGameTimer)
timer::kill(RestartGameTimer);
RestartGameTimer = 0;
if (ActiveFlag != 0)
RestartGameTimer = timer::set(5.0, this, NewGameRestartTimer);
break;
case MessageCode::Reset:
if (FlipLeftTimer)
timer::kill(FlipLeftTimer);
FlipLeftTimer = 0;
if (FlipRightTimer)
timer::kill(FlipRightTimer);
FlipRightTimer = 0;
if (FlipLeftFlag != 0)
UnFlipLeft(0, this);
if (FlipRightFlag)
UnFlipRight(0, this);
if (PlungerFlag)
PlungerRelease(0, this);
break;
default:
break;
}
return 0;
}
void TDemo::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
ball->not_again(edge);
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
switch (reinterpret_cast<size_t>(edge->WallValue))
{
case 1400:
if (!FlipLeftTimer && !FlipLeftFlag)
{
float time = FlipTimerTime1 + FlipTimerTime2 - RandFloat() * (FlipTimerTime2 + FlipTimerTime2);
FlipLeftTimer = timer::set(time, this, FlipLeft);
}
break;
case 1401:
FlipLeft(0, this);
break;
case 1402:
if (!FlipRightTimer && !FlipRightFlag)
{
float time = FlipTimerTime1 + FlipTimerTime2 - RandFloat() * (FlipTimerTime2 + FlipTimerTime2);
FlipRightTimer = timer::set(time, this, FlipRight);
}
break;
case 1403:
FlipRight(0, this);
break;
case 1404:
if (!PlungerFlag)
{
PinballTable->Message(MessageCode::PlungerInputPressed, 0);
float time = RandFloat() + 2.0f;
PlungerFlag = timer::set(time, this, PlungerRelease);
}
break;
default:
break;
}
}
void TDemo::PlungerRelease(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
demo->PlungerFlag = 0;
demo->PinballTable->Message(MessageCode::PlungerInputReleased, pb::time_next);
}
void TDemo::UnFlipRight(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
if (demo->FlipRightFlag)
demo->PinballTable->Message(MessageCode::RightFlipperInputReleased, pb::time_next);
demo->FlipRightFlag = 0;
}
void TDemo::UnFlipLeft(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
if (demo->FlipLeftFlag)
demo->PinballTable->Message(MessageCode::LeftFlipperInputReleased, pb::time_next);
demo->FlipLeftFlag = 0;
}
void TDemo::FlipRight(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
if (!demo->FlipRightFlag)
{
if (demo->FlipRightTimer)
{
timer::kill(demo->FlipRightTimer);
demo->FlipRightTimer = 0;
}
demo->PinballTable->Message(MessageCode::RightFlipperInputPressed, pb::time_next);
demo->FlipRightFlag = 1;
float time = demo->UnFlipTimerTime1 + demo->UnFlipTimerTime2 - RandFloat() *
(demo->UnFlipTimerTime2 + demo->UnFlipTimerTime2);
timer::set(time, demo, UnFlipRight);
}
}
void TDemo::FlipLeft(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
if (!demo->FlipLeftFlag)
{
if (demo->FlipLeftTimer)
{
timer::kill(demo->FlipLeftTimer);
demo->FlipLeftTimer = 0;
}
demo->PinballTable->Message(MessageCode::LeftFlipperInputPressed, pb::time_next);
demo->FlipLeftFlag = 1;
float time = demo->UnFlipTimerTime1 + demo->UnFlipTimerTime2 - RandFloat() *
(demo->UnFlipTimerTime2 + demo->UnFlipTimerTime2);
timer::set(time, demo, UnFlipLeft);
}
}
void TDemo::NewGameRestartTimer(int timerId, void* caller)
{
auto demo = static_cast<TDemo*>(caller);
pb::replay_level(true);
demo->PinballTable->Message(MessageCode::NewGame, static_cast<float>(demo->PinballTable->PlayerCount));
demo->RestartGameTimer = 0;
}

33
SpaceCadetPinball/TDemo.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include "TCollisionComponent.h"
class TDemo :
public TCollisionComponent
{
public:
TDemo(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void PlungerRelease(int timerId, void* caller);
static void UnFlipRight(int timerId, void* caller);
static void UnFlipLeft(int timerId, void* caller);
static void FlipRight(int timerId, void* caller);
static void FlipLeft(int timerId, void* caller);
static void NewGameRestartTimer(int timerId, void* caller);
float FlipTimerTime1;
float FlipTimerTime2;
float UnFlipTimerTime1;
float UnFlipTimerTime2;
int FlipLeftFlag;
int FlipRightFlag;
int FlipLeftTimer;
int FlipRightTimer;
int PlungerFlag;
int RestartGameTimer;
TEdgeSegment* Edge1;
TEdgeSegment* Edge2;
TEdgeSegment* Edge3;
};

View File

@ -0,0 +1,48 @@
#include "pch.h"
#include "TDrain.h"
#include "control.h"
#include "loader.h"
#include "TBall.h"
#include "timer.h"
#include "TPinballTable.h"
TDrain::TDrain(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
Timer = 0;
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
}
int TDrain::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset)
{
if (Timer)
{
timer::kill(Timer);
Timer = 0;
}
PinballTable->BallInDrainFlag = 0;
}
return 0;
}
void TDrain::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
ball->Disable();
--PinballTable->MultiballCount;
if (PinballTable->MultiballCount <= 0)
{
PinballTable->MultiballCount = 0;
PinballTable->BallInDrainFlag = 1;
Timer = timer::set(TimerTime, this, TimerCallback);
}
control::handler(MessageCode::ControlCollision, this);
}
void TDrain::TimerCallback(int timerId, void* caller)
{
auto drain = static_cast<TDrain*>(caller);
control::handler(MessageCode::ControlTimerExpired, drain);
}

View File

@ -0,0 +1,17 @@
#pragma once
#include "TCollisionComponent.h"
class TDrain :
public TCollisionComponent
{
public:
TDrain(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void TimerCallback(int timerId, void* caller);
float TimerTime;
int Timer;
};

View File

@ -0,0 +1,11 @@
#pragma once
struct field_effect_type;
class TEdgeSegment;
class TEdgeBox
{
public:
std::vector<TEdgeSegment*> EdgeList{};
std::vector<field_effect_type*> FieldList{};
};

View File

@ -0,0 +1,231 @@
#include "pch.h"
#include "TEdgeManager.h"
#include "maths.h"
#include "TBall.h"
#include "TEdgeBox.h"
#include "TEdgeSegment.h"
#include "TTableLayer.h"
TEdgeManager::TEdgeManager(float xMin, float yMin, float width, float height)
{
Width = width;
Height = height;
MinX = xMin;
MinY = yMin;
MaxX = MinX + width;
MaxY = MinY + height;
MaxBoxX = 10;
MaxBoxY = 15;
AdvanceX = width / static_cast<float>(MaxBoxX);
AdvanceY = height / static_cast<float>(MaxBoxY);
BoxArray = new TEdgeBox[MaxBoxX * MaxBoxY];
}
TEdgeManager::~TEdgeManager()
{
delete[] BoxArray;
}
int TEdgeManager::box_x(float x)
{
return std::max(0, std::min(static_cast<int>(floor((x - MinX) / AdvanceX)), MaxBoxX - 1));
}
int TEdgeManager::box_y(float y)
{
return std::max(0, std::min(static_cast<int>(floor((y - MinY) / AdvanceY)), MaxBoxY - 1));
}
int TEdgeManager::increment_box_x(int x)
{
return std::min(x + 1, MaxBoxX - 1);
}
int TEdgeManager::increment_box_y(int y)
{
return std::min(y + 1, MaxBoxY - 1);
}
void TEdgeManager::add_edge_to_box(int x, int y, TEdgeSegment* edge)
{
assertm((unsigned)x < (unsigned)MaxBoxX && (unsigned)y < (unsigned)MaxBoxY, "Box coordinates out of range");
auto& list = BoxArray[x + y * MaxBoxX].EdgeList;
assertm(std::find(list.begin(), list.end(), edge) == list.end(), "Duplicate inserted into box");
list.push_back(edge);
}
void TEdgeManager::add_field_to_box(int x, int y, field_effect_type* field)
{
assertm((unsigned)x < (unsigned)MaxBoxX && (unsigned)y < (unsigned)MaxBoxY, "Box coordinates out of range");
auto& list = BoxArray[x + y * MaxBoxX].FieldList;
assertm(std::find(list.begin(), list.end(), field) == list.end(), "Duplicate inserted into box");
list.push_back(field);
}
int TEdgeManager::TestGridBox(int x, int y, float* distPtr, TEdgeSegment** edgeDst, ray_type* ray, TBall* ball,
int edgeIndex)
{
if (x >= 0 && x < MaxBoxX && y >= 0 && y < MaxBoxY)
{
TEdgeBox* edgeBox = &BoxArray[x + y * MaxBoxX];
TEdgeSegment** edgePtr = &EdgeArray[edgeIndex];
for (auto it = edgeBox->EdgeList.rbegin(); it != edgeBox->EdgeList.rend(); ++it)
{
auto edge = *it;
if (!edge->ProcessedFlag && *edge->ActiveFlagPtr && (edge->CollisionGroup & ray->CollisionMask) != 0)
{
if (!ball->already_hit(*edge))
{
++edgeIndex;
*edgePtr = edge;
++edgePtr;
edge->ProcessedFlag = 1;
auto dist = edge->FindCollisionDistance(*ray);
if (dist < *distPtr)
{
*distPtr = dist;
*edgeDst = edge;
}
}
}
}
}
return edgeIndex;
}
void TEdgeManager::FieldEffects(TBall* ball, vector2* dstVec)
{
vector2 vec{};
TEdgeBox* edgeBox = &BoxArray[box_x(ball->Position.X) + box_y(ball->Position.Y) *
MaxBoxX];
for (auto it = edgeBox->FieldList.rbegin(); it != edgeBox->FieldList.rend(); ++it)
{
auto field = *it;
if (*field->ActiveFlag && ball->CollisionMask & field->CollisionGroup)
{
if (field->CollisionComp->FieldEffect(ball, &vec))
{
maths::vector_add(*dstVec, vec);
}
}
}
}
float TEdgeManager::FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegment** edge)
{
auto distance = 1000000000.0f;
auto edgeIndex = 0;
auto x0 = ray->Origin.X;
auto y0 = ray->Origin.Y;
auto x1 = ray->Direction.X * ray->MaxDistance + ray->Origin.X;
auto y1 = ray->Direction.Y * ray->MaxDistance + ray->Origin.Y;
auto xBox0 = box_x(x0);
auto yBox0 = box_y(y0);
auto xBox1 = box_x(x1);
auto yBox1 = box_y(y1);
auto dirX = x0 >= x1 ? -1 : 1;
auto dirY = y0 >= y1 ? -1 : 1;
if (yBox0 == yBox1)
{
if (dirX == 1)
{
for (auto indexX = xBox0; indexX <= xBox1; indexX++)
{
edgeIndex = TestGridBox(indexX, yBox0, &distance, edge, ray, ball, edgeIndex);
}
}
else
{
for (auto indexX = xBox0; indexX >= xBox1; indexX--)
{
edgeIndex = TestGridBox(indexX, yBox0, &distance, edge, ray, ball, edgeIndex);
}
}
}
else if (xBox0 == xBox1)
{
if (dirY == 1)
{
for (auto indexY = yBox0; indexY <= yBox1; indexY++)
{
edgeIndex = TestGridBox(xBox0, indexY, &distance, edge, ray, ball, edgeIndex);
}
}
else
{
for (auto indexY = yBox0; indexY >= yBox1; indexY--)
{
edgeIndex = TestGridBox(xBox0, indexY, &distance, edge, ray, ball, edgeIndex);
}
}
}
else
{
edgeIndex = TestGridBox(xBox0, yBox0, &distance, edge, ray, ball, 0);
// Bresenham line formula: y = dYdX * (x - x0) + y0; dYdX = (y0 - y1) / (x0 - x1)
auto dyDx = (y0 - y1) / (x0 - x1);
// Precompute constant part: dYdX * (-x0) + y0
auto precomp = -x0 * dyDx + y0;
// X and Y indexes are offset by one when going forwards, not sure why
auto xBias = dirX == 1 ? 1 : 0, yBias = dirY == 1 ? 1 : 0;
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
{
// Calculate y from indexY and from line formula
auto yDiscrete = (indexY + yBias) * AdvanceY + MinY;
auto ylinear = ((indexX + xBias) * AdvanceX + MinX) * dyDx + precomp;
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
{
// Advance indexY when discrete value is ahead/behind
// Advance indexX when discrete value matches linear value
indexY += dirY;
if (ylinear == yDiscrete)
indexX += dirX;
}
else
{
// Advance indexX otherwise
indexX += dirX;
}
edgeIndex = TestGridBox(indexX, indexY, &distance, edge, ray, ball, edgeIndex);
}
}
for (auto edgePtr = EdgeArray; edgeIndex > 0; --edgeIndex, ++edgePtr)
{
(*edgePtr)->ProcessedFlag = 0;
}
return distance;
}
vector2 TEdgeManager::NormalizeBox(vector2 pt) const
{
// Standard PB Box ranges: X [-8, 8]; Y [-14, 15]; Top right corner: (-8, -14)
// Bring them to: X [0, 16]; Y [0, 29]; Top right corner: (0, 0)
auto x = Clamp(pt.X, MinX, MaxX) + abs(MinX);
auto y = Clamp(pt.Y, MinY, MaxY) + abs(MinY);
// Normalize and invert to: X [0, 1]; Y [0, 1]; Top right corner: (1, 1)
x /= Width; y /= Height;
return vector2{ 1 - x, 1 - y };
}
vector2 TEdgeManager::DeNormalizeBox(vector2 pt) const
{
// Undo normalization by applying steps in reverse
auto x = (1 - pt.X) * Width - abs(MinX);
auto y = (1 - pt.Y) * Height - abs(MinY);
return vector2{ x, y };
}

View File

@ -0,0 +1,43 @@
#pragma once
#include "TCollisionComponent.h"
struct ray_type;
class TEdgeBox;
struct field_effect_type
{
char* ActiveFlag;
int CollisionGroup;
TCollisionComponent* CollisionComp;
};
class TEdgeManager
{
public:
TEdgeManager(float xMin, float yMin, float width, float height);
~TEdgeManager();
void FieldEffects(TBall* ball, struct vector2* dstVec);
int box_x(float x);
int box_y(float y);
int increment_box_x(int x);
int increment_box_y(int y);
void add_edge_to_box(int x, int y, TEdgeSegment* edge);
void add_field_to_box(int x, int y, field_effect_type* field);
int TestGridBox(int x, int y, float* distPtr, TEdgeSegment** edgeDst, ray_type* ray, TBall* ball, int edgeIndex);
float FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegment** edge);
vector2 NormalizeBox(vector2 pt) const;
vector2 DeNormalizeBox(vector2 pt) const;
float AdvanceX;
float AdvanceY;
int MaxBoxX;
int MaxBoxY;
float MinX;
float MinY;
float MaxX;
float MaxY;
float Width;
float Height;
TEdgeBox* BoxArray;
TEdgeSegment* EdgeArray[1000]{};
};

View File

@ -0,0 +1,116 @@
#include "pch.h"
#include "TEdgeSegment.h"
#include "TCircle.h"
#include "TCollisionComponent.h"
#include "TLine.h"
TEdgeSegment::TEdgeSegment(TCollisionComponent* collComp, char* activeFlag, unsigned collisionGroup)
{
CollisionComponent = collComp;
ActiveFlagPtr = activeFlag;
CollisionGroup = collisionGroup;
ProcessedFlag = 0;
}
void TEdgeSegment::port_draw()
{
}
TEdgeSegment* TEdgeSegment::install_wall(float* floatArr, TCollisionComponent* collComp, char* activeFlagPtr,
unsigned int collisionGroup, float offset, size_t wallValue)
{
vector2 center{}, start{}, end{}, prevCenter{};
vector3 vec1{}, vec2{}, dstVec{};
TEdgeSegment* edge = nullptr;
wall_type wallType = static_cast<wall_type>(static_cast<int>(floor(*floatArr) - 1.0f));
switch (wallType)
{
case wall_type::Circle:
{
center.X = floatArr[1];
center.Y = floatArr[2];
auto radius = offset + floatArr[3];
auto circle = new TCircle(collComp, activeFlagPtr, collisionGroup, &center, radius);
circle->WallValue = reinterpret_cast<void*>(wallValue);
circle->place_in_grid(&collComp->AABB);
collComp->EdgeList.push_back(circle);
edge = circle;
break;
}
case wall_type::Line:
{
start.X = floatArr[1];
start.Y = floatArr[2];
end.X = floatArr[3];
end.Y = floatArr[4];
auto line = new TLine(collComp, activeFlagPtr, collisionGroup, start, end);
line->WallValue = reinterpret_cast<void*>(wallValue);
line->Offset(offset);
line->place_in_grid(&collComp->AABB);
collComp->EdgeList.push_back(line);
edge = line;
break;
}
default:
{
int wallTypeI = static_cast<int>(wallType);
auto floatArrPtr = floatArr + 1;
prevCenter.X = floatArr[2 * wallTypeI - 1];
prevCenter.Y = floatArr[2 * wallTypeI];
for (int index = 0; index < wallTypeI; index++, floatArrPtr += 2)
{
float centerX2, centerY2;
if (index >= wallTypeI - 1)
{
centerX2 = floatArr[1];
centerY2 = floatArr[2];
}
else
{
centerX2 = floatArrPtr[2];
centerY2 = floatArrPtr[3];
}
auto centerX1 = floatArrPtr[0];
auto centerY1 = floatArrPtr[1];
center.X = centerX1;
center.Y = centerY1;
if (offset != 0.0f)
{
vec1.X = centerX1 - prevCenter.X;
vec1.Y = center.Y - prevCenter.Y;
vec2.X = centerX2 - centerX1;
vec2.Y = centerY2 - center.Y;
maths::cross(vec1, vec2, dstVec);
if ((dstVec.Z > 0.0f && offset > 0.0f) ||
(dstVec.Z < 0.0f && offset < 0.0f))
{
float radius = offset * 1.001f;
auto circle = new TCircle(collComp, activeFlagPtr, collisionGroup, &center, radius);
circle->WallValue = reinterpret_cast<void*>(wallValue);
circle->place_in_grid(&collComp->AABB);
collComp->EdgeList.push_back(circle);
}
}
start.X = floatArrPtr[0];
start.Y = floatArrPtr[1];
end.X = floatArrPtr[2];
end.Y = floatArrPtr[3];
auto line = new TLine(collComp, activeFlagPtr, collisionGroup, start, end);
line->WallValue = reinterpret_cast<void*>(wallValue);
line->Offset(offset);
line->place_in_grid(&collComp->AABB);
collComp->EdgeList.push_back(line);
edge = line;
prevCenter = center;
}
}
}
return edge;
}

View File

@ -0,0 +1,33 @@
#pragma once
class TBall;
class TCollisionComponent;
struct ray_type;
struct RectF;
enum class wall_type : int
{
Circle = 0,
Line = 1,
};
class TEdgeSegment
{
public:
TCollisionComponent* CollisionComponent;
char* ActiveFlagPtr;
char ProcessedFlag;
void* WallValue{};
unsigned int CollisionGroup;
TEdgeSegment(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup);
virtual ~TEdgeSegment() = default;
virtual void EdgeCollision(TBall* ball, float distance) = 0;
virtual void port_draw();
virtual void place_in_grid(RectF* aabb) = 0;
virtual float FindCollisionDistance(const ray_type& ray) = 0;
static TEdgeSegment* install_wall(float* floatArr, TCollisionComponent* collComp, char* activeFlagPtr,
unsigned int collisionGroup, float offset, size_t wallValue);
};

View File

@ -0,0 +1,120 @@
#include "pch.h"
#include "TFlagSpinner.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "TBall.h"
#include "timer.h"
#include "TLine.h"
#include "TPinballTable.h"
TFlagSpinner::TFlagSpinner(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
vector2 end{}, start{};
Timer = 0;
loader::query_visual(groupIndex, 0, &visual);
end.X = visual.FloatArr[0];
end.Y = visual.FloatArr[1];
start.X = visual.FloatArr[2];
start.Y = visual.FloatArr[3];
auto line = new TLine(this, &ActiveFlag, visual.CollisionGroup, start, end);
if (line)
{
line->place_in_grid(&AABB);
EdgeList.push_back(line);
}
line = new TLine(this, &ActiveFlag, visual.CollisionGroup, end, start);
PrevCollider = line;
if (line)
{
line->place_in_grid(&AABB);
EdgeList.push_back(line);
}
SpeedDecrement = 0.64999998f;
MaxSpeed = 50000.0f;
MinSpeed = 5.0f;
auto speedDec = loader::query_float_attribute(groupIndex, 0, 1202);
if (speedDec)
SpeedDecrement = *speedDec;
auto maxSpeed = loader::query_float_attribute(groupIndex, 0, 1200);
if (maxSpeed)
MaxSpeed = *maxSpeed;
auto minSpeed = loader::query_float_attribute(groupIndex, 0, 1201);
if (minSpeed)
MinSpeed = *minSpeed;
}
int TFlagSpinner::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset)
{
if (Timer)
{
timer::kill(Timer);
Timer = 0;
}
BmpIndex = 0;
SpriteSet(BmpIndex);
}
return 0;
}
void TFlagSpinner::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge)
{
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance = ball->RayMaxDistance - distance;
ball->not_again(edge);
SpinDirection = 2 * (PrevCollider != edge) - 1;
if (ball->Speed == 0.0f)
Speed = MinSpeed;
else
Speed = ball->Speed * 20.0f;
if (Speed < MinSpeed)
Speed = MinSpeed;
if (Speed > MaxSpeed)
Speed = MaxSpeed;
NextFrame();
}
void TFlagSpinner::NextFrame()
{
BmpIndex += SpinDirection;
int bmpIndex = BmpIndex;
int bmpCount = static_cast<int>(ListBitmap->size());
if (bmpIndex >= bmpCount)
BmpIndex = 0;
else if (bmpIndex < 0)
BmpIndex = bmpCount - 1;
if (!PinballTable->TiltLockFlag)
{
control::handler(MessageCode::ControlCollision, this);
if (SoftHitSoundId)
loader::play_sound(SoftHitSoundId, this, "TFlagSpinner");
if (!BmpIndex)
control::handler(MessageCode::ControlSpinnerLoopReset, this);
}
SpriteSet(BmpIndex);
Speed *= SpeedDecrement;
if (Speed >= MinSpeed)
{
timer::set(1.0f / Speed, this, SpinTimer);
}
}
void TFlagSpinner::SpinTimer(int timerId, void* caller)
{
auto spinner = static_cast<TFlagSpinner*>(caller);
spinner->Timer = 0;
spinner->NextFrame();
}

View File

@ -0,0 +1,25 @@
#pragma once
#include "TCollisionComponent.h"
class TFlagSpinner :
public TCollisionComponent
{
public:
TFlagSpinner(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
void NextFrame();
static void SpinTimer(int timerId, void* caller);
float Speed{};
float MaxSpeed;
float MinSpeed;
float SpeedDecrement;
int SpinDirection{};
int BmpIndex{};
int Timer;
TEdgeSegment* PrevCollider;
};

View File

@ -0,0 +1,203 @@
#include "pch.h"
#include "TFlipper.h"
#include "control.h"
#include "loader.h"
#include "pb.h"
#include "render.h"
#include "TBall.h"
#include "TFlipperEdge.h"
#include "timer.h"
#include "TPinballTable.h"
TFlipper::TFlipper(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
loader::query_visual(groupIndex, 0, &visual);
HardHitSoundId = visual.SoundIndex4;
SoftHitSoundId = visual.SoundIndex3;
Elasticity = visual.Elasticity;
Smoothness = visual.Smoothness;
auto collMult = *loader::query_float_attribute(groupIndex, 0, 803);
auto retractTime = *loader::query_float_attribute(groupIndex, 0, 805);
auto extendTime = *loader::query_float_attribute(groupIndex, 0, 804);
auto vecT2 = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 802));
auto vecT1 = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 801));
auto origin = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 800));
auto flipperEdge = new TFlipperEdge(
this,
&ActiveFlag,
visual.CollisionGroup,
table,
origin,
vecT1,
vecT2,
extendTime,
retractTime,
collMult,
Elasticity,
Smoothness);
flipperEdge->place_in_grid(&AABB);
FlipperEdge = flipperEdge;
BmpIndex = 0;
if (table)
table->FlipperList.push_back(this);
}
TFlipper::~TFlipper()
{
delete FlipperEdge;
if (PinballTable)
{
auto& flippers = PinballTable->FlipperList;
auto position = std::find(flippers.begin(), flippers.end(), this);
if (position != flippers.end())
flippers.erase(position);
}
}
int TFlipper::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TFlipperExtend:
case MessageCode::TFlipperRetract:
case MessageCode::Resume:
case MessageCode::LooseFocus:
case MessageCode::SetTiltLock:
case MessageCode::GameOver:
if (code == MessageCode::TFlipperExtend)
{
control::handler(MessageCode::TFlipperExtend, this);
loader::play_sound(HardHitSoundId, this, "TFlipper1");
}
else if (code == MessageCode::TFlipperRetract)
{
loader::play_sound(SoftHitSoundId, this, "TFlipper2");
}
else
{
// Retract for all non-input messages
code = MessageCode::TFlipperRetract;
}
MessageField = FlipperEdge->SetMotion(code);
break;
case MessageCode::PlayerChanged:
case MessageCode::Reset:
if (MessageField)
{
FlipperEdge->CurrentAngle = 0;
FlipperEdge->set_control_points(0);
MessageField = 0;
FlipperEdge->SetMotion(MessageCode::Reset);
UpdateSprite();
}
break;
default: break;
}
return 0;
}
void TFlipper::port_draw()
{
FlipperEdge->port_draw();
}
void TFlipper::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
}
void TFlipper::UpdateSprite()
{
auto bmpCountSub1 = static_cast<int>(ListBitmap->size()) - 1;
auto newBmpIndex = static_cast<int>(floor(FlipperEdge->CurrentAngle / FlipperEdge->AngleMax * bmpCountSub1 + 0.5f));
newBmpIndex = Clamp(newBmpIndex, 0, bmpCountSub1);
if (BmpIndex == newBmpIndex)
return;
BmpIndex = newBmpIndex;
SpriteSet(BmpIndex);
}
int TFlipper::GetFlipperStepAngle(float dt, float* dst) const
{
if (!MessageField)
return 0;
auto deltaAngle = FlipperEdge->flipper_angle_delta(dt);
auto step = std::fabs(std::ceil(FlipperEdge->DistanceDiv * deltaAngle * FlipperEdge->InvT1Radius));
if (step > 3.0f)
step = 3.0f;
if (step >= 2.0f)
{
*dst = deltaAngle / step;
return static_cast<int>(step);
}
*dst = deltaAngle;
return 1;
}
void TFlipper::FlipperCollision(float deltaAngle)
{
if (!MessageField)
return;
ray_type ray{}, rayDst{};
ray.MinDistance = 0.002f;
bool collisionFlag = false;
for (auto ball : pb::MainTable->BallList)
{
if ((FlipperEdge->CollisionGroup & ball->CollisionMask) != 0 &&
FlipperEdge->YMax >= ball->Position.Y && FlipperEdge->YMin <= ball->Position.Y &&
FlipperEdge->XMax >= ball->Position.X && FlipperEdge->XMin <= ball->Position.X)
{
if (FlipperEdge->ControlPointDirtyFlag)
FlipperEdge->set_control_points(FlipperEdge->CurrentAngle);
ray.CollisionMask = ball->CollisionMask;
ray.Origin = ball->Position;
float sin, cos;
auto ballPosRot = ray.Origin;
maths::SinCos(-deltaAngle, sin, cos);
maths::RotatePt(ballPosRot, sin, cos, FlipperEdge->RotOrigin);
ray.Direction.X = ballPosRot.X - ray.Origin.X;
ray.Direction.Y = ballPosRot.Y - ray.Origin.Y;
ray.MaxDistance = maths::normalize_2d(ray.Direction);
auto distance = maths::distance_to_flipper(FlipperEdge, ray, rayDst);
if (distance < 1e9f)
{
FlipperEdge->NextBallPosition = ball->Position;
FlipperEdge->CollisionDirection = rayDst.Direction;
FlipperEdge->EdgeCollision(ball, distance);
collisionFlag = true;
}
}
}
if (collisionFlag)
{
auto angleAdvance = deltaAngle / (std::fabs(FlipperEdge->MoveSpeed) * 5.0f);
FlipperEdge->CurrentAngle -= angleAdvance;
FlipperEdge->AngleRemainder += std::fabs(angleAdvance);
}
else
{
FlipperEdge->CurrentAngle += deltaAngle;
FlipperEdge->AngleRemainder -= std::fabs(deltaAngle);
}
if (FlipperEdge->AngleRemainder <= 0.0001f)
{
FlipperEdge->CurrentAngle = FlipperEdge->AngleDst;
FlipperEdge->FlipperFlag = MessageCode::TFlipperNull;
MessageField = 0;
}
FlipperEdge->ControlPointDirtyFlag = true;
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "TCollisionComponent.h"
class TFlipperEdge;
class TFlipper :
public TCollisionComponent
{
public:
TFlipper(TPinballTable* table, int groupIndex);
~TFlipper() override;
int Message(MessageCode code, float value) override;
void port_draw() override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
void UpdateSprite();
int GetFlipperStepAngle(float dt, float* dst) const;
void FlipperCollision(float deltaAngle);
int BmpIndex;
TFlipperEdge* FlipperEdge;
};

View File

@ -0,0 +1,273 @@
#include "pch.h"
#include "TFlipperEdge.h"
#include "pb.h"
#include "TLine.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TFlipperEdge::TFlipperEdge(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup,
TPinballTable* table,
vector3* origin, vector3* vecT1, vector3* vecT2, float extendSpeed, float retractSpeed,
float collMult, float elasticity, float smoothness): TEdgeSegment(
collComp, activeFlag, collisionGroup)
{
vector3 crossProd{}, vecOriginT1{}, vecOriginT2{};
Elasticity = elasticity;
Smoothness = smoothness;
CollisionMult = collMult;
T1Src = static_cast<vector2>(*vecT1);
T2Src = static_cast<vector2>(*vecT2);
RotOrigin.X = origin->X;
RotOrigin.Y = origin->Y;
CirclebaseRadius = origin->Z + table->CollisionCompOffset;
CirclebaseRadiusMSq = CirclebaseRadius * 1.01f * (CirclebaseRadius * 1.01f);
CirclebaseRadiusSq = CirclebaseRadius * CirclebaseRadius;
CircleT1Radius = vecT1->Z + table->CollisionCompOffset;
CircleT1RadiusMSq = CircleT1Radius * 1.01f * (CircleT1Radius * 1.01f);
CircleT1RadiusSq = CircleT1Radius * CircleT1Radius;
vecOriginT1.X = vecT1->X - origin->X;
vecOriginT1.Y = vecT1->Y - origin->Y;
vecOriginT1.Z = 0.0;
maths::normalize_2d(vecOriginT1);
vecOriginT2.X = vecT2->X - origin->X;
vecOriginT2.Y = vecT2->Y - origin->Y;
vecOriginT2.Z = 0.0;
maths::normalize_2d(vecOriginT2);
AngleMax = acos(maths::DotProduct(vecOriginT1, vecOriginT2));
maths::cross(vecOriginT1, vecOriginT2, crossProd);
if (crossProd.Z < 0.0f)
AngleMax = -AngleMax;
FlipperFlag = MessageCode::TFlipperNull;
AngleDst = 0.0;
// 3DPB and FT have different formats for flipper speed:
// 3DPB: Time it takes for flipper to go from source to destination, in sec.
// FT: Flipper movement speed, in radians per sec.
if (!pb::FullTiltMode)
{
auto angleMax = std::abs(AngleMax);
retractSpeed = angleMax / retractSpeed;
extendSpeed = angleMax / extendSpeed;
}
ExtendSpeed = extendSpeed;
RetractSpeed = retractSpeed;
const vector2 perpOriginT1Cc = {-vecOriginT1.Y, vecOriginT1.X};
A2Src.X = perpOriginT1Cc.X * CirclebaseRadius + origin->X;
A2Src.Y = perpOriginT1Cc.Y * CirclebaseRadius + origin->Y;
A1Src.X = perpOriginT1Cc.X * CircleT1Radius + vecT1->X;
A1Src.Y = perpOriginT1Cc.Y * CircleT1Radius + vecT1->Y;
const vector2 perpOriginT1C = {vecOriginT1.Y, -vecOriginT1.X};
B1Src.X = perpOriginT1C.X * CirclebaseRadius + origin->X;
B1Src.Y = perpOriginT1C.Y * CirclebaseRadius + origin->Y;
B2Src.X = perpOriginT1C.X * CircleT1Radius + vecT1->X;
B2Src.Y = perpOriginT1C.Y * CircleT1Radius + vecT1->Y;
if (AngleMax < 0.0f)
{
std::swap(A1Src, B1Src);
std::swap(A2Src, B2Src);
}
auto dx = vecT1->X - RotOrigin.X;
auto dy = vecT1->Y - RotOrigin.Y;
auto distance1 = sqrt(dy * dy + dx * dx) + table->CollisionCompOffset + vecT1->Z;
DistanceDiv = distance1;
DistanceDivSq = distance1 * distance1;
InvT1Radius = 1.0f / CircleT1Radius * 1.5f;
if (AngleMax <= 0.0f)
{
ExtendSpeed = -ExtendSpeed;
}
else
{
RetractSpeed = -RetractSpeed;
}
set_control_points(CurrentAngle);
}
void TFlipperEdge::port_draw()
{
set_control_points(CurrentAngle);
}
float TFlipperEdge::FindCollisionDistance(const ray_type& ray)
{
ray_type dstRay{};
if (ControlPointDirtyFlag)
set_control_points(CurrentAngle);
auto distance = maths::distance_to_flipper(this, ray, dstRay);
if (distance >= 1e9f)
return 1e9f;
NextBallPosition = dstRay.Origin;
CollisionDirection = dstRay.Direction;
return distance;
}
void TFlipperEdge::EdgeCollision(TBall* ball, float distance)
{
if (FlipperFlag == MessageCode::TFlipperNull)
{
maths::basic_collision(
ball,
&NextBallPosition,
&CollisionDirection,
Elasticity,
Smoothness,
1e9f,
0);
return;
}
vector2 t1NextPos{NextBallPosition.X - T1.X, NextBallPosition.Y - T1.Y},
t1RotOrigin{RotOrigin.X - T1.X, RotOrigin.Y - T1.Y};
auto crossProduct = maths::cross(t1RotOrigin, t1NextPos);
bool frontCollision = false;
if (crossProduct <= 0)
{
if (AngleMax > 0)
frontCollision = true;
}
else if (AngleMax <= 0)
{
frontCollision = true;
}
if (FlipperFlag == MessageCode::TFlipperRetract)
{
frontCollision ^= true;
CollisionLinePerp = LineB.PerpendicularC;
}
else
{
CollisionLinePerp = LineA.PerpendicularC;
}
auto dx = NextBallPosition.X - RotOrigin.X;
auto dy = NextBallPosition.Y - RotOrigin.Y;
auto distanceSq = dy * dy + dx * dx;
if (frontCollision)
{
float boost = 0;
if (circlebase.RadiusSq * 1.01f < distanceSq)
{
auto v21 = std::fabs(MoveSpeed) * std::sqrt(distanceSq / DistanceDivSq);
auto dot1 = maths::DotProduct(CollisionLinePerp, CollisionDirection);
if (dot1 >= 0)
boost = CollisionMult * dot1 * v21;
}
auto threshold = boost <= 0.0f ? 1e9f : -1.0f;
maths::basic_collision(
ball,
&NextBallPosition,
&CollisionDirection,
Elasticity,
Smoothness,
threshold,
boost);
}
else
{
auto elasticity = Elasticity;
if (circlebase.RadiusSq * 1.01f < distanceSq)
elasticity = (1.0f - std::sqrt(distanceSq / DistanceDivSq)) * Elasticity;
maths::basic_collision(ball, &NextBallPosition, &CollisionDirection, elasticity, Smoothness, 1e9f, 0.0);
}
}
void TFlipperEdge::place_in_grid(RectF* aabb)
{
auto xMax = std::max(std::max(T2Src.X + CircleT1Radius, T1Src.X + CircleT1Radius), RotOrigin.X + CirclebaseRadius);
auto yMax = std::max(std::max(T2Src.Y + CircleT1Radius, T1Src.Y + CircleT1Radius), RotOrigin.Y + CirclebaseRadius);
auto xMin = std::min(std::min(T2Src.X - CircleT1Radius, T1Src.X - CircleT1Radius), RotOrigin.X - CirclebaseRadius);
auto yMin = std::min(std::min(T2Src.Y - CircleT1Radius, T1Src.Y - CircleT1Radius), RotOrigin.Y - CirclebaseRadius);
if (aabb)
{
aabb->Merge({xMax, yMax, xMin, yMin});
}
TTableLayer::edges_insert_square(yMin, xMin, yMax, xMax, this, nullptr);
auto offset = 1.0f / InvT1Radius + pb::BallHalfRadius;
XMin = xMin - offset;
YMin = yMin - offset;
XMax = xMax + offset;
YMax = yMax + offset;
}
void TFlipperEdge::set_control_points(float angle)
{
float sin, cos;
maths::SinCos(angle, sin, cos);
A1 = A1Src;
A2 = A2Src;
B1 = B1Src;
B2 = B2Src;
T1 = T1Src;
maths::RotatePt(A1, sin, cos, RotOrigin);
maths::RotatePt(A2, sin, cos, RotOrigin);
maths::RotatePt(T1, sin, cos, RotOrigin);
maths::RotatePt(B1, sin, cos, RotOrigin);
maths::RotatePt(B2, sin, cos, RotOrigin);
maths::line_init(LineA, A1.X, A1.Y, A2.X, A2.Y);
maths::line_init(LineB, B1.X, B1.Y, B2.X, B2.Y);
circlebase = {RotOrigin, CirclebaseRadiusSq};
circleT1 = {T1, CircleT1RadiusSq};
ControlPointDirtyFlag = false;
}
float TFlipperEdge::flipper_angle_delta(float timeDelta)
{
if (FlipperFlag == MessageCode::TFlipperNull)
return 0.0f;
const auto deltaAngle = MoveSpeed * timeDelta;
if (std::fabs(deltaAngle) > AngleRemainder)
return AngleDst - CurrentAngle;
return deltaAngle;
}
int TFlipperEdge::SetMotion(MessageCode code)
{
switch (code)
{
case MessageCode::TFlipperExtend:
AngleRemainder = std::fabs(AngleMax - CurrentAngle);
AngleDst = AngleMax;
MoveSpeed = ExtendSpeed;
break;
case MessageCode::TFlipperRetract:
AngleRemainder = std::fabs(CurrentAngle);
AngleDst = 0.0f;
MoveSpeed = RetractSpeed;
break;
case MessageCode::Reset:
AngleRemainder = 0.0f;
AngleDst = 0.0f;
break;
default: break;
}
if (AngleRemainder == 0.0f)
code = MessageCode::TFlipperNull;
FlipperFlag = code;
return static_cast<int>(code);
}

View File

@ -0,0 +1,56 @@
#pragma once
#include "maths.h"
#include "TEdgeSegment.h"
#include "TPinballComponent.h"
class TPinballTable;
class TFlipperEdge : public TEdgeSegment
{
public:
TFlipperEdge(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup, TPinballTable* table,
vector3* origin, vector3* vecT1, vector3* vecT2, float extendSpeed, float retractSpeed, float collMult,
float elasticity, float smoothness);
void port_draw() override;
float FindCollisionDistance(const ray_type& ray) override;
void EdgeCollision(TBall* ball, float distance) override;
void place_in_grid(RectF* aabb) override;
void set_control_points(float angle);
float flipper_angle_delta(float timeDelta);
int SetMotion(MessageCode code);
MessageCode FlipperFlag{};
float Elasticity;
float Smoothness;
vector2 RotOrigin{};
float CirclebaseRadius;
float CircleT1Radius;
float CirclebaseRadiusSq;
float CircleT1RadiusSq;
float CirclebaseRadiusMSq;
float CircleT1RadiusMSq;
float AngleMax;
float AngleRemainder{};
float AngleDst;
float CurrentAngle{};
vector2 CollisionLinePerp{};
vector2 A1Src{};
vector2 A2Src{};
vector2 B1Src{};
vector2 B2Src{};
float CollisionMult;
vector2 T1Src{};
vector2 T2Src{};
float DistanceDiv, DistanceDivSq;
vector2 CollisionDirection{};
float ExtendSpeed;
float RetractSpeed;
float MoveSpeed;
vector2 NextBallPosition{};
vector2 A1, A2, B1, B2, T1;
line_type LineA, LineB;
circle_type circlebase, circleT1;
float InvT1Radius;
float YMin, YMax, XMin, XMax;
bool ControlPointDirtyFlag{};
};

View File

@ -0,0 +1,42 @@
#include "pch.h"
#include "TGate.h"
#include "control.h"
#include "loader.h"
#include "render.h"
TGate::TGate(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
visualStruct visual{};
loader::query_visual(groupIndex, 0, &visual);
SoundIndex4 = visual.SoundIndex4;
SoundIndex3 = visual.SoundIndex3;
ActiveFlag = 1;
SpriteSet(0);
control::handler(MessageCode::Reset, this);
}
int TGate::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TGateDisable:
ActiveFlag = 0;
SpriteSet(-1);
loader::play_sound(SoundIndex3, this, "TGate1");
break;
case MessageCode::Reset:
case MessageCode::TGateEnable:
ActiveFlag = 1;
SpriteSet(0);
if (code == MessageCode::TGateEnable)
loader::play_sound(SoundIndex4, this, "TGate2");
break;
default: break;
}
control::handler(code, this);
return 0;
}

13
SpaceCadetPinball/TGate.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "TCollisionComponent.h"
class TGate :
public TCollisionComponent
{
public:
TGate(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
int SoundIndex4;
int SoundIndex3;
};

148
SpaceCadetPinball/THole.cpp Normal file
View File

@ -0,0 +1,148 @@
#include "pch.h"
#include "THole.h"
#include "control.h"
#include "loader.h"
#include "pb.h"
#include "TBall.h"
#include "timer.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
THole::THole(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
circle_type circle{};
Unknown4 = 0.050000001f;
MessageField = 0;
Timer = 0;
BallCapturedFlag = false;
Unknown3 = loader::query_float_attribute(groupIndex, 0, 407, 0.25f);
GravityMult = loader::query_float_attribute(groupIndex, 0, 701, 0.2f);
GravityPull = *loader::query_float_attribute(groupIndex, 0, 305);
loader::query_visual(groupIndex, 0, &visual);
Circle.Center.X = visual.FloatArr[0];
Circle.Center.Y = visual.FloatArr[1];
Circle.RadiusSq = *loader::query_float_attribute(groupIndex, 0, 306) * visual.FloatArr[2];
if (Circle.RadiusSq == 0.0f)
Circle.RadiusSq = 0.001f;
auto tCircle = new TCircle(this, &ActiveFlag, visual.CollisionGroup,
reinterpret_cast<vector3*>(visual.FloatArr),
Circle.RadiusSq);
tCircle->place_in_grid(&AABB);
EdgeList.push_back(tCircle);
ZSetValue = loader::query_float_attribute(groupIndex, 0, 408)[2];
CollisionMask = static_cast<int>(floor(*loader::query_float_attribute(groupIndex, 0, 1304)));
// CollisionMask difference: 3DPB - value given as is, FT: mask = 1 << value
if (pb::FullTiltMode)
CollisionMask = 1 << CollisionMask;
Circle.RadiusSq = visual.FloatArr[2] * visual.FloatArr[2];
circle.RadiusSq = Circle.RadiusSq;
circle.Center.X = Circle.Center.X;
circle.Center.Y = Circle.Center.Y;
Field.ActiveFlag = &ActiveFlag;
Field.CollisionComp = this;
Field.CollisionGroup = visual.CollisionGroup;
TTableLayer::edges_insert_circle(&circle, nullptr, &Field);
}
int THole::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset && BallCapturedFlag)
{
if (Timer)
timer::kill(Timer);
Timer = 0;
BallCapturedSecondStage = true;
}
return 0;
}
void THole::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (!BallCapturedFlag)
{
BallCapturedSecondStage = false;
Threshold = 1e9f;
BallCapturedFlag = true;
ball->CollisionComp = this;
ball->Position.X = Circle.Center.X;
ball->Position.Y = Circle.Center.Y;
ball->Direction.Z = 0.0;
ball->CollisionDisabledFlag = true;
// Ramp hole has no delay in FT.
auto captureTime = pb::FullTiltMode ? 0 : 0.5f;
Timer = timer::set(captureTime, this, TimerExpired);
if (!PinballTable->TiltLockFlag)
{
loader::play_sound(HardHitSoundId, ball, "THole1");
control::handler(MessageCode::ControlBallCaptured, this);
}
}
else
{
DefaultCollision(ball, nextPosition, direction);
}
}
int THole::FieldEffect(TBall* ball, vector2* vecDst)
{
int result;
vector2 direction{};
if (BallCapturedFlag)
{
if (BallCapturedSecondStage)
{
ball->Direction.Z -= PinballTable->GravityDirVectMult * ball->TimeDelta * GravityMult;
ball->Position.Z += ball->Direction.Z;
if (ball->Position.Z <= ZSetValue)
{
BallCapturedFlag = false;
BallCapturedSecondStage = false;
ball->Position.Z = ZSetValue;
ball->CollisionMask = CollisionMask;
ball->CollisionComp = nullptr;
ball->Direction = {};
ball->Speed = 0.0f;
loader::play_sound(SoftHitSoundId, ball, "THole2");
control::handler(MessageCode::ControlBallReleased, this);
}
}
result = 0;
}
else
{
direction.X = Circle.Center.X - ball->Position.X;
direction.Y = Circle.Center.Y - ball->Position.Y;
if (direction.X * direction.X + direction.Y * direction.Y <= Circle.RadiusSq)
{
maths::normalize_2d(direction);
vecDst->X = direction.X * GravityPull - ball->Direction.X * ball->Speed;
vecDst->Y = direction.Y * GravityPull - ball->Direction.Y * ball->Speed;
result = 1;
}
else
{
result = 0;
}
}
return result;
}
void THole::TimerExpired(int timerId, void* caller)
{
auto hole = static_cast<THole*>(caller);
hole->Timer = 0;
hole->BallCapturedSecondStage = true;
}

29
SpaceCadetPinball/THole.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "TCircle.h"
#include "TCollisionComponent.h"
#include "TEdgeManager.h"
class THole :
public TCollisionComponent
{
public:
THole(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
int FieldEffect(TBall* ball, vector2* vecDst) override;
static void TimerExpired(int timerId, void* caller);
bool BallCapturedFlag{};
bool BallCapturedSecondStage{};
int Timer;
float Unknown3;
float Unknown4;
float GravityMult;
float ZSetValue;
int CollisionMask;
float GravityPull;
circle_type Circle{};
field_effect_type Field{};
};

View File

@ -0,0 +1,73 @@
#include "pch.h"
#include "TKickback.h"
#include "control.h"
#include "loader.h"
#include "maths.h"
#include "render.h"
#include "timer.h"
#include "TPinballTable.h"
TKickback::TKickback(TPinballTable* table, int groupIndex): TCollisionComponent(table, groupIndex, true)
{
MessageField = 0;
Timer = 0;
KickActiveFlag = 0;
TimerTime = 0.7f;
TimerTime2 = 0.1f;
Threshold = 1000000000.0f;
}
int TKickback::Message(MessageCode code, float value)
{
if ((code == MessageCode::SetTiltLock || code == MessageCode::Reset) && Timer)
{
timer::kill(Timer);
SpriteSet(-1);
Timer = 0;
KickActiveFlag = 0;
Threshold = 1000000000.0;
}
return 0;
}
void TKickback::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge)
{
if (PinballTable->TiltLockFlag)
{
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 0.0,
Boost);
}
else
{
if (!KickActiveFlag)
{
Threshold = 1000000000.0;
KickActiveFlag = 1;
Timer = timer::set(TimerTime, this, TimerExpired);
}
if (DefaultCollision(ball, nextPosition, direction))
KickActiveFlag = 0;
}
}
void TKickback::TimerExpired(int timerId, void* caller)
{
auto kick = static_cast<TKickback*>(caller);
if (kick->KickActiveFlag)
{
kick->Threshold = 0.0;
kick->Timer = timer::set(kick->TimerTime2, kick, TimerExpired);
loader::play_sound(kick->HardHitSoundId, kick, "TKickback");
kick->SpriteSet(1);
}
else
{
kick->SpriteSet(0);
kick->Timer = 0;
control::handler(MessageCode::ControlTimerExpired, kick);
}
}

View File

@ -0,0 +1,19 @@
#pragma once
#include "TCollisionComponent.h"
class TKickback :
public TCollisionComponent
{
public:
TKickback(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void TimerExpired(int timerId, void* caller);
float TimerTime;
float TimerTime2;
int Timer;
int KickActiveFlag;
};

View File

@ -0,0 +1,169 @@
#include "pch.h"
#include "TKickout.h"
#include "control.h"
#include "loader.h"
#include "pb.h"
#include "TBall.h"
#include "TCircle.h"
#include "timer.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TKickout::TKickout(TPinballTable* table, int groupIndex, bool someFlag): TCollisionComponent(
table, groupIndex, false)
{
visualStruct visual{};
circle_type circle{};
NotSomeFlag = !someFlag;
if (!someFlag)
ActiveFlag = 0;
TimerTime1 = 1.5;
TimerTime2 = 0.05f;
MessageField = 0;
Timer = 0;
BallCaputeredFlag = 0;
FieldMult = *loader::query_float_attribute(groupIndex, 0, 305);
loader::query_visual(groupIndex, 0, &visual);
SoftHitSoundId = visual.SoftHitSoundId;
HardHitSoundId = visual.Kicker.HardHitSoundId;
Circle.Center.X = visual.FloatArr[0];
Circle.Center.Y = visual.FloatArr[1];
Circle.RadiusSq = *loader::query_float_attribute(groupIndex, 0, 306) * visual.FloatArr[2];
if (Circle.RadiusSq == 0.0f)
Circle.RadiusSq = 0.001f;
auto tCircle = new TCircle(this, &ActiveFlag, visual.CollisionGroup,
reinterpret_cast<vector3*>(visual.FloatArr), Circle.RadiusSq);
if (tCircle)
{
tCircle->place_in_grid(&AABB);
EdgeList.push_back(tCircle);
}
Circle.RadiusSq = visual.FloatArr[2] * visual.FloatArr[2];
auto zAttr = loader::query_float_attribute(groupIndex, 0, 408);
CollisionBallSetZ = pb::FullTiltMode && !pb::FullTiltDemoMode ? zAttr[3] : zAttr[2];
ThrowSpeedMult2 = visual.Kicker.ThrowBallMult * 0.01f;
BallThrowDirection = visual.Kicker.ThrowBallDirection;
ThrowAngleMult = visual.Kicker.ThrowBallAngleMult;
ThrowSpeedMult1 = visual.Kicker.Boost;
circle.RadiusSq = Circle.RadiusSq;
circle.Center.X = Circle.Center.X;
circle.Center.Y = Circle.Center.Y;
Field.ActiveFlag = &ActiveFlag;
Field.CollisionComp = this;
Field.CollisionGroup = visual.CollisionGroup;
TTableLayer::edges_insert_circle(&circle, nullptr, &Field);
}
int TKickout::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TKickoutRestartTimer:
if (BallCaputeredFlag)
{
if (value < 0.0f)
value = TimerTime1;
Timer = timer::set(value, this, TimerExpired);
}
break;
case MessageCode::SetTiltLock:
if (NotSomeFlag)
ActiveFlag = 0;
break;
case MessageCode::Reset:
if (BallCaputeredFlag)
{
if (Timer)
timer::kill(Timer);
TimerExpired(0, this);
}
if (NotSomeFlag)
ActiveFlag = 0;
break;
default:
break;
}
return 0;
}
void TKickout::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (!BallCaputeredFlag)
{
Ball = ball;
Threshold = 1000000000.0;
BallCaputeredFlag = 1;
ball->CollisionComp = this;
ball->Position.X = Circle.Center.X;
ball->Position.Y = Circle.Center.Y;
OriginalBallZ = ball->Position.Z;
ball->Position.Z = CollisionBallSetZ;
ball->CollisionDisabledFlag = true;
if (PinballTable->TiltLockFlag)
{
Message(MessageCode::TKickoutRestartTimer, 0.1f);
}
else
{
loader::play_sound(SoftHitSoundId, ball, "TKickout1");
control::handler(MessageCode::ControlCollision, this);
}
}
else
{
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
ball->not_again(edge);
}
}
int TKickout::FieldEffect(TBall* ball, vector2* dstVec)
{
vector2 direction{};
if (BallCaputeredFlag)
return 0;
direction.X = Circle.Center.X - ball->Position.X;
direction.Y = Circle.Center.Y - ball->Position.Y;
if (direction.Y * direction.Y + direction.X * direction.X > Circle.RadiusSq)
return 0;
maths::normalize_2d(direction);
dstVec->X = direction.X * FieldMult - ball->Direction.X * ball->Speed;
dstVec->Y = direction.Y * FieldMult - ball->Direction.Y * ball->Speed;
return 1;
}
void TKickout::TimerExpired(int timerId, void* caller)
{
auto kick = static_cast<TKickout*>(caller);
if (kick->BallCaputeredFlag)
{
kick->BallCaputeredFlag = 0;
kick->Timer = timer::set(kick->TimerTime2, kick, ResetTimerExpired);
if (kick->Ball)
{
loader::play_sound(kick->HardHitSoundId, kick->Ball, "TKickout2");
kick->Ball->Position.Z = kick->OriginalBallZ;
kick->Ball->throw_ball(&kick->BallThrowDirection, kick->ThrowAngleMult, kick->ThrowSpeedMult1,
kick->ThrowSpeedMult2);
kick->ActiveFlag = 0;
kick->Ball = nullptr;
}
}
}
void TKickout::ResetTimerExpired(int timerId, void* caller)
{
auto kick = static_cast<TKickout*>(caller);
if (!kick->NotSomeFlag)
kick->ActiveFlag = 1;
kick->Timer = 0;
}

View File

@ -0,0 +1,34 @@
#pragma once
#include "maths.h"
#include "TCollisionComponent.h"
#include "TEdgeManager.h"
class TKickout :
public TCollisionComponent
{
public:
TKickout(TPinballTable* table, int groupIndex, bool someFlag);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
int FieldEffect(TBall* ball, vector2* vecDst) override;
static void TimerExpired(int timerId, void* caller);
static void ResetTimerExpired(int timerId, void* caller);
int BallCaputeredFlag;
int NotSomeFlag;
int Timer;
float TimerTime1;
float TimerTime2;
float CollisionBallSetZ;
TBall* Ball{};
float FieldMult;
circle_type Circle{};
float OriginalBallZ{};
vector3 BallThrowDirection{};
float ThrowAngleMult;
float ThrowSpeedMult1;
float ThrowSpeedMult2;
field_effect_type Field{};
};

View File

@ -0,0 +1,349 @@
#include "pch.h"
#include "TLight.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "timer.h"
#include "TPinballTable.h"
TLight::TLight(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, true)
{
TimeoutTimer = 0;
FlasherOnFlag = false;
UndoOverrideTimer = 0;
FlashTimer = 0;
Reset();
float* floatArr1 = loader::query_float_attribute(groupIndex, 0, 900);
FlashDelay[0] = *floatArr1;
SourceDelay[0] = *floatArr1;
float* floatArr2 = loader::query_float_attribute(groupIndex, 0, 901);
FlashDelay[1] = *floatArr2;
SourceDelay[1] = *floatArr2;
}
int TLight::Message(MessageCode code, float value)
{
int bmpIndex;
switch (code)
{
case MessageCode::Reset:
Reset();
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
{
auto playerPtr = &PlayerData[index];
playerPtr->FlasherOnFlag = FlasherOnFlag;
playerPtr->LightOnBmpIndex = LightOnBmpIndex;
playerPtr->LightOnFlag = LightOnFlag;
playerPtr->MessageField = MessageField;
}
break;
case MessageCode::PlayerChanged:
{
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
playerPtr->FlasherOnFlag = FlasherOnFlag;
playerPtr->LightOnBmpIndex = LightOnBmpIndex;
playerPtr->LightOnFlag = LightOnFlag;
playerPtr->MessageField = MessageField;
Reset();
playerPtr = &PlayerData[static_cast<int>(floor(value))];
FlasherOnFlag = playerPtr->FlasherOnFlag;
LightOnBmpIndex = playerPtr->LightOnBmpIndex;
LightOnFlag = playerPtr->LightOnFlag;
MessageField = playerPtr->MessageField;
if (LightOnBmpIndex)
{
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(LightOnBmpIndex));
}
if (LightOnFlag)
Message(MessageCode::TLightTurnOn, 0.0);
if (FlasherOnFlag)
Message(MessageCode::TLightFlasherStart, 0.0);
break;
}
case MessageCode::TLightTurnOff:
LightOnFlag = false;
if (!FlasherOnFlag && !ToggledOffFlag && !ToggledOnFlag)
SetSpriteBmp(BmpArr[0]);
break;
case MessageCode::TLightTurnOn:
LightOnFlag = true;
if (!FlasherOnFlag && !ToggledOffFlag && !ToggledOnFlag)
SetSpriteBmp(BmpArr[1]);
break;
case MessageCode::TLightGetLightOnFlag:
return LightOnFlag;
case MessageCode::TLightGetFlasherOnFlag:
return FlasherOnFlag;
case MessageCode::TLightFlasherStart:
schedule_timeout(0.0);
if (!FlasherOnFlag || !FlashTimer)
{
FlasherOnFlag = true;
ToggledOnFlag = false;
ToggledOffFlag = false;
TurnOffAfterFlashingFg = false;
flasher_start(LightOnFlag);
}
break;
case MessageCode::TLightApplyMultDelay:
FlashDelay[0] = value * SourceDelay[0];
FlashDelay[1] = value * SourceDelay[1];
break;
case MessageCode::TLightApplyDelay:
FlashDelay[0] = SourceDelay[0];
FlashDelay[1] = SourceDelay[1];
break;
case MessageCode::TLightFlasherStartTimed:
if (!FlasherOnFlag)
flasher_start(LightOnFlag);
FlasherOnFlag = true;
ToggledOnFlag = false;
TurnOffAfterFlashingFg = false;
ToggledOffFlag = false;
schedule_timeout(value);
break;
case MessageCode::TLightTurnOffTimed:
if (!ToggledOffFlag)
{
if (FlasherOnFlag)
{
flasher_stop(0);
FlasherOnFlag = false;
}
else
{
SetSpriteBmp(BmpArr[0]);
}
ToggledOffFlag = true;
ToggledOnFlag = false;
}
schedule_timeout(value);
break;
case MessageCode::TLightTurnOnTimed:
if (!ToggledOnFlag)
{
if (FlasherOnFlag)
{
flasher_stop(1);
FlasherOnFlag = false;
}
else
{
SetSpriteBmp(BmpArr[1]);
}
ToggledOnFlag = true;
ToggledOffFlag = false;
}
schedule_timeout(value);
break;
case MessageCode::TLightSetOnStateBmpIndex:
LightOnBmpIndex = Clamp(static_cast<int>(floor(value)), 0, static_cast<int>(ListBitmap->size()) - 1);
BmpArr[0] = -1;
BmpArr[1] = LightOnBmpIndex;
if (!FlasherOnFlag)
{
if (ToggledOffFlag)
bmpIndex = 0;
else if (ToggledOnFlag)
bmpIndex = 1;
else
bmpIndex = LightOnFlag;
}
else
{
bmpIndex = FlashLightOnFlag;
}
SetSpriteBmp(BmpArr[bmpIndex]);
break;
case MessageCode::TLightIncOnStateBmpIndex:
bmpIndex = LightOnBmpIndex + 1;
if (bmpIndex >= static_cast<int>(ListBitmap->size()))
bmpIndex = static_cast<int>(ListBitmap->size()) - 1;
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(bmpIndex));
break;
case MessageCode::TLightDecOnStateBmpIndex:
bmpIndex = LightOnBmpIndex - 1;
if (bmpIndex < 0)
bmpIndex = 0;
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(bmpIndex));
break;
case MessageCode::TLightResetTimed:
if (TimeoutTimer)
timer::kill(TimeoutTimer);
TimeoutTimer = 0;
if (FlasherOnFlag)
flasher_stop(-1);
FlasherOnFlag = false;
ToggledOffFlag = false;
ToggledOnFlag = false;
SetSpriteBmp(BmpArr[LightOnFlag]);
break;
case MessageCode::TLightFlasherStartTimedThenStayOn:
TurnOffAfterFlashingFg = false;
if (UndoOverrideTimer)
timer::kill(UndoOverrideTimer);
UndoOverrideTimer = 0;
Message(MessageCode::TLightTurnOn, 0.0);
Message(MessageCode::TLightFlasherStartTimed, value);
break;
case MessageCode::TLightFlasherStartTimedThenStayOff:
if (UndoOverrideTimer)
timer::kill(UndoOverrideTimer);
UndoOverrideTimer = 0;
Message(MessageCode::TLightFlasherStartTimed, value);
TurnOffAfterFlashingFg = true;
break;
case MessageCode::TLightToggleValue:
Message(static_cast<int>(floor(value)) ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
return LightOnFlag;
case MessageCode::TLightResetAndToggleValue:
Message(MessageCode::TLightToggleValue, value);
Message(MessageCode::TLightResetTimed, 0.0);
return LightOnFlag;
case MessageCode::TLightResetAndTurnOn:
Message(MessageCode::TLightTurnOn, 0.0);
Message(MessageCode::TLightResetTimed, 0.0);
break;
case MessageCode::TLightResetAndTurnOff:
Message(MessageCode::TLightTurnOff, 0.0);
Message(MessageCode::TLightResetTimed, 0.0);
break;
case MessageCode::TLightToggle:
Message(MessageCode::TLightToggleValue, !LightOnFlag);
return LightOnFlag;
case MessageCode::TLightResetAndToggle:
Message(MessageCode::TLightResetAndToggleValue, !LightOnFlag);
return LightOnFlag;
case MessageCode::TLightSetMessageField:
MessageField = static_cast<int>(floor(value));
break;
case MessageCode::TLightFtTmpOverrideOn:
case MessageCode::TLightFtTmpOverrideOff:
// FT codes in negative to avoid overlap with 3DPB TLightGroup codes
SpriteSet(BmpArr[code == MessageCode::TLightFtTmpOverrideOn]);
if (UndoOverrideTimer)
timer::kill(UndoOverrideTimer);
UndoOverrideTimer = 0;
if (value > 0)
{
TemporaryOverrideFlag = true;
UndoOverrideTimer = timer::set(value, this, UndoTmpOverride);
}
break;
case MessageCode::TLightFtResetOverride:
if (UndoOverrideTimer)
timer::kill(UndoOverrideTimer);
UndoOverrideTimer = 0;
TemporaryOverrideFlag = false;
SpriteSet(PreviousBitmap);
break;
default:
break;
}
return 0;
}
void TLight::Reset()
{
if (TimeoutTimer)
timer::kill(TimeoutTimer);
if (UndoOverrideTimer)
timer::kill(UndoOverrideTimer);
if (FlasherOnFlag)
flasher_stop(-1);
TimeoutTimer = 0;
UndoOverrideTimer = 0;
LightOnFlag = false;
LightOnBmpIndex = 0;
ToggledOffFlag = false;
ToggledOnFlag = false;
FlasherOnFlag = false;
TemporaryOverrideFlag = false;
TurnOffAfterFlashingFg = false;
PreviousBitmap = -1;
BmpArr[0] = -1;
BmpArr[1] = 0;
SetSpriteBmp(BmpArr[0]);
MessageField = 0;
}
void TLight::schedule_timeout(float time)
{
FlashDelay[0] = SourceDelay[0];
FlashDelay[1] = SourceDelay[1];
if (TimeoutTimer)
timer::kill(TimeoutTimer);
TimeoutTimer = 0;
if (time > 0.0f)
TimeoutTimer = timer::set(time, this, TimerExpired);
}
void TLight::TimerExpired(int timerId, void* caller)
{
auto light = static_cast<TLight*>(caller);
if (light->FlasherOnFlag)
light->flasher_stop(-1);
light->SetSpriteBmp(light->BmpArr[light->LightOnFlag]);
light->ToggledOffFlag = false;
light->ToggledOnFlag = false;
light->FlasherOnFlag = false;
light->TimeoutTimer = 0;
if (light->TurnOffAfterFlashingFg)
{
light->TurnOffAfterFlashingFg = false;
light->Message(MessageCode::TLightResetAndTurnOff, 0.0);
}
if (light->Control)
control::handler(MessageCode::ControlTimerExpired, light);
}
void TLight::flasher_stop(int bmpIndex)
{
if (FlashTimer)
timer::kill(FlashTimer);
FlashTimer = 0;
if (bmpIndex >= 0)
{
FlashLightOnFlag = bmpIndex;
SetSpriteBmp(BmpArr[FlashLightOnFlag]);
}
}
void TLight::flasher_start(bool bmpIndex)
{
FlashLightOnFlag = bmpIndex;
flasher_callback(0, this);
}
void TLight::SetSpriteBmp(int index)
{
PreviousBitmap = index;
if (!TemporaryOverrideFlag)
SpriteSet(index);
}
void TLight::flasher_callback(int timerId, void* caller)
{
auto light = static_cast<TLight*>(caller);
light->FlashLightOnFlag ^= true;
light->SetSpriteBmp(light->BmpArr[light->FlashLightOnFlag]);
light->FlashTimer = timer::set(light->FlashDelay[light->FlashLightOnFlag], light, flasher_callback);
}
void TLight::UndoTmpOverride(int timerId, void* caller)
{
auto light = static_cast<TLight*>(caller);
light->Message(MessageCode::TLightFtResetOverride, 0.0f);
}
bool TLight::light_on() const
{
return LightOnFlag || ToggledOnFlag || FlasherOnFlag;
}

View File

@ -0,0 +1,46 @@
#pragma once
#include "TPinballComponent.h"
struct TLight_player_backup
{
int MessageField;
bool LightOnFlag;
int LightOnBmpIndex;
bool FlasherOnFlag;
};
class TLight :
public TPinballComponent
{
public:
TLight(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Reset();
void schedule_timeout(float time);
void flasher_stop(int bmpIndex);
void flasher_start(bool bmpIndex);
void SetSpriteBmp(int index);
bool light_on() const;
static void TimerExpired(int timerId, void* caller);
static void flasher_callback(int timerId, void* caller);
static void UndoTmpOverride(int timerId, void* caller);
int BmpArr[2]{-1};
float FlashDelay[2]{};
int FlashTimer;
bool FlashLightOnFlag{};
bool LightOnFlag{};
bool FlasherOnFlag;
bool ToggledOffFlag{};
bool ToggledOnFlag{};
bool TurnOffAfterFlashingFg{};
int LightOnBmpIndex{};
float SourceDelay[2]{};
int TimeoutTimer;
int UndoOverrideTimer;
bool TemporaryOverrideFlag{};
int PreviousBitmap = -1;
TLight_player_backup PlayerData[4]{};
};

View File

@ -0,0 +1,130 @@
#include "pch.h"
#include "TLightBargraph.h"
#include "control.h"
#include "loader.h"
#include "timer.h"
#include "TPinballTable.h"
TLightBargraph::TLightBargraph(TPinballTable* table, int groupIndex) : TLightGroup(table, groupIndex)
{
TimerTimeArray = nullptr;
TLightBargraph::Reset();
if (groupIndex > 0)
{
float* floatArr = loader::query_float_attribute(groupIndex, 0, 904);
if (floatArr)
{
auto count = 2 * List.size();
TimerTimeArray = new float[count];
if (TimerTimeArray)
{
for (auto i = 0u; i < count; ++floatArr)
TimerTimeArray[i++] = *floatArr;
}
}
}
}
TLightBargraph::~TLightBargraph()
{
delete[] TimerTimeArray;
}
int TLightBargraph::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TLightGroupGetOnCount:
return TimeIndex;
case MessageCode::TLightGroupToggleSplitIndex:
{
if (TimerBargraph)
{
timer::kill(TimerBargraph);
TimerBargraph = 0;
}
auto timeIndex = static_cast<int>(floor(value));
auto maxCount = static_cast<int>(List.size()) * 2;
if (timeIndex >= maxCount)
timeIndex = maxCount - 1;
if (timeIndex >= 0)
{
TLightGroup::Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(timeIndex / 2));
if (!(timeIndex & 1))
TLightGroup::Message(MessageCode::TLightGroupStartFlasher, 0.0);
if (TimerTimeArray)
TimerBargraph = timer::set(TimerTimeArray[timeIndex], this, BargraphTimerExpired);
TimeIndex = timeIndex;
}
else
{
TLightGroup::Message(MessageCode::TLightResetAndTurnOff, 0.0);
TimeIndex = 0;
}
break;
}
case MessageCode::SetTiltLock:
Reset();
break;
case MessageCode::PlayerChanged:
if (TimerBargraph)
{
timer::kill(TimerBargraph);
TimerBargraph = 0;
}
PlayerTimerIndexBackup[PinballTable->CurrentPlayer] = TimeIndex;
Reset();
TimeIndex = PlayerTimerIndexBackup[static_cast<int>(floor(value))];
if (TimeIndex)
{
TLightBargraph::Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(TimeIndex));
}
break;
case MessageCode::Reset:
{
Reset();
int* playerPtr = PlayerTimerIndexBackup;
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
{
*playerPtr = TimeIndex;
++playerPtr;
}
TLightGroup::Message(MessageCode::Reset, value);
break;
}
default:
TLightGroup::Message(code, value);
break;
}
return 0;
}
void TLightBargraph::Reset()
{
if (TimerBargraph)
{
timer::kill(TimerBargraph);
TimerBargraph = 0;
}
TimeIndex = 0;
TLightGroup::Reset();
}
void TLightBargraph::BargraphTimerExpired(int timerId, void* caller)
{
auto bar = static_cast<TLightBargraph*>(caller);
bar->TimerBargraph = 0;
if (bar->TimeIndex)
{
bar->Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(bar->TimeIndex - 1));
control::handler(MessageCode::ControlTimerExpired, bar);
}
else
{
bar->Message(MessageCode::TLightResetAndTurnOff, 0.0);
control::handler(MessageCode::TLightGroupCountdownEnded, bar);
}
}

View File

@ -0,0 +1,19 @@
#pragma once
#include "TLightGroup.h"
class TLightBargraph :
public TLightGroup
{
public:
TLightBargraph(TPinballTable* table, int groupIndex);
~TLightBargraph() override;
int Message(MessageCode code, float value) override;
void Reset() override;
static void BargraphTimerExpired(int timerId, void* caller);
float* TimerTimeArray;
int TimerBargraph{};
int TimeIndex{};
int PlayerTimerIndexBackup[4]{};
};

View File

@ -0,0 +1,464 @@
#include "pch.h"
#include "TLightGroup.h"
#include "control.h"
#include "loader.h"
#include "timer.h"
#include "TLight.h"
#include "TPinballTable.h"
TLightGroup::TLightGroup(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, false)
{
Timer = 0;
NotifyTimer = 0;
TLightGroup::Reset();
if (groupIndex > 0)
{
int count;
Timer1TimeDefault = *loader::query_float_attribute(groupIndex, 0, 903);
int16_t* groupIndArr = loader::query_iattribute(groupIndex, 1027, &count);
for (int index = 0; index < count; ++groupIndArr)
{
auto comp = dynamic_cast<TLight*>(table->find_component(*groupIndArr));
if (comp)
List.push_back(comp);
++index;
}
}
}
int TLightGroup::Message(MessageCode code, float value)
{
auto const count = static_cast<int>(List.size());
switch (code)
{
case MessageCode::SetTiltLock:
case MessageCode::GameOver:
break;
case MessageCode::PlayerChanged:
{
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
playerPtr->MessageField = MessageField;
playerPtr->MessageField2 = MessageField2;
playerPtr->Timer1Time = Timer1Time;
Reset();
playerPtr = &PlayerData[static_cast<int>(floor(value))];
MessageField = playerPtr->MessageField;
MessageField2 = playerPtr->MessageField2;
Timer1Time = playerPtr->Timer1Time;
if (!(MessageField == 0))
TimerExpired(0, this);
break;
}
case MessageCode::Reset:
Reset();
for (auto index = 0; index < PinballTable->PlayerCount; index++)
{
auto playerPtr = &PlayerData[index];
playerPtr->MessageField = MessageField;
playerPtr->MessageField2 = MessageField2;
playerPtr->Timer1Time = Timer1Time;
}
break;
case MessageCode::TLightGroupStepBackward:
{
auto lastLight = List.at(count - 1);
if (lastLight->FlasherOnFlag || lastLight->ToggledOnFlag || lastLight->ToggledOffFlag)
break;
if (MessageField2 != MessageCode::TLightGroupNull)
{
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
}
AnimationFlag = 1;
MessageField2 = code;
auto lastMessage = lastLight->MessageField;
auto lastStatus = lastLight->LightOnFlag;
for (auto index = count - 1; index > 0; --index)
{
auto lightCur = List.at(index);
auto lightPrev = List.at(index - 1);
lightCur->Message(lightPrev->LightOnFlag ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
lightCur->MessageField = lightPrev->MessageField;
}
auto firstLight = List.at(0);
firstLight->Message(lastStatus ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
firstLight->MessageField = lastMessage;
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupStepForward:
{
auto lastLight = List.at(count - 1);
if (lastLight->FlasherOnFlag || lastLight->ToggledOnFlag || lastLight->ToggledOffFlag)
break;
if (MessageField2 != MessageCode::TLightGroupNull)
{
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
}
auto firstLight = List.at(0);
AnimationFlag = 1;
MessageField2 = code;
auto firstMessage = firstLight->MessageField;
auto firstStatus = firstLight->LightOnFlag;
for (auto index = 0; index < count - 1; index++)
{
auto lightCur = List.at(index);
auto lightNext = List.at(index + 1);
lightCur->Message(lightNext->LightOnFlag ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
lightCur->MessageField = lightNext->MessageField;
}
lastLight->Message(firstStatus ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
lastLight->MessageField = firstMessage;
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupAnimationBackward:
{
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
start_animation();
MessageField2 = code;
AnimationFlag = 0;
auto lastLight = List.at(count - 1);
auto lastStatus = lastLight->ToggledOnFlag;
for (auto i = count - 1; i > 0; --i)
{
auto lightCur = List.at(i);
auto lightPrev = List.at(i - 1);
lightCur->Message(lightPrev->ToggledOnFlag ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0.0);
}
auto firstLight = List.at(0);
firstLight->Message(lastStatus ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0);
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupAnimationForward:
{
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
start_animation();
MessageField2 = code;
AnimationFlag = 0;
auto firstLight = List.at(0);
auto firstStatus = firstLight->ToggledOnFlag;
for (auto i = 0; i < count - 1; i++)
{
auto lightCur = List.at(i);
auto lightNext = List.at(i + 1);
lightCur->Message(lightNext->ToggledOnFlag ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0.0);
}
auto lastLight = List.at(count - 1);
lastLight->Message(firstStatus ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0);
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupLightShowAnimation:
{
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
start_animation();
MessageField2 = code;
AnimationFlag = 0;
for (auto light : List)
{
if (rand() % 100 > 70)
{
auto randVal = RandFloat() * value * 3.0f + 0.1f;
light->Message(MessageCode::TLightTurnOnTimed, randVal);
}
}
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupGameOverAnimation:
{
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
start_animation();
MessageField2 = code;
AnimationFlag = 0;
for (auto light : List)
{
auto randVal = static_cast<float>(rand() % 100 > 70);
light->Message(MessageCode::TLightResetAndToggleValue, randVal);
}
reschedule_animation(value);
break;
}
case MessageCode::TLightGroupRandomAnimationSaturation:
{
auto noBmpInd1Count = 0;
for (auto light : List)
{
if (!light->LightOnFlag)
++noBmpInd1Count;
}
if (!noBmpInd1Count)
break;
auto randModCount = rand() % noBmpInd1Count;
for (auto it = List.rbegin(); it != List.rend(); ++it)
{
auto light = *it;
if (!light->LightOnFlag && randModCount-- == 0)
{
light->Message(MessageCode::TLightTurnOn, 0.0);
break;
}
}
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
break;
}
case MessageCode::TLightGroupRandomAnimationDesaturation:
{
auto bmpInd1Count = 0;
for (auto light : List)
{
if (light->LightOnFlag)
++bmpInd1Count;
}
if (!bmpInd1Count)
break;
auto randModCount = rand() % bmpInd1Count;
for (auto it = List.rbegin(); it != List.rend(); ++it)
{
auto light = *it;
if (light->LightOnFlag && randModCount-- == 0)
{
light->Message(MessageCode::TLightTurnOff, 0.0);
break;
}
}
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
break;
}
case MessageCode::TLightGroupOffsetAnimationForward:
{
auto index = next_light_up();
if (index < 0)
break;
List.at(index)->Message(MessageCode::TLightTurnOn, 0.0);
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
return 1;
}
case MessageCode::TLightGroupOffsetAnimationBackward:
{
auto index = next_light_down();
if (index < 0)
break;
List.at(index)->Message(MessageCode::TLightTurnOff, 0.0);
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
return 1;
}
case MessageCode::TLightGroupReset:
{
if (Timer)
timer::kill(Timer);
Timer = 0;
if (MessageField2 == MessageCode::TLightGroupAnimationBackward ||
MessageField2 == MessageCode::TLightGroupAnimationForward || MessageField2 == MessageCode::TLightGroupLightShowAnimation)
TLightGroup::Message(MessageCode::TLightResetTimed, 0.0);
MessageField2 = MessageCode::TLightGroupNull;
AnimationFlag = 0;
break;
}
case MessageCode::TLightGroupTurnOnAtIndex:
{
auto index = static_cast<int>(floor(value));
if (index >= count || index < 0)
break;
auto light = List.at(index);
light->Message(MessageCode::TLightTurnOn, 0.0);
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
break;
}
case MessageCode::TLightGroupTurnOffAtIndex:
{
auto index = static_cast<int>(floor(value));
if (index >= count || index < 0)
break;
auto light = List.at(index);
light->Message(MessageCode::TLightTurnOff, 0.0);
if (MessageField2 != MessageCode::TLightGroupNull)
start_animation();
break;
}
case MessageCode::TLightGroupGetOnCount:
{
auto bmp1Count = 0;
for (auto light : List)
{
if (light->LightOnFlag)
++bmp1Count;
}
return bmp1Count;
}
case MessageCode::TLightGroupGetLightCount:
return count;
case MessageCode::TLightGroupGetMessage2:
return static_cast<int>(MessageField2);
case MessageCode::TLightGroupGetAnimationFlag:
return AnimationFlag;
case MessageCode::TLightGroupResetAndTurnOn:
{
auto index = next_light_up();
if (index < 0)
break;
if (MessageField2 != MessageCode::TLightGroupNull || AnimationFlag)
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
List.at(index)->Message(MessageCode::TLightFlasherStartTimedThenStayOn, value);
return 1;
}
case MessageCode::TLightGroupResetAndTurnOff:
{
auto index = next_light_down();
if (index < 0)
break;
if (MessageField2 != MessageCode::TLightGroupNull || AnimationFlag)
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
List.at(index)->Message(MessageCode::TLightFlasherStartTimedThenStayOff, value);
return 1;
}
case MessageCode::TLightGroupRestartNotifyTimer:
if (NotifyTimer)
timer::kill(NotifyTimer);
NotifyTimer = 0;
if (value > 0.0f)
NotifyTimer = timer::set(value, this, NotifyTimerExpired);
break;
case MessageCode::TLightGroupFlashWhenOn:
{
for (auto it = List.rbegin(); it != List.rend(); ++it)
{
auto light = *it;
if (light->LightOnFlag)
{
light->Message(MessageCode::TLightTurnOff, 0.0);
light->Message(MessageCode::TLightFlasherStartTimedThenStayOff, value);
}
}
break;
}
case MessageCode::TLightGroupToggleSplitIndex:
{
control::handler(code, this);
auto index = static_cast<int>(floor(value));
if (index >= 0 && index < count)
{
// Turn off lights (index, end]
for (auto i = count - 1; i > index; i--)
{
List.at(i)->Message(MessageCode::TLightResetAndTurnOff, 0.0);
}
// Turn on lights [begin, index]
for (auto i = index; i >= 0; i--)
{
List.at(i)->Message(MessageCode::TLightResetAndTurnOn, 0.0);
}
}
break;
}
case MessageCode::TLightGroupStartFlasher:
{
auto index = next_light_down();
if (index >= 0)
{
List.at(index)->Message(MessageCode::TLightFlasherStart, 0.0);
}
break;
}
default:
for (auto it = List.rbegin(); it != List.rend(); ++it)
{
(*it)->Message(code, value);
}
break;
}
return 0;
}
void TLightGroup::Reset()
{
if (Timer)
timer::kill(Timer);
Timer = 0;
if (NotifyTimer)
timer::kill(NotifyTimer);
NotifyTimer = 0;
MessageField2 = MessageCode::TLightGroupNull;
AnimationFlag = 0;
Timer1Time = Timer1TimeDefault;
}
void TLightGroup::reschedule_animation(float time)
{
if (Timer)
timer::kill(Timer);
Timer = 0;
if (time == 0)
{
MessageField2 = MessageCode::TLightGroupNull;
AnimationFlag = 0;
return;
}
Timer1Time = time > 0.0f ? time : Timer1TimeDefault;
Timer = timer::set(Timer1Time, this, TimerExpired);
}
void TLightGroup::start_animation()
{
for (auto it = List.rbegin(); it != List.rend(); ++it)
{
auto light = *it;
if (light->LightOnFlag)
light->Message(MessageCode::TLightTurnOnTimed, 0.0);
else
light->Message(MessageCode::TLightTurnOffTimed, 0.0);
}
}
int TLightGroup::next_light_up()
{
for (auto index = 0u; index < List.size(); ++index)
{
if (!List[index]->LightOnFlag)
return static_cast<int>(index);
}
return -1;
}
int TLightGroup::next_light_down()
{
for (auto index = static_cast<int>(List.size()) - 1; index >= 0; --index)
{
if (List.at(index)->LightOnFlag)
return index;
}
return -1;
}
void TLightGroup::TimerExpired(int timerId, void* caller)
{
auto group = static_cast<TLightGroup*>(caller);
group->Timer = 0;
group->Message(group->MessageField2, group->Timer1Time);
}
void TLightGroup::NotifyTimerExpired(int timerId, void* caller)
{
auto group = static_cast<TLightGroup*>(caller);
group->NotifyTimer = 0;
control::handler(MessageCode::ControlNotifyTimerExpired, group);
}

View File

@ -0,0 +1,40 @@
#pragma once
#include "TPinballComponent.h"
class TLight;
struct TLightGroup_player_backup
{
int MessageField;
MessageCode MessageField2;
float Timer1Time;
int Unknown3;
};
class TLightGroup :
public TPinballComponent
{
public:
TLightGroup(TPinballTable* table, int groupIndex);
~TLightGroup() override = default;
int Message(MessageCode code, float value) override;
virtual void Reset();
void reschedule_animation(float time);
void start_animation();
int next_light_up();
int next_light_down();
static void TimerExpired(int timerId, void* caller);
static void NotifyTimerExpired(int timerId, void* caller);
std::vector<TLight*> List;
float Timer1Time{};
float Timer1TimeDefault;
MessageCode MessageField2{};
int AnimationFlag{};
int NotifyTimer;
int Timer;
TLightGroup_player_backup PlayerData[4]{};
};

View File

@ -0,0 +1,67 @@
#include "pch.h"
#include "TLightRollover.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "TBall.h"
#include "timer.h"
#include "TPinballTable.h"
TLightRollover::TLightRollover(TPinballTable* table, int groupIndex) : TRollover(table, groupIndex, false)
{
RolloverFlag = 0;
Timer = 0;
SpriteSet(-1);
build_walls(groupIndex);
FloatArr = *loader::query_float_attribute(groupIndex, 0, 407);
}
int TLightRollover::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset)
{
ActiveFlag = 1;
RolloverFlag = 0;
if (Timer)
timer::kill(Timer);
Timer = 0;
SpriteSet(-1);
}
return 0;
}
void TLightRollover::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge)
{
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
ball->not_again(edge);
if (!PinballTable->TiltLockFlag)
{
if (RolloverFlag)
{
timer::set(0.1f, this, TimerExpired);
ActiveFlag = 0;
RolloverFlag = RolloverFlag == 0;
if (Timer == 0)
Timer = timer::set(FloatArr, this, delay_expired);
}
else
{
loader::play_sound(SoftHitSoundId, this, "TLightRollover");
control::handler(MessageCode::ControlCollision, this);
RolloverFlag = RolloverFlag == 0;
SpriteSet(0);
}
}
}
void TLightRollover::delay_expired(int timerId, void* caller)
{
auto roll = static_cast<TLightRollover*>(caller);
roll->SpriteSet(-1);
roll->Timer = 0;
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "TRollover.h"
class TLightRollover :
public TRollover
{
public:
TLightRollover(TPinballTable* table, int groupIndex);
~TLightRollover() override = default;
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void delay_expired(int timerId, void* caller);
float FloatArr;
int Timer;
};

131
SpaceCadetPinball/TLine.cpp Normal file
View File

@ -0,0 +1,131 @@
#include "pch.h"
#include "TLine.h"
#include "TTableLayer.h"
TLine::TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, float x0, float y0, float x1,
float y1): TEdgeSegment(collCmp, activeFlag, collisionGroup)
{
X0 = x0;
Y0 = y0;
X1 = x1;
Y1 = y1;
maths::line_init(Line, x0, y0, x1, y1);
}
TLine::TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, const vector2& start,
const vector2& end) : TEdgeSegment(collCmp, activeFlag, collisionGroup)
{
X0 = start.X;
Y0 = start.Y;
X1 = end.X;
Y1 = end.Y;
maths::line_init(Line, X0, Y0, X1, Y1);
}
void TLine::Offset(float offset)
{
float offX = offset * Line.PerpendicularC.X;
float offY = offset * Line.PerpendicularC.Y;
X0 += offX;
Y0 += offY;
X1 += offX;
Y1 += offY;
maths::line_init(Line, X0, Y0, X1, Y1);
}
float TLine::FindCollisionDistance(const ray_type& ray)
{
return maths::ray_intersect_line(ray, Line);
}
void TLine::EdgeCollision(TBall* ball, float distance)
{
CollisionComponent->Collision(
ball,
&Line.RayIntersect,
&Line.PerpendicularC,
distance,
this);
}
void TLine::place_in_grid(RectF* aabb)
{
if (aabb)
{
aabb->Merge({
std::max(X0, X1), std::max(Y0, Y1),
std::min(X0, X1), std::min(Y0, Y1)
});
}
auto edgeMan = TTableLayer::edge_manager;
auto xBox0 = edgeMan->box_x(X0);
auto yBox0 = edgeMan->box_y(Y0);
auto xBox1 = edgeMan->box_x(X1);
auto yBox1 = edgeMan->box_y(Y1);
int dirX = X0 >= X1 ? -1 : 1;
int dirY = Y0 >= Y1 ? -1 : 1;
if (yBox0 == yBox1)
{
if (dirX == 1)
{
while (xBox0 <= xBox1)
edgeMan->add_edge_to_box(xBox0++, yBox0, this);
}
else
{
while (xBox0 >= xBox1)
edgeMan->add_edge_to_box(xBox0--, yBox0, this);
}
}
else if (xBox0 == xBox1)
{
if (dirY == 1)
{
while (yBox0 <= yBox1)
edgeMan->add_edge_to_box(xBox0, yBox0++, this);
}
else
{
while (yBox0 >= yBox1)
edgeMan->add_edge_to_box(xBox0, yBox0--, this);
}
}
else
{
edgeMan->add_edge_to_box(xBox0, yBox0, this);
// Bresenham line formula: y = dYdX * (x - x0) + y0; dYdX = (y0 - y1) / (x0 - x1)
auto dyDx = (Y0 - Y1) / (X0 - X1);
// Precompute constant part: dYdX * (-x0) + y0
auto precomp = -X0 * dyDx + Y0;
// X and Y indexes are offset by one when going forwards, not sure why
auto xBias = dirX == 1 ? 1 : 0, yBias = dirY == 1 ? 1 : 0;
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
{
// Calculate y from indexY and from line formula
auto yDiscrete = (indexY + yBias) * edgeMan->AdvanceY + edgeMan->MinY;
auto ylinear = ((indexX + xBias) * edgeMan->AdvanceX + edgeMan->MinX) * dyDx + precomp;
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
{
// Advance indexY when discrete value is ahead/behind
// Advance indexX when discrete value matches linear value
indexY += dirY;
if (ylinear == yDiscrete)
indexX += dirX;
}
else
{
// Advance indexX otherwise
indexX += dirX;
}
edgeMan->add_edge_to_box(indexX, indexY, this);
}
}
}

17
SpaceCadetPinball/TLine.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include "maths.h"
#include "TEdgeSegment.h"
class TLine :
public TEdgeSegment
{
public:
line_type Line{};
float X0, Y0, X1, Y1;
TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, float x0, float y0, float x1, float y1);
TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, const vector2& start, const vector2& end);
void Offset(float offset);
float FindCollisionDistance(const ray_type& ray) override;
void EdgeCollision(TBall* ball, float distance) override;
void place_in_grid(RectF* aabb) override;
};

View File

@ -0,0 +1,74 @@
#include "pch.h"
#include "TOneway.h"
#include "control.h"
#include "loader.h"
#include "TBall.h"
#include "TLine.h"
#include "TPinballTable.h"
TOneway::TOneway(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
vector2 linePt1{}, linePt2{};
loader::query_visual(groupIndex, 0, &visual);
if (visual.FloatArrCount == 2)
{
linePt2.X = visual.FloatArr[0];
linePt2.Y = visual.FloatArr[1];
linePt1.X = visual.FloatArr[2];
linePt1.Y = visual.FloatArr[3];
auto line = new TLine(this, &ActiveFlag, visual.CollisionGroup, linePt2, linePt1);
if (line)
{
line->Offset(table->CollisionCompOffset);
line->place_in_grid(&AABB);
EdgeList.push_back(line);
}
line = new TLine(this, &ActiveFlag, visual.CollisionGroup, linePt1, linePt2);
Line = line;
if (line)
{
line->Offset(-table->CollisionCompOffset * 0.8f);
Line->place_in_grid(&AABB);
EdgeList.push_back(Line);
}
}
}
void TOneway::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (edge == Line)
{
ball->not_again(edge);
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
if (!PinballTable->TiltLockFlag)
{
if (HardHitSoundId)
loader::play_sound(HardHitSoundId, ball, "TOneway1");
control::handler(MessageCode::ControlCollision, this);
}
}
else if (PinballTable->TiltLockFlag)
{
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
}
else if (maths::basic_collision(
ball,
nextPosition,
direction,
Elasticity,
Smoothness,
Threshold,
Boost) > 0.2f)
{
if (SoftHitSoundId)
loader::play_sound(SoftHitSoundId, ball, "TOneway2");
}
}

View File

@ -0,0 +1,15 @@
#pragma once
#include "TCollisionComponent.h"
class TLine;
class TOneway : public TCollisionComponent
{
public:
TOneway(TPinballTable* table, int groupIndex);
~TOneway() override = default;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
TLine* Line;
};

View File

@ -0,0 +1,165 @@
#include "pch.h"
#include "TPinballComponent.h"
#include "control.h"
#include "loader.h"
#include "proj.h"
#include "render.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TPinballComponent::TPinballComponent(TPinballTable* table, int groupIndex, bool loadVisuals)
{
visualStruct visual{};
MessageField = 0;
UnusedBaseFlag = 0;
ActiveFlag = 0;
PinballTable = table;
RenderSprite = nullptr;
ListBitmap = nullptr;
GroupName = nullptr;
Control = nullptr;
VisualPosNormX= -1.0f;
VisualPosNormY = -1.0f;
GroupIndex = groupIndex;
if (table)
table->ComponentList.push_back(this);
if (groupIndex >= 0)
GroupName = loader::query_name(groupIndex);
if (loadVisuals && groupIndex >= 0)
{
int visualCount = loader::query_visual_states(groupIndex);
for (int index = 0; index < visualCount; ++index)
{
loader::query_visual(groupIndex, index, &visual);
if (visual.Bitmap.Bmp)
{
assertm(visual.Bitmap.ZMap, "Bitmap/zMap pairing is mandatory");
if (!ListBitmap)
ListBitmap = new std::vector<SpriteData>();
ListBitmap->push_back(visual.Bitmap);
}
}
if (ListBitmap)
{
rectangle_type bmp1Rect{}, tmpRect{};
const auto rootSprite = ListBitmap->at(0);
const auto rootBmp = rootSprite.Bmp;
bmp1Rect.XPosition = rootBmp->XPosition - table->XOffset;
bmp1Rect.YPosition = rootBmp->YPosition - table->YOffset;
bmp1Rect.Width = rootBmp->Width;
bmp1Rect.Height = rootBmp->Height;
for (auto index = 1u; index < ListBitmap->size(); index++)
{
auto bmp = ListBitmap->at(index).Bmp;
tmpRect.XPosition = bmp->XPosition - table->XOffset;
tmpRect.YPosition = bmp->YPosition - table->YOffset;
tmpRect.Width = bmp->Width;
tmpRect.Height = bmp->Height;
maths::enclosing_box(bmp1Rect, tmpRect, bmp1Rect);
}
RenderSprite = new render_sprite(
VisualTypes::Sprite,
rootBmp,
rootSprite.ZMap,
rootBmp->XPosition - table->XOffset,
rootBmp->YPosition - table->YOffset,
&bmp1Rect);
// Sound position = center of root visual, reverse-projected, normalized.
auto& rect = RenderSprite->BmpRect;
vector2i pos2D{ rect.XPosition + rect.Width / 2, rect.YPosition + rect.Height / 2 };
auto pos3D = proj::ReverseXForm(pos2D);
auto posNorm = TTableLayer::edge_manager->NormalizeBox(pos3D);
VisualPosNormX = posNorm.X;
VisualPosNormY = posNorm.Y;
}
}
}
TPinballComponent::~TPinballComponent()
{
if (PinballTable)
{
// ComponentList contains one reference to each component.
auto& components = PinballTable->ComponentList;
auto position = std::find(components.begin(), components.end(), this);
if (position != components.end())
components.erase(position);
}
delete ListBitmap;
}
int TPinballComponent::Message(MessageCode code, float value)
{
MessageField = static_cast<int>(code);
if (code == MessageCode::Reset)
MessageField = 0;
return 0;
}
void TPinballComponent::port_draw()
{
}
int TPinballComponent::get_scoring(unsigned int index) const
{
return Control == nullptr || index >= Control->ScoreCount ? 0 : Control->Scores[index];
}
vector2 TPinballComponent::get_coordinates()
{
return {VisualPosNormX, VisualPosNormY};
}
void TPinballComponent::SpriteSet(int index) const
{
if (!ListBitmap)
return;
int xPos, yPos;
gdrv_bitmap8* bmp;
zmap_header_type* zMap;
if (index >= 0)
{
auto& spriteData = ListBitmap->at(index);
bmp = spriteData.Bmp;
zMap = spriteData.ZMap;
xPos = bmp->XPosition - PinballTable->XOffset;
yPos = bmp->YPosition - PinballTable->YOffset;
}
else
{
bmp = nullptr;
zMap = nullptr;
xPos = RenderSprite->BmpRect.XPosition;
yPos = RenderSprite->BmpRect.YPosition;
}
RenderSprite->set(bmp, zMap, xPos, yPos);
}
void TPinballComponent::SpriteSetBall(int index, vector2i pos, float depth) const
{
if (ListBitmap)
{
gdrv_bitmap8* bmp = nullptr;
if (index >= 0)
{
bmp = ListBitmap->at(index).Bmp;
pos.X -= bmp->Width / 2;
pos.Y -= bmp->Height / 2;
}
RenderSprite->ball_set(bmp, depth, pos.X, pos.Y);
}
}

View File

@ -0,0 +1,158 @@
#pragma once
struct SpriteData;
struct vector2i;
struct zmap_header_type;
struct gdrv_bitmap8;
struct render_sprite;
struct component_control;
struct vector2;
class TPinballTable;
enum class MessageCode
{
// Private codes <1000, different meaning for each component
TFlipperNull = 0,
TFlipperExtend = 1,
TFlipperRetract = 2,
TLightTurnOff = 0,
TLightTurnOn = 1,
TLightGetLightOnFlag = 2,
TLightGetFlasherOnFlag = 3,
TLightFlasherStart = 4,
TLightApplyMultDelay = 5,
TLightApplyDelay = 6,
TLightFlasherStartTimed = 7,
TLightTurnOffTimed = 8,
TLightTurnOnTimed = 9,
TLightSetOnStateBmpIndex = 11,
TLightIncOnStateBmpIndex = 12,
TLightDecOnStateBmpIndex = 13,
TLightResetTimed = 14,
TLightFlasherStartTimedThenStayOn = 15,
TLightFlasherStartTimedThenStayOff = 16,
TLightToggleValue = 17,
TLightResetAndToggleValue = 18,
TLightResetAndTurnOn = 19,
TLightResetAndTurnOff = 20,
TLightToggle = 21,
TLightResetAndToggle = 22,
TLightSetMessageField = 23,
TLightFtTmpOverrideOn = -24,
TLightFtTmpOverrideOff = -25,
TLightFtResetOverride = -26,
TLightGroupNull = 0,
TLightGroupStepBackward = 24,
TLightGroupStepForward = 25,
TLightGroupAnimationBackward = 26,
TLightGroupAnimationForward = 27,
TLightGroupLightShowAnimation = 28,
TLightGroupGameOverAnimation = 29,
TLightGroupRandomAnimationSaturation = 30,
TLightGroupRandomAnimationDesaturation = 31,
TLightGroupOffsetAnimationForward = 32,
TLightGroupOffsetAnimationBackward = 33,
TLightGroupReset = 34,
TLightGroupTurnOnAtIndex = 35,
TLightGroupTurnOffAtIndex = 36,
TLightGroupGetOnCount = 37,
TLightGroupGetLightCount = 38,
TLightGroupGetMessage2 = 39,
TLightGroupGetAnimationFlag = 40,
TLightGroupResetAndTurnOn = 41,
TLightGroupResetAndTurnOff = 42,
TLightGroupRestartNotifyTimer = 43,
TLightGroupFlashWhenOn = 44,
TLightGroupToggleSplitIndex = 45,
TLightGroupStartFlasher = 46,
TLightGroupCountdownEnded = 47,
TBumperSetBmpIndex = 11,
TBumperIncBmpIndex = 12,
TBumperDecBmpIndex = 13,
TComponentGroupResetNotifyTimer = 48,
TPopupTargetDisable = 49,
TPopupTargetEnable = 50,
TBlockerDisable = 51,
TBlockerEnable = 52,
TBlockerRestartTimeout = 59,
TGateDisable = 53,
TGateEnable = 54,
TKickoutRestartTimer = 55,
TSinkUnknown7 = 7,
TSinkResetTimer = 56,
TSoloTargetDisable = 49,
TSoloTargetEnable = 50,
TTimerResetTimer = 59,
ControlBallCaptured = 57,
ControlBallReleased = 58,
ControlTimerExpired = 60,
ControlNotifyTimerExpired = 61,
ControlSpinnerLoopReset = 62,
ControlCollision = 63,
ControlEnableMultiplier = 64,
ControlDisableMultiplier = 65,
ControlMissionComplete = 66,
ControlMissionStarted = 67,
// Public codes 1000+, apply to all components
LeftFlipperInputPressed = 1000,
LeftFlipperInputReleased = 1001,
RightFlipperInputPressed = 1002,
RightFlipperInputReleased = 1003,
PlungerInputPressed = 1004,
PlungerInputReleased = 1005,
Pause = 1008,
Resume = 1009,
LooseFocus = 1010,
SetTiltLock = 1011,
ClearTiltLock = 1012,
StartGamePlayer1 = 1013,
NewGame = 1014,
PlungerFeedBall = 1015,
PlungerStartFeedTimer = 1016,
PlungerLaunchBall = 1017,
PlungerRelaunchBall = 1018,
PlayerChanged = 1020,
SwitchToNextPlayer = 1021,
GameOver = 1022,
Reset = 1024,
};
class TPinballComponent
{
public:
TPinballComponent(TPinballTable* table, int groupIndex, bool loadVisuals);
virtual ~TPinballComponent();
virtual int Message(MessageCode code, float value);
virtual void port_draw();
int get_scoring(unsigned int index) const;
virtual vector2 get_coordinates();
void SpriteSet(int index) const;
void SpriteSetBall(int index, vector2i pos, float depth) const;
char UnusedBaseFlag;
char ActiveFlag;
int MessageField;
char* GroupName;
component_control* Control;
int GroupIndex;
render_sprite* RenderSprite;
TPinballTable* PinballTable;
std::vector<SpriteData>* ListBitmap;
private:
float VisualPosNormX;
float VisualPosNormY;
};

View File

@ -0,0 +1,714 @@
#include "pch.h"
#include "TPinballTable.h"
#include "control.h"
#include "loader.h"
#include "midi.h"
#include "pb.h"
#include "render.h"
#include "TBall.h"
#include "TBlocker.h"
#include "TBumper.h"
#include "TComponentGroup.h"
#include "TDemo.h"
#include "TFlagSpinner.h"
#include "TGate.h"
#include "THole.h"
#include "timer.h"
#include "TKickback.h"
#include "TKickout.h"
#include "TLight.h"
#include "TLightBargraph.h"
#include "TLightGroup.h"
#include "TLightRollover.h"
#include "TOneway.h"
#include "TPopupTarget.h"
#include "TRamp.h"
#include "TRollover.h"
#include "TSink.h"
#include "TSoloTarget.h"
#include "TSound.h"
#include "TTableLayer.h"
#include "TTextBox.h"
#include "TTimer.h"
#include "TTripwire.h"
#include "TWall.h"
#include "TPlunger.h"
#include "TFlipper.h"
#include "TDrain.h"
#include "translations.h"
int TPinballTable::score_multipliers[5] = {1, 2, 3, 5, 10};
TPinballTable::TPinballTable(): TPinballComponent(nullptr, -1, false)
{
int shortArrLength;
CurScoreStruct = nullptr;
ScoreBallcount = nullptr;
ScorePlayerNumber1 = nullptr;
BallInDrainFlag = 0;
ActiveFlag = 1;
TiltLockFlag = 0;
EndGameTimeoutTimer = 0;
LightShowTimer = 0;
ReplayTimer = 0;
TiltTimeoutTimer = 0;
MultiballFlag = false;
PlayerCount = 0;
auto ball = AddBall({0.0f, 0.0f});
ball->Disable();
new TTableLayer(this);
LightGroup = new TLightGroup(this, 0);
auto score1 = score::create("score1", render::background_bitmap);
CurScoreStruct = score1;
PlayerScores[0].ScoreStruct = score1;
for (int scoreIndex = 1; scoreIndex < 4; scoreIndex++)
{
PlayerScores[scoreIndex].ScoreStruct = score::dup(CurScoreStruct, scoreIndex);
}
CurrentPlayer = 0;
MaxBallCount = 3;
ScoreBallcount = score::create("ballcount1", render::background_bitmap);
ScorePlayerNumber1 = score::create("player_number1", render::background_bitmap);
int groupIndexObjects = loader::query_handle("table_objects");
short* shortArr = loader::query_iattribute(groupIndexObjects, 1025, &shortArrLength);
if (shortArrLength > 0)
{
for (int i = 0; i < shortArrLength / 2; ++i)
{
int objectType = *shortArr;
short* shortArrPlus1 = shortArr + 1;
int groupIndex = *shortArrPlus1;
shortArr = shortArrPlus1 + 1;
switch (objectType)
{
case 1000:
case 1010:
new TWall(this, groupIndex);
break;
case 1001:
Plunger = new TPlunger(this, groupIndex);
break;
case 1002:
LightGroup->List.push_back(new TLight(this, groupIndex));
break;
case 1003:
FlipperL = new TFlipper(this, groupIndex);
break;
case 1004:
FlipperR = new TFlipper(this, groupIndex);
break;
case 1005:
new TBumper(this, groupIndex);
break;
case 1006:
new TPopupTarget(this, groupIndex);
break;
case 1007:
Drain = new TDrain(this, groupIndex);
break;
case 1011:
new TBlocker(this, groupIndex);
break;
case 1012:
new TKickout(this, groupIndex, true);
break;
case 1013:
new TGate(this, groupIndex);
break;
case 1014:
new TKickback(this, groupIndex);
break;
case 1015:
new TRollover(this, groupIndex);
break;
case 1016:
new TOneway(this, groupIndex);
break;
case 1017:
new TSink(this, groupIndex);
break;
case 1018:
new TFlagSpinner(this, groupIndex);
break;
case 1019:
new TSoloTarget(this, groupIndex);
break;
case 1020:
new TLightRollover(this, groupIndex);
break;
case 1021:
new TRamp(this, groupIndex);
break;
case 1022:
new THole(this, groupIndex);
break;
case 1023:
new TDemo(this, groupIndex);
break;
case 1024:
new TTripwire(this, groupIndex);
break;
case 1026:
new TLightGroup(this, groupIndex);
break;
case 1028:
new TComponentGroup(this, groupIndex);
break;
case 1029:
new TKickout(this, groupIndex, false);
break;
case 1030:
new TLightBargraph(this, groupIndex);
break;
case 1031:
new TSound(this, groupIndex);
break;
case 1032:
new TTimer(this, groupIndex);
break;
case 1033:
new TTextBox(this, groupIndex);
break;
default:
continue;
}
}
}
render::build_occlude_list();
pb::InfoTextBox = dynamic_cast<TTextBox*>(find_component("info_text_box"));
pb::MissTextBox = dynamic_cast<TTextBox*>(find_component("mission_text_box"));
control::make_links(this);
}
TPinballTable::~TPinballTable()
{
for (auto& PlayerScore : PlayerScores)
{
delete PlayerScore.ScoreStruct;
}
if (ScorePlayerNumber1)
{
delete ScorePlayerNumber1;
ScorePlayerNumber1 = nullptr;
}
if (ScoreBallcount)
{
delete ScoreBallcount;
ScoreBallcount = nullptr;
}
delete LightGroup;
while (!ComponentList.empty())
{
// Component destructor removes it from the list.
delete ComponentList[0];
}
control::ClearLinks();
pb::InfoTextBox = pb::MissTextBox = nullptr;
}
TPinballComponent* TPinballTable::find_component(LPCSTR componentName)
{
for (auto component : ComponentList)
{
const char* groupName = component->GroupName;
if (groupName && !strcmp(groupName, componentName))
{
return component;
}
}
pb::ShowMessageBox(SDL_MESSAGEBOX_WARNING, "Table cant find:", componentName);
return nullptr;
}
TPinballComponent* TPinballTable::find_component(int groupIndex)
{
char Buffer[40]{};
for (auto component : ComponentList)
{
if (component->GroupIndex == groupIndex)
return component;
}
snprintf(Buffer, sizeof Buffer, "%d", groupIndex);
pb::ShowMessageBox(SDL_MESSAGEBOX_WARNING, "Table cant find (lh):", Buffer);
return nullptr;
}
int TPinballTable::AddScore(int score)
{
if (JackpotScoreFlag)
{
JackpotScore += score;
const auto jackpotLimit = !pb::FullTiltMode ? 5000000 : 10000000;
if (JackpotScore > jackpotLimit)
JackpotScore = jackpotLimit;
}
if (BonusScoreFlag)
{
BonusScore += score;
if (BonusScore > 5000000)
BonusScore = 5000000;
}
int addedScore = ScoreAdded + score * score_multipliers[ScoreMultiplier];
CurScore += addedScore;
if (CurScore > 1000000000)
{
++CurScoreE9;
CurScore = CurScore - 1000000000;
}
score::set(CurScoreStruct, CurScore);
return addedScore;
}
void TPinballTable::ChangeBallCount(int count)
{
BallCount = count;
if (count <= 0)
{
score::erase(ScoreBallcount, 1);
}
else
{
score::set(ScoreBallcount, MaxBallCount - count + 1);
score::update(ScoreBallcount);
}
}
void TPinballTable::tilt(float time)
{
if (!TiltLockFlag && !BallInDrainFlag)
{
pb::InfoTextBox->Clear();
pb::MissTextBox->Clear();
pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING136), -1.0);
loader::play_sound(SoundIndex3, nullptr, "TPinballTable1");
TiltTimeoutTimer = timer::set(30.0, this, tilt_timeout);
for (auto component : ComponentList)
{
component->Message(MessageCode::SetTiltLock, time);
}
LightGroup->Message(MessageCode::TLightTurnOffTimed, 0);
TiltLockFlag = 1;
control::table_control_handler(MessageCode::SetTiltLock);
}
}
void TPinballTable::port_draw()
{
for (auto component : ComponentList)
{
component->port_draw();
}
}
int TPinballTable::Message(MessageCode code, float value)
{
const char* rc_text;
switch (code)
{
case MessageCode::LeftFlipperInputPressed:
if (!TiltLockFlag)
{
FlipperL->Message(MessageCode::TFlipperExtend, value);
}
break;
case MessageCode::LeftFlipperInputReleased:
if (!TiltLockFlag)
{
FlipperL->Message(MessageCode::TFlipperRetract, value);
}
break;
case MessageCode::RightFlipperInputPressed:
if (!TiltLockFlag)
{
FlipperR->Message(MessageCode::TFlipperExtend, value);
}
break;
case MessageCode::RightFlipperInputReleased:
if (!TiltLockFlag)
{
FlipperR->Message(MessageCode::TFlipperRetract, value);
}
break;
case MessageCode::PlungerInputPressed:
case MessageCode::PlungerInputReleased:
Plunger->Message(code, value);
break;
case MessageCode::Pause:
case MessageCode::Resume:
case MessageCode::LooseFocus:
for (auto component : ComponentList)
{
component->Message(code, value);
}
break;
case MessageCode::ClearTiltLock:
LightGroup->Message(MessageCode::TLightResetTimed, 0.0);
if (TiltLockFlag)
{
TiltLockFlag = 0;
if (TiltTimeoutTimer)
timer::kill(TiltTimeoutTimer);
TiltTimeoutTimer = 0;
}
break;
case MessageCode::StartGamePlayer1:
LightGroup->Message(MessageCode::TLightGroupReset, 0.0);
LightGroup->Message(MessageCode::TLightResetAndTurnOff, 0.0);
Plunger->Message(MessageCode::PlungerStartFeedTimer, 0.0);
if (Demo && Demo->ActiveFlag)
rc_text = pb::get_rc_string(Msg::STRING131);
else
rc_text = pb::get_rc_string(Msg::STRING127);
pb::InfoTextBox->Display(rc_text, -1.0);
if (Demo)
Demo->Message(MessageCode::NewGame, 0.0);
break;
case MessageCode::NewGame:
if (EndGameTimeoutTimer)
{
timer::kill(EndGameTimeoutTimer);
EndGame_timeout(0, this);
pb::mode_change(GameModes::InGame);
}
if (LightShowTimer)
{
timer::kill(LightShowTimer);
LightShowTimer = 0;
Message(MessageCode::StartGamePlayer1, 0.0);
}
else
{
// Some of the control cheats persist across games.
// Was this loose anti-cheat by design?
CheatsUsed = 0;
Message(MessageCode::Reset, 0.0);
auto ball = BallList[0];
ball->Position.Y = 0.0;
ball->Position.X = 0.0;
ball->Position.Z = -0.8f;
auto playerCount = static_cast<int>(floor(value));
PlayerCount = playerCount;
if (playerCount >= 1)
{
if (playerCount > 4)
PlayerCount = 4;
}
else
{
PlayerCount = 1;
}
auto plr1Score = PlayerScores[0].ScoreStruct;
CurrentPlayer = 0;
CurScoreStruct = plr1Score;
CurScore = 0;
score::set(plr1Score, 0);
ScoreMultiplier = 0;
for (int plrIndex = 1; plrIndex < PlayerCount; ++plrIndex)
{
auto scorePtr = &PlayerScores[plrIndex];
score::set(scorePtr->ScoreStruct, 0);
scorePtr->Score = 0;
scorePtr->ScoreE9Part = 0;
scorePtr->BallCount = MaxBallCount;
scorePtr->ExtraBalls = ExtraBalls;
scorePtr->BallLockedCounter = BallLockedCounter;
scorePtr->JackpotScore = JackpotScore;
}
BallCount = MaxBallCount;
ChangeBallCount(BallCount);
score::set(ScorePlayerNumber1, CurrentPlayer + 1);
score::update(ScorePlayerNumber1);
for (auto scoreIndex = 4 - PlayerCount; scoreIndex > 0; scoreIndex--)
{
score::set(PlayerScores[scoreIndex].ScoreStruct, -1);
}
JackpotScoreFlag = false;
BonusScoreFlag = false;
UnknownP71 = 0;
pb::InfoTextBox->Clear();
pb::MissTextBox->Clear();
LightGroup->Message(MessageCode::TLightGroupLightShowAnimation, 0.2f);
auto time = loader::play_sound(SoundIndex1, nullptr, "TPinballTable2");
if (time < 0)
time = 5.0f;
LightShowTimer = timer::set(time, this, LightShow_timeout);
}
if (pb::FullTiltMode)
{
// Multi-ball is FT exclusive feature, at least for now.
MultiballFlag = true;
JackpotScore = 500000;
}
midi::play_track(MidiTracks::Track1, true);
break;
case MessageCode::PlungerRelaunchBall:
if (ReplayTimer)
timer::kill(ReplayTimer);
ReplayTimer = timer::set(floor(value), this, replay_timer_callback);
ReplayActiveFlag = 1;
break;
case MessageCode::SwitchToNextPlayer:
{
if (PlayerCount <= 1)
{
const char* textboxText;
if (Demo->ActiveFlag)
textboxText = pb::get_rc_string(Msg::STRING131);
else
textboxText = pb::get_rc_string(Msg::STRING127);
pb::InfoTextBox->Display(textboxText, -1.0);
break;
}
auto nextPlayer = (CurrentPlayer + 1) % PlayerCount;
auto nextScorePtr = &PlayerScores[nextPlayer];
if (nextScorePtr->BallCount <= 0)
break;
PlayerScores[CurrentPlayer].Score = CurScore;
PlayerScores[CurrentPlayer].ScoreE9Part = CurScoreE9;
PlayerScores[CurrentPlayer].BallCount = BallCount;
PlayerScores[CurrentPlayer].ExtraBalls = ExtraBalls;
PlayerScores[CurrentPlayer].BallLockedCounter = BallLockedCounter;
PlayerScores[CurrentPlayer].JackpotScore = JackpotScore;
CurScore = nextScorePtr->Score;
CurScoreE9 = nextScorePtr->ScoreE9Part;
BallCount = nextScorePtr->BallCount;
ExtraBalls = nextScorePtr->ExtraBalls;
BallLockedCounter = nextScorePtr->BallLockedCounter;
JackpotScore = nextScorePtr->JackpotScore;
CurScoreStruct = nextScorePtr->ScoreStruct;
score::set(CurScoreStruct, CurScore);
CurScoreStruct->DirtyFlag = true;
ChangeBallCount(BallCount);
score::set(ScorePlayerNumber1, nextPlayer + 1);
score::update(ScorePlayerNumber1);
for (auto component : ComponentList)
{
component->Message(MessageCode::PlayerChanged, static_cast<float>(nextPlayer));
}
const char* textboxText = nullptr;
switch (nextPlayer)
{
case 0:
if (Demo->ActiveFlag)
textboxText = pb::get_rc_string(Msg::STRING131);
else
textboxText = pb::get_rc_string(Msg::STRING127);
break;
case 1:
if (Demo->ActiveFlag)
textboxText = pb::get_rc_string(Msg::STRING132);
else
textboxText = pb::get_rc_string(Msg::STRING128);
break;
case 2:
if (Demo->ActiveFlag)
textboxText = pb::get_rc_string(Msg::STRING133);
else
textboxText = pb::get_rc_string(Msg::STRING129);
break;
case 3:
if (Demo->ActiveFlag)
textboxText = pb::get_rc_string(Msg::STRING134);
else
textboxText = pb::get_rc_string(Msg::STRING130);
break;
default:
break;
}
if (textboxText != nullptr)
pb::InfoTextBox->Display(textboxText, -1);
JackpotScoreFlag = false;
BonusScoreFlag = false;
UnknownP71 = 0;
CurrentPlayer = nextPlayer;
}
break;
case MessageCode::GameOver:
loader::play_sound(SoundIndex2, nullptr, "TPinballTable3");
pb::MissTextBox->Clear();
pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING135), -1.0);
EndGameTimeoutTimer = timer::set(3.0, this, EndGame_timeout);
break;
case MessageCode::Reset:
for (auto component : ComponentList)
{
component->Message(MessageCode::Reset, 0);
}
if (ReplayTimer)
timer::kill(ReplayTimer);
ReplayTimer = 0;
if (LightShowTimer)
{
timer::kill(LightShowTimer);
LightGroup->Message(MessageCode::TLightGroupReset, 0.0);
}
LightShowTimer = 0;
ScoreMultiplier = 0;
ScoreAdded = 0;
ReflexShotScore = 0;
BonusScore = 10000;
BonusScoreFlag = false;
JackpotScore = 20000;
JackpotScoreFlag = false;
UnknownP71 = 0;
ExtraBalls = 0;
MultiballCount = 0;
BallLockedCounter = 0;
MultiballFlag = false;
UnknownP78 = 0;
ReplayActiveFlag = 0;
ReplayTimer = 0;
TiltLockFlag = 0;
break;
default: break;
}
control::table_control_handler(code);
return 0;
}
TBall* TPinballTable::AddBall(vector2 position)
{
TBall* ball = nullptr;
for (auto curBall : BallList)
{
if (!curBall->ActiveFlag)
{
ball = curBall;
break;
}
}
if (ball != nullptr)
{
ball->ActiveFlag = 1;
ball->Position.Z = ball->Radius;
ball->Direction = {};
ball->Speed = 0;
ball->TimeDelta = 0;
ball->EdgeCollisionCount = 0;
ball->CollisionFlag = 0;
ball->CollisionMask = 1;
ball->CollisionComp = nullptr;
}
else
{
if (BallList.size() >= 20)
return nullptr;
ball = new TBall(this, -1);
BallList.push_back(ball);
}
ball->Position.X = position.X;
ball->Position.Y = position.Y;
ball->PrevPosition = ball->Position;
ball->StuckCounter = 0;
ball->LastActiveTime = pb::time_ticks;
return ball;
}
int TPinballTable::BallCountInRect(const RectF& rect)
{
int count = 0;
for (const auto ball : BallList)
{
if (ball->ActiveFlag &&
ball->Position.X >= rect.XMin &&
ball->Position.Y >= rect.YMin &&
ball->Position.X <= rect.XMax &&
ball->Position.Y <= rect.YMax)
{
count++;
}
}
return count;
}
int TPinballTable::BallCountInRect(const vector2& pos, float margin)
{
RectF rect{};
rect.XMin = pos.X - margin;
rect.XMax = pos.X + margin;
rect.YMin = pos.Y - margin;
rect.YMax = pos.Y + margin;
return BallCountInRect(rect);
}
void TPinballTable::EndGame_timeout(int timerId, void* caller)
{
auto table = static_cast<TPinballTable*>(caller);
table->EndGameTimeoutTimer = 0;
pb::end_game();
for (auto component : table->ComponentList)
{
component->Message(MessageCode::GameOver, 0);
}
if (table->Demo)
table->Demo->Message(MessageCode::GameOver, 0.0);
control::handler(MessageCode::ControlMissionStarted, pb::MissTextBox);
pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING125), -1.0);
}
void TPinballTable::LightShow_timeout(int timerId, void* caller)
{
auto table = static_cast<TPinballTable*>(caller);
table->LightShowTimer = 0;
table->Message(MessageCode::StartGamePlayer1, 0.0);
}
void TPinballTable::replay_timer_callback(int timerId, void* caller)
{
auto table = static_cast<TPinballTable*>(caller);
table->ReplayActiveFlag = 0;
table->ReplayTimer = 0;
}
void TPinballTable::tilt_timeout(int timerId, void* caller)
{
auto table = static_cast<TPinballTable*>(caller);
vector2 vec{};
table->TiltTimeoutTimer = 0;
if (table->TiltLockFlag)
{
for (auto ball : table->BallList)
{
table->Drain->Collision(ball, &vec, &vec, 0.0, nullptr);
}
}
}

View File

@ -0,0 +1,104 @@
#pragma once
#include "maths.h"
#include "TPinballComponent.h"
class TBall;
struct scoreStruct;
class TFlipper;
class TPlunger;
class TDrain;
class TDemo;
class TLightGroup;
struct score_struct_super
{
scoreStruct* ScoreStruct;
int Score;
int ScoreE9Part;
int JackpotScore;
int BallCount;
int ExtraBalls;
int BallLockedCounter;
};
class TPinballTable : public TPinballComponent
{
public:
TPinballTable();
~TPinballTable() override;
TPinballComponent* find_component(LPCSTR componentName);
TPinballComponent* find_component(int groupIndex);
int AddScore(int score);
void ChangeBallCount(int count);
void tilt(float time);
void port_draw() override;
int Message(MessageCode code, float value) override;
TBall* AddBall(vector2 position);
int BallCountInRect(const RectF& rect);
int BallCountInRect(const vector2& pos, float margin);
static void EndGame_timeout(int timerId, void* caller);
static void LightShow_timeout(int timerId, void* caller);
static void replay_timer_callback(int timerId, void* caller);
static void tilt_timeout(int timerId, void* caller);
TFlipper* FlipperL;
TFlipper* FlipperR;
scoreStruct* CurScoreStruct;
scoreStruct* ScoreBallcount;
scoreStruct* ScorePlayerNumber1;
int CheatsUsed{};
int SoundIndex1{};
int SoundIndex2{};
int SoundIndex3{};
int BallInDrainFlag;
int CurScore{};
int CurScoreE9{};
int LightShowTimer;
int EndGameTimeoutTimer;
int TiltTimeoutTimer;
score_struct_super PlayerScores[4]{};
int PlayerCount;
int CurrentPlayer;
TPlunger* Plunger;
TDrain* Drain;
TDemo* Demo{};
int XOffset{};
int YOffset{};
int Width{};
int Height{};
std::vector<TPinballComponent*> ComponentList;
std::vector<TBall*> BallList;
std::vector<TFlipper*> FlipperList;
TLightGroup* LightGroup;
float GravityDirVectMult{};
float GravityAngleX{};
float GravityAnglY{};
float CollisionCompOffset{};
vector2 PlungerPosition{};
int ScoreMultiplier{};
int ScoreAdded{};
int ReflexShotScore{};
int BonusScore{};
bool BonusScoreFlag{};
int JackpotScore{};
bool JackpotScoreFlag{};
int UnknownP71{};
int BallCount{};
int MaxBallCount;
int ExtraBalls{};
int MultiballCount{};
int BallLockedCounter{};
bool MultiballFlag;
int UnknownP78{};
int ReplayActiveFlag{};
int ReplayTimer;
int UnknownP81{};
int UnknownP82{};
int TiltLockFlag;
private:
static int score_multipliers[5];
};

View File

@ -0,0 +1,199 @@
#include "pch.h"
#include "TPlunger.h"
#include "control.h"
#include "loader.h"
#include "maths.h"
#include "pb.h"
#include "render.h"
#include "TBall.h"
#include "timer.h"
#include "TPinballTable.h"
TPlunger::TPlunger(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
visualStruct visual{};
loader::query_visual(groupIndex, 0, &visual);
Boost = 0.0;
BallFeedTimer_ = 0;
PullbackTimer_ = 0;
SoundIndexP1 = visual.SoundIndex4;
SoundIndexP2 = visual.SoundIndex3;
HardHitSoundId = visual.Kicker.HardHitSoundId;
Threshold = 1000000000.0;
// FT:MaxPullback = 50; 3DPB: MaxPullback = 100, PullbackIncrement is floored.
if (pb::FullTiltMode)
{
MaxPullback = 50;
PullbackIncrement = MaxPullback / (ListBitmap->size() * 8.0f);
}
else
{
MaxPullback = 100;
PullbackIncrement = std::floor(MaxPullback / (ListBitmap->size() * 8.0f));
}
Elasticity = 0.5f;
Smoothness = 0.5f;
PullbackDelay = 0.025f;
float* floatArr = loader::query_float_attribute(groupIndex, 0, 601);
table->PlungerPosition = {floatArr[0], floatArr[1]};
}
void TPlunger::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (PinballTable->TiltLockFlag || SomeCounter > 0)
{
auto boost = RandFloat() * MaxPullback * 0.1f + MaxPullback;
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 0, boost);
if (SomeCounter)
SomeCounter--;
Message(MessageCode::PlungerInputReleased, 0.0);
}
else
{
auto boost = RandFloat() * Boost * 0.1f + Boost;
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, Threshold, boost);
}
}
int TPlunger::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::PlungerInputPressed:
if (!PullbackStartedFlag && (!pb::FullTiltMode || PinballTable->MultiballCount > 0 && !PinballTable->
TiltLockFlag))
{
PullbackStartedFlag = true;
Boost = 0.0;
Threshold = 1000000000.0;
loader::play_sound(HardHitSoundId, this, "TPlunger1");
PullbackTimer(0, this);
}
break;
case MessageCode::PlungerFeedBall:
{
if (PinballTable->BallCountInRect(PinballTable->PlungerPosition, PinballTable->CollisionCompOffset * 1.2f))
{
timer::set(1.0f, this, BallFeedTimer);
}
else
{
auto ball = PinballTable->AddBall(PinballTable->PlungerPosition);
assertm(ball, "Failure to create ball in plunger");
PinballTable->MultiballCount++;
PinballTable->BallInDrainFlag = 0;
pb::tilt_no_more();
}
break;
}
case MessageCode::PlungerStartFeedTimer:
timer::set(0.95999998f, this, BallFeedTimer);
loader::play_sound(SoundIndexP1, this, "TPlunger2");
break;
case MessageCode::PlungerLaunchBall:
PullbackStartedFlag = true;
Boost = MaxPullback;
Message(MessageCode::PlungerInputReleased, 0.0f);
break;
case MessageCode::PlungerRelaunchBall:
SomeCounter++;
timer::set(value, this, BallFeedTimer);
loader::play_sound(SoundIndexP1, this, "TPlunger2_1");
PullbackStartedFlag = true;
PullbackTimer(0, this);
break;
case MessageCode::PlayerChanged:
PullbackStartedFlag = false;
Boost = 0.0f;
Threshold = 1000000000.0f;
SomeCounter = 0;
timer::kill(BallFeedTimer);
timer::kill(PullbackTimer);
timer::kill(ReleasedTimer);
break;
case MessageCode::SetTiltLock:
SomeCounter = 0;
timer::kill(BallFeedTimer);
break;
case MessageCode::PlungerInputReleased:
case MessageCode::Resume:
case MessageCode::LooseFocus:
if (PullbackStartedFlag && !SomeCounter)
{
PullbackStartedFlag = false;
Threshold = 0.0;
if (PullbackTimer_)
timer::kill(PullbackTimer_);
PullbackTimer_ = 0;
loader::play_sound(SoundIndexP2, this, "TPlunger3");
SpriteSet(0);
timer::set(PullbackDelay, this, ReleasedTimer);
}
break;
case MessageCode::Reset:
{
PullbackStartedFlag = false;
Boost = 0.0f;
Threshold = 1000000000.0f;
SomeCounter = 0;
timer::kill(BallFeedTimer);
timer::kill(PullbackTimer);
timer::kill(ReleasedTimer);
SpriteSet(0);
break;
}
default:
break;
}
control::handler(code, this);
return 0;
}
void TPlunger::BallFeedTimer(int timerId, void* caller)
{
auto plunger = static_cast<TPlunger*>(caller);
plunger->Message(MessageCode::PlungerFeedBall, 0.0);
}
void TPlunger::PullbackTimer(int timerId, void* caller)
{
auto plunger = static_cast<TPlunger*>(caller);
plunger->Boost += plunger->PullbackIncrement;
if (plunger->Boost <= plunger->MaxPullback)
{
if (plunger->SomeCounter)
{
plunger->PullbackTimer_ = timer::set(plunger->PullbackDelay / 4.0f, plunger, PullbackTimer);
}
else
{
plunger->PullbackTimer_ = timer::set(plunger->PullbackDelay, plunger, PullbackTimer);
}
}
else
{
plunger->PullbackTimer_ = 0;
plunger->Boost = plunger->MaxPullback;
}
int index = static_cast<int>(floor(
static_cast<float>(plunger->ListBitmap->size() - 1) *
(plunger->Boost / plunger->MaxPullback)));
plunger->SpriteSet(index);
}
void TPlunger::ReleasedTimer(int timerId, void* caller)
{
auto plunger = static_cast<TPlunger*>(caller);
plunger->Threshold = 1000000000.0;
plunger->Boost = 0.0;
}

View File

@ -0,0 +1,27 @@
#pragma once
#include "TCollisionComponent.h"
class TPlunger :
public TCollisionComponent
{
public:
TPlunger(TPinballTable* table, int groupIndex);
~TPlunger() override = default;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
int Message(MessageCode code, float value) override;
static void BallFeedTimer(int timerId, void* caller);
static void PullbackTimer(int timerId, void* caller);
static void ReleasedTimer(int timerId, void* caller);
int PullbackTimer_;
int BallFeedTimer_;
float MaxPullback;
float PullbackIncrement;
float PullbackDelay;
int SoundIndexP1;
int SoundIndexP2;
bool PullbackStartedFlag{};
int SomeCounter{};
};

View File

@ -0,0 +1,88 @@
#include "pch.h"
#include "TPopupTarget.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "timer.h"
#include "TPinballTable.h"
TPopupTarget::TPopupTarget(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
Timer = 0;
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
}
int TPopupTarget::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TPopupTargetDisable:
ActiveFlag = 0;
SpriteSet(-1);
break;
case MessageCode::TPopupTargetEnable:
Timer = timer::set(TimerTime, this, TimerExpired);
break;
case MessageCode::PlayerChanged:
PlayerMessagefieldBackup[PinballTable->CurrentPlayer] = MessageField;
MessageField = PlayerMessagefieldBackup[static_cast<int>(floor(value))];
TPopupTarget::Message(MessageField ? MessageCode::TPopupTargetDisable : MessageCode::TPopupTargetEnable, 0.0);
break;
case MessageCode::Reset:
{
MessageField = 0;
int* playerPtr = PlayerMessagefieldBackup;
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
{
*playerPtr = 0;
++playerPtr;
}
if (Timer)
timer::kill(Timer);
TimerExpired(0, this);
break;
}
default:
break;
}
return 0;
}
void TPopupTarget::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge)
{
if (PinballTable->TiltLockFlag)
{
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
}
else if (maths::basic_collision(
ball,
nextPosition,
direction,
Elasticity,
Smoothness,
Threshold,
Boost) > Threshold)
{
if (HardHitSoundId)
loader::play_sound(HardHitSoundId, this, "TPopupTarget1");
Message(MessageCode::TPopupTargetDisable, 0.0);
control::handler(MessageCode::ControlCollision, this);
}
}
void TPopupTarget::TimerExpired(int timerId, void* caller)
{
auto target = static_cast<TPopupTarget*>(caller);
target->Timer = 0;
target->ActiveFlag = 1;
target->SpriteSet(0);
if (timerId)
{
if (target->SoftHitSoundId)
loader::play_sound(target->SoftHitSoundId, target, "TPopupTarget2");
}
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "TCollisionComponent.h"
class TPopupTarget :
public TCollisionComponent
{
public:
TPopupTarget(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void TimerExpired(int timerId, void* caller);
int Timer;
float TimerTime;
int PlayerMessagefieldBackup[4]{};
};

188
SpaceCadetPinball/TRamp.cpp Normal file
View File

@ -0,0 +1,188 @@
#include "pch.h"
#include "TRamp.h"
#include "control.h"
#include "loader.h"
#include "TBall.h"
#include "TEdgeSegment.h"
#include "TLine.h"
#include "TPinballTable.h"
#include "TTableLayer.h"
TRamp::TRamp(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
visualStruct visual{};
vector2 wall1End{}, wall1Start{}, wall2Start{}, wall2End{};
MessageField = 0;
UnusedBaseFlag = 1;
loader::query_visual(groupIndex, 0, &visual);
CollisionGroup = visual.CollisionGroup;
BallFieldMult = loader::query_float_attribute(groupIndex, 0, 701, 0.2f);
BallZOffsetFlag = static_cast<int>(loader::query_float_attribute(groupIndex, 0, 1305, 0));
auto floatArr3Plane = loader::query_float_attribute(groupIndex, 0, 1300);
RampPlaneCount = static_cast<int>(floor(*floatArr3Plane));
RampPlane = reinterpret_cast<ramp_plane_type*>(floatArr3Plane + 1);
auto wall0Arr = loader::query_float_attribute(groupIndex, 0, 1303);
auto wall0CollisionGroup = 1 << static_cast<int>(floor(wall0Arr[0]));
auto wall0Pts = reinterpret_cast<wall_point_type*>(wall0Arr + 2);
Line1 = new TLine(this, &ActiveFlag, wall0CollisionGroup, wall0Pts->Pt1, wall0Pts->Pt0);
Line1->WallValue = nullptr;
Line1->place_in_grid(&AABB);
EdgeList.push_back(Line1);
auto wall1Arr = loader::query_float_attribute(groupIndex, 0, 1301);
Wall1CollisionGroup = 1 << static_cast<int>(floor(wall1Arr[0]));
auto wall1Enabled = static_cast<int>(floor(wall1Arr[1]));
Wall1BallOffset = wall1Arr[7];
maths::find_closest_edge(
RampPlane,
RampPlaneCount,
reinterpret_cast<wall_point_type*>(wall1Arr + 3),
wall1End,
wall1Start);
Line2 = new TLine(this, &ActiveFlag, CollisionGroup, wall1Start, wall1End);
Line2->WallValue = nullptr;
Line2->place_in_grid(&AABB);
EdgeList.push_back(Line2);
auto wall2Arr = loader::query_float_attribute(groupIndex, 0, 1302);
Wall2CollisionGroup = 1 << static_cast<int>(floor(wall2Arr[0]));
auto wall2Enabled = static_cast<int>(floor(wall2Arr[1]));
Wall2BallOffset = wall2Arr[7];
maths::find_closest_edge(
RampPlane,
RampPlaneCount,
reinterpret_cast<wall_point_type*>(wall2Arr + 3),
wall2End,
wall2Start);
Line3 = new TLine(this, &ActiveFlag, CollisionGroup, wall2Start, wall2End);
Line3->WallValue = nullptr;
Line3->place_in_grid(&AABB);
EdgeList.push_back(Line3);
auto xMin = 1000000000.0f;
auto yMin = 1000000000.0f;
auto yMax = -1000000000.0f;
auto xMax = -1000000000.0f;
for (auto index = 0; index < RampPlaneCount; index++)
{
auto& plane = RampPlane[index];
vector2* pointOrder[4] = { &plane.V1, &plane.V2, &plane.V3, &plane.V1 };
xMin = std::min(std::min(std::min(plane.V3.X, plane.V1.X), plane.V2.X), xMin);
yMin = std::min(std::min(std::min(plane.V3.Y, plane.V1.Y), plane.V2.Y), xMin); // Sic
xMax = std::max(std::max(std::max(plane.V3.X, plane.V1.X), plane.V2.X), xMin);
yMax = std::max(std::max(std::max(plane.V3.Y, plane.V1.Y), plane.V2.Y), xMin);
for (auto pt = 0; pt < 3; pt++)
{
auto& point1 = *pointOrder[pt], point2 = *pointOrder[pt + 1];
auto collisionGroup = 0;
if (point1 == wall1End && point2 == wall1Start)
{
if (wall1Enabled)
collisionGroup = Wall1CollisionGroup;
}
else if (point1 == wall2End && point2 == wall2Start)
{
if (wall2Enabled)
collisionGroup = Wall2CollisionGroup;
}
else
collisionGroup = visual.CollisionGroup;
if (collisionGroup)
{
auto line = new TLine(this, &ActiveFlag, collisionGroup, point1, point2);
line->WallValue = &plane;
line->place_in_grid(&AABB);
EdgeList.push_back(line);
}
}
plane.FieldForce.X = cos(plane.GravityAngle2) * sin(plane.GravityAngle1) *
PinballTable->GravityDirVectMult;
plane.FieldForce.Y = sin(plane.GravityAngle2) * sin(plane.GravityAngle1) *
PinballTable->GravityDirVectMult;
}
Field.ActiveFlag = &ActiveFlag;
Field.CollisionComp = this;
Field.CollisionGroup = visual.CollisionGroup;
auto x1 = xMax;
auto y1 = yMax;
auto x0 = xMin;
auto y0 = yMin;
TTableLayer::edges_insert_square(y0, x0, y1, x1, nullptr, &Field);
}
void TRamp::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
ball->not_again(edge);
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
auto plane = static_cast<ramp_plane_type*>(edge->WallValue);
if (plane)
{
ball->CollisionFlag = 1;
ball->CollisionOffset.X = plane->BallCollisionOffset.X;
ball->CollisionOffset.Y = plane->BallCollisionOffset.Y;
ball->CollisionOffset.Z = plane->BallCollisionOffset.Z;
ball->RampFieldForce.X = plane->FieldForce.X;
ball->RampFieldForce.Y = plane->FieldForce.Y;
ball->Position.Z = ball->Position.X * ball->CollisionOffset.X + ball->Position.Y * ball->CollisionOffset.Y +
ball->Radius + ball->CollisionOffset.Z;
ball->CollisionMask = CollisionGroup;
return;
}
if (edge == Line1)
{
if (!PinballTable->TiltLockFlag)
{
loader::play_sound(SoftHitSoundId, ball, "TRamp");
control::handler(MessageCode::ControlCollision, this);
}
}
else
{
ball->CollisionFlag = 0;
if (edge == Line2)
{
ball->CollisionMask = Wall1CollisionGroup;
if (BallZOffsetFlag)
ball->Position.Z = ball->Radius + Wall1BallOffset;
}
else
{
ball->CollisionMask = Wall2CollisionGroup;
if (BallZOffsetFlag)
ball->Position.Z = ball->Radius + Wall2BallOffset;
}
}
}
int TRamp::FieldEffect(TBall* ball, vector2* vecDst)
{
vecDst->X = ball->RampFieldForce.X - ball->Direction.X * ball->Speed * BallFieldMult;
vecDst->Y = ball->RampFieldForce.Y - ball->Direction.Y * ball->Speed * BallFieldMult;
return 1;
}
void TRamp::port_draw()
{
TCollisionComponent::port_draw();
}

31
SpaceCadetPinball/TRamp.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include "TCollisionComponent.h"
#include "TEdgeManager.h"
struct ramp_plane_type;
class TRamp :
public TCollisionComponent
{
public:
TRamp(TPinballTable* table, int groupIndex);
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
int FieldEffect(TBall* ball, vector2* vecDst) override;
void port_draw() override;
field_effect_type Field{};
int CollisionGroup;
bool BallZOffsetFlag;
int RampPlaneCount;
float BallFieldMult;
ramp_plane_type* RampPlane;
TEdgeSegment* Line2;
TEdgeSegment* Line3;
TEdgeSegment* Line1;
int Wall1CollisionGroup;
int Wall2CollisionGroup;
float Wall1BallOffset;
float Wall2BallOffset;
};

View File

@ -0,0 +1,78 @@
#include "pch.h"
#include "TRollover.h"
#include "control.h"
#include "gdrv.h"
#include "loader.h"
#include "render.h"
#include "TBall.h"
#include "TEdgeSegment.h"
#include "timer.h"
#include "TPinballTable.h"
TRollover::TRollover(TPinballTable* table, int groupIndex, bool createWall) : TCollisionComponent(
table, groupIndex, createWall)
{
}
TRollover::TRollover(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
{
SpriteSet(0);
build_walls(groupIndex);
}
int TRollover::Message(MessageCode code, float value)
{
if (code == MessageCode::Reset)
{
ActiveFlag = 1;
RolloverFlag = 0;
SpriteSet(0);
}
return 0;
}
void TRollover::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge)
{
ball->Position.X = nextPosition->X;
ball->Position.Y = nextPosition->Y;
ball->RayMaxDistance -= distance;
ball->not_again(edge);
if (!PinballTable->TiltLockFlag)
{
if (RolloverFlag)
{
timer::set(0.1f, this, TimerExpired);
ActiveFlag = 0;
}
else
{
loader::play_sound(SoftHitSoundId, ball, "TRollover");
control::handler(MessageCode::ControlCollision, this);
}
RolloverFlag = RolloverFlag == 0;
SpriteSet(RolloverFlag ? -1 : 0);
}
}
void TRollover::build_walls(int groupIndex)
{
visualStruct visual{};
loader::query_visual(groupIndex, 0, &visual);
float* arr1 = loader::query_float_attribute(groupIndex, 0, 600);
TEdgeSegment::install_wall(arr1, this, &ActiveFlag, visual.CollisionGroup, 0.0, 600);
float* arr2 = loader::query_float_attribute(groupIndex, 0, 603);
TEdgeSegment::install_wall(arr2, this, &RolloverFlag, visual.CollisionGroup, 0.0, 603);
}
void TRollover::TimerExpired(int timerId, void* caller)
{
auto roll = static_cast<TRollover*>(caller);
roll->ActiveFlag = 1;
}

View File

@ -0,0 +1,20 @@
#pragma once
#include "TCollisionComponent.h"
class TRollover :
public TCollisionComponent
{
protected:
TRollover(TPinballTable* table, int groupIndex, bool createWall);
public:
TRollover(TPinballTable* table, int groupIndex);
~TRollover() override = default;
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
void build_walls(int groupIndex);
static void TimerExpired(int timerId, void* caller);
char RolloverFlag{};
};

View File

@ -0,0 +1,91 @@
#include "pch.h"
#include "TSink.h"
#include "control.h"
#include "loader.h"
#include "render.h"
#include "TPinballTable.h"
#include "TBall.h"
#include "TDrain.h"
#include "timer.h"
TSink::TSink(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
{
visualStruct visual{};
MessageField = 0;
loader::query_visual(groupIndex, 0, &visual);
BallThrowDirection = visual.Kicker.ThrowBallDirection;
ThrowAngleMult = visual.Kicker.ThrowBallAngleMult;
ThrowSpeedMult1 = visual.Kicker.Boost;
ThrowSpeedMult2 = visual.Kicker.ThrowBallMult * 0.01f;
SoundIndex4 = visual.SoundIndex4;
SoundIndex3 = visual.SoundIndex3;
auto floatArr = loader::query_float_attribute(groupIndex, 0, 601);
BallPosition.X = floatArr[0];
BallPosition.Y = floatArr[1];
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
}
int TSink::Message(MessageCode code, float value)
{
switch (code)
{
case MessageCode::TSinkResetTimer:
if (value < 0.0f)
value = TimerTime;
timer::set(value, this, TimerExpired);
break;
case MessageCode::PlayerChanged:
timer::kill(TimerExpired);
PlayerMessagefieldBackup[PinballTable->CurrentPlayer] = MessageField;
MessageField = PlayerMessagefieldBackup[static_cast<int>(floor(value))];
break;
case MessageCode::Reset:
{
timer::kill(TimerExpired);
MessageField = 0;
for (auto &msgBackup : PlayerMessagefieldBackup)
msgBackup = 0;
break;
}
default:
break;
}
return 0;
}
void TSink::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
{
if (PinballTable->TiltLockFlag)
{
PinballTable->Drain->Collision(ball, nextPosition, direction, distance, edge);
}
else
{
ball->Disable();
loader::play_sound(SoundIndex4, ball, "TSink1");
control::handler(MessageCode::ControlCollision, this);
}
}
void TSink::TimerExpired(int timerId, void* caller)
{
auto sink = static_cast<TSink*>(caller);
auto table = sink->PinballTable;
if (table->BallCountInRect(sink->BallPosition, table->CollisionCompOffset * 2.0f))
{
timer::set(0.5f, sink, TimerExpired);
}
else
{
auto ball = table->AddBall(sink->BallPosition);
assertm(ball, "Failure to create ball in sink");
ball->CollisionDisabledFlag = true;
ball->throw_ball(&sink->BallThrowDirection, sink->ThrowAngleMult, sink->ThrowSpeedMult1,
sink->ThrowSpeedMult2);
if (sink->SoundIndex3)
loader::play_sound(sink->SoundIndex3, ball, "TSink2");
}
}

25
SpaceCadetPinball/TSink.h Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include "maths.h"
#include "TCollisionComponent.h"
class TSink :
public TCollisionComponent
{
public:
TSink(TPinballTable* table, int groupIndex);
int Message(MessageCode code, float value) override;
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
TEdgeSegment* edge) override;
static void TimerExpired(int timerId, void* caller);
float TimerTime;
vector2 BallPosition{};
vector3 BallThrowDirection{};
float ThrowAngleMult;
float ThrowSpeedMult1;
float ThrowSpeedMult2;
int SoundIndex4;
int SoundIndex3;
int PlayerMessagefieldBackup[4]{};
};

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