diff --git a/core/include/vf/NetSession.h b/core/include/vf/NetSession.h new file mode 100644 index 0000000..aba77df --- /dev/null +++ b/core/include/vf/NetSession.h @@ -0,0 +1,209 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "ChunkSync.h" +#include "Math.h" +#include "Protocol.h" +#include "Transport.h" +#include "World.h" + +namespace vf { + +enum class SessionPhase : uint8_t { + Handshake = 0, + Login = 1, + Play = 2, + Disconnected = 3, +}; + +// Which chunks belong around a centre, and what changed the last time that +// centre moved. The diff is what the server acts on: a client entering a chunk +// costs a snapshot, a client leaving it costs nothing but a forget. +class ViewTracker { +public: + explicit ViewTracker(int radius = 2); + + void setRadius(int radius); + int radius() const { return viewRadius; } + + // `entered` comes back nearest first, so the chunk under the player is + // streamed before the rim of the sphere. + void update(const Vec3i& newCentre, std::vector& entered, std::vector& left); + + bool contains(const Vec3i& coord) const { return resident.count(coord) != 0; } + std::size_t size() const { return resident.size(); } + const std::unordered_set& chunks() const { return resident; } + bool hasCentre() const { return centred; } + const Vec3i& centre() const { return centreChunk; } + void clear(); + +private: + int viewRadius; + Vec3i centreChunk; + bool centred = false; + std::unordered_set resident; +}; + +// The server's picture of one client: where it is, which chunks it should +// hold, which it has actually been sent, and which are still waiting to go out. +struct ClientView { + Vec3 position; + Vec3i centreChunk; + ViewTracker tracker; + std::deque pending; + std::unordered_set queued; + std::unordered_set sent; + + explicit ClientView(int radius = 2) : tracker(radius) {} + + bool hasChunk(const Vec3i& coord) const { return sent.count(coord) != 0; } + void queueChunk(const Vec3i& coord); + bool takeNextPending(Vec3i& out); + void forget(const Vec3i& coord); + std::size_t pendingCount() const { return queued.size(); } + void reset(); +}; + +struct SessionConfig { + int32_t protocolVersion = 47; + int viewRadius = 2; + uint32_t chunksPerTick = 2; + uint32_t keepAliveIntervalTicks = 20; + uint32_t keepAliveTimeoutTicks = 100; + std::size_t multiBlockThreshold = 4; + std::size_t bandwidthBytesPerTick = 0; + uint8_t maxPlayers = 8; + uint8_t gameMode = 0; + int32_t dimension = 0; + uint64_t seedHash = 0; + std::string levelType = "default"; +}; + +struct SessionStats { + uint64_t chunksSent = 0; + uint64_t blockChangesSent = 0; + uint64_t multiBlockChangesSent = 0; + uint64_t entityMovesSent = 0; + uint64_t keepAlivesSent = 0; + uint64_t packetsFlushed = 0; + uint64_t packetsReceived = 0; + uint64_t packetsRejected = 0; + uint64_t chunksApplied = 0; + uint64_t blockChangesApplied = 0; + uint64_t multiBlockChangesApplied = 0; + uint64_t entityMovesApplied = 0; + uint64_t bytesQueued = 0; + uint64_t bytesFlushed = 0; +}; + +// One connection, seen from either end. The phase machine gates which packets +// are legal; anything out of place ends the session with a reason instead of +// being fed to a decoder that was never meant to see it. +class NetSession { +public: + enum class Role : uint8_t { Client = 0, Server = 1 }; + + NetSession(Transport& transport, Role role, SessionConfig config = {}); + + void attachWorld(World& world) { worldRef = &world; } + void attachChunkSync(ServerChunkSync& sync) { syncRef = &sync; } + void setEntityId(int32_t id) { ownEntityId = id; } + + // Client side: opens the handshake and moves straight into login. + void beginHandshake(const std::string& playerName, uint64_t uuidHi, uint64_t uuidLo); + + void receive(uint64_t tick); + void send(uint64_t tick); + void update(uint64_t tick); + + SessionPhase phase() const { return currentPhase; } + bool timedOut() const { return keepAliveExpired; } + const std::string& disconnectReason() const { return reason; } + void disconnect(const std::string& why); + + ClientView& view() { return clientView; } + const ClientView& view() const { return clientView; } + + // Server side: this chunk has entries in the shared ServerChunkSync that + // still have to be turned into packets for this particular client. + void markChunkDirty(const Vec3i& coord); + void sendEntityMove(int32_t movedEntityId, const Vec3& position, float yaw, float pitch, + bool onGround); + void sendPlayerState(const PlayerState& state); + const std::vector& requestedEdits() const { return edits; } + void clearRequestedEdits() { edits.clear(); } + const std::string& playerName() const { return name; } + uint64_t uuidHigh() const { return uuidHi; } + uint64_t uuidLow() const { return uuidLo; } + + // Client side. + void requestBlockEdit(int32_t x, int32_t y, int32_t z, Block block); + const PlayerState& playerState() const { return latestPlayerState; } + bool entityMove(int32_t movedEntityId, EntityMove& out) const; + std::size_t trackedEntityCount() const { return entities.size(); } + int32_t entityId() const { return ownEntityId; } + + const SessionStats& stats() const { return statistics; } + std::size_t queuedPacketCount() const { return outbound.size(); } + std::size_t queuedByteCount() const; + uint64_t lastKeepAliveTick() const { return lastKeepAliveHeard; } + Role side() const { return role; } + +private: + struct QueuedPacket { + ByteBuffer frame; + bool reliable = true; + }; + + void enqueue(PacketId id, const PacketWriter& body, bool reliable); + void handleMessage(const ByteBuffer& message, uint64_t tick); + void handleFrame(const Frame& frame, uint64_t tick); + bool legalHere(PacketId id) const; + void enterPlay(uint64_t tick); + void recentre(const Vec3& position); + void streamChunks(); + void flushBlockChanges(); + void serviceKeepAlive(uint64_t tick); + void flushOutbound(); + + Transport* transport; + Role role; + SessionConfig config; + + SessionPhase currentPhase = SessionPhase::Handshake; + std::string reason; + bool keepAliveExpired = false; + bool handshakeStarted = false; + + World* worldRef = nullptr; + ServerChunkSync* syncRef = nullptr; + ClientChunkSync clientSync; + + std::string name; + uint64_t uuidHi = 0; + uint64_t uuidLo = 0; + int32_t ownEntityId = 0; + + ClientView clientView; + std::vector dirtyChunks; + std::unordered_set dirtyLookup; + std::vector edits; + std::unordered_map entities; + PlayerState latestPlayerState; + + std::deque outbound; + uint64_t lastKeepAliveSent = 0; + uint64_t lastKeepAliveHeard = 0; + int64_t pendingKeepAliveToken = 0; + int64_t acceptedKeepAliveToken = -1; + + SessionStats statistics; +}; + +} // namespace vf