161 lines
5.6 KiB
Rust
161 lines
5.6 KiB
Rust
//! 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 })
|
|
}
|