github BlackBookOfficial/ue4ss-linux-palworld v1.0.0-palworld-linux

one month ago

UE4SS v1.0.0-palworld-linux — Linux Native Build

🐛 Bug Fixes

  • Fix ProcessLocalScriptFunction hot path: avoid vector allocation on every call

    • Root cause: The script_hook callback called get_object_names(Stack.Node())
    • on EVERY ProcessLocalScriptFunction call, which:
      1. Creates a std::vector (heap allocation)
      1. Traverses the Outer chain (multiple pointer dereferences)
      1. Destroys the vector (heap deallocation)
    • During player login, thousands of script function calls occur.
    • The per-call heap allocation overhead caused the loading screen to freeze.
    • Fix: Add a fast pre-check using FName::Equals (single int comparison)
    • against the first name part of each registered hook BEFORE calling
    • get_object_names. If no hook's first name matches, skip the expensive
    • vector creation entirely.
    • With 4 registered hooks, the pre-check does 4 int comparisons per call
    • (nanoseconds) instead of a vector allocation + Outer traversal (microseconds).
    • Result: Player can connect with AdminCommands hooks active.
    • !help and !announce commands work correctly.
    • Server stable at 118 FPS.
  • fix: prevent crashes in limited mode on Linux

    • Fix busy-wait in UnrealInitializer (add sleep_for to object construction wait)
    • On Linux, skip KismetStringLibrary lookup when GUObjectArray has < 1000 elements
    • Skip PostInitialize (required objects, hooks) when GUObjectArray is not fully populated
    • Defer C++ mod start until after setup_unreal() on Linux (PalSentinel needs UE addresses)
    • Skip ObjectDumper::init() in limited mode
    • Start event loop in limited mode so server keeps running
    • Use >= 1000 element threshold consistently for limited mode checks

🔧 Refactoring & Optimization

  • Cleanup: remove all temporary debug logging, convert to proper Output::send
    • Removed all fprintf(stderr, ...) debug statements that were added during
    • the reverse-engineering phase. Converted important diagnostic messages to
    • Output::send() (Unreal library) or UE4SS_DBG/UE4SS_ERR (UE4SS library).
    • Files cleaned:
    • UnrealInitializer.cpp: 26 fprintf → Output::send
    • TypeChecker.cpp: 13 fprintf → removed (temporary diagnostics)
    • NameTypes.cpp: 5 fprintf → removed (temporary diagnostics)
    • NameTypes.hpp: 2 fprintf → removed (temporary diagnostics)
    • FMemory.cpp: 2 fprintf → removed (temporary diagnostics)
    • UObjectGlobals.cpp: 1 fprintf → removed (temporary diagnostic)
    • UE4SSProgram.cpp: 3 fprintf → UE4SS_DBG
    • main_linux.cpp: 4 fprintf → UE4SS_ERR/UE4SS_DBG, removed verbose SET/CLEAR
    • Zero fprintf(stderr) calls remain in the codebase.

📝 Other Changes

  • CI: fix first-release changelog NameError (fmt defined only under prev_tag)

  • CI: auto-release on v tags, package tarball artifact*

    • Release assets now include ue4ss-linux-palworld--Game__Dev__Linux64.tar.gz
    • (stripped libUE4SS.so + BUILD_INFO.txt) alongside the raw .so. Pushing a v*
    • tag or running the workflow manually both publish a GitHub Release.
  • CI: drop clang matrix jobs (vendored glaze breaks on clang C++23 std::expected)

  • Linux: fix clang build (const_cast in find-or-add scan); README: full rewrite for server owners, mod developers and contributors

  • README: remove previous fork owner's donation section

  • README: fix account capitalization in links

  • README: repoint to ue4ss-linux-palworld, add Palworld status and build-from-source sections

  • Docs: update-resilience layers (AOB, vtable sweep, validation gate)

  • Linux: self-healing AActor vtable sweep for Palworld updates

    • At boot, scans the executable for the RemoveTickPrerequisiteComponent
    • adjustor thunk (AOB), finds every AActor-family vtable referencing it,
    • locates each vtable's base via the Itanium header (offset-to-top=0 +
    • non-code typeinfo slot + code first slot), and derives the BeginPlay/EndPlay
    • slot offsets by consensus (505 vtables; adapters unanimous, BeginPlay 52%
    • un-overridden).
    • Replaces the hardcoded offsets only when the geometry is unanimous and the
    • candidate values dominate; otherwise keeps fallbacks. Verified against the
    • current binary: independently re-derives 0x388/0x390 (BeginPlay=0x9f778f0,
    • EndPlay=0x9f64320), matching the manually verified values.
    • Note: vtables do not hold the raw ProcessEvent address (slot 0x268 holds a
    • shared wrapper), so PE cannot serve as an anchor; and m_modules_info's
    • SizeOfImage covers only the first LOAD segment, so mappings are enumerated
    • from /proc/self/maps instead.
  • Linux: hook-target validation gate for vtable-resolved detours

    • Disassembles each resolved hook target before installing its detour and
    • checks register usage against the detour signature. Refuses only the two
    • crash classes proven against the Palworld binary: junk targets (no sane
    • prologue) and float/int register-class truncation (the Tick@0x2F8 crash
    • class). All other mismatches install anyway with a loud NOTE log, since
    • verified-working hooks legitimately read extra argument registers that the
    • trampoline passes through (forwarding wrappers, Pocketpair signature drift).
    • After a Palworld update shuffles vtable slots, hooks either keep working or
    • fail loudly in UE4SS.log instead of corrupting the server mid-session.
  • Docs: all hooks verified working — CFBNWA and UObjectProcessEvent confirmed in-game

  • Docs: rewrite Palworld Linux status — verified hook table, vtable offsets, landmines

    • Also drops the short-lived PLSF/CFBNWA force-off guard: those detours power
    • the general script-hook path mods rely on, and PLSF proved working after the
    • thunk-resolution fix. CFBNWA and UObjectProcessEvent remain off pending retest.
  • Linux: correct AGameModeBase::InitGameState vtable slot to 0x740 for Palworld

    • Verified across six GameMode-family vtables in the shipping binary:
    • slot 0x740 consistently holds the base implementation, and the single
    • overriding class chains into it with rdi only (void(AGameModeBase*)).
    • The baked 0x738 slot holds the upstream neighbor instead.
    • Note UEngine's region is NOT shifted the way AActor's is: GameEngine::Tick
    • remains at baked 0x2F0 (30h-stable hook), and the blanket +8 shift was
    • previously reverted for mis-slotting it. Per-entry verification only.
  • Linux: thunk-aware JMP resolution and verified Palworld vtable overrides

    • ASMHelper::resolve_function_address_from_potential_jmp now scans up to 8
    • instructions and follows a JMP that appears before any RET, handling
    • Itanium this-adjustment thunks in vtable slots (e.g. AActor BeginPlay).
    • Palworld vtable overrides are now per-entry and individually verified
    • against the shipping binary (a blanket +8 shift was reverted after it
    • mis-slotted UEngine::Tick): UObject ProcessEvent region (+8, stub at
    • 0x260), AActor BeginPlay 0x380->0x388, EndPlay 0x388->0x390. The baked
    • offsets pointed at tick-prerequisite adapter thunks (3-arg); hooking
    • them as BeginPlay/EndPlay corrupted call arguments and crashed the
    • server (pals freezing, then SIGSEGV in the GUObjectArray accessor).
    • Also: HookLocalPlayerExec ini default now matches the header/template
    • default (true).
  • Linux: replicate engine _N suffix parsing for FName Number field

    • The engine's find-or-add splits a trailing _N suffix and stores Number =
    • suffix + 1 (Number 0 = no suffix, ToString prints Number - 1). Replicate
    • this so constructed FNames compare equal to engine-created names such as
    • inventory item StaticIDs (BountyProof_1 = base BountyProof, Number 2).
    • Verified against live server: FName('BountyProof_1') round-trips and
    • matches game items; storing the raw suffix (Number 1) does not.
    • Also: log once if the find-or-add signature scan fails (game update broke
    • kWrapperSig) instead of silently returning None for every FName, and
    • simplify suffix parsing to a bounded 9-digit accumulate (fixes a latent
    • overflow-check bug for 10+ digit suffixes).
    • Keep the Lua FName API identical to Windows UE4SS (string/int overloads
    • only); the integer second argument remains EFindName. Explicit numbers are
    • expressible via the string suffix, so the extra overload is removed.
  • Linux: fix FProperty GetMinAlignment vtable offset (+8 shift)

    • Palworld's UE5.1 build has the same extra virtual slot pattern in the
    • FProperty vtable as in UObject. An extra slot between InstanceSubobjects
    • (0x140) and GetMinAlignment (0x148) shifts all subsequent virtuals by +8.
    • Evidence (runtime vtable dump):
    • vtable[0x148] = ret;int3 (empty stub) — should be GetMinAlignment
    • vtable[0x150] = real function returning alignment values (4, 8, etc.)
    • vtable[0x140] = InstanceSubobjects (at standard offset, no shift)
    • Without this fix, GetMinAlignment calls the wrong vtable entry, returning
    • a garbage value (function pointer interpreted as int). When AddZeroed
    • uses this garbage as the alignment, it crashes with SIGFPE in
    • DefaultCalculateSlackGrow (division by zero from corrupted parameters).
    • Overridden offsets:
    • GetMinAlignment: 0x148 → 0x150
    • ContainsObjectReference: 0x150 → 0x158
    • EmitReferenceInfo: 0x158 → 0x160
    • SameType: 0x160 → 0x168
    • Gated to Palworld binary via /proc/self/exe detection.
  • Docs: add Linux port audit classifying all changes

    • 15 generic upstream fixes, 2 Palworld-specific workarounds (both gated),
    • 0 temporary debug code remaining.
  • Linux: fix Lua SIGABRT by using longjmp instead of C++ exceptions

    • Root cause: libsteam_api.so exports __cxa_throw and __gxx_personality_v0,
    • which intercept C++ exceptions thrown by Lua's error handling (luaD_throw)
    • and call abort() instead of letting pcall catch the error.
    • When AdminCommands' Json.lua library calls require('lpeg') and lpeg is not
    • installed, Lua throws a 'module not found' error via luaD_throw. On Linux,
    • this calls __cxa_throw, which resolves to the libsteam_api.so version that
    • calls abort(), crashing the server with SIGABRT.
    • On Windows, this doesn't happen because:
      1. Lua uses setjmp/longjmp on Windows (MSVC doesn't have the C++ exception path)
      1. Steamworks on Windows doesn't export __cxa_throw
    • The fix: define LUA_USE_LONGJMP when building LuaRaw on Linux with
    • LUA_COMPILE_AS_CPP=ON. This makes Lua use _longjmp/_setjmp instead of
    • C++ throw/catch, bypassing the Steamworks crash reporter entirely.
    • This is a build-time configuration change, not a code change. Lua's
    • functionality is identical — only the error propagation mechanism differs.
    • Evidence (from GDB backtrace):
    • #4 abort()
    • #5 libsteam_api.so::__cxa_throw
    • #6 libsteam_api.so::__gxx_personality_v0
    • #10 luaD_throw (errcode=2)
    • #13 luaL_error('module not found:%s')
    • #14 findloader(name='lpeg')
    • #15 ll_require
    • Results:
    • AdminCommands mod loads successfully
    • All admin commands registered (unfreeze, goto, etc.)
    • Game hooks registered (EnterChat_Receive, PalPlayerController, etc.)
    • Server stable at 118 FPS
    • No SIGABRT
  • Audit: gate Palworld vtable override to game binary, add GetFunctionCallspace/CallRemoteFunction

    • Audit findings for the two architectural fixes:
      1. bit_cast_mfp:
    • Single definition in UnrealVirtualBaseVC.hpp
    • All 4 IMPLEMENT_UNREAL_VIRTUAL_WRAPPER macros route through it
    • No duplicate implementations found
    • Fix covers all virtual function calls
      1. Vtable offset override:
    • Evidence confirms the +8 shift is Palworld-specific, NOT engine-wide
    • Other UObject virtuals (PostLoad 0xA0, BeginDestroy 0xB0, FinishDestroy 0xC0)
    • are at standard offsets — only ProcessEvent region is shifted
    • The extra slot at 0x260 is an empty stub (ret; int3), indicating a
    • Pocketpair-added virtual function in UObject
    • Now gated to Palworld binary detection via /proc/self/exe
    • Added GetFunctionCallspace (0x270) and CallRemoteFunction (0x278)
    • which were also shifted but not yet overridden
    • Added docs/ITANIUM_ABI_NOTES.md explaining both bugs and why they only
    • manifested on Linux.
  • Linux: restore Default__Object lookup and ProcessEvent hook initialization

    • Three root causes were preventing ProcessEvent from being resolved:
      1. bit_cast_mfp uninitialized 'this' adjustment field:
    • On the Itanium ABI (Linux), a pointer-to-member-function is 16 bytes
    • (function ptr + this_adjustment). The union-based bit_cast_mfp only
    • initialized 8 bytes, leaving this_adjustment as garbage, corrupting
    • the 'this' pointer and causing SIGSEGV in every virtual function call.
    • Fixed by zero-initializing the full PMF before memcpy.
      1. Palworld UObject vtable offset shift:
    • Palworld's UE5.1 build has an extra virtual slot (Itanium ABI), shifting
    • ProcessEvent from 0x260 to 0x268 and ProcessConsoleExec from 0x278 to 0x280.
    • The standard UE5.1 layout pointed to no-op stubs, causing Conv_NameToString
    • to return empty strings. Fixed by overriding the vtable offsets at runtime
    • after set_virtual_offsets().
      1. Default__Object lookup was hardcoded to nullptr:
    • The lookup was disabled due to assumed stale-pointer crashes during
    • ForEachUObject iteration. In reality, GetFullName() was returning empty
    • strings (due to #2), so the string comparison never matched. With #1 and
    • #2 fixed, GetFullName() returns correct paths and the lookup succeeds.
    • Results:
    • Default__Object found (index 1629, CDO of UObject)
    • Default__Struct found (CDO of UStruct)
    • ProcessEvent address resolved (0x7b4c080)
    • ProcessConsoleExec, UStruct::Link, LoadMap, InitGameState, BeginPlay,
    • EndPlay, GameEngine::Tick hooks all resolved and installed
    • GameEngine, GameMode, Actor CDO lookups re-enabled
    • Server stable at 115-119 FPS with all hooks active
    • UE4SSStatus mod executes successfully
    • Conv_NameToString returns correct strings for all FName values tested

Full commit history: initial...v1.0.0-palworld-linux


Build Configuration

  • Build Type: Game__Dev__Linux64
  • GUI: Enabled (GLFW3/OpenGL3)
  • Input: Disabled
  • Profilers: Disabled
  • Compiler: GCC (Ubuntu 24.04)

Artifacts

  • libUE4SS.so — Stripped shared library
  • BUILD_INFO.txt — Build metadata

Usage

LD_PRELOAD=./libUE4SS.so ./YourGameBinary

Compatibility

  • Built on Ubuntu 24.04 (x86_64)
  • Requires: libGL, libEGL, libX11, libXrandr, libXinerama, libXcursor, libXi
  • Engine Tick Hook: Auto-detected via dlsym (or manual override in UE4SS_Addresses.ini)

Don't miss a new ue4ss-linux-palworld release

NewReleases is sending notifications on new releases.