diff --git a/.gitignore b/.gitignore index 9e95ae6..dbb4b53 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ __pycache__/ *.swp *~ .DS_Store +dist/ diff --git a/CMakeLists.txt b/CMakeLists.txt index def67e6..8888f87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index f1edaea..b1e5554 100644 --- a/README.md +++ b/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) diff --git a/bot/main.cpp b/bot/main.cpp index 54ee2fc..29593f6 100644 --- a/bot/main.cpp +++ b/bot/main.cpp @@ -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; } diff --git a/client/App.cpp b/client/App.cpp index 4fe83a0..582a0bc 100644 --- a/client/App.cpp +++ b/client/App.cpp @@ -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 diff --git a/client/Common.cpp b/client/Common.cpp index 845b8f2..56a9ba1 100644 --- a/client/Common.cpp +++ b/client/Common.cpp @@ -2,10 +2,21 @@ #include #include +#include #include 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_) { diff --git a/client/Common.h b/client/Common.h index 9ba3243..026ba40 100644 --- a/client/Common.h +++ b/client/Common.h @@ -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 @@ -11,17 +13,18 @@ #include #include -#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: `/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: `/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 diff --git a/client/LocalServer.cpp b/client/LocalServer.cpp index 9563d54..863a123 100644 --- a/client/LocalServer.cpp +++ b/client/LocalServer.cpp @@ -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 { diff --git a/client/LocalServer.h b/client/LocalServer.h index 28f9fc4..f8362e2 100644 --- a/client/LocalServer.h +++ b/client/LocalServer.h @@ -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 -#include #include 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; diff --git a/cmake/mingw-w64-x86_64.cmake b/cmake/mingw-w64-x86_64.cmake new file mode 100644 index 0000000..a8810cb --- /dev/null +++ b/cmake/mingw-w64-x86_64.cmake @@ -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= +# 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) diff --git a/server/main.cpp b/server/main.cpp index 7b38e29..ae9ecf2 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -7,6 +7,7 @@ #include "Game.h" #include "Log.h" #include "Net.h" +#include "Paths.h" #include #include @@ -35,16 +36,7 @@ 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"; -} +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) { diff --git a/shared/Paths.cpp b/shared/Paths.cpp new file mode 100644 index 0000000..ea82107 --- /dev/null +++ b/shared/Paths.cpp @@ -0,0 +1,61 @@ +#include "Paths.h" + +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#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 diff --git a/shared/Paths.h b/shared/Paths.h new file mode 100644 index 0000000..5b61db1 --- /dev/null +++ b/shared/Paths.h @@ -0,0 +1,21 @@ +#pragma once +// Where things live on disk. +// +// Assets: a release package keeps them next to the executables +// (`/assets` for the game, `/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 +#include + +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 diff --git a/tools/package_windows.sh b/tools/package_windows.sh new file mode 100755 index 0000000..7dfc41b --- /dev/null +++ b/tools/package_windows.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Builds a self-contained Windows release zip from Linux: +# tools/package_windows.sh [version] -> dist/BrandlingSafari--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" <