// Headless test client for the Brandling Safari server. // safari_bot [--host 127.0.0.1] [--port N] [--token hex] [--seconds 60] [--name Bot] // Connects, creates a character if needed, finishes Professor Henry's intro, talks to him, // runs through tall grass throwing capsules at nearby wild creatures, fishes once from the // dock, then prints a summary. Movement uses the shared stepMovement with client-side // prediction + reconciliation against the server's acknowledged input sequence. #include "Appearance.h" #include "Movement.h" #include "Net.h" #include "Protocol.h" #include "Rng.h" #include "Zone.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace bs; namespace { enum class Phase { Joining, Henry, Grass, Dock, Grass2, Done }; const char* phaseName(Phase p) { switch (p) { case Phase::Joining: return "joining"; case Phase::Henry: return "henry"; case Phase::Grass: return "grass"; case Phase::Dock: return "dock"; case Phase::Grass2: return "grass2"; case Phase::Done: return "done"; } return "?"; } class Bot { public: std::string hostName = "127.0.0.1"; uint16_t port = kDefaultPort; std::string token, name = "Bot"; double seconds = 60.0; int run(); private: void log(const char* fmt, ...); void handle(const std::vector& data); void onSnapshot(const SnapshotMsg& s); void onEvent(const GameEvent& e); void update(float dt); void sendMsg(const Writer& w, bool reliable = true) { host_.send(peer_, reliable ? net::Reliable : net::Unreliable, w.data()); } void sendAction(ActionKind k, uint32_t target = 0, float x = 0, float z = 0) { ActionMsg a; a.kind = k; a.target = target; a.x = x; a.z = z; Writer w(Msg::Action); a.write(w); sendMsg(w); } void sendDialogueDone(const std::string& id) { Writer w(Msg::DialogueDone); w.str(id); sendMsg(w); log("-> DialogueDone %s", id.c_str()); } void setPhase(Phase p) { if (p == phase_) return; log("phase %s -> %s", phaseName(phase_), phaseName(p)); phase_ = p; phaseTime_ = 0.0; path_.clear(); } // navigation bool planPath(glm::vec2 goal); glm::vec2 steer(glm::vec2 goal, float arrive, bool& arrived); glm::vec2 randomGrassPoint(); net::Host host_; uint32_t peer_ = 0; bool connected_ = false, welcomed_ = false, rejected_ = false, spawned_ = false, haveSnap_ = false; Zone zone_; CharacterManifest manifest_; Rng rng_{1}; double time_ = 0.0, phaseTime_ = 0.0; Phase phase_ = Phase::Joining; uint32_t me_ = 0; ProfileMsg profile_; bool haveProfile_ = false; MoveState pred_; uint32_t seq_ = 0, lastAck_ = 0, lastSnapTick_ = 0; std::deque pending_; float maxCorrection_ = 0.0f; std::map ents_; std::map names_; std::set followersSeen_, playersSeen_; glm::vec2 firstPos_{0.0f}; bool firstPosSet_ = false; // path following std::vector path_; size_t pathIdx_ = 0; glm::vec2 pathGoal_{0.0f}; glm::vec2 lastProgressPos_{0.0f}; float stuckTimer_ = 0.0f; glm::vec2 grassGoal_{0.0f}; bool grassGoalSet_ = false; int patch_ = -1; // behaviour state float throwCooldown_ = 0.0f; std::set pendingThrows_; std::vector> pendingDialogue_; // reply at time bool introDialogSeen_ = false, talkedHenry_ = false, interactSent_ = false; double reelAt_ = -1.0, castSentAt_ = -1.0, nextPlanAt_ = 0.0; uint32_t hookedId_ = 0; bool lineOut_ = false; int casts_ = 0; double dockDoneAt_ = -1.0; bool chatSent_ = false; int toggles_ = 0; uint32_t toggledUid_ = 0; double lastSnapLog_ = -10.0; // summary int encounters_ = 0, throws_ = 0, catches_ = 0, bites_ = 0, hooks_ = 0, linesLost_ = 0, fled_ = 0, rustles_ = 0; int capsulesStart_ = -1; }; void Bot::log(const char* fmt, ...) { std::printf("[%s %6.2f] ", name.c_str(), time_); va_list ap; va_start(ap, fmt); std::vprintf(fmt, ap); va_end(ap); std::printf("\n"); std::fflush(stdout); } // ------------------------------------------------------------------ messages void Bot::handle(const std::vector& data) { if (data.empty()) return; Reader r(data); Msg m = Msg(r.u8()); switch (m) { case Msg::Welcome: { WelcomeMsg w; w.read(r); if (!r.ok()) return; welcomed_ = true; log("<- Welcome entity %u hasCharacter %d zone %s seed %u server \"%s\"", w.entityId, w.hasCharacter, w.zoneId.c_str(), w.zoneSeed, w.serverName.c_str()); if (!zone_.generate(w.zoneId, w.zoneSeed)) { log("unknown zone %s", w.zoneId.c_str()); rejected_ = true; return; } if (w.entityId) me_ = w.entityId; if (!w.hasCharacter) { CreateCharacterMsg c; c.name = name; c.look = manifest_.defaultLook(rng_.chance(0.5f) ? "male" : "female"); manifest_.sanitize(c.look); Writer cw(Msg::CreateCharacter); c.write(cw); sendMsg(cw); log("-> CreateCharacter \"%s\" body %s", c.name.c_str(), c.look.body.c_str()); } break; } case Msg::Reject: { std::string why = r.str(); log("<- Reject: %s", why.c_str()); rejected_ = true; break; } case Msg::Profile: { ProfileMsg p; p.read(r); if (!r.ok()) return; int party = int(std::count_if(p.creatures.begin(), p.creatures.end(), [](auto& c) { return c.inParty; })); log("<- Profile entity %u \"%s\" capsules %d intro %d dex %u/%u creatures %zu (party %d)", p.entityId, p.name.c_str(), p.capsules, p.introDone, p.caught, p.seen, p.creatures.size(), party); profile_ = p; haveProfile_ = true; me_ = p.entityId; if (capsulesStart_ < 0) capsulesStart_ = p.capsules; spawned_ = true; break; } case Msg::PlayerInfo: { PlayerInfoMsg p; p.read(r); if (!r.ok()) return; names_[p.entityId] = p.name; log("<- PlayerInfo entity %u \"%s\" (%s, hat '%s')%s", p.entityId, p.name.c_str(), p.look.body.c_str(), p.look.items[Hat].c_str(), p.entityId == me_ ? " (me)" : ""); break; } case Msg::PlayerLeft: { uint32_t id = r.u32(); log("<- PlayerLeft entity %u \"%s\"", id, names_.count(id) ? names_[id].c_str() : "?"); break; } case Msg::Snapshot: { SnapshotMsg s; s.read(r); if (r.ok()) onSnapshot(s); break; } case Msg::Event: { GameEvent e; e.read(r); if (r.ok()) onEvent(e); break; } case Msg::Dialogue: { std::string id = r.str(); log("<- Dialogue %s", id.c_str()); if (id == "henry_intro") introDialogSeen_ = true; if (id == "henry_tips") talkedHenry_ = true; pendingDialogue_.push_back({time_ + 0.8, id}); break; } case Msg::ChatLine: { uint32_t id = r.u32(); std::string n = r.str(), text = r.str(); log("<- ChatLine [%u] <%s> %s", id, n.c_str(), text.c_str()); break; } case Msg::Toast: { std::string t = r.str(); log("<- Toast \"%s\"", t.c_str()); break; } default: log("<- unknown message %u (%zu bytes)", unsigned(m), data.size()); break; } } void Bot::onSnapshot(const SnapshotMsg& s) { if (haveSnap_ && s.tick <= lastSnapTick_) return; // out of order lastSnapTick_ = s.tick; ents_.clear(); int players = 0, wild = 0, followers = 0, mine = 0, npc = 0; for (const EntityState& e : s.entities) { ents_[e.id] = e; switch (e.kind) { case EntityKind::Player: ++players; if (e.id != me_ && playersSeen_.insert(e.id).second) log("sees player entity %u \"%s\" at (%.1f, %.1f)", e.id, names_.count(e.id) ? names_[e.id].c_str() : "?", e.pos.x, e.pos.z); break; case EntityKind::Wild: ++wild; break; case EntityKind::Follower: ++followers; if (e.owner == me_) ++mine; if (followersSeen_.insert(e.id).second) log("follower entity %u species %u order %u owner %u%s at (%.1f, %.1f)", e.id, e.species, e.order, e.owner, e.owner == me_ ? " (mine)" : "", e.pos.x, e.pos.z); break; case EntityKind::Npc: ++npc; break; } } auto it = ents_.find(me_); if (me_ && it != ents_.end()) { glm::vec2 server(it->second.pos.x, it->second.pos.z); if (!haveSnap_) { pred_.pos = server; pred_.yaw = it->second.yaw; firstPos_ = server; firstPosSet_ = true; log("first snapshot: I am at (%.2f, %.2f), hour %.2f, weather %s", server.x, server.y, s.hour, weatherName(s.weather)); } haveSnap_ = true; if (s.ackInput >= lastAck_) { lastAck_ = s.ackInput; while (!pending_.empty() && pending_.front().seq <= s.ackInput) pending_.pop_front(); MoveState st = pred_; st.pos = server; for (const InputCmd& c : pending_) stepMovement(zone_, st, c, kTickDt); maxCorrection_ = std::max(maxCorrection_, glm::length(st.pos - pred_.pos)); pred_ = st; } } if (time_ - lastSnapLog_ >= 5.0) { lastSnapLog_ = time_; log("<- Snapshot tick %u ack %u hour %.2f %s blend %.2f: %zu entities (players %d wild %d followers %d " "[mine %d] npc %d)", s.tick, s.ackInput, s.hour, weatherName(s.weather), s.weatherBlend, s.entities.size(), players, wild, followers, mine, npc); } } void Bot::onEvent(const GameEvent& e) { bool mine = e.a == me_; switch (e.kind) { case EventKind::Emerge: log("<- Event Emerge entity %u %s at (%.1f, %.1f)", e.a, e.i ? "water" : "grass", e.p.x, e.p.z); if (!e.i && me_ && ents_.count(me_)) { const EntityState& m = ents_[me_]; if (glm::length(glm::vec2(e.p.x - m.pos.x, e.p.z - m.pos.z)) < 7.0f) ++encounters_; } break; case EventKind::Throw: log("<- Event Throw player %u -> entity %u flight %.2fs", e.a, e.b, e.f); break; case EventKind::CaptureResult: log("<- Event CaptureResult player %u entity %u %s wobbles %d", e.a, e.b, e.i ? "SUCCESS" : "miss", e.j); if (mine) pendingThrows_.erase(e.b); break; case EventKind::Caught: log("<- Event Caught player %u species %u%s", e.a, e.b, e.i ? " (first!)" : ""); if (mine) ++catches_; break; case EventKind::Fled: log("<- Event Fled entity %u", e.a); if (pendingThrows_.count(e.a)) ++fled_; break; case EventKind::Cast: log("<- Event Cast player %u bobber (%.1f, %.1f)", e.a, e.p.x, e.p.z); if (mine) lineOut_ = true, castSentAt_ = -1.0; break; case EventKind::Bite: log("<- Event Bite player %u", e.a); if (mine) { ++bites_; reelAt_ = time_ + 0.25 + rng_.uniform() * 0.4; } break; case EventKind::Hooked: log("<- Event Hooked player %u entity %u", e.a, e.b); if (mine) { ++hooks_; lineOut_ = false; hookedId_ = e.b; dockDoneAt_ = time_ + 7.0; // stay a moment to throw at the catch } break; case EventKind::LineLost: log("<- Event LineLost player %u", e.a); if (mine) { ++linesLost_; lineOut_ = false; reelAt_ = -1.0; castSentAt_ = -1.0; } break; case EventKind::Splash: log("<- Event Splash at (%.1f, %.1f)", e.p.x, e.p.z); break; case EventKind::Rustle: ++rustles_; log("<- Event Rustle at (%.1f, %.1f)", e.p.x, e.p.z); break; } } // ------------------------------------------------------------------ navigation bool Bot::planPath(glm::vec2 goal) { const int W = zone_.width(), D = zone_.depth(); auto idx = [&](int x, int z) { return z * W + x; }; auto center = [](int x, int z) { return glm::vec2(float(x) + 0.5f, float(z) + 0.5f); }; int sx = int(pred_.pos.x), sz = int(pred_.pos.y), gx = int(goal.x), gz = int(goal.y); if (!zone_.inBounds(sx, sz) || !zone_.inBounds(gx, gz)) return false; std::vector cost(size_t(W) * D, 1e30f); std::vector from(size_t(W) * D, -1); using Node = std::pair; std::priority_queue, std::greater> open; cost[idx(sx, sz)] = 0; open.push({0, idx(sx, sz)}); const int dx[8] = {1, -1, 0, 0, 1, 1, -1, -1}, dz[8] = {0, 0, 1, -1, 1, -1, 1, -1}; bool found = false; while (!open.empty()) { auto [f, cur] = open.top(); open.pop(); int cx = cur % W, cz = cur / W; if (cx == gx && cz == gz) { found = true; break; } glm::vec2 a = (cx == sx && cz == sz) ? pred_.pos : center(cx, cz); for (int k = 0; k < 8; ++k) { int nx = cx + dx[k], nz = cz + dz[k]; if (!zone_.inBounds(nx, nz)) continue; glm::vec2 b = center(nx, nz); // generous clearance so the straight-line follow does not clip props if (!zone_.walkable(a, b, 0.45f) || !zone_.walkable(a, (a + b) * 0.5f, 0.45f)) continue; if (k >= 4 && (!zone_.walkable(a, center(nx, cz), 0.45f) || !zone_.walkable(a, center(cx, nz), 0.45f))) continue; float nc = cost[cur] + (k >= 4 ? 1.414f : 1.0f); if (nc < cost[idx(nx, nz)]) { cost[idx(nx, nz)] = nc; from[idx(nx, nz)] = cur; float h = glm::length(b - goal); open.push({nc + h, idx(nx, nz)}); } } } if (!found) return false; path_.clear(); for (int n = idx(gx, gz); n != -1 && n != idx(sx, sz); n = from[n]) path_.push_back(center(n % W, n / W)); std::reverse(path_.begin(), path_.end()); if (!path_.empty()) path_.back() = goal; else path_.push_back(goal); pathIdx_ = 0; pathGoal_ = goal; stuckTimer_ = 0.0f; lastProgressPos_ = pred_.pos; return true; } // Direction toward the goal along the planned path; replans when stuck. glm::vec2 Bot::steer(glm::vec2 goal, float arrive, bool& arrived) { arrived = glm::length(goal - pred_.pos) <= arrive; if (arrived) return glm::vec2(0.0f); if (path_.empty() || glm::length(pathGoal_ - goal) > 0.5f) { // A* is not free: at most one plan attempt per 0.7 s if (time_ < nextPlanAt_) return glm::normalize(goal - pred_.pos); nextPlanAt_ = time_ + 0.7; if (!planPath(goal)) { path_.clear(); return glm::normalize(goal - pred_.pos); } } while (pathIdx_ + 1 < path_.size() && glm::length(path_[pathIdx_] - pred_.pos) < 0.45f) ++pathIdx_; glm::vec2 d = path_[pathIdx_] - pred_.pos; if (glm::length(d) < 1e-3f) return glm::vec2(0.0f); return glm::normalize(d); } glm::vec2 Bot::randomGrassPoint() { const ZonePatch& p = zone_.tallGrassPatches[size_t(patch_)]; for (int i = 0; i < 60; ++i) { float a = rng_.range(0.0f, 6.2832f), r = rng_.range(0.3f, 0.85f) * p.r; glm::vec2 q = p.c + glm::vec2(std::cos(a), std::sin(a)) * r; if (zone_.inTallGrass(q) && zone_.walkable(q, q) && glm::length(q - pred_.pos) > 2.5f) return q; } return p.c; } // ------------------------------------------------------------------ behaviour void Bot::update(float dt) { for (auto it = pendingDialogue_.begin(); it != pendingDialogue_.end();) { if (time_ >= it->first) { sendDialogueDone(it->second); it = pendingDialogue_.erase(it); } else { ++it; } } if (!spawned_ || !haveSnap_) return; if (!chatSent_) { chatSent_ = true; Writer w(Msg::Chat); w.str("Hello from " + name + "!"); sendMsg(w); } phaseTime_ += dt; throwCooldown_ = std::max(0.0f, throwCooldown_ - dt); glm::vec2 dir(0.0f); bool run = false, arrived = false; switch (phase_) { case Phase::Joining: // wait for the intro dialogue to be answered (or a few seconds for returning players) if ((haveProfile_ && profile_.introDone) || phaseTime_ > 5.0) setPhase(Phase::Henry); break; case Phase::Henry: { glm::vec2 goal = zone_.henry + glm::normalize(zone_.spawn - zone_.henry) * 1.6f; dir = steer(goal, 0.4f, arrived); if (arrived && !interactSent_) { interactSent_ = true; sendAction(ActionKind::Interact, 1); log("-> Interact Henry (entity 1) at %.2f units", glm::length(pred_.pos - zone_.henry)); } if (talkedHenry_ || phaseTime_ > 15.0) setPhase(Phase::Grass); break; } case Phase::Grass: case Phase::Grass2: { if (patch_ < 0) { float best = 1e9f; for (size_t i = 0; i < zone_.tallGrassPatches.size(); ++i) { float d = glm::length(zone_.tallGrassPatches[i].c - pred_.pos); if (d < best) best = d, patch_ = int(i); } log("nearest tall grass patch #%d at (%.1f, %.1f)", patch_, zone_.tallGrassPatches[patch_].c.x, zone_.tallGrassPatches[patch_].c.y); } if (!grassGoalSet_) { grassGoal_ = zone_.inTallGrass(pred_.pos) ? randomGrassPoint() : zone_.tallGrassPatches[patch_].c; grassGoalSet_ = true; } dir = steer(grassGoal_, 0.5f, arrived); run = true; if (arrived || stuckTimer_ > 3.0f) grassGoalSet_ = false, path_.clear(); if (phase_ == Phase::Grass && time_ > seconds * 0.45) setPhase(Phase::Dock); break; } case Phase::Dock: { const ZoneDock& d = zone_.docks[0]; glm::vec2 goal(d.min.x + 0.7f, (d.min.y + d.max.y) * 0.5f); if (!lineOut_ && castSentAt_ < 0.0 && dockDoneAt_ < 0.0) { dir = steer(goal, 0.3f, arrived); } else if (dockDoneAt_ > 0.0 && ents_.count(hookedId_)) { // walk up to the creature we just fished out so it is in throwing range const EntityState& h = ents_[hookedId_]; glm::vec2 hp(h.pos.x, h.pos.z); if (glm::length(hp - pred_.pos) > 4.0f) dir = steer(hp, 3.5f, arrived); arrived = true; } else { arrived = true; } run = !arrived; if (castSentAt_ > 0.0 && !lineOut_ && time_ - castSentAt_ > 2.0) castSentAt_ = -1.0; // cast refused if (arrived && !lineOut_ && castSentAt_ < 0.0 && dockDoneAt_ < 0.0 && casts_ < 3 && throwCooldown_ <= 0.0f) { ++casts_; castSentAt_ = time_; sendAction(ActionKind::Cast, 0, -1.0f, 0.0f); log("-> Cast west from (%.2f, %.2f)", pred_.pos.x, pred_.pos.y); throwCooldown_ = 1.5f; } if (reelAt_ > 0.0 && time_ >= reelAt_) { reelAt_ = -1.0; sendAction(ActionKind::Reel); log("-> Reel"); } bool gaveUp = casts_ >= 3 && !lineOut_ && castSentAt_ < 0.0 && dockDoneAt_ < 0.0; if ((dockDoneAt_ > 0.0 && time_ >= dockDoneAt_) || gaveUp || phaseTime_ > 35.0) { patch_ = -1; grassGoalSet_ = false; setPhase(Phase::Grass2); } break; } case Phase::Done: break; } // near the end: send one party creature to the box and bring it back (ToggleParty) if (phase_ == Phase::Grass2 && haveProfile_ && toggles_ < 2 && time_ > seconds - 8.0 + 3.0 * toggles_) { if (!toggledUid_) for (const OwnedCreature& c : profile_.creatures) if (c.inParty) { toggledUid_ = c.uid; break; } if (toggledUid_) { sendAction(ActionKind::ToggleParty, toggledUid_); log("-> ToggleParty uid %u (%s)", toggledUid_, toggles_ == 0 ? "to box" : "back to party"); } ++toggles_; } // stuck detection for path following if (glm::length(dir) > 0.0f) { if (glm::length(pred_.pos - lastProgressPos_) > 0.3f) { lastProgressPos_ = pred_.pos; stuckTimer_ = 0.0f; } else if ((stuckTimer_ += dt) > 1.0f) { path_.clear(); // replan from here if (stuckTimer_ > 1.5f) dir = glm::vec2(-dir.y, dir.x); // sidestep } } // throw at any wild creature within 7 units (not while a line is out; keep 3 capsules for the fishing catch) int reserve = phase_ == Phase::Grass || (phase_ == Phase::Dock && hooks_ == 0) ? 3 : 0; if (!lineOut_ && haveProfile_ && profile_.capsules > reserve && throwCooldown_ <= 0.0f) { uint32_t best = 0; float bestD = 7.0f; for (auto& [id, e] : ents_) { if (e.kind != EntityKind::Wild || pendingThrows_.count(id)) continue; if (e.anim == Anim::Capturing || e.anim == Anim::Flee || e.anim == Anim::Hooked) continue; float d = glm::length(glm::vec2(e.pos.x, e.pos.z) - pred_.pos); if (d < bestD) bestD = d, best = id; } if (best) { sendAction(ActionKind::Throw, best); pendingThrows_.insert(best); ++throws_; throwCooldown_ = 1.0f; log("-> Throw at entity %u (species %u, %.1f units)", best, ents_[best].species, bestD); } } InputCmd c = makeInput(++seq_, dir, run); stepMovement(zone_, pred_, c, kTickDt); pending_.push_back(c); while (pending_.size() > 90) pending_.pop_front(); InputMsg im; for (size_t i = pending_.size() >= 4 ? pending_.size() - 4 : 0; i < pending_.size(); ++i) im.cmds.push_back(pending_[i]); Writer w(Msg::Input); im.write(w); sendMsg(w, false); } int Bot::run() { if (token.empty()) { Rng seedRng(uint64_t(std::chrono::high_resolution_clock::now().time_since_epoch().count())); const char* hex = "0123456789abcdef"; for (int i = 0; i < 32; ++i) token.push_back(hex[seedRng.next() % 16]); } rng_.reseed(hash32(token.c_str())); std::string err; if (!manifest_.load(std::string(BS_ASSET_DIR) + "/characters/manifest.json", &err)) { std::fprintf(stderr, "manifest: %s\n", err.c_str()); return 1; } if (!net::initialize() || !host_.connect(hostName, port)) { std::fprintf(stderr, "cannot connect to %s:%u\n", hostName.c_str(), unsigned(port)); return 1; } log("connecting to %s:%u with token %s", hostName.c_str(), unsigned(port), token.c_str()); using Clock = std::chrono::steady_clock; const auto step = std::chrono::duration_cast(std::chrono::duration(kTickDt)); auto next = Clock::now(); auto start = next; bool disconnected = false; while (!rejected_ && !disconnected) { net::Event ev; while (host_.poll(ev, 0)) { if (ev.type == net::Event::Connect) { connected_ = true; peer_ = ev.peer; HelloMsg h; h.token = token; Writer w(Msg::Hello); h.write(w); sendMsg(w); log("-> Hello v%u", unsigned(h.version)); } else if (ev.type == net::Event::Disconnect) { log("disconnected by server"); disconnected = true; } else if (ev.type == net::Event::Receive) { handle(ev.data); } } time_ = std::chrono::duration(Clock::now() - start).count(); if (time_ >= seconds) break; if (!connected_ && time_ > 5.0) { log("connection timed out"); return 1; } update(kTickDt); host_.flush(); next += step; auto now = Clock::now(); if (now - next > std::chrono::milliseconds(200)) next = now; // never burst-send a backlog of inputs std::this_thread::sleep_until(next); } if (connected_ && !disconnected) { host_.disconnect(peer_); auto until = Clock::now() + std::chrono::milliseconds(300); net::Event ev; while (Clock::now() < until) host_.poll(ev, 20); } int party = 0; for (auto& c : profile_.creatures) party += c.inParty; std::printf("\n===== %s summary (token %s) =====\n", name.c_str(), token.c_str()); std::printf(" spawned: %s as entity %u, start position (%.2f, %.2f)\n", spawned_ ? "yes" : "no", me_, firstPos_.x, firstPos_.y); std::printf(" intro done: %s, talked to Henry: %s\n", profile_.introDone ? "yes" : "no", talkedHenry_ ? "yes" : "no"); std::printf(" other players seen: %zu, followers seen: %zu\n", playersSeen_.size(), followersSeen_.size()); std::printf(" encounters near me: %d, rustles: %d, throws: %d, catches: %d, fled: %d\n", encounters_, rustles_, throws_, catches_, fled_); std::printf(" casts: %d, bites: %d, hooks: %d, lines lost: %d\n", casts_, bites_, hooks_, linesLost_); std::printf(" capsules: %d (started %d), creatures: %zu, party: %d, dex caught/seen: %u/%u\n", profile_.capsules, capsulesStart_, profile_.creatures.size(), party, profile_.caught, profile_.seen); std::printf(" final position (%.2f, %.2f), max prediction correction %.3f units\n", pred_.pos.x, pred_.pos.y, maxCorrection_); std::fflush(stdout); net::shutdown(); return (!rejected_ && spawned_) ? 0 : 1; } } // namespace int main(int argc, char** argv) { Bot bot; for (int i = 1; i < argc; ++i) { std::string a = argv[i]; auto next = [&]() -> std::string { if (i + 1 >= argc) { std::fprintf(stderr, "missing value for %s\n", a.c_str()); std::exit(2); } return argv[++i]; }; if (a == "--host") bot.hostName = next(); else if (a == "--port") bot.port = uint16_t(std::atoi(next().c_str())); else if (a == "--token") bot.token = next(); else if (a == "--seconds") bot.seconds = std::atof(next().c_str()); else if (a == "--name") bot.name = next(); else { std::fprintf(stderr, "usage: safari_bot [--host 127.0.0.1] [--port N] [--token hex] [--seconds 60] [--name Bot]\n"); return 2; } } return bot.run(); }