Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Resiege

Quality-of-life and content patches for the original Dungeon Siege (2002) — the first game, by Gas Powered Games, as sold by GOG and Steam. Not Dungeon Siege II, not Dungeon Siege III, and not the Legends of Aranna expansion.

Built and run on Linux, against the Windows game under Wine/Proton, with no Siege Editor and no Windows toolchain required. The tools read and rewrite the game's own .dsres/.dsmap tank archives directly. Most of what this adds is content the game already ships and does not offer you.

It is an unofficial fan project with no affiliation to Gas Powered Games, Microsoft, Square Enix or GOG. It ships no game assets — you need your own copy of Dungeon Siege.

Evidence for every change is in docs/qol-backlog.md; recon notes are in docs/findings.md.

The multiplayer half is a separate repo

Dungeon Siege's ZoneMatch matchmaking service was shut down years ago. Bringing it back — a self-hosted lobby, the MOTD server, the DirectPlay work and the proxy DLL — lives in a companion repo, zonematch, not here.

The split is by who installs it: this repo is what a player installs, zonematch is what runs on a machine other people connect to. point-at-server.sh and both exe patches are here rather than there, because a player installs them and a server operator does not.

Setup

Every tool reads your install location from one environment variable. The GOG, Steam and disc installs all land somewhere different, so nothing here guesses: each tool checks that the path it was given actually holds a Logic.dsres (or a DungeonSiege.exe) and refuses with a message naming what it wanted.

There is a last-resort fallback to ~/Games/Dungeon Siege, which fails that check on most machines and is therefore not a working default. It is a real one on a machine where the game is there — so if you keep a second install for testing (second-install.sh), set the variable rather than relying on it:

export DS_GAME_DIR="/path/to/Dungeon Siege"
export DS_PREFIX="$HOME/Games/Heroic/Prefixes/default/Dungeon Siege"  # Wine prefix

Or put both in a .dsrc at the repo root, which game-mode.sh sources and which is gitignored — it is the one file here allowed to name a real directory on a real machine:

# .dsrc
: "${DS_GAME_DIR:=/path/to/Dungeon Siege}"
: "${DS_PREFIX:=$HOME/Games/Heroic/Prefixes/default/Dungeon Siege}"

The := form means an existing environment value still wins, so a one-off DS_GAME_DIR=/other ./tools/game-mode.sh … overrides the file for one command.

Then:

./tools/game-mode.sh choose     # build and install
./tools/game-mode.sh status     # what is actually installed right now
./tools/game-mode.sh classic    # the world as shipped

Requirements: Python 3.11+ and bash. That is all. Every tool runs on the standard library, so you can clone this and use it with no package manager at all. python-lzo is optional and needed only for an LZO-compressed tank — the shipped ones are zlib, and tank.py turns the missing import into an explanation rather than a traceback. If you use uv:

uv sync                # dev tools (ruff, pyright)
uv sync --extra lzo    # ...and python-lzo, if you hit an LZO tank

Nothing here contacts the network, with exactly one opt-in exception: if you pass a URL to build_qol_mod.py --seed, it fetches that URL to read a world seed your group publishes. No telemetry, no update check, no downloads.

game-mode.sh is the only supported way to build. Anything not in its BASE array or in a build default is not shipping, whatever a module docstring says — that rule was learned twice, from features that silently un-shipped themselves.

How it installs, and what it never does

The build patches the tanks in your own install, in place, keeping a pristine *.orig beside each and always building from that. It ships no game assets and redistributes nothing copyrighted — you need your own copy of the game, and that is deliberate.

Add-on tanks in Mods/ and Maps/ are silently ignored by the engine, and a same-path file in a higher-priority .dsres does not override Logic.dsres (findings 28) — which is why in-place patching is the delivery route rather than a choice.

tools/install_state.py prints what differs from stock and which repo artifact explains it.

Nothing here is an opaque binary

Every byte this project puts into your install is auditable, and there is a command that proves it rather than a promise that it is so.

Exe patches ship as annotated assembly, not hex — docs/patches.md is the disassembly with the reasoning for each one, and it is generated, not hand-maintained:

tools/patch_exe.py --status       # which patches are applied to your exe
tools/patch_exe.py --audit        # regenerate the reviewable patch document
tools/patch_exe.py --verify-asm   # disassemble each patch's bytes and check
                                  # them against the listings in the document

Every patch is reversible with --revert.

The plugin DLL (tools/dsdll/dsr_fubi.dsdll) is the one compiled artifact in the repo, and it is committed next to its source — 163 lines of hand-written x86 in dsr_fubi.S, a nine-name .def, and a build script — so that you do not need a Windows cross-compiler just to use the camera sliders. Two commands settle whether the committed bytes are what the committed source says:

cd tools/dsdll
./verify_offsets.py   # read the offsets back OUT of the DLL and check each
                      # one against your own DungeonSiege.exe. No toolchain
                      # needed -- this is the check that matters.
./build.sh            # rebuild from source and diff against the committed
                      # binary. Needs mingw-w64 (Arch: mingw-w64-gcc,
                      # Debian/Ubuntu: gcc-mingw-w64-i686).

The build is deliberately reproducible — the link pins the image base, zeroes the PE timestamp and disables the build-id, because a binary you cannot rebuild bit-for-bit is a binary nobody can check. A clean ./build.sh ends in reproduces the committed binary byte for byte; if it does not, it replaces the binary and tells you, and verify_offsets.py prints the disassembly that settles which side was wrong.

What it does

  • Companions in multiplayer. Kingdom of Ehb shipped ten recruitable companions with the join offer stripped out; the Utraean Peninsula shipped none. Both fixed, the second by re-templating a standing townsperson.
  • Conscripts — filler party members placed in the world and recruited by talking to them, so a party can be filled without eight named companions existing.
  • Monster variation at runtime. Multiplayer keeps no world state, so every session restarts and the road between towns is the same road every time. A component baked onto 117 monster templates picks a power-and-role-matched substitute as each actor enters the world, covering ~9,100 placed actors across both maps. Host-only, and the choice replicates — so there is no seed to agree on, no rebuild between sessions, and nothing for joiners to install. The Peninsula's World Difficulty dropdown gains a varied twin of every setting, so the host chooses in the create-game screen.
  • Content the game ships and does not offer. The character creator was reading a hand-written shortlist, not enumerating the art: 76 of 207 shipped textures. Five more playable models cost nothing. 113 of the 127 gated monster spells are made holdable and priced by fitting required_level against the shipped start gates rather than by taste. 62 gated equipment templates — the Gobbot arsenal, the Seck/Ice/Droog lines, named boss weapons — are unlocked and priced with GPG's own calc_gold_value$ formula.
  • Draw distance. The camera pullback, the distance fog and the streaming radius are one system: raise the fog alone and the horizon goes black, pull the camera back alone and the frame washes out. --see-far sets all three from one number and refuses the combinations that do not hold.
  • Camera controls in the game's own options menu. Options > Game > More gains Minimum/Maximum Camera Azimuth and Distance, and they move the live camera. This is the first thing here that needs code rather than data: the exe exports the four camera getters and no setter, and UISlider is the one UI control class it does not register with FuBi. tools/dsdll/ is a nine- export plugin DLL that supplies both, as annotated assembly with every offset cross-checked against the exe's own getters. The engine has scanned for *.dsdll since 2002 — see docs/findings.md section 47.
  • Render resolution, which turned out to be an INI setting rather than a patch, plus the resolutions the video menu never offered.
  • Modern GPUs, recognised as such. config/system_detail.gas is a hardware database from 2002, and a card that is not in it inherits vendor defaults written for pre-Radeon ATI parts: a reduced render path, downgraded shadows, no trilinear filtering, "below minimum spec". Every real Radeon in the file overrides all of it; a modern one gets the Rage profile only because it is not in the list. Unrecognised cards now get the profile GPG gave the newest card they supported. Measured effect on frame rate: none so far — it is on because what it says about the hardware is true, not because it bought anything. docs/performance.md has that measurement, the flag whose name means the opposite of what it looks like, and the DungeonSiege.ini keys that override the database outright.
  • Exe patches for the ZoneMatch update nag and multiplayer portrait selection, distributed as annotated assembly rather than opaque binaries.

Known dead ends are recorded as such in the backlog rather than deleted, because several of them looked correct for days first.

Layout

LICENSE                   MIT, plus what it does and does not cover
pyproject.toml            uv project + ruff config -- and why each waiver exists
uv.lock                   pinned dev tools, so `uv sync` is reproducible
pyrightconfig.json        type checking: standard everywhere, strict per file
typings/lzo.pyi           stub for python-lzo, which ships none
.dsrc                     YOUR install paths. Gitignored; see Setup

docs/qol-backlog.md       what we want to build, with the evidence behind it
docs/findings.md          recon evidence — tanks, content database, the client
docs/performance.md       why the draw distance costs what it does, and the
                          knobs that are not game settings
docs/patches.md           the exe patches, as annotated assembly
docs/install-baseline.json  sha256 of a stock install, for drift detection

tools/game-mode.sh        THE ENTRY POINT. Build, install, and report status
tools/tank.py             read, rewrite and add to .dsres/.dsmap tanks (no Wine)
tools/build_qol_mod.py    the content mod: patches the shipped tanks in place
tools/patch_exe.py        exe patches, as annotated assembly rather than hex
tools/install_state.py    what differs from stock, and what explains it
tools/perf.py             render-path knobs: which D3D device, CPU pinning, DXVK
tools/hotstacks.py        where the CPU time goes: per-thread load, sampled stacks
tools/dxvk.conf           DXVK tuning for the ddraw->d3d9 wrapper path
tools/party_saves.py      inspect and tidy the multiplayer character list

tools/dsdll/              the FuBi plugin DLL: what the engine cannot be told
                          in data alone. Assembly, a .def, and a verifier that
                          checks every offset against your own exe

tools/monsters.py         monster power/role/size model + the substitution plan
tools/monster_census.py   what each map places, how strong it is, what is wired
tools/spells.py           the spell model: triage and pricing for monster spells
tools/items.py            the item model: level, band power, GPG's gold formula
tools/characters.py       the creator's skin lists, and what a playable needs
tools/unlock_census.py    what the game ships and does not offer you

tools/point-at-server.sh  repoints DungeonSiege.ini at a lobby, with backup
tools/second-client.sh    launch a second client for two-player testing
tools/second-install.sh   a hardlinked 2nd install, for testing MISMATCHED content

Artifacts this build writes into your install are prefixed dsrResources/zz-dsr-qol.dsres, .dsr-build.json, dsr_fubi.dsdll, and a // dsr: comment on every generated line of .gas. It stands for Dungeon Siege Resiege, and it is there so that install_state.py — and you — can tell at a glance which files in a 1.1 GB install came from this repo.

Working on the tools

uv sync                        # once: ruff and pyright, pinned by uv.lock
uv run ruff check tools/       # lint
uv run pyright                 # type check

Without uv, uvx ruff check tools/ and uvx --from pyright pyright do the same thing without installing anything, and the tools themselves need neither.

Both are clean, and are meant to stay that way. Type checking runs at pyright's standard level across tools/, with a per-file strict list in pyrightconfig.json that files join once they are fully annotated — tank.py is on it. strict across the whole tree reports ~5,500 problems, of which about 130 are real; standard reports the real ones and nothing else, which is why the strictness is ratcheted per file rather than set globally.

Every lint waiver in pyproject.toml carries the reason it is waived. The large one is UP031: these modules generate .gas and .skrit, both brace-delimited, so f-strings would mean doubling every brace in the emitted text.

Ruff's target-version and pyright's pythonVersion are both pinned to the requires-python in pyproject.toml (3.11), not to whatever is installed — otherwise the linters would happily suggest idioms that break for anyone on the oldest version this claims to support.

The one structural thing still unsolved

Mods/ scanning does not work (findings 9c). It is the difference between a distributable mod and patching the stock tanks in place. Everything here becomes shippable to other people the moment that is solved, so it gates the rest.

License

MIT, for this project's own source: the Python tools, the shell scripts, the hand-written x86 in tools/dsdll/, and the documentation.

It covers nothing else. No game assets are included or redistributed — every tool here operates on a copy of Dungeon Siege that you already own and installed yourself, and docs/install-baseline.json holds SHA-256 hashes of a stock install, not any of its contents. Dungeon Siege is a trademark of its respective owners; this project is unofficial, non-commercial, and unaffiliated with Gas Powered Games, Microsoft, Square Enix or GOG.

About

All-in-one enhancement and quality of life mod for the original Dungeon Siege

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages