gmod-rs/crates/client/src/lib.rs
Niko Marmeladkov b28d6f9f25 Android: паника не проходит через границу SDL_main
catch_unwind вокруг запуска клиента: аддон или драйвер не должны ронять
процесс через FFI-барьер (правило AGENTS.md №2).
2026-10-23 14:21:51 +03:00

236 lines
7.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Клиент Source RS: общий запуск для нативного бинаря и Android-библиотеки.
mod convars;
mod doctor;
mod host;
mod lua_cmd;
mod map;
mod menu;
#[cfg(not(target_os = "android"))]
mod net_cmd;
mod touch;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
name = "gmod-rs",
version,
about = "Source RS — клиент Garry's Mod на Rust"
)]
struct Cli {
/// Корень установки Garry's Mod (например, ~/gmod_server/server)
#[arg(long, global = true, value_name = "PATH")]
assets: Option<PathBuf>,
/// Подробность логов: -v — debug, -vv — trace
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Главное меню: выбор карты (действие по умолчанию)
Run(menu::MenuArgs),
/// Меню выбора карты
Menu(menu::MenuArgs),
/// Загрузить карту и отрисовать её (полёт камеры)
Map(Box<map::MapArgs>),
/// Сетевые операции (A2S, подключение)
#[cfg(not(target_os = "android"))]
Net {
#[command(subcommand)]
cmd: NetCmd,
},
/// Операции с игровой файловой системой
Fs {
#[command(subcommand)]
cmd: FsCmd,
},
/// Lua-скрипты (GMod-подобный рантайм)
Lua {
#[command(subcommand)]
cmd: LuaCmd,
},
}
#[derive(Debug, Subcommand)]
enum LuaCmd {
/// Выполнить Lua-файл
Run(lua_cmd::RunArgs),
/// Показать autorun-скрипты из ассетов
List,
}
#[cfg(not(target_os = "android"))]
#[derive(Debug, Subcommand)]
enum NetCmd {
/// A2S-информация о сервере
Info {
/// Адрес сервера (`host:port`)
addr: String,
},
/// Тестовое подключение к серверу (хендшейк + события)
Connect(net_cmd::ConnectArgs),
}
#[derive(Debug, Subcommand)]
enum FsCmd {
/// Найти ассеты, разобрать VPK, проверить выборку CRC
Doctor,
/// Показать файлы, найденные в смонтированных VPK
List {
/// Подстрока пути (регистронезависимо); пусто — все
pattern: Option<String>,
/// Максимум строк вывода
#[arg(long, default_value_t = 100)]
limit: usize,
},
/// Определить формат файла (BSP/VTF/VMT/...) и показать сводку
Inspect {
/// Путь на диске или внутри игры (например, materials/brick/brickfloor001a.vtf)
path: String,
},
/// Вывести файл из смонтированных путей
Cat {
/// Путь внутри игры, например models/props_c17/oildrum001.mdl
path: String,
/// Не проверять CRC
#[arg(long)]
unverified: bool,
/// Сохранить в файл вместо вывода
#[arg(long, value_name = "PATH")]
out: Option<PathBuf>,
},
}
/// Точка входа: разбирает аргументы и выполняет команду.
pub fn run<I, T>(args: I) -> anyhow::Result<()>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = Cli::parse_from(args);
tier::logging::init(cli.verbose);
convars::register_all();
match cli
.command
.unwrap_or(Command::Run(menu::MenuArgs::default()))
{
Command::Run(args) | Command::Menu(args) => menu::menu(cli.assets, args),
Command::Map(args) => map::map(cli.assets, *args),
#[cfg(not(target_os = "android"))]
Command::Net { cmd } => match cmd {
NetCmd::Info { addr } => net_cmd::info(&addr),
NetCmd::Connect(args) => net_cmd::connect(args),
},
Command::Lua { cmd } => match cmd {
LuaCmd::Run(args) => lua_cmd::run(cli.assets, args),
LuaCmd::List => lua_cmd::list(cli.assets),
},
Command::Fs { cmd } => match cmd {
FsCmd::Doctor => doctor::doctor(cli.assets),
FsCmd::List { pattern, limit } => doctor::list(cli.assets, pattern, limit),
FsCmd::Inspect { path } => doctor::inspect(cli.assets, &path),
FsCmd::Cat {
path,
unverified,
out,
} => doctor::cat(cli.assets, &path, unverified, out),
},
}
}
/// Каталог ассетов по умолчанию на Android: внешний каталог приложения.
///
/// Пакет задан в `android/AndroidManifest.xml`; при смене пакета путь нужно
/// поправить (или задать `GMOD_RS_ASSETS`).
#[cfg(target_os = "android")]
pub const ANDROID_DEFAULT_ASSETS: &str =
"/sdcard/Android/data/org.gmodrs.client/files/gmod_server/server";
/// Точка входа SDL на Android.
///
/// Java-обвязка SDL (`SDLActivity`) загружает `libSDL3.so` и `libmain.so` и
/// вызывает этот символ. По умолчанию открываем `gm_construct` с ассетами из
/// внешнего каталога приложения; аргументы можно передать из Java.
///
/// # Safety
///
/// Вызывается только из SDL: `argv` — массив из `argc` валидных
/// NUL-терминированных строк.
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn SDL_main(
argc: std::os::raw::c_int,
argv: *mut *mut std::os::raw::c_char,
) -> std::os::raw::c_int {
let mut args: Vec<std::ffi::OsString> = Vec::new();
for index in 0..argc.max(0) as usize {
// SAFETY: SDL передаёт массив argc указателей на NUL-терминированные
// строки, живущие всё время вызова.
let Some(ptr) = (unsafe { argv.add(index).as_ref() }) else {
continue;
};
let Some(ptr) = std::ptr::NonNull::new(*ptr) else {
continue;
};
// SAFETY: указатель из argv SDL; проверка на NULL выше.
let text = unsafe { std::ffi::CStr::from_ptr(ptr.as_ptr()) };
args.push(std::ffi::OsString::from(
text.to_string_lossy().into_owned(),
));
}
let assets = std::env::var_os("GMOD_RS_ASSETS")
.map(PathBuf::from)
.filter(|p| p.is_dir())
.or_else(|| {
let default = PathBuf::from(ANDROID_DEFAULT_ASSETS);
default.is_dir().then_some(default)
});
if args.len() <= 1 {
args = vec!["gmod-rs".into(), "map".into(), "gm_construct".into()];
if let Some(assets) = assets {
args.push("--assets".into());
args.push(assets.into_os_string());
}
}
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run(args))) {
Ok(Ok(())) => 0,
Ok(Err(err)) => {
tracing::error!("{err:#}");
1
}
Err(payload) => {
// Паника не должна проходить через FFI-границу SDL: ловим её и
// отдаём код ошибки, как того требует правило проекта.
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("неизвестная паника");
tracing::error!("паника в SDL_main: {message}");
2
}
}
}