Skip to content

Derezo/parasite

Repository files navigation

Parasite

Parasite title screen

A top-down, real-time, 8-bit-styled survival action-RPG for up to 25 concurrent players. Wake up in an open prison cell at the start of an extraterrestrial zombie outbreak, escape the prison, traverse a hostile forest, find a boat on the coast, and row out to a rescue ship. Persistent world, PvP everywhere, infection that spreads NPC-to-NPC in real time so the world degrades whether or not you play.

The look is deliberate: strict 256-colour indexed graphics at 480×270 internal resolution, scaled up to whatever display you're on, scored by an atmospheric soundtrack. SNES-era visual discipline applied to a modern dystopian apocalypse.

Dialogue with Frank in the prison   Combat against shamblers on the beach

Named NPCs remember every choice — and the shore is never as empty as it looks.


Download

Pre-built Linux x86_64 binaries: https://parasite.mittonvillage.com

The client downloads its asset bundle (~24 MB) on first launch over HTTPS, verified by SHA-256. After that, runs are offline-safe.

System requirements: Ubuntu 22.04 LTS or newer plus the Allegro 5.2 + libcurl runtime packages — see the in-tarball README.md or run ./check_deps.sh before launching.


For players

The game in one paragraph

You spawn in a cell. You craft a rope from the bedding, you scavenge the prison for food / medicine / a weapon, you climb out into the forest, you fight or sneak through hand-authored zones (treeline, logging road, a forester's camp, a river crossing, a gas station, coastal pines, the beach), you find an intact dinghy, you row out to the rescue ship, you board it, the run is over. Total run length: 1–3 hours of focused play. Die at any stage and you respawn back in a cell — same character, vitals reset, inventory mostly intact, one anchor item dropped at the death site for whoever finds it first.

Controls

Input Action
W A S D Move (8-directional)
Left Shift Sprint (drains stamina; no regen while held)
Mouse left Melee swing (cone hitbox) — or fire while aiming
Mouse right Aim (precision reticle for ranged weapons)
E Interact — talk to NPCs, open doors, pick up loot, board the rescue ship, advance dialogue
Tab Open / close inventory grid
M Open map
Enter Open chat input (commit with Enter, cancel with Esc)
V Push-to-talk for proximity voice
F1 / F2 / 3 / F4 Quick emotes (wave, point, nod, etc.) — slot 3 lives on the digit 3 key because F3 is reserved
F3 Toggle debug overlay (frame profiler)
C Craft cure from antiseptic + antibiotic + bandage
Esc Pause / back out of menus

Reloading is automatic: when the equipped firearm's magazine empties, a 5-second reload begins if you're carrying compatible ammo. There is no manual reload key.

Survival systems

  • Health (100 base). Bleeding from open wounds ticks until bandaged.
  • Hunger drops over time; below 25% your maximum HP is capped. Eating restores it.
  • Stamina drains while sprinting. Regenerates while walking, and 1.5× faster while standing still.
  • Infection is the central pressure. A bite from a zombie has a 15% chance to expose you. Once exposed, a 2-minute countdown starts (visible as a corrupting palette tint on the HUD). To cure: collect antiseptic + antibiotic + a bandage, press C to craft them into a single cure item, then use that cure from your inventory. (The three ingredients aren't applied individually — they're combined first, then the combined cure is consumed.) Inside the prison, all three ingredients live in the locked guard room, which needs a cell_key. The key drops from yard zombies at a 40% chance per kill, and the first kill in a fresh prison run is guaranteed — so if you clear the yard methodically you will always come out with at least one. If the timer expires you respawn — but you keep climbing only while exposed; once you cross into incubating, the parasite shuts down higher motor control and you cannot climb anymore. The 2-minute window is your chance to escape vertically before that happens.

Combat

Melee is the default. Bullets are scarce, loud (gunshots create a 30-tile alarm event), and reserved for emergencies. Weapons divide into:

  • Improvised melee — shanks, broken bottles, mop handles. Low damage, no durability cost.
  • Solid melee — batons, fire axes, machetes. Medium damage, durability.
  • Pistols, shotguns, rifles — found in the armoury and forest set-pieces. Each has its own audible signature.
  • Special — Molotovs and flares. Fire kills the parasite outright; flares distract.

Zombies have a 90° sight cone (~6 tiles), hear gunfire from 30 tiles and footsteps from 4 (sprinting). They cannot climb, cannot jump gaps, and move at ~25% of their base speed while idle, snapping to 100% on contact — so an idle horde looks dormant until you give it a reason.

PvP and the Marked system

PvP is always active everywhere. No safe zones, no opt-in, no mechanical immunity inside the prison. What stops the prison from devolving into a deathmatch is the Marked status: kill a named NPC (anywhere a named settlement exists — the prison, the forester's camp, the gas station) and your character is flagged. While Marked, guard NPCs across the map attack you on sight, and every named NPC refuses to talk or trade — the server replies INTERACT_DENY(MARKED) and the client renders a "they won't deal with you" cue. Killing anonymous infected or wandering zombies does not Mark you. The status persists for the rest of that character's life and is only cleared by death and respawn. Permadeath is the redemption mechanic.

A trade-window exploit lock prevents the "shoot them while they're confirming the trade" trick: the moment either party opens the trade window, a 10-second corpse-lock timer arms — and the items locked into the trade UI stay flagged on the corpse even if the dead party closes the window early.

Communication

  • Global text chat/say reaches everyone on the server. /me for actions, /w <name> for whisper. Up to 256 chars per /say / /me, 64 per whisper; client retains the last 32 entries with 6 visible.
  • Local emotes — fanned out to every player in the sender's 3×3 chunk interest window (so effectively everyone nearby on screen).
  • Proximity voice — Opus-encoded, 48 kHz, 20 ms frames, server-mixed. Client sends mono at 24 kbps; the server returns the spatial mix as stereo (24 kbps/channel, 48 kbps total). Full gain inside 2 tiles, linear falloff to silence at 16. Walls drop blocked talkers to 25% gain. Voice persists through death and respawn — being killed doesn't silence your radio.

The rescue and your run

Get to the beach, find a dinghy that's intact AND has oars (some are stove in, some don't), row ~2–3 minutes out to the ship, board the ladder. The run ends. The "You Survived" screen shows your final score, a leaderboard, and your per-run stats. Press Enter to return to title or Esc to exit. Your character is retired into a Survivors gallery and the next time you connect, you start fresh in a new cell.

Leaderboard scoring

The 'You Survived' run-end screen, with leaderboard and per-run stats

Each completed run produces a score. Higher is better. The formula is integer-clamped to [0, INT32_MAX]:

Positive contributions

Action Points
Zombie killed +10 each
Melee kill (any kind, on top of the kind-specific bonus) +25 each
Player killed (PvP) +50 each
Net items picked up minus dropped (clamped at zero) +5 each
NPC conversation initiated +20 each
Quest unlocked +100 each (the formula reserves this term but v1 ships with no quest registry, so the field always reads zero in practice)
Accuracy bonus +5 per integer percent (max +500 at 100%)
Damage dealt to other players +2 per HP

Negative contributions

Action Penalty
Damage taken from any source −1 per HP
Damage taken from other players −3 per HP (in addition to the above)
Shots fired (encourages efficiency) −1 each
Food eaten (encourages frugality) −2 each
Named NPC killed (this is what Marked tracks) −30 each
Time spent on the run −1 per 10 seconds

Killing zombies efficiently with melee and finishing fast rewards more than spraying ammo and grinding. Killing named NPCs is heavily penalised — that's the design lever that keeps the prison social rather than a free-for-all. The leaderboard shows the top 5 server-wide; if you don't appear in the top 5, your own row is appended below the cutoff with your actual score so you can see where you stand.


For developers and contributors

Architecture in one breath

A C11 client (Allegro 5.2) talks to a C11 headless Linux server over ENet on UDP 7777 (game) and a separate raw UDP socket on 7778 (Opus voice). Up to 25 concurrent players. The server is a single multi-threaded process with four threads — Net I/O, Sim (20 Hz), Voice Mix, DB Write — sharing world state under a mutex with an RCU-style 4-slot snapshot pool for the voice thread. Persistence is SQLite (WAL) plus a periodic world.bin snapshot. Auth is Ed25519 identity + X25519 ECDH → HKDF-SHA256 session keys + HMAC-SHA256 (16-byte truncated) per packet. No payload encryption in v1 — only authentication. Rendering is strict 256-colour indexed at 480×270 internal, upscaled by a fragment shader.

                                          ┌─────────────────────────────────────────────┐
                                          │              parasite_server                │
                                          │  ┌────────┐ ┌────────┐ ┌────────┐ ┌──────┐ │
        UDP 7777 (ENet, reliable + unrel) │  │ Net I/O│ │  Sim   │ │ Voice  │ │  DB  │ │
       ┌─────────────────────────────────►│  │        │◄┤ 20 Hz  ├►│  Mix   │ │ Write│ │
       │                                  │  │ ch0/1/2│ │ phases │ │ Opus   │ │ WAL  │ │
       │  ┌─────────────────────────────► │  └────────┘ └────┬───┘ └────────┘ └───┬──┘ │
       │  │ UDP 7778 (raw, Opus voice)    │                  │                    │    │
       │  │                               │       atomic 4-slot snapshot pool ────┘    │
parasite_client × N                       │       (sim publish → voice acquire)        │
( ≤25 concurrent )                        │                                            │
                                          │       SQLite (WAL) + world.bin snapshot ◄──┘
                                          └─────────────────────────────────────────────┘

Stack at a glance

Layer Technology
Language C11 (no compiler extensions, _Static_assert for layout invariants)
Build CMake 3.20+, out-of-tree in build/
Client framework Allegro 5.2.x (locked to 5.2)
Networking ENet 1.3.18 for game, raw UDP for voice
Crypto libsodium 1.0.20 (Ed25519 / X25519 / HKDF-SHA256 / HMAC-SHA256)
Voice codec libopus 1.5.2, 48 kHz, 20 ms frames, server-mixed
Persistence SQLite (vendored amalgamation), WAL, numbered SQL migrations
Config cJSON for asset tables; TOML-ish for server / client configs
Tests µunit (vendored), ctest at the top level, libFuzzer harnesses under tests/fuzz/
Asset pipeline Python codegen (tools/gen_*.py) emits paletted PNGs + C sources

Shared-core boundary

libparasite_core is a static library linked by both client and server. Anything that must agree on both sides — protocol structs, infection state machine, inventory grid rules, item registry, vec2 math, RNG, session-key derivation, replay window — lives there. core/ is forbidden from including Allegro, ENet, libsodium, SQLite, Opus, or cJSON headers, and from including anything under client/, server/, or platform/. The pre-commit hook enforces this with a grep. If it blocks an include you think is legitimate, the code is wrong, not the hook.

The infection state machine, inventory grid rules, and trade-lock timer exist in exactly one place — core/. Client-side prediction and server-side authority link the same implementation. Duplication of these in client/ or server/ is a bug.

Languages, toolchain, dependencies

  • C11 strictly. No GCC/Clang extensions unless guarded by feature macros. No VLAs in structs, no flexible array members in public headers, no computed gotos. _Static_assert is used liberally for compile-time invariants on struct sizes and protocol field offsets.
  • CMake 3.20+ at the top level. Out-of-tree builds in build/ (gitignored). Top-level targets: parasite_client, parasite_server, parasite_core (static lib), parasite_tests.
  • Vendored (third_party/, pinned): enet 1.3.18, libsodium 1.0.20, libopus 1.5.x, cjson, the sqlite3 amalgamation, µunit for tests.
  • System dep: Allegro 5.2.x. Locked to 5.2; do not adopt 5.3.

Build

Install Allegro 5.2 dev packages (Ubuntu 22.04+):

sudo apt-get install liballegro5-dev liballegro-image5-dev \
    liballegro-audio5-dev liballegro-acodec5-dev liballegro-ttf5-dev \
    liballegro-dialog5-dev

(Primitives / color / font ship inside liballegro5-dev on Ubuntu 24+; no separate packages needed.)

macOS: brew install allegro. Windows: vcpkg install allegro:x64-windows and pass -DCMAKE_TOOLCHAIN_FILE=... at configure.

First-time setup — vendored sources are fetched out-of-band, not committed:

./tools/fetch_third_party.sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
./tools/hooks/install.sh   # opt-in pre-commit (build + ctest + boundary grep)

Targeted builds (skip the client when Allegro is unavailable; skip tests for a faster server-only iteration):

cmake -S . -B build -DPARASITE_BUILD_CLIENT=OFF      # server + tests only
cmake --build build --target parasite_server         # one target

Run the binaries:

./build/server/parasite_server --config server/server.toml.example
./build/client/parasite_client --server 127.0.0.1:7777

Tests

ctest is enabled at the top level. µunit provides the framework.

ctest --test-dir build --output-on-failure              # full suite
ctest --test-dir build --output-on-failure -R <regex>   # subset by test name
./build/tests/parasite_tests --help                     # µunit flags
./build/tests/parasite_tests /core/infection            # single suite directly

Sanitizer builds

Use separate build trees — TSan is incompatible with ASan.

# ASan + UBSan
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=address,undefined
cmake --build build-asan --parallel
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ctest --test-dir build-asan --output-on-failure

# TSan
cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=thread
cmake --build build-tsan --parallel
./tools/run_tsan_tests.sh        # wraps ctest with setarch x86_64 -R

TSan needs an ASLR workaround on Linux x86-64. ThreadSanitizer uses a fixed shadow-memory layout that conflicts with the kernel's PIE + ASLR address randomization. Use tools/run_tsan_tests.sh instead of calling ctest directly; cmake/ParasiteHardening.cmake also disables -fPIE/-pie when PARASITE_SANITIZE=thread so the shadow region can be reserved at the expected address.

MSan (Clang only, experimental):

cmake -S . -B build-msan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=memory \
      -DCMAKE_C_COMPILER=clang
cmake --build build-msan --parallel

Fuzz targets (LibFuzzer, Clang)

cmake -S . -B build-fuzz \
      -DPARASITE_BUILD_FUZZ=ON \
      -DCMAKE_C_COMPILER=clang \
      -DPARASITE_SANITIZE=address,undefined
cmake --build build-fuzz --parallel
./build-fuzz/tests/fuzz/fuzz_protocol_auth -max_total_time=60

See tests/fuzz/README.md for per-harness details.

Static analysis

# scan-build (Clang static analyser)
scan-build cmake --build build 2>&1 | tee scan-build-report.txt

# clang-tidy (requires compile_commands.json)
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
run-clang-tidy -p build 'client/|server/|core/|tests/'

The .clang-tidy config at the repo root enables cert-*, bugprone-*, clang-analyzer-security-*, and readability-non-const-parameter check groups.

Local hooks, no remote CI

Verification runs on the developer's machine; there is no GitHub Actions setup. ./tools/hooks/install.sh installs a pre-commit hook that runs cmake --build build, ctest, and the core/ include-boundary grep. The hook is opt-in. If you genuinely need to bypass it (rare), git commit --no-verify works — don't make a habit of it.

Wire-format hygiene (cross-cutting invariants)

These are easy to violate by accident; reviewers will catch them, but knowing them up front saves a round trip.

  • Wire format is little-endian everywhere, including the on-disk world.bin and SQLite blobs. No struct-cast shortcuts; use the pst_write_*_le / pst_read_*_le helpers in core/protocol.h.
  • htonl/htons are forbidden for wire-format encoding. They are only legitimate in POSIX socket-struct setup (sockaddr_in.sin_port) where the kernel API contract requires them.
  • Floats and doubles are forbidden on the wire AND in server-local persistence. Positions and continuous quantities cross the wire as i16 at 1/16-tile (the _q15 suffix convention). world.bin and SQLite both store Q15 integers for player/NPC/item coordinates. Float varies by FPU rounding and breaks deterministic replay/reconciliation.
  • Sequence numbers compare via signed difference, never > or < directly. Direct unsigned comparison silently misbehaves at wraparound and breaks replay windows, prediction ring-buffer lookups, and acked-input-tick checks.
  • Replay protection is per-channel. Reliable channels (ch0) enforce strict monotonic seq; unreliable channels (ch1, ch2) use a 64-bit sliding bitmap. The INPUT-redundancy mechanism (3 backup commands per packet) depends on the bitmap — a strict monotonic check would defeat it.
  • HMAC and key comparison uses sodium_memcmp, not memcmp. memcmp leaks timing and is a forgery oracle.
  • Unpredictable values come from randombytes_buf (libsodium), never rand()/random()/srand(). Session IDs, challenge nonces, voice session IDs — anything an attacker must not predict.
  • Attacker-supplied strings are sanitized before logging. Player display names, chat text, BYE reason strings, and any other wire-sourced text have control bytes (0x000x1F, 0x7F) stripped or escaped before they reach the logger. Raw bytes enable ANSI-escape terminal hijacking and log-line forgery.

Threading rules

  • Game state mutates only on the sim thread. Other threads read via the RCU snapshot pattern documented in docs/SERVER_ARCHITECTURE.md §3.3 — atomic pointer exchange, old snapshots freed after a 2-tick (~100 ms) grace period. This is the canonical pub/sub pattern; use it whenever sim publishes read-mostly state.
  • Identity-key memory protection. The Ed25519 secret lives in sodium_malloc-allocated memory marked sodium_mprotect_noaccess between uses. sodium_mprotect_readonly brackets every sign call, after which the region returns to noaccess. Never store the secret in a stack buffer or normal heap.

Asset pipeline

All sprites, tilesheets, fonts, dialogue tables, item tables, SFX, and .pmap map files are generated by Python scripts in tools/ (gen_*.py). CMake wires them in and re-runs them on input change. The generators emit C source or paletted PNG output as appropriate, all keyed to a single master palette at assets/palette/master.gpl. Patch the JSON sources or the generator, never the emitted file. gen_*.py output under core/generated/ carries a "do not edit" banner.

The palette discipline is strict: every paletted PNG uses indices from the master palette, and the entire UI renders into the same indexed framebuffer — there is no separate RGB compositing layer. Day/night blending, infection tint, open-water tinting, water-cycle animation, and fire flicker are all palette LUT operations, not per-pixel shading.

Performance budgets

Subsystem Budget
Client frame rate 60 fps locked at 480×270 internal (any display res)
Client memory ceiling 256 MB resident
Server tick 20 Hz game sim
Snapshot rate per client 10 Hz
Server CPU per tick 50 ms total across input apply, AI, infection sim, snapshot encode, DB queue, voice mix
Voice mix ≤ 5 ms per 20 ms tick for 25 concurrent talkers (target = 25% of one core)
Server memory ceiling 1 GB resident on a 2 GB VPS
Bandwidth per client ≤ 64 kbps game + ≤ 32 kbps voice (one talker burst)

Versioning & migrations

  • world.bin and the wire protocol both serialise field-by-field. Any struct-layout change bumps the relevant version field (PST_PROTOCOL_VERSION in core/protocol.h; schema_version in world.bin). The changelog comment at the top of core/protocol.h is the source of truth.
  • SQLite migrations are plain SQL under server/migrations/NNN_description.sql, applied in order on startup and tracked by a schema_version table.
  • Wire-format correctness tests live under tests/core/test_protocol_*.c and gate the pre-commit hook. JSON test vectors under tests/protocol/vectors/ drive a language-agnostic vector runner. Any change to a packet's byte layout MUST update the matching _Static_assert, the C test, and the JSON vector — pre-commit catches a missed one.

Subagents (Claude Code workflow)

This repository includes project-scoped subagent definitions under .claude/agents/. They pre-load the relevant docs sections and are preferred over general-purpose whenever the work matches their domain:

  • c-developer — C11 hygiene, build system, threading conventions, cross-platform portability.
  • game-developer — game loop, entity model, AI, world sim, palette pipeline, content pipeline.
  • network-engineer — ENet, packet protocol, handshake, snapshot delta encoding, prediction/reconciliation, security.
  • voice-engineer — Opus encode/decode, voice UDP transport, jitter buffer, server-side spatial mixing.
  • security-auditor — read-only vulnerability audits for the networking code (memory safety, integer arithmetic, crypto misuse, auth/replay, concurrency).

These are optional; you don't need to use them to contribute. They exist because the codebase has wide cross-cutting invariants and pre-loading the right docs section is the single biggest accuracy win for AI-assisted work.

Documentation index

Document Owns
docs/GAME_DESIGN.md Gameplay rules, infection model, PvP/Marked semantics, canonical constants
docs/TECHNICAL_REQUIREMENTS.md Language/toolchain, deps, palette pipeline, perf budgets, security surface
docs/SERVER_ARCHITECTURE.md Server thread model, world sim, SQLite schema, deploy/systemd
docs/CLIENT_ARCHITECTURE.md Client modules, render pipeline, state machine, input, audio
docs/NETWORK_PROTOCOL.md Wire format, packet types, handshake, snapshots, voice
docs/DEPLOYMENT.md VPS provisioning, deploy scripts, backup, admin socket
docs/ROADMAP.md Milestone schedule and parallel-track guidance

When code and docs disagree, fix one or the other in the same PR — never leave them in conflict.

Status

v1 is shipped (current release: see VERSION). See docs/ROADMAP.md for the per-milestone closure list and the small post-v1 backlog.

Working with assets locally

assets/ is not tracked in git. To work on the game locally, get a copy of assets/ by one of:

  • Regenerate from source. Every authored asset has a tools/gen_*.py generator that runs as part of the CMake build:

    cmake --build build --target parasite_assets
  • Download the prebuilt bundle.

    curl -fLO https://parasite.mittonvillage.com/downloads/parasite-assets-v$(cat VERSION).tar.gz
    tar xzf parasite-assets-v$(cat VERSION).tar.gz

Contributing

See CONTRIBUTING.md for the dev setup, commit conventions, and the rules around the core/ boundary.

License

zlib. See LICENSE.

About

The zombies are coming

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors