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.
- 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
# 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 .# 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-
Right-click Baldur's Gate 3 in Steam → Properties
-
Under General → Launch Options, enter the full path to
bg3w.sh:/Users/YOUR_USERNAME/bg3se-macos/scripts/bg3w.sh %command%Important: Replace
YOUR_USERNAMEwith 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%"
| 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 |
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:
- Download the mod's
.pakfile from Nexus Mods - Install by placing it in
~/Documents/Larian Studios/Baldur's Gate 3/Mods/ - Enable the mod using the in-game mod manager (or a mod manager like BG3 Mod Manager)
- 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.
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.
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 | |
| 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 | |
| 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.
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_FireballAll 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.
| 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 |
Three ways to interact with the Lua runtime:
- In-Game Overlay - Press Ctrl+` to toggle
- Socket Console -
./build/bin/bg3se-console - 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.sockbg3se-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
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.
- 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
MIT License
- Tom di Mino (the artist formerly known as Pnutmaster / Nexus)
- Claude Code (Anthropic)
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.
Donations help fund continued development, testing across game updates, and expanding mod compatibility. Every contribution is appreciated!
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 :)
"πολλοὶ μὲν ναρθηκοφόροι, παῦροι δέ τε βάκχοι." — "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
-
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:CreateComponentdispatches through the verified ComponentOps registry;entity:RemoveComponentreturns 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). ↩ -
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 inghidra/offsets/COMPONENT_OPS_AND_PROTO_INIT.md). TreasureTable/TreasureCategory reads, GetStatsLoadedMods, and spell/status prototype sync return real data (Wave 2). ↩