UE4SS v1.0.2-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:
-
- Creates a std::vector (heap allocation)
-
- Traverses the Outer chain (multiple pointer dereferences)
-
- 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
-
Release packaging: full drop-in tree like upstream
- The tarball now ships libUE4SS.so + UE4SS-settings.ini (Palworld-verified
- hooks incl. HookUObjectProcessEvent/ProcessConsoleExec; AActorTick off until
- soak-tested) + MemberVariableLayout.ini + the bundled upstream Mods set +
- INSTALL.txt quickstart. New users drop the tree next to PalServer.sh and
- set LD_PRELOAD — no reading docs required.
-
Linux: Lua error safety and thread safety hardening
- Error handling: LuaMadeSimple gains Lua::call_function_report, a noexcept
- variant that returns the error as a value; LuaMod::process_delayed_actions
- uses it — unprotected mod errors log with traceback instead of crashing
- (previous throw could not travel the EH machinery in a process whose exe
- vendors a different C++ runtime; proven via gdb across four configurations).
- Thread safety: every Lua entry channel now holds the process-wide recursive
- m_thread_actions_mutex — game-thread detour/script-hook callbacks,
- on_program_start callback lambdas, executor drains, the update_async loop,
- start_mod. Mod states share one global_State via lua_newthread; unguarded
- execute_hook/process_delayed_actions raced it. Zero FPS impact measured.
- Also restores LUA_USE_LONGJMP with corrected rationale and adds tests/lua-mods
- regression fixtures (ErrorStorm, LuaStress).
-
Docs: note production-untested paths (headless GUI, C++ dlopen mods, ProcessInternal)
-
README: condense CI details into one line in contributor section
-
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:
-
- Lua uses setjmp/longjmp on Windows (MSVC doesn't have the C++ exception path)
-
- 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:
-
- 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
-
- 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:
-
- 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.
-
- 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().
-
- 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.2-palworld-linux
Build Configuration
- Build Type: Game__Dev__Linux64
- GUI: Enabled (GLFW3/OpenGL3)
- Input: Disabled
- Profilers: Disabled
- Compiler: GCC (Ubuntu 24.04)
Artifacts
ue4ss-linux-palworld-<tag>.tar.gz— Full drop-in: library, settings, Mods folder, configlibUE4SS.so— Stripped shared library only (advanced users)BUILD_INFO.txt— Build metadata
Usage
LD_PRELOAD=./libUE4SS.so ./YourGameBinaryCompatibility
- 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)