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
This commit is contained in:
236
shared/Net.cpp
Normal file
236
shared/Net.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user