Files
simple-updater/README.md
T
2026-09-09 01:42:52 +03:00

9.3 KiB

Universal Game Updater

A simple, self-hosted updater for distributing game client files. Consists of three parts:

  • updater.py — the client-side app (compiled to updater.exe) that players run. Checks the server for changed files and downloads only what's needed.
  • server.py — a lightweight HTTP server that serves client files and manifests.
  • generate_manifest.py — a script you run on your build machine to generate the manifest the server distributes.
  • generate_key.py — a helper to generate access keys (see Access keys below).

Works with any game — just fill in the configuration blocks at the top of each script.

How it works

  1. You put your game's files in a folder on the server (e.g. game_client/).
  2. You run generate_manifest.py on that folder. It walks the folder, hashes every file (MD5), and writes a manifest.json listing metadata for every file. It also gzip-compresses every file into a <file>.gz copy next to the original — if the compressed copy isn't actually smaller (common for already-compressed formats like .jpg, .mp3, .zip), the .gz copy is discarded and the manifest just records the original.
  3. server.py serves that folder over HTTP, keyed by an access key defined in config.json.
  4. Players run updater.exe. It downloads the server's manifest.json, compares hashes against their local files, and for anything missing or different: downloads the .gz copy if one exists, verifies it, decompresses it locally, and writes the result — or downloads the original directly if there's no compressed copy.
  5. Once everything matches, the "Play" button launches the game executable.

Setup

1. Server

Install Python 3 (no external dependencies needed — everything uses the standard library).

Generate an access key for each client version you want to distribute:

python generate_key.py

This prints a random 64-character hex string. Generate one per client (e.g. one for main, one for test).

Edit config.json:

{
    "host": "0.0.0.0",
    "port": 3232,
    "clients": {
        "<your-generated-hex-key>": "game_client",
        "<another-generated-hex-key>": "game_client_test"
    }
}
  • host / port — what the server listens on.
  • clients — maps an access key to the folder (relative to server.py) that key serves.

Put your game files in the folders referenced above (game_client/, etc.), then generate the manifest for each:

python generate_manifest.py game_client
python generate_manifest.py game_client_test

Re-run this after every update to the client files, then start (or restart) the server:

python server.py

Files become available at http://<server-address>:<port>/<access-key>/....

2. Updater (client)

Edit the configuration block at the top of updater.py:

GAME_NAME = "My Game"
UPDATER_NAME = "Launcher"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 300
BANNER_WIDTH = 430
BANNER_HEIGHT = 140

SERVER_ADDRESS = "update.mygame.com"   # domain or IP
SERVER_PORT = "443"                    # 80/443 for HTTP/HTTPS, or a custom port for direct IP
USE_HTTPS = True

CLIENT_KEYS = {
    "Main": "<same-hex-key-as-in-server-config.json>",
    "Test": "<same-hex-key-as-in-server-config.json>",
}
DEFAULT_CLIENT = "Main"

GAME_EXE_RELATIVE_PATH = os.path.join("system", "game.exe")
BANNER_RELATIVE_PATH = "banner.png"
ICON_RELATIVE_PATH = "icon.ico"
  • SERVER_ADDRESS accepts either a domain name or a raw IP — nothing else needs to change.
  • SERVER_PORT / USE_HTTPS should match how your server is actually reachable. If nginx force-redirects port 80 to 443, set SERVER_PORT = "443" and USE_HTTPS = True directly rather than relying on the redirect — the updater downloads files one request at a time, so an extra 80→443 redirect on every file adds real overhead on large clients.
  • CLIENT_KEYS maps a friendly name shown in the UI to the actual access key sent in the request URL. Keys must match config.json on the server exactly.
  • GAME_EXE_RELATIVE_PATH is where the updater looks for the game's executable, relative to the folder the updater itself is placed in.
  • BANNER_WIDTH / BANNER_HEIGHT — the banner image is always scaled to exactly this size, so it doesn't matter what dimensions the source banner.png actually has; the layout won't shift.

Banner scaling uses Pillow if it's installed (pip install Pillow), for proper resizing. If Pillow isn't available, the updater falls back to Tkinter's built-in (cruder, integer-only) zoom/subsample — it'll still work, just less precisely scaled. Install Pillow for best results, and make sure it's bundled when compiling (see below).

Then compile with your favorite tool, e.g. PyInstaller:

pyinstaller --onefile --noconsole --icon=icon.ico --add-data "banner.png;." --add-data "icon.ico;." updater.py

Ship the resulting updater.exe alongside the game folder.

Settings

On first run, the updater creates a settings.ini file next to itself with the selected client version:

[updater]
client = Main

Whenever the player changes the client version dropdown, it's saved immediately so the choice persists across restarts. If settings.ini is missing, corrupted, or contains an unrecognized client name, the updater silently falls back to DEFAULT_CLIENT and continues — it never crashes over a bad settings file.

This is currently the only setting. To add more later, extend load_settings() / save_settings() in updater.py with additional keys under the same [updater] section.

Access keys

Client folders are addressed by a long random hex key instead of a plain name like main, so the update path isn't obvious or easy to stumble on by guessing.

This is obscurity, not real security. The key has to live inside updater.exe for it to work, and anyone motivated enough to disassemble the executable or inspect network traffic can recover it. Don't rely on it to gate anything sensitive.

If a key leaks and you want to rotate it:

  1. Generate a new key with generate_key.py.
  2. Update it in the server's config.json.
  3. Update it in updater.py's CLIENT_KEYS.
  4. Rebuild and redistribute updater.exe.

No folder renaming or manifest regeneration needed.

Ignoring files during manifest generation

Edit the configuration block at the top of generate_manifest.py:

IGNORE_FILES = {
    MANIFEST_FILENAME,
    "updater.exe",
    "Option.ini",
    "WindowsInfo.ini",
}

IGNORE_FOLDERS = {
    "screenshots",
    "logs",
    "replays",
}

IGNORE_EXTENSIONS = {
    ".log",
}

Add any player-specific files (settings, saves, screenshots, logs) here so the updater never overwrites or deletes them.

Compression

generate_manifest.py gzip-compresses every distributable file into a <file>.gz copy alongside the original, unconditionally — no file-type filtering. If the compressed copy ends up the same size or larger than the original (common for already-compressed formats), it's discarded automatically and that file is just served uncompressed; there's nothing to configure per file type.

The manifest records both the original file's hash/size and, when a compressed copy exists, its hash/size too:

{
    "script.txt": {
        "hash": "d3650c70e7d942736c3ca77daf381262",
        "size": 60000,
        "compressed": true,
        "compressed_size": 177,
        "compressed_hash": "852ab2df07a4c65cddd6ed3ec0282301"
    },
    "texture.bin": {
        "hash": "1b6d041423b9863dee54e87fed1f0cdd",
        "size": 20000,
        "compressed": false
    }
}

updater.py uses this to download the smaller .gz copy when one is available, verifies its hash, decompresses it in memory, and verifies the decompressed result against the original hash before writing it to disk. If anything about the compressed copy fails (missing, corrupted, hash mismatch), it automatically falls back to downloading the uncompressed original — a bad .gz on the server never blocks an update.

Two settings control this, and must be kept in sync between generate_manifest.py and updater.py:

  • COMPRESSED_SUFFIX (default .gz) — the suffix used for compressed copies.
  • COMPRESSION_LEVEL (default 6, in generate_manifest.py only) — gzip level 1 (fastest, larger) to 9 (slowest, smallest).

Re-run generate_manifest.py after any change to the client files — it regenerates both the manifest and the .gz copies. Compressed output is generated with a fixed mtime so re-running it without any real file changes produces byte-identical .gz files and stable hashes.

Notes / limitations

  • File integrity is checked with MD5 — fine for detecting changed files, not intended as a cryptographic guarantee.
  • Compression roughly doubles the disk space needed on the server for each client version (original + .gz copies), in exchange for faster downloads for compressible files.
  • The server has no built-in rate limiting or HTTPS; put it behind a reverse proxy (nginx, Caddy) if you need either.
  • The server never serves directory listings — a bare /, an unknown key, or a path that tries to escape the client folder (..) all get a 404, including the server's own script/config files.
  • updater.py uses Tkinter for the UI, which ships with most standard Python installs on Windows.