diff --git a/.gitignore b/.gitignore index 9c5ef9b2..7abf489e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ **/*.rs.bk .vscode +.idea/ \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 19b5a117..d9db3485 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use bevy::{ prelude::{ apply_deferred, in_state, AddAsset, App, AssetServer, Assets, Camera, Camera3dBundle, Color, Commands, IntoSystemConfigs, IntoSystemSetConfigs, Msaa, OnEnter, OnExit, - PluginGroup, PostStartup, PostUpdate, PreUpdate, Quat, Res, ResMut, Resource, Startup, - State, SystemSet, Transform, Update, Vec3, + OnTransition, PluginGroup, PostStartup, PostUpdate, PreUpdate, Quat, Res, ResMut, Resource, + Startup, State, SystemSet, Transform, Update, Vec3, }, render::{render_resource::WgpuFeatures, settings::WgpuSettings}, transform::TransformSystem, @@ -64,10 +64,10 @@ use model_loader::ModelLoader; use render::{DamageDigitMaterial, RoseRenderPlugin}; use resources::{ load_ui_resources, run_network_thread, ui_requested_cursor_apply_system, update_ui_resources, - AppState, ClientEntityList, DamageDigitsSpawner, DebugRenderConfig, GameData, HotkeysConfig, - InterfaceConfig, NameTagCache, NetworkThread, NetworkThreadMessage, RenderConfiguration, - SelectedTarget, ServerConfiguration, SoundCache, SoundConfig, SpecularTexture, VfsResource, - WorldTime, ZoneTime, + AppState, AutoLogin, ClientEntityList, DamageDigitsSpawner, DebugRenderConfig, GameData, + HotkeysConfig, InterfaceConfig, NameTagCache, NetworkThread, NetworkThreadMessage, + RenderConfiguration, SelectedTarget, ServerConfiguration, SoundCache, SoundConfig, + SpecularTexture, VfsResource, WorldTime, ZoneTime, }; use scripting::RoseScriptingPlugin; use systems::{ @@ -81,20 +81,20 @@ use systems::{ debug_render_collider_system, debug_render_directional_light_system, debug_render_skeleton_system, directional_light_system, effect_system, facing_direction_system, free_camera_system, game_connection_system, game_mouse_input_system, game_state_enter_system, - game_zone_change_system, hit_event_system, item_drop_model_add_collider_system, - item_drop_model_system, login_connection_system, login_event_system, login_state_enter_system, - login_state_exit_system, login_system, model_viewer_enter_system, model_viewer_exit_system, - model_viewer_system, move_destination_effect_system, name_tag_system, - name_tag_update_color_system, name_tag_update_healthbar_system, name_tag_visibility_system, - network_thread_system, npc_idle_sound_system, npc_model_add_collider_system, - npc_model_update_system, orbit_camera_system, particle_sequence_system, - passive_recovery_system, pending_damage_system, pending_skill_effect_system, - personal_store_model_add_collider_system, personal_store_model_system, player_command_system, - projectile_system, quest_trigger_system, sound_trigger_system, spawn_effect_system, - spawn_projectile_system, status_effect_system, system_func_event_system, - update_position_system, use_item_event_system, vehicle_model_system, vehicle_sound_system, - visible_status_effects_system, world_connection_system, world_time_system, zone_time_system, - zone_viewer_enter_system, DebugInspectorPlugin, + game_state_exit_system, game_zone_change_system, hit_event_system, + item_drop_model_add_collider_system, item_drop_model_system, login_connection_system, + login_event_system, login_state_enter_system, login_state_exit_system, login_system, + model_viewer_enter_system, model_viewer_exit_system, model_viewer_system, + move_destination_effect_system, name_tag_system, name_tag_update_color_system, + name_tag_update_healthbar_system, name_tag_visibility_system, network_thread_system, + npc_idle_sound_system, npc_model_add_collider_system, npc_model_update_system, + orbit_camera_system, particle_sequence_system, passive_recovery_system, pending_damage_system, + pending_skill_effect_system, personal_store_model_add_collider_system, + personal_store_model_system, player_command_system, projectile_system, quest_trigger_system, + sound_trigger_system, spawn_effect_system, spawn_projectile_system, status_effect_system, + system_func_event_system, update_position_system, use_item_event_system, vehicle_model_system, + vehicle_sound_system, visible_status_effects_system, world_connection_system, + world_time_system, zone_time_system, zone_viewer_enter_system, DebugInspectorPlugin, }; use ui::{ init_window_system, load_dialog_sprites_system, ui_bank_system, ui_character_create_system, @@ -105,14 +105,14 @@ use ui::{ ui_debug_entity_inspector_system, ui_debug_item_list_system, ui_debug_menu_system, ui_debug_npc_list_system, ui_debug_physics_system, ui_debug_render_system, ui_debug_skill_list_system, ui_debug_zone_lighting_system, ui_debug_zone_list_system, - ui_debug_zone_time_system, ui_drag_and_drop_system, ui_game_menu_system, ui_hotbar_system, - ui_inventory_system, ui_item_drop_name_system, ui_login_system, ui_message_box_system, - ui_minimap_system, ui_npc_store_system, ui_number_input_dialog_system, ui_party_option_system, - ui_party_system, ui_personal_store_system, ui_player_info_system, ui_quest_list_system, - ui_respawn_system, ui_selected_target_system, ui_server_select_system, ui_settings_system, - ui_skill_list_system, ui_skill_tree_system, ui_sound_event_system, ui_status_effects_system, - ui_window_sound_system, ui_window_system, widgets::Dialog, DialogLoader, UiSoundEvent, - UiStateDebugWindows, UiStateDragAndDrop, UiStateWindows, + ui_debug_zone_time_system, ui_drag_and_drop_system, ui_exit_system, ui_game_menu_system, + ui_hotbar_system, ui_inventory_system, ui_item_drop_name_system, ui_login_system, + ui_message_box_system, ui_minimap_system, ui_npc_store_system, ui_number_input_dialog_system, + ui_party_option_system, ui_party_system, ui_personal_store_system, ui_player_info_system, + ui_quest_list_system, ui_respawn_system, ui_selected_target_system, ui_server_select_system, + ui_settings_system, ui_skill_list_system, ui_skill_tree_system, ui_sound_event_system, + ui_status_effects_system, ui_window_sound_system, ui_window_system, widgets::Dialog, + DialogLoader, UiSoundEvent, UiStateDebugWindows, UiStateDragAndDrop, UiStateWindows, }; use vfs_asset_io::VfsAssetIo; use zms_asset_loader::{ZmsAssetLoader, ZmsMaterialNumFaces, ZmsNoSkinAssetLoader}; @@ -568,10 +568,6 @@ fn run_client(config: &Config, app_state: AppState, mut systems_config: SystemsC port: format!("{}", config.server.port), preset_username: Some(config.account.username.clone()), preset_password: Some(config.account.password.clone()), - preset_server_id: config.auto_login.server_id, - preset_channel_id: config.auto_login.channel_id, - preset_character_name: config.auto_login.character_name.clone(), - auto_login: config.auto_login.enabled, }) .insert_resource(config.clone()) .add_systems(Startup, init_window_system) @@ -583,6 +579,16 @@ fn run_client(config: &Config, app_state: AppState, mut systems_config: SystemsC DebugInspectorPlugin, )); + if config.auto_login.enabled { + app.insert_resource(AutoLogin { + preset_username: Some(config.account.username.clone()), + preset_password: Some(config.account.password.clone()), + preset_server_id: config.auto_login.server_id, + preset_channel_id: config.auto_login.channel_id, + preset_character_name: config.auto_login.character_name.clone(), + }); + } + // Setup state app.add_state::() .insert_resource(State::new(app_state)); @@ -627,6 +633,7 @@ fn run_client(config: &Config, app_state: AppState, mut systems_config: SystemsC Update, (free_camera_system, orbit_camera_system).in_set(GameSystemSets::UpdateCamera), ); + app.insert_resource(NameTagCache::default()).add_systems( Update, ( @@ -708,6 +715,7 @@ fn run_client(config: &Config, app_state: AppState, mut systems_config: SystemsC ( ui_window_sound_system.before(ui_sound_event_system), ui_sound_event_system, + ui_exit_system, ) .after(UiSystemSets::UiLast), ); @@ -856,7 +864,22 @@ fn run_client(config: &Config, app_state: AppState, mut systems_config: SystemsC .init_resource::() .init_resource::(); - app.add_systems(OnEnter(AppState::Game), game_state_enter_system); + app.add_systems(OnEnter(AppState::Game), game_state_enter_system) + // Don't run exit when transitioning from model or zone viewer + .add_systems( + OnTransition { + from: AppState::Game, + to: AppState::GameLogin, + }, + game_state_exit_system, + ) + .add_systems( + OnTransition { + from: AppState::Game, + to: AppState::GameCharacterSelect, + }, + game_state_exit_system, + ); app.add_systems( Update, diff --git a/src/protocol/irose/game_client.rs b/src/protocol/irose/game_client.rs index 200c3d81..6cf9e74c 100644 --- a/src/protocol/irose/game_client.rs +++ b/src/protocol/irose/game_client.rs @@ -3,6 +3,7 @@ use num_traits::FromPrimitive; use std::net::SocketAddr; use tokio::net::TcpStream; +use crate::protocol::{ProtocolClient, ProtocolClientError}; use rose_data::{QuestTriggerHash, SkillId}; use rose_game_common::{ components::MoveMode, @@ -23,14 +24,15 @@ use rose_network_irose::{ PacketClientChangeVehiclePart, PacketClientChat, PacketClientClanCommand, PacketClientConnectRequest, PacketClientCraftItem, PacketClientDropItemFromInventory, PacketClientEmote, PacketClientIncreaseBasicStat, PacketClientJoinZone, - PacketClientLevelUpSkill, PacketClientMove, PacketClientMoveCollision, - PacketClientMoveToggle, PacketClientMoveToggleType, PacketClientNpcStoreTransaction, - PacketClientPartyReply, PacketClientPartyRequest, PacketClientPartyUpdateRules, - PacketClientPersonalStoreBuyItem, PacketClientPersonalStoreListItems, - PacketClientPickupItemDrop, PacketClientQuestRequest, PacketClientQuestRequestType, - PacketClientRepairItemUsingItem, PacketClientRepairItemUsingNpc, PacketClientReviveRequest, - PacketClientSetHotbarSlot, PacketClientSetReviveZone, PacketClientUseItem, - PacketClientWarpGateRequest, + PacketClientLevelUpSkill, PacketClientLogoutRequest, PacketClientMove, + PacketClientMoveCollision, PacketClientMoveToggle, PacketClientMoveToggleType, + PacketClientNpcStoreTransaction, PacketClientPartyReply, PacketClientPartyRequest, + PacketClientPartyUpdateRules, PacketClientPersonalStoreBuyItem, + PacketClientPersonalStoreListItems, PacketClientPickupItemDrop, PacketClientQuestRequest, + PacketClientQuestRequestType, PacketClientRepairItemUsingItem, + PacketClientRepairItemUsingNpc, PacketClientReturnToCharacterSelect, + PacketClientReviveRequest, PacketClientSetHotbarSlot, PacketClientSetReviveZone, + PacketClientUseItem, PacketClientWarpGateRequest, }, game_server_packets::{ ConnectResult, PacketConnectionReply, PacketServerAdjustPosition, PacketServerAnnounceChat, @@ -65,8 +67,6 @@ use rose_network_irose::{ ClientPacketCodec, IROSE_112_TABLE, }; -use crate::protocol::{ProtocolClient, ProtocolClientError}; - pub struct GameClient { server_address: SocketAddr, client_message_rx: tokio::sync::mpsc::UnboundedReceiver, @@ -1580,6 +1580,16 @@ impl GameClient { })) .await?; } + ClientMessage::ReturnToCharacterSelect => { + connection + .write_packet(Packet::from(&PacketClientReturnToCharacterSelect)) + .await?; + } + ClientMessage::Logout => { + connection + .write_packet(Packet::from(&PacketClientLogoutRequest)) + .await?; + } unimplemented => { log::info!("Unimplemented GameClient ClientMessage {:?}", unimplemented); } diff --git a/src/protocol/irose/world_client.rs b/src/protocol/irose/world_client.rs index fa883ab6..4b2eed7b 100644 --- a/src/protocol/irose/world_client.rs +++ b/src/protocol/irose/world_client.rs @@ -120,7 +120,11 @@ impl WorldClient { }; self.server_message_tx.send(message).ok(); } - // ServerPackets::ReturnToCharacterSelect -> ServerMessage::ReturnToCharacterSelect + Some(ServerPackets::ReturnToCharacterSelect) => { + self.server_message_tx + .send(ServerMessage::ReturnToCharacterSelect) + .ok(); + } _ => log::info!("Unhandled WorldClient packet {:?}", packet), } diff --git a/src/resources/hotkeys_config.rs b/src/resources/hotkeys_config.rs index 034ad178..f8edaa8f 100644 --- a/src/resources/hotkeys_config.rs +++ b/src/resources/hotkeys_config.rs @@ -16,6 +16,8 @@ pub struct HotkeysConfig { pub clan: KeyboardShortcut, #[serde(with = "KeyboardShortcutDef")] pub settings: KeyboardShortcut, + #[serde(with = "KeyboardShortcutDef")] + pub exit: KeyboardShortcut, #[serde(with = "KeyboardShortcutDef")] pub hotbar_1: KeyboardShortcut, @@ -44,6 +46,7 @@ impl Default for HotkeysConfig { quests: KeyboardShortcut::new(Modifiers::ALT, Key::Q), clan: KeyboardShortcut::new(Modifiers::ALT, Key::N), settings: KeyboardShortcut::new(Modifiers::ALT, Key::O), + exit: KeyboardShortcut::new(Modifiers::ALT, Key::X), hotbar_1: KeyboardShortcut::new(Modifiers::NONE, Key::F1), hotbar_2: KeyboardShortcut::new(Modifiers::NONE, Key::F2), diff --git a/src/resources/mod.rs b/src/resources/mod.rs index dbf90124..b4bda763 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -50,7 +50,7 @@ pub use name_tag_settings::NameTagSettings; pub use network_thread::{run_network_thread, NetworkThread, NetworkThreadMessage}; pub use render_configuration::RenderConfiguration; pub use selected_target::SelectedTarget; -pub use server_configuration::ServerConfiguration; +pub use server_configuration::{AutoLogin, ServerConfiguration}; pub use server_list::{ServerList, ServerListGameServer, ServerListWorldServer}; pub use sound_cache::SoundCache; pub use sound_config::SoundConfig; diff --git a/src/resources/server_configuration.rs b/src/resources/server_configuration.rs index de283068..31bb7320 100644 --- a/src/resources/server_configuration.rs +++ b/src/resources/server_configuration.rs @@ -6,8 +6,13 @@ pub struct ServerConfiguration { pub port: String, pub preset_username: Option, pub preset_password: Option, +} + +#[derive(Resource)] +pub struct AutoLogin { + pub preset_username: Option, + pub preset_password: Option, pub preset_server_id: Option, pub preset_channel_id: Option, pub preset_character_name: Option, - pub auto_login: bool, } diff --git a/src/resources/server_list.rs b/src/resources/server_list.rs index 5925c060..ab8c1068 100644 --- a/src/resources/server_list.rs +++ b/src/resources/server_list.rs @@ -13,5 +13,17 @@ pub struct ServerListWorldServer { #[derive(Resource)] pub struct ServerList { + pub selected_server: Option, + pub selected_channel: Option, pub world_servers: Vec, } + +impl From> for ServerList { + fn from(value: Vec) -> Self { + ServerList { + selected_server: None, + selected_channel: None, + world_servers: value, + } + } +} diff --git a/src/resources/ui_resources.rs b/src/resources/ui_resources.rs index aeb02863..b8da9421 100644 --- a/src/resources/ui_resources.rs +++ b/src/resources/ui_resources.rs @@ -128,6 +128,7 @@ pub struct UiResources { pub dialog_quest_list: Handle, pub dialog_respawn: Handle, pub dialog_select_server: Handle, + pub dialog_exit: Handle, pub dialog_skill_list: Handle, pub dialog_skill_tree: Handle, pub skill_tree_dealer: Handle, @@ -541,6 +542,7 @@ pub fn load_ui_resources( dialog_quest_list: dialog_files["DLGQUEST.XML"].clone(), dialog_respawn: dialog_files["DLGRESTART.XML"].clone(), dialog_select_server: dialog_files["DLGSELSVR.XML"].clone(), + dialog_exit: dialog_files["DLGSYSTEM.XML"].clone(), dialog_skill_list: dialog_files["DLGSKILL.XML"].clone(), dialog_skill_tree: dialog_files["DLGSKILLTREE.XML"].clone(), skill_tree_dealer: dialog_files["SKILLTREE_DEALER.XML"].clone(), diff --git a/src/systems/auto_login_system.rs b/src/systems/auto_login_system.rs index 23eaa2ac..107f4815 100644 --- a/src/systems/auto_login_system.rs +++ b/src/systems/auto_login_system.rs @@ -1,8 +1,8 @@ -use bevy::prelude::{EventWriter, Local, Res, State}; +use bevy::prelude::{Commands, EventWriter, Local, Res, State}; use crate::{ events::{CharacterSelectEvent, LoginEvent}, - resources::{AppState, CharacterList, ServerConfiguration, ServerList}, + resources::{AppState, AutoLogin, CharacterList, ServerList}, }; #[derive(Default)] @@ -11,47 +11,51 @@ pub enum AutoLoginState { Login, WaitServerList, WaitCharacterList, - SelectedCharacter, + Idle, } pub fn auto_login_system( + mut commands: Commands, mut auto_login_state: Local, app_state: Res>, character_list: Option>, server_list: Option>, - server_configuration: Res, + auto_login: Option>, mut login_events: EventWriter, mut character_select_events: EventWriter, ) { - if !server_configuration.auto_login { + if auto_login.is_none() { return; } match *auto_login_state { AutoLoginState::Login => { - if matches!(app_state.get(), AppState::GameLogin) { - if let (Some(username), Some(password)) = ( - &server_configuration.preset_username, - &server_configuration.preset_password, - ) { - login_events.send(LoginEvent::Login { - username: username.clone(), - password: password.clone(), - }); - *auto_login_state = AutoLoginState::WaitServerList; - } + if !matches!(app_state.get(), AppState::GameLogin) { + return; + } - if server_list.is_some() { - // If the user logged in without us, move on to next stage - *auto_login_state = AutoLoginState::WaitCharacterList; - } + if let (Some(username), Some(password)) = ( + auto_login + .as_ref() + .and_then(|it| it.preset_username.clone()), + auto_login + .as_ref() + .and_then(|it| it.preset_password.clone()), + ) { + login_events.send(LoginEvent::Login { username, password }); + *auto_login_state = AutoLoginState::WaitServerList; + } + + if server_list.is_some() { + // If the user logged in without us, move on to next stage + *auto_login_state = AutoLoginState::WaitCharacterList; } } AutoLoginState::WaitServerList => { if let Some(server_list) = server_list { - if let (&Some(server_id), &Some(channel_id)) = ( - &server_configuration.preset_server_id, - &server_configuration.preset_channel_id, + if let (Some(server_id), Some(channel_id)) = ( + auto_login.as_ref().and_then(|it| it.preset_server_id), + auto_login.as_ref().and_then(|it| it.preset_channel_id), ) { for world_server in server_list.world_servers.iter() { if world_server.id == server_id { @@ -82,23 +86,41 @@ pub fn auto_login_system( } } AutoLoginState::WaitCharacterList => { - if matches!(app_state.get(), AppState::GameCharacterSelect) { - if let Some(preset_character_name) = - server_configuration.preset_character_name.as_ref() - { - if let Some(character_list) = character_list.as_ref() { - for (i, character) in character_list.characters.iter().enumerate() { - if &character.info.name == preset_character_name { - character_select_events - .send(CharacterSelectEvent::SelectCharacter(i)); - character_select_events.send(CharacterSelectEvent::PlaySelected); - *auto_login_state = AutoLoginState::SelectedCharacter; - } - } - } + if !matches!(app_state.get(), AppState::GameCharacterSelect) { + return; + } + + let preset_character_name = match auto_login + .as_ref() + .and_then(|it| it.preset_character_name.as_ref()) + { + None => { + *auto_login_state = AutoLoginState::Idle; + return; } + Some(preset_character_name) => preset_character_name, + }; + + let character_list = match character_list.as_ref() { + None => return, + Some(character_list) => character_list, + }; + + for (i, character) in character_list.characters.iter().enumerate() { + if &character.info.name != preset_character_name { + continue; + } + + character_select_events.send(CharacterSelectEvent::SelectCharacter(i)); + character_select_events.send(CharacterSelectEvent::PlaySelected); + *auto_login_state = AutoLoginState::Idle; + } + } + AutoLoginState::Idle => { + if !matches!(app_state.get(), AppState::GameCharacterSelect) { + commands.remove_resource::(); + *auto_login_state = AutoLoginState::Login; } } - AutoLoginState::SelectedCharacter => {} } } diff --git a/src/systems/character_select_system.rs b/src/systems/character_select_system.rs index 4109e340..7bfefdbd 100644 --- a/src/systems/character_select_system.rs +++ b/src/systems/character_select_system.rs @@ -24,12 +24,28 @@ use crate::{ }, events::{CharacterSelectEvent, GameConnectionEvent, LoadZoneEvent, WorldConnectionEvent}, resources::{ - AppState, CharacterList, CharacterSelectState, GameData, NameTagCache, ServerConfiguration, + AppState, AutoLogin, CharacterList, CharacterSelectState, GameData, NameTagCache, WorldConnection, }, - systems::{FreeCamera, OrbitCamera}, + systems::{login_system::ZONE_LOGIN, FreeCamera, OrbitCamera}, }; +pub struct CharacterSelect { + initial_load: bool, + join_zone_id: Option, + last_selected_time: Option, +} + +impl Default for CharacterSelect { + fn default() -> Self { + Self { + initial_load: true, + join_zone_id: None, + last_selected_time: None, + } + } +} + #[derive(Component)] pub struct CharacterSelectCharacter { pub index: usize, @@ -44,10 +60,13 @@ pub struct CharacterSelectModelList { pub fn character_select_enter_system( mut commands: Commands, mut query_window: Query<&mut Window, With>, + mut loaded_zone: EventWriter, query_cameras: Query>, asset_server: Res, game_data: Res, + world_connection: Res, ) { + // Ensure cursor is not locked if let Ok(mut window) = query_window.get_single_mut() { window.cursor.grab_mode = CursorGrabMode::None; window.cursor.visible = true; @@ -81,10 +100,18 @@ pub fn character_select_enter_system( .id(); models.push((None, entity)); } + commands.insert_resource(CharacterSelectModelList { models, select_motion: asset_server.load("3DDATA/MOTION/AVATAR/EVENT_SELECT_M1.ZMO"), }); + + world_connection + .client_message_tx + .send(ClientMessage::GetCharacterList) + .ok(); + + loaded_zone.send(LoadZoneEvent::new(ZoneId::new(ZONE_LOGIN).unwrap())); } pub fn character_select_exit_system( @@ -162,14 +189,14 @@ pub fn character_select_system( mut game_connection_events: EventReader, mut world_connection_events: EventReader, mut load_zone_events: EventWriter, - mut join_zone_id: Local>, + mut character_select: Local, query_camera: Query< (Entity, &Camera, &GlobalTransform, Option<&CameraAnimation>), With, >, world_connection: Option>, mut character_list: Option>, - server_configuration: Res, + auto_login: Option>, asset_server: Res, ) { let character_select_state = &mut *character_select_state; @@ -260,7 +287,7 @@ pub fn character_select_system( CharacterSelectState::Entering => { let (_, _, _, camera_motion) = query_camera.single(); if camera_motion.map_or(true, |animation| animation.completed()) - || server_configuration.auto_login + || (auto_login.is_some() && character_select.initial_load) { *character_select_state = CharacterSelectState::CharacterSelect(None); } @@ -293,17 +320,21 @@ pub fn character_select_system( )); *character_select_state = CharacterSelectState::Leaving; - *join_zone_id = Some(zone_id); + character_select.join_zone_id = Some(zone_id); } } CharacterSelectState::Leaving => { let (_, _, _, camera_motion) = query_camera.single(); if camera_motion.map_or(true, |animation| animation.completed()) - || server_configuration.auto_login + || (auto_login.is_some() && character_select.initial_load) { + character_select.initial_load = false; + // Wait until camera motion complete, then load the zone! *character_select_state = CharacterSelectState::Loading; - load_zone_events.send(LoadZoneEvent::new(join_zone_id.take().unwrap())); + load_zone_events.send(LoadZoneEvent::new( + character_select.join_zone_id.take().unwrap(), + )); } } CharacterSelectState::Loading => {} @@ -311,9 +342,9 @@ pub fn character_select_system( } pub fn character_select_event_system( - mut commands: Commands, mut character_select_state: ResMut, mut character_select_events: EventReader, + mut app_state_next: ResMut>, character_list: Option>, world_connection: Option>, ) { @@ -376,7 +407,7 @@ pub fn character_select_event_system( } } CharacterSelectEvent::Disconnect => { - commands.remove_resource::(); + app_state_next.set(AppState::GameLogin); } } } @@ -388,7 +419,7 @@ pub fn character_select_input_system( mut egui_ctx: EguiContexts, mouse_button_input: Res>, rapier_context: Res, - mut last_selected_time: Local>, + mut character_select: Local, query_camera: Query<(&Camera, &GlobalTransform), With>, query_collider_parent: Query<&ColliderParent>, query_select_character: Query<&CharacterSelectCharacter>, @@ -438,7 +469,7 @@ pub fn character_select_input_system( let now = Instant::now(); if *selected_character_index == Some(select_character.index) { - if let Some(last_selected_time) = *last_selected_time { + if let Some(last_selected_time) = character_select.last_selected_time { if now - last_selected_time < Duration::from_millis(250) { character_select_events .send(CharacterSelectEvent::PlaySelected); @@ -447,7 +478,7 @@ pub fn character_select_input_system( } *selected_character_index = Some(select_character.index); - *last_selected_time = Some(now); + character_select.last_selected_time = Some(now); } } } diff --git a/src/systems/game_connection_system.rs b/src/systems/game_connection_system.rs index 67510acf..88287d73 100644 --- a/src/systems/game_connection_system.rs +++ b/src/systems/game_connection_system.rs @@ -7,7 +7,6 @@ use bevy::{ Mut, NextState, Res, ResMut, State, Transform, Visibility, World, }, }; - use rose_data::{ AbilityType, EquipmentItem, Item, ItemReference, ItemSlotBehaviour, ItemType, SkillCooldown, StatusEffectType, @@ -2281,14 +2280,12 @@ pub fn game_connection_system( log::warn!("Received unimplemented ServerMessage::RepairedItemUsingNpc"); } Ok(ServerMessage::LogoutSuccess) => { - log::warn!("Received unimplemented ServerMessage::LogoutSuccess"); + commands.remove_resource::(); + break Ok(()); } Ok(ServerMessage::LogoutFailed { .. }) => { log::warn!("Received unimplemented ServerMessage::LogoutFailed"); } - Ok(ServerMessage::ReturnToCharacterSelect) => { - log::warn!("Received unimplemented ServerMessage::ReturnToCharacterSelect"); - } Ok(ServerMessage::LoginError { .. }) | Ok(ServerMessage::LoginSuccess { .. }) | Ok(ServerMessage::ChannelList { .. }) | @@ -2303,7 +2300,8 @@ pub fn game_connection_system( Ok(ServerMessage::SelectCharacterError { .. }) | Ok(ServerMessage::DeleteCharacterStart { .. }) | Ok(ServerMessage::DeleteCharacterCancel { .. }) | - Ok(ServerMessage::DeleteCharacterError { .. }) => { + Ok(ServerMessage::DeleteCharacterError { .. }) | + Ok(ServerMessage::ReturnToCharacterSelect) => { // These should only be login / world server packets, not game server log::warn!("Received unexpected game server message"); } diff --git a/src/systems/game_system.rs b/src/systems/game_system.rs index 90ff88f9..552eaff0 100644 --- a/src/systems/game_system.rs +++ b/src/systems/game_system.rs @@ -1,6 +1,7 @@ use bevy::{ + hierarchy::DespawnRecursiveExt, math::Vec3, - prelude::{Camera3d, Commands, Entity, EventReader, Query, Res, With}, + prelude::{Camera3d, Commands, Entity, EventReader, Query, Res, ResMut, With}, }; use rose_game_common::messages::client::ClientMessage; @@ -8,7 +9,7 @@ use crate::{ animation::CameraAnimation, components::PlayerCharacter, events::ZoneEvent, - resources::GameConnection, + resources::{ClientEntityList, GameConnection}, systems::{FreeCamera, OrbitCamera}, }; @@ -32,6 +33,22 @@ pub fn game_state_enter_system( } } +pub fn game_state_exit_system( + mut commands: Commands, + mut client_entity_list: ResMut, +) { + // Despawn entities + for entity in client_entity_list.client_entities.iter() { + let entity = match entity { + Some(entity) => entity, + None => continue, + }; + + commands.entity(*entity).despawn_recursive(); + } + *client_entity_list = ClientEntityList::default(); +} + #[allow(clippy::too_many_arguments)] pub fn game_zone_change_system( mut zone_events: EventReader, diff --git a/src/systems/login_connection_system.rs b/src/systems/login_connection_system.rs index f3c4defc..6f09a475 100644 --- a/src/systems/login_connection_system.rs +++ b/src/systems/login_connection_system.rs @@ -60,7 +60,7 @@ pub fn login_connection_system( game_servers: Vec::new(), }); } - commands.insert_resource(ServerList { world_servers }); + commands.insert_resource(ServerList::from(world_servers)); } Ok(ServerMessage::LoginError { error }) => { break Err(error.into()); diff --git a/src/systems/login_system.rs b/src/systems/login_system.rs index 4284c1d1..d115398e 100644 --- a/src/systems/login_system.rs +++ b/src/systems/login_system.rs @@ -12,10 +12,14 @@ use rose_game_common::messages::client::ClientMessage; use crate::{ animation::CameraAnimation, events::{LoadZoneEvent, LoginEvent, NetworkEvent}, - resources::{Account, LoginConnection, LoginState, ServerConfiguration, ServerList}, + resources::{ + Account, LoginConnection, LoginState, ServerConfiguration, ServerList, WorldConnection, + }, systems::{FreeCamera, OrbitCamera}, }; +pub const ZONE_LOGIN: u16 = 4; + pub fn login_state_enter_system( mut commands: Commands, mut loaded_zone: EventWriter, @@ -41,10 +45,11 @@ pub fn login_state_enter_system( )); } + commands.remove_resource::(); commands.remove_resource::(); commands.insert_resource(LoginState::Input); - loaded_zone.send(LoadZoneEvent::new(ZoneId::new(4).unwrap())); + loaded_zone.send(LoadZoneEvent::new(ZoneId::new(ZONE_LOGIN).unwrap())); } pub fn login_state_exit_system(mut commands: Commands) { @@ -95,6 +100,7 @@ pub fn login_event_system( mut login_events: EventReader, login_connection: Option>, server_configuration: Res, + mut server_list: Option>, mut network_events: EventWriter, ) { for event in login_events.iter() { @@ -118,6 +124,11 @@ pub fn login_event_system( server_id, channel_id, } => { + if let Some(ref mut server_list) = server_list { + server_list.selected_server = Some(server_id); + server_list.selected_channel = Some(channel_id); + } + if let Some(login_connection) = &login_connection { login_connection .client_message_tx diff --git a/src/systems/mod.rs b/src/systems/mod.rs index 34afea39..7edf4003 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -95,7 +95,7 @@ pub use facing_direction_system::facing_direction_system; pub use free_camera_system::{free_camera_system, FreeCamera}; pub use game_connection_system::game_connection_system; pub use game_mouse_input_system::game_mouse_input_system; -pub use game_system::{game_state_enter_system, game_zone_change_system}; +pub use game_system::{game_state_enter_system, game_state_exit_system, game_zone_change_system}; pub use hit_event_system::hit_event_system; pub use item_drop_model_system::{item_drop_model_add_collider_system, item_drop_model_system}; pub use login_connection_system::login_connection_system; diff --git a/src/systems/world_connection_system.rs b/src/systems/world_connection_system.rs index c97b0972..a4c46184 100644 --- a/src/systems/world_connection_system.rs +++ b/src/systems/world_connection_system.rs @@ -1,17 +1,18 @@ use bevy::prelude::{Commands, EventWriter, NextState, Res, ResMut, State}; -use rose_game_common::messages::{client::ClientMessage, server::ServerMessage}; +use rose_game_common::messages::server::ServerMessage; use rose_network_common::ConnectionError; use crate::{ events::{NetworkEvent, WorldConnectionEvent}, - resources::{Account, AppState, CharacterList, WorldConnection}, + resources::{Account, AppState, AutoLogin, CharacterList, ServerList, WorldConnection}, }; pub fn world_connection_system( mut commands: Commands, world_connection: Option>, account: Option>, + server_list: Option>, app_state_current: Res>, mut app_state_next: ResMut>, mut network_events: EventWriter, @@ -34,10 +35,7 @@ pub fn world_connection_system( Ok(ServerMessage::ConnectionRequestSuccess { packet_sequence_id: _, }) => { - world_connection - .client_message_tx - .send(ClientMessage::GetCharacterList) - .ok(); + app_state_next.set(AppState::GameCharacterSelect); } Ok(ServerMessage::ConnectionRequestError { error: _ }) => { break Err(ConnectionError::ConnectionLost.into()); @@ -85,7 +83,20 @@ pub fn world_connection_system( Ok(ServerMessage::DeleteCharacterError { name }) => { world_connection_events.send(WorldConnectionEvent::DeleteCharacterError { name }); } - // ServerMessage::ReturnToCharacterSelect + Ok(ServerMessage::ReturnToCharacterSelect) => { + commands.remove_resource::(); + commands.insert_resource(AutoLogin { + preset_username: Some(account.username.clone()), + preset_password: Some(account.password.clone()), + preset_server_id: server_list.as_ref().and_then(|it| it.selected_server), + preset_channel_id: server_list.as_ref().and_then(|it| it.selected_channel), + preset_character_name: None, + }); + + // This is a hack, it should just work by going to GameCharacterSelect + // game_state_exit_system despawns all client entities which causes the player model to no longer be visible + app_state_next.set(AppState::GameLogin); + } Ok(message) => { log::warn!("Received unexpected world server message: {:#?}", message); } @@ -99,6 +110,6 @@ pub fn world_connection_system( if let Err(error) = result { // TODO: Store error somewhere to display to user log::warn!("World server connection error: {}", error); - commands.remove_resource::(); + app_state_next.set(AppState::GameLogin); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ed0293e3..768a4cd3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -26,6 +26,7 @@ mod ui_debug_zone_lighting_system; mod ui_debug_zone_list_system; mod ui_debug_zone_time_system; mod ui_drag_and_drop_system; +mod ui_exit_system; mod ui_game_menu_system; mod ui_hotbar_system; mod ui_inventory_system; @@ -64,6 +65,7 @@ pub struct UiStateWindows { pub menu_open: bool, pub party_open: bool, pub party_options_open: bool, + pub exit_open: bool, // Below are only opened via in game events rather than directly pub bank_open: bool, @@ -102,6 +104,7 @@ pub use ui_debug_zone_lighting_system::ui_debug_zone_lighting_system; pub use ui_debug_zone_list_system::ui_debug_zone_list_system; pub use ui_debug_zone_time_system::ui_debug_zone_time_system; pub use ui_drag_and_drop_system::{ui_drag_and_drop_system, UiStateDragAndDrop}; +pub use ui_exit_system::ui_exit_system; pub use ui_game_menu_system::ui_game_menu_system; pub use ui_hotbar_system::ui_hotbar_system; pub use ui_inventory_system::ui_inventory_system; diff --git a/src/ui/ui_exit_system.rs b/src/ui/ui_exit_system.rs new file mode 100644 index 00000000..71264ee1 --- /dev/null +++ b/src/ui/ui_exit_system.rs @@ -0,0 +1,129 @@ +use crate::{ + resources::{GameConnection, UiResources}, + ui::{ + widgets::{DataBindings, Dialog}, + UiSoundEvent, UiStateWindows, + }, +}; +use bevy::{ + app::AppExit, + prelude::{Assets, EventWriter, Local, Res, ResMut}, +}; +use bevy_egui::{egui, EguiContexts}; +use rose_game_common::messages::client::ClientMessage; + +const IID_BTN_EXIT: i32 = 10; +const IID_BTN_BACK: i32 = 11; +const IID_BTN_CHAR_SELECT: i32 = 12; +const IID_BTN_CLOSE: i32 = 13; + +pub struct ExitState { + pub pending_exit: bool, + pub pending_char_select: bool, +} + +impl Default for ExitState { + fn default() -> Self { + Self { + pending_exit: false, + pending_char_select: false, + } + } +} + +pub fn ui_exit_system( + mut ui_state: Local, + mut egui_context: EguiContexts, + mut ui_state_windows: ResMut, + mut ui_sound_events: EventWriter, + mut exit_events: EventWriter, + ui_resources: Res, + dialog_assets: Res>, + game_connection: Option>, +) { + let ui_state = &mut *ui_state; + let dialog = if let Some(dialog) = dialog_assets.get(&ui_resources.dialog_exit) { + dialog + } else { + return; + }; + + let mut response_close_button = None; + let mut response_back_button = None; + let mut response_char_select_button = None; + let mut response_exit_button = None; + + egui::Window::new("System Exit") + .frame(egui::Frame::none()) + .open(&mut ui_state_windows.exit_open) + .title_bar(false) + .resizable(false) + .default_width(dialog.width) + .default_height(dialog.height) + .show(egui_context.ctx_mut(), |ui| { + dialog.draw( + ui, + DataBindings { + sound_events: Some(&mut ui_sound_events), + response: &mut [ + (IID_BTN_CLOSE, &mut response_close_button), + (IID_BTN_BACK, &mut response_back_button), + (IID_BTN_CHAR_SELECT, &mut response_char_select_button), + (IID_BTN_EXIT, &mut response_exit_button), + ], + ..Default::default() + }, + |_, _| {}, + ); + }); + + if response_close_button.map_or(false, |r| r.clicked()) + || response_back_button.map_or(false, |r| r.clicked()) + { + ui_state_windows.exit_open = false; + } + + if response_char_select_button.map_or(false, |r| r.clicked()) { + ui_state_windows.exit_open = false; + ui_state.pending_char_select = true; + + if let Some(game_connection) = game_connection.as_ref() { + game_connection + .client_message_tx + .send(ClientMessage::ReturnToCharacterSelect) + .ok(); + } + } + + if response_exit_button.map_or(false, |r| r.clicked()) { + ui_state_windows.exit_open = false; + ui_state.pending_exit = true; + + if let Some(game_connection) = game_connection.as_ref() { + game_connection + .client_message_tx + .send(ClientMessage::Logout) + .ok(); + } + } + + if ui_state.pending_exit || ui_state.pending_char_select { + egui::Window::new("Disconnecting...") + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + .collapsible(false) + .show(egui_context.ctx_mut(), |ui| { + ui.label("Logging out"); + }); + } + + if game_connection.is_none() { + if ui_state.pending_exit { + ui_state.pending_exit = false; + exit_events.send(AppExit); + } + + if ui_state.pending_char_select { + ui_state.pending_char_select = false; + } + } +} diff --git a/src/ui/ui_game_menu_system.rs b/src/ui/ui_game_menu_system.rs index 2cd13d3a..70069ac4 100644 --- a/src/ui/ui_game_menu_system.rs +++ b/src/ui/ui_game_menu_system.rs @@ -114,7 +114,7 @@ pub fn ui_game_menu_system( } if response_button_exit.map_or(false, |r| r.clicked()) { - // TODO: Exit dialog + ui_state_windows.exit_open = !ui_state_windows.exit_open; } if !egui_context.ctx_mut().wants_keyboard_input() { @@ -142,6 +142,10 @@ pub fn ui_game_menu_system( if input.consume_shortcut(&config.hotkeys.settings) { ui_state_windows.settings_open = !ui_state_windows.settings_open; } + + if input.consume_shortcut(&config.hotkeys.exit) { + ui_state_windows.exit_open = !ui_state_windows.exit_open; + } }); } } diff --git a/src/ui/ui_settings_system.rs b/src/ui/ui_settings_system.rs index f0ba6e96..a2795e8d 100644 --- a/src/ui/ui_settings_system.rs +++ b/src/ui/ui_settings_system.rs @@ -310,6 +310,7 @@ pub fn ui_settings_system( add_shortcut_setting(ui, "Quest Log", &mut config.hotkeys.quests); add_shortcut_setting(ui, "Clan", &mut config.hotkeys.clan); add_shortcut_setting(ui, "Settings", &mut config.hotkeys.settings); + add_shortcut_setting(ui, "Exit", &mut config.hotkeys.exit); ui.end_row(); diff --git a/src/ui/ui_window_sound_system.rs b/src/ui/ui_window_sound_system.rs index bd59a62d..9ab11d71 100644 --- a/src/ui/ui_window_sound_system.rs +++ b/src/ui/ui_window_sound_system.rs @@ -88,4 +88,9 @@ pub fn ui_window_sound_system( next.create_clan_open, &ui_resources.dialog_create_clan, ); + play_dialog_sound( + &mut state.exit_open, + next.exit_open, + &ui_resources.dialog_exit, + ); }