Brandling Safari: LAN safari-zone creature catching on the HD-2D engine
- 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
This commit is contained in:
265
shared/Protocol.h
Normal file
265
shared/Protocol.h
Normal file
@@ -0,0 +1,265 @@
|
||||
#pragma once
|
||||
// Wire protocol shared by server and client. Every message is one ENet packet:
|
||||
// [u8 Msg][payload]. Payloads are little-endian, strings are u16 length + bytes.
|
||||
// The server is authoritative: clients only send intents (inputs, actions).
|
||||
|
||||
#include "Appearance.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace bs {
|
||||
|
||||
constexpr uint16_t kProtocolVersion = 3;
|
||||
constexpr uint16_t kDefaultPort = 27420;
|
||||
constexpr uint16_t kDiscoveryPort = 27421;
|
||||
constexpr int kTickRate = 30; // simulation ticks per second (server and client prediction)
|
||||
constexpr float kTickDt = 1.0f / float(kTickRate);
|
||||
constexpr int kMaxParty = 6; // creatures following a player
|
||||
constexpr int kStartCapsules = 30;
|
||||
|
||||
enum class Msg : uint8_t {
|
||||
// client -> server
|
||||
Hello = 1, // HelloMsg
|
||||
CreateCharacter, // CreateCharacterMsg
|
||||
Input, // InputMsg (unreliable)
|
||||
Action, // ActionMsg
|
||||
Chat, // string
|
||||
DialogueDone, // string script id
|
||||
// server -> client
|
||||
Welcome = 64, // WelcomeMsg
|
||||
Reject, // string reason
|
||||
Profile, // ProfileMsg (owner only)
|
||||
PlayerInfo, // PlayerInfoMsg (name + look of a player entity)
|
||||
PlayerLeft, // u32 entity id
|
||||
Snapshot, // SnapshotMsg (unreliable)
|
||||
Event, // GameEvent
|
||||
Dialogue, // string script id (server-triggered NPC talk)
|
||||
ChatLine, // u32 entity id, string name, string text
|
||||
Toast, // string text
|
||||
};
|
||||
|
||||
enum class EntityKind : uint8_t { Player = 0, Wild = 1, Follower = 2, Npc = 3 };
|
||||
|
||||
enum class Anim : uint8_t {
|
||||
Idle = 0,
|
||||
Walk,
|
||||
Run,
|
||||
Fish, // player holding a cast line
|
||||
Throw, // player throwing a capsule
|
||||
Emerge, // wild creature popping out of grass / water
|
||||
Swim, // water creature at the surface
|
||||
Capturing, // inside a capsule (hidden by the client)
|
||||
Flee, // running away before despawning
|
||||
Hooked, // pulled out of the water on a line
|
||||
Alert, // noticed a player; stands still facing them
|
||||
};
|
||||
|
||||
enum class Weather : uint8_t { Clear = 0, Cloudy, Rain, Storm, Fog, Count };
|
||||
const char* weatherName(Weather w);
|
||||
bool parseWeather(const std::string& s, Weather& out);
|
||||
|
||||
enum class ActionKind : uint8_t {
|
||||
Throw = 0, // target = wild entity
|
||||
Cast, // x,z = facing direction (bobber is placed by the server)
|
||||
Reel, // pull the line in (hooks a creature if it is biting)
|
||||
Interact, // talk to the NPC in front (target = npc entity)
|
||||
ToggleParty, // target = creature uid: move between party and box
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------ serialization
|
||||
|
||||
class Writer {
|
||||
public:
|
||||
explicit Writer(Msg m) { u8(uint8_t(m)); }
|
||||
void u8(uint8_t v) { buf_.push_back(v); }
|
||||
void u16(uint16_t v) { raw(&v, 2); }
|
||||
void u32(uint32_t v) { raw(&v, 4); }
|
||||
void i32(int32_t v) { raw(&v, 4); }
|
||||
void i64(int64_t v) { raw(&v, 8); }
|
||||
void f32(float v) { raw(&v, 4); }
|
||||
void i8(int8_t v) { raw(&v, 1); }
|
||||
void vec3(const glm::vec3& v) { f32(v.x), f32(v.y), f32(v.z); }
|
||||
void str(const std::string& s) {
|
||||
uint16_t n = uint16_t(std::min<size_t>(s.size(), 4000));
|
||||
u16(n);
|
||||
raw(s.data(), n);
|
||||
}
|
||||
const std::vector<uint8_t>& data() const { return buf_; }
|
||||
|
||||
private:
|
||||
void raw(const void* p, size_t n) {
|
||||
const auto* b = static_cast<const uint8_t*>(p);
|
||||
buf_.insert(buf_.end(), b, b + n);
|
||||
}
|
||||
std::vector<uint8_t> buf_;
|
||||
};
|
||||
|
||||
// Reads past the end yield zeros and set `ok() == false`, so malformed packets
|
||||
// can be rejected after parsing instead of crashing.
|
||||
class Reader {
|
||||
public:
|
||||
Reader(const uint8_t* data, size_t size) : p_(data), end_(data + size) {}
|
||||
explicit Reader(const std::vector<uint8_t>& v) : Reader(v.data(), v.size()) {}
|
||||
uint8_t u8() { uint8_t v = 0; raw(&v, 1); return v; }
|
||||
uint16_t u16() { uint16_t v = 0; raw(&v, 2); return v; }
|
||||
uint32_t u32() { uint32_t v = 0; raw(&v, 4); return v; }
|
||||
int32_t i32() { int32_t v = 0; raw(&v, 4); return v; }
|
||||
int64_t i64() { int64_t v = 0; raw(&v, 8); return v; }
|
||||
int8_t i8() { int8_t v = 0; raw(&v, 1); return v; }
|
||||
float f32() { float v = 0; raw(&v, 4); return v; }
|
||||
glm::vec3 vec3() { glm::vec3 v; v.x = f32(); v.y = f32(); v.z = f32(); return v; }
|
||||
std::string str() {
|
||||
uint16_t n = u16();
|
||||
if (size_t(end_ - p_) < n) { ok_ = false; p_ = end_; return {}; }
|
||||
std::string s(reinterpret_cast<const char*>(p_), n);
|
||||
p_ += n;
|
||||
return s;
|
||||
}
|
||||
bool ok() const { return ok_; }
|
||||
bool atEnd() const { return p_ >= end_; }
|
||||
|
||||
private:
|
||||
void raw(void* out, size_t n) {
|
||||
if (size_t(end_ - p_) < n) { ok_ = false; p_ = end_; std::memset(out, 0, n); return; }
|
||||
std::memcpy(out, p_, n);
|
||||
p_ += n;
|
||||
}
|
||||
const uint8_t* p_;
|
||||
const uint8_t* end_;
|
||||
bool ok_ = true;
|
||||
};
|
||||
|
||||
void writeAppearance(Writer& w, const Appearance& a);
|
||||
Appearance readAppearance(Reader& r);
|
||||
|
||||
// ------------------------------------------------------------ messages
|
||||
|
||||
struct HelloMsg {
|
||||
uint16_t version = kProtocolVersion;
|
||||
std::string token; // persistent client identity (random hex), the account key
|
||||
void write(Writer& w) const { w.u16(version); w.str(token); }
|
||||
void read(Reader& r) { version = r.u16(); token = r.str(); }
|
||||
};
|
||||
|
||||
struct CreateCharacterMsg {
|
||||
std::string name;
|
||||
Appearance look;
|
||||
void write(Writer& w) const { w.str(name); writeAppearance(w, look); }
|
||||
void read(Reader& r) { name = r.str(); look = readAppearance(r); }
|
||||
};
|
||||
|
||||
// One simulation tick of movement intent. `move` is a world-space direction
|
||||
// (x east, z south) scaled to [-127, 127]; the server clamps its length.
|
||||
struct InputCmd {
|
||||
uint32_t seq = 0;
|
||||
int8_t moveX = 0, moveZ = 0;
|
||||
uint8_t flags = 0; // bit 0 = run
|
||||
glm::vec2 move() const { return {float(moveX) / 127.0f, float(moveZ) / 127.0f}; }
|
||||
bool run() const { return flags & 1; }
|
||||
};
|
||||
|
||||
// Clients resend the last few inputs in every packet so a lost datagram does
|
||||
// not drop movement; the server skips sequence numbers it already applied.
|
||||
struct InputMsg {
|
||||
std::vector<InputCmd> cmds; // oldest first, at most 4
|
||||
void write(Writer& w) const;
|
||||
void read(Reader& r);
|
||||
};
|
||||
|
||||
struct ActionMsg {
|
||||
ActionKind kind = ActionKind::Interact;
|
||||
uint32_t target = 0;
|
||||
float x = 0, z = 0;
|
||||
void write(Writer& w) const { w.u8(uint8_t(kind)); w.u32(target); w.f32(x); w.f32(z); }
|
||||
void read(Reader& r) { kind = ActionKind(r.u8()); target = r.u32(); x = r.f32(); z = r.f32(); }
|
||||
};
|
||||
|
||||
struct WelcomeMsg {
|
||||
uint32_t entityId = 0; // this client's player entity (0 until the character exists)
|
||||
bool hasCharacter = false;
|
||||
std::string zoneId; // e.g. "meadow"
|
||||
uint32_t zoneSeed = 0;
|
||||
std::string serverName;
|
||||
void write(Writer& w) const { w.u32(entityId); w.u8(hasCharacter); w.str(zoneId); w.u32(zoneSeed); w.str(serverName); }
|
||||
void read(Reader& r) { entityId = r.u32(); hasCharacter = r.u8(); zoneId = r.str(); zoneSeed = r.u32(); serverName = r.str(); }
|
||||
};
|
||||
|
||||
struct OwnedCreature {
|
||||
uint32_t uid = 0;
|
||||
uint16_t species = 0;
|
||||
bool inParty = false;
|
||||
int64_t caughtAt = 0; // unix seconds
|
||||
};
|
||||
|
||||
struct ProfileMsg {
|
||||
uint32_t entityId = 0;
|
||||
std::string name;
|
||||
Appearance look;
|
||||
int32_t capsules = 0;
|
||||
bool introDone = false;
|
||||
uint32_t seen = 0, caught = 0; // distinct species
|
||||
std::vector<OwnedCreature> creatures;
|
||||
void write(Writer& w) const;
|
||||
void read(Reader& r);
|
||||
};
|
||||
|
||||
struct PlayerInfoMsg {
|
||||
uint32_t entityId = 0;
|
||||
std::string name;
|
||||
Appearance look;
|
||||
void write(Writer& w) const { w.u32(entityId); w.str(name); writeAppearance(w, look); }
|
||||
void read(Reader& r) { entityId = r.u32(); name = r.str(); look = readAppearance(r); }
|
||||
};
|
||||
|
||||
struct EntityState {
|
||||
uint32_t id = 0;
|
||||
EntityKind kind = EntityKind::Wild;
|
||||
Anim anim = Anim::Idle;
|
||||
uint16_t species = 0; // wild / follower species id
|
||||
uint32_t owner = 0; // follower: owning player entity; wild: player it is fleeing/hooked by
|
||||
glm::vec3 pos{0.0f};
|
||||
float yaw = 0.0f; // facing, radians: 0 = north (-z), pi/2 = east (+x)
|
||||
uint8_t order = 0; // follower position in the party chain
|
||||
};
|
||||
|
||||
struct SnapshotMsg {
|
||||
uint32_t tick = 0;
|
||||
uint32_t ackInput = 0; // last input seq applied for the receiving client
|
||||
float hour = 12.0f; // world clock, [0, 24)
|
||||
Weather weather = Weather::Clear;
|
||||
float weatherBlend = 1.0f; // reserved: server-side transition progress
|
||||
std::vector<EntityState> entities;
|
||||
void write(Writer& w) const;
|
||||
void read(Reader& r);
|
||||
};
|
||||
|
||||
enum class EventKind : uint8_t {
|
||||
Emerge = 0, // a = entity, i = 0 grass / 1 water, p = position
|
||||
Splash, // p = position
|
||||
Throw, // a = player, b = target, p = from, q = to, f = flight seconds
|
||||
CaptureResult, // a = player, b = target, i = 1 success, j = wobbles
|
||||
Fled, // a = entity, p = position
|
||||
Cast, // a = player, p = bobber position
|
||||
Bite, // a = player, p = bobber
|
||||
Hooked, // a = player, b = hooked entity, p = bobber
|
||||
LineLost, // a = player (reeled too early / too late)
|
||||
Caught, // a = player, b = species, i = first catch of that species for the player
|
||||
Rustle, // p = position (tall grass shakes without anything coming out)
|
||||
};
|
||||
|
||||
struct GameEvent {
|
||||
EventKind kind = EventKind::Splash;
|
||||
uint32_t a = 0, b = 0;
|
||||
int32_t i = 0, j = 0;
|
||||
glm::vec3 p{0.0f}, q{0.0f};
|
||||
float f = 0.0f;
|
||||
void write(Writer& w) const { w.u8(uint8_t(kind)); w.u32(a); w.u32(b); w.i32(i); w.i32(j); w.vec3(p); w.vec3(q); w.f32(f); }
|
||||
void read(Reader& r) { kind = EventKind(r.u8()); a = r.u32(); b = r.u32(); i = r.i32(); j = r.i32(); p = r.vec3(); q = r.vec3(); f = r.f32(); }
|
||||
};
|
||||
|
||||
} // namespace bs
|
||||
Reference in New Issue
Block a user