catch_unwind вокруг запуска клиента: аддон или драйвер не должны ронять процесс через FFI-барьер (правило AGENTS.md №2).
236 lines
7.9 KiB
Rust
236 lines
7.9 KiB
Rust
//! Клиент 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
|
||
}
|
||
}
|
||
}
|