#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 #include #include #include #include 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(s.size(), 4000)); u16(n); raw(s.data(), n); } const std::vector& data() const { return buf_; } private: void raw(const void* p, size_t n) { const auto* b = static_cast(p); buf_.insert(buf_.end(), b, b + n); } std::vector 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& 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(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 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 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 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