Sharing a fix in case it helps others here — not a code change, just documentation and a small
reproducible patch. Tested on Fedora 44, Wine 11.0 Staging, AMD RX 6700 XT (Mesa RADV), 4K
native, launching game.dat directly (no Lutris). Based on withmorten's SafeDisc-free game.dat.
Symptom
Campaign runs fine, but Skirmish dies around the 3-minute mark. It turned out to be two
independent problems that happen to strike at nearly the same time — which is why single fixes
never fully worked.
Problem 1 — Auto-defeat DRM (the ~3:25 unit wipe)
The launcher is supposed to hand a GUID to game.dat via PostThreadMessageA(0xBEEF) +
MapViewOfFileEx. That IPC doesn't deliver under Wine, so the in-game validator never sees a
valid GUID and, on frame 1024 (~3:25 @ 5 logic-fps), destroys all your units. Campaign modes
(4 and 8) are checked earlier and skip it — hence Campaign is unaffected.
The DRM code at 0x502090–0x502290 looks like dead code (no direct refs) but is reached via
SafeDisc jump tables. The actual trigger lives in the game-tick function:
0078E190 cmp dword [ebp+0x3C], 0x400 ; frame == 1024?
0078E197 jne 0x78E1BB
0078E199 call 0x409999 ; -> GUID validator (thunk to 0x502240)
0078E19E test al, al
0078E1A0 jne 0x78E1BB ; GUID valid -> skip
0078E1A2 ... ; <-- destroys all player units
Fix (launcher-independent, 1 byte): make the skip unconditional so the auto-defeat block is
never reached:
VA 0x0078E1A0 (file offset 0x38E1A0): 75 -> EB (JNE -> JMP)
Verified with a full Ghidra scan that this cmp …,0x400 is the only such timer and the GUID
validator has exactly two callers. (Note: don't force the validator 0x502240 to return true —
its other caller at 0x8E47AB then enters uninitialised online code and crashes. Patch the
branch instead.)
Problem 2 — wined3d virtual-address-space leak (the real crash)
With the DRM gone, Skirmish still crashed, just later (abnormal program termination). The Wine
log shows the cause:
err:virtual:allocate_virtual_memory out of memory for allocation, base (nil) size 04000000
err:opengl:buffer_vm_alloc NtAllocateVirtualMemory failed
err:d3d:wined3d_allocator_chunk_gl_map Failed to map chunk memory
It's not RAM (64 GB free) — the 32-bit process exhausts its virtual address space. Sampling
/proc/PID/status shows VmSize climbing linearly ~340 MB/min while VmRSS stays flat: a
leak in wined3d's OpenGL buffer-object allocator. Skirmish allocates faster than Campaign, so it
hits the wall sooner.
Fixes:
- DXVK (D3D9→Vulkan) — the real cure. Copy DXVK's 32-bit
d3d9.dll into the prefix's
syswow64 and launch with WINEDLLOVERRIDES="d3d9=n". On RADV this is rock-solid.
LARGE_ADDRESS_AWARE — game.dat doesn't set it; enabling it (PE characteristics
0x010F → 0x012F, offset 0x146) lifts the cap from 2 GB to 4 GB. On its own it only delays
the crash (proves the cause); with DXVK the leak is gone entirely.
Result: validated a 49-minute Skirmish at 4K with DXVK — VmSize drifted just 69 MB
total (5105 → 5170 MB), essentially flat.
Reproducible patcher
patch_bfme1.py — applies both binary patches (idempotent, with backup)
#!/usr/bin/env python3
"""
patch_bfme1.py — fix BFME1 Skirmish on Linux/Wine with two 1-byte patches.
Operates on the SafeDisc-free, debuggable `game.dat` (withmorten's unpacked build).
Both patches are verified against expected bytes and are idempotent (safe to re-run).
1. Auto-defeat DRM: VA 0x0078E1A0 75 -> EB (JNE -> JMP; auto-defeat becomes dead code)
2. LARGE_ADDRESS_AWARE: PE Characteristics bit 0x0020 set (2 GB -> 4 GB address space)
Usage:
python3 patch_bfme1.py "/path/to/game.dat"
python3 patch_bfme1.py --revert "/path/to/game.dat" # restore from .orig backup
After patching, install DXVK's 32-bit d3d9.dll into the prefix and launch with
WINEDLLOVERRIDES="d3d9=n". See README.md.
"""
import struct
import sys
import shutil
import os
IMAGE_BASE = 0x00400000
# Patch 1 — auto-defeat branch
DRM_VA = 0x0078E1A0
DRM_FOFF = DRM_VA - IMAGE_BASE # 0x38E1A0
DRM_ORIG = 0x75 # JNE rel8
DRM_NEW = 0xEB # JMP rel8
# Reference: the unpatched, SafeDisc-free game.dat this script targets
EXPECTED_SIZE = 16424960
EXPECTED_MD5 = "bd8630a0850c04cff1bfef1d3794e2b3"
def pe_characteristics_offset(data: bytes) -> int:
pe_off = struct.unpack_from("<I", data, 0x3C)[0]
if data[pe_off:pe_off + 4] != b"PE\x00\x00":
raise ValueError("Not a PE file (bad NT signature)")
return pe_off + 0x16
def patch(path: str) -> None:
with open(path, "rb") as f:
data = bytearray(f.read())
if len(data) != EXPECTED_SIZE:
print(f" ! warning: size {len(data)} != expected {EXPECTED_SIZE} "
f"(wrong/already-different game.dat?)")
# Backup once
backup = path + ".orig"
if not os.path.exists(backup):
shutil.copy2(path, backup)
print(f" backup -> {backup}")
# --- Patch 1: auto-defeat branch ---
cur = data[DRM_FOFF]
if cur == DRM_NEW:
print(f" [1] DRM branch @ 0x{DRM_VA:08X}: already patched (EB)")
elif cur == DRM_ORIG:
data[DRM_FOFF] = DRM_NEW
print(f" [1] DRM branch @ 0x{DRM_VA:08X}: 75 -> EB (auto-defeat disabled)")
else:
raise ValueError(f"[1] unexpected byte 0x{cur:02X} at 0x{DRM_VA:08X} "
f"(expected 0x75 or 0xEB)")
# --- Patch 2: LARGE_ADDRESS_AWARE ---
coff = pe_characteristics_offset(data)
ch = struct.unpack_from("<H", data, coff)[0]
if ch & 0x0020:
print(f" [2] LARGE_ADDRESS_AWARE: already set (0x{ch:04X})")
else:
struct.pack_into("<H", data, coff, ch | 0x0020)
print(f" [2] LARGE_ADDRESS_AWARE: 0x{ch:04X} -> 0x{ch | 0x0020:04X} (4 GB)")
with open(path, "wb") as f:
f.write(data)
print(" done.")
def revert(path: str) -> None:
backup = path + ".orig"
if not os.path.exists(backup):
sys.exit(f"No backup found at {backup}")
shutil.copy2(backup, path)
print(f" restored {path} from {backup}")
def main() -> None:
args = sys.argv[1:]
if not args:
sys.exit(__doc__)
if args[0] == "--revert":
if len(args) != 2:
sys.exit("usage: patch_bfme1.py --revert /path/to/game.dat")
revert(args[1])
else:
patch(args[0])
if __name__ == "__main__":
main()
Launch (no Lutris needed)
export WINEPREFIX=~/Games/bfme1/prefix
export WINE_LARGE_ADDRESS_AWARE=1
export WINEDLLOVERRIDES="d3d9=n" # DXVK's d3d9.dll in syswow64
cd ".../EA GAMES/The Battle for Middle-earth (tm)"
wine game.dat
Optional dxvk.conf next to game.dat for sharper 4K: d3d9.samplerAnisotropy = 16.
Credits
The new bits here: a launcher-independent 1-byte auto-defeat patch (so no replacement launcher
is needed), and identifying the wined3d address-space leak (→ DXVK) as the real crash — so
Skirmish runs unlimited at 4K on Wine.
Sharing a fix in case it helps others here — not a code change, just documentation and a small
reproducible patch. Tested on Fedora 44, Wine 11.0 Staging, AMD RX 6700 XT (Mesa RADV), 4K
native, launching
game.datdirectly (no Lutris). Based on withmorten's SafeDisc-freegame.dat.Symptom
Campaign runs fine, but Skirmish dies around the 3-minute mark. It turned out to be two
independent problems that happen to strike at nearly the same time — which is why single fixes
never fully worked.
Problem 1 — Auto-defeat DRM (the ~3:25 unit wipe)
The launcher is supposed to hand a GUID to
game.datviaPostThreadMessageA(0xBEEF)+MapViewOfFileEx. That IPC doesn't deliver under Wine, so the in-game validator never sees avalid GUID and, on frame 1024 (~3:25 @ 5 logic-fps), destroys all your units. Campaign modes
(4 and 8) are checked earlier and skip it — hence Campaign is unaffected.
The DRM code at
0x502090–0x502290looks like dead code (no direct refs) but is reached viaSafeDisc jump tables. The actual trigger lives in the game-tick function:
Fix (launcher-independent, 1 byte): make the skip unconditional so the auto-defeat block is
never reached:
Verified with a full Ghidra scan that this
cmp …,0x400is the only such timer and the GUIDvalidator has exactly two callers. (Note: don't force the validator
0x502240to return true —its other caller at
0x8E47ABthen enters uninitialised online code and crashes. Patch thebranch instead.)
Problem 2 — wined3d virtual-address-space leak (the real crash)
With the DRM gone, Skirmish still crashed, just later (
abnormal program termination). The Winelog shows the cause:
It's not RAM (64 GB free) — the 32-bit process exhausts its virtual address space. Sampling
/proc/PID/statusshowsVmSizeclimbing linearly ~340 MB/min whileVmRSSstays flat: aleak in wined3d's OpenGL buffer-object allocator. Skirmish allocates faster than Campaign, so it
hits the wall sooner.
Fixes:
d3d9.dllinto the prefix'ssyswow64and launch withWINEDLLOVERRIDES="d3d9=n". On RADV this is rock-solid.LARGE_ADDRESS_AWARE—game.datdoesn't set it; enabling it (PE characteristics0x010F → 0x012F, offset0x146) lifts the cap from 2 GB to 4 GB. On its own it only delaysthe crash (proves the cause); with DXVK the leak is gone entirely.
Result: validated a 49-minute Skirmish at 4K with DXVK —
VmSizedrifted just 69 MBtotal (5105 → 5170 MB), essentially flat.
Reproducible patcher
patch_bfme1.py— applies both binary patches (idempotent, with backup)Launch (no Lutris needed)
Optional
dxvk.confnext togame.datfor sharper 4K:d3d9.samplerAnisotropy = 16.Credits
game.datthis patches(GameReplays: https://www.gamereplays.org/community/BFME1_SafeDisc_unpacked_+_patched-t1060667.html).
This is the binary the patch targets.
(https://github.com/vs49688/lotrbfme-launchers, now archived).
patch I used to get the game booting.
The new bits here: a launcher-independent 1-byte auto-defeat patch (so no replacement launcher
is needed), and identifying the wined3d address-space leak (→ DXVK) as the real crash — so
Skirmish runs unlimited at 4K on Wine.