Windows support: portable local server launch + clean quit, %APPDATA% data dir, Win32 libs, MinGW/MSVC build docs
This commit is contained in:
@@ -41,12 +41,19 @@ target_include_directories(bs_shared PUBLIC shared)
|
|||||||
target_link_libraries(bs_shared PUBLIC glm::glm nlohmann_json::nlohmann_json enet)
|
target_link_libraries(bs_shared PUBLIC glm::glm nlohmann_json::nlohmann_json enet)
|
||||||
target_compile_definitions(bs_shared PUBLIC GLM_FORCE_RADIANS GLM_ENABLE_EXPERIMENTAL
|
target_compile_definitions(bs_shared PUBLIC GLM_FORCE_RADIANS GLM_ENABLE_EXPERIMENTAL
|
||||||
BS_ASSET_DIR="${CMAKE_SOURCE_DIR}/assets")
|
BS_ASSET_DIR="${CMAKE_SOURCE_DIR}/assets")
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(bs_shared PUBLIC ws2_32 winmm) # ENet sockets + timers
|
||||||
|
endif()
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(bs_shared PRIVATE /W4)
|
target_compile_options(bs_shared PRIVATE /W4 /utf-8)
|
||||||
target_link_libraries(bs_shared PUBLIC ws2_32 winmm)
|
|
||||||
else()
|
else()
|
||||||
target_compile_options(bs_shared PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
target_compile_options(bs_shared PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
||||||
endif()
|
endif()
|
||||||
|
if(MINGW)
|
||||||
|
# no libgcc / libstdc++ DLLs to ship next to the executables; C99 printf (%zu) on msvcrt
|
||||||
|
add_link_options(-static-libgcc -static-libstdc++)
|
||||||
|
target_compile_definitions(bs_shared PUBLIC __USE_MINGW_ANSI_STDIO=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- Server -------------------------------------------------------------------
|
# --- Server -------------------------------------------------------------------
|
||||||
if(BS_BUILD_SERVER)
|
if(BS_BUILD_SERVER)
|
||||||
@@ -54,7 +61,12 @@ if(BS_BUILD_SERVER)
|
|||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
file(GLOB BS_SERVER_SOURCES CONFIGURE_DEPENDS server/*.cpp)
|
file(GLOB BS_SERVER_SOURCES CONFIGURE_DEPENDS server/*.cpp)
|
||||||
add_executable(safari_server ${BS_SERVER_SOURCES})
|
add_executable(safari_server ${BS_SERVER_SOURCES})
|
||||||
target_link_libraries(safari_server PRIVATE bs_shared SQLite::SQLite3 Threads::Threads)
|
if(TARGET SQLite3::SQLite3) # CMake >= 4.3 name; older releases only define SQLite::SQLite3
|
||||||
|
set(BS_SQLITE SQLite3::SQLite3)
|
||||||
|
else()
|
||||||
|
set(BS_SQLITE SQLite::SQLite3)
|
||||||
|
endif()
|
||||||
|
target_link_libraries(safari_server PRIVATE bs_shared ${BS_SQLITE} Threads::Threads)
|
||||||
if(NOT MSVC)
|
if(NOT MSVC)
|
||||||
target_compile_options(safari_server PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
target_compile_options(safari_server PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
||||||
endif()
|
endif()
|
||||||
@@ -86,6 +98,13 @@ if(BS_BUILD_CLIENT)
|
|||||||
if(NOT MSVC)
|
if(NOT MSVC)
|
||||||
target_compile_options(safari_client PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
target_compile_options(safari_client PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
||||||
endif()
|
endif()
|
||||||
|
if(WIN32)
|
||||||
|
# A game, not a console tool: no console window opens next to it.
|
||||||
|
set_target_properties(safari_client PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
|
if(MSVC)
|
||||||
|
target_link_options(safari_client PRIVATE /ENTRY:mainCRTStartup) # keep int main()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
# The client launches the dedicated server for "Host game" from its own folder.
|
# The client launches the dedicated server for "Host game" from its own folder.
|
||||||
if(BS_BUILD_SERVER)
|
if(BS_BUILD_SERVER)
|
||||||
add_dependencies(safari_client safari_server)
|
add_dependencies(safari_client safari_server)
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -56,6 +56,56 @@ ninja -C build # safari_client, safari_server, safari_bot
|
|||||||
|
|
||||||
Server only (no GPU needed): `cmake -S . -B build_srv -G Ninja -DBS_BUILD_CLIENT=OFF`.
|
Server only (no GPU needed): `cmake -S . -B build_srv -G Ninja -DBS_BUILD_CLIENT=OFF`.
|
||||||
|
|
||||||
|
Built files load assets from the source checkout (`BS_ASSET_DIR` and the engine's asset path are
|
||||||
|
baked in at configure time), so run them where they were built.
|
||||||
|
|
||||||
|
### Windows (MSYS2, recommended)
|
||||||
|
|
||||||
|
Install [MSYS2](https://www.msys2.org/) and open the **MSYS2 UCRT64** shell:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pacman -S --needed git mingw-w64-ucrt-x86_64-{gcc,cmake,ninja,pkgconf,glfw,glm,libepoxy,sqlite3,nlohmann-json}
|
||||||
|
|
||||||
|
git clone ssh://git@git.gabjimmy.com:30009/Gabe/Vibe-HD-2D.git
|
||||||
|
git clone <this repo> Brandling-Safari # side by side with Vibe-HD-2D
|
||||||
|
cd Brandling-Safari
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
ninja -C build # safari_client.exe, safari_server.exe, safari_bot.exe
|
||||||
|
./build/safari_client.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
The executables find `glfw3.dll`, `libepoxy-0.dll`, `libsqlite3-0.dll` and
|
||||||
|
`libwinpthread-1.dll` on the UCRT64 shell's `PATH`. To start the game from Explorer or a shortcut,
|
||||||
|
copy them next to the executables once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ldd build/safari_client.exe build/safari_server.exe | grep -i /ucrt64/ | awk '{print $3}' | sort -u | xargs -I{} cp {} build/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (Visual Studio + vcpkg)
|
||||||
|
|
||||||
|
In an **x64 Native Tools Command Prompt for VS 2022**:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
git clone https://github.com/microsoft/vcpkg C:\vcpkg && C:\vcpkg\bootstrap-vcpkg.bat
|
||||||
|
C:\vcpkg\vcpkg install glfw3 glm libepoxy sqlite3 nlohmann-json --triplet x64-windows
|
||||||
|
|
||||||
|
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo ^
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||||
|
cmake --build build
|
||||||
|
build\safari_client.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
vcpkg copies the DLLs next to the executables. The MinGW build is verified (zero warnings);
|
||||||
|
the MSVC build has not been compiled yet, so expect possible small fixes there.
|
||||||
|
|
||||||
|
Windows notes: the client has no console window. Your identity and a hosted game's `world.db`
|
||||||
|
and `server.log` live in `%APPDATA%\brandling-safari`. The first time a server runs, allow it
|
||||||
|
through Windows Defender Firewall on private networks so LAN players can join (UDP 27420 for the
|
||||||
|
game, 27421 for discovery). The art tools in `tools/` work on Windows with Python 3 (`pip install
|
||||||
|
requests pillow numpy`); only Company_Sprite_Gen's part-library generator (`pixellab_library.py`)
|
||||||
|
is Linux/macOS-only.
|
||||||
|
|
||||||
## Playing
|
## Playing
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -9,18 +9,13 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <csignal>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <spawn.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
extern char** environ;
|
|
||||||
|
|
||||||
namespace bs {
|
namespace bs {
|
||||||
|
|
||||||
@@ -29,15 +24,6 @@ namespace ui = hd2d::ui;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
fs::path dataDir() {
|
|
||||||
const char* xdg = std::getenv("XDG_DATA_HOME");
|
|
||||||
fs::path base = xdg && *xdg ? fs::path(xdg) : fs::path(std::getenv("HOME") ? std::getenv("HOME") : ".") / ".local/share";
|
|
||||||
fs::path d = base / "brandling-safari";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::create_directories(d, ec);
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string randomToken() {
|
std::string randomToken() {
|
||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
static const char* hex = "0123456789abcdef";
|
static const char* hex = "0123456789abcdef";
|
||||||
@@ -106,7 +92,7 @@ int keyCode(const std::string& k) {
|
|||||||
// ------------------------------------------------------------------ identity
|
// ------------------------------------------------------------------ identity
|
||||||
|
|
||||||
void App::loadIdentity() {
|
void App::loadIdentity() {
|
||||||
fs::path p = dataDir() / (opt_.profile.empty() ? "client.json" : "client-" + opt_.profile + ".json");
|
fs::path p = userDataDir() / (opt_.profile.empty() ? "client.json" : "client-" + opt_.profile + ".json");
|
||||||
std::ifstream f(p);
|
std::ifstream f(p);
|
||||||
if (f) {
|
if (f) {
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +112,7 @@ void App::loadIdentity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::saveIdentity() {
|
void App::saveIdentity() {
|
||||||
fs::path p = dataDir() / (opt_.profile.empty() ? "client.json" : "client-" + opt_.profile + ".json");
|
fs::path p = userDataDir() / (opt_.profile.empty() ? "client.json" : "client-" + opt_.profile + ".json");
|
||||||
nlohmann::json j = {{"token", token_}, {"lastHost", lastHost_}, {"lastPort", lastPort_}, {"name", savedName_}};
|
nlohmann::json j = {{"token", token_}, {"lastHost", lastHost_}, {"lastPort", lastPort_}, {"name", savedName_}};
|
||||||
std::ofstream(p) << j.dump(2);
|
std::ofstream(p) << j.dump(2);
|
||||||
}
|
}
|
||||||
@@ -283,38 +269,13 @@ bool App::scripted(const char* key) const { return pressed(keyCode(key)); }
|
|||||||
// ------------------------------------------------------------------ local server
|
// ------------------------------------------------------------------ local server
|
||||||
|
|
||||||
bool App::startLocalServer() {
|
bool App::startLocalServer() {
|
||||||
if (serverPid_ > 0) return true;
|
if (localServer_.running()) return true;
|
||||||
std::error_code ec;
|
if (!localServer_.start(opt_.port, std::string(nameBuf_) + "'s Safari", message_)) return false;
|
||||||
fs::path exe = fs::read_symlink("/proc/self/exe", ec);
|
std::this_thread::sleep_for(std::chrono::milliseconds(250)); // let it bind (the client retries anyway)
|
||||||
fs::path server = exe.parent_path() / "safari_server";
|
|
||||||
if (ec || !fs::exists(server)) {
|
|
||||||
message_ = "safari_server was not found next to the game";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::string port = std::to_string(opt_.port);
|
|
||||||
std::string name = std::string(nameBuf_) + "'s Safari";
|
|
||||||
std::vector<std::string> args = {server.string(), "--port", port, "--name", name};
|
|
||||||
std::vector<char*> argv;
|
|
||||||
for (std::string& a : args) argv.push_back(a.data());
|
|
||||||
argv.push_back(nullptr);
|
|
||||||
pid_t pid = 0;
|
|
||||||
if (posix_spawn(&pid, server.c_str(), nullptr, nullptr, argv.data(), environ) != 0) {
|
|
||||||
message_ = "could not start the local server";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
serverPid_ = int(pid);
|
|
||||||
HD2D_LOG("started local server (pid %d) on port %s", serverPid_, port.c_str());
|
|
||||||
usleep(250000); // let it bind before we connect (the client retries anyway)
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::stopLocalServer() {
|
void App::stopLocalServer() { localServer_.stop(); }
|
||||||
if (serverPid_ <= 0) return;
|
|
||||||
kill(pid_t(serverPid_), SIGTERM);
|
|
||||||
int status = 0;
|
|
||||||
for (int i = 0; i < 50 && waitpid(pid_t(serverPid_), &status, WNOHANG) == 0; ++i) usleep(100000);
|
|
||||||
serverPid_ = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ flow
|
// ------------------------------------------------------------------ flow
|
||||||
|
|
||||||
@@ -345,7 +306,7 @@ void App::handleNet() {
|
|||||||
NetClient::State st = net_.state();
|
NetClient::State st = net_.state();
|
||||||
if (st == NetClient::State::Failed && screen_ != Screen::Title && screen_ != Screen::Join) {
|
if (st == NetClient::State::Failed && screen_ != Screen::Title && screen_ != Screen::Join) {
|
||||||
// a freshly started local server may not be listening yet: retry for a few seconds
|
// a freshly started local server may not be listening yet: retry for a few seconds
|
||||||
if (screen_ == Screen::Connecting && serverPid_ > 0 && connectTimer_ < 6.0f) {
|
if (screen_ == Screen::Connecting && localServer_.running() && connectTimer_ < 6.0f) {
|
||||||
net_.connect(lastHost_, lastPort_, token_);
|
net_.connect(lastHost_, lastPort_, token_);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// creator, play) and the glue between NetClient, World and the HD-2D UI.
|
// creator, play) and the glue between NetClient, World and the HD-2D UI.
|
||||||
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
#include "LocalServer.h"
|
||||||
#include "NetClient.h"
|
#include "NetClient.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ private:
|
|||||||
std::vector<net::ServerInfo> found_;
|
std::vector<net::ServerInfo> found_;
|
||||||
float scanTimer_ = 0.0f;
|
float scanTimer_ = 0.0f;
|
||||||
char addressBuf_[128] = "127.0.0.1";
|
char addressBuf_[128] = "127.0.0.1";
|
||||||
int serverPid_ = -1;
|
LocalServer localServer_;
|
||||||
float connectTimer_ = 0.0f;
|
float connectTimer_ = 0.0f;
|
||||||
|
|
||||||
// creator
|
// creator
|
||||||
|
|||||||
195
client/LocalServer.cpp
Normal file
195
client/LocalServer.cpp
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
#include "LocalServer.h"
|
||||||
|
|
||||||
|
#include "hd2d/Log.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <spawn.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
extern char** environ;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace bs {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
fs::path userDataDir() {
|
||||||
|
fs::path base;
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (const char* a = std::getenv("APPDATA"); a && *a) base = a;
|
||||||
|
#else
|
||||||
|
if (const char* x = std::getenv("XDG_DATA_HOME"); x && *x) base = x;
|
||||||
|
else if (const char* h = std::getenv("HOME"); h && *h) base = fs::path(h) / ".local/share";
|
||||||
|
#endif
|
||||||
|
if (base.empty()) base = ".";
|
||||||
|
fs::path d = base / "brandling-safari";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(d, ec);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path executableDir() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
std::wstring buf(32768, L'\0');
|
||||||
|
DWORD n = GetModuleFileNameW(nullptr, buf.data(), DWORD(buf.size()));
|
||||||
|
buf.resize(n);
|
||||||
|
return fs::path(buf).parent_path();
|
||||||
|
#else
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path exe = fs::read_symlink("/proc/self/exe", ec);
|
||||||
|
return ec ? fs::current_path() : exe.parent_path();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::wstring quoteArg(const std::wstring& a) {
|
||||||
|
std::wstring out = L"\"";
|
||||||
|
for (wchar_t c : a) {
|
||||||
|
if (c == L'"') out += L'\\';
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
return out + L"\"";
|
||||||
|
}
|
||||||
|
std::wstring widen(const std::string& s) {
|
||||||
|
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), int(s.size()), nullptr, 0);
|
||||||
|
std::wstring w(size_t(n), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, s.data(), int(s.size()), w.data(), n);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool LocalServer::start(uint16_t port, const std::string& name, std::string& error) {
|
||||||
|
if (running()) return true;
|
||||||
|
fs::path server = executableDir() / "safari_server.exe";
|
||||||
|
if (!fs::exists(server)) {
|
||||||
|
error = "safari_server.exe was not found next to the game";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SECURITY_ATTRIBUTES sa{sizeof sa, nullptr, TRUE};
|
||||||
|
HANDLE readEnd = nullptr, writeEnd = nullptr;
|
||||||
|
if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) {
|
||||||
|
error = "could not create a pipe for the local server";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SetHandleInformation(writeEnd, HANDLE_FLAG_INHERIT, 0); // only the read end goes to the child
|
||||||
|
HANDLE log = CreateFileW((userDataDir() / "server.log").wstring().c_str(), GENERIC_WRITE, FILE_SHARE_READ, &sa,
|
||||||
|
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
|
||||||
|
STARTUPINFOW si{};
|
||||||
|
si.cb = sizeof si;
|
||||||
|
si.dwFlags = STARTF_USESTDHANDLES;
|
||||||
|
si.hStdInput = readEnd;
|
||||||
|
si.hStdOutput = log != INVALID_HANDLE_VALUE ? log : nullptr;
|
||||||
|
si.hStdError = si.hStdOutput;
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
std::wstring cmd = quoteArg(server.wstring()) + L" --port " + std::to_wstring(port) + L" --name " +
|
||||||
|
quoteArg(widen(name));
|
||||||
|
BOOL ok = CreateProcessW(server.wstring().c_str(), cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr,
|
||||||
|
nullptr, &si, &pi);
|
||||||
|
CloseHandle(readEnd);
|
||||||
|
if (log != INVALID_HANDLE_VALUE) CloseHandle(log);
|
||||||
|
if (!ok) {
|
||||||
|
CloseHandle(writeEnd);
|
||||||
|
error = "could not start the local server";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
process_ = pi.hProcess;
|
||||||
|
stdin_ = writeEnd;
|
||||||
|
HD2D_LOG("started local server (pid %lu) on port %u", pi.dwProcessId, unsigned(port));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::stop() {
|
||||||
|
if (!process_) return;
|
||||||
|
DWORD written = 0;
|
||||||
|
WriteFile(stdin_, "quit\n", 5, &written, nullptr);
|
||||||
|
CloseHandle(stdin_);
|
||||||
|
if (WaitForSingleObject(process_, 5000) != WAIT_OBJECT_0) TerminateProcess(process_, 1);
|
||||||
|
CloseHandle(process_);
|
||||||
|
process_ = stdin_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LocalServer::running() const { return process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT; }
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
bool LocalServer::start(uint16_t port, const std::string& name, std::string& error) {
|
||||||
|
if (running()) return true;
|
||||||
|
fs::path server = executableDir() / "safari_server";
|
||||||
|
if (!fs::exists(server)) {
|
||||||
|
error = "safari_server was not found next to the game";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int fds[2];
|
||||||
|
if (pipe(fds) != 0) {
|
||||||
|
error = "could not create a pipe for the local server";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fcntl(fds[1], F_SETFD, FD_CLOEXEC);
|
||||||
|
std::string log = (userDataDir() / "server.log").string();
|
||||||
|
posix_spawn_file_actions_t fa;
|
||||||
|
posix_spawn_file_actions_init(&fa);
|
||||||
|
posix_spawn_file_actions_adddup2(&fa, fds[0], 0);
|
||||||
|
posix_spawn_file_actions_addopen(&fa, 1, log.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
posix_spawn_file_actions_adddup2(&fa, 1, 2);
|
||||||
|
std::vector<std::string> args = {server.string(), "--port", std::to_string(port), "--name", name};
|
||||||
|
std::vector<char*> argv;
|
||||||
|
for (std::string& a : args) argv.push_back(a.data());
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
pid_t pid = 0;
|
||||||
|
int rc = posix_spawn(&pid, server.c_str(), &fa, nullptr, argv.data(), environ);
|
||||||
|
posix_spawn_file_actions_destroy(&fa);
|
||||||
|
close(fds[0]);
|
||||||
|
if (rc != 0) {
|
||||||
|
close(fds[1]);
|
||||||
|
error = "could not start the local server";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
signal(SIGPIPE, SIG_IGN); // writing `quit` to a server that already exited must not kill us
|
||||||
|
pid_ = int(pid);
|
||||||
|
stdin_ = fds[1];
|
||||||
|
HD2D_LOG("started local server (pid %d) on port %u", pid_, unsigned(port));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalServer::stop() {
|
||||||
|
if (pid_ <= 0) return;
|
||||||
|
if (write(stdin_, "quit\n", 5) < 0) {
|
||||||
|
}
|
||||||
|
close(stdin_);
|
||||||
|
int status = 0;
|
||||||
|
bool exited = false;
|
||||||
|
for (int i = 0; i < 50 && !exited; ++i) {
|
||||||
|
exited = waitpid(pid_t(pid_), &status, WNOHANG) != 0;
|
||||||
|
if (!exited) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
if (!exited) {
|
||||||
|
kill(pid_t(pid_), SIGKILL);
|
||||||
|
waitpid(pid_t(pid_), &status, 0);
|
||||||
|
}
|
||||||
|
pid_ = stdin_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LocalServer::running() const { return pid_ > 0 && waitpid(pid_t(pid_), nullptr, WNOHANG) == 0; }
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace bs
|
||||||
41
client/LocalServer.h
Normal file
41
client/LocalServer.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
// Runs safari_server next to the client executable for "Host & Play".
|
||||||
|
// The child's stdin is a pipe: stop() sends the console command `quit` so the
|
||||||
|
// server saves and exits cleanly on every platform (Windows has no SIGTERM),
|
||||||
|
// and only kills it if it does not exit in time. Its output goes to a log file.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace bs {
|
||||||
|
|
||||||
|
// Per-user data folder: %APPDATA%\brandling-safari on Windows,
|
||||||
|
// $XDG_DATA_HOME/brandling-safari or ~/.local/share/brandling-safari elsewhere
|
||||||
|
// (the server uses the same folder for world.db). Created if missing.
|
||||||
|
std::filesystem::path userDataDir();
|
||||||
|
std::filesystem::path executableDir();
|
||||||
|
|
||||||
|
class LocalServer {
|
||||||
|
public:
|
||||||
|
LocalServer() = default;
|
||||||
|
LocalServer(const LocalServer&) = delete;
|
||||||
|
LocalServer& operator=(const LocalServer&) = delete;
|
||||||
|
~LocalServer() { stop(); }
|
||||||
|
|
||||||
|
// Starts `safari_server --port <port> --name <name>`; false + `error` on failure.
|
||||||
|
bool start(uint16_t port, const std::string& name, std::string& error);
|
||||||
|
void stop();
|
||||||
|
bool running() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(_WIN32)
|
||||||
|
void* process_ = nullptr; // HANDLE
|
||||||
|
void* stdin_ = nullptr; // HANDLE (write end)
|
||||||
|
#else
|
||||||
|
int pid_ = -1;
|
||||||
|
int stdin_ = -1;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bs
|
||||||
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
namespace bs {
|
namespace bs {
|
||||||
|
|
||||||
#if defined(__GNUC__)
|
// MinGW builds define __USE_MINGW_ANSI_STDIO (see CMakeLists.txt) so %zu works on old msvcrt.
|
||||||
|
#if defined(__MINGW32__)
|
||||||
|
__attribute__((format(gnu_printf, 1, 2)))
|
||||||
|
#elif defined(__GNUC__)
|
||||||
__attribute__((format(printf, 1, 2)))
|
__attribute__((format(printf, 1, 2)))
|
||||||
#endif
|
#endif
|
||||||
inline void logf(const char* fmt, ...) {
|
inline void logf(const char* fmt, ...) {
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ enum class ActionKind : uint8_t {
|
|||||||
|
|
||||||
class Writer {
|
class Writer {
|
||||||
public:
|
public:
|
||||||
explicit Writer(Msg m) { u8(uint8_t(m)); }
|
explicit Writer(Msg m) {
|
||||||
|
buf_.reserve(64); // most messages fit; avoids regrowth for every field
|
||||||
|
u8(uint8_t(m));
|
||||||
|
}
|
||||||
void u8(uint8_t v) { buf_.push_back(v); }
|
void u8(uint8_t v) { buf_.push_back(v); }
|
||||||
void u16(uint16_t v) { raw(&v, 2); }
|
void u16(uint16_t v) { raw(&v, 2); }
|
||||||
void u32(uint32_t v) { raw(&v, 4); }
|
void u32(uint32_t v) { raw(&v, 4); }
|
||||||
|
|||||||
Reference in New Issue
Block a user