diff --git a/core/src/NetServer.cpp b/core/src/NetServer.cpp new file mode 100644 index 0000000..cb9d8b3 --- /dev/null +++ b/core/src/NetServer.cpp @@ -0,0 +1,185 @@ +#include "vf/NetServer.h" + +#include + +namespace vf { + +namespace { + +Vec3i chunkOfPosition(const Vec3& position) { + int gx = static_cast(std::floor(position.x)); + int gy = static_cast(std::floor(position.y)); + int gz = static_cast(std::floor(position.z)); + return Vec3i{floorDiv(gx, chunkSize), floorDiv(gy, chunkSize), floorDiv(gz, chunkSize)}; +} + +} // namespace + +NetServer::NetServer(World& world, ServerConfig config) : worldRef(&world), config(config) { + if (this->config.maxClients == 0) this->config.maxClients = 1; + slots.resize(this->config.maxClients); +} + +uint32_t NetServer::activeClients() const { + uint32_t count = 0; + for (const ClientSlot& slot : slots) + if (slot.active) ++count; + return count; +} + +bool NetServer::isActive(int slot) const { + if (slot < 0 || static_cast(slot) >= slots.size()) return false; + return slots[static_cast(slot)].active; +} + +int NetServer::acceptClient() { + for (std::size_t i = 0; i < slots.size(); ++i) { + ClientSlot& slot = slots[i]; + if (slot.active) continue; + + SessionConfig sessionConfig; + sessionConfig.protocolVersion = config.protocolVersion; + sessionConfig.viewRadius = config.viewRadius; + sessionConfig.chunksPerTick = config.chunksPerClientPerTick; + sessionConfig.keepAliveIntervalTicks = config.keepAliveIntervalTicks; + sessionConfig.keepAliveTimeoutTicks = config.keepAliveTimeoutTicks; + sessionConfig.multiBlockThreshold = config.multiBlockThreshold; + sessionConfig.bandwidthBytesPerTick = config.bytesPerClientPerTick; + sessionConfig.maxPlayers = static_cast(config.maxClients); + sessionConfig.seedHash = config.seedHash; + sessionConfig.levelType = config.levelType; + + slot.active = true; + slot.entityId = static_cast(i) + 1; + slot.lastBytesFlushed = 0; + slot.lastChunksSent = 0; + slot.lastBlockUpdates = 0; + slot.link.reset(new LoopbackLink(config.link)); + slot.transport.reset(new Transport(*slot.link, 0, config.transport)); + slot.session.reset(new NetSession(*slot.transport, NetSession::Role::Server, sessionConfig)); + slot.session->attachWorld(*worldRef); + slot.session->attachChunkSync(sync); + slot.session->setEntityId(slot.entityId); + + ++statistics.acceptedConnections; + return static_cast(i); + } + ++statistics.rejectedConnections; + return -1; +} + +void NetServer::dropClient(int slot) { + if (!isActive(slot)) return; + ClientSlot& entry = slots[static_cast(slot)]; + entry.active = false; + entry.session.reset(); + entry.transport.reset(); + entry.link.reset(); +} + +LoopbackLink* NetServer::linkFor(int slot) { + if (!isActive(slot)) return nullptr; + return slots[static_cast(slot)].link.get(); +} + +NetSession* NetServer::sessionFor(int slot) { + if (!isActive(slot)) return nullptr; + return slots[static_cast(slot)].session.get(); +} + +void NetServer::applyEdit(const BlockChange& change) { + worldRef->setBlock(change.x, change.y, change.z, change.block); + Vec3i coord{floorDiv(change.x, chunkSize), floorDiv(change.y, chunkSize), + floorDiv(change.z, chunkSize)}; + sync.markDirty(coord, floorMod(change.x, chunkSize), floorMod(change.y, chunkSize), + floorMod(change.z, chunkSize)); + for (ClientSlot& slot : slots) + if (slot.active) slot.session->markChunkDirty(coord); + ++statistics.editsApplied; +} + +void NetServer::setBlock(int gx, int gy, int gz, Block block) { + BlockChange change; + change.x = gx; + change.y = gy; + change.z = gz; + change.block = block; + applyEdit(change); +} + +void NetServer::moveEntity(int32_t entityId, const Vec3& position, float yaw, float pitch, + bool onGround) { + Vec3i coord = chunkOfPosition(position); + for (ClientSlot& slot : slots) { + if (!slot.active) continue; + if (slot.session->phase() != SessionPhase::Play) continue; + // An entity outside the client's view costs nothing to skip and cannot + // be drawn there anyway. + if (!slot.session->view().tracker.contains(coord)) continue; + slot.session->sendEntityMove(entityId, position, yaw, pitch, onGround); + } +} + +void NetServer::collectStats() { + statistics.bytesSentThisTick = 0; + statistics.queuedPackets = 0; + statistics.queuedBytes = 0; + for (ClientSlot& slot : slots) { + if (!slot.active) continue; + const SessionStats& sessionStats = slot.session->stats(); + + uint64_t bytes = sessionStats.bytesFlushed - slot.lastBytesFlushed; + slot.lastBytesFlushed = sessionStats.bytesFlushed; + statistics.bytesSentThisTick += bytes; + statistics.totalBytesSent += bytes; + + uint64_t chunks = sessionStats.chunksSent - slot.lastChunksSent; + slot.lastChunksSent = sessionStats.chunksSent; + statistics.chunksSent += chunks; + + uint64_t updates = sessionStats.blockChangesSent + sessionStats.multiBlockChangesSent; + statistics.blockUpdatesSent += updates - slot.lastBlockUpdates; + slot.lastBlockUpdates = updates; + + statistics.queuedPackets += slot.session->queuedPacketCount(); + statistics.queuedBytes += slot.session->queuedByteCount(); + } +} + +void NetServer::reap() { + for (std::size_t i = 0; i < slots.size(); ++i) { + ClientSlot& slot = slots[i]; + if (!slot.active) continue; + if (slot.session->phase() != SessionPhase::Disconnected) continue; + if (slot.session->timedOut()) ++statistics.timeouts; + dropClient(static_cast(i)); + } +} + +void NetServer::update(uint64_t tick) { + for (ClientSlot& slot : slots) { + if (!slot.active) continue; + slot.session->receive(tick); + slot.lastServicedTick = tick; + } + + // Upstream edits land in the world before any session builds its outgoing + // packets, so every client sees the same authoritative state this tick. + for (ClientSlot& slot : slots) { + if (!slot.active) continue; + for (const BlockChange& change : slot.session->requestedEdits()) applyEdit(change); + slot.session->clearRequestedEdits(); + } + + for (ClientSlot& slot : slots) { + if (!slot.active) continue; + slot.session->send(tick); + } + + sync.clearAll(); + collectStats(); + reap(); + statistics.connectedClients = activeClients(); +} + +} // namespace vf