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:
Gabe
2026-10-02 15:20:27 -05:00
commit 238cd9c6c3
3409 changed files with 100493 additions and 0 deletions

170
client/NetClient.cpp Normal file
View File

@@ -0,0 +1,170 @@
#include "NetClient.h"
#include "hd2d/Log.h"
namespace bs {
bool NetClient::connect(const std::string& host, uint16_t port, const std::string& token) {
disconnect();
net::initialize();
token_ = token;
error_.clear();
hasProfile_ = false;
players.clear();
snapshots.clear();
events.clear();
dialogues.clear();
toasts.clear();
chat.clear();
welcome_ = {};
profile_ = {};
host_ = std::make_unique<net::Host>();
if (!host_->connect(host, port)) {
state_ = State::Failed;
error_ = "Could not resolve " + host;
return false;
}
peer_ = 1; // first (only) peer of a client host
state_ = State::Connecting;
return true;
}
void NetClient::disconnect() {
if (state_ != State::Idle && state_ != State::Failed && peer_) {
host_->disconnect(peer_);
host_->flush();
}
state_ = State::Idle;
peer_ = 0;
}
uint32_t NetClient::pingMs() const { return peer_ && host_ ? host_->roundTripMs(peer_) : 0; }
void NetClient::send(net::Channel ch, const Writer& w) {
if (peer_ && host_) host_->send(peer_, ch, w.data());
}
void NetClient::update() {
if (!host_ || !host_->valid()) return;
net::Event ev;
while (host_ && host_->poll(ev, 0)) {
switch (ev.type) {
case net::Event::Connect: {
peer_ = ev.peer;
Writer w(Msg::Hello);
HelloMsg{kProtocolVersion, token_}.write(w);
send(net::Reliable, w);
break;
}
case net::Event::Disconnect:
if (state_ != State::Failed)
error_ = state_ == State::Connecting ? "Could not reach the server" : "Disconnected from the server";
state_ = State::Failed;
peer_ = 0;
break;
case net::Event::Receive: handle(ev.data); break;
default: break;
}
}
if (host_) host_->flush();
}
void NetClient::handle(const std::vector<uint8_t>& data) {
if (data.empty()) return;
Reader r(data.data() + 1, data.size() - 1);
switch (Msg(data[0])) {
case Msg::Welcome:
welcome_.read(r);
if (!r.ok()) return;
state_ = welcome_.hasCharacter ? State::InGame : State::NeedCharacter;
break;
case Msg::Reject:
error_ = r.str();
state_ = State::Failed;
break;
case Msg::Profile: {
ProfileMsg p;
p.read(r);
if (!r.ok()) return;
profile_ = std::move(p);
hasProfile_ = true;
profileChanged = true;
if (state_ == State::NeedCharacter) state_ = State::InGame;
break;
}
case Msg::PlayerInfo: {
PlayerInfoMsg p;
p.read(r);
if (r.ok()) players[p.entityId] = std::move(p);
break;
}
case Msg::PlayerLeft: {
uint32_t id = r.u32();
players.erase(id);
playersLeft.push_back(id);
break;
}
case Msg::Snapshot: {
SnapshotMsg s;
s.read(r);
if (!r.ok()) return;
snapshots.push_back(std::move(s));
while (snapshots.size() > 8) snapshots.pop_front();
break;
}
case Msg::Event: {
GameEvent e;
e.read(r);
if (r.ok()) events.push_back(e);
break;
}
case Msg::Dialogue: dialogues.push_back(r.str()); break;
case Msg::ChatLine: {
ChatEntry c;
c.entity = r.u32();
c.name = r.str();
c.text = r.str();
if (r.ok()) chat.push_back(std::move(c));
if (chat.size() > 40) chat.erase(chat.begin());
break;
}
case Msg::Toast: toasts.push_back(r.str()); break;
default: break;
}
}
void NetClient::sendCreate(const std::string& name, const Appearance& look) {
Writer w(Msg::CreateCharacter);
CreateCharacterMsg{name, look}.write(w);
send(net::Reliable, w);
}
void NetClient::sendInput(const InputCmd& cmd) {
recentInputs_.push_back(cmd);
if (recentInputs_.size() > 4) recentInputs_.erase(recentInputs_.begin());
Writer w(Msg::Input);
InputMsg m;
m.cmds = recentInputs_;
m.write(w);
send(net::Unreliable, w);
}
void NetClient::sendAction(ActionKind kind, uint32_t target, float x, float z) {
Writer w(Msg::Action);
ActionMsg{kind, target, x, z}.write(w);
send(net::Reliable, w);
}
void NetClient::sendChat(const std::string& text) {
Writer w(Msg::Chat);
w.str(text);
send(net::Reliable, w);
}
void NetClient::sendDialogueDone(const std::string& script) {
Writer w(Msg::DialogueDone);
w.str(script);
send(net::Reliable, w);
}
} // namespace bs