station/crates/game_client/src/main.rs

167 lines
5.6 KiB
Rust
Raw Normal View History

//! Game client.
//!
//! ```text
//! game singleplayer (persistent local world)
//! game --host [PORT] singleplayer world that friends can join
//! game --connect ADDR[:PORT] join a dedicated server or host
//! ```
//! Common options: `--name NAME`, `--save FILE`, `--map MAP`.
// Release builds on Windows run without a console window.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
mod input;
mod net;
mod render;
mod ui;
use std::{net::SocketAddr, path::PathBuf};
use bevy::{prelude::*, window::WindowResolution};
use bevy_replicon::prelude::*;
use bevy_replicon_renet::RepliconRenetPlugins;
use game_core::CorePlugin;
use game_server::{GameServerPlugin, NetworkServerPlugin, ServerSettings};
/// How this client runs.
#[derive(Clone, Debug)]
pub enum Mode {
/// Server logic runs in-process without networking.
Singleplayer,
/// Server logic runs in-process and accepts remote players.
Host { port: u16 },
/// Pure client connected to a remote server.
Join { addr: SocketAddr },
}
/// The local player's login name, used to find our character among replicated ones.
#[derive(Resource, Clone, Debug)]
pub struct LocalPlayer {
pub name: String,
}
#[derive(Resource, Clone, Debug)]
pub struct ClientMode(pub Mode);
fn main() -> AppExit {
let (mode, name, settings) = parse_args();
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: format!("Station - {name}"),
resolution: WindowResolution::new(1280, 800),
..default()
}),
..default()
}),
RepliconPlugins,
RepliconRenetPlugins,
CorePlugin,
));
match &mode {
Mode::Singleplayer => {
app.add_plugins(GameServerPlugin { settings });
}
Mode::Host { port } => {
app.add_plugins((
GameServerPlugin { settings },
NetworkServerPlugin { port: *port },
));
}
Mode::Join { .. } => {}
}
app.insert_resource(LocalPlayer { name })
.insert_resource(ClientMode(mode))
.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.05)))
.add_plugins((
net::NetPlugin,
render::RenderPlugin,
input::InputPlugin,
ui::UiPlugin,
))
.run()
}
fn parse_args() -> (Mode, String, ServerSettings) {
let mut mode = Mode::Singleplayer;
let mut name = std::env::var("USER")
.ok()
.filter(|n| game_server::players::valid_name(n))
.unwrap_or_else(|| "Player".to_owned());
let mut settings = ServerSettings {
save_path: Some(PathBuf::from("saves/singleplayer.rsn")),
..default()
};
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" => {
let port = match args.peek() {
Some(p) if !p.starts_with("--") => args
.next()
.unwrap()
.parse()
.unwrap_or_else(|_| usage("invalid port")),
_ => game_core::DEFAULT_PORT,
};
mode = Mode::Host { port };
}
"--connect" => {
let addr = value("an address");
let addr = if addr.contains(':') {
addr
} else {
format!("{addr}:{}", game_core::DEFAULT_PORT)
};
let addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.ok()
.and_then(|mut a| a.next())
.unwrap_or_else(|| usage(&format!("can't resolve `{addr}`")));
mode = Mode::Join { addr };
}
"--name" => name = value("a name"),
"--save" => settings.save_path = Some(value("a file").into()),
"--no-save" => settings.save_path = None,
"--map" => settings.map = value("a map path"),
"-h" | "--help" => usage(""),
other => usage(&format!("unknown argument `{other}`")),
}
}
if !game_server::players::valid_name(&name) {
usage("names may only contain letters, digits, spaces, `_` and `-` (max 24)");
}
(mode, name, settings)
}
fn usage(error: &str) -> ! {
if !error.is_empty() {
eprintln!("error: {error}\n");
}
eprintln!(
"usage: game [--host [PORT] | --connect ADDR[:PORT]] [--name NAME] [--save FILE | --no-save] [--map MAP]\n\n\
(no mode) singleplayer, world saved to saves/singleplayer.rsn\n\
--host play your world and let others join (default port {port})\n\
--connect join a server (default port {port})\n\
--name character name; characters persist per world\n\
--save world file for singleplayer/host\n\
--map Tiled map for new worlds (default {map})",
port = game_core::DEFAULT_PORT,
map = game_core::DEFAULT_MAP,
);
std::process::exit(if error.is_empty() { 0 } else { 2 })
}