station/crates/game_client/src/main.rs

161 lines
5.6 KiB
Rust
Raw Normal View History

//! Game client.
//!
//! ```text
//! game start menu
//! game --save NAME play a local world directly
//! game --host [PORT] play a local world that friends can join
//! game --connect ADDR[:PORT] join a dedicated server or host
//! ```
//! Common options: `--name NAME`, `--password PASSWORD`, `--map MAP`, `--no-save`.
// Release builds on Windows run without a console window.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
mod hud;
mod input;
mod machine;
mod menu;
mod profile;
mod render;
mod saves;
mod session;
mod ui;
use bevy::{prelude::*, window::WindowResolution};
use bevy_replicon::prelude::*;
use bevy_replicon_renet::RepliconRenetPlugins;
use game_core::CorePlugin;
use crate::{
profile::Profile,
session::{AppState, SessionRequest, SessionTarget},
};
fn main() -> AppExit {
let mut profile = Profile::load();
let (request, map) = parse_args(&mut profile);
let mut app = App::new();
let assets = game_core::assets_dir();
app.add_plugins((
DefaultPlugins
.set(AssetPlugin {
file_path: assets.to_string_lossy().into_owned(),
..default()
})
.set(ImagePlugin::default_nearest())
.set(WindowPlugin {
primary_window: Some(Window {
title: "Station".to_owned(),
resolution: WindowResolution::new(1280, 800),
..default()
}),
..default()
}),
RepliconPlugins,
RepliconRenetPlugins,
CorePlugin,
))
.insert_resource(profile)
.insert_resource(menu::StartMap(map))
.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.05)))
.add_plugins((
ui::UiFrameworkPlugin,
session::SessionPlugin,
menu::MenuPlugin,
hud::HudPlugin,
machine::MachinePlugin,
render::RenderPlugin,
input::InputPlugin,
));
if let Some(request) = request {
app.insert_resource(request);
app.world_mut()
.resource_mut::<NextState<AppState>>()
.set(AppState::Connecting);
}
app.run()
}
/// Returns the session to start right away (skipping the menu), if any, and the map for new worlds.
fn parse_args(profile: &mut Profile) -> (Option<SessionRequest>, String) {
let mut host = None;
let mut connect = None;
let mut save = None;
let mut no_save = false;
let mut password = String::new();
let mut map = game_core::DEFAULT_MAP.to_owned();
let mut args = std::env::args().skip(1).peekable();
while let Some(arg) = args.next() {
let mut value = |what: &str| {
args.next()
.unwrap_or_else(|| usage(&format!("{arg} needs {what}")))
};
match arg.as_str() {
"--host" => {
host = Some(match args.peek() {
Some(p) if !p.starts_with("--") => args
.next()
.unwrap()
.parse()
.unwrap_or_else(|_| usage("invalid port")),
_ => game_core::DEFAULT_PORT,
});
}
"--connect" => connect = Some(value("an address")),
"--name" => profile.name = value("a name"),
"--password" => password = value("a password"),
"--save" => save = Some(value("a save name")),
"--no-save" => no_save = true,
"--map" => map = value("a map path"),
"-h" | "--help" => usage(""),
other => usage(&format!("unknown argument `{other}`")),
}
}
if !game_server::players::valid_name(&profile.name) {
usage("names may only contain letters, digits, spaces, `_` and `-` (max 24)");
}
let target = match (connect, host) {
(Some(_), Some(_)) => usage("--connect and --host can't be combined"),
(Some(address), None) => {
if save.is_some() || no_save {
usage("--save and --no-save only apply to local worlds");
}
SessionTarget::Remote { address }
}
(None, host) if host.is_some() || save.is_some() || no_save => SessionTarget::Local {
storage: (!no_save).then(|| saves::storage(save.as_deref().unwrap_or("singleplayer"))),
map: map.clone(),
host: host.map(|port| (port, password.clone())),
},
_ => return (None, map),
};
let request = SessionRequest {
target,
name: profile.name.clone(),
password,
};
(Some(request), map)
}
fn usage(error: &str) -> ! {
if !error.is_empty() {
eprintln!("error: {error}\n");
}
eprintln!(
"usage: game [--save NAME | --no-save] [--host [PORT]] [--connect ADDR[:PORT]]\n \
[--name NAME] [--password PASSWORD] [--map MAP]\n\n\
(no options) show the start menu\n\
--save play the local world NAME (created if missing), stored in {saves}\n\
--no-save play a local world that is not saved\n\
--host let others join your local world (default port {port}, password from --password)\n\
--connect join a server (default port {port})\n\
--name character name; characters persist per world\n\
--password server password (joining) or password for others (hosting)\n\
--map Tiled map for new worlds (default {map})",
port = game_core::DEFAULT_PORT,
map = game_core::DEFAULT_MAP,
saves = saves::saves_dir().display(),
);
std::process::exit(if error.is_empty() { 0 } else { 2 })
}