station/README.md

8.7 KiB
Raw Permalink Blame History

Station

A persistent-world, RPG-style station simulation inspired by Space Station 13, built with Bevy in Rust.

  • ECS – all game state lives in Bevy entities and components.
  • Data driven – items, recipes, buildings and skills are defined in rsn files under assets/data/.
  • Scriptable – behaviour is written in Rune scripts under assets/scripts/.
  • Tiled maps – maps are regular Tiled .tmx files.
  • Persistent world – no rounds. The world and every character (inventory, skills, vitals, position) are saved and restored across restarts.
  • Singleplayer and multiplayer – one authoritative server simulation, used locally in singleplayer and over UDP in multiplayer (bevy_replicon + renet).

Crates

Crate Purpose
crates/game_core Shared data definitions, components, Tiled loader, network protocol
crates/game_server Authoritative simulation, Rune scripting, persistence, game-server binary
crates/game_client Rendering, UI and input (game binary); embeds the server for singleplayer/host

The client never changes game state directly. It sends requests (Login, SetMoveDir, Interact, Craft, …) and the server replicates the resulting state back. In singleplayer the same server systems run inside the client process, so singleplayer and multiplayer behave identically.

Running

cargo run                                   # singleplayer (world: saves/singleplayer.rsn)
cargo run -- --host 5000                    # singleplayer world that others can join
cargo run -- --connect 127.0.0.1:5000       # join a server or host
cargo run -p game_server --bin game-server  # dedicated server (world: saves/server.rsn)

Client options: --name NAME, --save FILE, --no-save, --map MAP. Server options: --port PORT, --save FILE, --no-save, --map MAP.

On Windows, release builds of the client open no console window, so command-line output such as --help and logs isn't shown. Use a debug build to see it.

The world autosaves periodically and on exit (window close, Ctrl-C, SIGTERM). Characters are identified by name and are restored when that name logs in again.

Controls

Key Action
WASD / arrows Move (you face the direction you move)
E Pick up (own tile, otherwise the faced tile)
F / left click Interact with the faced / clicked building
U Use the selected item (eat, heal, …)
G Drop the selected item
X Deconstruct the faced building
1–9, 0, mouse wheel Select an inventory slot
C / B Crafting / build menu (W/S select, Enter confirm, Esc close)
H Help

Modding

All content is loaded from assets/ at startup. Every .rsn file below assets/data/ (recursively) may contain any mix of items, recipes, buildings and skills, so a feature can ship in a single file. References between prototypes and script hooks are validated at load time and errors name the offending file and id.

Items

items: [
    { id: "apple", name: "Apple", sprite: "sprites/items/apple.png", max_stack: 10,
      tags: ["food"], on_use: Some("food::eat"), consume_on_use: true,
      props: { "nutrition": 15 } },
]

Required: id, name, sprite. Optional: description, max_stack (default 50), tags, on_use (script hook), consume_on_use, props (numbers readable by scripts).

Recipes

recipes: [
    { id: "smelt_iron", name: Some("Smelt iron plate"),
      inputs: [("iron_ore", 2)], outputs: [("iron_plate", 1)],
      tools: [], station: Some("furnace"),
      skill: Some({ skill: "smithing", level: 1 }), xp: 10 },
]

Required: id, inputs, outputs. Optional: name, tools (needed but not consumed), station (a tag provided by a building adjacent to the player), skill, xp, on_craft (script hook). Ingredients are written as ("item", count) or { item: "item", count: n }.

Buildings

Anything placed on the grid: walls, doors, machines, trees, rocks.

buildings: [
    { id: "door", name: "Door", sprite: "sprites/buildings/door_closed.png",
      variants: { "open": "sprites/buildings/door_open.png" },
      construction: Some({ materials: [("iron_plate", 2), ("plank", 1)],
                           tools: ["screwdriver"], xp: 10,
                           skill: Some({ skill: "construction", level: 1 }) }),
      on_interact: Some("door::toggle") },
]

Required: id, name, sprite. Optional: description, variants (name → sprite), solid (default true), max_health, stations (tags for recipes), construction (makes it buildable from the build menu), deconstructible (default true; refunds materials), on_interact (script hook), vars (integer state, persisted per building).

Skills

skills: [ { id: "smithing", name: "Smithing" } ]

Scripts

Each .rn file in assets/scripts/ is a module named after its path (scripts/door.rn → door, scripts/npc/guard.rn → npc::guard). Data files refer to public functions as "module::function". Every hook receives one argument, ctx; returning false signals failure (for example, a used item is then not consumed). Script errors are logged and have no effect on the world.

// assets/scripts/door.rn
pub fn toggle(ctx) {
    if ctx.is_solid() {
        ctx.set_solid(false);
        ctx.set_variant("open");
        ctx.message("You open the door.");
    } else {
        ctx.set_solid(true);
        ctx.set_variant("");
        ctx.message("You close the door.");
    }
}

ctx methods:

Area Methods
Actor actor_name(), message(text)
Inventory item_count(id), has_item(id, n), give_item(id, n), take_item(id, n) -> bool, spawn_item(id, n) (on the ground)
Vitals health(), max_health(), heal(n), damage(n), nutrition(), feed(n) (integers)
Skills skill_level(id), add_xp(id, n)
Used item item_id(), item_prop(key)
Building target_name(), get_var(key), set_var(key, n), is_solid(), set_solid(b), variant(), set_variant(name) ("" = default sprite), destroy_target()
Misc random(max) → 0..max

Maps (Tiled)

Maps are orthogonal .tmx files with embedded or external tilesets. Each map cell is one game tile (drawn at 32×32; 32 px tilesets look best).

  • Tile layers are rendered in order. A tile is solid if the tile has the boolean custom property solid = true in its tileset, or its layer has solid = true.
  • Object layers spawn gameplay entities by object class:
    • spawn – player spawn point.
    • building – custom property proto = building id; any integer properties override the building's vars.
    • item – custom property proto = item id and optional count.
  • Objects are placed on the tile containing their position (point objects work well).

A map is only used to create a new world; after that the save file is authoritative. Delete the save file to start a fresh world.

tools/gen_placeholder_assets.py regenerates the placeholder sprites and example map.

Development

cargo nextest run --workspace   # unit tests + headless full-simulation test
cargo clippy --workspace --all-targets

Tests run with cargo-nextest (cargo install cargo-nextest --locked); its configuration is in .config/nextest.toml. CI uses --profile ci, which writes a JUnit report to target/nextest/ci/junit.xml.

Packaging

cargo xtask pack                                  # for the host platform
cargo xtask pack --target x86_64-pc-windows-gnu   # cross-compile for Windows (needs MinGW)

pack builds the client (game) and the dedicated server (game-server) with the stripped dist profile and writes dist/station-<version>-<target>.zip (Windows) or .tar.gz (other platforms). The archive contains both binaries, assets/, the README and the changelog. Unpack it and run the binaries from that directory. The unpacked staging directory is kept next to the archive. Options: --format zip|tar.gz, --out-dir DIR, --debug.