use std::collections::HashMap; use std::net::SocketAddr; use std::ops::DerefMut; use std::rc::Rc; use godot::classes::{ CanvasLayer, Engine, IPanel, InputEvent, Panel, RichTextLabel, ScrollContainer, TextEdit, }; use godot::prelude::*; use godot::tools::get_autoload_by_name; pub enum CliColor { Command, Info, Help, Error, } impl CliColor { pub fn as_str(&self) -> &str { match self { CliColor::Command => "#a2a2a2", CliColor::Info => "#fff", CliColor::Help => "#ffdc2e", CliColor::Error => "#cc3535", } } } pub const CLI_GLOBAL_NAME: &str = "CommandLinePanelGlobal"; #[derive(GodotClass)] #[class(base=Panel)] pub struct CommandLinePanel { #[export] pub input_field: Option>, #[export] pub text_field: Option>, #[export] pub scroll_container: Option>, #[export] pub opened: bool, scroll_next_update: bool, commands: HashMap>>, idle_y: f32, base: Base, } #[godot_api] impl IPanel for CommandLinePanel { fn init(base: Base) -> Self { let mut commands: HashMap>> = HashMap::new(); commands.insert("help".to_owned(), Rc::new(Box::new(HelpCommand))); commands.insert("host".to_string(), Rc::new(Box::new(HostCommand))); commands.insert("join".to_string(), Rc::new(Box::new(JoinCommand))); commands.insert( "disconnect".to_string(), Rc::new(Box::new(DisconnectCommand)), ); CommandLinePanel { input_field: None, text_field: None, scroll_container: None, opened: false, scroll_next_update: false, idle_y: 0., commands, base, } } fn ready(&mut self) { self.idle_y = self.base().get_position().y; } fn process(&mut self, _delta: f64) { if let Some(scroll) = &mut self.scroll_container && self.scroll_next_update { scroll.set_v_scroll(i32::MAX); self.scroll_next_update = false; } if let Some(input) = self.input_field.clone() { let text = input.get_text(); if text.contains("\n") { let parts = text.split("\n"); let first = parts.get(0).unwrap(); self.input_command(first.to_string()); } } let old_pos = self.base().get_position(); let mut target_pos = self.base().get_position(); if self.opened { target_pos.y = 0.; } else { target_pos.y = self.idle_y; } let new_pos = old_pos.lerp(target_pos, 0.3); self.base_mut().set_position(new_pos); } fn input(&mut self, event: Gd) { if event.is_action_pressed("toggle_cli") { self.opened = !self.opened; if self.opened { self.signals().on_open().emit(); } else { self.signals().on_close().emit(); } if let Some(input_field) = &mut self.input_field { if self.opened { input_field.grab_focus(); input_field.set_editable(true); self.run_deferred(|s| s.input_field.as_mut().unwrap().clear()); } else { input_field.release_focus(); input_field.set_editable(false); } } } } } #[godot_api] impl CommandLinePanel { #[signal] pub fn on_host(port: u16); #[signal] pub fn on_join(port: String); #[signal] pub fn on_disconnect(); #[signal] pub fn on_open(); #[signal] pub fn on_close(); pub fn singleton() -> Gd { get_autoload_by_name::(CLI_GLOBAL_NAME) .get_child(0) .unwrap() .cast() } pub fn input_command(&mut self, command: String) { self.publish_message(format!("> {}", command), CliColor::Command); if let Some(input) = &mut self.input_field { input.clear(); } self.handle_command(&command); } pub fn publish_message(&mut self, message: String, color: CliColor) { if let Some(field) = &mut self.text_field { if !field.get_parsed_text().is_empty() { field.append_text("\n"); } field.append_text(&format!("[color={}]", color.as_str())); field.append_text(&message); field.append_text("[/color]"); self.scroll_next_update = true; } } fn handle_command(&mut self, text: &String) { let mut command_and_params = text.split(" ").into_iter(); let command = command_and_params.next().clone().unwrap_or(""); let params = command_and_params.collect::>(); if let Some(command) = self.commands.get(command) { command.clone().execute(self, ¶ms); } else { self.publish_message(format!("Unknown command: {}", command), CliColor::Info); } } } trait Command { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]); fn help(&self) -> &str; } #[derive(Clone)] struct HelpCommand; impl Command for HelpCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(command_name) = params.first() { if let Some(command) = cli.commands.get(*command_name) { cli.publish_message(command.help().to_string(), CliColor::Help); } else { cli.publish_message(format!("Unknown command: {}", command_name), CliColor::Help); } } else { let mut keys = cli.commands.keys().cloned().collect::>(); keys.sort(); for key in keys { cli.publish_message( format!("\t- {}: {}", key, cli.commands.get(&key).unwrap().help()), CliColor::Help, ); } } } fn help(&self) -> &str { "Shows this message" } } #[derive(Clone)] struct HostCommand; impl Command for HostCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(port_str) = params.first() { if let Ok(port) = u16::from_str_radix(*port_str, 10) { cli.signals().on_host().emit(port); } else { cli.publish_message(format!("Invalid port: {}", port_str), CliColor::Error); } } else { cli.publish_message(format!("Missing port"), CliColor::Error); cli.publish_message(format!("Usage: host [port]"), CliColor::Help); } } fn help(&self) -> &str { "Hosts a server at a given port" } } #[derive(Clone)] struct JoinCommand; impl Command for JoinCommand { fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) { if let Some(address_str) = params.first() { if let Ok(_) = address_str.parse::() { cli.signals().on_join().emit(address_str.to_string()); } else { cli.publish_message(format!("Invalid address: {}", address_str), CliColor::Error); } } else { cli.publish_message(format!("Missing address"), CliColor::Error); cli.publish_message(format!("Usage: join [address]:[port]"), CliColor::Help); } } fn help(&self) -> &str { "Joins server at a given address:port" } } #[derive(Clone)] struct DisconnectCommand; impl Command for DisconnectCommand { fn execute(&self, cli: &mut CommandLinePanel, _params: &[&str]) { cli.signals().on_disconnect().emit(); } fn help(&self) -> &str { "Disconnects from the server, or stops hosting the server" } }