Windows support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ __pycache__/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
dist/
|
||||
|
||||
@@ -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
|
||||
|
||||
17
README.md
17
README.md
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
18
cmake/mingw-w64-x86_64.cmake
Normal file
18
cmake/mingw-w64-x86_64.cmake
Normal 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)
|
||||
@@ -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
61
shared/Paths.cpp
Normal 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
21
shared/Paths.h
Normal 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
97
tools/package_windows.sh
Executable 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"
|
||||
Reference in New Issue
Block a user