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:
170
client/NetClient.cpp
Normal file
170
client/NetClient.cpp
Normal 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
|
||||
Reference in New Issue
Block a user