- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass / water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon - Client: title / join / character creator / play screens, prediction + reconciliation, interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection) - Shared deterministic zone generator, movement, protocol, roster, appearance - Paper-doll character art pipeline (PixelLab base walk + per-item animation edits) - Brandling roster from the company list (logo colors, jev types), Blender safari props - Headless test bot
Brandling Safari
A small LAN-multiplayer creature-catching game set in a safari zone, rendered in the HD-2D style (Octopath Traveler / Triangle Strategy) with the Vibe-HD-2D engine. The creatures, Brandlings, are named after real companies: each one's sprite is generated by Company_Sprite_Gen from the company name, recolored with its logo's brand colors, and its type comes from the jev business classifier.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Features
- Meadow Safari Zone: terraced grass diorama with a north ridge and an east plateau, a lake with a fishing dock, a pond, eight tall-grass patches, an entrance gate and plaza with Professor Henry's field tent, lamps, fences and props (3D models built in Blender, pixel-art ground and foliage).
- Character creator (persists server-side): masculine / feminine base body, 6 skin tones, 5 hair styles, 3 hats, 5 tops, 4 bottoms, 3 shoes, any color per slot. Every combination is animated by one shared walk cycle (see Character art).
- Professor Henry greets new rangers with an intro, hands out 30 Safari Capsules and restocks them later.
- Finding Brandlings: they burst out of tall grass around you as you walk (more often when running), swim in the lake and surface with splashes, wander the meadow, and bite on a fishing line cast from the shore or dock.
- Catching: throw a Safari Capsule (closer and calmer = better odds; hooked ones are dazed); misses can make them flee. Caught Brandlings join your party (up to 6) and follow behind you in a chain, bouncing and turning to face their direction; the rest go to your box.
- Multiplayer: dedicated authoritative server, LAN discovery, client-side prediction with server reconciliation, interpolated remote players and their parties, chat bubbles.
- World state from the server: time of day (dawn, day, dusk, night with stars, moon and lantern light) and weather (clear, cloudy, rain, thunderstorm, mist) change the look on every client and which Brandlings appear.
Building
Requirements: CMake >= 3.21, a C++20 compiler, OpenGL 4.5, libepoxy, SQLite 3, and the
engine checked out next to this repo (../Vibe-HD-2D; set HD2D_ENGINE_DIR otherwise, or
leave it missing to fetch it from HD2D_ENGINE_GIT). GLFW, GLM, stb, cgltf and Dear ImGui
come from the engine; nlohmann/json is used from the system or fetched, ENet is always fetched.
# Arch / CachyOS
sudo pacman -S cmake ninja glfw glm libepoxy stb sqlite nlohmann-json
cmake -S . -B build -G Ninja
ninja -C build # safari_client, safari_server, safari_bot
Server only (no GPU needed): cmake -S . -B build_srv -G Ninja -DBS_BUILD_CLIENT=OFF.
Playing
./build/safari_client # title screen: Host & Play, or Join a LAN Game
- Host & Play starts
safari_servernext to the client on port 27420 and joins it. Other players on the network see it under Join a LAN Game (UDP broadcast on 27421) or can type the host's address. - A dedicated server:
./build/safari_server [--port 27420] [--name "My Safari"].
| Input | Action |
|---|---|
| WASD / arrows | Move (camera-relative); hold Shift to run |
| Space | Throw a Safari Capsule at the targeted Brandling |
| F | Cast a line toward the water you face / reel in when it bites |
| E | Talk to Professor Henry (advances dialogue too) |
| Tab | Your Brandlings: Enter moves one between party and box |
| Enter / T | Chat |
| Q / E, right-drag, wheel | Camera |
| Esc | Menu |
| F1 / F12 | Render tuning panels / screenshot |
Your identity is a random token in ~/.local/share/brandling-safari/client.json; the server
keys your ranger, Brandlings and dex to it. --profile NAME uses a separate identity (several
clients on one machine).
Server
safari_server [--port N] [--name NAME] [--db PATH] [--seed N] [--zone meadow]
[--roster PATH] [--manifest PATH] [--day-minutes M] [--hour H]
[--weather clear|cloudy|rain|storm|fog]
Everything is saved in SQLite (~/.local/share/brandling-safari/world.db by default):
players (name, look, position, capsules, intro), caught Brandlings, the per-player dex, the
world clock and weather. Console commands: help, players, time <h>,
weather <name>, give <player> capsules <n>, spawn <species>, save, quit.
safari_bot is a headless test client that walks into tall grass, throws capsules, fishes and
talks to Henry, printing everything it receives.
Architecture
flowchart LR
subgraph Server [safari_server, 30 Hz, authoritative]
G[Game: players, wild AI, spawns,<br/>fishing, captures, followers,<br/>clock + weather]
DB[(SQLite)]
G <--> DB
end
subgraph Client [safari_client]
N[NetClient] --> W[World: prediction,<br/>interpolation, effects]
W --> R[HD-2D Renderer]
A[App: screens + UI] --> N
end
N <-- "ENet UDP: inputs, actions /<br/>snapshots, events" --> G
S[shared/: Protocol, Zone,<br/>Movement, Roster, Appearance] --- G
S --- W
shared/is compiled into both programs.Zonegenerates the whole zone deterministically from(zone id, seed), so the server (collision, spawn points, water, tall grass) and every client (terrain, props, decoration) agree on the map and only the id + seed go over the wire.Movementis the single player-movement function: the server applies it once per input command, the client predicts with it and replays unacknowledged inputs when a snapshot arrives.- Clients send only intents: movement inputs (unreliable, last 4 resent) and actions (throw, cast, reel, talk, party, chat). The server validates everything (ranges, capsules, speed budget, walkability) and broadcasts snapshots (unreliable, 30 Hz) and events (reliable: emerge, splash, throw, capture result, bite, hook, catch...).
- Protocol:
shared/Protocol.h. BumpkProtocolVersionon any wire change.
| Path | What |
|---|---|
shared/ |
protocol, ENet wrapper + LAN discovery, zone generator, movement, roster + appearance |
server/ |
dedicated server: game simulation, spawns, creatures, SQLite persistence |
client/ |
App (screens, HUD, input), World (scene, entities, effects), CharacterSprite (paper-doll compositor), NetClient |
bot/ |
headless test client |
assets/ |
characters, creatures (roster + sprites), models (safari.glb), dialogue |
tools/ |
art + data pipelines (below) |
art_src/ |
raw PixelLab output, caches and reports the tools build from |
Content pipelines
The game ships the generated assets; the tools are only needed to change them. API keys go in
.env here or in the parent folder (see .env.example).
Brandlings (tools/build_roster.py)
python3 tools/build_roster.py --csv ../clickup_web_design_export.csv [--limit N] [--jobs 16]
Reads only the company name, domain, staging-site URL (logo search only, never stored), service
list and logo-note colour words from the ClickUp export (the export itself holds private client
data and is never committed). For each company: logo fetched from its domain, then its staging
site (Company_Sprite_Gen/logo_fetch.py; a logo shared by 2+ companies is a template and is
dropped), type from the jev classifier, sprite from Company_Sprite_Gen coloured from the logo
(else the logo note's colour words, else a per-creature shade of the type colour), plus
deterministic rarity, catch/flee rates and habitat. Writes assets/creatures/roster.json,
assets/creatures/sprites/, a review contact_sheet.png and art_src/creatures/report.json
(counts, logo hit rate by method, type histogram). Logos, classifier answers and renders are
cached in art_src/creatures/cache/, so reruns only do new work. Later this will read from the
ClickUp API instead of the CSV.
Species ids are permanent, because saved creatures store them and the loader requires
id == index. The first run numbers species by slug; later runs keep every slug's id, append
new companies after the last id, and keep companies that left the CSV as "retired": true
entries (sprite kept) so no id is reused. A --limit N test run leaves unsampled species as
they were. Retired Brandlings stay in players' collections but no longer spawn.
Character art
tools/character_art.py builds the player layer library and Professor Henry with PixelLab;
see docs/CHARACTER_ART.md for how one base walk cycle animates every
outfit and skin tone with 20 generations per item.
3D props
python3 tools/safari_textures.py # painted textures -> assets/models/src/
blender -b -P tools/blender_safari.py # -> assets/models/safari.glb
Gate, tent, fences, lamps, signposts, dock, crates, barrels, benches and hay bales, following
the engine's glTF conventions (see the engine README). Trees, bushes and rocks come from the
engine's scenery.glb.
Verification
Captures render in a hidden window into an offscreen target, so nothing appears on screen and keyboard input cannot disturb them:
./build/safari_server --port 27500 --db /tmp/test.db &
./build/safari_client --screenshot /tmp/play.png --frames 600 --connect 127.0.0.1:27500 \
--auto-create --auto-dialogue --profile test --move 0.8,0.6 --run --key 450:space
--look body,skin,hair,hat,top,bottom,shoes[,colors] sets the look, --screen creator
captures the creator, --hour / --weather set the title backdrop, --key frame:key
presses space / e / f / tab / enter on a frame, --show makes the window visible.







