diff --git a/data b/data index 6944fcfd..20cb4cb0 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 6944fcfdeef9a0d708322047ffde45e4088cd4a0 +Subproject commit 20cb4cb04df95279aed6637a49349ebba59cfa4a diff --git a/game/client-containers/src/entities.rs b/game/client-containers/src/entities.rs index 566dfe5f..d7a48d35 100644 --- a/game/client-containers/src/entities.rs +++ b/game/client-containers/src/entities.rs @@ -150,7 +150,7 @@ impl LoadEntities { name: &str, ) -> TextureContainer2dArray { texture_handle - .load_texture_3d_rgba_u8(img.data, name) + .load_texture_2d_array_rgba_u8(img.data, name) .unwrap() } } diff --git a/game/client-render-base/src/map/render_map_base.rs b/game/client-render-base/src/map/render_map_base.rs index 148d3976..b1ede3e7 100644 --- a/game/client-render-base/src/map/render_map_base.rs +++ b/game/client-render-base/src/map/render_map_base.rs @@ -562,7 +562,7 @@ impl ClientMapRender { .map(|img| { map_upload .texture_handle - .load_texture_3d_rgba_u8(img.mem, &img.name) + .load_texture_2d_array_rgba_u8(img.mem, &img.name) }) .collect::>>()?; diff --git a/game/editor/src/action_logic.rs b/game/editor/src/action_logic.rs index 02dd8f89..eda395b4 100644 --- a/game/editor/src/action_logic.rs +++ b/game/editor/src/action_logic.rs @@ -1,4 +1,4 @@ -use std::{rc::Rc, sync::Arc}; +use std::{collections::BTreeMap, rc::Rc, sync::Arc}; use anyhow::anyhow; use base::hash::generate_hash_for; @@ -49,9 +49,9 @@ use crate::{ ActLayerChangeSoundIndex, ActMoveGroup, ActMoveLayer, ActQuadLayerAddQuads, ActQuadLayerAddRemQuads, ActQuadLayerRemQuads, ActRemColorAnim, ActRemGroup, ActRemImage, ActRemImage2dArray, ActRemPhysicsTileLayer, ActRemPosAnim, ActRemQuadLayer, ActRemSound, - ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSoundLayerAddRemSounds, - ActSoundLayerAddSounds, ActSoundLayerRemSounds, ActTileLayerReplTilesBase, - ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, + ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSetCommands, ActSetMetadata, + ActSoundLayerAddRemSounds, ActSoundLayerAddSounds, ActSoundLayerRemSounds, + ActTileLayerReplTilesBase, ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, ActTilePhysicsLayerReplaceTiles, EditorAction, }, map::{ @@ -530,6 +530,14 @@ fn merge_actions_group( EditorAction::RemSoundAnim(act1), Some(EditorAction::RemSoundAnim(act2)), )), + (EditorAction::SetCommands(mut act1), EditorAction::SetCommands(act2)) => { + act1.new_commands = act2.new_commands; + Ok((EditorAction::SetCommands(act1), None)) + } + (EditorAction::SetMetadata(mut act1), EditorAction::SetMetadata(act2)) => { + act1.new_meta = act2.new_meta; + Ok((EditorAction::SetMetadata(act1), None)) + } (act1, act2) => Ok((act1, Some(act2))), } } @@ -838,7 +846,7 @@ pub fn do_action( EditorImage2dArray { user: EditorResource { user: texture_handle - .load_texture_3d_rgba_u8(mem, act.base.res.name.as_str())?, + .load_texture_2d_array_rgba_u8(mem, act.base.res.name.as_str())?, file: Rc::new(act.base.file.clone()), hq: None, }, @@ -1614,8 +1622,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -1627,8 +1639,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -1640,8 +1656,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -1653,8 +1673,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -1666,8 +1690,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -1679,8 +1707,12 @@ pub fn do_action( attr: Default::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }) } @@ -2879,6 +2911,28 @@ pub fn do_action( ); map.animations.sound.remove(act.base.index); } + EditorAction::SetCommands(act) => { + if fix_action { + act.old_commands = map.config.def.commands.clone(); + } + let old_cmds: BTreeMap<_, _> = act.old_commands.clone().into_iter().collect(); + let cur_cmds: BTreeMap<_, _> = map.config.def.commands.clone().into_iter().collect(); + anyhow::ensure!( + old_cmds == cur_cmds, + "commands in action did not match the ones in map." + ); + map.config.def.commands = act.new_commands.clone(); + } + EditorAction::SetMetadata(act) => { + if fix_action { + act.old_meta = map.meta.def.clone(); + } + anyhow::ensure!( + act.old_meta == map.meta.def, + "metadata in action did not match the ones in map." + ); + map.meta.def = act.new_meta.clone(); + } } Ok(action) } @@ -3494,6 +3548,34 @@ pub fn undo_action( map, false, ), + EditorAction::SetCommands(act) => do_action( + tp, + sound_mt, + graphics_mt, + buffer_object_handle, + backend_handle, + texture_handle, + EditorAction::SetCommands(ActSetCommands { + old_commands: act.new_commands, + new_commands: act.old_commands, + }), + map, + false, + ), + EditorAction::SetMetadata(act) => do_action( + tp, + sound_mt, + graphics_mt, + buffer_object_handle, + backend_handle, + texture_handle, + EditorAction::SetMetadata(ActSetMetadata { + old_meta: act.new_meta, + new_meta: act.old_meta, + }), + map, + false, + ), } .map(|_| ()) } diff --git a/game/editor/src/actions/actions.rs b/game/editor/src/actions/actions.rs index 75bcb490..dddcfc1a 100644 --- a/game/editor/src/actions/actions.rs +++ b/game/editor/src/actions/actions.rs @@ -1,5 +1,6 @@ use base::linked_hash_map_view::FxLinkedHashMap; use enum_dispatch::enum_dispatch; +use hashlink::LinkedHashMap; use map::{ map::{ animations::{ColorAnimation, PosAnimation, SoundAnimation}, @@ -14,6 +15,7 @@ use map::{ }, MapGroup, MapGroupAttr, MapGroupPhysicsAttr, }, + metadata::Metadata, resources::MapResourceRef, }, types::NonZeroU16MinusOne, @@ -95,6 +97,9 @@ pub enum EditorAction { RemColorAnim(ActRemColorAnim), AddSoundAnim(ActAddSoundAnim), RemSoundAnim(ActRemSoundAnim), + // server settings + SetCommands(ActSetCommands), + SetMetadata(ActSetMetadata), } /// actions are always grouped, even single actions @@ -1802,3 +1807,43 @@ impl EditorActionInterface for ActRemSoundAnim { } } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActSetCommands { + pub old_commands: LinkedHashMap, + pub new_commands: LinkedHashMap, +} + +impl EditorActionInterface for ActSetCommands { + fn undo_info(&self) -> String { + format!( + "Replace (back) {} commands with {} commands", + self.new_commands.len(), + self.old_commands.len() + ) + } + + fn redo_info(&self) -> String { + format!( + "Replace {} commands with {} commands", + self.old_commands.len(), + self.new_commands.len() + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActSetMetadata { + pub old_meta: Metadata, + pub new_meta: Metadata, +} + +impl EditorActionInterface for ActSetMetadata { + fn undo_info(&self) -> String { + "Replace (back) meta data change".to_string() + } + + fn redo_info(&self) -> String { + "Replace meta data change".to_string() + } +} diff --git a/game/editor/src/client.rs b/game/editor/src/client.rs index 33e78ab1..2f073c2c 100644 --- a/game/editor/src/client.rs +++ b/game/editor/src/client.rs @@ -50,6 +50,8 @@ pub struct EditorClient { pub(crate) undo_label: Option, pub(crate) redo_label: Option, + pub(crate) should_save: bool, + mapper_name: String, color: [u8; 3], } @@ -90,6 +92,8 @@ impl EditorClient { mapper_name: mapper_name.unwrap_or_else(|| "mapper".to_string()), color: color.unwrap_or([255, 255, 255]), + + should_save: !local_client, }; res.network @@ -144,6 +148,7 @@ impl EditorClient { redo_label, undo_label, } => { + self.should_save = true; if !self.local_client { let actions: Box> = if undo_event { Box::new(action.actions.into_iter().rev()) diff --git a/game/editor/src/dbg/invalid.rs b/game/editor/src/dbg/invalid.rs index ce1f3b75..c7f6abe1 100644 --- a/game/editor/src/dbg/invalid.rs +++ b/game/editor/src/dbg/invalid.rs @@ -1,6 +1,7 @@ use std::time::Duration; use base::hash::generate_hash_for; +use hashlink::LinkedHashMap; use map::map::{ animations::{AnimBase, AnimPoint, AnimPointCurveType}, groups::{ @@ -17,6 +18,7 @@ use map::map::{ }, MapGroup, MapGroupAttr, MapGroupPhysicsAttr, }, + metadata::Metadata, resources::{MapResourceMetaData, MapResourceRef}, }; use math::math::vector::uffixed; @@ -35,9 +37,9 @@ use crate::{ ActLayerChangeSoundIndex, ActMoveGroup, ActMoveLayer, ActQuadLayerAddQuads, ActQuadLayerAddRemQuads, ActQuadLayerRemQuads, ActRemColorAnim, ActRemGroup, ActRemImage, ActRemImage2dArray, ActRemPhysicsTileLayer, ActRemPosAnim, ActRemQuadLayer, - ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSoundLayerAddRemSounds, - ActSoundLayerAddSounds, ActSoundLayerRemSounds, ActTileLayerReplTilesBase, - ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, + ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSetCommands, ActSetMetadata, + ActSoundLayerAddRemSounds, ActSoundLayerAddSounds, ActSoundLayerRemSounds, + ActTileLayerReplTilesBase, ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, ActTilePhysicsLayerReplaceTiles, EditorAction, }, dbg::valid::{ @@ -1182,10 +1184,79 @@ fn rem_sound_anim_invalid(_map: &EditorMap) -> Vec { })] } +fn set_commands_invalid(_map: &EditorMap) -> Vec { + vec![EditorAction::SetCommands(ActSetCommands { + old_commands: Default::default(), + new_commands: { + let mut cmds: LinkedHashMap<_, _> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 20 { + cmds.insert( + format!("{}", rand::rngs::OsRng.next_u64()), + format!("{}", rand::rngs::OsRng.next_u64()), + ); + } + + cmds + }, + })] +} + +fn set_metadata_invalid(_map: &EditorMap) -> Vec { + vec![EditorAction::SetMetadata(ActSetMetadata { + old_meta: Metadata { + authors: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + licenses: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + version: format!("{}", rand::rngs::OsRng.next_u64()), + credits: format!("{}", rand::rngs::OsRng.next_u64()), + memo: format!("{}", rand::rngs::OsRng.next_u64()), + }, + new_meta: Metadata { + authors: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + licenses: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + version: format!("{}", rand::rngs::OsRng.next_u64()), + credits: format!("{}", rand::rngs::OsRng.next_u64()), + memo: format!("{}", rand::rngs::OsRng.next_u64()), + }, + })] +} + /// Invalid here still makes sure that no memory exhaustion happens. pub fn random_invalid_action(map: &EditorMap) -> Vec { // must match the last value in the `match` + 1 - const TOTAL_ACTIONS: u64 = 44; + const TOTAL_ACTIONS: u64 = 46; loop { match match rand::rngs::OsRng.next_u64() % TOTAL_ACTIONS { 0 => move_group_invalid(map), @@ -1230,6 +1301,8 @@ pub fn random_invalid_action(map: &EditorMap) -> Vec { 41 => rem_color_anim_invalid(map), 42 => add_sound_anim_invalid(map), 43 => rem_sound_anim_invalid(map), + 44 => set_commands_invalid(map), + 45 => set_metadata_invalid(map), _ => panic!("unsupported action count"), } { act if !act.is_empty() => return act, diff --git a/game/editor/src/dbg/valid.rs b/game/editor/src/dbg/valid.rs index 085b9302..6e94e959 100644 --- a/game/editor/src/dbg/valid.rs +++ b/game/editor/src/dbg/valid.rs @@ -1,4 +1,5 @@ use base::hash::generate_hash_for; +use hashlink::LinkedHashMap; use map::map::{ animations::AnimBase, groups::{ @@ -15,6 +16,7 @@ use map::map::{ }, MapGroup, MapGroupAttr, MapGroupPhysicsAttr, }, + metadata::Metadata, resources::{MapResourceMetaData, MapResourceRef}, }; use math::math::vector::uffixed; @@ -33,9 +35,9 @@ use crate::{ ActLayerChangeSoundIndex, ActMoveGroup, ActMoveLayer, ActQuadLayerAddQuads, ActQuadLayerAddRemQuads, ActQuadLayerRemQuads, ActRemColorAnim, ActRemGroup, ActRemImage, ActRemImage2dArray, ActRemPhysicsTileLayer, ActRemPosAnim, ActRemQuadLayer, - ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSoundLayerAddRemSounds, - ActSoundLayerAddSounds, ActSoundLayerRemSounds, ActTileLayerReplTilesBase, - ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, + ActRemSoundAnim, ActRemSoundLayer, ActRemTileLayer, ActSetCommands, ActSetMetadata, + ActSoundLayerAddRemSounds, ActSoundLayerAddSounds, ActSoundLayerRemSounds, + ActTileLayerReplTilesBase, ActTileLayerReplaceTiles, ActTilePhysicsLayerReplTilesBase, ActTilePhysicsLayerReplaceTiles, EditorAction, }, map::{EditorLayer, EditorMap, EditorPhysicsLayer}, @@ -2132,9 +2134,56 @@ fn rem_sound_anim_valid(map: &EditorMap) -> Vec { .concat() } +fn set_commands_valid(map: &EditorMap) -> Vec { + vec![EditorAction::SetCommands(ActSetCommands { + old_commands: map.config.def.commands.clone(), + new_commands: { + let mut cmds: LinkedHashMap<_, _> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 20 { + cmds.insert( + format!("{}", rand::rngs::OsRng.next_u64()), + format!("{}", rand::rngs::OsRng.next_u64()), + ); + } + + cmds + }, + })] +} + +fn set_metadata_valid(map: &EditorMap) -> Vec { + vec![EditorAction::SetMetadata(ActSetMetadata { + old_meta: map.meta.def.clone(), + new_meta: Metadata { + authors: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + licenses: { + let mut s: Vec<_> = Default::default(); + + for _ in 0..rand::rngs::OsRng.next_u64() % 5 { + s.push(format!("{}", rand::rngs::OsRng.next_u64())); + } + + s + }, + version: format!("{}", rand::rngs::OsRng.next_u64()), + credits: format!("{}", rand::rngs::OsRng.next_u64()), + memo: format!("{}", rand::rngs::OsRng.next_u64()), + }, + })] +} + pub fn random_valid_action(map: &EditorMap) -> Vec { // must match the last value in the `match` + 1 - const TOTAL_ACTIONS: u64 = 44; + const TOTAL_ACTIONS: u64 = 46; loop { match match rand::rngs::OsRng.next_u64() % TOTAL_ACTIONS { 0 => move_group_valid(map), @@ -2179,6 +2228,8 @@ pub fn random_valid_action(map: &EditorMap) -> Vec { 41 => rem_color_anim_valid(map), 42 => add_sound_anim_valid(map), 43 => rem_sound_anim_valid(map), + 44 => set_commands_valid(map), + 45 => set_metadata_valid(map), _ => panic!("unsupported action count"), } { act if !act.is_empty() => return act, diff --git a/game/editor/src/editor.rs b/game/editor/src/editor.rs index 7b542b71..b1aeb104 100644 --- a/game/editor/src/editor.rs +++ b/game/editor/src/editor.rs @@ -108,6 +108,7 @@ use crate::{ upload_design_tile_layer_buffer, upload_physics_layer_buffer, }, notifications::{EditorNotification, EditorNotifications}, + physics_layers::PhysicsLayerOverlaysDdnet, server::EditorServer, tab::EditorTab, tools::{ @@ -123,7 +124,10 @@ use crate::{ }, utils::{render_rect, render_rect_from_state, render_rect_state}, }, - ui::user_data::{EditorTabsRefMut, EditorUiEvent, EditorUiEventHostMap}, + ui::user_data::{ + EditorMenuDialogMode, EditorModalDialogMode, EditorTabsRefMut, EditorUiEvent, + EditorUiEventHostMap, + }, utils::{ui_pos_to_world_pos, UiCanvasSize}, }; @@ -270,7 +274,7 @@ impl Editor { let fake_texture_array = graphics .texture_handle - .load_texture_3d_rgba_u8(mem, "fake-editor-texture") + .load_texture_2d_array_rgba_u8(mem, "fake-editor-texture") .unwrap(); // fake texture texture for non textured quads @@ -296,6 +300,9 @@ impl Editor { let mut ui_creator = UiCreator::default(); ui_creator.load_font(font_data); + let overlays = PhysicsLayerOverlaysDdnet::new(io, tp, graphics) + .expect("Data files for editor are wrong"); + let mut res = Self { tabs: Default::default(), active_tab: "".into(), @@ -309,6 +316,7 @@ impl Editor { &graphics_mt, &graphics.buffer_object_handle, &graphics.backend_handle, + &overlays, ), selection: TileSelection::new(), }, @@ -504,8 +512,12 @@ impl Editor { attr: EditorCommonGroupOrLayerAttr::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }, }, )], @@ -522,7 +534,7 @@ impl Editor { def: Config { commands: Default::default(), }, - user: (), + user: Default::default(), }, meta: EditorMetadata { def: Metadata { @@ -868,14 +880,14 @@ impl Editor { .map(|((mem, file), hq_mem_file, i)| EditorImage2dArray { user: EditorResource { user: texture_handle - .load_texture_3d_rgba_u8(mem, i.name.as_str()) + .load_texture_2d_array_rgba_u8(mem, i.name.as_str()) .unwrap(), file: file.into(), hq: hq_mem_file.map(|(mem, file)| { ( file.into(), texture_handle - .load_texture_3d_rgba_u8(mem, i.name.as_str()) + .load_texture_2d_array_rgba_u8(mem, i.name.as_str()) .unwrap(), ) }), @@ -940,8 +952,12 @@ impl Editor { attr: EditorCommonGroupOrLayerAttr::default(), selected: Default::default(), number_extra: Default::default(), - number_extra_texts: Default::default(), + number_extra_text: Default::default(), context_menu_open: false, + switch_delay: Default::default(), + speedup_force: Default::default(), + speedup_angle: Default::default(), + speedup_max_speed: Default::default(), }; match layer { MapLayerPhysics::Arbitrary(layer) => EditorPhysicsLayer::Arbitrary( @@ -998,7 +1014,7 @@ impl Editor { }, config: EditorConfig { def: map.config, - user: (), + user: Default::default(), }, meta: EditorMetadata { def: map.meta, @@ -1464,6 +1480,7 @@ impl Editor { notifications_overlay: &mut ClientNotifications, path: &Path, ) { + tab.client.should_save = false; if path.extension().is_some_and(|ext| ext == "map") { match Self::save_map_legacy(tab, io, tp, path) { Ok(task) => { @@ -1515,6 +1532,62 @@ impl Editor { } } + fn save_tab(&mut self, tab: &str) -> bool { + if let Some((path, tab)) = self + .tabs + .get_mut(tab) + .and_then(|tab| tab.auto_saver.path.clone().map(|path| (path.clone(), tab))) + { + Self::save_map_tab( + tab, + &self.io, + &self.thread_pool, + &mut self.save_tasks, + &mut self.notifications_overlay, + &path, + ); + true + } else { + let msg = "The current map has never been saved.\n\ + It has to be saved using the GUI at least once."; + log::info!("{msg}"); + self.notifications_overlay + .add_err(msg, Duration::from_secs(10)); + + self.ui.menu_dialog_mode = EditorMenuDialogMode::save(&self.io); + false + } + } + + fn save_all_tabs(&mut self) -> bool { + let mut all_saved = true; + for (path, tab) in self + .tabs + .values_mut() + .map(|tab| (tab.auto_saver.path.clone(), tab)) + { + if let Some(path) = path { + Self::save_map_tab( + tab, + &self.io, + &self.thread_pool, + &mut self.save_tasks, + &mut self.notifications_overlay, + &path, + ); + } else { + let msg = "Some maps have never been saved.\n\ + It has to be saved using the GUI at least once."; + log::info!("{msg}"); + self.notifications_overlay + .add_err(msg, Duration::from_secs(10)); + all_saved = false; + } + } + + all_saved + } + fn update(&mut self) { let time_now = self.sys.time_get(); let time_diff = time_now - self.last_time; @@ -1561,6 +1634,7 @@ impl Editor { &mut tab.map, &mut tab.auto_saver, &mut self.notifications_overlay, + &mut tab.client.should_save, ); } @@ -2351,18 +2425,19 @@ impl Editor { self.save_map(&name); } EditorUiEvent::SaveCurMap => { - if let Some(path) = self - .tabs - .get(&self.active_tab) - .and_then(|tab| tab.auto_saver.path.clone()) - { - self.save_map(&path); - } else { - let msg = "The current map has never been saved.\n\ - It has to be saved using the GUI at least once."; - log::info!("{msg}"); - self.notifications_overlay - .add_err(msg, Duration::from_secs(10)); + self.save_tab(&self.active_tab.clone()); + } + EditorUiEvent::SaveMapAndClose { tab } => { + if self.save_tab(&tab) { + self.tabs.remove(&self.active_tab); + } + } + EditorUiEvent::SaveAll => { + self.save_all_tabs(); + } + EditorUiEvent::SaveAllAndClose => { + if self.save_all_tabs() { + forced_result = Some(EditorResult::Close); } } EditorUiEvent::HostMap(host_map) => { @@ -2411,6 +2486,13 @@ impl Editor { }, ), EditorUiEvent::Close => { + if self.tabs.values().any(|t| t.client.should_save) { + self.ui.modal_dialog_mode = EditorModalDialogMode::CloseEditor; + } else { + forced_result = Some(EditorResult::Close); + } + } + EditorUiEvent::ForceClose => { forced_result = Some(EditorResult::Close); } EditorUiEvent::Minimize => { diff --git a/game/editor/src/editor_ui.rs b/game/editor/src/editor_ui.rs index 2a99cf59..bfd25a53 100644 --- a/game/editor/src/editor_ui.rs +++ b/game/editor/src/editor_ui.rs @@ -23,7 +23,9 @@ use crate::{ tools::{tile_layer::auto_mapper::TileLayerAutoMapper, tool::Tools}, ui::{ page::EditorUi, - user_data::{EditorMenuDialogMode, EditorTabsRefMut, EditorUiEvent, UserData}, + user_data::{ + EditorMenuDialogMode, EditorModalDialogMode, EditorTabsRefMut, EditorUiEvent, UserData, + }, }, utils::UiCanvasSize, }; @@ -46,7 +48,8 @@ pub struct EditorUiRender { pub ui: UiContainer, editor_ui: EditorUi, - menu_dialog_mode: EditorMenuDialogMode, + pub menu_dialog_mode: EditorMenuDialogMode, + pub modal_dialog_mode: EditorModalDialogMode, backend_handle: GraphicsBackendHandle, canvas_handle: GraphicsCanvasHandle, @@ -68,6 +71,7 @@ impl EditorUiRender { editor_ui: EditorUi::new(), menu_dialog_mode: EditorMenuDialogMode::None, + modal_dialog_mode: EditorModalDialogMode::None, backend_handle: graphics.backend_handle.clone(), canvas_handle: graphics.canvas_handle.clone(), @@ -104,6 +108,7 @@ impl EditorUiRender { canvas_size: pipe.canvas_size, menu_dialog_mode: &mut self.menu_dialog_mode, + modal_dialog_mode: &mut self.modal_dialog_mode, tools: pipe.tools, auto_mapper: pipe.auto_mapper, diff --git a/game/editor/src/lib.rs b/game/editor/src/lib.rs index 2abdff7e..ffe4ce70 100644 --- a/game/editor/src/lib.rs +++ b/game/editor/src/lib.rs @@ -14,6 +14,7 @@ pub mod map; pub mod map_tools; pub mod network; pub mod notifications; +pub mod physics_layers; pub mod server; pub mod tab; pub mod tools; diff --git a/game/editor/src/map.rs b/game/editor/src/map.rs index 21ec1f6d..26766aad 100644 --- a/game/editor/src/map.rs +++ b/game/editor/src/map.rs @@ -175,7 +175,14 @@ pub struct EditorPhysicsLayerProps { /// for physics layers that have numbers that reference other stuff /// e.g. tele, switch & tune zone layer pub number_extra: FxLinkedHashMap, - pub number_extra_texts: (String, String), + pub number_extra_text: String, + + pub switch_delay: u8, + + pub speedup_force: u8, + pub speedup_angle: i16, + pub speedup_max_speed: u8, + pub context_menu_open: bool, } @@ -507,7 +514,12 @@ pub trait EditorMapGroupsInterface { fn active_layer_mut(&mut self) -> Option; } -pub type EditorConfig = ConfigSkeleton<()>; +#[derive(Debug, Clone, Default)] +pub struct EditorMapConfig { + pub cmd_string: String, + pub selected_cmd: Option, +} +pub type EditorConfig = ConfigSkeleton; pub type EditorMetadata = MetadataSkeleton<()>; #[derive(Default)] @@ -601,7 +613,7 @@ pub type EditorMap = MapSkeleton< EditorArbitraryLayerProps, EditorAnimationsProps, EditorAnimationProps, - (), + EditorMapConfig, (), >; diff --git a/game/editor/src/physics_layers.rs b/game/editor/src/physics_layers.rs new file mode 100644 index 00000000..9d270d6a --- /dev/null +++ b/game/editor/src/physics_layers.rs @@ -0,0 +1,161 @@ +use std::{path::Path, rc::Rc, sync::Arc}; + +use anyhow::anyhow; +use base_io::io::Io; +use graphics::{ + graphics::graphics::Graphics, graphics_mt::GraphicsMultiThreaded, + handles::texture::texture::TextureContainer2dArray, +}; +use graphics_types::{ + commands::TexFlags, + types::{GraphicsBackendMemory, GraphicsMemoryAllocationType}, +}; +use hiarc::Hiarc; +use image_utils::{png::load_png_image_as_rgba, utils::texture_2d_to_3d}; + +#[derive(Debug)] +struct PhysicsLayerTexturesDdnetLoading { + game: GraphicsBackendMemory, + front: GraphicsBackendMemory, + tele: GraphicsBackendMemory, + speedup: GraphicsBackendMemory, + switch: GraphicsBackendMemory, + tune: GraphicsBackendMemory, +} + +#[derive(Debug, Hiarc)] +pub struct PhysicsLayerOverlaysDdnet { + pub game: TextureContainer2dArray, + pub front: TextureContainer2dArray, + pub tele: TextureContainer2dArray, + pub speedup: TextureContainer2dArray, + pub switch: TextureContainer2dArray, + pub tune: TextureContainer2dArray, +} + +impl PhysicsLayerOverlaysDdnet { + pub fn new( + io: &Io, + thread_pool: &Arc, + graphics: &Graphics, + ) -> anyhow::Result> { + let fs = io.fs.clone(); + let tp = thread_pool.clone(); + let graphics_mt = graphics.get_graphics_mt(); + let loading = io + .rt + .spawn(async move { + let editor_base: &Path = "editor/physics_layers/ddnet".as_ref(); + + fn load( + file: Vec, + thread_pool: &rayon::ThreadPool, + graphics_mt: &GraphicsMultiThreaded, + ) -> anyhow::Result { + let mut img = Vec::new(); + let img = load_png_image_as_rgba(&file, |w, h, _| { + img = vec![0; w * h * 4]; + &mut img + })?; + + anyhow::ensure!(img.width % 16 == 0, "width not divislbe by 16"); + anyhow::ensure!(img.height % 16 == 0, "width not divislbe by 16"); + anyhow::ensure!(img.height > 0 && img.width > 0, "width or height 0"); + + let mut dst: Vec = vec![0; img.width as usize * img.height as usize * 4]; + let mut dst_w = 0; + let mut dst_h = 0; + + if !texture_2d_to_3d( + thread_pool, + img.data, + img.width as usize, + img.height as usize, + 4, + 16, + 16, + &mut dst, + &mut dst_w, + &mut dst_h, + ) { + return Err(anyhow!( + "Failed to read editor physics layer, \ + not convertable to 2d array texture." + )); + } + + let width = dst_w.try_into()?; + let height = dst_h.try_into()?; + + let mut mem = + graphics_mt.mem_alloc(GraphicsMemoryAllocationType::TextureRgbaU82dArray { + width, + height, + depth: 256.try_into().unwrap(), + flags: TexFlags::empty(), + }); + + mem.as_mut_slice().copy_from_slice(&dst); + + let _ = graphics_mt.try_flush_mem(&mut mem, true); + + Ok(mem) + } + + Ok(PhysicsLayerTexturesDdnetLoading { + game: load( + fs.read_file(&editor_base.join("game.png")).await?, + &tp, + &graphics_mt, + )?, + front: load( + fs.read_file(&editor_base.join("front.png")).await?, + &tp, + &graphics_mt, + )?, + tele: load( + fs.read_file(&editor_base.join("tele.png")).await?, + &tp, + &graphics_mt, + )?, + speedup: load( + fs.read_file(&editor_base.join("speedup.png")).await?, + &tp, + &graphics_mt, + )?, + switch: load( + fs.read_file(&editor_base.join("switch.png")).await?, + &tp, + &graphics_mt, + )?, + tune: load( + fs.read_file(&editor_base.join("tune.png")).await?, + &tp, + &graphics_mt, + )?, + }) + }) + .get_storage()?; + + Ok(Rc::new(Self { + game: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.game, "game")?, + front: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.front, "front")?, + tele: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.tele, "tele")?, + speedup: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.speedup, "speedup")?, + switch: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.switch, "switch")?, + tune: graphics + .texture_handle + .load_texture_2d_array_rgba_u8(loading.tune, "tune")?, + })) + } +} diff --git a/game/editor/src/server.rs b/game/editor/src/server.rs index bea55a03..91c955ad 100644 --- a/game/editor/src/server.rs +++ b/game/editor/src/server.rs @@ -126,6 +126,7 @@ impl EditorServer { map: &mut EditorMap, auto_saver: &mut AutoSaver, notifications: &mut ClientNotifications, + should_save: &mut bool, ) { // check if client exist and is authed if let Some(client) = self.clients.get_mut(&id) { @@ -257,6 +258,7 @@ impl EditorServer { } } if !valid_act.actions.is_empty() { + *should_save = true; if let Some(cur_action_group) = self.cur_action_group { self.action_groups.truncate(cur_action_group + 1); } else { @@ -333,6 +335,7 @@ impl EditorServer { }))) && !self.action_groups.is_empty() { + *should_save = true; if !is_undo { self.cur_action_group = match self.cur_action_group { @@ -462,11 +465,14 @@ impl EditorServer { self.broadcast_client_infos(); } EditorEventClientToServer::Chat { msg } => { - self.network - .send(EditorEvent::Server(EditorEventServerToClient::Chat { - from: client.props.mapper_name.clone(), - msg, - })); + if !msg.is_empty() { + self.network.send(EditorEvent::Server( + EditorEventServerToClient::Chat { + from: client.props.mapper_name.clone(), + msg, + }, + )); + } } EditorEventClientToServer::AdminAuth { password } => { if self.admin_password == Some(password) { @@ -524,6 +530,7 @@ impl EditorServer { map, auto_saver, notifications, + should_save, ); }; let gen_actions = |map: &mut _| { @@ -579,6 +586,7 @@ impl EditorServer { map, auto_saver, notifications, + should_save, ); } else { let shuffle_action = rand::rngs::OsRng.next_u64() % u8::MAX as u64; @@ -629,6 +637,7 @@ impl EditorServer { map: &mut EditorMap, auto_saver: &mut AutoSaver, notifications: &mut ClientNotifications, + should_save: &mut bool, ) { if self.has_events.load(std::sync::atomic::Ordering::Relaxed) { let events = self.event_generator.take(); @@ -648,6 +657,7 @@ impl EditorServer { map, auto_saver, notifications, + should_save, ); } EditorNetEvent::Editor(EditorEvent::Server(_)) => { diff --git a/game/editor/src/tools/tile_layer/brush.rs b/game/editor/src/tools/tile_layer/brush.rs index 43ebe722..62726d48 100644 --- a/game/editor/src/tools/tile_layer/brush.rs +++ b/game/editor/src/tools/tile_layer/brush.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, collections::HashSet, sync::Arc}; +use std::{cell::Cell, collections::HashSet, rc::Rc, sync::Arc}; use client_containers::{container::ContainerKey, entities::EntitiesContainer}; use client_render_base::map::{ @@ -46,6 +46,7 @@ use crate::{ finish_design_tile_layer_buffer, finish_physics_layer_buffer, upload_design_tile_layer_buffer, upload_physics_layer_buffer, }, + physics_layers::PhysicsLayerOverlaysDdnet, tools::utils::{ render_checkerboard_background, render_filled_rect, render_filled_rect_from_state, render_rect, render_rect_from_state, @@ -106,6 +107,8 @@ pub struct TileBrushTiles { pub struct TileBrushTilePicker { pub render: TileLayerVisuals, pub map_render: MapGraphics, + + physics_overlay: Rc, } impl TileBrushTilePicker { @@ -113,6 +116,7 @@ impl TileBrushTilePicker { graphics_mt: &GraphicsMultiThreaded, buffer_object_handle: &GraphicsBufferObjectHandle, backend_handle: &GraphicsBackendHandle, + physics_overlay: Rc, ) -> Self { let map_render = MapGraphics::new(backend_handle); @@ -123,6 +127,7 @@ impl TileBrushTilePicker { backend_handle, ), map_render, + physics_overlay, } } } @@ -151,6 +156,7 @@ impl TileBrush { graphics_mt: &GraphicsMultiThreaded, buffer_object_handle: &GraphicsBufferObjectHandle, backend_handle: &GraphicsBackendHandle, + physics_overlay: &Rc, ) -> Self { Self { brush: None, @@ -159,6 +165,7 @@ impl TileBrush { graphics_mt, buffer_object_handle, backend_handle, + physics_overlay.clone(), ), pointer_down_world_pos: None, @@ -338,7 +345,7 @@ impl TileBrush { }) .collect(), ), - EditorPhysicsLayer::Speedup(_) => { + EditorPhysicsLayer::Speedup(layer) => { MapTileLayerPhysicsTiles::Speedup( tile_indices .into_iter() @@ -347,12 +354,14 @@ impl TileBrush { index, flags: TileFlags::empty(), }, - ..Default::default() + angle: layer.user.speedup_angle, + force: layer.user.speedup_force, + max_speed: layer.user.speedup_max_speed, }) .collect(), ) } - EditorPhysicsLayer::Switch(_) => { + EditorPhysicsLayer::Switch(layer) => { MapTileLayerPhysicsTiles::Switch( tile_indices .into_iter() @@ -362,7 +371,7 @@ impl TileBrush { flags: TileFlags::empty(), }, number: physics_group_editor.active_switch, - ..Default::default() + delay: layer.user.switch_delay, }) .collect(), ) @@ -380,11 +389,20 @@ impl TileBrush { .collect(), ), }), - entities_container - .get_or_default::(&"default".try_into().unwrap()) - // TODO: - .get_or_default("ddnet") - .clone(), + { + let physics = entities_container + .get_or_default::( + &"default".try_into().unwrap(), + ); + if matches!(layer, EditorPhysicsLayer::Speedup(_)) { + physics.speedup.clone() + } else { + physics + // TODO: + .get_or_default("ddnet") + .clone() + } + }, ), EditorLayerUnionRef::Design { layer, .. } => { let EditorLayer::Tile(layer) = layer else { @@ -580,11 +598,20 @@ impl TileBrush { )) } }), - entities_container - .get_or_default::(&"default".try_into().unwrap()) - // TODO: - .get_or_default("ddnet") - .clone(), + { + let physics = entities_container + .get_or_default::( + &"default".try_into().unwrap(), + ); + if matches!(layer, EditorPhysicsLayer::Speedup(_)) { + physics.speedup.clone() + } else { + physics + // TODO: + .get_or_default("ddnet") + .clone() + } + }, ), EditorLayerUnionRef::Design { layer, .. } => { let EditorLayer::Tile(layer) = layer else { @@ -1642,12 +1669,16 @@ impl TileBrush { tl_y + ui_canvas.height() * size_ratio_y, ); let texture = match layer.as_ref().unwrap() { - EditorLayerUnionRef::Physics { .. } => { - entities_container - .get_or_default::(&"default".try_into().unwrap()) - // TODO: - .get_or_default("ddnet") - } + EditorLayerUnionRef::Physics { layer, .. } => match layer { + EditorPhysicsLayer::Arbitrary(_) | EditorPhysicsLayer::Game(_) => { + &self.tile_picker.physics_overlay.game + } + EditorPhysicsLayer::Front(_) => &self.tile_picker.physics_overlay.front, + EditorPhysicsLayer::Tele(_) => &self.tile_picker.physics_overlay.tele, + EditorPhysicsLayer::Speedup(_) => &self.tile_picker.physics_overlay.speedup, + EditorPhysicsLayer::Switch(_) => &self.tile_picker.physics_overlay.switch, + EditorPhysicsLayer::Tune(_) => &self.tile_picker.physics_overlay.tune, + }, EditorLayerUnionRef::Design { layer, .. } => match layer { EditorLayer::Tile(layer) => layer .layer diff --git a/game/editor/src/ui/animation_panel/panel.rs b/game/editor/src/ui/animation_panel/panel.rs index f30c057e..fd13d98c 100644 --- a/game/editor/src/ui/animation_panel/panel.rs +++ b/game/editor/src/ui/animation_panel/panel.rs @@ -32,7 +32,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st let mut panel = egui::TopBottomPanel::bottom("animations_panel") .resizable(true) .height_range(300.0..=600.0); - panel = panel.default_height(200.0); + panel = panel.default_height(300.0); // if anim panel is open, and quads/sounds are selected // they basically automatically select their active animations diff --git a/game/editor/src/ui/bottom_panel/panel.rs b/game/editor/src/ui/bottom_panel/panel.rs index 2e8cef92..9d91fd45 100644 --- a/game/editor/src/ui/bottom_panel/panel.rs +++ b/game/editor/src/ui/bottom_panel/panel.rs @@ -1,4 +1,4 @@ -use egui::{text::LayoutJob, Color32}; +use egui::{text::LayoutJob, Button, Color32}; use egui_extras::Size; use math::math::vector::vec2; use ui_base::types::{UiRenderPipe, UiState}; @@ -38,7 +38,12 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st editor_tab.map.user.ui_values.animations_panel_open = !editor_tab.map.user.ui_values.animations_panel_open; } - if ui.button("Server settings").clicked() { + if ui + .add(Button::new("Server settings").selected( + editor_tab.map.user.ui_values.server_settings_open, + )) + .clicked() + { editor_tab.map.user.ui_values.server_settings_open = !editor_tab.map.user.ui_values.server_settings_open; } diff --git a/game/editor/src/ui/chat_panel/panel.rs b/game/editor/src/ui/chat_panel/panel.rs index 2724b0f4..f3ef261b 100644 --- a/game/editor/src/ui/chat_panel/panel.rs +++ b/game/editor/src/ui/chat_panel/panel.rs @@ -1,4 +1,8 @@ -use egui::{Key, KeyboardShortcut, Layout, Modifiers}; +use egui::{ + scroll_area::ScrollBarVisibility, Color32, Frame, Key, KeyboardShortcut, Layout, Modifiers, + ScrollArea, TextEdit, +}; +use egui_extras::{Size, StripBuilder}; use ui_base::types::{UiRenderPipe, UiState}; use crate::{ @@ -22,27 +26,59 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st .width_range(300.0..=600.0); panel = panel.default_width(500.0); - let mut send_chat = None; + let mut close_chat = None; let res = panel.show_inside(ui, |ui| { - ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| { - let inp = ui.text_edit_singleline(&mut chat_state.msg); - if inp.lost_focus() { - send_chat = Some(std::mem::take(&mut chat_state.msg)); - } else { - inp.request_focus(); - } - - for (author, msg) in pipe.user_data.editor_tab.client.msgs.iter() { - ui.label(msg); - ui.label(format!("{author}:")); - ui.add_space(10.0); - } - }) + StripBuilder::new(ui) + .size(Size::remainder()) + .size(Size::exact(30.0)) + .cell_layout(Layout::top_down(egui::Align::Min).with_cross_justify(true)) + .vertical(|mut strip| { + strip.cell(|ui| { + ScrollArea::vertical() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) + .stick_to_bottom(true) + .show(ui, |ui| { + // small workaround to align text to bottom + ui.add_space(ui.available_height()); + for (author, msg) in + pipe.user_data.editor_tab.client.msgs.iter().rev() + { + Frame::default() + .fill(Color32::from_black_alpha(150)) + .inner_margin(10.0) + .rounding(5.0) + .show(ui, |ui| { + ui.label(author); + ui.colored_label(Color32::WHITE, msg); + }); + ui.add_space(10.0); + } + }); + }); + strip.cell(|ui| { + ui.style_mut().wrap_mode = None; + let is_enter = ui.input(|i| i.key_pressed(Key::Enter)); + let pointer_action = ui.input(|i| { + i.pointer.any_down() + || i.pointer.any_pressed() + || i.pointer.any_released() + }); + let inp = ui.add(TextEdit::singleline(&mut chat_state.msg)); + if inp.lost_focus() && !pointer_action { + close_chat = + Some(is_enter.then(|| std::mem::take(&mut chat_state.msg))); + } else if !pointer_action { + inp.request_focus(); + } + }); + }); }); - if let Some(msg) = send_chat { - pipe.user_data.ui_events.push(EditorUiEvent::Chat { msg }); + if let Some(msg) = close_chat { + if let Some(msg) = msg { + pipe.user_data.ui_events.push(EditorUiEvent::Chat { msg }); + } map.user.ui_values.chat_panel_open = None; } diff --git a/game/editor/src/ui/close_modal.rs b/game/editor/src/ui/close_modal.rs new file mode 100644 index 00000000..4a754948 --- /dev/null +++ b/game/editor/src/ui/close_modal.rs @@ -0,0 +1,28 @@ +use egui::Modal; +use ui_base::types::UiRenderPipe; + +use super::user_data::{EditorModalDialogMode, EditorUiEvent, UserData}; + +pub fn render(ui: &egui::Ui, pipe: &mut UiRenderPipe) { + if let EditorModalDialogMode::CloseEditor = pipe.user_data.modal_dialog_mode { + Modal::new("close-tab-confirm".into()).show(ui.ctx(), |ui| { + ui.label("There are still unsaved tabs in the editor."); + ui.horizontal(|ui| { + if ui.button("Save all & close").clicked() { + pipe.user_data + .ui_events + .push(EditorUiEvent::SaveAllAndClose); + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + if ui.button("Close without saving").clicked() { + pipe.user_data.ui_events.push(EditorUiEvent::ForceClose); + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + if ui.button("Cancel").clicked() { + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + }); + }); + *pipe.user_data.pointer_is_used = true; + } +} diff --git a/game/editor/src/ui/main_frame.rs b/game/editor/src/ui/main_frame.rs index c25e76aa..ccc07c22 100644 --- a/game/editor/src/ui/main_frame.rs +++ b/game/editor/src/ui/main_frame.rs @@ -39,6 +39,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_state: &m super::top_toolbar::toolbar::render(ui, &mut pipe, ui_state); super::bottom_panel::panel::render(ui, &mut pipe, ui_state); super::animation_panel::panel::render(ui, &mut pipe, ui_state); + super::server_settings::panel::render(ui, &mut pipe, ui_state); super::group_and_layer::group_props::render(ui, &mut pipe, ui_state); super::group_and_layer::layer_props::render(ui, &mut pipe, ui_state); super::group_and_layer::quad_props::render(ui, &mut pipe, ui_state); @@ -79,6 +80,8 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_state: &m } } + super::close_modal::render(ui, pipe); + *pipe.user_data.pointer_is_used |= ui.memory(|i| i.any_popup_open()); *pipe.user_data.unused_rect = Some(ui.available_rect_before_wrap()); diff --git a/game/editor/src/ui/mod.rs b/game/editor/src/ui/mod.rs index 1691f569..1c8f7f52 100644 --- a/game/editor/src/ui/mod.rs +++ b/game/editor/src/ui/mod.rs @@ -3,12 +3,14 @@ pub mod auto_mapper; pub mod auto_saver; pub mod bottom_panel; pub mod chat_panel; +pub mod close_modal; pub mod dbg_panel; pub mod group_and_layer; pub mod left_panel; pub mod main_frame; pub mod mapper_cursors; pub mod page; +pub mod server_settings; pub mod top_menu; pub mod top_tabs; pub mod top_toolbar; diff --git a/game/editor/src/ui/server_settings/mod.rs b/game/editor/src/ui/server_settings/mod.rs new file mode 100644 index 00000000..7a6b9583 --- /dev/null +++ b/game/editor/src/ui/server_settings/mod.rs @@ -0,0 +1 @@ +pub mod panel; diff --git a/game/editor/src/ui/server_settings/panel.rs b/game/editor/src/ui/server_settings/panel.rs new file mode 100644 index 00000000..a4b928b2 --- /dev/null +++ b/game/editor/src/ui/server_settings/panel.rs @@ -0,0 +1,109 @@ +use std::collections::BTreeMap; + +use egui::{Button, Layout, ScrollArea, UiBuilder}; +use ui_base::{ + components::clearable_edit_field::clearable_edit_field, + types::{UiRenderPipe, UiState}, +}; + +use crate::{ + actions::actions::{ActSetCommands, EditorAction}, + ui::user_data::UserDataWithTab, +}; + +pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_state: &mut UiState) { + let tab = &mut pipe.user_data.editor_tab; + let map = &mut tab.map; + if !map.user.ui_values.server_settings_open { + return; + } + + let res = { + let mut panel = egui::TopBottomPanel::bottom("server_settings_panel") + .resizable(true) + .height_range(300.0..=600.0); + panel = panel.default_height(300.0); + + Some(panel.show_inside(ui, |ui| { + ui.allocate_new_ui( + UiBuilder::new().max_rect(ui.available_rect_before_wrap()), + |ui| { + ui.horizontal(|ui| { + clearable_edit_field( + ui, + &mut map.config.user.cmd_string, + Some(200.0), + None, + ); + + if let Some((cmd, args)) = ui + .button("\u{f0fe}") + .clicked() + .then_some(map.config.user.cmd_string.split_once(" ")) + .flatten() + { + let old_commands = map.config.def.commands.clone(); + let mut new_commands = map.config.def.commands.clone(); + new_commands.insert(cmd.to_string(), args.to_string()); + tab.client.execute( + EditorAction::SetCommands(ActSetCommands { + old_commands, + new_commands, + }), + Some("server-commands"), + ); + + map.config.user.cmd_string.clear(); + } + }); + ScrollArea::vertical().show(ui, |ui| { + ui.vertical(|ui| { + let cmds: BTreeMap<_, _> = + map.config.def.commands.clone().into_iter().collect(); + for (index, (cmd_name, args)) in cmds.iter().enumerate() { + ui.with_layout(Layout::right_to_left(egui::Align::Min), |ui| { + // trash can icon + if ui.button("\u{f1f8}").clicked() { + let old_commands = map.config.def.commands.clone(); + let mut new_commands = map.config.def.commands.clone(); + new_commands.remove(cmd_name); + tab.client.execute( + EditorAction::SetCommands(ActSetCommands { + old_commands, + new_commands, + }), + Some("server-commands"), + ); + } + + ui.with_layout( + Layout::left_to_right(egui::Align::Min) + .with_main_justify(true), + |ui| { + if ui + .add( + Button::new(format!("{} {}", cmd_name, args)) + .selected( + Some(index) + == map.config.user.selected_cmd, + ), + ) + .clicked() + { + map.config.user.selected_cmd = Some(index); + } + }, + ); + }); + } + }); + }); + }, + ) + })) + }; + + if let Some(res) = res { + ui_state.add_blur_rect(res.response.rect, 0.0); + } +} diff --git a/game/editor/src/ui/top_menu/menu.rs b/game/editor/src/ui/top_menu/menu.rs index 77b4a673..60e4bb9a 100644 --- a/game/editor/src/ui/top_menu/menu.rs +++ b/game/editor/src/ui/top_menu/menu.rs @@ -226,6 +226,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { mode: EditorMenuHostDialogMode::SelectMap { file_dialog }, } = menu_dialog_mode { + *pipe.user_data.pointer_is_used = true; if file_dialog.state() == DialogState::Open { let mode = file_dialog.mode(); if let Some(selected) = file_dialog.update(ui.ctx()).picked() { @@ -248,6 +249,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { .push(EditorUiEvent::SaveFile { name: selected }); } } + *menu_dialog_mode = EditorMenuDialogMode::None; } else if let EditorMenuDialogMode::Host { mode } = menu_dialog_mode { let (cert, private_key) = create_certifified_keys(); @@ -264,6 +266,8 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { )); } } + } else { + *menu_dialog_mode = EditorMenuDialogMode::None; } } @@ -271,6 +275,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { mode: EditorMenuHostDialogMode::HostNetworkOptions(mode), } = menu_dialog_mode { + *pipe.user_data.pointer_is_used = true; let EditorMenuHostNetworkOptions { port, password, @@ -375,6 +380,7 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { color, }) = menu_dialog_mode { + *pipe.user_data.pointer_is_used = true; let window = egui::Window::new("Join map network options") .resizable(false) .collapsible(false); diff --git a/game/editor/src/ui/top_tabs/main_frame.rs b/game/editor/src/ui/top_tabs/main_frame.rs index 8b79a6f2..48584e27 100644 --- a/game/editor/src/ui/top_tabs/main_frame.rs +++ b/game/editor/src/ui/top_tabs/main_frame.rs @@ -1,7 +1,7 @@ -use egui::{Button, Grid}; +use egui::{text::LayoutJob, Button, Color32, FontId, Grid, Modal, WidgetText}; use ui_base::types::UiRenderPipe; -use crate::ui::user_data::UserData; +use crate::ui::user_data::{EditorModalDialogMode, EditorUiEvent, UserData}; pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { let style = ui.style(); @@ -20,6 +20,30 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { } else { tab_name.clone() }; + let tab_display_name: WidgetText = if tab.client.should_save { + let mut job = LayoutJob::default(); + job.append( + "\u{f192}", + 0.0, + egui::TextFormat { + font_id: FontId::proportional(7.0), + valign: egui::Align::Center, + color: Color32::LIGHT_GRAY, + ..Default::default() + }, + ); + job.append( + &tab_display_name, + 8.0, + egui::TextFormat { + color: Color32::LIGHT_GRAY, + ..Default::default() + }, + ); + job.into() + } else { + tab_display_name.into() + }; let mut btn = ui.add( Button::new(tab_display_name) .selected(pipe.user_data.editor_tabs.active_tab == tab_name), @@ -48,12 +72,41 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe) { *pipe.user_data.editor_tabs.active_tab = tab_name.clone(); } if ui.add(Button::new("\u{f00d}")).clicked() { - remove_tab = Some(tab_name.clone()); + remove_tab = Some((tab_name.clone(), tab.client.should_save)); } ui.add_space(10.0); } - if let Some(tab) = remove_tab { - pipe.user_data.editor_tabs.tabs.remove(&tab); + + if let Some((tab, should_save)) = remove_tab { + if !should_save { + pipe.user_data.editor_tabs.tabs.remove(&tab); + } else { + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::CloseTab { tab }; + } + } + if let EditorModalDialogMode::CloseTab { tab } = pipe.user_data.modal_dialog_mode { + let tab = tab.clone(); + Modal::new("close-tab-confirm".into()).show(ui.ctx(), |ui| { + ui.label( + "You are about to close this editor tab, while the map is not saved.", + ); + ui.horizontal(|ui| { + if ui.button("Save & close").clicked() { + pipe.user_data + .ui_events + .push(EditorUiEvent::SaveMapAndClose { tab: tab.clone() }); + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + if ui.button("Close without saving").clicked() { + pipe.user_data.editor_tabs.tabs.remove(&tab); + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + if ui.button("Cancel").clicked() { + *pipe.user_data.modal_dialog_mode = EditorModalDialogMode::None; + } + }); + }); + *pipe.user_data.pointer_is_used = true; } }) }); diff --git a/game/editor/src/ui/top_toolbar/mod.rs b/game/editor/src/ui/top_toolbar/mod.rs index 3b2125ea..70a85f62 100644 --- a/game/editor/src/ui/top_toolbar/mod.rs +++ b/game/editor/src/ui/top_toolbar/mod.rs @@ -1,3 +1,4 @@ +pub mod speedup; pub mod switch; pub mod tele; pub mod tile_mirror; diff --git a/game/editor/src/ui/top_toolbar/speedup.rs b/game/editor/src/ui/top_toolbar/speedup.rs new file mode 100644 index 00000000..032e8e50 --- /dev/null +++ b/game/editor/src/ui/top_toolbar/speedup.rs @@ -0,0 +1,49 @@ +use egui::DragValue; +use ui_base::types::{UiRenderPipe, UiState}; + +use crate::{ + map::{EditorLayerUnionRefMut, EditorMapGroupsInterface, EditorPhysicsLayer}, + ui::user_data::UserDataWithTab, +}; + +pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_state: &mut UiState) { + let map = &mut pipe.user_data.editor_tab.map; + let Some(EditorLayerUnionRefMut::Physics { + layer: EditorPhysicsLayer::Speedup(layer), + .. + }) = map.groups.active_layer_mut() + else { + return; + }; + let style = ui.style(); + let height = style.spacing.interact_size.y + style.spacing.item_spacing.y; + + let res = egui::TopBottomPanel::top("top_toolbar_speedup_extra") + .resizable(false) + .default_height(height) + .height_range(height..=height) + .show_inside(ui, |ui| { + egui::ScrollArea::horizontal().show(ui, |ui| { + ui.horizontal(|ui| { + ui.add( + DragValue::new(&mut layer.user.speedup_force) + .update_while_editing(false) + .prefix("Force: "), + ); + ui.add( + DragValue::new(&mut layer.user.speedup_angle) + .range(0..=359) + .update_while_editing(false) + .prefix("Angle: "), + ); + ui.add( + DragValue::new(&mut layer.user.speedup_max_speed) + .update_while_editing(false) + .prefix("Max speed: "), + ); + }); + }); + }); + + ui_state.add_blur_rect(res.response.rect, 0.0); +} diff --git a/game/editor/src/ui/top_toolbar/switch.rs b/game/editor/src/ui/top_toolbar/switch.rs index 6454c26a..dea420d9 100644 --- a/game/editor/src/ui/top_toolbar/switch.rs +++ b/game/editor/src/ui/top_toolbar/switch.rs @@ -137,6 +137,12 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st } layer.user.context_menu_open = context_menu_open; + ui.add( + DragValue::new(&mut layer.user.switch_delay) + .update_while_editing(false) + .prefix("Delay: "), + ); + map.groups.physics.user.active_switch = active_switch; if prev_switch != map.groups.physics.user.active_switch { // recheck used diff --git a/game/editor/src/ui/top_toolbar/toolbar.rs b/game/editor/src/ui/top_toolbar/toolbar.rs index 73df99dd..9c1cca42 100644 --- a/game/editor/src/ui/top_toolbar/toolbar.rs +++ b/game/editor/src/ui/top_toolbar/toolbar.rs @@ -399,5 +399,6 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st super::tune::render(ui, pipe, ui_state); super::switch::render(ui, pipe, ui_state); + super::speedup::render(ui, pipe, ui_state); super::tele::render(ui, pipe, ui_state); } diff --git a/game/editor/src/ui/top_toolbar/tune.rs b/game/editor/src/ui/top_toolbar/tune.rs index c5ee4652..2b2d8d5f 100644 --- a/game/editor/src/ui/top_toolbar/tune.rs +++ b/game/editor/src/ui/top_toolbar/tune.rs @@ -120,7 +120,10 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st old_tunes: old_zones, new_tunes: tune.extra.clone(), }), - Some("tune_zone_change_zones"), + Some(&format!( + "tune_zone_change_zones-{}", + active_tune + )), ); } tune.name = tune_name; @@ -144,46 +147,79 @@ pub fn render(ui: &mut egui::Ui, pipe: &mut UiRenderPipe, ui_st } layer.user.context_menu_open = context_menu_open; - ui.menu_button("tunes", |ui| { - let tune = layer - .user - .number_extra - .entry(active_tune) - .or_insert_with(Default::default); - - for (tune, val) in tune.extra.iter_mut() { + let tune = layer + .user + .number_extra + .entry(active_tune) + .or_insert_with(Default::default); + ui.menu_button( + format!( + "Tunes of {}", + if tune.name.is_empty() { + active_tune.to_string() + } else { + tune.name.clone() + }, + ), + |ui| { + let tunes_clone = tune.extra.clone(); + for (cmd_name, val) in tunes_clone.iter() { + ui.horizontal(|ui| { + ui.label(format!("Command: {} {}", cmd_name, val)); + if ui.button("\u{f1f8}").clicked() { + let old_tunes = tune.extra.clone(); + tune.extra.remove(cmd_name); + pipe.user_data.editor_tab.client.execute( + EditorAction::ChangeTuneZone(ActChangeTuneZone { + index: active_tune, + old_name: tune.name.clone(), + new_name: tune.name.clone(), + old_tunes, + new_tunes: tune.extra.clone(), + }), + Some(&format!( + "tune_zone_change_zones-{}", + active_tune + )), + ); + } + }); + } + let val = &mut layer.user.number_extra_text; + ui.add_space(10.0); + ui.separator(); + ui.label("Add commands"); ui.horizontal(|ui| { - ui.label(tune); + ui.label("Tune command:"); ui.text_edit_singleline(val); - ui.label("delete_icon"); - }); - } - let (name, val) = &mut layer.user.number_extra_texts; - ui.horizontal(|ui| { - ui.text_edit_singleline(name); - ui.text_edit_singleline(val); - if ui.button("add_btn").clicked() && !name.is_empty() { - tune.extra.insert(name.clone(), val.clone()); + if ui.button("\u{f0fe}").clicked() && !val.is_empty() { + if let Some((name, val)) = val.split_once(" ") { + tune.extra.insert(name.to_string(), val.to_string()); - let (old_name, old_zones) = layer - .layer - .tune_zones - .get(&active_tune) - .map(|zone| (zone.name.clone(), zone.tunes.clone())) - .unwrap_or_default(); - pipe.user_data.editor_tab.client.execute( - EditorAction::ChangeTuneZone(ActChangeTuneZone { - index: active_tune, - old_name, - new_name: tune.name.clone(), - old_tunes: old_zones, - new_tunes: tune.extra.clone(), - }), - Some("tune_zone_change_zones"), - ); - } - }); - }); + let (old_name, old_zones) = layer + .layer + .tune_zones + .get(&active_tune) + .map(|zone| (zone.name.clone(), zone.tunes.clone())) + .unwrap_or_default(); + pipe.user_data.editor_tab.client.execute( + EditorAction::ChangeTuneZone(ActChangeTuneZone { + index: active_tune, + old_name, + new_name: tune.name.clone(), + old_tunes: old_zones, + new_tunes: tune.extra.clone(), + }), + Some(&format!( + "tune_zone_change_zones-{}", + active_tune + )), + ); + } + } + }); + }, + ); map.groups.physics.user.active_tune_zone = active_tune; if prev_tune != map.groups.physics.user.active_tune_zone { diff --git a/game/editor/src/ui/user_data.rs b/game/editor/src/ui/user_data.rs index e7b4de0d..9f35232a 100644 --- a/game/editor/src/ui/user_data.rs +++ b/game/editor/src/ui/user_data.rs @@ -4,7 +4,7 @@ use base::linked_hash_map_view::FxLinkedHashMap; use base_io::io::Io; use config::config::ConfigEngine; use ed25519_dalek::SigningKey; -use egui::InputState; +use egui::{Align2, InputState}; use egui_file_dialog::FileDialog; use graphics::{ graphics_mt::GraphicsMultiThreaded, @@ -46,6 +46,11 @@ pub enum EditorUiEvent { name: PathBuf, }, SaveCurMap, + SaveMapAndClose { + tab: String, + }, + SaveAll, + SaveAllAndClose, HostMap(Box), Join { ip_port: String, @@ -56,6 +61,7 @@ pub enum EditorUiEvent { }, Minimize, Close, + ForceClose, Undo, Redo, CursorWorldPos { @@ -106,17 +112,74 @@ pub enum EditorMenuDialogMode { } impl EditorMenuDialogMode { + fn icons(dialog: FileDialog) -> FileDialog { + dialog + .err_icon("\u{f06a}") + .device_icon("\u{f390}") + .default_file_icon("\u{f15b}") + .default_folder_icon("\u{f07c}") + .removable_device_icon("\u{f1f8}") + .labels(egui_file_dialog::FileDialogLabels { + title_select_directory: "\u{f07c} Select Folder".to_string(), + title_select_file: "\u{f07c} Open File".to_string(), + title_select_multiple: "\u{f24d} Select Multiple".to_string(), + title_save_file: "\u{f0c7} Save File".to_string(), + + cancel: "Cancel".to_string(), + overwrite: "Overwrite".to_string(), + + reload: "\u{f2f9} Reload".to_string(), + show_hidden: " Show hidden".to_string(), + show_system_files: " Show system files".to_string(), + + heading_pinned: "Pinned".to_string(), + heading_places: "Places".to_string(), + heading_devices: "Devices".to_string(), + heading_removable_devices: "Removable Devices".to_string(), + + home_dir: "\u{f015} Home".to_string(), + desktop_dir: "\u{f390} Desktop".to_string(), + documents_dir: "\u{f15c} Documents".to_string(), + downloads_dir: "\u{f0c7} Downloads".to_string(), + audio_dir: "🎵 Audio".to_string(), + pictures_dir: "\u{f03e} Pictures".to_string(), + videos_dir: "\u{f008} Videos".to_string(), + + pin_folder: "\u{f08d} Pin folder".to_string(), + unpin_folder: "\u{e68f} Unpin folder".to_string(), + + selected_directory: "Selected directory:".to_string(), + selected_file: "Selected file:".to_string(), + selected_items: "Selected items:".to_string(), + file_name: "File name:".to_string(), + file_filter_all_files: "All Files".to_string(), + + open_button: "\u{f07b} Open".to_string(), + save_button: "\u{f0c7} Save".to_string(), + cancel_button: "\u{f05e} Cancel".to_string(), + + overwrite_file_modal_text: "already exists. Do you want to overwrite it?" + .to_string(), + + err_empty_folder_name: "Name of the folder cannot be empty".to_string(), + err_empty_file_name: "The file name cannot be empty".to_string(), + err_directory_exists: "A directory with the name already exists".to_string(), + err_file_exists: "A file with the name already exists".to_string(), + }) + } + pub fn open(io: &Io) -> Self { let mut open_path = io.fs.get_save_path(); open_path.push("map/maps"); - let mut file_dialog = Box::new( + let mut file_dialog = Box::new(Self::icons( FileDialog::new() .title("Open Map File") + .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) .movable(false) .initial_directory(open_path) .default_file_name("ctf1.twmap"), - ); + )); file_dialog.pick_file(); @@ -126,13 +189,14 @@ impl EditorMenuDialogMode { let mut open_path = io.fs.get_save_path(); open_path.push("map/maps"); - let mut file_dialog = Box::new( + let mut file_dialog = Box::new(Self::icons( FileDialog::new() .title("Save Map File") + .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) .movable(false) .initial_directory(open_path) .default_file_name("ctf1.twmap"), - ); + )); file_dialog.save_file(); @@ -142,13 +206,14 @@ impl EditorMenuDialogMode { let mut open_path = io.fs.get_save_path(); open_path.push("map/maps"); - let mut file_dialog = Box::new( + let mut file_dialog = Box::new(Self::icons( FileDialog::new() .title("Map File to host") + .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) .movable(false) .initial_directory(open_path) .default_file_name("ctf1.twmap"), - ); + )); file_dialog.pick_file(); @@ -178,6 +243,13 @@ impl EditorMenuDialogMode { } } +#[derive(Debug)] +pub enum EditorModalDialogMode { + None, + CloseTab { tab: String }, + CloseEditor, +} + pub struct EditorTabsRefMut<'a> { pub tabs: &'a mut FxLinkedHashMap, pub active_tab: &'a mut String, @@ -199,6 +271,7 @@ pub struct UserData<'a> { pub input_state: &'a mut Option, pub canvas_size: &'a mut Option, pub menu_dialog_mode: &'a mut EditorMenuDialogMode, + pub modal_dialog_mode: &'a mut EditorModalDialogMode, pub tools: &'a mut Tools, pub auto_mapper: &'a mut TileLayerAutoMapper, pub pointer_is_used: &'a mut bool, diff --git a/game/game-base/src/datafile.rs b/game/game-base/src/datafile.rs index 7cd058ca..4cfff883 100644 --- a/game/game-base/src/datafile.rs +++ b/game/game-base/src/datafile.rs @@ -1849,7 +1849,7 @@ impl CDatafileWrapper { tune_zone.tunes.insert(tune_param, tune_val); None - } else { + } else if !setting.is_empty() { Some( setting .trim() @@ -1857,6 +1857,8 @@ impl CDatafileWrapper { .map(|(s1, s2)| (s1.to_string(), s2.to_string())) .unwrap_or_else(|| (setting.clone(), "".to_string())), ) + } else { + None } }) .collect(), @@ -3386,17 +3388,18 @@ impl CDatafileWrapper { global_map_settings.push(setting); } let data_offset = data_compressed_data.len() as i32; - let uncompressed_size = - global_map_settings.len() * std::mem::size_of::<[u8; 256]>(); - let compressed_data = Self::compress_data( - &global_map_settings - .into_iter() - .map(|v| v.to_vec()) - .collect::>() - .into_iter() - .flatten() - .collect::>(), - ); + let uncompressed_data = global_map_settings + .into_iter() + .filter_map(|v| { + CString::from_vec_with_nul( + v.into_iter().take_while_inclusive(|v| *v != 0).collect(), + ) + .ok() + }) + .flat_map(|s| s.as_bytes_with_nul().to_vec()) + .collect::>(); + let uncompressed_size = uncompressed_data.len(); + let compressed_data = Self::compress_data(&uncompressed_data); data_compressed_data.extend(compressed_data); let data_index = res.data_file.info.data_offsets.len(); res.data_file.info.data_offsets.push(data_offset); diff --git a/game/map/src/map/config.rs b/game/map/src/map/config.rs index ccb8f58a..2942ef74 100644 --- a/game/map/src/map/config.rs +++ b/game/map/src/map/config.rs @@ -7,6 +7,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Hiarc, Clone, Serialize, Deserialize)] pub struct Config { /// commands that can be interpreted by server or theoretically even client - /// e.g. sv_team_size 2 + /// e.g. `sv_team_size 2` pub commands: LinkedHashMap, } diff --git a/game/map/src/map/metadata.rs b/game/map/src/map/metadata.rs index 1315d44b..8cc2aed9 100644 --- a/game/map/src/map/metadata.rs +++ b/game/map/src/map/metadata.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// The meta data is not useful for the game. /// They simply exist for completeness -#[derive(Debug, Hiarc, Clone, Serialize, Deserialize)] +#[derive(Debug, Hiarc, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Metadata { pub authors: Vec, pub licenses: Vec, diff --git a/lib/graphics/src/handles/texture.rs b/lib/graphics/src/handles/texture.rs index f0c71289..4f9aeb48 100644 --- a/lib/graphics/src/handles/texture.rs +++ b/lib/graphics/src/handles/texture.rs @@ -69,7 +69,7 @@ pub mod texture { )) } - pub fn load_texture_3d_rgba_u8( + pub fn load_texture_2d_array_rgba_u8( &mut self, data: GraphicsBackendMemory, tex_name: &str,