- 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
171 lines
4.8 KiB
C++
171 lines
4.8 KiB
C++
#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
|