Inspect FlandersMod yourself
A step-by-step guide to checking what FlandersMod does, using your own copy of the download and free tools. You never run it: everything here reads the files without executing them.
1Before you start
You need about an hour. The steps work on Windows, macOS or Linux.
| Tool | Used for | Get it |
|---|---|---|
| Python 3.9+ | The two small scripts on this page | python.org |
| dnfile | Reading the launcher's embedded files | pip install dnfile |
| ILSpy or dnSpyEx | Turning the launcher back into readable C# | ILSpy · dnSpyEx |
| Ghidra (optional) | Looking inside the native DLL and server | ghidra-sre.org (12.x needs JDK 25) |
2Check your download
First make sure your FlandersMod.exe is one of the releases this guide was written against. Compute its SHA-256 hash:
Get-FileHash .\FlandersMod.exe -Algorithm SHA256
shasum -a 256 FlandersMod.exe # macOS sha256sum FlandersMod.exe # Linux
| Release | Size (bytes) | SHA-256 of FlandersMod.exe |
|---|---|---|
| 0.0.1 | 53,416,960 | 7c98cafd0162d1059f295ca72f6788f5735ac73046bb6dc04ac7212a322a74d3 |
| 0.0.2 | 52,562,944 | f7ce04d4c9821a6726d0be9bfbda47020b4a64a73fc0687bcbb069bc63e4d76e |
| 0.0.3 | 80,159,232 | e00f041996bec62ba6026dc46daa8668c0ccf5c905942cce5314f943a4372980 |
A different hash doesn't automatically mean something is wrong. It may be a newer release, or a file someone repackaged. Either way, the steps below still work, and they're how you find out what changed.
3Unpack the launcher
FlandersMod.exe is a .NET Framework 4.8 program. Everything else it installs is embedded inside it as a .NET resource: the in-game DLL, the server, the weapon-wheel art and the bundled mods. You can save them out without running anything.
Option A: with ILSpy (no code)
- Open ILSpy and drag
FlandersMod.exeonto it. - Expand FlandersMod → Resources. You'll see entries named
embed.FlandersMP.dll,embed.FlandersMP_Server.exe,embed.ww.zipandembed.mods.*.lmlm. - Right-click each one → Save.
Option B: with the script
Save extract_resources.py next to the exe, then:
pip install dnfile python extract_resources.py FlandersMod.exe unpacked
You should get files like these (the 0.0.3 list is shown):
| Resource | What it is |
|---|---|
embed.FlandersMP.dll | The in-game part. It's copied into your game folder as d3d8.dll, so the game loads it instead of Windows' Direct3D 8 DLL. |
embed.FlandersMP_Server.exe | The multiplayer server, and an optional server list ("master"). |
embed.ww.zip | Weapon wheel art, sounds and WeaponWheel.ini. |
embed.mods.*.lmlm | The bundled mods: 12 in 0.0.1/0.0.2, 15 in 0.0.3. |
Compare their hashes with the reference table to see whether you have the same files.
4Read the launcher's code
.NET programs decompile back to nearly the original C#. Open FlandersMod.exe in ILSpy (or dnSpyEx) and expand the FlandersMod namespace. The interesting classes:
| Class | Look at |
|---|---|
Core | Report (usage events), Records (the public event list), Install/Launch (what goes into your game folder and which environment variables are set), the uninstall code, the hosting code |
Lobby | Every place Core.Report(...) is called, i.e. which of your actions get reported |
Mods, Mod | How .lmlm files are read and which ones count as "code mods" |
DiscordPresence | Discord "now playing" status. This talks only to the Discord app on your own PC. |
Search tip: in ILSpy press Ctrl+Shift+F, choose "Constant", and search for http. That lists every web address the launcher contains. To keep a copy of the code, right-click the assembly → Save Code…, which writes out a C# project you can grep.
5What it sends online
Here's what the launcher's code shows (all three releases behave the same). Check each point in ILSpy against your own copy.
Usage events Core.Report
Each event is a plain web request to the author's Cloudflare Worker:
https://flandersmod-master.flandersmodcanadaservers.workers.dev/event?kind=<event>&name=<your player name>&detail=<details>
- There's no setting that turns this off.
Reportdoesn't check any option before sending. (The Discord checkbox in Settings only controls Discord status.) - It includes your player name as typed in the launcher, and the receiving server also sees your IP address, as with any web request.
- Events sent (search
Core.Report(inLobby):
| kind | When | detail |
|---|---|---|
opened | The launcher window opens | empty |
page | You switch to a page | the page name |
rampage / classic | You start the game in that mode | titles of the mods you have enabled |
coop | You start local co-op | titles of enabled mods |
modon / modoff | You tick or untick a mod | the mod's title |
modview | You select a mod to read about it | the mod's title |
mod | You add a .lmlm file | its file name |
The public event list Core.Records
The launcher also downloads …workers.dev/records, a JSON list of recent events (time, kind, player name, server) for all players, and shows it in the launcher. The client code requests it without any password or key. That's how anyone can read it.
Other connections
- Server browser: the same
workers.devaddress serves the official server list. - Hosting: 0.0.2 and 0.0.3 fetch your public IP from
https://api.ipify.orgwhen you host a server list. 0.0.1 used a fixed IP address written into the program instead. - The server program announces itself to the server list with
/announce?port=…&name=…&players=…&max=…&pw=…&ip=…&owner=…. - In game, the DLL talks UDP to the multiplayer server you join (default port 7777).
6What it changes on your PC
All of these are in Core. Search for WriteAllBytes, File.Copy, Process.Start and SetEnvironmentVariable.
- Game folder: writes
d3d8.dll(the FlandersMP DLL) next toSimpsons.exe, plus the weapon wheel's files. If a differentd3d8.dllis already there, it leaves it alone and turns weapons and mods off. - Next to the launcher: a
FlandersMod_Datafolder with the server exe, your mods and settings. - When the game starts: it sets environment variables such as
FLANDERSMP_NAME,FLANDERSMOD_MODSandFLANDERSMOD_PLUGINS(the code-mod folders), which the DLL reads. - Hosting and co-op start
FlandersMP_Server.exein the background. - Uninstall runs a hidden PowerShell command that deletes its files.
- Inside the game, the DLL patches
Simpsons.exein memory and hooks Direct3D, keyboard, mouse and controller input. That's how the weapons and multiplayer work. It doesn't changeSimpsons.exeon disk.
7Look inside .lmlm mods
FlandersMod's .lmlm files are simple archives. Use flmod_extract.py to list or unpack them:
python flmod_extract.py list "Blood Mod.lmlm" python flmod_extract.py extract "Blood Mod.lmlm" python flmod_extract.py extract-all unpacked # every .lmlm in a folder
FlandersMod/ folder (for example FlandersMod/Blood.dll). FlandersMod loads it straight into the game, where it runs as native code with your user's full permissions. There's no sandbox. Treat a mod that contains a DLL like any other program you download: only use it if you trust where it came from. Mods with only .p3d, .mfk and .ini files are data.The DLLs in the bundled mods (SimpsonsFlySim, WorldDestruction) and in Blood Mod can be opened in Ghidra exactly like the main DLL. They export FlandersMod_Init, FlandersMod_Frame, and optionally FlandersMod_Hit and FlandersMod_Draw3D.
8The in-game DLL and server optional
These are native 32-bit Windows programs built with MinGW, with no debug names, so they need Ghidra rather than ILSpy. You don't need this part to check the network and install claims above. It's for going deeper.
- Create a Ghidra project, then File → Import
FlandersMP.dll(or the server exe). Accept the defaults and run auto-analysis. It takes about a minute. - Search → For Strings, then filter for things like
FLMP(the multiplayer packet tag),plugin,FLANDERSMOD_,workers.devorDirect3DCreate8. Double-click a result, then use References → Show References to to jump to the code that uses it. - The DLL exports
Direct3DCreate8and a few otherd3d8.dllfunctions, which is how it gets loaded as the game's Direct3D DLL. Start reading from there, and from the entry point.
Two Ghidra pitfalls with these files
- Missing functions. Ghidra misses several hundred functions in these binaries, mostly code reached only through jump tables or tail jumps. If you see gray "undefined" bytes between functions, click the first one, press D (disassemble) and then F (create function). Examples in 0.0.3:
0x66152eb0in the DLL (the shotgun pose code), and0x004028c0and0x00404ff0in the server (the server-list announce code). - Decompiler timeouts. A couple of very large animation functions use old-style x87 floating-point code and time out. Raise Edit → Tool Options → Decompiler → Decompiler Timeout (e.g. 600 s), or read their assembly.
In all three releases, the server program is the same build: only its timestamp changes. The DLL changes a little between releases. 0.0.2 added the code-mod loader, and 0.0.3 reworked the splash and "WASTED" screens.
9Limiting what it sends
If you decide to use FlandersMod anyway but don't want the usage events sent, you can block the worker's address. Add this line to your hosts file (C:\Windows\System32\drivers\etc\hosts, edited as administrator):
0.0.0.0 flandersmod-master.flandersmodcanadaservers.workers.dev
Trade-off: the same address also provides the official server browser, so that list will be empty. Direct connect, hosting your own server, co-op and single player don't need it. A firewall rule blocking outbound traffic from FlandersMod.exe has the same effect. It also blocks the public-IP lookup used when you host a server list.
To check it worked, keep a network monitor such as Windows' Resource Monitor (Network tab) or Wireshark open while you use the launcher.
10Reference: known files
SHA-256 hashes of the files embedded in each release, as saved in step 3. Matching hashes mean you have exactly the files this guide was written against.
Main components
| File | 0.0.1 | 0.0.2 | 0.0.3 |
|---|---|---|---|
| FlandersMP.dll | 8c787cbbf57d81a2bdb0acf7b50ef9beae7256b0aae138d5d852a8c87f9f1ed7 | 0709bdfd9222764969edbc0abad1b3fea5ce5cf98f2d30e366572ad852f06811 | 202aa0b2dc8d2622286b7418ba80f260edda3830203570e5346c695e5751373c |
| FlandersMP_Server.exe | 9389d59563ab0afbeffd445dbb8f06e511a0b053e9a4d7494c495356d2d36d09 | 09560ad6dc375609efa5102a54cec788a5b2546819db3661c30dec5931903715 | 5b7732c08b40ff03b3ab2b47d349a6f1d2b122fd26716c690706036e9f8fa929 |
| ww.zip | 3e03282d941ef5684844142e72525db30b12ac49c7e81304565f58bd4d8dd14d | 199b2885b6026f193791a801c0c0546e112dc42711e59f16578b38e67575d31a | 04f9e4c2748357fde639cdadcf04017cd96711d480c96617e69cc27f9340c373 |
Mods
The first 12 are byte-identical in all three releases.
| Mod | In | Has code? | SHA-256 |
|---|---|---|---|
| Barney Mod | 0.0.1–0.0.3 | No | ecd4c195146c41e2365d53c1140de618d007fedf398bb6c32ca8b2d384a4ea84 |
| Burns Mod | 0.0.1–0.0.3 | No | 154f459967d0bdeca8c632170b17012bff4aa7e010127092b9a34d0f1410b763 |
| Cursed Tommy Mod | 0.0.1–0.0.3 | No | f1f5fea34efe1e15545cb7b2eea8df2cea8c4d95084cc39be5effbb1584605fc |
| Doom E1M1 Map | 0.0.1–0.0.3 | No | dd390fc95beb1ec280bdec207fadea52a4f10482358b99cd11f41b64242b5d18 |
| Dust2 Map | 0.0.1–0.0.3 | No | 7a9ff618f2577faf016733df2f9f2742b6b21d010c2add57279a7be8c0f56a0f |
| Final Fantasy Enemies | 0.0.1–0.0.3 | No | f7a6c4f818cc36f04ddc9f42d1d97bc275265f1da4b0c249964faaf10eb85b58 |
| Flanders Mod | 0.0.1–0.0.3 | No | a4cc72335daa61d80ea3be3d145b6931dbd320bad2ad9fce59cffa2a885a99f5 |
| Hookers Everywhere | 0.0.1–0.0.3 | No | dc61891a77a655af75b87d80e88f72c43c0e6db87af1907079cee393546283bb |
| Moe Mod | 0.0.1–0.0.3 | No | 1977923e92b1499205ec4a22dbbc42ad900eebf06a22248a0485442a0be99095 |
| Overlord Bean Mod | 0.0.1–0.0.3 | No | 11b7875e724085784f814b3430c03c3a3db67cd0ea6fa66bcae7b385f3d5c7f6 |
| Peter Mod | 0.0.1–0.0.3 | No | 3c7a3933568b9e4c209e5da2db8805b2bfe79de63174055d0a94ba8f9f48900e |
| Tommy Mod | 0.0.1–0.0.3 | No | 559783199408bcb77918cd79eb285c74faf83ec671efaa41e10a723f80626e74 |
| SimpsonsFlySim | 0.0.3 | Yes: FlandersMod/SimpsonsFlySim.dll | 8bbcddef76ab05f93665250a5c65571a710f5b1ed205c420bdb516c1652f92c9 |
| WorldDestruction | 0.0.3 | Yes: FlandersMod/WorldDestruction.dll | 0169337cd6830c6e85bd9a00367e222fdbfcdb6e87d06df6f8dd6c2504a9f5ef |
| Lois Rambo Mod | 0.0.3 | No | 179f45d6a0c55ca2bcb9581df81f8efc04439e69705d4937f5dbf332110c864c |
| Blood Mod | separate download (needs 0.0.2+) | Yes: FlandersMod/Blood.dll | 64e8355e3967041c1feb8cad7fd2a368658a4ed95c761a4455ec4754f9f4b511 |
11The two scripts
Both are short and use only Python's standard library, plus dnfile for the first. Read them before you run them, then save each under the name shown.
extract_resources.py: save the launcher's embedded files
#!/usr/bin/env python3
"""Save every embedded resource from FlandersMod.exe WITHOUT running it.
Needs: pip install dnfile
Usage: python extract_resources.py FlandersMod.exe out_folder
"""
import os
import sys
import dnfile
exe, out = sys.argv[1], sys.argv[2]
os.makedirs(out, exist_ok=True)
pe = dnfile.dnPE(exe)
for res in pe.net.resources or []:
name = getattr(res.name, "value", res.name)
name = str(name)
data = bytes(res.data)
safe = "".join(c if c.isalnum() or c in "._-" else "_" for c in name)
with open(os.path.join(out, safe), "wb") as f:
f.write(data)
print("%10d %s" % (len(data), name))flmod_extract.py: list and unpack FlandersMod .lmlm packages
#!/usr/bin/env python3
"""Extract FlandersMod (.lmlm) mod packages, as shipped inside FlandersMod.exe.
Layout:
0x000 "LSPA", then zeros up to 0x400
The rest is 0x200-byte sectors; every field starts 2 bytes into its sector.
0x400 root sector: u16 entry count
Each entry:
name sector u16 0, then the name in UTF-16LE (NUL-terminated)
info sector u64 size, u32 offset (offset 0 => folder; the u16 at the same spot is its child count)
pad sector files only, all zeros
A folder's children follow it directly (depth-first). File data is stored raw after the tree.
Usage:
flmod_extract.py list <mod.lmlm>
flmod_extract.py extract <mod.lmlm> [outdir] (default outdir: ./<mod name>)
flmod_extract.py extract-all <folder> [outdir] (every .lmlm in folder -> outdir/<mod name>/)
"""
import os
import struct
import sys
SECTOR = 0x200
ROOT = 0x400
MAX_DEPTH = 16
class FormatError(Exception):
pass
def read_tree(data):
"""Return a list of (path, size, offset) for every file in the package."""
if len(data) < ROOT + SECTOR or data[:4] != b"LSPA":
raise FormatError("not a FlandersMod package (missing LSPA header)")
if any(data[4:ROOT]):
raise FormatError("not a FlandersMod package (unexpected header fields)")
files = []
pos = ROOT
def need(n):
if pos + n > len(data):
raise FormatError("truncated package")
def walk(prefix, depth):
nonlocal pos
if depth > MAX_DEPTH:
raise FormatError("folder tree too deep")
need(SECTOR)
count = struct.unpack_from("<H", data, pos + 2)[0]
pos += SECTOR
for _ in range(count):
need(2 * SECTOR)
if struct.unpack_from("<H", data, pos)[0] != 0:
raise FormatError("not a FlandersMod package (unexpected entry flags at 0x%x)" % pos)
name = data[pos + 2:pos + SECTOR].decode("utf-16le", "replace").split("\x00")[0]
if not name or name in (".", "..") or "/" in name or "\\" in name or ":" in name:
raise FormatError("bad entry name %r at 0x%x" % (name, pos))
pos += SECTOR
size, offset = struct.unpack_from("<QI", data, pos + 2)
if offset == 0:
walk(prefix + name + "/", depth + 1) # folder: children follow
else:
if offset + size > len(data):
raise FormatError("%s%s runs past the end of the package" % (prefix, name))
files.append((prefix + name, size, offset))
pos += 2 * SECTOR # info + pad sector
walk("", 0)
return files
def extract(path, outdir):
data = open(path, "rb").read()
files = read_tree(data)
root = os.path.realpath(outdir)
for rel, size, offset in files:
dst = os.path.realpath(os.path.join(root, *rel.split("/")))
if not dst.startswith(root + os.sep):
raise FormatError("refusing to write outside %s: %s" % (outdir, rel))
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "wb") as f:
f.write(data[offset:offset + size])
print(" %-60s %10d" % (rel, size))
print("%d files -> %s" % (len(files), outdir))
def main(argv):
if len(argv) < 3 or argv[1] not in ("list", "extract", "extract-all"):
print(__doc__)
return 2
cmd, src = argv[1], argv[2]
try:
if cmd == "list":
data = open(src, "rb").read()
files = read_tree(data)
for rel, size, offset in files:
print(" %-60s %10d @0x%08x" % (rel, size, offset))
print("%d files" % len(files))
elif cmd == "extract":
name = os.path.splitext(os.path.basename(src))[0]
extract(src, argv[3] if len(argv) > 3 else name)
else:
out = argv[3] if len(argv) > 3 else "."
failed = 0
for fn in sorted(os.listdir(src)):
if not fn.lower().endswith(".lmlm"):
continue
print("== %s" % fn)
try:
extract(os.path.join(src, fn), os.path.join(out, os.path.splitext(fn)[0]))
except FormatError as e:
print(" skipped: %s" % e)
failed += 1
return 1 if failed else 0
except FormatError as e:
print("error: %s" % e, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))