Fry3DX 0.5: builds for Windows, Linux and Android, a phone layout, Apache 2.0

- Apache License 2.0
- Windows: cross-built with clang-cl (cmake --preset windows), a window
  program with its icon; Linux: a portable preset (SDL and the C++
  library inside); Android: android/build_apk.py makes the APK
- tools/package.py packs the downloads; a GitHub workflow builds all
  three and makes a release for a v* tag
- A phone layout (fry_touch, on by default on Android): the UI drawn
  bigger, panels when asked for, a bar of big buttons, one finger to
  turn the view, two to move and pinch, a long press for a right-click;
  files through the phone's file picker
- Settings and autosaves now in a Nexoniarz folder (copied over once)
- README rewritten, the website shows the downloads and the phone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 22:05:01 +02:00
commit f6f5e5f288
40 changed files with 1180 additions and 158 deletions

View file

@ -312,13 +312,7 @@ int runClient(Game& game) {
window.setIcon(px, w, h);
stbi_image_free(px);
}
} else if (auto data = fs::read("branding/keel_emblem.yapf"))
if (yapf_texture_t* tex = yapf_tex_load_memory(data->data(), data->size(), 0, 1, nullptr)) {
std::vector<uint8_t> rgba(size_t(tex->width) * tex->height * 4);
if (yapf_tex_to_rgba8(tex, yapf_tex_surface(tex, 0, 0, 0, 0), 0, rgba.data()) == YAPF_OK)
window.setIcon(rgba.data(), int(tex->width), int(tex->height));
yapf_tex_free(tex);
}
}
if (!g_testMode) audio::init();
ui::init();
log::info("Press ` (tilde key) to open the console");
@ -539,17 +533,16 @@ int run(Game& game, int argc, char** argv) {
crash::install(g_prefPath, args, dog);
}
// Content: the engine's own (<exe dir>/keel/), then the game's (<exe dir>/<game>/) over it,
// Content: the engine's own (<exe dir>/engine/), then the game's (<exe dir>/<game>/) over it,
// each with its packs — like Source's hl2/ under a mod.
#if defined(__ANDROID__)
const std::string exeDir = unpackAndroidContent();
if (const char* cache = SDL_GetAndroidCachePath()) setenv("TMPDIR", cache, 1); // temp files (script modules) go there
const bool firstRun = !std::filesystem::exists(g_configPath);
if (const char* cache = SDL_GetAndroidCachePath()) setenv("TMPDIR", cache, 1); // temp files go there
#else
const char* base = SDL_GetBasePath();
const std::string exeDir = base ? base : "./";
#endif
if (!fs::mountGameDir(exeDir + "keel")) log::warn("Engine folder {}keel not found; fonts and UI won't load", exeDir);
if (!fs::mountGameDir(exeDir + "engine")) log::warn("Engine folder {}engine not found; fonts and UI won't load", exeDir);
const std::string gameDir = exeDir + game.name();
if (!fs::mountGameDir(gameDir)) log::warn("Game folder {} not found; no content will load", gameDir);
@ -558,9 +551,6 @@ int run(Game& game, int argc, char** argv) {
// Like Source's config_default.cfg: default binds/settings, then the player's own.
if (!g_dedicated && fs::exists("cfg/default.cfg")) execute("exec default.cfg");
if (std::filesystem::exists(g_configPath)) execute("exec \"" + g_configPath + "\"");
#if defined(__ANDROID__)
if (firstRun) execute("preset Low"); // a phone: start light (the options change it)
#endif
if (fs::exists("cfg/autoexec.cfg")) execute("exec autoexec.cfg");
executeCommandLine(startup, 0);
g_modsDir = exeDir + "mods/" + game.name();

View file

@ -9,6 +9,7 @@
// window-space winding, so front faces are clockwise. Only the final blit to the window
// (which shows row 0 at the bottom) flips.
// Requires ARB_bindless_texture (NVIDIA, AMD); without it init fails and the engine uses Vulkan.
#include "ui/ui.h"
#include "render/backend.h"
#include "render/screenshot.h"
#include "rhi/gl/gl_loader.h"
@ -332,6 +333,7 @@ public:
void beginUi() override {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ui::applyPixelScale();
ImGui::NewFrame();
}
void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); }

View file

@ -6,6 +6,7 @@
// 3.1 lightmaps, sun with shadows (one tap), sky, fog; flat surfaces, no highlights or lights
// Neither has clustered lights, light shadows or occlusion culling. Depth is GL's -1..1 here
// (no glClipControl), so vertex shaders turn the 0..1 matrices' z into it.
#include "ui/ui.h"
#include "render/backend.h"
#include "core/log.h"
#include "platform/window.h"
@ -480,6 +481,7 @@ public:
void beginUi() override {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ui::applyPixelScale();
ImGui::NewFrame();
}
void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); }

View file

@ -1,5 +1,6 @@
// The Vulkan 1.3 backend: bindless textures (one descriptor array), buffer device addresses
// for every per-draw and per-frame buffer (passed in push constants), dynamic rendering.
#include "ui/ui.h"
#include "render/backend.h"
#include "render/screenshot.h"
#include "rhi/vulkan/vk_device.h"
@ -398,6 +399,7 @@ public:
void beginUi() override {
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL3_NewFrame();
ui::applyPixelScale();
ImGui::NewFrame();
}
void setVsync(bool on) override { device_.setVsync(on); }

View file

@ -3,6 +3,7 @@
#include "fs/vfs.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <algorithm>
#include <cstring>
@ -95,6 +96,29 @@ ImFont* font(Font f) {
float scale() { return g_scale; }
namespace {
float g_pixelScale = 1.0f;
ImU32 g_lastScaledEvent = 0;
}
void setPixelScale(float k) { g_pixelScale = std::clamp(k, 0.5f, 6.0f); }
float pixelScale() { return g_pixelScale; }
void applyPixelScale() {
const float k = g_pixelScale;
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize.x /= k, io.DisplaySize.y /= k; // (set again each frame by the platform)
io.DisplayFramebufferScale.x *= k, io.DisplayFramebufferScale.y *= k;
// The mouse's positions queued since last frame, in the screen's pixels: into the UI's.
// (Events left over for the next frame were done already: by their number.)
ImU32 last = g_lastScaledEvent;
for (ImGuiInputEvent& e : GImGui->InputEventsQueue) {
if (e.EventId <= g_lastScaledEvent) continue;
last = std::max(last, e.EventId);
if (e.Type == ImGuiInputEventType_MousePos && e.MousePos.PosX > -FLT_MAX)
e.MousePos.PosX /= k, e.MousePos.PosY /= k;
}
g_lastScaledEvent = last;
}
void beginFrame(float windowHeight) {
g_scale = std::clamp(windowHeight / 1080.0f, 0.75f, 2.5f);
ImGui::GetStyle().FontScaleMain = g_scale;

View file

@ -18,6 +18,12 @@ ImFont* font(Font f);
// Sizes in pixels; scaled with the window height so 1080p looks like Source at 1080p.
float scale();
void beginFrame(float windowHeight);
// The UI laid out `k` times smaller than the screen's pixels and drawn scaled up, everything
// alike (a phone: its pixels are tiny). 1: as it is.
void setPixelScale(float k);
float pixelScale();
// The backends call this between the platform's NewFrame and ImGui::NewFrame.
void applyPixelScale();
// The highlight colour: highlights, the logo.
inline constexpr uint8_t kAccent[3] = {227, 34, 45};