Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/game-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ bevy = { workspace = true, features = ["3d", "ui"] }
hex-core = { workspace = true }
match-bindings = { path = "../match-bindings" }
spacetimedb-sdk.workspace = true
worldgen = { path = "../worldgen" }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bevy = { workspace = true, features = ["x11"] }
Expand Down
87 changes: 85 additions & 2 deletions crates/game-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,46 @@ const DEFAULT_DATABASE: &str = match option_env!("OF_WEB_DATABASE") {
#[derive(Parser, Debug)]
#[command(
name = "game-client",
about = "Native V1 hex RTS client",
about = "Native hex RTS client",
disable_version_flag = true
)]
struct ClientArgs {
/// Use the local deterministic fixture instead of `SpacetimeDB`.
#[arg(long)]
offline: bool,

/// Generate a composable layered V2 map for the offline viewer.
#[arg(long, requires = "offline")]
worldgen_v2: bool,

/// Width of the generated V2 viewer map (default: 256).
#[arg(
long,
requires = "worldgen_v2",
value_parser = clap::value_parser!(u32).range(24..)
)]
map_width: Option<u32>,

/// Height of the generated V2 viewer map (default: 256).
#[arg(
long,
requires = "worldgen_v2",
value_parser = clap::value_parser!(u32).range(24..)
)]
map_height: Option<u32>,

/// Seed for the generated V2 viewer map (default: 42).
#[arg(long, requires = "worldgen_v2")]
map_seed: Option<u64>,

/// Player spawn regions generated on the V2 viewer map (default: 2).
#[arg(
long,
requires = "worldgen_v2",
value_parser = clap::value_parser!(u16).range(2..=500)
)]
map_players: Option<u16>,

/// `SpacetimeDB` host URI (env: `OF_HOST`).
#[arg(long)]
host: Option<String>,
Expand All @@ -58,9 +90,18 @@ struct ClientArgs {
auto_join: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LayeredWorldOptions {
pub width: u32,
pub height: u32,
pub seed: u64,
pub players: u16,
}

#[derive(Resource, Clone, Debug)]
pub struct ClientConfig {
pub offline: bool,
pub layered_world: Option<LayeredWorldOptions>,
pub host: String,
pub database: String,
pub preferred_player: u16,
Expand All @@ -74,6 +115,11 @@ impl ClientConfig {
#[cfg(not(target_arch = "wasm32"))]
pub fn from_process() -> Self {
let args = ClientArgs::parse();
Self::from_args(args)
}

#[cfg(not(target_arch = "wasm32"))]
fn from_args(args: ClientArgs) -> Self {
let explicit_player = args.player.is_some() || env_nonempty("OF_PLAYER").is_some();
let auto_join = args.auto_join || env_flag("OF_AUTO_JOIN") || explicit_player;
let preferred_player = args
Expand Down Expand Up @@ -101,6 +147,12 @@ impl ClientConfig {
});
Self {
offline: args.offline || env_flag("OF_OFFLINE"),
layered_world: args.worldgen_v2.then(|| LayeredWorldOptions {
width: args.map_width.unwrap_or(256),
height: args.map_height.unwrap_or(256),
seed: args.map_seed.unwrap_or(42),
players: args.map_players.unwrap_or(2),
}),
host: args
.host
.or_else(|| env_nonempty("OF_HOST"))
Expand Down Expand Up @@ -133,6 +185,7 @@ impl ClientConfig {

Self {
offline: browser_flag("offline"),
layered_world: None,
host: browser_param("host").unwrap_or_else(|| DEFAULT_HOST.to_owned()),
database: browser_param("database")
.or_else(|| browser_param("db"))
Expand Down Expand Up @@ -164,7 +217,13 @@ impl ClientConfig {
}

pub const fn mode_label(&self) -> &'static str {
if self.offline { "Offline" } else { "Online" }
if self.layered_world.is_some() {
"Offline V2"
} else if self.offline {
"Offline"
} else {
"Online"
}
}
}

Expand Down Expand Up @@ -230,4 +289,28 @@ mod tests {
assert_eq!(safe_profile("../../token"), None);
assert_eq!(safe_profile(""), None);
}

#[test]
fn layered_viewer_arguments_have_safe_defaults() {
let args = ClientArgs::try_parse_from(["game-client", "--offline", "--worldgen-v2"])
.expect("layered viewer arguments");
let config = ClientConfig::from_args(args);
assert_eq!(
config.layered_world,
Some(LayeredWorldOptions {
width: 256,
height: 256,
seed: 42,
players: 2,
})
);
assert_eq!(config.mode_label(), "Offline V2");
}

#[test]
fn layered_viewer_requires_offline_mode() {
let error = ClientArgs::try_parse_from(["game-client", "--worldgen-v2"])
.expect_err("online layered generation must be rejected");
assert!(error.to_string().contains("--offline"));
}
}
7 changes: 6 additions & 1 deletion crates/game-client/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,8 @@ mod tests {
CellView {
coordinate,
terrain: TerrainKind::Plains,
river: false,
lake: false,
elevation,
owner,
civilians: 0,
Expand Down Expand Up @@ -2797,7 +2799,10 @@ mod tests {
input
}

fn order_input_app(view: MatchView, interaction: InteractionState) -> App {
fn order_input_app(mut view: MatchView, interaction: InteractionState) -> App {
// Order-input tests build from MatchView::connecting (Lobby); the
// production gate requires Running before accepting map input.
view.phase = crate::model::MatchPhase::Running;
let mut app = App::new();
app.add_message::<UiAction>()
.add_message::<ClientIntent>()
Expand Down
34 changes: 32 additions & 2 deletions crates/game-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,50 @@ use overlays::OverlayPlugin;
use performance::PerformanceOverlayPlugin;
use population_outline::PopulationOutlinePlugin;
use terrain::{spawn_terrain, sync_terrain_chunks};
use worldgen::v2::{WorldSpec, generate as generate_v2};

fn main() {
let config = ClientConfig::from_process();
let match_view = if config.offline {
let match_view = if let Some(options) = &config.layered_world {
eprintln!(
"Generating layered V2 viewer map {}x{} · {} players · seed {}…",
options.width, options.height, options.players, options.seed
);
let mut spec = WorldSpec::new(
format!("viewer-v2-{}x{}", options.width, options.height),
options.width,
options.height,
options.seed,
);
spec.player_count = options.players;
let world = generate_v2(&spec).unwrap_or_else(|error| {
eprintln!("failed to generate layered V2 viewer map: {error}");
std::process::exit(2);
});
eprintln!(
"Generated {:016x} · {} land · {} lake · {} river cells",
world.manifest.content_hash,
world.manifest.land_cells,
world.manifest.lake_cells,
world.manifest.river_cells,
);
MatchView::offline_layered_world(&world, config.preferred_player)
} else if config.offline {
MatchView::offline_fixture()
} else {
MatchView::connecting(config.preferred_player)
};
let window_title = if config.layered_world.is_some() {
"Hex RTS · Layered V2 Viewer".to_owned()
} else {
format!("Hex RTS · V1 {}", config.mode_label())
};
let mut app = App::new();
app.insert_resource(config.clone())
.insert_resource(match_view)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: format!("Hex RTS · V1 {}", config.mode_label()),
title: window_title,
resolution: WindowResolution::new(1440, 900),
resizable: true,
canvas: Some("#game-canvas".to_owned()),
Expand Down
2 changes: 2 additions & 0 deletions crates/game-client/src/map_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ mod tests {
CellView {
coordinate: Axial::ZERO,
terrain: TerrainKind::Plains,
river: false,
lake: false,
elevation: 0,
owner: Some(1),
civilians,
Expand Down
Loading
Loading