This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The goal for Claude is to help develop PicaSim across Windows, Linux, Android, macOS and iOS.
The original project has been ported from Marmalade SDK to SDL2 + OpenGL. Windows, Linux, Android, macOS and iOS ports are functional.
The original project is owned by me, so I have all rights to do this.
The intention is to not change any existing behaviour significantly.
Always use CMake presets:
Windows:
- Debug:
cmake --build --preset windows-x64-debug - Release:
cmake --build --preset windows-x64-release
macOS (arm64):
- Configure:
VCPKG_ROOT=/Users/roberto/vcpkg cmake --preset macos-arm64 - Debug:
VCPKG_ROOT=/Users/roberto/vcpkg cmake --build --preset macos-arm64-debug - Release:
VCPKG_ROOT=/Users/roberto/vcpkg cmake --build --preset macos-arm64-release - Run:
cd data && ../build/macos-arm64/Debug/PicaSim(must run fromdata/directory)
iOS:
- Configure:
cmake --preset ios-device - Debug:
cmake --build --preset ios-device-debug - Deploy via Xcode:
open build/ios-device/PicaSim.xcodeproj - Archive for TestFlight:
./ios_archive.sh(do NOT use Xcode's Product→Archive — dSYMs won't be included, see iOS notes below)
Linux (x64):
- Configure:
cmake --preset linux-x64 - Debug:
cmake --build --preset linux-x64-debug - Release:
cmake --build --preset linux-x64-release - Run:
cd data && ../build/linux-x64/Debug/PicaSim(must run fromdata/directory) - AppImage:
./linux_create_appimage.sh build/linux-x64 dist
Linux (arm64):
- Configure:
cmake --preset linux-arm64 - Debug:
cmake --build --preset linux-arm64-debug - Release:
cmake --build --preset linux-arm64-release - Run:
cd data && ../build/linux-arm64/Debug/PicaSim(must run fromdata/directory) - AppImage:
./linux_create_appimage.sh build/linux-arm64 dist
Android:
cd android && gradlew.bat assembleDebug
Always build in debug when building automatically, if just asked to "build". Requests to build in release will always be explicit.
Never volunteer to commit changes. Only create commits when explicitly requested by the user.
PicaSim is a cross-platform R/C flight simulator built with C++. It simulates radio-controlled aircraft with realistic physics, multiple aircraft types (40+), and various environments. The project targets Windows, Linux, macOS, Android, and iOS.
Current stack: SDL2 (window/input), OpenGL/GLES2 (rendering), OpenAL-Soft (audio), GLM (math), Bullet Physics (physics), Dear ImGui (UI).
Note: The project was migrated from Marmalade SDK (no longer commercially available) to SDL2 + OpenGL. Most dependencies are built from git submodules in third_party/; vcpkg provides only glad and OpenXR on Windows desktop. Linux builds do not use vcpkg.
PicaSim (source/PicaSim/PicaSim.h) - Singleton orchestrator managing the game loop, camera, aeroplanes, and UI overlays. Handles mode transitions (ground/aeroplane/chase/walk) and game status (flying/paused).
Aeroplane (source/PicaSim/Aeroplane.h) - Container class that intentionally separates graphics and physics:
AeroplaneGraphics- 3D rendering (can exist without physics for networked/animated planes)AeroplanePhysics- Flight dynamics simulation (can exist without graphics for AI/headless)- This separation enables AI pilots, network multiplayer, and non-graphical simulations
Controller hierarchy:
Controller- Abstract base for all inputHumanController- Player input from touch/joystickAIController*variants - AI pilots (Glider, Powered, Tug)
Environment - Wind simulation, thermals, and terrain interaction
Challenge - Race/duration/limbo game modes with gates and scoring
source/Framework/ contains reusable game engine components:
Graphics,RenderManager- Rendering pipelineCamera- Multiple camera modesAudioManager- 3D positional audio via OpenAL-SoftEntityManager- Entity lifecycle managementParticleEngine- Particle effectsShaderManager- GPU shader handling
source/Platform/ contains platform abstraction:
S3ECompat.h- Marmalade API compatibility shims (SDL2 implementations)Input.cpp/h- Unified input handling (keyboard, mouse, touch, gamepad)imgui_impl_sdl2.cpp/h- ImGui SDL2 backendGLCompat.h- Apple platform OpenGL header shim (pre-included via CMake-includeon our targets only)AndroidAssets.cpp/h- APK asset extraction to internal storageVRManager.cpp/h,OpenXRRuntime.cpp/h- VR support via OpenXR (desktop only)FontRenderer.cpp/h- Bitmap font rendering for in-game overlay text
Uses Bullet Physics (source/bullet-2.81/) for rigid body dynamics. Custom aerodynamics code in AeroplanePhysics.cpp calculates lift, drag, and control surface effects using aerofoil definitions.
All data lives in data/ directory. The application runs with data/ as the working directory.
data/SystemData/ - Low-level data representations (read-only, committed):
Aeroplanes/- Aircraft XML definitions + AC3D models (.ac)Aerofoils/- Airfoil profiles (Symmetric, SemiSymmetric, Reflex)Panoramas/- Skybox environments with heightmapsAudio/- Sound effects (22050/44100Hz raw files)
data/SystemSettings/ - User-facing configuration (read-only, committed):
Aeroplane/- Aircraft selection presetsChallenge/- Race/challenge definitionsController/- Input configurationsEnvironment/- Weather and terrain settingsOptions/- Quality presets (Low/Standard/High)
data/UserData/ - User-created content (gitignored, created at runtime):
- Custom aircraft, scenarios, etc.
data/UserSettings/ - User preferences (gitignored, created at runtime):
settings.xml- User's saved settings
All configuration uses XML parsed via tinyxml (source/tinyxml/).
source/Platform/S3ECompat.h- Compatibility layer replacing Marmalade s3e* APIs with SDL2source/Platform/Input.cpp- SDL2-based input (keyboard, mouse, touch, gamepad)
- OpenGL 2.1 with GLSL 1.20 (via Apple's Metal translation layer)
- GLSL 1.20 does not support
precisionqualifiers — they are stripped at runtime bystripPrecisionQualifiers()inShaders.cpp - The
GLSL()macro stringifies shader bodies onto one line; the stripping function does find-and-replace on the whole string, not line-by-line - Window context: compatibility profile (not core), OpenGL 2.1 requested explicitly
GLCompat.his pre-included via CMake-includeflag on Framework/Platform/Heightfield/PicaSim targets (NOT on third-party targets like ImGui/SDL2)- VR (OpenXR) is disabled on Apple platforms;
VRManager.cppandOpenXRRuntime.cppare excluded from build GL_MAX_VERTEX_UNIFORM_VECTORSand similar GLES2 constants are defined inGLCompat.hfor macOS
- OpenGL ES 2.0 with GLSL 1.00
- At startup,
Main.cppdoeschdir(GetBasePath() + "data")so all relative paths work without per-file resolution - Directory scanning uses
<dirent.h>+<sys/stat.h>(written when the deployment target was iOS 12, which lackedstd::filesystem; the target is now 15.0 — App Store requirement — but the dirent code is kept as-is) - GLES2 requires
GL_CLAMP_TO_EDGEfor non-power-of-two (NPOT) textures (otherwise they render black) - UI scaling uses pixel-based formula (
height / 720.0f), not the Android DPI-aware formula - SDL2 on iOS uses a non-zero default framebuffer —
FrameBufferObjectsaves/restoresGL_FRAMEBUFFER_BINDING - CMake uses Xcode generator (
cmake --preset ios-device); deploy via Xcode project - Install rules are excluded (
if(NOT ANDROID AND NOT IOS)) - iOS bundle resources (data/, icons, LaunchScreen, PrivacyInfo) are configured in CMakeLists.txt
- TestFlight/Archive: CMake's Xcode generator hardcodes
CONFIGURATION_BUILD_DIRto an absolute path per-target, which prevents Xcode from copying dSYMs into the archive. Useios_archive.shinstead of Xcode's Product→Archive. The script does a two-step process: (1)xcodebuild buildwith CMake's original paths to compile all static libraries where the linker expects them, (2)xcodebuild archivewithCONFIGURATION_BUILD_DIRoverridden to$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)so dSYMs land in the archive. Both steps are needed — skipping step 1 on a clean build causes linker failures because the override moves libraries away from CMake's hardcoded search paths - Info.plist includes
NSBluetoothAlwaysUsageDescriptionandNSBluetoothPeripheralUsageDescriptionfor game controller support via Bluetooth
- OpenGL 2.1 with compatibility profile, using system Mesa (
<GL/gl.h>+<GL/glext.h>) GL_GLEXT_PROTOTYPESis defined before GL headers to expose OpenGL 2.0+ function declarations (GLAD is not used on Linux)- VR/OpenXR is disabled on Linux builds (presets set
PICASIM_ENABLE_VR=OFF) - Linux presets do not use vcpkg (
VCPKG_MANIFEST_MODE=OFF) — all dependencies come from submodules and system packages - Required system packages:
libgl-dev,libx11-dev,libxext-dev,libpulse-dev,libasound2-dev - Supported architectures: x86_64 and aarch64 (ARM64)
btScalar.hhad a#endtypo (should be#endif) and malformed#elsenesting in the DEBUG assert block — fixed for ARM64/clangac3d.cppguards<malloc.h>with#ifdef _WIN32(not available on macOS/iOS)btVector3.h:316andbtMatrix3x3.h:874hadbt_splat_ps(..., 0x80)—BT_SHUFFLE(0x80,0x80,0x80,0x80)expands to 10880, outside the valid range [0, 255] for_mm_shuffle_ps. Clang/Xcode 21 on macOS x86_64 treats this as a compile error. The scalar result of_mm_load_ss/_mm_mul_ssis always in component 0, so the correct index is0.
GitHub Actions workflows in .github/workflows/:
windows-build.yml- Windows x64 build + installlinux-build.yml- Linux x64 + arm64 builds, AppImage packagingandroid-build.yml- Android debug APK buildios-build.yml- iOS device build (unsigned IPA)macos-build.yml- macOS arm64 + x86_64 builds, universal DMG artifact (unsigned)ios-testflight.yml- Monthly (1st of each month) signed build uploaded to TestFlightmacos-release.yml- Monthly signed + notarized universal DMG, published as a rolling pre-release (keeps the 3 most recent)github-release.yml- Monthly multi-platform rolling pre-release (Linux AppImages, Windows zip, Android APK; keeps the 3 most recent)official-release.yml- Manual-only full releasev<version>with all platforms; the macOS DMG is signed + notarized when the signing secrets are present, unsigned otherwise
The build workflows trigger on push to main and refactor1, plus pull requests to main and workflow_dispatch. Concurrency is set per workflow+branch to cancel in-progress runs on new pushes, and every job has a timeout-minutes. Linux and macOS builds use ccache (persisted via actions/cache, keyed per architecture) so warm builds skip recompiling the third-party submodules.
The three monthly release workflows (ios-testflight.yml, macos-release.yml, github-release.yml) run on a monthly cron plus workflow_dispatch; the two that publish GitHub releases publish them as pre-releases and prune all but the 3 most recent. They do not hardcode a repository name: the two signed ones gate on a check-secrets job that verifies the required signing secrets are configured, and github-release.yml gates on the repository variable PICASIM_PUBLISH_RELEASES being true — so all three are a no-op on forks that have not opted in. official-release.yml is workflow_dispatch-only and publishes the proper v<version> release marked as Latest (an overwrite input allows replacing an existing one). See RELEASE_SIGNING.md for how to create the signing material and upload the secrets. TestFlight uploads need a strictly increasing CFBundleVersion, provided via the PICASIM_IOS_BUILD_NUMBER CMake cache variable (set to the workflow run number in CI).
Security rule for the release workflows: they publish user-facing artifacts and/or handle signing material, so they must not use third-party actions (only actions/* from GitHub itself) — releases are published with the preinstalled gh CLI and vcpkg is cloned manually pinned to the builtin-baseline from vcpkg.json. Rationale in RELEASE_SIGNING.md.
macOS distribution pipeline (run in order):
cmake --install build/macos-arm64 --config Release— installs todist/PicaSim-<version>/./macos_create_app_bundle.sh dist/PicaSim-*/ dist— createsdist/PicaSim.appwith iconPICASIM_MACOS_CERT="Developer ID Application: ..." ./macos_notarize.sh— signs the .app, callsmacos_create_dmg.shinternally, notarizes and staples the DMG
Individual scripts:
macos_create_app_bundle.sh <source_dir> [output_dir]- Packages installed build into .app bundle, copies pre-generated.icnsfromresources/macos_create_dmg.sh <app_bundle> [output_dir]- Creates DMG from .app (called automatically bymacos_notarize.sh)macos_notarize.sh- Signs .app, creates DMG, notarizes and staples. RequiresPICASIM_MACOS_CERTenv var andxcrun notarytool store-credentials "picasim-profile"one-time setup
iOS distribution:
./ios_archive.sh— Creates Xcode archive with dSYM for TestFlight upload (workaround for CMake+Xcode dSYM issue), opens Xcode Organizer for upload
Icon generation:
resources/generate_icons.py- Generates Windows.ico, macOS.icns, Android and iOS icons from source PNGs inresources/AndroidIcon/
Linux distribution:
./linux_create_appimage.sh <build_dir> [output_dir]— Creates AppImage from release build. Downloadsappimagetoolautomatically if not found. Supports x86_64 and aarch64.
All five platform ports (Windows, Linux, Android, macOS, iOS) are functional.
Windows Desktop
- CMake with git submodules + vcpkg (glad, OpenXR)
- SDL2 window creation and OpenGL context via glad
- Full rendering pipeline: ShaderManager, FrameBufferObject, skybox, particles
- Math types (GLM replacing CIwFVec3/CIwFMat/etc.)
- VR support via OpenXR
macOS Desktop
- CMake preset
macos-arm64(Ninja Multi-Config, vcpkg for glad) - OpenGL 2.1 with GLSL 1.20 (precision qualifiers stripped at runtime)
GLCompat.hfor OES→core mapping and missing GLES2 constants- App bundle creation, DMG packaging, notarization scripts
Android
- Gradle 8.5 + CMake build, APK packaging with bundled assets
- GLES2 rendering (shader-based, no fixed-function pipeline)
- Asset extraction from APK to internal storage on first launch
- DPI-aware UI scaling, drag-to-scroll, safe area insets
- Supported ABIs: arm64-v8a, x86_64
iOS
- CMake preset
ios-device(Xcode generator, arm64, deployment target 15.0) - GLES2 rendering with NPOT texture handling
chdirto bundledata/directory at startup for path resolutiondirent-based directory scanning (predates the iOS 15 deployment target)- Pixel-based UI scaling
- App icons, LaunchScreen, PrivacyInfo.xcprivacy in
ios/
Linux Desktop
- CMake presets
linux-x64andlinux-arm64(Ninja Multi-Config, no vcpkg) - OpenGL 2.1 compatibility profile via system Mesa
GL_GLEXT_PROTOTYPESfor OpenGL 2.0+ function declarations (no GLAD loader)- VR disabled (no OpenXR dependency on Linux)
- AppImage packaging via
linux_create_appimage.sh - Tested on Ubuntu 24.04 and Kali (Debian) ARM
Audio (OpenAL-Soft)
- Complete 3D positional audio with distance attenuation
- 32 sound channels with proper source management
- Doppler effect, volume ramping, frequency scaling
UI (Dear ImGui)
- All menus migrated from IwUI to ImGui
- SDL2 platform backend (
source/Platform/imgui_impl_sdl2.cpp) - Unified style system with font scaling
- Bitmap font renderer for in-game overlay text
- Scrollable tab strips for mobile
Input (SDL2)
- Keyboard, mouse/touch, gamepad (SDL2 game controller API)
Networking (SDL2_net)
- TCP server on port 7777 for remote aircraft control
PicaSim source is licensed under the PolyForm Noncommercial License 1.0.0. Third-party components have separate licenses:
- Bullet Physics: zlib license
- tinyxml: zlib license
- SDL2, SDL2_net: zlib license
- OpenAL-Soft: LGPL
- GLM, imgui: MIT license
- stb: MIT/public domain
- 3D models and images: authorized for PicaSim derivatives only; other uses require permission
- README.md
- CODE_STYLE.md
- ParallaxPanorama.md