Windows support

This commit is contained in:
Gabrieal Jimmy
2026-10-02 16:04:30 -05:00
parent 34fa21139f
commit d31689cf86
14 changed files with 242 additions and 57 deletions

1
.gitignore vendored
View File

@@ -34,3 +34,4 @@ __pycache__/
*.swp
*~
.DS_Store
dist/

View File

@@ -36,7 +36,7 @@ target_include_directories(enet PUBLIC ${enet_SOURCE_DIR}/include)
# --- Shared game core (no graphics) ---------------------------------------------
add_library(bs_shared STATIC
shared/Appearance.cpp shared/Net.cpp shared/Protocol.cpp shared/Roster.cpp shared/Zone.cpp)
shared/Appearance.cpp shared/Net.cpp shared/Paths.cpp shared/Protocol.cpp shared/Roster.cpp shared/Zone.cpp)
target_include_directories(bs_shared PUBLIC shared)
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

View File

@@ -56,8 +56,21 @@ 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`.
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.
Development builds load assets from the source checkout (`BS_ASSET_DIR` and the engine's asset
path are baked in at configure time). A release package instead keeps them next to the
executables (`assets/` and `engine/`), which the programs check first.
### Windows release zip (cross-compiled from Linux)
```sh
sudo pacman -S mingw-w64-gcc zip # once
tools/package_windows.sh v0.1.0 # -> dist/BrandlingSafari-v0.1.0-win64.zip
```
Downloads the Windows builds of GLFW, libepoxy and SQLite from the MSYS2 repository (cached in
`~/.cache/brandling-safari`), builds in `build_win/` with `cmake/mingw-w64-x86_64.cmake`, and
zips the executables, their DLLs, the game and engine assets and a player `README.txt`.
Unzip anywhere and run `safari_client.exe`.
### Windows (MSYS2, recommended)

View File

@@ -8,6 +8,7 @@
#include "Appearance.h"
#include "Movement.h"
#include "Net.h"
#include "Paths.h"
#include "Protocol.h"
#include "Rng.h"
#include "Zone.h"
@@ -621,7 +622,7 @@ int Bot::run() {
}
rng_.reseed(hash32(token.c_str()));
std::string err;
if (!manifest_.load(std::string(BS_ASSET_DIR) + "/characters/manifest.json", &err)) {
if (!manifest_.load(gameAssetDir() + "/characters/manifest.json", &err)) {
std::fprintf(stderr, "manifest: %s\n", err.c_str());
return 1;
}

View File

@@ -135,7 +135,7 @@ bool App::init() {
if (capture_) ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouse;
if (!world_.init()) {
HD2D_ERR("failed to load game assets from %s", BS_ASSET_DIR);
HD2D_ERR("failed to load game assets from %s", gameAssetDir().c_str());
return false;
}
world_.buildZone("meadow", 1); // title backdrop; rebuilt with the server's seed on join

View File

@@ -2,10 +2,21 @@
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <vector>
namespace bs {
const std::string& engineAssetDir() {
static const std::string dir = [] {
std::filesystem::path packaged = executableDir() / "engine";
std::error_code ec;
if (std::filesystem::exists(packaged / "shaders" / "common.glsl", ec)) return packaged.string();
return std::string(HD2D_ENGINE_ASSET_DIR);
}();
return dir;
}
void UiTexture::upload(const hd2d::Image& img) {
if (id_ && size_ != glm::ivec2(img.width, img.height)) reset();
if (!id_) {

View File

@@ -2,6 +2,8 @@
// Small client-wide helpers: asset paths, UI textures, color conversion.
#include "hd2d/GL.h"
#include "Paths.h"
#include "hd2d/Texture.h"
#include <imgui.h>
@@ -11,17 +13,18 @@
#include <cstdint>
#include <string>
#ifndef BS_ASSET_DIR
#define BS_ASSET_DIR "assets"
#endif
#ifndef HD2D_ENGINE_ASSET_DIR
#define HD2D_ENGINE_ASSET_DIR "../Vibe-HD-2D/assets"
#endif
namespace bs {
inline std::string gameAsset(const std::string& rel) { return std::string(BS_ASSET_DIR) + "/" + rel; }
inline std::string engineAsset(const std::string& rel) { return std::string(HD2D_ENGINE_ASSET_DIR) + "/" + rel; }
// Game assets: `<exe dir>/assets` in a release package, else the source checkout (see Paths.h).
inline std::string gameAsset(const std::string& rel) { return gameAssetDir() + "/" + rel; }
// Engine shaders, fonts, textures, models: `<exe dir>/engine` in a release package, else the
// engine checkout baked in by CMake.
const std::string& engineAssetDir();
inline std::string engineAsset(const std::string& rel) { return engineAssetDir() + "/" + rel; }
constexpr float kPixelsPerUnit = 32.0f; // world texel density shared by all pixel art
constexpr float kCreaturePixelsPerUnit = 40.0f; // creatures render a bit smaller than trainers

View File

@@ -28,34 +28,6 @@ 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 {

View File

@@ -4,18 +4,13 @@
// 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 "Paths.h"
#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;

View File

@@ -0,0 +1,18 @@
# Cross-compile for 64-bit Windows from Linux with mingw-w64 (UCRT).
# cmake -S . -B build_win -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
# -DMINGW_SYSROOT=<dir with include/ lib/ bin/ of MSYS2 ucrt64 packages>
# tools/package_windows.sh downloads that sysroot and runs this for you.
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(MINGW_SYSROOT "" CACHE PATH "MSYS2 ucrt64 package tree (include/, lib/, bin/)")
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32 ${MINGW_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# The host's pkg-config would report Linux libraries.
set(CMAKE_DISABLE_FIND_PACKAGE_PkgConfig ON)

View File

@@ -7,6 +7,7 @@
#include "Game.h"
#include "Log.h"
#include "Net.h"
#include "Paths.h"
#include <algorithm>
#include <atomic>
@@ -35,16 +36,7 @@ std::atomic<bool> 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";
}
std::string defaultDbPath() { return (userDataDir() / "world.db").string(); }
// 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
@@ -112,8 +104,8 @@ 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";
std::string rosterPath = gameAssetDir() + "/creatures/roster.json";
std::string manifestPath = gameAssetDir() + "/characters/manifest.json";
bool seedGiven = false;
for (int i = 1; i < argc; ++i) {

61
shared/Paths.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "Paths.h"
#include <cstdlib>
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
#ifndef BS_ASSET_DIR
#define BS_ASSET_DIR "assets"
#endif
namespace bs {
namespace fs = std::filesystem;
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
}
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;
}
const std::string& gameAssetDir() {
static const std::string dir = [] {
fs::path packaged = executableDir() / "assets";
std::error_code ec;
if (fs::exists(packaged / "characters" / "manifest.json", ec)) return packaged.string();
return std::string(BS_ASSET_DIR);
}();
return dir;
}
} // namespace bs

21
shared/Paths.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
// Where things live on disk.
//
// Assets: a release package keeps them next to the executables
// (`<exe dir>/assets` for the game, `<exe dir>/engine` for the engine's shaders,
// fonts, textures and models); a development build falls back to the source
// checkout paths baked in by CMake (BS_ASSET_DIR / HD2D_ENGINE_ASSET_DIR).
#include <filesystem>
#include <string>
namespace bs {
std::filesystem::path executableDir();
// %APPDATA%\brandling-safari on Windows, $XDG_DATA_HOME/brandling-safari or
// ~/.local/share/brandling-safari elsewhere. Created if missing.
std::filesystem::path userDataDir();
// Game asset root (contains characters/manifest.json).
const std::string& gameAssetDir();
} // namespace bs

97
tools/package_windows.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Builds a self-contained Windows release zip from Linux:
# tools/package_windows.sh [version] -> dist/BrandlingSafari-<version>-win64.zip
#
# Needs: x86_64-w64-mingw32-g++ (Arch: mingw-w64-gcc), cmake, ninja, curl, zstd-capable tar, zip.
# The Windows libraries (GLFW, libepoxy, SQLite, ...) come from the MSYS2 ucrt64 repository and
# are cached in ~/.cache/brandling-safari/msys2-ucrt64. The engine checkout is ../Vibe-HD-2D
# (or $HD2D_ENGINE_DIR).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ENGINE="${HD2D_ENGINE_DIR:-$ROOT/../Vibe-HD-2D}"
VERSION="${1:-$(git -C "$ROOT" describe --tags --always --dirty 2>/dev/null || echo dev)}"
NAME="BrandlingSafari-$VERSION-win64"
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/brandling-safari/msys2-ucrt64"
SYSROOT="$CACHE/ucrt64"
BUILD="$ROOT/build_win"
STAGE="$ROOT/dist/$NAME"
REPO="https://repo.msys2.org/mingw/ucrt64"
PACKAGES=(glfw libepoxy sqlite3 nlohmann-json glm libwinpthread)
# --- 1. Windows libraries ------------------------------------------------------
mkdir -p "$CACHE"
if [[ ! -f "$CACHE/.done" ]]; then
echo "== downloading MSYS2 ucrt64 packages"
curl -fsSL "$REPO/" -o "$CACHE/index.html"
for p in "${PACKAGES[@]}"; do
f=$(grep -oE "mingw-w64-ucrt-x86_64-$p-[0-9][^\"]*-any\.pkg\.tar\.zst\"" "$CACHE/index.html" | tr -d '"' | sort -V | tail -1)
[[ -n "$f" ]] || { echo "package $p not found in $REPO" >&2; exit 1; }
echo " $f"
curl -fsSL "$REPO/$f" -o "$CACHE/$f"
tar --zstd -xf "$CACHE/$f" -C "$CACHE" 2>/dev/null
done
touch "$CACHE/.done"
fi
# --- 2. Build ------------------------------------------------------------------
echo "== building"
cmake -S "$ROOT" -B "$BUILD" -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/mingw-w64-x86_64.cmake" -DMINGW_SYSROOT="$SYSROOT" \
-DHD2D_ENGINE_DIR="$ENGINE" > /dev/null
ninja -C "$BUILD" safari_client safari_server
# --- 3. Stage --------------------------------------------------------------------
echo "== staging $STAGE"
rm -rf "$STAGE"
mkdir -p "$STAGE/assets" "$STAGE/engine/textures" "$STAGE/engine/models"
cp "$BUILD/safari_client.exe" "$BUILD/safari_server.exe" "$STAGE/"
x86_64-w64-mingw32-strip "$STAGE"/*.exe
# Every non-system DLL the executables import (one level deep is enough: the MSYS2 DLLs
# used here only import Windows system DLLs).
for dll in $(x86_64-w64-mingw32-objdump -p "$STAGE"/*.exe | awk '/DLL Name/ {print $3}' | sort -u); do
for dir in "$SYSROOT/bin" /usr/x86_64-w64-mingw32/bin; do
if [[ -f "$dir/$dll" ]]; then cp "$dir/$dll" "$STAGE/"; break; fi
done
done
# Game assets (no review sheets, no Blender texture sources: the .glb embeds them).
cp -r "$ROOT/assets/characters" "$ROOT/assets/data" "$STAGE/assets/"
mkdir -p "$STAGE/assets/creatures" "$STAGE/assets/models"
cp -r "$ROOT/assets/creatures/sprites" "$ROOT/assets/creatures/roster.json" "$STAGE/assets/creatures/"
cp "$ROOT/assets/models/safari.glb" "$STAGE/assets/models/"
# Engine assets the game uses.
cp -r "$ENGINE/assets/shaders" "$ENGINE/assets/fonts" "$STAGE/engine/"
cp "$ENGINE"/assets/textures/*.png "$STAGE/engine/textures/"
cp "$ENGINE/assets/models/scenery.glb" "$STAGE/engine/models/"
cat > "$STAGE/README.txt" <<EOF
Brandling Safari $VERSION (Windows, 64-bit)
PLAY
Double-click safari_client.exe.
- Host & Play: starts a game on this PC that friends on the same network can join.
Windows Firewall will ask about safari_server.exe the first time: allow it on
private networks (UDP ports 27420 and 27421).
- Join a LAN Game: lists games found on your network, or type the host's IP address.
Windows may show "Windows protected your PC" because the game is not code-signed:
click "More info" then "Run anyway".
CONTROLS
WASD / arrows move, Shift run, Space throw a Safari Capsule, F cast / reel a fishing line,
E talk to Professor Henry, Tab your Brandlings, Enter chat, Q/E or right-drag camera,
mouse wheel zoom, Esc menu, F12 screenshot.
DEDICATED SERVER
safari_server.exe --port 27420 --name "My Safari" (type "help" in its window)
Saves, your identity and server.log are in %APPDATA%\\brandling-safari.
Needs a GPU with OpenGL 4.5 (any GPU from the last ~10 years with current drivers).
EOF
sed -i 's/$/\r/' "$STAGE/README.txt"
# --- 4. Zip -------------------------------------------------------------------------
rm -f "$ROOT/dist/$NAME.zip"
(cd "$ROOT/dist" && zip -qr9 "$NAME.zip" "$NAME")
echo "== $(du -h "$ROOT/dist/$NAME.zip" | cut -f1) $ROOT/dist/$NAME.zip"