Files
brandling-safari/shared/Protocol.h

269 lines
9.8 KiB
C++

#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) {
buf_.reserve(64); // most messages fit; avoids regrowth for every field
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