Starfield Script Extender (SFSE)
repository·master·Indexed 43 days ago
https://github.com/ianpatt/sfseA plugin and modding framework for the Steam version of Starfield. Includes the sfse_loader utility for injecting the SFSE DLL into the game process and integrates the Xbyak C++ header-only library for dynamic x86 and x64 assembly generation.
What's inside SFSE
- Xbyak is a C++ header-only library that allows for the dynamic assembly of x86 (IA32) and x64 (AMD64, x86-64) mnemonics. It uses an Intel/MASM-like syntax and provides full support for AVX-512 instructions.
Enable AutoGrow mode for dynamic memory expansion
masterBy default,
CodeGeneratorthrows an exception if the generated code exceeds the initialmaxSize. InAutoGrowmode, the library automatically reallocates and expands the memory.Important Requirements:
- You must call
ready()before callinggetCode()to resolve jump instruction addresses. - In
AutoGrowmode, 64-bit relative addressing[rip]is not supported. - If you need to maintain a pointer to a specific offset within the code, store the offset using
getSize()before callingready(), then access it viagetCode() + offsetafterready()returns.
struct Code : Xbyak::CodeGenerator { Code() : Xbyak::CodeGenerator(<default memory size>, Xbyak::AutoGrow) { // assembly here } }; Code c; c.ready(); // Mandatory call- You must call
Check SFSE runtime compatibility
masterSFSE is compatible with the latest version of Starfield available on Steam.
Note: The MS Store/Gamepass version of Starfield is not supported.
Use Xbyak::CodeGenerator to generate JIT code
masterTo generate machine code, inherit from
Xbyak::CodeGenerator. Inside the class methods, write x86/x64 assembly using a syntax similar to NASM. After calling the method, usegetCode()to retrieve the generated code and cast it to a function pointer for execution.By default, assembly errors are reported via exceptions.
struct MyCode : Xbyak::CodeGenerator { MyCode() : CodeGenerator(4096) {} void generate() { mov(eax, 123); ret(); } }; // Usage MyCode c; c.generate(); using FuncType = void (*)(); FuncType f = (FuncType)c.getCode(); f();Build SFSE from source
masterTo build SFSE, clone the repository and use CMake to configure and build the project. The build process generates a Visual Studio solution file located at
sfse/build/umbrella.sln.git clone https://github.com/ianpatt/sfse cmake -B sfse/build -S sfse cmake --build sfse/build --config ReleaseInstall Xbyak
masterXbyak is a header-only C++ library for x86 (IA-32) and x64 (AMD64, x86-64) JIT assembly. To use it, include
xbyak.handxbyak_bin2hex.hin your project. Ensure they are in the same directory or added to your include path.On Linux, you can use
make installto copy the headers to/usr/local/include/xbyak.# On Linux make installConfigure compiler for logical operator names
masterBy default, Xbyak uses names likeand_(),or_(), etc., to avoid conflicts with C++ operator names. If you prefer to use the standardand(),or(), etc., you must specify the-fno-operator-namesoption when using GCC or Clang.How the SFSE loader works
masterThe
sfse_loaderis a utility designed to launch a game runtime (likeStarfield.exeorCreationKit.exe) in a suspended state, inject the SFSE DLL, and then resume the process. This allows the SFSE plugin to initialize before the game's main logic begins.Key Workflow:
- Initialization: Reads command-line arguments and initializes logging to
%USERPROFILE%\Documents\My Games\<Folder>\SFSE\Logs\sfse_loader.txt. - Runtime Identification: Locates the game executable. It can use a custom name from an
.iniconfiguration via theRuntimeNamekey or an alternate path via command-line arguments. - DLL Selection: Determines the correct SFSE DLL name (e.g.,
sfse_Starfield.dll) based on the game version and suffix identified viaIdentifyEXE. - Validation: Verifies the DLL exists, checks its signature, and ensures the
runtimeVersionembedded in the DLL resource matches the game's version. - Process Launch: Starts the game process using
CREATE_SUSPENDED. - Injection: Uses
InjectDLLThreadto inject the validated DLL into the suspended process. - Resumption: Resumes the primary thread to start the game.
- Initialization: Reads command-line arguments and initializes logging to
Use Xbyak in Exception-free mode
masterIf you want to compile with
-fno-exceptions(using gcc/clang), defineXBYAK_NO_EXCEPTION. In this mode, errors are reported viaXbyak::GetError()instead of exceptions.If
Xbyak::GetError()returns a non-zero value, an error occurred. You must manually reset the error state usingXbyak::ClearError(). Note thatCodeGenerator::reset()callsClearError()automatically.// Define this during compilation // -DXBYAK_NO_EXCEPTION if (Xbyak::GetError() != 0) { // Handle error Xbyak::ClearError(); }Configure memory protection modes
masterBy default,
CodeGeneratorsets the JIT region to read/write/execute (RWX). If you want to follow a stricter security model (e.g., generating in RW and then switching to RX for execution), use the following methods:- Initialize with
Xbyak::DontUseProtectto keep the region RW during generation. - Call
setProtectModeRE()to switch to read/execute mode before running the code. - Call
setProtectModeRW()to return to read/write mode.
For
AutoGrowmode, usereadyRE()instead ofready()to switch to read/execute mode.struct Code : Xbyak::CodeGenerator { Code() : CodeGenerator(4096, Xbyak::DontUseProtect) { mov(eax, 123); ret(); } }; Code c; c.setProtectModeRE(); // Switch to read/exec- Initialize with
Configure the SFSE loader via .ini
masterThe loader can be customized using an
.inifile. One primary configuration option isRuntimeName, which allows you to specify a custom executable name if you are not using the default (e.g.,Starfield.exe).If you specify a
RuntimeNamethat does not match the actual executable found in the directory, the loader will fail with an error. To revert to default behavior, remove theRuntimeNameline from your.inifile.Reference: Xbyak Labels and Jumps
masterLabels can be defined using strings or the
Labelclass.String-based Labels:
- Define:
L("label_name"); - Jump:
jmp("label_name"); - For long jumps (where relative address exceeds 8 bits), use
T_NEAR:jmp("label_name", T_NEAR); - Use
hasUndefinedLabel()to check if a jump target exists.
Label Class:
- Define:
Label label1; L(label1); - Jump:
jmp(label1); - Assign:
assignL(dstLabel, srcLabel);(assigns the address ofsrcLabeltodstLabel). - Get Address:
label.getAddress();(returns 0 if undefined).
Local Labels: Wrap code in
inLocalLabel()andoutLocalLabel()to allow duplicate label names in different scopes.- Define: