Files
brandling-safari/shared/Net.cpp
Gabe 238cd9c6c3 Brandling Safari: LAN safari-zone creature catching on the HD-2D engine
- 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
2026-10-02 15:20:27 -05:00

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