- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass / water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon - Client: title / join / character creator / play screens, prediction + reconciliation, interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection) - Shared deterministic zone generator, movement, protocol, roster, appearance - Paper-doll character art pipeline (PixelLab base walk + per-item animation edits) - Brandling roster from the company list (logo colors, jev types), Blender safari props - Headless test bot
237 lines
7.2 KiB
C++
237 lines
7.2 KiB
C++
#include "Net.h"
|
|
|
|
#include <enet/enet.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
namespace bs::net {
|
|
|
|
namespace {
|
|
bool gInit = false;
|
|
constexpr const char* kQuery = "BSAFARI?";
|
|
constexpr const char* kReply = "BSAFARI!";
|
|
|
|
// ENetBuffer's field order differs per platform (it aliases iovec / WSABUF).
|
|
ENetBuffer buffer(void* data, size_t size) {
|
|
ENetBuffer b;
|
|
b.data = data;
|
|
b.dataLength = size;
|
|
return b;
|
|
}
|
|
} // namespace
|
|
|
|
bool initialize() {
|
|
if (gInit) return true;
|
|
if (enet_initialize() != 0) return false;
|
|
gInit = true;
|
|
return true;
|
|
}
|
|
|
|
void shutdown() {
|
|
if (gInit) enet_deinitialize();
|
|
gInit = false;
|
|
}
|
|
|
|
Host::~Host() {
|
|
if (host_) {
|
|
for (auto& [id, p] : peers_) enet_peer_disconnect_now(p, 0);
|
|
enet_host_flush(host_);
|
|
enet_host_destroy(host_);
|
|
}
|
|
}
|
|
|
|
bool Host::listen(uint16_t port, size_t maxPeers) {
|
|
ENetAddress addr;
|
|
addr.host = ENET_HOST_ANY;
|
|
addr.port = port;
|
|
host_ = enet_host_create(&addr, maxPeers, kChannelCount, 0, 0);
|
|
return host_ != nullptr;
|
|
}
|
|
|
|
bool Host::connect(const std::string& address, uint16_t port) {
|
|
if (!host_) host_ = enet_host_create(nullptr, 1, kChannelCount, 0, 0);
|
|
if (!host_) return false;
|
|
ENetAddress addr;
|
|
if (enet_address_set_host(&addr, address.c_str()) != 0) return false;
|
|
addr.port = port;
|
|
ENetPeer* p = enet_host_connect(host_, &addr, kChannelCount, 0);
|
|
if (!p) return false;
|
|
uint32_t id = nextId_++;
|
|
p->data = reinterpret_cast<void*>(uintptr_t(id));
|
|
peers_[id] = p;
|
|
enet_peer_timeout(p, 0, 4000, 8000);
|
|
return true;
|
|
}
|
|
|
|
bool Host::poll(Event& ev, uint32_t timeoutMs) {
|
|
ev = Event{};
|
|
if (!host_) return false;
|
|
ENetEvent e;
|
|
int r = enet_host_service(host_, &e, timeoutMs);
|
|
if (r <= 0) return false;
|
|
switch (e.type) {
|
|
case ENET_EVENT_TYPE_CONNECT: {
|
|
uint32_t id = uint32_t(uintptr_t(e.peer->data));
|
|
if (!id) { // incoming connection
|
|
id = nextId_++;
|
|
e.peer->data = reinterpret_cast<void*>(uintptr_t(id));
|
|
peers_[id] = e.peer;
|
|
enet_peer_timeout(e.peer, 0, 5000, 10000);
|
|
}
|
|
ev.type = Event::Connect;
|
|
ev.peer = id;
|
|
return true;
|
|
}
|
|
case ENET_EVENT_TYPE_DISCONNECT: {
|
|
uint32_t id = uint32_t(uintptr_t(e.peer->data));
|
|
peers_.erase(id);
|
|
e.peer->data = nullptr;
|
|
ev.type = Event::Disconnect;
|
|
ev.peer = id;
|
|
return true;
|
|
}
|
|
case ENET_EVENT_TYPE_RECEIVE: {
|
|
ev.type = Event::Receive;
|
|
ev.peer = uint32_t(uintptr_t(e.peer->data));
|
|
ev.data.assign(e.packet->data, e.packet->data + e.packet->dataLength);
|
|
enet_packet_destroy(e.packet);
|
|
return true;
|
|
}
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
void Host::send(uint32_t peer, Channel ch, const std::vector<uint8_t>& data) {
|
|
auto it = peers_.find(peer);
|
|
if (it == peers_.end() || data.empty()) return;
|
|
ENetPacket* pk = enet_packet_create(data.data(), data.size(), ch == Reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
|
|
if (enet_peer_send(it->second, ch, pk) != 0) enet_packet_destroy(pk);
|
|
}
|
|
|
|
void Host::disconnect(uint32_t peer) {
|
|
auto it = peers_.find(peer);
|
|
if (it != peers_.end()) enet_peer_disconnect_later(it->second, 0);
|
|
}
|
|
|
|
void Host::flush() {
|
|
if (host_) enet_host_flush(host_);
|
|
}
|
|
|
|
std::string Host::address(uint32_t peer) const {
|
|
auto it = peers_.find(peer);
|
|
if (it == peers_.end()) return "?";
|
|
char buf[64] = {};
|
|
enet_address_get_host_ip(&it->second->address, buf, sizeof buf);
|
|
return buf;
|
|
}
|
|
|
|
uint32_t Host::roundTripMs(uint32_t peer) const {
|
|
auto it = peers_.find(peer);
|
|
return it == peers_.end() ? 0 : it->second->roundTripTime;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ LAN discovery
|
|
|
|
Beacon::~Beacon() {
|
|
if (sock_ >= 0) enet_socket_destroy(ENetSocket(sock_));
|
|
}
|
|
|
|
bool Beacon::open(uint16_t discoveryPort) {
|
|
ENetSocket s = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
|
|
if (s == ENET_SOCKET_NULL) return false;
|
|
enet_socket_set_option(s, ENET_SOCKOPT_REUSEADDR, 1);
|
|
enet_socket_set_option(s, ENET_SOCKOPT_NONBLOCK, 1);
|
|
enet_socket_set_option(s, ENET_SOCKOPT_BROADCAST, 1);
|
|
ENetAddress addr;
|
|
addr.host = ENET_HOST_ANY;
|
|
addr.port = discoveryPort;
|
|
if (enet_socket_bind(s, &addr) != 0) {
|
|
enet_socket_destroy(s);
|
|
return false;
|
|
}
|
|
sock_ = intptr_t(s);
|
|
return true;
|
|
}
|
|
|
|
void Beacon::poll(uint16_t gamePort, const std::string& serverName, int players) {
|
|
if (sock_ < 0) return;
|
|
char buf[256];
|
|
for (int i = 0; i < 16; ++i) {
|
|
ENetAddress from;
|
|
ENetBuffer b = buffer(buf, sizeof buf - 1);
|
|
int n = enet_socket_receive(ENetSocket(sock_), &from, &b, 1);
|
|
if (n <= 0) return;
|
|
buf[n] = 0;
|
|
if (std::strncmp(buf, kQuery, std::strlen(kQuery)) != 0) continue;
|
|
char reply[256];
|
|
int len = std::snprintf(reply, sizeof reply, "%s%u|%d|%s", kReply, unsigned(gamePort), players,
|
|
serverName.substr(0, 120).c_str());
|
|
ENetBuffer out = buffer(reply, size_t(len));
|
|
enet_socket_send(ENetSocket(sock_), &from, &out, 1);
|
|
}
|
|
}
|
|
|
|
Scanner::~Scanner() {
|
|
if (sock_ >= 0) enet_socket_destroy(ENetSocket(sock_));
|
|
}
|
|
|
|
bool Scanner::open() {
|
|
ENetSocket s = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
|
|
if (s == ENET_SOCKET_NULL) return false;
|
|
enet_socket_set_option(s, ENET_SOCKOPT_NONBLOCK, 1);
|
|
enet_socket_set_option(s, ENET_SOCKOPT_BROADCAST, 1);
|
|
ENetAddress addr;
|
|
addr.host = ENET_HOST_ANY;
|
|
addr.port = 0;
|
|
enet_socket_bind(s, &addr);
|
|
sock_ = intptr_t(s);
|
|
return true;
|
|
}
|
|
|
|
void Scanner::ping(uint16_t discoveryPort) {
|
|
if (sock_ < 0) return;
|
|
ENetBuffer b = buffer(const_cast<char*>(kQuery), std::strlen(kQuery));
|
|
ENetAddress to;
|
|
to.host = ENET_HOST_BROADCAST;
|
|
to.port = discoveryPort;
|
|
enet_socket_send(ENetSocket(sock_), &to, &b, 1);
|
|
// also ask localhost directly (broadcast does not always loop back)
|
|
enet_address_set_host(&to, "127.0.0.1");
|
|
enet_socket_send(ENetSocket(sock_), &to, &b, 1);
|
|
}
|
|
|
|
void Scanner::poll(std::vector<ServerInfo>& found) {
|
|
if (sock_ < 0) return;
|
|
char buf[256];
|
|
for (int i = 0; i < 32; ++i) {
|
|
ENetAddress from;
|
|
ENetBuffer b = buffer(buf, sizeof buf - 1);
|
|
int n = enet_socket_receive(ENetSocket(sock_), &from, &b, 1);
|
|
if (n <= 0) return;
|
|
buf[n] = 0;
|
|
size_t rl = std::strlen(kReply);
|
|
if (std::strncmp(buf, kReply, rl) != 0) continue;
|
|
ServerInfo info;
|
|
char ip[64] = {};
|
|
enet_address_get_host_ip(&from, ip, sizeof ip);
|
|
info.address = ip;
|
|
unsigned port = 0;
|
|
int players = 0;
|
|
int consumed = 0;
|
|
if (std::sscanf(buf + rl, "%u|%d|%n", &port, &players, &consumed) < 2) continue;
|
|
info.port = uint16_t(port);
|
|
info.players = players;
|
|
info.name = buf + rl + consumed;
|
|
bool dup = false;
|
|
for (ServerInfo& s : found)
|
|
if (s.port == info.port && (s.address == info.address || s.name == info.name)) {
|
|
s.players = info.players;
|
|
dup = true;
|
|
}
|
|
if (!dup) found.push_back(info);
|
|
}
|
|
}
|
|
|
|
} // namespace bs::net
|