use std::net::SocketAddr;
use std::time::Instant;
use clap::Parser;
use flatland_client_lib::{default_game_server_addr, load_client_settings};
use flatland_client_ui::{
ActiveOverlay, InputAction, UiKeyCode, UiKeyEvent, UiKeyEventKind, UiKeyModifiers,
UiMouseButton, UiPointerEvent,
};
use flatland_gfx_engine::{
blocks_map_click, blocks_map_zoom, is_map_zoom_key, menu_blocking, route_editor_active,
screen_to_world, zoom_from_input, GfxInputResult, GfxRenderer,
};
use flatland_play_loop::{
build_play_hud, movement_interval_elapsed, should_dispatch_to_net, PlayHudInput, PlayLocalState,
};
use flatland_protocol::ChatChannel;
use macroquad::prelude::*;
use crate::network::{GfxNet, NetCmd};
use crate::window_prefs::{apply_saved_position, capture_window_geometry, save_window_geometry};
#[derive(Debug, Parser)]
pub struct GfxPlayArgs {
#[arg(short, long)]
pub name: Option<String>,
#[arg(long, default_value_t = default_game_server_addr())]
pub server: SocketAddr,
#[arg(long)]
pub no_reconnect: bool,
#[arg(long)]
pub skip_asset_sync: bool,
}
pub async fn run(args: GfxPlayArgs) -> anyhow::Result<()> {
let net = GfxNet::spawn(args)?;
let keys = load_client_settings().keys;
let mut local = PlayLocalState::new(keys);
let mut renderer = GfxRenderer::init().await;
renderer.hud_view = local.hud_view;
let mut movement_sent_at = Instant::now();
let mut window_position_applied = false;
let mut last_window_save = Instant::now();
let mut last_saved_geometry = capture_window_geometry();
'play: loop {
let snapshot = net.snapshot();
let state = &snapshot.game;
let input = renderer.poll_input(local.chat.active);
let block_zoom = blocks_map_zoom(state, local.chat.active, &local.map_target);
let zoom_result = if block_zoom {
GfxInputResult::default()
} else {
zoom_from_input(&input, renderer.map_zoom)
};
renderer.apply_input_result(zoom_result);
let route_editing = route_editor_active(state);
if is_key_pressed(KeyCode::Escape) {
if renderer.show_connection_settings {
renderer.show_connection_settings = false;
} else if route_editing {
net.send(NetCmd::Esc);
} else if state.show_npc_chat
|| state.show_npc_verb_menu
|| state.show_shop_menu
|| (state.show_quest_offer
&& (state.show_npc_chat || state.npc_verb_target.is_some()))
{
net.send(NetCmd::Esc);
} else if local.map_target.active {
local.clear_nav();
net.send(NetCmd::CancelAutoNav);
} else if local.chat.active {
local.chat.close();
} else if renderer.show_help {
renderer.show_help = false;
} else if state.show_loadout_menu || state.show_rotation_editor {
let overlay = if state.show_loadout_menu {
ActiveOverlay::Loadout
} else {
ActiveOverlay::RotationEditor(state.rotation_editor.mode)
};
let esc = UiKeyEvent {
kind: UiKeyEventKind::Press,
code: UiKeyCode::Esc,
modifiers: UiKeyModifiers::default(),
};
match local.movement.apply_ui_key(esc, overlay, false) {
InputAction::Quit => {}
action => net.send(NetCmd::Action(action)),
}
local.movement.reset();
} else {
net.send(NetCmd::Esc);
local.clear_nav();
}
}
if is_key_pressed(KeyCode::F2) {
renderer.show_connection_settings = !renderer.show_connection_settings;
}
for key in input.keys {
if key.kind == UiKeyEventKind::Press && matches!(key.code, UiKeyCode::Esc) {
continue;
}
if local.chat.active {
if let Some(text) = local.chat.handle_key(key) {
let channel = if local.chat.whisper {
ChatChannel::WhisperStone
} else {
ChatChannel::Nearby
};
net.send(NetCmd::Say(channel, text));
}
continue;
}
if route_editing {
if key.kind == UiKeyEventKind::Press {
if local.map_target.active {
match key.code {
UiKeyCode::Enter => {
let (_, _, pz) = state.player_position_with_z();
net.send(NetCmd::RouteEditorWaypoint {
x: local.map_target.cursor_x,
y: local.map_target.cursor_y,
z: pz,
});
local.map_target.deactivate();
}
UiKeyCode::Char('m') | UiKeyCode::Esc => {
local.map_target.deactivate();
}
UiKeyCode::Up | UiKeyCode::Char('k') | UiKeyCode::Char('w') => {
local.map_target.nudge(
0,
1,
state.world_width_m,
state.world_height_m,
);
}
UiKeyCode::Down | UiKeyCode::Char('j') => {
local.map_target.nudge(
0,
-1,
state.world_width_m,
state.world_height_m,
);
}
UiKeyCode::Left | UiKeyCode::Char('a') => {
local.map_target.nudge(
-1,
0,
state.world_width_m,
state.world_height_m,
);
}
UiKeyCode::Right | UiKeyCode::Char('d') => {
local.map_target.nudge(
1,
0,
state.world_width_m,
state.world_height_m,
);
}
_ => {}
}
} else {
match key.code {
UiKeyCode::Char('m') => {
let (px, py, _) = state.player_position_with_z();
local.map_target.activate_at(px, py);
}
_ => {
net.send(NetCmd::MenuKey(key));
}
}
}
}
continue;
}
if menu_blocking(state) {
net.send(NetCmd::MenuKey(key));
continue;
}
if key.kind == UiKeyEventKind::Press && matches!(key.code, UiKeyCode::Char(';')) {
renderer.show_use_radius = !renderer.show_use_radius;
continue;
}
if is_map_zoom_key(&key) && !state.show_rotation_editor {
continue;
}
let overlay = if state.show_loadout_menu {
ActiveOverlay::Loadout
} else if state.show_rotation_editor {
ActiveOverlay::RotationEditor(state.rotation_editor.mode)
} else {
ActiveOverlay::None
};
match local
.movement
.apply_ui_key(key, overlay, local.map_target.active)
{
InputAction::Quit => break 'play,
InputAction::ToggleHelp => renderer.show_help = !renderer.show_help,
InputAction::CycleHudView => {
local.cycle_hud_view();
renderer.hud_view = local.hud_view;
}
InputAction::ToggleMapTarget => {
if local.map_target.active {
local.map_target.deactivate();
} else {
let (px, py, _) = state.player_position_with_z();
local.map_target.activate_at(px, py);
}
}
InputAction::ConfirmMapTarget => {
if local.map_target.active {
local.auto_nav_goal =
Some((local.map_target.cursor_x, local.map_target.cursor_y));
net.send(NetCmd::AutoNavTo {
x: local.map_target.cursor_x,
y: local.map_target.cursor_y,
});
local.map_target.deactivate();
}
}
InputAction::CancelMapTarget | InputAction::CancelAutoNav => {
local.clear_nav();
net.send(NetCmd::CancelAutoNav);
}
InputAction::StopMovement => {
local.clear_nav();
net.send(NetCmd::StopMovement);
}
InputAction::StartChat { whisper } => local.chat.open(whisper),
InputAction::ToggleSprintMode => local.movement.toggle_sprint_mode(),
InputAction::MapTargetNudge { dx, dy } => {
local
.map_target
.nudge(dx, dy, state.world_width_m, state.world_height_m);
}
other => {
if should_dispatch_to_net(other) {
net.send(NetCmd::Action(other));
}
}
}
}
for ptr in input.pointers {
match ptr {
UiPointerEvent::Move { x, y } => {
local.mouse_hover = screen_to_world(
&renderer.map_grid,
x,
y,
&state,
&renderer.world_rect,
&renderer.camera,
);
}
UiPointerEvent::Click { x, y, button } => {
if let Some((wx, wy)) = screen_to_world(
&renderer.map_grid,
x,
y,
&state,
&renderer.world_rect,
&renderer.camera,
) {
if route_editing && button == UiMouseButton::Left {
net.send(NetCmd::RouteEditorMapClick { x: wx, y: wy });
continue;
}
if blocks_map_click(state, local.chat.active, false) {
continue;
}
match button {
UiMouseButton::Left => {
local.auto_nav_goal = Some((wx, wy));
net.send(NetCmd::AutoNavTo { x: wx, y: wy });
local.map_target.deactivate();
}
UiMouseButton::Right => {
local.clear_nav();
net.send(NetCmd::CancelAutoNav);
}
UiMouseButton::Middle => {}
}
}
}
UiPointerEvent::Scroll { .. } => {}
}
}
let movement_blocked = menu_blocking(state)
|| route_editing
|| state.show_loadout_menu
|| state.show_rotation_editor
|| local.chat.active
|| local.map_target.active;
let now = Instant::now();
if !movement_blocked && movement_interval_elapsed(movement_sent_at, now) {
movement_sent_at = now;
let (f, s) = local.movement.current().components();
net.send(NetCmd::Movement {
forward: f,
strafe: s,
vertical: local.movement.vertical_axis(),
sprint: local.movement.sprinting(),
});
}
local.movement.expire_idle(local.movement.idle_timeout());
let (px, py) = state.player_position();
let (facing_f, facing_s) = if let Some((f, s)) = snapshot.presentation.auto_nav_facing_axes
{
(f, s)
} else {
let (lf, ls) = local.movement.last_move_axes();
let (cf, cs) = local.movement.current().components();
if cf.abs() > f32::EPSILON || cs.abs() > f32::EPSILON {
(cf, cs)
} else {
(lf, ls)
}
};
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
renderer.update_camera(px, py, facing_f, facing_s, now_ms);
let movement_input = local.movement.current();
let hud = build_play_hud(PlayHudInput {
movement_label: movement_input.label(),
sprint_mode: local.movement.sprint_mode(),
map_target: &local.map_target,
auto_nav_goal: local.auto_nav_goal,
mouse_hover: local.mouse_hover,
view_mode: local.hud_view,
});
let chat_line = local.chat.prompt_line();
if local.was_harvesting && !state.harvest_in_progress {
renderer.trigger_harvest_flash();
}
local.was_harvesting = state.harvest_in_progress;
renderer
.maybe_reload_sprites_for_publish_rev(state.content_rev, state.publish_rev)
.await;
renderer.tick_fx(get_frame_time());
renderer.draw_frame(state, &hud, &chat_line);
let menu_actions = renderer.take_menu_actions();
if let Some(idx) = menu_actions.keychain_select {
net.send(NetCmd::KeychainSelect(idx));
}
for click in menu_actions.route_editor {
net.send(NetCmd::RouteEditorClick(click));
}
if !window_position_applied {
apply_saved_position();
window_position_applied = true;
}
let geometry = capture_window_geometry();
if geometry.width != last_saved_geometry.width
|| geometry.height != last_saved_geometry.height
|| geometry.x != last_saved_geometry.x
|| geometry.y != last_saved_geometry.y
{
last_saved_geometry = geometry;
if last_window_save.elapsed() >= std::time::Duration::from_millis(500) {
save_window_geometry(last_saved_geometry.clone());
last_window_save = Instant::now();
}
}
next_frame().await;
}
save_window_geometry(capture_window_geometry());
Ok(())
}