// Brandling Safari dedicated server. // safari_server [--port 27420] [--name "Brandling Safari"] [--db ] [--seed N] [--zone meadow] // [--roster ] [--manifest ] [--day-minutes 30] [--hour H] // [--weather clear|cloudy|rain|storm|fog] #include "Database.h" #include "Game.h" #include "Log.h" #include "Net.h" #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #include #include #else #include #include #endif using namespace bs; namespace { std::atomic gStop{false}; void onSignal(int) { gStop = true; } std::string defaultDbPath() { std::string base; if (const char* x = std::getenv("XDG_DATA_HOME"); x && *x) base = x; #if defined(_WIN32) else if (const char* a = std::getenv("APPDATA"); a && *a) base = a; #endif else if (const char* h = std::getenv("HOME"); h && *h) base = std::string(h) + "/.local/share"; else base = "."; return base + "/brandling-safari/world.db"; } // Non-blocking stdin lines for the console. POSIX polls fd 0 from the tick loop (a reader thread // blocked inside stdio would deadlock glibc's exit-time stream flush); Windows uses a detached // reader thread with a process-lifetime queue. class ConsoleInput { public: bool pop(std::string& out) { #if defined(_WIN32) static bool started = false; if (!started) { started = true; std::thread([] { std::string line; while (std::getline(std::cin, line)) { std::lock_guard lock(mutex()); queue().push_back(line); } }).detach(); } std::lock_guard lock(mutex()); if (queue().empty()) return false; out = std::move(queue().front()); queue().pop_front(); return true; #else while (open_) { pollfd p{0, POLLIN, 0}; if (::poll(&p, 1, 0) <= 0 || !(p.revents & (POLLIN | POLLHUP))) break; char buf[512]; ssize_t n = ::read(0, buf, sizeof buf); if (n <= 0) { open_ = false; // EOF (or error): no console, keep serving break; } pending_.append(buf, size_t(n)); } size_t nl = pending_.find('\n'); if (nl == std::string::npos) return false; out = pending_.substr(0, nl); pending_.erase(0, nl + 1); return true; #endif } private: #if defined(_WIN32) static std::mutex& mutex() { static std::mutex m; return m; } static std::deque& queue() { static std::deque q; return q; } #else bool open_ = true; std::string pending_; #endif }; void usage() { std::printf( "usage: safari_server [--port N] [--name NAME] [--db PATH] [--seed N] [--zone meadow]\n" " [--roster PATH] [--manifest PATH] [--day-minutes M] [--hour H]\n" " [--weather clear|cloudy|rain|storm|fog]\n"); } } // namespace int main(int argc, char** argv) { uint16_t port = kDefaultPort; GameConfig cfg; std::string dbPath = defaultDbPath(); std::string rosterPath = std::string(BS_ASSET_DIR) + "/creatures/roster.json"; std::string manifestPath = std::string(BS_ASSET_DIR) + "/characters/manifest.json"; bool seedGiven = false; 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 == "--port") port = uint16_t(std::atoi(next().c_str())); else if (a == "--name") cfg.serverName = next().substr(0, 64); else if (a == "--db") dbPath = next(); else if (a == "--seed") cfg.seed = uint32_t(std::strtoul(next().c_str(), nullptr, 10)), seedGiven = true; else if (a == "--zone") cfg.zoneId = next(); else if (a == "--roster") rosterPath = next(); else if (a == "--manifest") manifestPath = next(); else if (a == "--day-minutes") cfg.dayMinutes = std::max(0.1f, std::strtof(next().c_str(), nullptr)); else if (a == "--hour") cfg.hour = std::clamp(std::strtof(next().c_str(), nullptr), 0.0f, 23.999f); else if (a == "--weather") { Weather w; std::string v = next(); if (!parseWeather(v, w)) { std::fprintf(stderr, "unknown weather '%s'\n", v.c_str()); return 2; } cfg.weather = int(w); } else if (a == "--help" || a == "-h") { usage(); return 0; } else { std::fprintf(stderr, "unknown argument '%s'\n", a.c_str()); usage(); return 2; } } std::string err; Roster roster; if (!roster.load(rosterPath, &err)) { std::fprintf(stderr, "roster: %s\n", err.c_str()); return 1; } CharacterManifest manifest; if (!manifest.load(manifestPath, &err)) { std::fprintf(stderr, "manifest: %s\n", err.c_str()); return 1; } Database db; if (!db.open(dbPath, &err)) { std::fprintf(stderr, "database %s: %s\n", dbPath.c_str(), err.c_str()); return 1; } // The zone seed is part of the world: keep the first one unless --seed overrides it. std::string saved; if (!seedGiven && db.getWorld("seed", saved)) cfg.seed = uint32_t(std::strtoul(saved.c_str(), nullptr, 10)); db.setWorld("seed", std::to_string(cfg.seed)); Zone zone; if (!zone.generate(cfg.zoneId, cfg.seed)) { std::fprintf(stderr, "unknown zone '%s'\n", cfg.zoneId.c_str()); return 1; } if (!net::initialize()) { std::fprintf(stderr, "enet initialization failed\n"); return 1; } { net::Host host; if (!host.listen(port, 32)) { std::fprintf(stderr, "cannot listen on UDP port %u\n", unsigned(port)); net::shutdown(); return 1; } net::Beacon beacon; bool lan = beacon.open(kDiscoveryPort); Game game(host, db, zone, roster, manifest, cfg); logf("\"%s\" listening on UDP %u (LAN discovery %s), db %s", cfg.serverName.c_str(), unsigned(port), lan ? "on" : "unavailable", dbPath.c_str()); logf("type 'help' for console commands"); std::signal(SIGINT, onSignal); std::signal(SIGTERM, onSignal); ConsoleInput input; using Clock = std::chrono::steady_clock; const auto step = std::chrono::duration_cast(std::chrono::duration(kTickDt)); auto next = Clock::now(); bool running = true; while (running && !gStop) { auto now = Clock::now(); uint32_t waitMs = 0; if (next > now) waitMs = uint32_t(std::chrono::duration_cast(next - now).count()); net::Event ev; bool got = host.poll(ev, waitMs); while (got) { switch (ev.type) { case net::Event::Connect: game.onConnect(ev.peer); break; case net::Event::Disconnect: game.onDisconnect(ev.peer); break; case net::Event::Receive: game.onReceive(ev.peer, ev.data); break; default: break; } got = host.poll(ev, 0); } std::string line; while (running && input.pop(line)) running = game.console(line); now = Clock::now(); int steps = 0; while (now >= next && steps < 5) { game.tick(); next += step; ++steps; } if (now - next > std::chrono::seconds(1)) next = now; // fell far behind: drop the backlog beacon.poll(port, cfg.serverName, game.playerCount()); host.flush(); } logf("shutting down, saving..."); game.saveAll(); host.flush(); } db.close(); net::shutdown(); logf("bye"); return 0; }