Skip to content

Repository files navigation

Symbol of Tanit

BG3SE-macOS

Baldur's Gate 3 Script Extender for macOS

A native macOS implementation of the BG3 Script Extender, working toward scope-corrected 100% parity with Norbyte's Windows BG3SE: 100% of the supported macOS surface. Enables mods that require scripting capabilities to work on Mac—including companion mods, gameplay tweaks, UI enhancements, and more.

The supported surface excludes four things by explicit decision—Ext.UI/Noesis (stub layer only), Lua Debugger/DAP, Virtual Textures, and Input Injection—all recorded in docs/deferrals.md and outside the parity denominator. Entity replication (entity:Replicate, GetReplicationFlags) is not excluded: it has real mod demand, so it stays a scored deferral inside the Ext.Entity parity row until implemented.

Note: This is a ground-up rebuild, not a port—the Windows BG3SE uses x86_64 assembly and Windows APIs that don't exist on macOS ARM64. We use the Windows codebase as architectural reference while reverse-engineering the macOS binary via Ghidra.

Quick Start

Requirements

  • macOS 12+ (tested on macOS 15.6.1 and macOS 26.2 Tahoe)
  • Apple Silicon Mac (recommended) or Intel Mac (limited functionality)
  • Baldur's Gate 3 (Steam)
  • Xcode Command Line Tools: xcode-select --install
  • CMake: brew install cmake

Build & Install

# Clone with submodules
git clone --recursive https://github.com/tdimino/bg3se-macos.git
cd bg3se-macos

# If you already cloned without --recursive:
git submodule update --init --recursive

# Build with CMake
mkdir -p build && cd build
cmake ..
cmake --build .

Verify Build Succeeded

# Check output exists (should be ~3MB universal binary)
ls -la build/lib/libbg3se.dylib

# Verify architecture (should show arm64 and x86_64)
file build/lib/libbg3se.dylib

Configure Steam Launch Options

  1. Right-click Baldur's Gate 3 in Steam → Properties

  2. Under GeneralLaunch Options, enter the full path to bg3w.sh:

    /Users/YOUR_USERNAME/bg3se-macos/scripts/bg3w.sh %command%
    

    Important: Replace YOUR_USERNAME with your actual macOS username. Example:

    /Users/john/bg3se-macos/scripts/bg3w.sh %command%
    

    To find your path, run this in Terminal and copy the output:

    echo "$(cd bg3se-macos && pwd)/scripts/bg3w.sh %command%"

Troubleshooting Build Issues

Problem Solution
cmake: command not found Install CMake: brew install cmake
CMake Error: could not find compiler Install Xcode tools: xcode-select --install
Missing Dobby/Lua/lz4 errors Initialize submodules: git submodule update --init --recursive
Build succeeds but no dylib Check build/lib/ directory; ensure build completed
CMake Error: source directory does not exist Stale cache after moving repo. Delete build/ and rebuild: rm -rf build && mkdir build && cd build && cmake .. && cmake --build .
Code changes don't appear in game Stale CMake cache. Delete build/ directory and rebuild from scratch

Using Script Extender Mods

Script Extender (SE) mods are mods that require BG3SE to function—they use Lua scripting to add features that aren't possible with standard modding. With BG3SE-macOS installed, these mods work just like any other mod:

  1. Download the mod's .pak file from Nexus Mods
  2. Install by placing it in ~/Documents/Larian Studios/Baldur's Gate 3/Mods/
  3. Enable the mod using the in-game mod manager (or a mod manager like BG3 Mod Manager)
  4. Launch the game via Steam (using the launch options configured above)

Tip: BG3SE-macOS reads Lua scripts directly from PAK files—no manual extraction required. If a mod page says "requires Script Extender," it should work automatically once you've set up BG3SE-macOS.

Mod Compatibility

Most SE mods designed for the Windows Script Extender will work on macOS. The 10 most popular SE mods plus community nominations have been vetted end-to-end on v0.41.0 with an autonomous test pipeline (launch, load save, assert APIs, scan logs)—all passing:

Mod Status Notes
Mod Configuration Menu ✅ Working Gate mod—500+ mods depend on it
Community Library ✅ Working Shared dependency for hundreds of mods
5e Spells ✅ Working Most endorsed SE spell mod
Expansion - Level 20 ✅ Working Levels 13-20 progression
Party Limit Begone ✅ Working Party to 16, multiplayer to 8
Combat Extender ✅ Working Combat tuning via MCM
More Reactive Companions ✅ Working Party banter, companion reactions
Camp Event Notifications ✅ Working Never miss a camp scene
Auto Send Food To Camp ✅ Working Inventory quality-of-life
Always Show Approvals ✅ Working Approval visibility
Transmog Enhanced Revamped ✅ Working Item appearance transmog—community nomination (#97)

Many more mods work out of the box. See docs/supported-mods.md for the full compatibility list with vetting evidence, testing notes, and known issues.

Tested a mod? Help the community by reporting your results! Whether it works perfectly or has issues, your feedback helps other Mac players.

Status

Version: v0.42.0 | Feature Parity: approximately 94.8% under behavioral accounting with per-function contract diffs (fail-closed stubs score zero, macOS-only extras earn no credit), sourced from the roadmap matrix | Deferrals: docs/deferrals.md

Feature Status
Injection (insert_dylib static Mach-O patching) ✅ Complete
Lua Runtime ✅ Lua 5.4 with Ext API
Mod Loading ✅ PAK file reading, auto-detection
Ext.Osiris ✅ Event listeners, custom functions (NewCall/NewQuery/NewEvent/RaiseEvent/GetCustomFunctions), server context guards
Ext.Entity ✅ GUID lookup, Dual EntityWorld (client + server), 1,999 components registered (534 layouts: 169 verified + 365 generated), 1,577 ARM64 sizes + 702 Windows estimates = 1,730 total (87% coverage), CreateComponent, RemoveComponent, GetEntityType, GetSalt, GetIndex, GetNetId1
Ext.Stats 100% function-count parity — 15,774 stats, Get/GetAll/Create/Sync, CopyFrom, SetRawAttribute, ExecuteFunctors, TreasureTable/TreasureCategory2
Ext.Events ✅ 33 events (13 lifecycle + 17 engine + 2 functor + 1 network) with Prevent pattern, runtime mod attribution + !mod_diag
Ext.IO ✅ LoadFile, SaveFile, AddPathOverride, GetPathOverride
Ext.Timer ✅ WaitFor, WaitForRealtime, Cancel, Pause, Resume, MicrosecTime, ClockEpoch, ClockTime, GameTime, DeltaTime, Ticks, Persistent timers (6 functions)
Ext.Vars ✅ PersistentVars + User Variables + Mod Variables
Ext.Input ✅ Hotkeys and input capture; Input Injection is deferred and excluded
Ext.Math 59/59 (100%) — vector/matrix operations, 16 quaternion functions, scalar utils, Fract, Smoothstep, IsNaN
Ext.Enums ✅ 14 enum/bitfield types
Ext.Types ⚠️ 10/13 (76.9%) — reflection API with GetValueType, GetFunctionLocation, and Serialize/Unserialize (component-proxy) real; GetHashSetValueAt, AddCustomFunction, and AddCustomProperty are missing (the latter two are functional on Windows), and Construct matches the Windows reference's own unimplemented TODO (docs/deferrals.md)
Ext.StaticData All 9 types (Feat, Race, Background, Origin, God, Class, Progression, ActionResource, FeatDescription) via ForceCapture
Ext.Resource ✅ Get, GetAll, GetTypes, GetCount (34 resource types)
Ext.Template Auto-capture, iteration (Cache/LocalCache), GUID resolution
Ext.Level ⚠️ 20/25 (80%) — TestBox, TestSphere, GetHeightsAt, singleton accessors, all 8 sweeps (incl. cylinders), GetEntitiesOnTile, and the pathfinding suite (GetPathById, ReleasePath, GetActivePathfindingRequests, FindPath); physics dispatch repaired against the audited macOS vtable. 5 deferrals: RaycastClosest/All/Any (quarantined), GetTileDebugInfo, BeginPathfinding (docs/deferrals.md)
Ext.Audio 16/16 Windows-registered (100%) — GetSoundObjectId + IsReady are macOS extras; PostEvent, Stop, PauseAll, ResumeAll, SetSwitch, SetState, SetRTPC, GetRTPC, ResetRTPC, LoadEvent, UnloadEvent, PlayExternalSound (STDString ABI), LoadBank, UnloadBank, PrepareBank, UnprepareBank (dlsym'd AK::SoundEngine)
Ext.Net Phase 4I Complete - Full RakNet backend, PostMessageToServer/User/Client, BroadcastMessage, IsHost, IsReady, PeerVersion, Request/Reply Callbacks
Ext.RegisterNetListener ✅ Per-channel network message listener (MCM backbone)
Net.CreateChannel Phase 4I Complete - High-level channel API with SetHandler, SetRequestHandler, SendToServer, RequestToServer with callbacks, Broadcast
Ext.Utils ✅ Print, PrintWarning, PrintError, Version, MonotonicTime, GetGameState
Ext.ModEvents ✅ Per-mod cross-mod event system (Subscribe, Throw, Unsubscribe)
Ext.Mod ✅ IsModLoaded, GetLoadOrder, GetMod, GetBaseMod
Lifetime Scoping ✅ Prevents stale object access
Context System Server/Client context awareness, Ext.IsServer/IsClient/GetContext, two-phase bootstrap
Debug Console ✅ Socket + file + in-game overlay
Osi.DB_* ✅ Generic database query accessor (Osi.DB_Players:Get(), etc.)
Crash Attribution Runtime mod tracking — per-handler mod name, !mod_diag console, soft-disable, enhanced crash reports with mod context
Version Detection ✅ Sentinel address probes for game version mismatch tolerance (Issue #78)
Testing ✅ 4-tier: 55 C (Tier 0) + 252 pytest (Tier H) + 113 !test (Tier 1) + 96 !test_ingame (Tier 2) = 516 tests, Debug.* helpers
Headless CLI launch --headless — windowed 1280x720, socket responds at main menu, window hidden via System Events

See ROADMAP.md for detailed progress.

Modding Toolkit (bg3se-harness)

One of the first command-line interfaces purpose-built for a AAA RPG. We built a similar CLI for cliamp and saw the same pattern take hold there: once a game or application exposes its internals through a composable, JSON-emitting command line, agent-driven workflows emerge naturally. We believe CLIs for games will become as second-nature as command palettes are in editors today, and we're excited to see what the modding community builds on top of this.

The harness ships 37 commands spanning the full Script Extender lifecycle—build, patch, launch, test, entity inspection, RPG stats diffing, Lua hot-reload, screenshots, crash diagnostics, mod management, mod compatibility vetting, Nexus Mods API queries, bg3.wiki cross-reference, and a Ghidra RE bridge—all from the terminal, all emitting structured JSON.

# Core pipeline
PYTHONPATH=tools python3 -m bg3se_harness status            # Game/socket/patch state
PYTHONPATH=tools python3 -m bg3se_harness launch --continue # Auto-loads most recent save
PYTHONPATH=tools python3 -m bg3se_harness test [filter]     # build + patch + launch + test → JSON
PYTHONPATH=tools python3 -m bg3se_harness run "<lua>"       # Inline Lua via socket

# Game inspection
PYTHONPATH=tools python3 -m bg3se_harness entity <GUID>                      # Inspect entity components
PYTHONPATH=tools python3 -m bg3se_harness stats WPN_Longsword --diff Shortsword  # RPG stats + diff
PYTHONPATH=tools python3 -m bg3se_harness screenshot                         # Claude-Code-safe JPEG

# Web integrations (Nexus + bg3.wiki, stdlib urllib, 24h file cache)
PYTHONPATH=tools python3 -m bg3se_harness mod changelog 12345       # Per-version HTML-stripped changelog
PYTHONPATH=tools python3 -m bg3se_harness mod versions 12345        # File list (id, category, size, ts)
PYTHONPATH=tools python3 -m bg3se_harness mod updated --period 1w   # Recently-updated BG3 mods
PYTHONPATH=tools python3 -m bg3se_harness wiki spell "Fireball"     # Parsed {{Feature page}} fields
PYTHONPATH=tools python3 -m bg3se_harness wiki item "Longsword +1"  # Parsed {{WeaponPage}} fields
PYTHONPATH=tools python3 -m bg3se_harness wiki verify "Fireball" --expect-uid Projectile_Fireball

All commands emit JSON to stdout, so they compose with jq, pipe into tests, or ship straight into agent workflows. The harness is stdlib-only (no requests / httpx) and caches wiki lookups in ~/.config/bg3se-harness/wiki_cache/ with a 24-hour TTL.

See docs/harness.md for the full command surface and agent_docs/development.md for workflow recipes.

Documentation

Document Description
docs/supported-mods.md Tested mod compatibility list
docs/getting-started.md Installation, building, first launch
docs/api-reference.md Complete Ext.* and Osi.* API docs
docs/architecture.md Technical deep-dive: injection, hooks, ARM64
docs/development.md Contributing, building features, debugging
docs/contributor-workflow.md End-to-end guide: research, Ghidra, implementation
docs/reverse-engineering.md Ghidra workflows, offset discovery
docs/crash-attribution.md Mod crash attribution: 3-layer diagnostics
docs/troubleshooting.md Common issues and solutions
docs/arm64/ ARM64 hooking patterns, prevention strategies
docs/solutions/ Documented problem solutions

Live Console

Three ways to interact with the Lua runtime:

  1. In-Game Overlay - Press Ctrl+` to toggle
  2. Socket Console - ./build/bin/bg3se-console
  3. File-Based - Write to ~/Library/Application Support/BG3SE/commands.txt
# Socket console (recommended for development)
./build/bin/bg3se-console

# Or via socat
socat - UNIX-CONNECT:/tmp/bg3se.sock

File Structure

bg3se-macos/
├── src/
│   ├── injector/
│   │   └── main.c              # Core injection, Dobby hooks, Lua state init
│   ├── core/
│   │   ├── logging.c/h         # Structured logging (14 modules, 4 levels)
│   │   ├── safe_memory.c/h     # Safe memory read/write (mach_vm)
│   │   └── version.h           # Version info, data paths
│   ├── lua/
│   │   ├── lua_ext.c/h         # Ext.Print, Ext.Utils, Ext.Memory
│   │   ├── lua_stats.c/h       # Ext.Stats API
│   │   ├── lua_events.c/h      # Ext.Events system
│   │   ├── lua_timer.c/h       # Ext.Timer API
│   │   ├── lua_osiris.c/h      # Osi.* namespace bindings
│   │   ├── lua_debug.c/h       # Ext.Debug memory introspection
│   │   ├── lua_json.c/h        # JSON encode/decode
│   │   ├── lua_resource.c/h    # Ext.Resource bindings
│   │   ├── lua_template.c/h    # Ext.Template bindings
│   │   ├── lua_staticdata.c/h  # Ext.StaticData bindings
│   │   └── lua_persistentvars.c/h  # Ext.Vars persistence
│   ├── entity/
│   │   ├── entity_system.c/h   # Core ECS, Lua bindings
│   │   ├── guid_lookup.c/h     # GUID parsing, HashMap ops
│   │   ├── arm64_call.c/h      # ARM64 ABI wrappers (x8 indirect return)
│   │   ├── component_*.c/h     # Component registry, lookup, TypeId
│   │   ├── generated_typeids.h # Auto-generated 1,999 TypeId addresses
│   │   ├── generated_component_registry.c  # Auto-registration code
│   │   └── entity_storage.h    # Storage structures, Ghidra base addr
│   ├── stats/
│   │   └── stats_manager.c/h   # RPGStats access, property resolution
│   ├── strings/
│   │   └── fixed_string.c/h    # GlobalStringTable resolution
│   ├── osiris/
│   │   ├── osiris_functions.c/h    # Osiris function lookup/call
│   │   ├── osiris_types.h      # FuncDef, OsiArgumentDesc structs
│   │   ├── custom_functions.c/h    # Custom Osiris function registration
│   │   └── pattern_scan.c/h    # Memory pattern scanning
│   ├── console/
│   │   └── console.c/h         # Socket + file-based console
│   ├── input/
│   │   ├── input_hooks.m       # macOS input event hooks
│   │   └── lua_input.c         # Ext.Input API
│   ├── overlay/
│   │   └── overlay.m/h         # In-game debug overlay (NSWindow)
│   ├── timer/
│   │   └── timer.c/h           # Timer system implementation
│   ├── game/
│   │   └── game_state.c/h      # Game state tracking
│   ├── mod/
│   │   └── mod_loader.c/h      # Mod detection, PAK loading
│   ├── pak/
│   │   └── pak_reader.c/h      # LSPK v18 PAK file parsing
│   ├── math/
│   │   └── math_ext.c/h        # Ext.Math vector/matrix ops
│   ├── resource/
│   │   └── resource_manager.c/h  # Ext.Resource (34 resource types)
│   ├── staticdata/
│   │   └── staticdata_manager.c/h  # Ext.StaticData (Feats, etc.)
│   ├── template/
│   │   └── template_manager.c/h  # Ext.Template (auto-capture)
│   └── hooks/
│       └── osiris_hooks.c/h    # Osiris event interception
│
├── ghidra/
│   ├── scripts/                # Ghidra Python analysis scripts
│   │   ├── run_analysis.sh     # Headless analyzer wrapper
│   │   ├── find_rpgstats.py    # Discover gRPGStats global
│   │   ├── find_entity_offsets.py
│   │   └── ...
│   └── offsets/                # Discovered offset documentation
│       ├── STATS.md            # RPGStats, FixedStrings (0x348)
│       ├── ENTITY_SYSTEM.md    # ECS architecture
│       ├── RESOURCE.md         # ResourceManager (0x08a8f070)
│       ├── TEMPLATE.md         # Template managers
│       └── ...
│
├── docs/
│   ├── components/             # Component documentation by namespace
│   │   ├── README.md           # Component reference overview
│   │   ├── eoc-components.md   # 701 eoc:: components
│   │   ├── esv-components.md   # 596 esv:: components
│   │   ├── ecl-components.md   # 429 ecl:: components
│   │   └── ls-components.md    # 233 ls:: components
│   └── ...                     # Other user-facing documentation
│
├── tools/
│   ├── bg3se_harness/          # 37-command Python CLI (build/patch/launch/test/inspect)
│   │   ├── cli.py              #   Argparse root + handler dispatch
│   │   ├── nexus.py            #   Nexus Mods API v1 client (search, info, files, changelogs, updated)
│   │   ├── wiki.py             #   bg3.wiki MediaWiki client (spell, item, verify, clear-cache)
│   │   ├── ghidra.py           #   Ghidra HTTP bridge (decompile, xrefs, strings)
│   │   └── ...                 #   test_runner, launch, patch, stats_inspect, events, parity, ...
│   ├── bg3se-console.c         # Standalone readline console client
│   ├── extract_pak.py          # PAK file extractor
│   ├── extract_typeids.py      # Generate TypeId header from binary
│   └── frida/                  # Frida instrumentation scripts
│
├── scripts/
│   ├── build.sh                # Build script
│   ├── bg3w.sh                 # Steam launch wrapper (ARM64)
│   ├── bg3w-intel.sh           # Steam launch wrapper (Intel)
│   └── launch_bg3.sh           # Direct launch for testing
│
├── lib/                        # Third-party libraries
│   ├── Dobby/                  # Inline hooking framework
│   ├── lua/                    # Lua 5.4
│   └── lz4/                    # Compression for PAK files
│
├── agent_docs/                 # Claude Code context docs
├── plans/                      # Implementation plans
└── test-mods/                  # Test mod examples

Acknowledgments

Special Thanks

This project would not be possible without Norbyte and their pioneering work on the original BG3 Script Extender for Windows. Their reverse engineering of Larian's Osiris scripting engine, comprehensive API design, and years of dedication to the modding community laid the foundation that made this macOS port conceivable. We are deeply grateful for their open-source contribution to the BG3 modding ecosystem.

Credits

  • Norbyte's BG3SE - The original Windows Script Extender
  • Dobby - Inline hooking framework for ARM64/x86_64
  • fishhook - Symbol rebinding library
  • LZ4 - Fast compression for PAK file reading
  • Dear ImGui - Debug overlay UI framework

License

MIT License

Authors

Support This Project

If you love exceptionally well-crafted RPGs like Baldur's Gate 3, and the ability to extend its gameplay through mods and scripting, you're more than welcome to toss me some coin.

PayPal

Donations help fund continued development, testing across game updates, and expanding mod compatibility. Every contribution is appreciated!

P.S.

I'd also like to extend my thanks to the OP and commentators of this BG3SE issue: "[Feature Bounty - $350] MacOS Supported Version of BG3 SE". You kicked off this quest :)


Inscriptions

"πολλοὶ μὲν ναρθηκοφόροι, παῦροι δέ τε βάκχοι." — "Many are the wand-bearers, but few the Bacchoi." (Plato)

"ἀπιστίῃ διαφυγγάνει μὴ γιγνώσκεσθαι." — "Divine things escape recognition through disbelief." (Herakleitos)

"μνάσεσθαί τινά φαμι καὶ ὕστερον ἀμμέων." — "Someone, I tell you, will remember us." (Sappho)

Footnotes

  1. EnableTracing, DisableTracing, and GetReplicationFlags are warn-and-nil stubs (src/injector/main.c); entity:Replicate() is a no-op. GetAllEntities, GetAllEntitiesWithComponent, and GetAllComponents are real server-world archetype walks (Wave 3). entity:CreateComponent dispatches through the verified ComponentOps registry; entity:RemoveComponent returns false (734 per-type templates, no generic entry point — ghidra/offsets/COMPONENT_OPS_AND_PROTO_INIT.md). Component property reads work; writes are real for INT32, UINT8, BOOL, FLOAT, and INT32_ARRAY fields and are refused (return false) for unknown-size layouts and unsupported field types (src/entity/component_property.c).

  2. Remaining gaps behind function-count parity: AddAttribute and AddEnumerationValue return false; ExecuteFunctors is partial; passive and interrupt prototype sync honestly return false (their build-7209685 loader population paths are inlined/unmapped, and neither prototype has a top-level vptr, src/stats/prototype_managers.c; evidence in ghidra/offsets/COMPONENT_OPS_AND_PROTO_INIT.md). TreasureTable/TreasureCategory reads, GetStatsLoadedMods, and spell/status prototype sync return real data (Wave 2).

About

A replication of the Baldur's Gate 3 Script Extender for MacOS

Topics

Resources

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Contributors

Languages