diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 02d6074..4bf798f 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -31,9 +31,9 @@ android:theme="@android:style/Theme.NoTitleBar.Fullscreen"> std::os::raw::c_int { diff --git a/crates/client/src/map.rs b/crates/client/src/map.rs index 403fee6..560d4f1 100644 --- a/crates/client/src/map.rs +++ b/crates/client/src/map.rs @@ -798,7 +798,7 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { if touch.duck() { move_up -= 1.0; } - let touch_jump = touch.jump(); + let touch_jump = touch.take_jump(); if player.noclip { let speed = if pressed.contains(&Keycode::LShift) { args.speed * 3.0 @@ -901,6 +901,20 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { // HUD (тень + текст, чтобы читалось на любом фоне). let (width, height) = gfx.size(); + // Android рисует поверх всего экрана, включая системные панели: + // сдвигаем HUD внутрь, иначе строки уезжают под статус-бар. + let hud_top = if cfg!(target_os = "android") { + 96.0 + } else { + 8.0 + }; + let hud_bottom = if cfg!(target_os = "android") { + // Нижняя системная панель на Android выше, чем кажется: держим + // строку над ней, иначе подсказка уходит под чёрную полосу. + height as f32 - 110.0 + } else { + height as f32 - 28.0 + }; fps = fps * 0.9 + (1.0 / dt.max(1e-4)) * 0.1; renderer.clear_text(); let mut hud = |x: f32, y: f32, scale: f32, color: [f32; 4], text: String| { @@ -909,14 +923,14 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { }; hud( 8.0, - 8.0, + hud_top, 1.5, [1.0, 1.0, 1.0, 1.0], format!("FPS {:.0}", fps), ); hud( 8.0, - 28.0, + hud_top + 20.0, 1.5, [0.85, 0.95, 1.0, 1.0], format!( @@ -935,7 +949,7 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { ); hud( 8.0, - height as f32 - 28.0, + hud_bottom, 1.5, [0.95, 0.95, 0.8, 1.0], if touch_enabled { @@ -970,7 +984,7 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { ); } if show_log { - let mut y = height as f32 - 60.0; + let mut y = hud_bottom - 32.0; for line in log.iter().rev().take(4) { hud(8.0, y, 1.2, [0.8, 1.0, 0.8, 0.95], line.clone()); y -= 16.0; diff --git a/crates/client/src/touch.rs b/crates/client/src/touch.rs index 8be68e0..dde9d0b 100644 --- a/crates/client/src/touch.rs +++ b/crates/client/src/touch.rs @@ -52,6 +52,9 @@ pub struct TouchControls { look_finger: Option, jump_finger: Option, duck_finger: Option, + /// Короткое касание кнопки прыжка могло начаться и кончиться между + /// кадрами — запоминаем его до ближайшего кадра. + jump_pending: bool, } impl TouchControls { @@ -71,6 +74,7 @@ impl TouchControls { if JUMP_BUTTON.contains(x, y) { self.jump_finger = Some(finger_id); + self.jump_pending = true; return true; } if DUCK_BUTTON.contains(x, y) { @@ -159,6 +163,7 @@ impl TouchControls { self.look_finger = None; self.jump_finger = None; self.duck_finger = None; + self.jump_pending = false; self.move_vector = (0.0, 0.0); } @@ -170,8 +175,14 @@ impl TouchControls { self.move_vector } - pub fn jump(&self) -> bool { - self.enabled && self.jump_finger.is_some() + /// Прыжок на этот кадр: кнопка удерживается или была коротко нажата. + pub fn take_jump(&mut self) -> bool { + if !self.enabled { + return false; + } + let jump = self.jump_finger.is_some() || self.jump_pending; + self.jump_pending = false; + jump } pub fn duck(&self) -> bool { @@ -206,10 +217,10 @@ mod tests { assert!(touch.finger_up(2)); assert!(touch.finger_down(3, 0.8, 0.6)); - assert!(touch.jump()); + assert!(touch.take_jump()); assert!(!touch.duck()); assert!(touch.finger_up(3)); - assert!(!touch.jump()); + assert!(!touch.take_jump()); } #[test] @@ -217,6 +228,6 @@ mod tests { let mut touch = TouchControls::new(false); assert!(!touch.finger_down(1, 0.2, 0.8)); assert_eq!(touch.movement(), (0.0, 0.0)); - assert!(!touch.jump()); + assert!(!touch.take_jump()); } } diff --git a/crates/tier/src/logging.rs b/crates/tier/src/logging.rs index 1584cbc..6d75f2b 100644 --- a/crates/tier/src/logging.rs +++ b/crates/tier/src/logging.rs @@ -1,6 +1,5 @@ //! Инициализация логирования (tracing). -use std::io::IsTerminal; use std::sync::Once; use tracing_subscriber::EnvFilter; @@ -25,13 +24,16 @@ pub fn init(verbosity: u8) { EnvFilter::new(level) }); - let ansi = std::io::stdout().is_terminal(); let builder = tracing_subscriber::fmt() .with_env_filter(filter) .with_target(true); #[cfg(not(target_os = "android"))] - let _ = builder.with_ansi(ansi).try_init(); + let _ = { + use std::io::IsTerminal; + let ansi = std::io::stdout().is_terminal(); + builder.with_ansi(ansi).try_init() + }; // На Android stdout/stderr не видны: пишем строки в logcat // (`adb logcat -s gmod-rs`), иначе диагностика запуска теряется. diff --git a/docs/ANDROID.md b/docs/ANDROID.md new file mode 100644 index 0000000..aa39f5c --- /dev/null +++ b/docs/ANDROID.md @@ -0,0 +1,61 @@ +# Android-порт клиента + +Цель — тот же клиент `gmod-rs` (SDL3 + wgpu), запускаемый как APK на Android +12+ (API 31+). Отдельного «мобильного движка» нет: карта, физика, Lua и рендер +общие с десктопом. + +## Сборка + +```bash +./android/build.sh # arm64-v8a, APK для телефона +ANDROID_ABI=x86_64 ./android/build.sh # x86_64, для эмулятора +``` + +Скрипт использует SDK/NDK, `cargo`, `aapt2`, `d8`, `zipalign`, `apksigner` +(Gradle не нужен). Результат — `target/android/gmod-rs-debug.apk` +(подписан debug-ключом `android/debug.keystore`). Подробности и команды +установки — в `android/README.md`. + +## Крейты и target cfg + +- `crates/client` теперь и библиотека (`src/lib.rs`), и бинарь (`src/main.rs`). + Бинарь вызывает `client::run(args)`, Android-сборка — `cargo build --lib` + с `crate-type = ["cdylib", "rlib"]`. +- Экспорт `SDL_main` (в `lib.rs` под `cfg(target_os = "android")`) — точка + входа для штатной Java-обвязки SDL3 (`SDLActivity`). +- `steamworks` подключён только для не-Android; команда `net` на Android + отсутствует. +- `mlua`: на десктопе LuaJIT + `vendored`, на Android — Lua 5.4 + `vendored` + (LuaJIT не собирается кросс-компилятором mlua). API GMod-скриптов сохраняется. +- `tracing` на Android пишет в logcat (`adb logcat -s gmod-rs`), а не в stdout. + +## APK + +- Java-классы SDL3 (`org.libsdl.app.*`) взяты из `sdl3-src` (zlib-лицензия) и + лежат в `android/app/src/main/java`. +- `MainActivity` наследует `SDLActivity`, фиксирует ландшафт и передаёт + нативной части аргументы `map gm_construct --assets /...`. +- Нативные библиотеки пакуются как `lib/arm64-v8a/libSDL3.so` и + `libmain.so` (внутри — `libclient.so` с `SDL_main`). +- Ассеты не входят в APK: их кладут во внешний каталог приложения + (`Android/data/org.gmodrs.client/files/gmod_server/server`), см. README. + +## Управление + +Обрабатываются `SDL_EVENT_FINGER_*`; синтетические события мыши от касаний +отключены (`SDL_TOUCH_MOUSE_EVENTS=0`): + +- левая половина экрана — аналоговый стик (forward/right идут в тот же + `Player::update`, что WASD); +- правая половина — обзор свайпом (приращение углов камеры); +- кнопки `JUMP`/`DUCK` — прыжок (с защёлкой короткого тапа) и присед; +- на Android движение сразу бегом (отдельной кнопки Shift нет). + +## Проверка + +- `cargo clippy -p client --lib --target aarch64-linux-android -- -D warnings`. +- Эмулятор API 35 x86_64: APK запускается, `gm_construct` грузится, кадры + рисуются (SwiftShader, ~11–14 fps), стик/обзор/прыжок проверены через + `adb shell input`. +- Реальное arm64-устройство не проверялось (нет устройства); при отладке — + `adb logcat -s gmod-rs SDL`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9d65c82..40bd355 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -22,7 +22,8 @@ crates/ ui — нативный UI: меню, консоль, шрифты, HUD-рамка, загрузочные экраны lua — mlua (vendored LuaJIT), sandbox, рельсы API, мосты к engine/render/studio steam — фичи steamworks-rs (тикет, UGC/workshop), отдельный steam-поток - client — бинарь `gmod-rs`: сборка всего, главный цикл, конфиг, CLI + client — бинарь `gmod-rs` и библиотека: сборка всего, главный цикл, + конфиг, CLI; на Android — cdylib с `SDL_main` (см. `ANDROID.md`) ``` ### Правило зависимостей @@ -158,6 +159,19 @@ texture streaming и VRAM-бюджет (с M9). - Авто-детект ассетов: (1) явный путь; (2) `~/gmod_server/server`; (3) Steam common. - Внутриигровые настройки — конвары (как в Source), сохраняются в `cfg/`. +### 3.10 Android + +- Тот же клиент собирается как `cdylib` и грузится штатной `SDLActivity` из + SDL3; экспорт — `SDL_main`. Сборка APK — `android/build.sh` (aapt2 + d8, + без Gradle), debug-подпись. +- Целевые ABI: `arm64-v8a` (телефон) и `x86_64` (эмулятор). `minSdk 31`, + `targetSdk 35`, фиксированный ландшафт. +- `target.'cfg(target_os = "android")'`: без `steamworks`, Lua 5.4 вместо + LuaJIT; логирование в logcat. +- Ассеты — во внешнем каталоге приложения; касания (`FingerDown/Motion/Up`) + переведены в те же `wish_forward/right`, прыжок и присед, что клавиатура. +- Детали и ограничения — `docs/ANDROID.md`, команды установки — `android/README.md`. + ## 4. Конвенции кода 1. `unwrap/expect/panic` запрещены в путях обработки внешних данных (сеть/диск/Lua).