//! 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::>() .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, 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 }) }