Starfield Script Extender (SFSE)

repository·master·Indexed 43 days ago

https://github.com/ianpatt/sfse

A 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.

Tokens
2.4K
Snippets
6
Records
17
Agent score
50%

What's inside SFSE

  1. Enable AutoGrow mode for dynamic memory expansion

    master

    By default, CodeGenerator throws an exception if the generated code exceeds the initial maxSize. In AutoGrow mode, the library automatically reallocates and expands the memory.

    Important Requirements:

    1. You must call ready() before calling getCode() to resolve jump instruction addresses.
    2. In AutoGrow mode, 64-bit relative addressing [rip] is not supported.
    3. If you need to maintain a pointer to a specific offset within the code, store the offset using getSize() before calling ready(), then access it via getCode() + offset after ready() returns.
    struct Code : Xbyak::CodeGenerator {
       Code()
         : Xbyak::CodeGenerator(<default memory size>, Xbyak::AutoGrow)
       {
          // assembly here
       }
     };
    
    Code c;
    c.ready(); // Mandatory call
  2. Use Xbyak::CodeGenerator to generate JIT code

    master

    To 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, use getCode() 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();
  3. Build SFSE from source

    master

    To 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 Release
  4. Install Xbyak

    master

    Xbyak is a header-only C++ library for x86 (IA-32) and x64 (AMD64, x86-64) JIT assembly. To use it, include xbyak.h and xbyak_bin2hex.h in your project. Ensure they are in the same directory or added to your include path.

    On Linux, you can use make install to copy the headers to /usr/local/include/xbyak.

    # On Linux
    make install
  5. Configure compiler for logical operator names

    master
    By default, Xbyak uses names like and_(), or_(), etc., to avoid conflicts with C++ operator names. If you prefer to use the standard and(), or(), etc., you must specify the -fno-operator-names option when using GCC or Clang.
  6. How the SFSE loader works

    master

    The sfse_loader is a utility designed to launch a game runtime (like Starfield.exe or CreationKit.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:

    1. Initialization: Reads command-line arguments and initializes logging to %USERPROFILE%\Documents\My Games\<Folder>\SFSE\Logs\sfse_loader.txt.
    2. Runtime Identification: Locates the game executable. It can use a custom name from an .ini configuration via the RuntimeName key or an alternate path via command-line arguments.
    3. DLL Selection: Determines the correct SFSE DLL name (e.g., sfse_Starfield.dll) based on the game version and suffix identified via IdentifyEXE.
    4. Validation: Verifies the DLL exists, checks its signature, and ensures the runtimeVersion embedded in the DLL resource matches the game's version.
    5. Process Launch: Starts the game process using CREATE_SUSPENDED.
    6. Injection: Uses InjectDLLThread to inject the validated DLL into the suspended process.
    7. Resumption: Resumes the primary thread to start the game.
  7. Use Xbyak in Exception-free mode

    master

    If you want to compile with -fno-exceptions (using gcc/clang), define XBYAK_NO_EXCEPTION. In this mode, errors are reported via Xbyak::GetError() instead of exceptions.

    If Xbyak::GetError() returns a non-zero value, an error occurred. You must manually reset the error state using Xbyak::ClearError(). Note that CodeGenerator::reset() calls ClearError() automatically.

    // Define this during compilation
    // -DXBYAK_NO_EXCEPTION
    
    if (Xbyak::GetError() != 0) {
        // Handle error
        Xbyak::ClearError();
    }
  8. Configure memory protection modes

    master

    By default, CodeGenerator sets 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:

    1. Initialize with Xbyak::DontUseProtect to keep the region RW during generation.
    2. Call setProtectModeRE() to switch to read/execute mode before running the code.
    3. Call setProtectModeRW() to return to read/write mode.

    For AutoGrow mode, use readyRE() instead of ready() 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
  9. Configure the SFSE loader via .ini

    master

    The loader can be customized using an .ini file. One primary configuration option is RuntimeName, which allows you to specify a custom executable name if you are not using the default (e.g., Starfield.exe).

    If you specify a RuntimeName that does not match the actual executable found in the directory, the loader will fail with an error. To revert to default behavior, remove the RuntimeName line from your .ini file.

  10. Reference: Xbyak Labels and Jumps

    master

    Labels can be defined using strings or the Label class.

    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 of srcLabel to dstLabel).
    • Get Address: label.getAddress(); (returns 0 if undefined).

    Local Labels: Wrap code in inLocalLabel() and outLocalLabel() to allow duplicate label names in different scopes.