FlandersMod docsInspect it yourselfOverviewArchitectureDecompilation notesIssues

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.

Why this page existsFlandersMod is a closed-source launcher for The Simpsons: Hit & Run. It unpacks a DLL into your game folder, sends usage events to its author's server, and can load native-code mods. You shouldn't have to take anyone's word for what it does, ours included. This guide shows you how to check each claim yourself. It covers releases 0.0.1, 0.0.2 and 0.0.3 and should also work for later ones.

1Before you start

You need about an hour. The steps work on Windows, macOS or Linux.

ToolUsed forGet it
Python 3.9+The two small scripts on this pagepython.org
dnfileReading the launcher's embedded filespip install dnfile
ILSpy or dnSpyExTurning the launcher back into readable C#ILSpy · dnSpyEx
Ghidra (optional)Looking inside the native DLL and serverghidra-sre.org (12.x needs JDK 25)
Don't run the files while you inspect them.None of the steps below execute anything. Work on a copy of the download in its own folder. If you later decide to run FlandersMod, use a virtual machine or a spare PC.

2Check your download

First make sure your FlandersMod.exe is one of the releases this guide was written against. Compute its SHA-256 hash:

Windows (PowerShell)
Get-FileHash .\FlandersMod.exe -Algorithm SHA256
macOS / Linux
shasum -a 256 FlandersMod.exe      # macOS
sha256sum FlandersMod.exe          # Linux
ReleaseSize (bytes)SHA-256 of FlandersMod.exe
0.0.153,416,9607c98cafd0162d1059f295ca72f6788f5735ac73046bb6dc04ac7212a322a74d3
0.0.252,562,944f7ce04d4c9821a6726d0be9bfbda47020b4a64a73fc0687bcbb069bc63e4d76e
0.0.380,159,232e00f041996bec62ba6026dc46daa8668c0ccf5c905942cce5314f943a4372980

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)

  1. Open ILSpy and drag FlandersMod.exe onto it.
  2. Expand FlandersMod → Resources. You'll see entries named embed.FlandersMP.dll, embed.FlandersMP_Server.exe, embed.ww.zip and embed.mods.*.lmlm.
  3. Right-click each one → Save.

Option B: with the script

Save extract_resources.py next to the exe, then:

Any OS
pip install dnfile
python extract_resources.py FlandersMod.exe unpacked

You should get files like these (the 0.0.3 list is shown):

ResourceWhat it is
embed.FlandersMP.dllThe 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.exeThe multiplayer server, and an optional server list ("master").
embed.ww.zipWeapon wheel art, sounds and WeaponWheel.ini.
embed.mods.*.lmlmThe 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:

ClassLook at
CoreReport (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
LobbyEvery place Core.Report(...) is called, i.e. which of your actions get reported
Mods, ModHow .lmlm files are read and which ones count as "code mods"
DiscordPresenceDiscord "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:

From Core.Report
https://flandersmod-master.flandersmodcanadaservers.workers.dev/event?kind=<event>&name=<your player name>&detail=<details>
kindWhendetail
openedThe launcher window opensempty
pageYou switch to a pagethe page name
rampage / classicYou start the game in that modetitles of the mods you have enabled
coopYou start local co-optitles of enabled mods
modon / modoffYou tick or untick a modthe mod's title
modviewYou select a mod to read about itthe mod's title
modYou add a .lmlm fileits 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

What you can't see from hereThe client code shows what is sent. It can't show what the worker does with it afterwards (storage, forwarding, who can read it). Only the server's owner, or watching the service from outside, can answer that.

6What it changes on your PC

All of these are in Core. Search for WriteAllBytes, File.Copy, Process.Start and SetEnvironmentVariable.

7Look inside .lmlm mods

FlandersMod's .lmlm files are simple archives. Use flmod_extract.py to list or unpack them:

Any OS
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
Watch for .dll files.Since 0.0.2, a mod can carry a DLL in a 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.

  1. 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.
  2. Search → For Strings, then filter for things like FLMP (the multiplayer packet tag), plugin, FLANDERSMOD_, workers.dev or Direct3DCreate8. Double-click a result, then use References → Show References to to jump to the code that uses it.
  3. The DLL exports Direct3DCreate8 and a few other d3d8.dll functions, 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

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):

hosts file
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

File0.0.10.0.20.0.3
FlandersMP.dll8c787cbbf57d81a2bdb0acf7b50ef9beae7256b0aae138d5d852a8c87f9f1ed70709bdfd9222764969edbc0abad1b3fea5ce5cf98f2d30e366572ad852f06811202aa0b2dc8d2622286b7418ba80f260edda3830203570e5346c695e5751373c
FlandersMP_Server.exe9389d59563ab0afbeffd445dbb8f06e511a0b053e9a4d7494c495356d2d36d0909560ad6dc375609efa5102a54cec788a5b2546819db3661c30dec59319037155b7732c08b40ff03b3ab2b47d349a6f1d2b122fd26716c690706036e9f8fa929
ww.zip3e03282d941ef5684844142e72525db30b12ac49c7e81304565f58bd4d8dd14d199b2885b6026f193791a801c0c0546e112dc42711e59f16578b38e67575d31a04f9e4c2748357fde639cdadcf04017cd96711d480c96617e69cc27f9340c373

Mods

The first 12 are byte-identical in all three releases.

ModInHas code?SHA-256
Barney Mod0.0.1–0.0.3Noecd4c195146c41e2365d53c1140de618d007fedf398bb6c32ca8b2d384a4ea84
Burns Mod0.0.1–0.0.3No154f459967d0bdeca8c632170b17012bff4aa7e010127092b9a34d0f1410b763
Cursed Tommy Mod0.0.1–0.0.3Nof1f5fea34efe1e15545cb7b2eea8df2cea8c4d95084cc39be5effbb1584605fc
Doom E1M1 Map0.0.1–0.0.3Nodd390fc95beb1ec280bdec207fadea52a4f10482358b99cd11f41b64242b5d18
Dust2 Map0.0.1–0.0.3No7a9ff618f2577faf016733df2f9f2742b6b21d010c2add57279a7be8c0f56a0f
Final Fantasy Enemies0.0.1–0.0.3Nof7a6c4f818cc36f04ddc9f42d1d97bc275265f1da4b0c249964faaf10eb85b58
Flanders Mod0.0.1–0.0.3Noa4cc72335daa61d80ea3be3d145b6931dbd320bad2ad9fce59cffa2a885a99f5
Hookers Everywhere0.0.1–0.0.3Nodc61891a77a655af75b87d80e88f72c43c0e6db87af1907079cee393546283bb
Moe Mod0.0.1–0.0.3No1977923e92b1499205ec4a22dbbc42ad900eebf06a22248a0485442a0be99095
Overlord Bean Mod0.0.1–0.0.3No11b7875e724085784f814b3430c03c3a3db67cd0ea6fa66bcae7b385f3d5c7f6
Peter Mod0.0.1–0.0.3No3c7a3933568b9e4c209e5da2db8805b2bfe79de63174055d0a94ba8f9f48900e
Tommy Mod0.0.1–0.0.3No559783199408bcb77918cd79eb285c74faf83ec671efaa41e10a723f80626e74
SimpsonsFlySim0.0.3Yes: FlandersMod/SimpsonsFlySim.dll8bbcddef76ab05f93665250a5c65571a710f5b1ed205c420bdb516c1652f92c9
WorldDestruction0.0.3Yes: FlandersMod/WorldDestruction.dll0169337cd6830c6e85bd9a00367e222fdbfcdb6e87d06df6f8dd6c2504a9f5ef
Lois Rambo Mod0.0.3No179f45d6a0c55ca2bcb9581df81f8efc04439e69705d4937f5dbf332110c864c
Blood Modseparate download (needs 0.0.2+)Yes: FlandersMod/Blood.dll64e8355e3967041c1feb8cad7fd2a368658a4ed95c761a4455ec4754f9f4b511

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))