Created
March 20, 2026 00:14
-
-
Save Kazyyk/bf03371f75ae457d4e9a96ae63aecee4 to your computer and use it in GitHub Desktop.
Hide Satisfactory Experimental Watermark Banner by Patching Game Version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # patch_gameversion.ps1 | |
| # Patches UFGVersionFunctionLibrary::GetGameVersion() to return GV_Main (0) | |
| # This hides the "EXPERIMENTAL" banner in the UI | |
| param( | |
| [switch]$Restore | |
| ) | |
| $gameDll = "C:\Program Files (x86)\Steam\steamapps\common\Satisfactory\FactoryGame\Binaries\Win64\FactoryGameSteam-FactoryGame-Win64-Shipping.dll" | |
| $backupDll = "$gameDll.bak" | |
| $pdbDir = "C:\Program Files (x86)\Steam\steamapps\common\Satisfactory\FactoryGame\Binaries\Win64" | |
| if ($Restore) { | |
| if (Test-Path $backupDll) { | |
| Copy-Item $backupDll $gameDll -Force | |
| Write-Host "Restored original DLL from backup." | |
| } else { | |
| Write-Host "No backup found." | |
| } | |
| exit 0 | |
| } | |
| Add-Type -TypeDefinition @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| public class DbgHelp4 { | |
| [DllImport("dbghelp.dll", SetLastError = true)] | |
| public static extern uint SymSetOptions(uint SymOptions); | |
| [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)] | |
| public static extern bool SymInitializeW(IntPtr hProcess, string UserSearchPath, bool fInvadeProcess); | |
| [DllImport("dbghelp.dll", SetLastError = true)] | |
| public static extern bool SymCleanup(IntPtr hProcess); | |
| [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)] | |
| public static extern ulong SymLoadModuleExW(IntPtr hProcess, IntPtr hFile, string ImageName, string ModuleName, ulong BaseOfDll, uint DllSize, IntPtr Data, uint Flags); | |
| [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)] | |
| public static extern bool SymFromNameW(IntPtr hProcess, string Name, IntPtr Symbol); | |
| } | |
| "@ | |
| [DbgHelp4]::SymSetOptions(0x00000002 -bor 0x00000004) | Out-Null | |
| $hProcess = [IntPtr]::new(0xDEAD) | |
| [DbgHelp4]::SymInitializeW($hProcess, $pdbDir, $false) | Out-Null | |
| $dllSize = [uint32](Get-Item $gameDll).Length | |
| $baseAddr = [DbgHelp4]::SymLoadModuleExW($hProcess, [IntPtr]::Zero, $gameDll, $null, [uint64]0x10000000, $dllSize, [IntPtr]::Zero, 0) | |
| Write-Host "Module loaded at base 0x$($baseAddr.ToString('X'))" | |
| # Look up GetGameVersion | |
| $buf = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(4200) | |
| for ($z = 0; $z -lt 256; $z += 4) { | |
| [System.Runtime.InteropServices.Marshal]::WriteInt32($buf, $z, 0) | |
| } | |
| [System.Runtime.InteropServices.Marshal]::WriteInt32($buf, 0, 88) | |
| [System.Runtime.InteropServices.Marshal]::WriteInt32($buf, 80, 2000) | |
| $found = [DbgHelp4]::SymFromNameW($hProcess, "UFGVersionFunctionLibrary::GetGameVersion", $buf) | |
| if (-not $found) { | |
| Write-Error "Symbol not found!" | |
| [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buf) | |
| [DbgHelp4]::SymCleanup($hProcess) | Out-Null | |
| exit 1 | |
| } | |
| $symAddr = [uint64][System.Runtime.InteropServices.Marshal]::ReadInt64($buf, 56) | |
| $symSize = [System.Runtime.InteropServices.Marshal]::ReadInt32($buf, 28) | |
| [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buf) | |
| [DbgHelp4]::SymCleanup($hProcess) | Out-Null | |
| $rva = [uint64]($symAddr - $baseAddr) | |
| Write-Host "GetGameVersion at RVA: 0x$($rva.ToString('X')) ($symSize bytes)" | |
| # Read DLL and convert RVA to file offset | |
| $dllBytes = [System.IO.File]::ReadAllBytes($gameDll) | |
| $peOffset = [BitConverter]::ToInt32($dllBytes, 0x3C) | |
| $numSections = [BitConverter]::ToUInt16($dllBytes, $peOffset + 6) | |
| $optHeaderSize = [BitConverter]::ToUInt16($dllBytes, $peOffset + 20) | |
| $sectionTableOffset = $peOffset + 24 + $optHeaderSize | |
| $fileOffset = -1 | |
| for ($i = 0; $i -lt $numSections; $i++) { | |
| $secOff = $sectionTableOffset + ($i * 40) | |
| $vSize = [BitConverter]::ToUInt32($dllBytes, $secOff + 8) | |
| $vAddr = [BitConverter]::ToUInt32($dllBytes, $secOff + 12) | |
| $rOff = [BitConverter]::ToUInt32($dllBytes, $secOff + 20) | |
| if ($rva -ge $vAddr -and $rva -lt ($vAddr + $vSize)) { | |
| $fileOffset = [long]$rOff + ([long]$rva - [long]$vAddr) | |
| break | |
| } | |
| } | |
| if ($fileOffset -lt 0) { | |
| Write-Error "Could not map RVA to file offset" | |
| exit 1 | |
| } | |
| Write-Host "File offset: 0x$($fileOffset.ToString('X'))" | |
| $currentBytes = $dllBytes[$fileOffset..($fileOffset + 15)] | |
| $hexCurrent = ($currentBytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' ' | |
| Write-Host "Current bytes: $hexCurrent" | |
| # Check if already patched | |
| if ($dllBytes[$fileOffset] -eq 0x31 -and $dllBytes[$fileOffset+1] -eq 0xC0 -and $dllBytes[$fileOffset+2] -eq 0xC3) { | |
| Write-Host "Already patched!" | |
| exit 0 | |
| } | |
| # Backup if not exists | |
| if (-not (Test-Path $backupDll)) { | |
| Copy-Item $gameDll $backupDll | |
| Write-Host "Backup saved to: $backupDll" | |
| } else { | |
| Write-Host "Backup already exists." | |
| } | |
| # Patch: xor eax, eax; ret (return 0 = GV_Main) | |
| $dllBytes[$fileOffset] = 0x31 # xor eax, eax | |
| $dllBytes[$fileOffset + 1] = 0xC0 | |
| $dllBytes[$fileOffset + 2] = 0xC3 # ret | |
| [System.IO.File]::WriteAllBytes($gameDll, $dllBytes) | |
| Write-Host "" | |
| Write-Host "PATCHED! GetGameVersion() now returns GV_Main (0)." | |
| Write-Host "The experimental banner should be gone." | |
| Write-Host "" | |
| Write-Host "To restore: powershell -ExecutionPolicy Bypass -File $($MyInvocation.MyCommand.Path) -Restore" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment