269 lines
9.8 KiB
C++
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
|