use bevy::prelude::*; use net::{ ClientActionInbox, LocalPresentClient, LookState, PresentedSnapshot, look_angles_from_degrees, }; use sim::{ClientAction, ClientLifecycle, MatchPhase, SpawnPick}; use ui::UiLayer; use crate::feature_dispatch::DebugPosOverlay; use crate::{ ConsoleCommand, ConsoleDispatch, ConsoleLine, ConsoleRegistry, ConsoleSettings, ConsoleState, plugin::WaitMovePose, }; #[derive(Component)] pub(crate) struct ShowposHud; pub(crate) fn spawn_showpos_hud(commands: &mut Commands, font: Handle) { commands.spawn(( ShowposHud, UiLayer::Overlay, Visibility::Hidden, Node { position_type: PositionType::Absolute, left: px(12), top: px(12), padding: UiRect::all(px(6)), ..default() }, BackgroundColor(Color::srgba(0.02, 0.03, 0.04, 0.72)), GlobalZIndex(19_000), Text::new(""), TextFont { font: font.into(), font_size: FontSize::Px(14.0), ..default() }, TextColor(Color::srgb(0.95, 0.85, 0.40)), )); } pub(crate) fn register_debug_move_commands(registry: &mut ConsoleRegistry) { if registry.resolve("showpos").is_none() { registry.register( crate::CommandSpec::new("showpos") .alias("debug_pos") .usage("showpos [on|off] — origin overlay (bare also prints once)"), ); } if registry.resolve("move").is_none() { registry.register( crate::CommandSpec::new("move") .alias("tp") .usage("move [yaw] [pitch] — write player origin (needs cheats)"), ); } if registry.resolve("god").is_none() { registry.register( crate::CommandSpec::new("god").usage("god — toggle invincibility (needs cheats)"), ); } if registry.resolve("kill").is_none() { registry .register(crate::CommandSpec::new("kill").usage( "kill — ForceDeath the local player (needs cheats; invented, not COMMANDS)", )); } if registry.resolve("force_spawn").is_none() { registry.register(crate::CommandSpec::new("force_spawn").usage( "force_spawn [random | at [yaw]] — respawn from any state through spawn resolution (needs cheats)", )); } if registry.resolve("damage").is_none() { registry.register( crate::CommandSpec::new("damage").usage( "damage [amount] — subtract Alive health (default 40; needs cheats; invented)", ), ); } if registry.resolve("look").is_none() { registry.register( crate::CommandSpec::new("look") .usage("look — write LookState + authority angles (needs cheats)"), ); } if registry.resolve("name").is_none() { registry.register( crate::CommandSpec::new("name") .usage("name [string] — write clientState.name[16] for the local client"), ); } if registry.resolve("nudge").is_none() { registry.register(crate::CommandSpec::new("nudge").usage( "nudge — authority-only origin shift, no teleport bit (needs cheats)", )); } if registry.resolve("force_match_start").is_none() { registry.register(crate::CommandSpec::new("force_match_start").usage( "force_match_start — skip waitForPlayers and matchStartTimer (needs cheats; invented)", )); } } pub(crate) fn route_debug_move_commands( mut events: MessageReader, mut console: ResMut, settings: Res, mut line: ResMut, mut debug_pos: ResMut, presented: Res, local: Res, mut authority: Option>, mut inbox: Option>, mut seq: ResMut, mut look: ResMut, mut dispatch: ResMut, ) { let capacity = settings.log_capacity; let echo = |msg: String, console: &mut ConsoleState, line: &mut ConsoleLine| { diag::info!(Console, "{msg}"); line.0 = msg.clone(); console.echo(msg, capacity); }; for cmd in events.read() { match cmd.name.as_str() { "showpos" | "debug_pos" => match cmd.args.first().map(String::as_str) { None => { debug_pos.0 = true; echo(format_showpos(&presented, local.0), &mut console, &mut line); } Some("on") | Some("1") => { debug_pos.0 = true; echo("showpos on".into(), &mut console, &mut line); } Some("off") | Some("0") => { debug_pos.0 = false; echo("showpos off".into(), &mut console, &mut line); } Some(other) => echo( format!("usage: showpos [on|off] (got `{other}`)"), &mut console, &mut line, ), }, "move" | "tp" => match parse_move(&cmd.args, &presented, local.0) { Err(msg) => echo(msg, &mut console, &mut line), Ok((origin, angles)) => { if !alive(&presented, local.0) { echo( "move: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("move: cheats are off".into(), &mut console, &mut line); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo( "move: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push( local.0, ClientAction::Move { request_id, origin, angles, }, ) { echo(format!("move: {error}"), &mut console, &mut line); continue; } look.angles = look_angles_from_degrees(angles); arm_wait_move( &mut dispatch, cmd.background, local.0, origin, angles, "move", ); echo( format!( "move: queued ({:.1} {:.1} {:.1}) yaw={:.0} pitch={:.0} request_id={request_id}{}", origin[0], origin[1], origin[2], angles[1], angles[0], if cmd.background { " (async)" } else { " (sync until presented pose)" } ), &mut console, &mut line, ); } }, "look" => match parse_look(&cmd.args, &presented, local.0) { Err(msg) => echo(msg, &mut console, &mut line), Ok((origin, angles)) => { if !alive(&presented, local.0) { echo( "look: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("look: cheats are off".into(), &mut console, &mut line); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo( "look: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push( local.0, ClientAction::Move { request_id, origin, angles, }, ) { echo(format!("look: {error}"), &mut console, &mut line); continue; } look.angles = look_angles_from_degrees(angles); arm_wait_move( &mut dispatch, cmd.background, local.0, origin, angles, "look", ); echo( format!( "look: queued yaw={:.0} pitch={:.0} request_id={request_id}{}", angles[1], angles[0], if cmd.background { " (async)" } else { " (sync until presented pose)" } ), &mut console, &mut line, ); } }, "god" => { if !cmd.args.is_empty() { echo("usage: god".into(), &mut console, &mut line); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("god: cheats are off".into(), &mut console, &mut line); continue; } if !alive(&presented, local.0) { echo( "god: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo("god: no action inbox".into(), &mut console, &mut line); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push(local.0, ClientAction::ToggleGod { request_id }) { echo(format!("god: {error}"), &mut console, &mut line); continue; } echo( format!("god: queued toggle request_id={request_id}"), &mut console, &mut line, ); } "kill" => { if !alive(&presented, local.0) { echo( "kill: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("kill: cheats are off".into(), &mut console, &mut line); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo( "kill: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push(local.0, ClientAction::ForceDeath { request_id }) { echo(format!("kill: {error}"), &mut console, &mut line); continue; } echo( format!("kill: queued ForceDeath request_id={request_id}"), &mut console, &mut line, ); } "force_spawn" => { let pick = match parse_force_spawn(&cmd.args) { Ok(pick) => pick, Err(error) => { echo(format!("force_spawn: {error}"), &mut console, &mut line); continue; } }; if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo( "force_spawn: cheats are off".into(), &mut console, &mut line, ); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo( "force_spawn: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push(local.0, ClientAction::ForceSpawn { request_id, pick }) { echo(format!("force_spawn: {error}"), &mut console, &mut line); continue; } if !cmd.background { let life = authority .as_ref() .and_then(|a| a.0.client_meta(local.0)) .map(|m| m.life_sequence) .unwrap_or_default(); dispatch.wait_alive = Some((local.0, life)); dispatch.wait_alive_elapsed = 0.0; } echo( format!("force_spawn: queued {pick:?} request_id={request_id}"), &mut console, &mut line, ); } "damage" => { let amount = match cmd.args.first() { None => Ok(40), Some(raw) => raw .parse::() .map_err(|_| format!("usage: damage [amount] (got `{raw}`)")), }; let amount = match amount { Ok(n) if n > 0 => n, Ok(_) => { echo("damage: amount must be > 0".into(), &mut console, &mut line); continue; } Err(msg) => { echo(msg, &mut console, &mut line); continue; } }; if !alive(&presented, local.0) { echo( "damage: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("damage: cheats are off".into(), &mut console, &mut line); continue; } let Some(inbox) = inbox.as_deref_mut() else { echo( "damage: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push(local.0, ClientAction::DebugDamage { request_id, amount }) { echo(format!("damage: {error}"), &mut console, &mut line); continue; } echo( format!("damage: queued {amount} request_id={request_id}"), &mut console, &mut line, ); } "nudge" => { if cmd.args.len() != 3 { echo( "usage: nudge ".into(), &mut console, &mut line, ); continue; } if !alive(&presented, local.0) { echo( "nudge: not Alive — spawn a class first".into(), &mut console, &mut line, ); continue; } if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo("nudge: cheats are off".into(), &mut console, &mut line); continue; } let parse = |s: &str| { s.parse::() .ok() .filter(|v| v.is_finite()) .ok_or_else(|| format!("nudge: not a finite number `{s}`")) }; let delta = match ( parse(&cmd.args[0]), parse(&cmd.args[1]), parse(&cmd.args[2]), ) { (Ok(x), Ok(y), Ok(z)) => [x, y, z], (Err(msg), _, _) | (_, Err(msg), _) | (_, _, Err(msg)) => { echo(msg, &mut console, &mut line); continue; } }; let Some(authority) = authority.as_deref_mut() else { echo( "nudge: no authority world (not a listen host)".into(), &mut console, &mut line, ); continue; }; authority.0.gate_nudge_origin(local.0, delta); echo( format!( "nudge: authority origin += ({:.1} {:.1} {:.1})", delta[0], delta[1], delta[2] ), &mut console, &mut line, ); } "name" => match cmd.args.as_slice() { [] => { let shown = presented .snapshot() .and_then(|s| s.meta.for_client(local.0)) .and_then(|m| entity_iw4::client_state_name(&m.name).map(str::to_owned)); match shown { Some(n) => echo(format!("name is \"{n}\""), &mut console, &mut line), None => echo("name is \"\"".into(), &mut console, &mut line), } } [raw] => { let Some(inbox) = inbox.as_deref_mut() else { echo( "name: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let packed = entity_iw4::pack_client_state_name(raw); let request_id = seq.allocate(); if let Err(error) = inbox.push( local.0, ClientAction::SetName { request_id, name: packed, }, ) { echo(format!("name: {error}"), &mut console, &mut line); continue; } echo( format!("name: queued `{raw}` request_id={request_id}"), &mut console, &mut line, ); } _ => echo("usage: name [string]".into(), &mut console, &mut line), }, "force_match_start" => { if authority.as_ref().is_some_and(|a| !a.0.cheats_enabled()) { echo( "force_match_start: cheats are off".into(), &mut console, &mut line, ); continue; } let Some(world) = authority.as_ref() else { echo( "force_match_start: no authority world (not a listen host)".into(), &mut console, &mut line, ); continue; }; match world.0.phase() { MatchPhase::Playing => { echo( "force_match_start: already playing".into(), &mut console, &mut line, ); continue; } MatchPhase::Warmup => {} other => { echo( format!("force_match_start: match is not in warmup (phase={other:?})"), &mut console, &mut line, ); continue; } } let Some(inbox) = inbox.as_deref_mut() else { echo( "force_match_start: no action inbox (not a listen host)".into(), &mut console, &mut line, ); continue; }; let request_id = seq.allocate(); if let Err(error) = inbox.push( local.0, ClientAction::SetMatchPhase { request_id, phase: MatchPhase::Playing, }, ) { echo( format!("force_match_start: {error}"), &mut console, &mut line, ); continue; } if !cmd.background { dispatch.wait_playing = true; dispatch.wait_playing_elapsed = 0.0; echo( format!( "force_match_start: queued SetMatchPhase Playing request_id={request_id} (sync until Playing)" ), &mut console, &mut line, ); } else { echo( format!( "force_match_start &: queued SetMatchPhase Playing request_id={request_id} (async)" ), &mut console, &mut line, ); } } _ => {} } } } pub(crate) fn update_showpos_overlay( debug_pos: Res, presented: Res, local: Res, clock: Res, time: Res