Files
voxelforge/core/src/NetSession.cpp
T
2026-09-08 18:24:05 +02:00

485 lines
16 KiB
C++

#include "vf/NetSession.h"
#include <algorithm>
#include <cmath>
namespace vf {
namespace {
Vec3i chunkOf(const Vec3& position) {
int gx = static_cast<int>(std::floor(position.x));
int gy = static_cast<int>(std::floor(position.y));
int gz = static_cast<int>(std::floor(position.z));
return Vec3i{floorDiv(gx, chunkSize), floorDiv(gy, chunkSize), floorDiv(gz, chunkSize)};
}
int64_t squaredDistance(const Vec3i& a, const Vec3i& b) {
int64_t dx = a.x - b.x;
int64_t dy = a.y - b.y;
int64_t dz = a.z - b.z;
return dx * dx + dy * dy + dz * dz;
}
} // namespace
ViewTracker::ViewTracker(int radius) : viewRadius(radius < 0 ? 0 : radius) {}
void ViewTracker::setRadius(int radius) { viewRadius = radius < 0 ? 0 : radius; }
void ViewTracker::clear() {
resident.clear();
centred = false;
centreChunk = Vec3i{0, 0, 0};
}
void ViewTracker::update(const Vec3i& newCentre, std::vector<Vec3i>& entered,
std::vector<Vec3i>& left) {
entered.clear();
left.clear();
std::vector<Vec3i> wanted;
int64_t limit = static_cast<int64_t>(viewRadius) * static_cast<int64_t>(viewRadius);
for (int dy = -viewRadius; dy <= viewRadius; ++dy) {
for (int dz = -viewRadius; dz <= viewRadius; ++dz) {
for (int dx = -viewRadius; dx <= viewRadius; ++dx) {
Vec3i coord{newCentre.x + dx, newCentre.y + dy, newCentre.z + dz};
if (squaredDistance(coord, newCentre) > limit) continue;
wanted.push_back(coord);
}
}
}
std::stable_sort(wanted.begin(), wanted.end(),
[&newCentre](const Vec3i& a, const Vec3i& b) {
int64_t da = squaredDistance(a, newCentre);
int64_t db = squaredDistance(b, newCentre);
if (da != db) return da < db;
if (a.y != b.y) return a.y < b.y;
if (a.z != b.z) return a.z < b.z;
return a.x < b.x;
});
std::unordered_set<Vec3i, Vec3iHash> next;
next.reserve(wanted.size() * 2);
for (const Vec3i& coord : wanted) {
next.insert(coord);
if (resident.count(coord) == 0) entered.push_back(coord);
}
for (const Vec3i& coord : resident)
if (next.count(coord) == 0) left.push_back(coord);
std::stable_sort(left.begin(), left.end(), [](const Vec3i& a, const Vec3i& b) {
if (a.y != b.y) return a.y < b.y;
if (a.z != b.z) return a.z < b.z;
return a.x < b.x;
});
resident.swap(next);
centreChunk = newCentre;
centred = true;
}
void ClientView::queueChunk(const Vec3i& coord) {
if (!queued.insert(coord).second) return;
pending.push_back(coord);
}
bool ClientView::takeNextPending(Vec3i& out) {
while (!pending.empty()) {
Vec3i coord = pending.front();
pending.pop_front();
if (queued.erase(coord) == 0) continue;
out = coord;
return true;
}
return false;
}
void ClientView::forget(const Vec3i& coord) {
sent.erase(coord);
queued.erase(coord);
}
void ClientView::reset() {
tracker.clear();
pending.clear();
queued.clear();
sent.clear();
centreChunk = Vec3i{0, 0, 0};
position = Vec3{0.0f, 0.0f, 0.0f};
}
NetSession::NetSession(Transport& transport, Role role, SessionConfig config)
: transport(&transport), role(role), config(config), clientView(config.viewRadius) {}
std::size_t NetSession::queuedByteCount() const {
std::size_t total = 0;
for (const QueuedPacket& packet : outbound) total += packet.frame.size();
return total;
}
void NetSession::disconnect(const std::string& why) {
if (currentPhase == SessionPhase::Disconnected) return;
currentPhase = SessionPhase::Disconnected;
reason = why;
outbound.clear();
}
void NetSession::enqueue(PacketId id, const PacketWriter& body, bool reliable) {
ByteWriter stream;
writeFramed(stream, id, body);
QueuedPacket packet;
packet.frame = stream.bytes();
packet.reliable = reliable;
statistics.bytesQueued += packet.frame.size();
outbound.push_back(std::move(packet));
}
void NetSession::beginHandshake(const std::string& playerName, uint64_t high, uint64_t low) {
name = playerName;
uuidHi = high;
uuidLo = low;
Handshake hello;
hello.protocolVersion = config.protocolVersion;
hello.serverAddress = "loopback";
hello.serverPort = 0;
hello.nextState = 2;
PacketWriter helloBody;
hello.encode(helloBody);
enqueue(PacketId::Handshake, helloBody, true);
LoginStart login;
login.name = playerName;
login.uuidHi = high;
login.uuidLo = low;
PacketWriter loginBody;
login.encode(loginBody);
enqueue(PacketId::LoginStart, loginBody, true);
handshakeStarted = true;
currentPhase = SessionPhase::Login;
}
bool NetSession::legalHere(PacketId id) const {
if (role == Role::Server) {
switch (currentPhase) {
case SessionPhase::Handshake:
return id == PacketId::Handshake;
case SessionPhase::Login:
return id == PacketId::LoginStart;
case SessionPhase::Play:
return id == PacketId::PlayerState || id == PacketId::KeepAlive ||
id == PacketId::BlockChange;
case SessionPhase::Disconnected:
return false;
}
return false;
}
switch (currentPhase) {
case SessionPhase::Handshake:
return false;
case SessionPhase::Login:
return id == PacketId::JoinGame;
case SessionPhase::Play:
return id == PacketId::ChunkData || id == PacketId::BlockChange ||
id == PacketId::MultiBlockChange || id == PacketId::EntityMove ||
id == PacketId::PlayerState || id == PacketId::KeepAlive;
case SessionPhase::Disconnected:
return false;
}
return false;
}
void NetSession::enterPlay(uint64_t tick) {
currentPhase = SessionPhase::Play;
lastKeepAliveHeard = tick;
lastKeepAliveSent = tick;
if (role == Role::Server) recentre(clientView.position);
}
void NetSession::recentre(const Vec3& position) {
clientView.position = position;
Vec3i centre = chunkOf(position);
if (clientView.tracker.hasCentre() && clientView.tracker.centre() == centre) return;
std::vector<Vec3i> entered;
std::vector<Vec3i> left;
clientView.tracker.update(centre, entered, left);
clientView.centreChunk = centre;
for (const Vec3i& coord : entered) clientView.queueChunk(coord);
for (const Vec3i& coord : left) clientView.forget(coord);
}
void NetSession::markChunkDirty(const Vec3i& coord) {
if (!dirtyLookup.insert(coord).second) return;
dirtyChunks.push_back(coord);
}
void NetSession::sendEntityMove(int32_t movedEntityId, const Vec3& position, float yaw, float pitch,
bool onGround) {
if (currentPhase != SessionPhase::Play) return;
EntityMove move;
move.entityId = movedEntityId;
move.x = position.x;
move.y = position.y;
move.z = position.z;
move.yaw = yaw;
move.pitch = pitch;
move.onGround = onGround;
PacketWriter body;
move.encode(body);
enqueue(PacketId::EntityMove, body, false);
++statistics.entityMovesSent;
}
void NetSession::sendPlayerState(const PlayerState& state) {
if (currentPhase != SessionPhase::Play) return;
PacketWriter body;
state.encode(body);
// Upstream position reports age out instantly, an authoritative correction
// from the server must not.
enqueue(PacketId::PlayerState, body, role == Role::Server);
}
void NetSession::requestBlockEdit(int32_t x, int32_t y, int32_t z, Block block) {
if (currentPhase != SessionPhase::Play) return;
BlockChange change;
change.x = x;
change.y = y;
change.z = z;
change.block = block;
PacketWriter body;
change.encode(body);
enqueue(PacketId::BlockChange, body, true);
}
bool NetSession::entityMove(int32_t movedEntityId, EntityMove& out) const {
auto it = entities.find(movedEntityId);
if (it == entities.end()) return false;
out = it->second;
return true;
}
void NetSession::handleMessage(const ByteBuffer& message, uint64_t tick) {
std::vector<Frame> frames;
std::size_t consumed = 0;
splitFrames(message.data(), message.size(), frames, consumed);
for (const Frame& frame : frames) {
if (currentPhase == SessionPhase::Disconnected) return;
handleFrame(frame, tick);
}
}
void NetSession::handleFrame(const Frame& frame, uint64_t tick) {
++statistics.packetsReceived;
if (!legalHere(frame.id)) {
++statistics.packetsRejected;
disconnect("packet out of phase");
return;
}
PacketReader reader(frame.payload.data(), frame.payload.size());
switch (frame.id) {
case PacketId::Handshake: {
Handshake hello;
if (!hello.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed handshake");
return;
}
if (hello.protocolVersion != config.protocolVersion) {
++statistics.packetsRejected;
disconnect("protocol version mismatch");
return;
}
if (hello.nextState != 2) {
++statistics.packetsRejected;
disconnect("unsupported next state");
return;
}
currentPhase = SessionPhase::Login;
return;
}
case PacketId::LoginStart: {
LoginStart login;
if (!login.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed login");
return;
}
name = login.name;
uuidHi = login.uuidHi;
uuidLo = login.uuidLo;
JoinGame join;
join.entityId = ownEntityId;
join.gameMode = config.gameMode;
join.dimension = config.dimension;
join.seedHash = config.seedHash;
join.maxPlayers = config.maxPlayers;
join.levelType = config.levelType;
join.viewDistance = config.viewRadius;
join.reducedDebugInfo = false;
PacketWriter body;
join.encode(body);
enqueue(PacketId::JoinGame, body, true);
enterPlay(tick);
return;
}
case PacketId::JoinGame: {
JoinGame join;
if (!join.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed join");
return;
}
ownEntityId = join.entityId;
clientView.tracker.setRadius(join.viewDistance);
enterPlay(tick);
return;
}
case PacketId::ChunkData: {
ChunkData snapshot;
if (!snapshot.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed chunk data");
return;
}
clientSync.applyFull(snapshot, *worldRef);
++statistics.chunksApplied;
return;
}
case PacketId::BlockChange: {
BlockChange change;
if (!change.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed block change");
return;
}
if (role == Role::Server) {
edits.push_back(change);
return;
}
clientSync.applyBlockChange(change, *worldRef);
++statistics.blockChangesApplied;
return;
}
case PacketId::MultiBlockChange: {
MultiBlockChange delta;
if (!delta.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed multi block change");
return;
}
clientSync.applyDelta(delta, *worldRef);
++statistics.multiBlockChangesApplied;
return;
}
case PacketId::EntityMove: {
EntityMove move;
if (!move.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed entity move");
return;
}
entities[move.entityId] = move;
++statistics.entityMovesApplied;
return;
}
case PacketId::PlayerState: {
PlayerState state;
if (!state.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed player state");
return;
}
if (role == Role::Server) {
recentre(Vec3{state.x, state.y, state.z});
return;
}
latestPlayerState = state;
return;
}
case PacketId::KeepAlive: {
KeepAlive beat;
if (!beat.decode(reader)) {
++statistics.packetsRejected;
disconnect("malformed keep alive");
return;
}
if (role == Role::Server) {
// Only a token this server actually issued, and only the first
// time it comes back, counts as proof the client is alive.
if (beat.token > pendingKeepAliveToken) return;
if (beat.token <= acceptedKeepAliveToken) return;
acceptedKeepAliveToken = beat.token;
lastKeepAliveHeard = tick;
return;
}
lastKeepAliveHeard = tick;
PacketWriter body;
beat.encode(body);
enqueue(PacketId::KeepAlive, body, true);
return;
}
}
}
void NetSession::streamChunks() {
uint32_t budget = config.chunksPerTick;
Vec3i coord;
while (budget > 0 && clientView.takeNextPending(coord)) {
if (!clientView.tracker.contains(coord)) continue;
ChunkData snapshot;
if (!syncRef->buildFull(*worldRef, coord, snapshot)) continue;
PacketWriter body;
snapshot.encode(body);
enqueue(PacketId::ChunkData, body, true);
clientView.sent.insert(coord);
++statistics.chunksSent;
--budget;
}
}
void NetSession::flushBlockChanges() {
for (const Vec3i& coord : dirtyChunks) {
if (!clientView.hasChunk(coord)) {
// Nothing to patch yet: the pending full snapshot already carries
// the current contents, so the delta would be redundant.
if (clientView.tracker.contains(coord)) clientView.queueChunk(coord);
continue;
}
MultiBlockChange delta;
if (!syncRef->buildDelta(*worldRef, coord, delta)) continue;
if (delta.changes.size() > config.multiBlockThreshold) {
PacketWriter body;
delta.encode(body);
enqueue(PacketId::MultiBlockChange, body, true);
++statistics.multiBlockChangesSent;
continue;
}
for (const MultiBlockChange::Change& change : delta.changes) {
int local = change.localIndex;
int lx = local % chunkSize;
int lz = (local / chunkSize) % chunkSize;
int ly = local / (chunkSize * chunkSize);
BlockChange single;
single.x = coord.x * chunkSize + lx;
single.y = coord.y * chunkSize + ly;
single.z = coord.z * chunkSize + lz;
single.block = change.block;
PacketWriter body;
single.encode(body);
enqueue(PacketId::BlockChange, body, true);
++statistics.blockChangesSent;
}
}
dirtyChunks.clear();
dirtyLookup.clear();
}