Skip to main content

rustapi/
editor.rs

1use crate::Embedded;
2use crate::prelude::*;
3#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
4use crate::self_update::{SelfUpdateEvent, SelfUpdater};
5use codegridfx::Module;
6use rusterix::render_settings::RendererBackend;
7use rusterix::server::message::AudioCommand;
8use rusterix::{
9    PlayerCamera, Rusterix, SceneManager, SceneManagerResult, Texture, Value, ValueContainer,
10};
11use shared::rusterix_utils::*;
12use std::fs;
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::sync::mpsc::Receiver;
16#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
17use std::sync::{
18    Arc, Mutex,
19    mpsc::{Sender, channel},
20};
21
22#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
23use std::thread;
24
25pub static PREVIEW_ICON: LazyLock<RwLock<(TheRGBATile, i32)>> =
26    LazyLock::new(|| RwLock::new((TheRGBATile::default(), 0)));
27
28pub static SIDEBARMODE: LazyLock<RwLock<SidebarMode>> =
29    LazyLock::new(|| RwLock::new(SidebarMode::Region));
30pub static UNDOMANAGER: LazyLock<RwLock<UndoManager>> =
31    LazyLock::new(|| RwLock::new(UndoManager::default()));
32pub static TOOLLIST: LazyLock<RwLock<ToolList>> =
33    LazyLock::new(|| RwLock::new(ToolList::default()));
34pub static ACTIONLIST: LazyLock<RwLock<ActionList>> =
35    LazyLock::new(|| RwLock::new(ActionList::default()));
36// pub static PANELS: LazyLock<RwLock<Panels>> = LazyLock::new(|| RwLock::new(Panels::new()));
37pub static PALETTE: LazyLock<RwLock<ThePalette>> =
38    LazyLock::new(|| RwLock::new(ThePalette::default()));
39pub static RUSTERIX: LazyLock<RwLock<Rusterix>> =
40    LazyLock::new(|| RwLock::new(Rusterix::default()));
41pub static CONFIGEDITOR: LazyLock<RwLock<ConfigEditor>> =
42    LazyLock::new(|| RwLock::new(ConfigEditor::new()));
43pub static CONFIG: LazyLock<RwLock<toml::Table>> =
44    LazyLock::new(|| RwLock::new(toml::Table::default()));
45pub static EDITCAMERA: LazyLock<RwLock<EditCamera>> =
46    LazyLock::new(|| RwLock::new(EditCamera::new()));
47pub static SCENEMANAGER: LazyLock<RwLock<SceneManager>> =
48    LazyLock::new(|| RwLock::new(SceneManager::default()));
49pub static DOCKMANAGER: LazyLock<RwLock<DockManager>> =
50    LazyLock::new(|| RwLock::new(DockManager::default()));
51
52pub static CODEGRIDFX: LazyLock<RwLock<Module>> =
53    LazyLock::new(|| RwLock::new(Module::as_type(codegridfx::ModuleType::CharacterTemplate)));
54
55#[derive(Clone)]
56struct ProjectSession {
57    project: Project,
58    project_path: Option<PathBuf>,
59    undo: UndoManager,
60    dirty: bool,
61}
62
63#[derive(Serialize, Deserialize, Clone, Debug, Default)]
64struct CreatorWindowState {
65    x: Option<i32>,
66    y: Option<i32>,
67    width: Option<usize>,
68    height: Option<usize>,
69}
70
71pub struct Editor {
72    project: Project,
73    project_path: Option<PathBuf>,
74    sessions: Vec<ProjectSession>,
75    active_session: usize,
76    replace_next_project_load_in_active_tab: bool,
77    last_active_dirty: bool,
78
79    sidebar: Sidebar,
80    mapeditor: MapEditor,
81
82    server_ctx: ServerContext,
83
84    update_tracker: UpdateTracker,
85    event_receiver: Option<Receiver<TheEvent>>,
86
87    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
88    self_update_rx: Receiver<SelfUpdateEvent>,
89    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
90    self_update_tx: Sender<SelfUpdateEvent>,
91    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
92    self_updater: Arc<Mutex<SelfUpdater>>,
93
94    update_counter: usize,
95    last_processed_log_len: usize,
96
97    build_values: ValueContainer,
98    window_state: CreatorWindowState,
99}
100
101impl Editor {
102    fn log_segment_has_warning_or_error(segment: &str) -> bool {
103        segment.contains("[error]") || segment.contains("[warning]")
104    }
105
106    fn window_state_file_path() -> Option<PathBuf> {
107        let home = std::env::var("HOME").ok()?;
108        Some(
109            PathBuf::from(home)
110                .join(".eldiron")
111                .join("creator_window_state.json"),
112        )
113    }
114
115    fn load_window_state() -> CreatorWindowState {
116        if let Some(path) = Self::window_state_file_path()
117            && let Ok(data) = fs::read_to_string(path)
118            && let Ok(state) = serde_json::from_str::<CreatorWindowState>(&data)
119        {
120            return state;
121        }
122        CreatorWindowState::default()
123    }
124
125    fn save_window_state(&self) {
126        if let Some(path) = Self::window_state_file_path() {
127            if let Some(dir) = path.parent() {
128                let _ = fs::create_dir_all(dir);
129            }
130            if let Ok(json) = serde_json::to_string(&self.window_state) {
131                let _ = fs::write(path, json);
132            }
133        }
134    }
135
136    fn persist_active_region_view_state(&mut self) {
137        if let Some(region) = self.project.get_region_mut(&self.server_ctx.curr_region) {
138            match self.server_ctx.editor_view_mode {
139                EditorViewMode::Iso => {
140                    region.editing_position_iso_3d = Some(region.editing_position_3d);
141                    region.editing_look_at_iso_3d = Some(region.editing_look_at_3d);
142                    region.editing_iso_scale = Some(EDITCAMERA.read().unwrap().iso_camera.scale);
143                }
144                EditorViewMode::Orbit => {
145                    region.editing_position_orbit_3d = Some(region.editing_position_3d);
146                    region.editing_look_at_orbit_3d = Some(region.editing_look_at_3d);
147                    region.editing_orbit_distance =
148                        Some(EDITCAMERA.read().unwrap().orbit_camera.distance);
149                }
150                EditorViewMode::FirstP => {
151                    region.editing_position_firstp_3d = Some(region.editing_position_3d);
152                    region.editing_look_at_firstp_3d = Some(region.editing_look_at_3d);
153                }
154                EditorViewMode::D2 => {}
155            }
156        }
157    }
158
159    fn project_tab_title_for(
160        project: &Project,
161        project_path: &Option<PathBuf>,
162        fallback_index: usize,
163        dirty: bool,
164    ) -> String {
165        let prefix = if dirty { "* " } else { "" };
166
167        if let Some(path) = project_path
168            && let Some(stem) = path.file_stem()
169            && let Some(name) = stem.to_str()
170            && !name.is_empty()
171        {
172            return format!("{}{}", prefix, name);
173        }
174        if !project.name.is_empty() {
175            return format!("{}{}", prefix, project.name);
176        }
177
178        if project_path.is_none() {
179            return format!("{}{}", prefix, fl!("new_project"));
180        }
181
182        format!("{}Project {}", prefix, fallback_index + 1)
183    }
184
185    fn sync_active_session_from_editor(&mut self) {
186        if self.active_session >= self.sessions.len() {
187            return;
188        }
189        self.persist_active_region_view_state();
190        self.sessions[self.active_session].project = self.project.clone();
191        self.sessions[self.active_session].project_path = self.project_path.clone();
192        self.sessions[self.active_session].undo = UNDOMANAGER.read().unwrap().clone();
193        self.sessions[self.active_session].dirty = self.active_session_has_changes();
194    }
195
196    fn sync_editor_from_active_session(&mut self) {
197        if self.active_session >= self.sessions.len() {
198            return;
199        }
200        let session = self.sessions[self.active_session].clone();
201        self.project = session.project;
202        self.project_path = session.project_path;
203        *UNDOMANAGER.write().unwrap() = session.undo;
204    }
205
206    fn rebuild_project_tabs(&self, ui: &mut TheUI) {
207        if let Some(widget) = ui.get_widget("Project Tabs")
208            && let Some(tabbar) = widget.as_tabbar()
209        {
210            tabbar.clear();
211            for (index, session) in self.sessions.iter().enumerate() {
212                tabbar.add_tab(Self::project_tab_title_for(
213                    &session.project,
214                    &session.project_path,
215                    index,
216                    session.dirty,
217                ));
218            }
219            tabbar.set_selection_index(self.active_session);
220        }
221    }
222
223    fn activate_loaded_project(
224        &mut self,
225        ui: &mut TheUI,
226        ctx: &mut TheContext,
227        update_server_icons: &mut bool,
228        redraw: &mut bool,
229    ) {
230        self.update_counter = 0;
231        self.sidebar.startup = true;
232
233        if let Some(widget) = ui.get_widget("Server Time Slider") {
234            widget.set_value(TheValue::Time(self.project.time));
235        }
236
237        {
238            let mut rusterix = RUSTERIX.write().unwrap();
239            rusterix.client.set_server_time(self.project.time);
240            if rusterix.server.state == rusterix::ServerState::Running
241                && let Some(map) = self.project.get_map(&self.server_ctx)
242            {
243                rusterix.server.set_time(&map.id, self.project.time);
244            }
245        }
246
247        self.server_ctx.clear();
248        if let Some(first) = self.project.regions.first() {
249            self.server_ctx.curr_region = first.id;
250        }
251
252        self.sidebar
253            .load_from_project(ui, ctx, &mut self.server_ctx, &mut self.project);
254        self.mapeditor.load_from_project(ui, ctx, &self.project);
255        *update_server_icons = true;
256        *redraw = true;
257
258        *PALETTE.write().unwrap() = self.project.palette.clone();
259        SCENEMANAGER
260            .write()
261            .unwrap()
262            .set_palette(self.project.palette.clone());
263
264        UNDOMANAGER.read().unwrap().set_undo_state_to_ui(ctx);
265    }
266
267    fn switch_to_session(
268        &mut self,
269        index: usize,
270        ui: &mut TheUI,
271        ctx: &mut TheContext,
272        update_server_icons: &mut bool,
273        redraw: &mut bool,
274    ) {
275        if index >= self.sessions.len() {
276            self.rebuild_project_tabs(ui);
277            return;
278        }
279        if index == self.active_session {
280            self.sync_editor_from_active_session();
281            self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
282            self.rebuild_project_tabs(ui);
283            return;
284        }
285        self.sync_active_session_from_editor();
286        self.active_session = index;
287        self.sync_editor_from_active_session();
288        self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
289        self.rebuild_project_tabs(ui);
290    }
291
292    fn sanitize_loaded_project(project: &mut Project) {
293        insert_content_into_maps(project);
294
295        for r in &mut project.regions {
296            for s in &mut r.map.sectors {
297                if let Some(floor) = s.properties.get("floor_source") {
298                    s.properties.set("source", floor.clone());
299                }
300                if s.properties.contains("rect_rendering") {
301                    s.properties.set("rect", Value::Bool(true));
302                }
303                s.properties.remove("floor_source");
304                s.properties.remove("rect_rendering");
305                s.properties.remove("ceiling_source");
306            }
307        }
308
309        let mut char_names = FxHashMap::default();
310        for c in &project.characters {
311            char_names.insert(c.0, c.1.name.clone());
312        }
313        for r in &mut project.regions {
314            for c in &mut r.characters {
315                if let Some(n) = char_names.get(&c.1.character_id) {
316                    c.1.name = n.clone();
317                }
318            }
319        }
320
321        let mut item_names = FxHashMap::default();
322        for c in &project.items {
323            item_names.insert(c.0, c.1.name.clone());
324        }
325        for r in &mut project.regions {
326            for c in &mut r.items {
327                if let Some(n) = item_names.get(&c.1.item_id) {
328                    c.1.name = n.clone();
329                }
330            }
331            for (_, p) in &mut r.map.profiles {
332                p.sanitize();
333            }
334            r.map.sanitize();
335        }
336
337        for (_, screen) in &mut project.screens {
338            screen.map.sanitize();
339        }
340
341        if project.tiles.is_empty() {
342            let tiles = project.extract_tiles();
343            for (id, t) in &tiles {
344                let mut texture_array: Vec<Texture> = vec![];
345                for b in &t.buffer {
346                    let mut texture = Texture::new(
347                        b.pixels().to_vec(),
348                        b.dim().width as usize,
349                        b.dim().height as usize,
350                    );
351                    texture.generate_normals(true);
352                    texture_array.push(texture);
353                }
354                let tile = rusterix::Tile {
355                    id: t.id,
356                    role: rusterix::TileRole::from_index(t.role),
357                    textures: texture_array.clone(),
358                    module: None,
359                    blocking: t.blocking,
360                    scale: t.scale,
361                    tags: t.name.clone(),
362                };
363                project.tiles.insert(*id, tile);
364            }
365        }
366
367        for (_, tile) in project.tiles.iter_mut() {
368            for texture in &mut tile.textures {
369                texture.generate_normals(true);
370            }
371        }
372
373        for (_, character) in project.characters.iter_mut() {
374            if character.source.starts_with("class") {
375                character.source = character.module.build(false);
376                character.source_debug = character.module.build(true);
377            }
378        }
379
380        for (_, item) in project.items.iter_mut() {
381            if item.source.starts_with("class") {
382                item.source = item.module.build(false);
383                item.source_debug = item.module.build(true);
384            }
385        }
386    }
387
388    fn close_active_session(
389        &mut self,
390        ui: &mut TheUI,
391        ctx: &mut TheContext,
392        update_server_icons: &mut bool,
393        redraw: &mut bool,
394    ) {
395        if self.sessions.is_empty() {
396            return;
397        }
398
399        self.sync_active_session_from_editor();
400        self.sessions.remove(self.active_session);
401
402        if self.sessions.is_empty() {
403            let mut project = Project::default();
404            if let Some(bytes) = crate::Embedded::get("starter_project.eldiron")
405                && let Ok(project_string) = std::str::from_utf8(bytes.data.as_ref())
406                && let Ok(loaded) = serde_json::from_str(&project_string.to_string())
407            {
408                project = loaded;
409            }
410            Self::sanitize_loaded_project(&mut project);
411            self.sessions.push(ProjectSession {
412                project,
413                project_path: None,
414                undo: UndoManager::default(),
415                dirty: false,
416            });
417            self.active_session = 0;
418        } else if self.active_session >= self.sessions.len() {
419            self.active_session = self.sessions.len() - 1;
420        }
421
422        self.sync_editor_from_active_session();
423        self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
424        self.rebuild_project_tabs(ui);
425    }
426
427    fn active_session_has_changes(&self) -> bool {
428        UNDOMANAGER.read().unwrap().has_unsaved() || DOCKMANAGER.read().unwrap().has_dock_changes()
429    }
430
431    fn is_realtime_mode(&self) -> bool {
432        self.server_ctx.game_mode
433            || RUSTERIX.read().unwrap().server.state == rusterix::ServerState::Running
434    }
435
436    fn redraw_interval_ms(&self) -> u64 {
437        let config = CONFIGEDITOR.read().unwrap();
438        if self.is_realtime_mode() {
439            (1000 / config.target_fps.clamp(1, 60)) as u64
440        } else {
441            config.game_tick_ms.max(1) as u64
442        }
443    }
444
445    fn help_url_for_data_context(&self) -> String {
446        match self.server_ctx.pc {
447            ProjectContext::ProjectSettings => "docs/configuration/game".to_string(),
448            ProjectContext::GameRules | ProjectContext::GameLocales => "docs/rules".to_string(),
449            ProjectContext::GameAudioFx => "docs/audio".to_string(),
450            ProjectContext::RegionSettings(_) => "docs/building_maps/region_settings".to_string(),
451            ProjectContext::CharacterPreviewRigging(_) => "docs/characters_items/rigging".into(),
452            ProjectContext::Character(_)
453            | ProjectContext::CharacterData(_)
454            | ProjectContext::Item(_)
455            | ProjectContext::ItemData(_) => "docs/characters_items/attributes".to_string(),
456            ProjectContext::Screen(_)
457            | ProjectContext::ScreenWidget(_, _)
458            | ProjectContext::RegionCharacterInstance(_, _)
459            | ProjectContext::RegionItemInstance(_, _) => "docs/screens/widgets".to_string(),
460            _ => "docs/creator/docks/attribute_editor".to_string(),
461        }
462    }
463
464    fn help_url_for_widget_name(&self, widget_name: &str) -> Option<String> {
465        match widget_name {
466            "Tiles" | "Tilemap" | "Tile Editor Dock RGBA Layout View" | "Tile Editor Tree" => {
467                Some("docs/creator/docks/tile_picker_editor".into())
468            }
469            "DockDataEditor" | "DockDataEditorMax" | "Data" => {
470                Some(self.help_url_for_data_context())
471            }
472            "DockCodeEditor" | "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
473            "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
474            "PolyView" => {
475                if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
476                    Some("docs/building_maps/creating_2d".into())
477                } else {
478                    Some("docs/building_maps/creating_3d_maps".into())
479                }
480            }
481            name if name.starts_with("DockVisualScripting") => {
482                Some("docs/creator/docks/visual_script_editor".into())
483            }
484            name if name.starts_with("Tile Editor ") => {
485                Some("docs/creator/docks/tile_picker_editor".into())
486            }
487            _ => None,
488        }
489    }
490
491    fn help_url_for_editor_event(&self, event: &TheEvent, ui: &mut TheUI) -> Option<String> {
492        let mut clicked = false;
493        let widget_name = match event {
494            TheEvent::StateChanged(id, state) if *state == TheWidgetState::Clicked => {
495                clicked = true;
496                Some(id.name.clone())
497            }
498            TheEvent::RenderViewClicked(id, _) => {
499                clicked = true;
500                Some(id.name.clone())
501            }
502            TheEvent::TilePicked(id, _) => {
503                clicked = true;
504                Some(id.name.clone())
505            }
506            TheEvent::TileEditorClicked(id, _) => {
507                clicked = true;
508                Some(id.name.clone())
509            }
510            TheEvent::MouseDown(coord) => {
511                clicked = true;
512                ui.get_widget_at_coord(*coord).map(|w| w.id().name.clone())
513            }
514            _ => None,
515        };
516
517        if let Some(widget_name) = widget_name
518            && let Some(url) = self.help_url_for_widget_name(&widget_name)
519        {
520            return Some(url);
521        }
522
523        if clicked {
524            let dm = DOCKMANAGER.read().unwrap();
525            if dm.state != DockManagerState::Minimized {
526                return match dm.dock.as_str() {
527                    "Tiles" => Some("docs/creator/docks/tile_picker_editor".into()),
528                    "Data" => Some(self.help_url_for_data_context()),
529                    "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
530                    "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
531                    _ => None,
532                };
533            }
534        }
535        None
536    }
537}
538
539impl TheTrait for Editor {
540    fn new() -> Self
541    where
542        Self: Sized,
543    {
544        let mut project = Project::new();
545        if let Some(bytes) = crate::Embedded::get("toml/config.toml") {
546            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
547                project.config = source.to_string();
548            }
549        }
550        if let Some(bytes) = crate::Embedded::get("toml/rules.toml") {
551            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
552                project.rules = source.to_string();
553            }
554        }
555        if let Some(bytes) = crate::Embedded::get("toml/locales.toml") {
556            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
557                project.locales = source.to_string();
558            }
559        }
560        if let Some(bytes) = crate::Embedded::get("toml/audio_fx.toml") {
561            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
562                project.audio_fx = source.to_string();
563            }
564        }
565
566        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
567        let (self_update_tx, self_update_rx) = channel();
568
569        #[cfg(all(
570            not(target_arch = "wasm32"),
571            feature = "self-update",
572            not(target_os = "macos")
573        ))]
574        let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "eldiron-creator");
575        #[cfg(all(
576            not(target_arch = "wasm32"),
577            feature = "self-update",
578            target_os = "macos"
579        ))]
580        let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "Eldiron-Creator.app");
581
582        let initial_session = ProjectSession {
583            project: project.clone(),
584            project_path: None,
585            undo: UndoManager::default(),
586            dirty: false,
587        };
588
589        Self {
590            project,
591            project_path: None,
592            sessions: vec![initial_session],
593            active_session: 0,
594            replace_next_project_load_in_active_tab: false,
595            last_active_dirty: false,
596
597            sidebar: Sidebar::new(),
598            mapeditor: MapEditor::new(),
599
600            server_ctx: ServerContext::default(),
601
602            update_tracker: UpdateTracker::new(),
603            event_receiver: None,
604
605            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
606            self_update_rx,
607            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
608            self_update_tx,
609            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
610            self_updater: Arc::new(Mutex::new(self_updater)),
611
612            update_counter: 0,
613            last_processed_log_len: 0,
614
615            build_values: ValueContainer::default(),
616            window_state: Self::load_window_state(),
617        }
618    }
619
620    fn init(&mut self, _ctx: &mut TheContext) {
621        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
622        {
623            let updater = Arc::clone(&self.self_updater);
624            let tx = self.self_update_tx.clone();
625
626            thread::spawn(move || {
627                let mut updater = updater.lock().unwrap();
628
629                if let Err(err) = updater.fetch_release_list() {
630                    tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
631                        .unwrap();
632                };
633            });
634        }
635    }
636
637    fn window_title(&self) -> String {
638        "Eldiron Creator".to_string()
639    }
640
641    fn target_fps(&self) -> f64 {
642        1000.0 / self.redraw_interval_ms() as f64
643    }
644
645    fn fonts_to_load(&self) -> Vec<TheFontScript> {
646        vec![TheFontScript::Han]
647    }
648
649    fn default_window_size(&self) -> (usize, usize) {
650        (
651            self.window_state.width.unwrap_or(1200),
652            self.window_state.height.unwrap_or(720),
653        )
654    }
655
656    fn min_window_size(&self) -> (usize, usize) {
657        (1200, 720)
658    }
659
660    fn default_window_position(&self) -> Option<(i32, i32)> {
661        Some((self.window_state.x?, self.window_state.y?))
662    }
663
664    fn window_icon(&self) -> Option<(Vec<u8>, u32, u32)> {
665        if let Some(file) = Embedded::get("window_logo.png") {
666            let data = std::io::Cursor::new(file.data);
667
668            let decoder = png::Decoder::new(data);
669            if let Ok(mut reader) = decoder.read_info() {
670                if let Some(buffer_size) = reader.output_buffer_size() {
671                    let mut buf = vec![0; buffer_size];
672                    let info = reader.next_frame(&mut buf).unwrap();
673                    let bytes = &buf[..info.buffer_size()];
674
675                    Some((bytes.to_vec(), info.width, info.height))
676                } else {
677                    None
678                }
679            } else {
680                None
681            }
682        } else {
683            None
684        }
685    }
686
687    fn init_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
688        RUSTERIX.write().unwrap().client.messages_font = ctx.ui.font.clone();
689
690        // Embedded Icons
691        for file in Embedded::iter() {
692            let name = file.as_ref();
693
694            if name.ends_with(".png") {
695                if let Some(file) = Embedded::get(name) {
696                    let data = std::io::Cursor::new(file.data);
697
698                    let decoder = png::Decoder::new(data);
699                    if let Ok(mut reader) = decoder.read_info() {
700                        if let Some(buffer_size) = reader.output_buffer_size() {
701                            let mut buf = vec![0; buffer_size];
702                            let info = reader.next_frame(&mut buf).unwrap();
703                            let bytes = &buf[..info.buffer_size()];
704
705                            let mut cut_name = name.replace("icons/", "");
706                            cut_name = cut_name.replace(".png", "");
707
708                            ctx.ui.add_icon(
709                                cut_name.to_string(),
710                                TheRGBABuffer::from(bytes.to_vec(), info.width, info.height),
711                            );
712                        }
713                    }
714                }
715            }
716        }
717
718        // ---
719
720        ui.set_statusbar_name("Statusbar".to_string());
721
722        let mut top_canvas = TheCanvas::new();
723        // Internal file/edit/game menu is hidden for the Xcode staticlib wrapper
724        // where native menu handling is expected.
725        #[cfg(not(feature = "staticlib"))]
726        {
727            let mut menu_canvas = TheCanvas::new();
728            let mut menu = TheMenu::new(TheId::named("Menu"));
729
730            let mut file_menu = TheContextMenu::named(fl!("menu_file"));
731            file_menu.add(TheContextMenuItem::new_with_accel(
732                fl!("menu_new"),
733                TheId::named("New"),
734                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'n'),
735            ));
736            file_menu.add_separator();
737            file_menu.add(TheContextMenuItem::new_with_accel(
738                fl!("menu_open"),
739                TheId::named("Open"),
740                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
741            ));
742            file_menu.add(TheContextMenuItem::new_with_accel(
743                fl!("menu_close"),
744                TheId::named("Close"),
745                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'w'),
746            ));
747            file_menu.add_separator();
748            file_menu.add(TheContextMenuItem::new_with_accel(
749                fl!("menu_save"),
750                TheId::named("Save"),
751                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 's'),
752            ));
753            file_menu.add(TheContextMenuItem::new_with_accel(
754                fl!("menu_save_as"),
755                TheId::named("Save As"),
756                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'a'),
757            ));
758            let mut edit_menu = TheContextMenu::named(fl!("menu_edit"));
759            edit_menu.add(TheContextMenuItem::new_with_accel(
760                fl!("menu_undo"),
761                TheId::named("Undo"),
762                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'z'),
763            ));
764            edit_menu.add(TheContextMenuItem::new_with_accel(
765                fl!("menu_redo"),
766                TheId::named("Redo"),
767                TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'z'),
768            ));
769            edit_menu.add_separator();
770            edit_menu.add(TheContextMenuItem::new_with_accel(
771                fl!("menu_cut"),
772                TheId::named("Cut"),
773                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'x'),
774            ));
775            edit_menu.add(TheContextMenuItem::new_with_accel(
776                fl!("menu_copy"),
777                TheId::named("Copy"),
778                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'c'),
779            ));
780            edit_menu.add(TheContextMenuItem::new_with_accel(
781                fl!("menu_paste"),
782                TheId::named("Paste"),
783                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'v'),
784            ));
785            edit_menu.add_separator();
786            edit_menu.add(TheContextMenuItem::new_with_accel(
787                fl!("menu_apply_action"),
788                TheId::named("Action Apply"),
789                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
790            ));
791
792            let mut game_menu = TheContextMenu::named(fl!("game"));
793            game_menu.add(TheContextMenuItem::new_with_accel(
794                fl!("menu_play"),
795                TheId::named("Play"),
796                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
797            ));
798            game_menu.add(TheContextMenuItem::new_with_accel(
799                fl!("menu_pause"),
800                TheId::named("Pause"),
801                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
802            ));
803            game_menu.add(TheContextMenuItem::new_with_accel(
804                fl!("menu_stop"),
805                TheId::named("Stop"),
806                TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'p'),
807            ));
808            game_menu.add_separator();
809            let mut show_menu = TheContextMenu::named("Show".to_string());
810            show_menu.add(TheContextMenuItem::new(
811                "Settings".to_string(),
812                TheId::named("Show Settings"),
813            ));
814            show_menu.add(TheContextMenuItem::new(
815                "Rules".to_string(),
816                TheId::named("Show Rules"),
817            ));
818            show_menu.add(TheContextMenuItem::new(
819                "Locales".to_string(),
820                TheId::named("Show Locales"),
821            ));
822            show_menu.add(TheContextMenuItem::new(
823                "Audio FX".to_string(),
824                TheId::named("Show Audio FX"),
825            ));
826            show_menu.add(TheContextMenuItem::new(
827                "Debug Log".to_string(),
828                TheId::named("Show Debug Log"),
829            ));
830            show_menu.add(TheContextMenuItem::new(
831                "Console".to_string(),
832                TheId::named("Show Console"),
833            ));
834            game_menu.add(TheContextMenuItem::new_submenu(
835                "Show".to_string(),
836                TheId::named("Show"),
837                show_menu,
838            ));
839
840            file_menu.register_accel(ctx);
841            edit_menu.register_accel(ctx);
842            game_menu.register_accel(ctx);
843
844            menu.add_context_menu(file_menu);
845            menu.add_context_menu(edit_menu);
846            menu.add_context_menu(game_menu);
847            menu_canvas.set_widget(menu);
848            top_canvas.set_top(menu_canvas);
849        }
850
851        let mut menubar = TheMenubar::new(TheId::named("Menubar"));
852        #[cfg(feature = "staticlib")]
853        menubar.limiter_mut().set_max_height(43);
854        #[cfg(not(feature = "staticlib"))]
855        menubar.limiter_mut().set_max_height(43 + 22);
856
857        let mut logo_button = TheMenubarButton::new(TheId::named("Logo"));
858        logo_button.set_icon_name("logo".to_string());
859        logo_button.set_status_text(&fl!("status_logo_button"));
860
861        let mut open_button = TheMenubarButton::new(TheId::named("Open"));
862        open_button.set_icon_name("icon_role_load".to_string());
863        open_button.set_status_text(&fl!("status_open_button"));
864
865        let mut save_button = TheMenubarButton::new(TheId::named("Save"));
866        save_button.set_status_text(&fl!("status_save_button"));
867        save_button.set_icon_name("icon_role_save".to_string());
868
869        let mut save_as_button = TheMenubarButton::new(TheId::named("Save As"));
870        save_as_button.set_icon_name("icon_role_save_as".to_string());
871        save_as_button.set_status_text(&fl!("status_save_as_button"));
872        save_as_button.set_icon_offset(Vec2::new(2, -5));
873
874        let mut undo_button = TheMenubarButton::new(TheId::named("Undo"));
875        undo_button.set_status_text(&fl!("status_undo_button"));
876        undo_button.set_icon_name("icon_role_undo".to_string());
877
878        let mut redo_button = TheMenubarButton::new(TheId::named("Redo"));
879        redo_button.set_status_text(&fl!("status_redo_button"));
880        redo_button.set_icon_name("icon_role_redo".to_string());
881
882        let mut play_button = TheMenubarButton::new(TheId::named("Play"));
883        play_button.set_status_text(&fl!("status_play_button"));
884        play_button.set_icon_name("play".to_string());
885        //play_button.set_fixed_size(vec2i(28, 28));
886
887        let mut pause_button = TheMenubarButton::new(TheId::named("Pause"));
888        pause_button.set_status_text(&fl!("status_pause_button"));
889        pause_button.set_icon_name("play-pause".to_string());
890
891        let mut stop_button = TheMenubarButton::new(TheId::named("Stop"));
892        stop_button.set_status_text(&fl!("status_stop_button"));
893        stop_button.set_icon_name("stop-fill".to_string());
894
895        let mut input_button = TheMenubarButton::new(TheId::named("GameInput"));
896        input_button.set_status_text(&fl!("status_game_input_button"));
897        input_button.set_icon_name("keyboard".to_string());
898        input_button.set_has_state(true);
899
900        let mut time_slider = TheTimeSlider::new(TheId::named("Server Time Slider"));
901        time_slider.set_status_text(&fl!("status_time_slider"));
902        time_slider.set_continuous(true);
903        time_slider.limiter_mut().set_max_width(400);
904        time_slider.set_value(TheValue::Time(TheTime::default()));
905
906        let mut patreon_button = TheMenubarButton::new(TheId::named("Patreon"));
907        patreon_button.set_status_text(&fl!("status_patreon_button"));
908        patreon_button.set_icon_name("patreon".to_string());
909        // patreon_button.set_fixed_size(vec2i(36, 36));
910        patreon_button.set_icon_offset(Vec2::new(-4, -2));
911
912        let mut help_button = TheMenubarButton::new(TheId::named("Help"));
913        help_button.set_status_text(&fl!("status_help_button"));
914        help_button.set_icon_name("question-mark".to_string());
915        help_button.set_has_state(true);
916        // patreon_button.set_fixed_size(vec2i(36, 36));
917        help_button.set_icon_offset(Vec2::new(-2, -2));
918
919        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
920        let mut update_button = {
921            let mut button = TheMenubarButton::new(TheId::named("Update"));
922            button.set_status_text(&fl!("status_update_button"));
923            button.set_icon_name("arrows-clockwise".to_string());
924            button
925        };
926
927        let mut hlayout = TheHLayout::new(TheId::named("Menu Layout"));
928        hlayout.set_background_color(None);
929        hlayout.set_margin(Vec4::new(10, 2, 10, 1));
930        hlayout.add_widget(Box::new(logo_button));
931        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
932        hlayout.add_widget(Box::new(open_button));
933        hlayout.add_widget(Box::new(save_button));
934        hlayout.add_widget(Box::new(save_as_button));
935        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
936        hlayout.add_widget(Box::new(undo_button));
937        hlayout.add_widget(Box::new(redo_button));
938        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
939        hlayout.add_widget(Box::new(play_button));
940        hlayout.add_widget(Box::new(pause_button));
941        hlayout.add_widget(Box::new(stop_button));
942        hlayout.add_widget(Box::new(input_button));
943        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
944        hlayout.add_widget(Box::new(time_slider));
945        //hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
946
947        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
948        {
949            hlayout.add_widget(Box::new(update_button));
950            hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
951            hlayout.add_widget(Box::new(patreon_button));
952            hlayout.set_reverse_index(Some(3));
953        }
954
955        #[cfg(not(all(not(target_arch = "wasm32"), feature = "self-update")))]
956        {
957            hlayout.add_widget(Box::new(patreon_button));
958            hlayout.add_widget(Box::new(help_button));
959            hlayout.set_reverse_index(Some(2));
960        }
961
962        top_canvas.set_widget(menubar);
963        top_canvas.set_layout(hlayout);
964        ui.canvas.set_top(top_canvas);
965
966        // Sidebar
967        self.sidebar.init_ui(ui, ctx, &mut self.server_ctx);
968
969        // Docks
970        let bottom_panels = DOCKMANAGER.write().unwrap().init(ctx);
971
972        let mut editor_canvas: TheCanvas = TheCanvas::new();
973
974        let mut editor_stack = TheStackLayout::new(TheId::named("Editor Stack"));
975        let poly_canvas = self.mapeditor.init_ui(ui, ctx, &mut self.project);
976        editor_stack.add_canvas(poly_canvas);
977
978        // Add Dock Editors
979        DOCKMANAGER
980            .write()
981            .unwrap()
982            .add_editors_to_stack(&mut editor_stack, ctx);
983
984        editor_canvas.set_layout(editor_stack);
985
986        // Main V Layout
987        let mut vsplitlayout = TheSharedVLayout::new(TheId::named("Shared VLayout"));
988        vsplitlayout.add_canvas(editor_canvas);
989        vsplitlayout.add_canvas(bottom_panels);
990        vsplitlayout.set_shared_ratio(crate::DEFAULT_VLAYOUT_RATIO);
991        vsplitlayout.set_mode(TheSharedVLayoutMode::Shared);
992
993        let mut shared_canvas = TheCanvas::new();
994        shared_canvas.set_layout(vsplitlayout);
995
996        let mut tabs_canvas = TheCanvas::new();
997        let mut tabs = TheTabbar::new(TheId::named("Project Tabs"));
998        tabs.limiter_mut().set_max_height(22);
999        tabs_canvas.set_widget(tabs);
1000        shared_canvas.set_top(tabs_canvas);
1001
1002        // Tool List
1003        let mut tool_list_canvas: TheCanvas = TheCanvas::new();
1004
1005        let mut tool_list_bar_canvas = TheCanvas::new();
1006        tool_list_bar_canvas.set_widget(TheToolListBar::new(TheId::empty()));
1007        tool_list_canvas.set_top(tool_list_bar_canvas);
1008
1009        let mut v_tool_list_layout = TheVLayout::new(TheId::named("Tool List Layout"));
1010        v_tool_list_layout.limiter_mut().set_max_width(51);
1011        v_tool_list_layout.set_margin(Vec4::new(2, 2, 2, 2));
1012        v_tool_list_layout.set_padding(1);
1013
1014        TOOLLIST
1015            .write()
1016            .unwrap()
1017            .set_active_editor(&mut v_tool_list_layout, ctx);
1018
1019        tool_list_canvas.set_layout(v_tool_list_layout);
1020
1021        let mut tool_list_border_canvas = TheCanvas::new();
1022        let mut border_widget = TheIconView::new(TheId::empty());
1023        border_widget.set_border_color(Some([82, 82, 82, 255]));
1024        border_widget.limiter_mut().set_max_width(1);
1025        border_widget.limiter_mut().set_max_height(i32::MAX);
1026        tool_list_border_canvas.set_widget(border_widget);
1027
1028        tool_list_canvas.set_right(tool_list_border_canvas);
1029        shared_canvas.set_left(tool_list_canvas);
1030
1031        ui.canvas.set_center(shared_canvas);
1032
1033        let mut status_canvas = TheCanvas::new();
1034        let mut statusbar = TheStatusbar::new(TheId::named("Statusbar"));
1035        statusbar.set_text(fl!("info_welcome"));
1036        status_canvas.set_widget(statusbar);
1037
1038        ui.canvas.set_bottom(status_canvas);
1039
1040        // -
1041
1042        // ctx.ui.set_disabled("Save");
1043        // ctx.ui.set_disabled("Save As");
1044        ctx.ui.set_disabled("Undo");
1045        ctx.ui.set_disabled("Redo");
1046
1047        // Init Rusterix
1048
1049        if let Some(icon) = ctx.ui.icon("light_on") {
1050            let texture = Texture::from_rgbabuffer(icon);
1051            self.build_values.set("light_on", Value::Texture(texture));
1052        }
1053        if let Some(icon) = ctx.ui.icon("light_off") {
1054            let texture = Texture::from_rgbabuffer(icon);
1055            self.build_values.set("light_off", Value::Texture(texture));
1056        }
1057        if let Some(icon) = ctx.ui.icon("character_on") {
1058            let texture = Texture::from_rgbabuffer(icon);
1059            self.build_values
1060                .set("character_on", Value::Texture(texture));
1061        }
1062        if let Some(icon) = ctx.ui.icon("character_off") {
1063            let texture = Texture::from_rgbabuffer(icon);
1064            self.build_values
1065                .set("character_off", Value::Texture(texture));
1066        }
1067        RUSTERIX
1068            .write()
1069            .unwrap()
1070            .client
1071            .builder_d2
1072            .set_properties(&self.build_values);
1073        RUSTERIX.write().unwrap().set_d2();
1074        SCENEMANAGER
1075            .write()
1076            .unwrap()
1077            .set_apply_preview_filters(true);
1078        SCENEMANAGER.write().unwrap().startup();
1079
1080        self.event_receiver = Some(ui.add_state_listener("Main Receiver".into()));
1081        self.rebuild_project_tabs(ui);
1082    }
1083
1084    /// Set the command line arguments
1085    fn set_cmd_line_args(&mut self, args: Vec<String>, ctx: &mut TheContext) {
1086        if args.len() > 1 {
1087            let mut queued_any = false;
1088            for arg in args.iter().skip(1) {
1089                #[allow(irrefutable_let_patterns)]
1090                if let Ok(path) = PathBuf::from_str(arg) {
1091                    if !queued_any {
1092                        self.replace_next_project_load_in_active_tab = true;
1093                    }
1094                    ctx.ui.send(TheEvent::FileRequesterResult(
1095                        TheId::named("Open"),
1096                        vec![path],
1097                    ));
1098                    queued_any = true;
1099                }
1100            }
1101            if queued_any {
1102                return;
1103            }
1104        }
1105
1106        self.replace_next_project_load_in_active_tab = true;
1107        ctx.ui.send(TheEvent::StateChanged(
1108            TheId::named("New"),
1109            TheWidgetState::Clicked,
1110        ));
1111    }
1112
1113    /// Handle UI events and UI state
1114    fn update_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) -> bool {
1115        let mut redraw = false;
1116        let mut update_server_icons = false;
1117
1118        // Make sure on first startup the active tool is properly selected
1119        if self.update_counter == 0 {
1120            let mut toollist = TOOLLIST.write().unwrap();
1121            let id = toollist.get_current_tool().id().uuid;
1122
1123            toollist.set_tool(id, ui, ctx, &mut self.project, &mut self.server_ctx);
1124        }
1125
1126        // Get build results from the scene manager if any
1127        while let Some(result) = SCENEMANAGER.write().unwrap().receive() {
1128            match result {
1129                SceneManagerResult::Startup => {
1130                    println!("Scene manager has started up.");
1131                }
1132                SceneManagerResult::ProcessedHeights(coord, heights) => {
1133                    if let Some(map) = &mut self.project.get_map_mut(&self.server_ctx) {
1134                        let local = map.terrain.get_chunk_coords(coord.x, coord.y);
1135                        if let Some(chunk) = &mut map.terrain.chunks.get_mut(&local) {
1136                            chunk.processed_heights = Some(heights);
1137                        }
1138                    }
1139                }
1140                SceneManagerResult::Chunk(chunk, togo, total, billboards) => {
1141                    if togo == 0 {
1142                        self.server_ctx.background_progress = None;
1143                    } else {
1144                        self.server_ctx.background_progress = Some(format!("{togo}/{total}"));
1145                    }
1146
1147                    let mut rusterix = RUSTERIX.write().unwrap();
1148
1149                    rusterix
1150                        .scene_handler
1151                        .vm
1152                        .execute(scenevm::Atom::RemoveChunkAt {
1153                            origin: chunk.origin,
1154                        });
1155
1156                    rusterix.scene_handler.vm.execute(scenevm::Atom::AddChunk {
1157                        id: Uuid::new_v4(),
1158                        chunk: chunk,
1159                    });
1160
1161                    // Add billboards to scene_handler (indexed by GeoId)
1162                    for billboard in billboards {
1163                        rusterix
1164                            .scene_handler
1165                            .billboards
1166                            .insert(billboard.geo_id, billboard);
1167                    }
1168
1169                    ctx.ui.send(TheEvent::Custom(
1170                        TheId::named("Update Minimap"),
1171                        TheValue::Empty,
1172                    ));
1173                }
1174                SceneManagerResult::UpdatedBatch3D(coord, batch) => {
1175                    let mut rusterix = RUSTERIX.write().unwrap();
1176                    if let Some(chunk) = rusterix.client.scene.chunks.get_mut(&coord) {
1177                        chunk.terrain_batch3d = Some(batch);
1178                    }
1179                }
1180                SceneManagerResult::Clear => {
1181                    let mut rusterix = RUSTERIX.write().unwrap();
1182                    rusterix
1183                        .scene_handler
1184                        .vm
1185                        .execute(scenevm::Atom::ClearGeometry);
1186
1187                    rusterix.scene_handler.billboards.clear();
1188                }
1189                SceneManagerResult::Quit => {
1190                    println!("Scene manager has shutdown.");
1191                }
1192            }
1193        }
1194
1195        // Check for redraw (30fps) and tick updates
1196        let redraw_ms = self.redraw_interval_ms();
1197        let tick_ms = CONFIGEDITOR.read().unwrap().game_tick_ms.max(1) as u64;
1198        let (mut redraw_update, tick_update) = self.update_tracker.update(redraw_ms, tick_ms);
1199
1200        // Handle queued UI events in the same update pass so input can trigger immediate redraw work.
1201        let mut pending_events = Vec::new();
1202        if let Some(receiver) = &mut self.event_receiver {
1203            while let Ok(event) = receiver.try_recv() {
1204                pending_events.push(event);
1205            }
1206        }
1207        if !pending_events.is_empty() {
1208            redraw_update = true;
1209        }
1210
1211        if tick_update {
1212            RUSTERIX.write().unwrap().client.inc_animation_frame();
1213
1214            self.server_ctx.animation_counter = self.server_ctx.animation_counter.wrapping_add(1);
1215            // To update animated minimaps (only for docks that need it)
1216            if DOCKMANAGER
1217                .read()
1218                .unwrap()
1219                .current_dock_supports_minimap_animation()
1220            {
1221                ctx.ui.send(TheEvent::Custom(
1222                    TheId::named("Soft Update Minimap"),
1223                    TheValue::Empty,
1224                ));
1225            }
1226        }
1227
1228        if redraw_update && !self.project.regions.is_empty() {
1229            // SCENEMANAGER.write().unwrap().tick();
1230            SCENEMANAGER.write().unwrap().tick_batch(8);
1231
1232            self.build_values.set(
1233                "no_rect_geo",
1234                Value::Bool(self.server_ctx.no_rect_geo_on_map),
1235            );
1236
1237            extract_build_values_from_config(&mut self.build_values);
1238
1239            let mut messages = Vec::new();
1240            let mut says = Vec::new();
1241            let mut choices = Vec::new();
1242
1243            // Update entities when the server is running
1244            {
1245                let rusterix = &mut RUSTERIX.write().unwrap();
1246                if rusterix.server.state == rusterix::ServerState::Running {
1247                    // Send a game tick to all servers
1248                    if tick_update {
1249                        rusterix.server.system_tick();
1250                    }
1251
1252                    // Send a redraw tick to all servers
1253                    if redraw_update {
1254                        rusterix.server.redraw_tick();
1255                    }
1256
1257                    if let Some(new_region_name) = rusterix.update_server() {
1258                        rusterix.client.current_map = new_region_name;
1259                    }
1260                    if rusterix.server.log_changed {
1261                        let log_text = rusterix.server.get_log();
1262                        ui.set_widget_value("LogEdit", ctx, TheValue::Text(log_text.clone()));
1263
1264                        // Auto-open Debug Log only when new log content contains warning/error.
1265                        let mut start = if log_text.len() < self.last_processed_log_len {
1266                            0
1267                        } else {
1268                            self.last_processed_log_len
1269                        };
1270                        while start < log_text.len() && !log_text.is_char_boundary(start) {
1271                            start += 1;
1272                        }
1273                        let new_segment = &log_text[start..];
1274                        if Self::log_segment_has_warning_or_error(new_segment) {
1275                            ctx.ui.send(TheEvent::StateChanged(
1276                                TheId::named("Debug Log"),
1277                                TheWidgetState::Clicked,
1278                            ));
1279                        }
1280                        self.last_processed_log_len = log_text.len();
1281                    }
1282                    let mut refresh_visual_debug = false;
1283                    for r in &mut self.project.regions {
1284                        rusterix.server.apply_entities_items(&mut r.map);
1285
1286                        if r.id == self.server_ctx.curr_region {
1287                            refresh_visual_debug = true;
1288                            if let Some(time) = rusterix.server.get_time(&r.map.id) {
1289                                rusterix.client.set_server_time(time);
1290                                if let Some(widget) = ui.get_widget("Server Time Slider") {
1291                                    widget.set_value(TheValue::Time(rusterix.client.server_time));
1292                                }
1293                            }
1294
1295                            rusterix::tile_builder(&mut r.map, &mut rusterix.assets);
1296                            messages = rusterix.server.get_messages(&r.map.id);
1297                            says = rusterix.server.get_says(&r.map.id);
1298                            choices = rusterix.server.get_choices(&r.map.id);
1299                            for cmd in rusterix.server.get_audio_commands(&r.map.id) {
1300                                match cmd {
1301                                    AudioCommand::Play {
1302                                        name,
1303                                        bus,
1304                                        gain,
1305                                        looping,
1306                                    } => {
1307                                        rusterix.play_audio_on_bus(&name, &bus, gain, looping);
1308                                    }
1309                                    AudioCommand::ClearBus { bus } => {
1310                                        rusterix.clear_audio_bus(&bus);
1311                                    }
1312                                    AudioCommand::ClearAll => {
1313                                        rusterix.clear_all_audio();
1314                                    }
1315                                    AudioCommand::SetBusVolume { bus, volume } => {
1316                                        rusterix.set_audio_bus_volume(&bus, volume);
1317                                    }
1318                                }
1319                            }
1320                        }
1321                    }
1322                    if refresh_visual_debug {
1323                        DOCKMANAGER.write().unwrap().apply_debug_data(
1324                            ui,
1325                            ctx,
1326                            &self.project,
1327                            &self.server_ctx,
1328                            &rusterix.server.debug,
1329                        );
1330                    }
1331                }
1332            }
1333
1334            // Draw Map
1335            if let Some(render_view) = ui.get_render_view("PolyView") {
1336                let dim = *render_view.dim();
1337
1338                let buffer = render_view.render_buffer_mut();
1339                buffer.resize(dim.width, dim.height);
1340
1341                {
1342                    // If we are drawing billboard vertices in the geometry overlay, update them.
1343                    if !self.server_ctx.game_mode
1344                        && self.server_ctx.editor_view_mode != EditorViewMode::D2
1345                        && self.server_ctx.curr_map_tool_type == MapToolType::Vertex
1346                    {
1347                        TOOLLIST
1348                            .write()
1349                            .unwrap()
1350                            .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1351                    }
1352
1353                    let rusterix = &mut RUSTERIX.write().unwrap();
1354                    let is_running = rusterix.server.state == rusterix::ServerState::Running;
1355
1356                    if is_running && self.server_ctx.game_mode {
1357                        for r in &mut self.project.regions {
1358                            if r.map.name == rusterix.client.current_map {
1359                                rusterix.draw_game(&r.map, messages, says, choices);
1360                                break;
1361                            }
1362                        }
1363
1364                        rusterix
1365                            .client
1366                            .insert_game_buffer(render_view.render_buffer_mut());
1367                    } else {
1368                        if self.server_ctx.editor_view_mode != EditorViewMode::D2
1369                            && self.server_ctx.get_map_context() == MapContext::Region
1370                        {
1371                            if let Some(region) =
1372                                self.project.get_region_ctx_mut(&mut self.server_ctx)
1373                            {
1374                                let follow_player_firstp = is_running
1375                                    && self.server_ctx.editor_view_mode == EditorViewMode::FirstP;
1376
1377                                if follow_player_firstp
1378                                    && let Some(player) =
1379                                        region.map.entities.iter().find(|e| e.is_player())
1380                                {
1381                                    let orientation =
1382                                        if player.orientation.magnitude_squared() > f32::EPSILON {
1383                                            player.orientation.normalized()
1384                                        } else {
1385                                            Vec2::new(1.0, 0.0)
1386                                        };
1387
1388                                    region.editing_position_3d = Vec3::new(
1389                                        player.position.x,
1390                                        player.position.y,
1391                                        player.position.z,
1392                                    );
1393                                    region.editing_look_at_3d = Vec3::new(
1394                                        player.position.x + orientation.x,
1395                                        player.position.y,
1396                                        player.position.z + orientation.y,
1397                                    );
1398                                } else {
1399                                    EDITCAMERA
1400                                        .write()
1401                                        .unwrap()
1402                                        .update_action(region, &mut self.server_ctx);
1403                                }
1404                                EDITCAMERA.write().unwrap().update_camera(
1405                                    region,
1406                                    &mut self.server_ctx,
1407                                    rusterix,
1408                                );
1409
1410                                // Keep editor 3D running mode in sync with runtime dynamic
1411                                // overlays (characters/items/lights).
1412                                let animation_frame = rusterix.client.animation_frame;
1413                                rusterix.build_dynamics_3d(&region.map, animation_frame);
1414                                rusterix.draw_d3(
1415                                    &region.map,
1416                                    render_view.render_buffer_mut().pixels_mut(),
1417                                    dim.width as usize,
1418                                    dim.height as usize,
1419                                );
1420                            }
1421                        } else
1422                        // Draw the region map
1423                        if self.server_ctx.get_map_context() == MapContext::Region
1424                            && self.server_ctx.editing_surface.is_none()
1425                        {
1426                            if let Some(region) =
1427                                self.project.get_region(&self.server_ctx.curr_region)
1428                            {
1429                                rusterix.client.builder_d2.set_clip_rect(None);
1430                                rusterix
1431                                    .client
1432                                    .builder_d2
1433                                    .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1434                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1435                                    rusterix.client.builder_d2.set_map_hover_info(
1436                                        self.server_ctx.hover,
1437                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1438                                    );
1439                                } else {
1440                                    rusterix
1441                                        .client
1442                                        .builder_d2
1443                                        .set_map_hover_info(self.server_ctx.hover, None);
1444                                }
1445
1446                                if let Some(camera_pos) = region.map.camera_xz {
1447                                    rusterix.client.builder_d2.set_camera_info(
1448                                        Some(Vec3::new(camera_pos.x, 0.0, camera_pos.y)),
1449                                        None,
1450                                    );
1451                                }
1452
1453                                // let start_time = ctx.get_time();
1454
1455                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1456                                    // During a paste operation we use a merged map
1457
1458                                    let mut map = region.map.clone();
1459                                    if let Some(hover) = self.server_ctx.hover_cursor {
1460                                        map.paste_at_position(clipboard, hover);
1461                                    }
1462
1463                                    rusterix.set_dirty();
1464                                    // rusterix.build_scene(
1465                                    //     Vec2::new(dim.width as f32, dim.height as f32),
1466                                    //     &map,
1467                                    //     &self.build_values,
1468                                    //     self.server_ctx.game_mode,
1469                                    // );
1470                                    rusterix.apply_entities_items(
1471                                        Vec2::new(dim.width as f32, dim.height as f32),
1472                                        &map,
1473                                        &self.server_ctx.editing_surface,
1474                                        false,
1475                                    );
1476                                } else {
1477                                    // rusterix.build_scene(
1478                                    //     Vec2::new(dim.width as f32, dim.height as f32),
1479                                    //     &region.map,
1480                                    //     &self.build_values,
1481                                    //     self.server_ctx.game_mode,
1482                                    // );
1483
1484                                    if let Some(map) = self.project.get_map(&self.server_ctx) {
1485                                        rusterix.apply_entities_items(
1486                                            Vec2::new(dim.width as f32, dim.height as f32),
1487                                            map,
1488                                            &self.server_ctx.editing_surface,
1489                                            false,
1490                                        );
1491                                    }
1492                                }
1493
1494                                // Prepare the messages for the region for drawing
1495                                rusterix.process_messages(&region.map, says);
1496
1497                                // let stop_time = ctx.get_time();
1498                                //println!("{} ms", stop_time - start_time);
1499                            }
1500
1501                            if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1502                                if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
1503                                    rusterix.scene_handler.settings.backend_2d =
1504                                        RendererBackend::Raster;
1505                                    rusterix.set_d2();
1506                                }
1507                                if is_running
1508                                    && self.server_ctx.editor_view_mode == EditorViewMode::D2
1509                                {
1510                                    let animation_frame = rusterix.client.animation_frame;
1511                                    rusterix.build_dynamics_2d(map, animation_frame);
1512                                }
1513                                if self.server_ctx.editor_view_mode == EditorViewMode::D2
1514                                    && rusterix.scene_handler.vm.vm_layer_count() > 1
1515                                {
1516                                    rusterix.scene_handler.vm.set_layer_enabled(
1517                                        1,
1518                                        self.server_ctx.show_editing_geometry,
1519                                    );
1520                                }
1521                                rusterix.draw_scene(
1522                                    map,
1523                                    render_view.render_buffer_mut().pixels_mut(),
1524                                    dim.width as usize,
1525                                    dim.height as usize,
1526                                );
1527                            }
1528                        } else if self.server_ctx.get_map_context() == MapContext::Region
1529                            && self.server_ctx.editing_surface.is_some()
1530                        {
1531                            rusterix
1532                                .client
1533                                .builder_d2
1534                                .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1535                            if let Some(profile) = self.project.get_map_mut(&self.server_ctx) {
1536                                if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1537                                    // Profile editor relies on 2D overlay guides.
1538                                    rusterix.scene_handler.vm.set_layer_enabled(1, true);
1539                                }
1540                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1541                                    rusterix.client.builder_d2.set_map_hover_info(
1542                                        self.server_ctx.hover,
1543                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1544                                    );
1545                                } else {
1546                                    rusterix
1547                                        .client
1548                                        .builder_d2
1549                                        .set_map_hover_info(self.server_ctx.hover, None);
1550                                }
1551
1552                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1553                                    // During a paste operation we use a merged map
1554                                    let mut map = profile.clone();
1555                                    if let Some(hover) = self.server_ctx.hover_cursor {
1556                                        map.paste_at_position(clipboard, hover);
1557                                    }
1558                                    rusterix.set_dirty();
1559                                    rusterix.build_custom_scene_d2(
1560                                        Vec2::new(dim.width as f32, dim.height as f32),
1561                                        &map,
1562                                        &self.build_values,
1563                                        &self.server_ctx.editing_surface,
1564                                        true,
1565                                    );
1566                                    rusterix.draw_custom_d2(
1567                                        &map,
1568                                        render_view.render_buffer_mut().pixels_mut(),
1569                                        dim.width as usize,
1570                                        dim.height as usize,
1571                                    );
1572                                } else {
1573                                    rusterix.build_custom_scene_d2(
1574                                        Vec2::new(dim.width as f32, dim.height as f32),
1575                                        profile,
1576                                        &self.build_values,
1577                                        &self.server_ctx.editing_surface,
1578                                        true,
1579                                    );
1580                                    rusterix.draw_custom_d2(
1581                                        profile,
1582                                        render_view.render_buffer_mut().pixels_mut(),
1583                                        dim.width as usize,
1584                                        dim.height as usize,
1585                                    );
1586                                }
1587                            }
1588                        } else
1589                        // Draw the screen / character / item map
1590                        if self.server_ctx.get_map_context() == MapContext::Character
1591                            || self.server_ctx.get_map_context() == MapContext::Item
1592                            || self.server_ctx.get_map_context() == MapContext::Screen
1593                        {
1594                            rusterix
1595                                .client
1596                                .builder_d2
1597                                .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1598                            if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1599                                if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1600                                    // Screen/character/item overlays should respect toggle.
1601                                    rusterix.scene_handler.vm.set_layer_enabled(
1602                                        1,
1603                                        self.server_ctx.show_editing_geometry,
1604                                    );
1605                                }
1606                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1607                                    rusterix.client.builder_d2.set_map_hover_info(
1608                                        self.server_ctx.hover,
1609                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1610                                    );
1611                                } else {
1612                                    rusterix
1613                                        .client
1614                                        .builder_d2
1615                                        .set_map_hover_info(self.server_ctx.hover, None);
1616                                }
1617
1618                                if self.server_ctx.get_map_context() != MapContext::Screen {
1619                                    rusterix.client.builder_d2.set_clip_rect(Some(
1620                                        rusterix::Rect {
1621                                            x: -5.0,
1622                                            y: -5.0,
1623                                            width: 10.0,
1624                                            height: 10.0,
1625                                        },
1626                                    ));
1627                                } else {
1628                                    let viewport = CONFIGEDITOR.read().unwrap().viewport;
1629                                    let grid_size = CONFIGEDITOR.read().unwrap().grid_size as f32;
1630                                    let w = viewport.x as f32 / grid_size;
1631                                    let h = viewport.y as f32 / grid_size;
1632                                    rusterix.client.builder_d2.set_clip_rect(Some(
1633                                        rusterix::Rect {
1634                                            x: -w / 2.0,
1635                                            y: -h / 2.0,
1636                                            width: w,
1637                                            height: h,
1638                                        },
1639                                    ));
1640                                }
1641
1642                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1643                                    // During a paste operation we use a merged map
1644                                    let mut map = map.clone();
1645                                    if let Some(hover) = self.server_ctx.hover_cursor {
1646                                        map.paste_at_position(clipboard, hover);
1647                                    }
1648                                    rusterix.set_dirty();
1649                                    rusterix.build_custom_scene_d2(
1650                                        Vec2::new(dim.width as f32, dim.height as f32),
1651                                        &map,
1652                                        &self.build_values,
1653                                        &self.server_ctx.editing_surface,
1654                                        true,
1655                                    );
1656                                    rusterix.draw_custom_d2(
1657                                        &map,
1658                                        render_view.render_buffer_mut().pixels_mut(),
1659                                        dim.width as usize,
1660                                        dim.height as usize,
1661                                    );
1662                                } else {
1663                                    rusterix.build_custom_scene_d2(
1664                                        Vec2::new(dim.width as f32, dim.height as f32),
1665                                        map,
1666                                        &self.build_values,
1667                                        &None,
1668                                        true,
1669                                    );
1670                                    rusterix.draw_custom_d2(
1671                                        map,
1672                                        render_view.render_buffer_mut().pixels_mut(),
1673                                        dim.width as usize,
1674                                        dim.height as usize,
1675                                    );
1676                                }
1677                            }
1678                        }
1679                    }
1680                }
1681                if !self.server_ctx.game_mode {
1682                    if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1683                        TOOLLIST.write().unwrap().draw_hud(
1684                            render_view.render_buffer_mut(),
1685                            map,
1686                            ctx,
1687                            &mut self.server_ctx,
1688                            &RUSTERIX.read().unwrap().assets,
1689                        );
1690                    }
1691                }
1692            }
1693
1694            // Draw the 3D Preview if active.
1695            // if !self.server_ctx.game_mode
1696            //     && self.server_ctx.curr_map_tool_helper == MapToolHelper::Preview
1697            // {
1698            //     if let Some(region) = self.project.get_region_ctx(&self.server_ctx) {
1699            //         PREVIEWVIEW
1700            //             .write()
1701            //             .unwrap()
1702            //             .draw(region, ui, ctx, &mut self.server_ctx);
1703            //     }
1704            // }
1705
1706            redraw = true;
1707        }
1708
1709        for event in pending_events {
1710            if self.server_ctx.help_mode
1711                && let Some(url) = self.help_url_for_editor_event(&event, ui)
1712            {
1713                ctx.ui.send(TheEvent::Custom(
1714                    TheId::named("Show Help"),
1715                    TheValue::Text(url),
1716                ));
1717                redraw = true;
1718                continue;
1719            }
1720
1721            if self.server_ctx.game_input_mode && !self.server_ctx.game_mode {
1722                // In game input mode send events to the game tool
1723                if let Some(game_tool) =
1724                    TOOLLIST.write().unwrap().get_game_tool_of_name("Game Tool")
1725                {
1726                    redraw = game_tool.handle_event(
1727                        &event,
1728                        ui,
1729                        ctx,
1730                        &mut self.project,
1731                        &mut self.server_ctx,
1732                    );
1733                }
1734            }
1735            if self
1736                .sidebar
1737                .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1738            {
1739                redraw = true;
1740            }
1741            if TOOLLIST.write().unwrap().handle_event(
1742                &event,
1743                ui,
1744                ctx,
1745                &mut self.project,
1746                &mut self.server_ctx,
1747            ) {
1748                redraw = true;
1749            }
1750            if DOCKMANAGER.write().unwrap().handle_event(
1751                &event,
1752                ui,
1753                ctx,
1754                &mut self.project,
1755                &mut self.server_ctx,
1756            ) {
1757                redraw = true;
1758            }
1759            if self
1760                .mapeditor
1761                .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1762            {
1763                redraw = true;
1764            }
1765            match event {
1766                TheEvent::IndexChanged(id, index) => {
1767                    if id.name == "Project Tabs" {
1768                        self.switch_to_session(
1769                            index,
1770                            ui,
1771                            ctx,
1772                            &mut update_server_icons,
1773                            &mut redraw,
1774                        );
1775                    }
1776                }
1777                TheEvent::CustomUndo(id, p, n) => {
1778                    if id.name == "ModuleUndo" {
1779                        let _ = (&p, &n);
1780                    }
1781                }
1782                TheEvent::Custom(id, value) => {
1783                    if id.name == "Show Help" {
1784                        if let TheValue::Text(url) = value {
1785                            _ = open::that(format!("https://www.eldiron.com/{}", url));
1786                            ctx.ui
1787                                .set_widget_state("Help".to_string(), TheWidgetState::None);
1788                            ctx.ui.clear_hover();
1789                            self.server_ctx.help_mode = false;
1790                            redraw = true;
1791                        }
1792                    }
1793                    if id.name == "Set Project Undo State" {
1794                        UNDOMANAGER.read().unwrap().set_undo_state_to_ui(ctx);
1795                    } else if id.name == "Render SceneManager Map" {
1796                        if self.server_ctx.pc.is_region() {
1797                            if self.server_ctx.editor_view_mode == EditorViewMode::D2
1798                                && self.server_ctx.profile_view.is_some()
1799                            {
1800                            } else {
1801                                crate::utils::scenemanager_render_map(
1802                                    &self.project,
1803                                    &self.server_ctx,
1804                                );
1805                                if self.server_ctx.editor_view_mode != EditorViewMode::D2 {
1806                                    TOOLLIST.write().unwrap().update_geometry_overlay_3d(
1807                                        &mut self.project,
1808                                        &mut self.server_ctx,
1809                                    );
1810                                }
1811                            }
1812                        }
1813                    } else if id.name == "Tool Changed" {
1814                        TOOLLIST
1815                            .write()
1816                            .unwrap()
1817                            .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1818                    } else if id.name == "Update Client Properties" {
1819                        let mut rusterix = RUSTERIX.write().unwrap();
1820                        self.build_values.set(
1821                            "no_rect_geo",
1822                            rusterix::Value::Bool(self.server_ctx.no_rect_geo_on_map),
1823                        );
1824                        self.build_values.set(
1825                            "editing_slice",
1826                            rusterix::Value::Float(self.server_ctx.editing_slice),
1827                        );
1828                        self.build_values.set(
1829                            "editing_slice_height",
1830                            rusterix::Value::Float(self.server_ctx.editing_slice_height),
1831                        );
1832                        rusterix
1833                            .client
1834                            .builder_d2
1835                            .set_properties(&self.build_values);
1836                        rusterix.set_dirty();
1837                    }
1838                }
1839
1840                TheEvent::DialogValueOnClose(role, name, uuid, _value) => {
1841                    if name == "Delete Character Instance ?" {
1842                        if role == TheDialogButtonRole::Delete {
1843                            if let Some(region) =
1844                                self.project.get_region_mut(&self.server_ctx.curr_region)
1845                            {
1846                                let character_id = uuid;
1847                                if region.characters.shift_remove(&character_id).is_some() {
1848                                    self.server_ctx.curr_region_content = ContentContext::Unknown;
1849                                    region.map.selected_entity_item = None;
1850                                    redraw = true;
1851
1852                                    // Remove from the content list
1853                                    if let Some(list) = ui.get_list_layout("Region Content List") {
1854                                        list.remove(TheId::named_with_id(
1855                                            "Region Content List Item",
1856                                            character_id,
1857                                        ));
1858                                        ui.select_first_list_item("Region Content List", ctx);
1859                                        ctx.ui.relayout = true;
1860                                    }
1861                                    insert_content_into_maps(&mut self.project);
1862                                    RUSTERIX.write().unwrap().set_dirty();
1863                                }
1864                            }
1865                        }
1866                    } else if name == "Delete Item Instance ?" {
1867                        if role == TheDialogButtonRole::Delete {
1868                            if let Some(region) =
1869                                self.project.get_region_mut(&self.server_ctx.curr_region)
1870                            {
1871                                let item_id = uuid;
1872                                if region.items.shift_remove(&item_id).is_some() {
1873                                    self.server_ctx.curr_region_content = ContentContext::Unknown;
1874                                    redraw = true;
1875
1876                                    // Remove from the content list
1877                                    if let Some(list) = ui.get_list_layout("Region Content List") {
1878                                        list.remove(TheId::named_with_id(
1879                                            "Region Content List Item",
1880                                            item_id,
1881                                        ));
1882                                        ui.select_first_list_item("Region Content List", ctx);
1883                                        ctx.ui.relayout = true;
1884                                    }
1885                                    insert_content_into_maps(&mut self.project);
1886                                    RUSTERIX.write().unwrap().set_dirty();
1887                                }
1888                            }
1889                        }
1890                    } else if name == "Close Project Tab" && role == TheDialogButtonRole::Accept {
1891                        self.close_active_session(ui, ctx, &mut update_server_icons, &mut redraw);
1892                    } else if name == "Update Eldiron" && role == TheDialogButtonRole::Accept {
1893                        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
1894                        {
1895                            let updater = self.self_updater.lock().unwrap();
1896
1897                            if updater.has_newer_release() {
1898                                let release = updater.latest_release().cloned().unwrap();
1899
1900                                let updater = Arc::clone(&self.self_updater);
1901                                let tx = self.self_update_tx.clone();
1902
1903                                self.self_update_tx
1904                                    .send(SelfUpdateEvent::UpdateStart(release.clone()))
1905                                    .unwrap();
1906
1907                                thread::spawn(move || {
1908                                    match updater.lock().unwrap().update_latest() {
1909                                        Ok(status) => match status {
1910                                            self_update::Status::UpToDate(_) => {
1911                                                tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
1912                                            }
1913                                            self_update::Status::Updated(_) => {
1914                                                tx.send(SelfUpdateEvent::UpdateCompleted(release))
1915                                                    .unwrap();
1916                                            }
1917                                        },
1918                                        Err(err) => {
1919                                            tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
1920                                                .unwrap();
1921                                        }
1922                                    }
1923                                });
1924                            } else {
1925                                self.self_update_tx
1926                                    .send(SelfUpdateEvent::AlreadyUpToDate)
1927                                    .unwrap();
1928                            }
1929                        }
1930                    }
1931                }
1932                TheEvent::RenderViewDrop(_id, location, drop) => {
1933                    if drop.id.name.starts_with("Shader") {
1934                        return true;
1935                    }
1936
1937                    let mut grid_pos = Vec2::zero();
1938                    let mut spawn_y = 0.0;
1939
1940                    if let Some(map) = self.project.get_map(&self.server_ctx) {
1941                        if let Some(render_view) = ui.get_render_view("PolyView") {
1942                            let dim = *render_view.dim();
1943                            grid_pos = self.server_ctx.local_to_map_cell(
1944                                Vec2::new(dim.width as f32, dim.height as f32),
1945                                Vec2::new(location.x as f32, location.y as f32),
1946                                map,
1947                                map.subdivisions,
1948                            );
1949                            grid_pos += 0.5;
1950                            let mut best_height: Option<f32> = None;
1951                            for sector in map
1952                                .sectors
1953                                .iter()
1954                                .filter(|s| s.layer.is_none() && s.is_inside(map, grid_pos))
1955                            {
1956                                let mut vertex_ids: Vec<u32> = Vec::new();
1957                                let mut sum_y = 0.0f32;
1958                                let mut count = 0usize;
1959                                for linedef_id in &sector.linedefs {
1960                                    if let Some(ld) = map.find_linedef(*linedef_id) {
1961                                        if !vertex_ids.contains(&ld.start_vertex) {
1962                                            vertex_ids.push(ld.start_vertex);
1963                                            if let Some(v) = map.get_vertex_3d(ld.start_vertex) {
1964                                                sum_y += v.y;
1965                                                count += 1;
1966                                            }
1967                                        }
1968                                        if !vertex_ids.contains(&ld.end_vertex) {
1969                                            vertex_ids.push(ld.end_vertex);
1970                                            if let Some(v) = map.get_vertex_3d(ld.end_vertex) {
1971                                                sum_y += v.y;
1972                                                count += 1;
1973                                            }
1974                                        }
1975                                    }
1976                                }
1977                                if count > 0 {
1978                                    let h = sum_y / count as f32;
1979                                    best_height = Some(best_height.map_or(h, |prev| prev.max(h)));
1980                                }
1981                            }
1982                            if let Some(h) = best_height {
1983                                spawn_y = h;
1984                            }
1985                        }
1986                    }
1987
1988                    if drop.id.name.starts_with("Character") {
1989                        let mut instance = Character {
1990                            character_id: drop.id.references,
1991                            position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
1992                            ..Default::default()
1993                        };
1994
1995                        if let Some(bytes) = crate::Embedded::get("python/instcharacter.py") {
1996                            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
1997                                instance.source = source.to_string();
1998                            }
1999                        }
2000
2001                        let mut name = "Character".to_string();
2002                        if let Some(character) = self.project.characters.get(&drop.id.references) {
2003                            name.clone_from(&character.name);
2004                        }
2005                        instance.name = name.clone();
2006
2007                        let atom = ProjectUndoAtom::AddRegionCharacterInstance(
2008                            self.server_ctx.curr_region,
2009                            instance,
2010                        );
2011                        atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
2012                        UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
2013                    } else if drop.id.name.starts_with("Item") {
2014                        let mut instance = Item {
2015                            item_id: drop.id.references,
2016                            position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
2017                            ..Default::default()
2018                        };
2019
2020                        if let Some(bytes) = crate::Embedded::get("python/institem.py") {
2021                            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
2022                                instance.source = source.to_string();
2023                            }
2024                        }
2025
2026                        let mut name = "Item".to_string();
2027                        if let Some(item) = self.project.items.get(&drop.id.references) {
2028                            name.clone_from(&item.name);
2029                        }
2030                        instance.name = name;
2031
2032                        let atom = ProjectUndoAtom::AddRegionItemInstance(
2033                            self.server_ctx.curr_region,
2034                            instance,
2035                        );
2036                        atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
2037                        UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
2038                    }
2039                }
2040                /*
2041                TheEvent::TileEditorDrop(_id, location, drop) => {
2042                    if drop.id.name.starts_with("Character") {
2043                        let mut instance = TheCodeBundle::new();
2044
2045                        let mut init = TheCodeGrid {
2046                            name: "init".into(),
2047                            ..Default::default()
2048                        };
2049                        init.insert_atom(
2050                            (0, 0),
2051                            TheCodeAtom::Set(
2052                                "@self.position".to_string(),
2053                                TheValueAssignment::Assign,
2054                            ),
2055                        );
2056                        init.insert_atom(
2057                            (1, 0),
2058                            TheCodeAtom::Assignment(TheValueAssignment::Assign),
2059                        );
2060                        init.insert_atom(
2061                            (2, 0),
2062                            TheCodeAtom::Value(TheValue::Position(Vec3::new(
2063                                location.x as f32,
2064                                0.0,
2065                                location.y as f32,
2066                            ))),
2067                        );
2068                        instance.insert_grid(init);
2069
2070                        // Set the character instance bundle, disabled for now
2071
2072                        // self.sidebar.code_editor.set_bundle(
2073                        //     instance.clone(),
2074                        //     ctx,
2075                        //     self.sidebar.width,
2076                        // );
2077
2078                        let character = Character {
2079                            id: instance.id,
2080                            character_id: drop.id.uuid,
2081                            instance,
2082                        };
2083
2084                        // Add the character instance to the region content list
2085
2086                        let mut name = "Character".to_string();
2087                        if let Some(character) = self.project.characters.get(&drop.id.uuid) {
2088                            name.clone_from(&character.name);
2089                        }
2090
2091                        if let Some(list) = ui.get_list_layout("Region Content List") {
2092                            let mut item = TheListItem::new(TheId::named_with_id(
2093                                "Region Content List Item",
2094                                character.id,
2095                            ));
2096                            item.set_text(name);
2097                            item.set_state(TheWidgetState::Selected);
2098                            item.add_value_column(100, TheValue::Text("Character".to_string()));
2099
2100                            list.deselect_all();
2101                            item.set_context_menu(Some(TheContextMenu {
2102                                items: vec![TheContextMenuItem::new(
2103                                    "Delete Character...".to_string(),
2104                                    TheId::named("Sidebar Delete Character Instance"),
2105                                )],
2106                                ..Default::default()
2107                            }));
2108                            list.add_item(item, ctx);
2109                            list.select_item(character.id, ctx, true);
2110                        }
2111
2112                        // Add the character instance to the project
2113
2114                        if let Some(region) =
2115                            self.project.get_region_mut(&self.server_ctx.curr_region)
2116                        {
2117                            region.characters.insert(character.id, character.clone());
2118                        }
2119
2120                        // Add the character instance to the server
2121
2122                        self.server_ctx.curr_character = Some(character.character_id);
2123                        self.server_ctx.curr_character_instance = Some(character.id);
2124                        self.server_ctx.curr_area = None;
2125                        //self.sidebar.deselect_all("Character List", ui);
2126
2127                        self.server_ctx.curr_grid_id =
2128                            self.server.add_character_instance_to_region(
2129                                self.server_ctx.curr_region,
2130                                character,
2131                                None,
2132                            );
2133
2134                        // Set the character instance debug info, disabled for now
2135
2136                        // if let Some(curr_grid_id) = self.server_ctx.curr_grid_id {
2137                        //     let debug_module = self.server.get_region_debug_module(
2138                        //         self.server_ctx.curr_region,
2139                        //         curr_grid_id,
2140                        //     );
2141
2142                        //     self.sidebar.code_editor.set_debug_module(debug_module, ui);
2143                        // }
2144                    } else if drop.id.name.starts_with("Item") {
2145                        let mut instance = TheCodeBundle::new();
2146
2147                        let mut init = TheCodeGrid {
2148                            name: "init".into(),
2149                            ..Default::default()
2150                        };
2151                        init.insert_atom(
2152                            (0, 0),
2153                            TheCodeAtom::Set(
2154                                "@self.position".to_string(),
2155                                TheValueAssignment::Assign,
2156                            ),
2157                        );
2158                        init.insert_atom(
2159                            (1, 0),
2160                            TheCodeAtom::Assignment(TheValueAssignment::Assign),
2161                        );
2162                        init.insert_atom(
2163                            (2, 0),
2164                            TheCodeAtom::Value(TheValue::Position(Vec3::new(
2165                                location.x as f32,
2166                                0.0,
2167                                location.y as f32,
2168                            ))),
2169                        );
2170                        instance.insert_grid(init);
2171
2172                        // Set the character instance bundle, disabled for now
2173
2174                        // self.sidebar.code_editor.set_bundle(
2175                        //     instance.clone(),
2176                        //     ctx,
2177                        //     self.sidebar.width,
2178                        // );
2179
2180                        let item = Item {
2181                            id: instance.id,
2182                            item_id: drop.id.uuid,
2183                            instance,
2184                        };
2185
2186                        // Add the item instance to the region content list
2187
2188                        let mut name = "Item".to_string();
2189                        if let Some(item) = self.project.items.get(&drop.id.uuid) {
2190                            name.clone_from(&item.name);
2191                        }
2192
2193                        if let Some(list) = ui.get_list_layout("Region Content List") {
2194                            let mut list_item = TheListItem::new(TheId::named_with_id(
2195                                "Region Content List Item",
2196                                item.id,
2197                            ));
2198                            list_item.set_text(name);
2199                            list_item.set_state(TheWidgetState::Selected);
2200                            list_item.add_value_column(100, TheValue::Text("Item".to_string()));
2201
2202                            list.deselect_all();
2203                            list.add_item(list_item, ctx);
2204                            list.select_item(item.id, ctx, true);
2205                        }
2206
2207                        // Add the item instance to the project
2208
2209                        if let Some(region) =
2210                            self.project.get_region_mut(&self.server_ctx.curr_region)
2211                        {
2212                            region.items.insert(item.id, item.clone());
2213                        }
2214
2215                        // Add the character instance to the server
2216
2217                        self.server_ctx.curr_character = None;
2218                        self.server_ctx.curr_character_instance = None;
2219                        self.server_ctx.curr_item = Some(item.item_id);
2220                        self.server_ctx.curr_item_instance = Some(item.id);
2221                        self.server_ctx.curr_area = None;
2222
2223                        self.server_ctx.curr_grid_id = self
2224                            .server
2225                            .add_item_instance_to_region(self.server_ctx.curr_region, item);
2226
2227                        // Set the character instance debug info, disabled for now
2228
2229                        // if let Some(curr_grid_id) = self.server_ctx.curr_grid_id {
2230                        //     let debug_module = self.server.get_region_debug_module(
2231                        //         self.server_ctx.curr_region,
2232                        //         curr_grid_id,
2233                        //     );
2234
2235                        //     self.sidebar.code_editor.set_debug_module(debug_module, ui);
2236                        // }
2237                    }
2238                }*/
2239                TheEvent::FileRequesterResult(id, paths) => {
2240                    // Load a palette from a file
2241                    if id.name == "Palette Import" {
2242                        for p in paths {
2243                            let contents = std::fs::read_to_string(p).unwrap_or("".to_string());
2244                            let prev = self.project.palette.clone();
2245                            self.project.palette.load_from_txt(contents);
2246                            *PALETTE.write().unwrap() = self.project.palette.clone();
2247
2248                            if let Some(palette_picker) = ui.get_palette_picker("Palette Picker") {
2249                                let index = palette_picker.index();
2250
2251                                palette_picker.set_palette(self.project.palette.clone());
2252                                if let Some(widget) = ui.get_widget("Palette Color Picker") {
2253                                    if let Some(color) = &self.project.palette[index] {
2254                                        widget.set_value(TheValue::ColorObject(color.clone()));
2255                                    }
2256                                }
2257                                if let Some(widget) = ui.get_widget("Palette Hex Edit") {
2258                                    if let Some(color) = &self.project.palette[index] {
2259                                        widget.set_value(TheValue::Text(color.to_hex()));
2260                                    }
2261                                }
2262                            }
2263                            redraw = true;
2264
2265                            let undo =
2266                                ProjectUndoAtom::PaletteEdit(prev, self.project.palette.clone());
2267                            UNDOMANAGER.write().unwrap().add_undo(undo, ctx);
2268                        }
2269                    } else
2270                    // Open
2271                    if id.name == "Open" {
2272                        for p in paths {
2273                            if let Ok(contents) = std::fs::read_to_string(&p) {
2274                                if let Ok(mut loaded) = serde_json::from_str::<Project>(&contents) {
2275                                    loaded.palette.current_index = 0;
2276                                    Self::sanitize_loaded_project(&mut loaded);
2277
2278                                    self.sync_active_session_from_editor();
2279                                    let new_index = if self.replace_next_project_load_in_active_tab
2280                                    {
2281                                        self.sessions[self.active_session] = ProjectSession {
2282                                            project: loaded,
2283                                            project_path: Some(p.clone()),
2284                                            undo: UndoManager::default(),
2285                                            dirty: false,
2286                                        };
2287                                        self.replace_next_project_load_in_active_tab = false;
2288                                        self.active_session
2289                                    } else {
2290                                        self.sessions.push(ProjectSession {
2291                                            project: loaded,
2292                                            project_path: Some(p.clone()),
2293                                            undo: UndoManager::default(),
2294                                            dirty: false,
2295                                        });
2296                                        self.sessions.len() - 1
2297                                    };
2298                                    self.switch_to_session(
2299                                        new_index,
2300                                        ui,
2301                                        ctx,
2302                                        &mut update_server_icons,
2303                                        &mut redraw,
2304                                    );
2305                                    ctx.ui.send(TheEvent::SetStatusText(
2306                                        TheId::empty(),
2307                                        "Project loaded successfully.".to_string(),
2308                                    ));
2309                                } else {
2310                                    self.replace_next_project_load_in_active_tab = false;
2311                                    ctx.ui.send(TheEvent::SetStatusText(
2312                                        TheId::empty(),
2313                                        "Unable to load project!".to_string(),
2314                                    ));
2315                                }
2316                            }
2317                        }
2318                    } else if id.name == "Save As" {
2319                        for p in paths {
2320                            self.persist_active_region_view_state();
2321                            let json = serde_json::to_string(&self.project);
2322                            if let Ok(json) = json {
2323                                if std::fs::write(p.clone(), json).is_ok() {
2324                                    self.project_path = Some(p);
2325                                    UNDOMANAGER.write().unwrap().mark_saved();
2326                                    DOCKMANAGER.write().unwrap().mark_saved();
2327                                    if self.active_session < self.sessions.len() {
2328                                        self.sessions[self.active_session].dirty = false;
2329                                    }
2330                                    self.sync_active_session_from_editor();
2331                                    self.rebuild_project_tabs(ui);
2332                                    ctx.ui.send(TheEvent::SetStatusText(
2333                                        TheId::empty(),
2334                                        "Project saved successfully.".to_string(),
2335                                    ))
2336                                } else {
2337                                    ctx.ui.send(TheEvent::SetStatusText(
2338                                        TheId::empty(),
2339                                        "Unable to save project!".to_string(),
2340                                    ))
2341                                }
2342                            }
2343                        }
2344                    }
2345                }
2346                TheEvent::StateChanged(id, state) => {
2347                    if id.name == "Help" {
2348                        self.server_ctx.help_mode = state == TheWidgetState::Clicked;
2349                    }
2350                    if id.name == "GameInput" {
2351                        self.server_ctx.game_input_mode = state == TheWidgetState::Clicked;
2352                    } else if id.name == "New" {
2353                        let mut project = Project::default();
2354                        if let Some(bytes) = crate::Embedded::get("starter_project.eldiron")
2355                            && let Ok(project_string) = std::str::from_utf8(bytes.data.as_ref())
2356                            && let Ok(loaded) = serde_json::from_str(&project_string.to_string())
2357                        {
2358                            project = loaded;
2359                        }
2360                        Self::sanitize_loaded_project(&mut project);
2361
2362                        self.sync_active_session_from_editor();
2363                        let new_index = if self.replace_next_project_load_in_active_tab {
2364                            self.sessions[self.active_session] = ProjectSession {
2365                                project,
2366                                project_path: None,
2367                                undo: UndoManager::default(),
2368                                dirty: false,
2369                            };
2370                            self.replace_next_project_load_in_active_tab = false;
2371                            self.active_session
2372                        } else {
2373                            self.sessions.push(ProjectSession {
2374                                project,
2375                                project_path: None,
2376                                undo: UndoManager::default(),
2377                                dirty: false,
2378                            });
2379                            self.sessions.len() - 1
2380                        };
2381                        self.switch_to_session(
2382                            new_index,
2383                            ui,
2384                            ctx,
2385                            &mut update_server_icons,
2386                            &mut redraw,
2387                        );
2388
2389                        ctx.ui.send(TheEvent::SetStatusText(
2390                            TheId::empty(),
2391                            "New project successfully initialized.".to_string(),
2392                        ));
2393                    } else if id.name == "Logo" {
2394                        _ = open::that("https://eldiron.com");
2395                        ctx.ui
2396                            .set_widget_state("Logo".to_string(), TheWidgetState::None);
2397                        ctx.ui.clear_hover();
2398                        redraw = true;
2399                    } else if id.name == "Patreon" {
2400                        _ = open::that("https://www.patreon.com/eldiron");
2401                        ctx.ui
2402                            .set_widget_state("Patreon".to_string(), TheWidgetState::None);
2403                        ctx.ui.clear_hover();
2404                        redraw = true;
2405                    } else if id.name == "Update" {
2406                        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2407                        {
2408                            let updater = self.self_updater.lock().unwrap();
2409
2410                            if updater.has_newer_release() {
2411                                self.self_update_tx
2412                                    .send(SelfUpdateEvent::UpdateConfirm(
2413                                        updater.latest_release().cloned().unwrap(),
2414                                    ))
2415                                    .unwrap();
2416                            } else {
2417                                if let Some(statusbar) = ui.get_widget("Statusbar") {
2418                                    statusbar
2419                                        .as_statusbar()
2420                                        .unwrap()
2421                                        .set_text(fl!("info_update_check"));
2422                                }
2423
2424                                let updater = Arc::clone(&self.self_updater);
2425                                let tx = self.self_update_tx.clone();
2426
2427                                thread::spawn(move || {
2428                                    let mut updater = updater.lock().unwrap();
2429
2430                                    match updater.fetch_release_list() {
2431                                        Ok(_) => {
2432                                            if updater.has_newer_release() {
2433                                                tx.send(SelfUpdateEvent::UpdateConfirm(
2434                                                    updater.latest_release().cloned().unwrap(),
2435                                                ))
2436                                                .unwrap();
2437                                            } else {
2438                                                tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
2439                                            }
2440                                        }
2441                                        Err(err) => {
2442                                            tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
2443                                                .unwrap();
2444                                        }
2445                                    }
2446                                });
2447                            }
2448
2449                            ctx.ui
2450                                .set_widget_state("Update".to_string(), TheWidgetState::None);
2451                            ctx.ui.clear_hover();
2452                            redraw = true;
2453                        }
2454                    } else if id.name == "Open" {
2455                        ctx.ui.open_file_requester(
2456                            TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2457                            "Open".into(),
2458                            TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2459                        );
2460                        ctx.ui
2461                            .set_widget_state("Open".to_string(), TheWidgetState::None);
2462                        ctx.ui.clear_hover();
2463                        redraw = true;
2464                    } else if id.name == "Close" {
2465                        if self.active_session_has_changes() {
2466                            let uuid = Uuid::new_v4();
2467                            let width = 380;
2468                            let height = 110;
2469
2470                            let mut canvas = TheCanvas::new();
2471                            canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2472
2473                            let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2474                            hlayout.limiter_mut().set_max_width(width);
2475
2476                            let mut text_widget =
2477                                TheText::new(TheId::named_with_id("Dialog Value", uuid));
2478                            text_widget.set_text(
2479                                "This tab has unsaved changes. Close it anyway?".to_string(),
2480                            );
2481                            text_widget.limiter_mut().set_max_width(280);
2482                            hlayout.add_widget(Box::new(text_widget));
2483
2484                            canvas.set_layout(hlayout);
2485                            ui.show_dialog(
2486                                "Close Project Tab",
2487                                canvas,
2488                                vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2489                                ctx,
2490                            );
2491                        } else {
2492                            self.close_active_session(
2493                                ui,
2494                                ctx,
2495                                &mut update_server_icons,
2496                                &mut redraw,
2497                            );
2498                        }
2499                        ctx.ui
2500                            .set_widget_state("Close".to_string(), TheWidgetState::None);
2501                        ctx.ui.clear_hover();
2502                        redraw = true;
2503                    } else if id.name == "Save" {
2504                        if let Some(path) = self.project_path.clone() {
2505                            let mut success = false;
2506                            // if let Ok(output) = postcard::to_allocvec(&self.project) {
2507                            self.persist_active_region_view_state();
2508                            if let Ok(output) = serde_json::to_string(&self.project) {
2509                                if std::fs::write(&path, output).is_ok() {
2510                                    UNDOMANAGER.write().unwrap().mark_saved();
2511                                    DOCKMANAGER.write().unwrap().mark_saved();
2512                                    if self.active_session < self.sessions.len() {
2513                                        self.sessions[self.active_session].dirty = false;
2514                                    }
2515                                    self.sync_active_session_from_editor();
2516                                    self.rebuild_project_tabs(ui);
2517                                    ctx.ui.send(TheEvent::SetStatusText(
2518                                        TheId::empty(),
2519                                        "Project saved successfully.".to_string(),
2520                                    ));
2521                                    success = true;
2522                                }
2523                            }
2524
2525                            if !success {
2526                                ctx.ui.send(TheEvent::SetStatusText(
2527                                    TheId::empty(),
2528                                    "Unable to save project!".to_string(),
2529                                ))
2530                            }
2531                        } else {
2532                            ctx.ui.send(TheEvent::StateChanged(
2533                                TheId::named("Save As"),
2534                                TheWidgetState::Clicked,
2535                            ));
2536                            ctx.ui
2537                                .set_widget_state("Save".to_string(), TheWidgetState::None);
2538                        }
2539                    } else if id.name == "Save As" {
2540                        ctx.ui.save_file_requester(
2541                            TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2542                            "Save".into(),
2543                            TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2544                        );
2545                        ctx.ui
2546                            .set_widget_state("Save As".to_string(), TheWidgetState::None);
2547                        ctx.ui.clear_hover();
2548                        redraw = true;
2549                    }
2550                    // Server
2551                    else if id.name == "Play" {
2552                        let state = RUSTERIX.read().unwrap().server.state;
2553                        if state == rusterix::ServerState::Paused {
2554                            RUSTERIX.write().unwrap().server.continue_instances();
2555                            update_server_icons = true;
2556                        } else {
2557                            if state == rusterix::ServerState::Off {
2558                                start_server(
2559                                    &mut RUSTERIX.write().unwrap(),
2560                                    &mut self.project,
2561                                    true,
2562                                );
2563                                RUSTERIX.write().unwrap().clear_say_messages();
2564                                let commands =
2565                                    setup_client(&mut RUSTERIX.write().unwrap(), &mut self.project);
2566                                RUSTERIX
2567                                    .write()
2568                                    .unwrap()
2569                                    .server
2570                                    .process_client_commands(commands);
2571                                ctx.ui.send(TheEvent::SetStatusText(
2572                                    TheId::empty(),
2573                                    "Server has been started.".to_string(),
2574                                ));
2575                                ui.set_widget_value("LogEdit", ctx, TheValue::Text(String::new()));
2576                                self.last_processed_log_len = 0;
2577                                RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2578                            }
2579                            update_server_icons = true;
2580                        }
2581                    } else if id.name == "Pause" {
2582                        let state = RUSTERIX.read().unwrap().server.state;
2583                        if state == rusterix::ServerState::Running {
2584                            RUSTERIX.write().unwrap().server.pause();
2585                            update_server_icons = true;
2586                        }
2587                    } else if id.name == "Stop" {
2588                        RUSTERIX.write().unwrap().server.stop();
2589                        RUSTERIX.write().unwrap().clear_say_messages();
2590                        RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2591                        {
2592                            let mut rusterix = RUSTERIX.write().unwrap();
2593                            rusterix.client.scene.d2_dynamic.clear();
2594                            rusterix.client.scene.d3_dynamic.clear();
2595                            rusterix.client.scene.dynamic_lights.clear();
2596                            rusterix.scene_handler.clear_runtime_overlays();
2597                            rusterix.set_dirty();
2598                        }
2599
2600                        ui.set_widget_value("InfoView", ctx, TheValue::Text("".into()));
2601                        insert_content_into_maps(&mut self.project);
2602                        update_server_icons = true;
2603
2604                        ctx.ui.send(TheEvent::Custom(
2605                            TheId::named("Render SceneManager Map"),
2606                            TheValue::Empty,
2607                        ));
2608                    } else if id.name == "Show Settings" {
2609                        set_project_context(
2610                            ctx,
2611                            ui,
2612                            &self.project,
2613                            &mut self.server_ctx,
2614                            ProjectContext::ProjectSettings,
2615                        );
2616                        redraw = true;
2617                    } else if id.name == "Show Rules" {
2618                        set_project_context(
2619                            ctx,
2620                            ui,
2621                            &self.project,
2622                            &mut self.server_ctx,
2623                            ProjectContext::GameRules,
2624                        );
2625                        redraw = true;
2626                    } else if id.name == "Show Locales" {
2627                        set_project_context(
2628                            ctx,
2629                            ui,
2630                            &self.project,
2631                            &mut self.server_ctx,
2632                            ProjectContext::GameLocales,
2633                        );
2634                        redraw = true;
2635                    } else if id.name == "Show Audio FX" {
2636                        set_project_context(
2637                            ctx,
2638                            ui,
2639                            &self.project,
2640                            &mut self.server_ctx,
2641                            ProjectContext::GameAudioFx,
2642                        );
2643                        redraw = true;
2644                    } else if id.name == "Show Debug Log" {
2645                        set_project_context(
2646                            ctx,
2647                            ui,
2648                            &self.project,
2649                            &mut self.server_ctx,
2650                            ProjectContext::DebugLog,
2651                        );
2652                        redraw = true;
2653                    } else if id.name == "Show Console" {
2654                        set_project_context(
2655                            ctx,
2656                            ui,
2657                            &self.project,
2658                            &mut self.server_ctx,
2659                            ProjectContext::Console,
2660                        );
2661                        redraw = true;
2662                    } else if id.name == "Undo" || id.name == "Redo" {
2663                        let mut refresh_action_ui = false;
2664                        if ui.focus_widget_supports_undo_redo(ctx) {
2665                            if id.name == "Undo" {
2666                                ui.undo(ctx);
2667                            } else {
2668                                ui.redo(ctx);
2669                            }
2670                        } else if DOCKMANAGER.read().unwrap().current_dock_supports_undo() {
2671                            if id.name == "Undo" {
2672                                DOCKMANAGER.write().unwrap().undo(
2673                                    ui,
2674                                    ctx,
2675                                    &mut self.project,
2676                                    &mut self.server_ctx,
2677                                );
2678                            } else {
2679                                DOCKMANAGER.write().unwrap().redo(
2680                                    ui,
2681                                    ctx,
2682                                    &mut self.project,
2683                                    &mut self.server_ctx,
2684                                );
2685                            }
2686                            refresh_action_ui = true;
2687                        } else {
2688                            let mut manager = UNDOMANAGER.write().unwrap();
2689
2690                            if id.name == "Undo" {
2691                                manager.undo(&mut self.server_ctx, &mut self.project, ui, ctx);
2692                            } else {
2693                                manager.redo(&mut self.server_ctx, &mut self.project, ui, ctx);
2694                            }
2695                            refresh_action_ui = true;
2696                        }
2697
2698                        // Keep action list and TOML params in sync only when project/dock state changed.
2699                        if refresh_action_ui {
2700                            // Drop focus to avoid stale focused text-edit state surviving toolbar rebuilds.
2701                            ctx.ui.clear_focus();
2702                            // Refresh both toolbars unconditionally.
2703                            // Dock undo/redo may not keep CODEEDITOR.active_panel in sync.
2704                            {
2705                                let mut module = CODEGRIDFX.write().unwrap();
2706                                module.clear_toolbar_settings(ui, ctx);
2707                                module.show_settings(ui, ctx);
2708                            }
2709                            ctx.ui.send(TheEvent::Custom(
2710                                TheId::named("Update Action List"),
2711                                TheValue::Empty,
2712                            ));
2713                            ctx.ui.send(TheEvent::Custom(
2714                                TheId::named("Update Action Parameters"),
2715                                TheValue::Empty,
2716                            ));
2717                        }
2718                    } else if id.name == "Cut" {
2719                        if ui.focus_widget_supports_clipboard(ctx) {
2720                            // Widget specific
2721                            ui.cut(ctx);
2722                        } else {
2723                            // Global
2724                            ctx.ui.send(TheEvent::Cut);
2725                        }
2726                    } else if id.name == "Copy" {
2727                        if ui.focus_widget_supports_clipboard(ctx) {
2728                            // Widget specific
2729                            ui.copy(ctx);
2730                        } else {
2731                            // Global
2732                            ctx.ui.send(TheEvent::Copy);
2733                        }
2734                    } else if id.name == "Paste" {
2735                        if ui.focus_widget_supports_clipboard(ctx) {
2736                            // Widget specific
2737                            ui.paste(ctx);
2738                        } else {
2739                            // Global
2740                            if let Some(value) = &ctx.ui.clipboard {
2741                                ctx.ui.send(TheEvent::Paste(
2742                                    value.clone(),
2743                                    ctx.ui.clipboard_app_type.clone(),
2744                                ));
2745                            } else {
2746                                ctx.ui.send(TheEvent::Paste(
2747                                    TheValue::Empty,
2748                                    ctx.ui.clipboard_app_type.clone(),
2749                                ));
2750                            }
2751                        }
2752                    }
2753                }
2754                TheEvent::ValueChanged(id, value) => {
2755                    if id.name == "Server Time Slider" {
2756                        if let TheValue::Time(time) = value {
2757                            self.project.time = time;
2758                            let mut rusterix = RUSTERIX.write().unwrap();
2759                            rusterix.client.set_server_time(time);
2760
2761                            if rusterix.server.state == rusterix::ServerState::Running {
2762                                if let Some(map) = self.project.get_map(&self.server_ctx) {
2763                                    rusterix.server.set_time(&map.id, time);
2764                                }
2765                            }
2766                        }
2767                    }
2768                }
2769                _ => {}
2770            }
2771        }
2772
2773        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2774        while let Ok(event) = self.self_update_rx.try_recv() {
2775            match event {
2776                SelfUpdateEvent::AlreadyUpToDate => {
2777                    let text = str!("Eldiron is already up-to-date.");
2778                    let uuid = Uuid::new_v4();
2779
2780                    let width = 300;
2781                    let height = 100;
2782
2783                    let mut canvas = TheCanvas::new();
2784                    canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2785
2786                    let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2787                    hlayout.limiter_mut().set_max_width(width);
2788
2789                    let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2790                    text_widget.set_text(text.to_string());
2791                    text_widget.limiter_mut().set_max_width(200);
2792                    hlayout.add_widget(Box::new(text_widget));
2793
2794                    canvas.set_layout(hlayout);
2795
2796                    ui.show_dialog(
2797                        "Eldiron Up-to-Date",
2798                        canvas,
2799                        vec![TheDialogButtonRole::Accept],
2800                        ctx,
2801                    );
2802                }
2803                SelfUpdateEvent::UpdateCompleted(release) => {
2804                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2805                        statusbar.as_statusbar().unwrap().set_text(format!(
2806                            "Updated to version {}. Please restart the application to enjoy the new features.",
2807                            release.version
2808                        ));
2809                    }
2810                }
2811                SelfUpdateEvent::UpdateConfirm(release) => {
2812                    let text = &format!("Update to version {}?", release.version);
2813                    let uuid = Uuid::new_v4();
2814
2815                    let width = 300;
2816                    let height = 100;
2817
2818                    let mut canvas = TheCanvas::new();
2819                    canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2820
2821                    let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2822                    hlayout.limiter_mut().set_max_width(width);
2823
2824                    let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2825                    text_widget.set_text(text.to_string());
2826                    text_widget.limiter_mut().set_max_width(200);
2827                    hlayout.add_widget(Box::new(text_widget));
2828
2829                    canvas.set_layout(hlayout);
2830
2831                    ui.show_dialog(
2832                        "Update Eldiron",
2833                        canvas,
2834                        vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2835                        ctx,
2836                    );
2837                }
2838                SelfUpdateEvent::UpdateError(err) => {
2839                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2840                        statusbar
2841                            .as_statusbar()
2842                            .unwrap()
2843                            .set_text(format!("Failed to update Eldiron: {err}"));
2844                    }
2845                }
2846                SelfUpdateEvent::UpdateStart(release) => {
2847                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2848                        statusbar
2849                            .as_statusbar()
2850                            .unwrap()
2851                            .set_text(format!("Updating to version {}...", release.version));
2852                    }
2853                }
2854            }
2855        }
2856
2857        if update_server_icons {
2858            self.update_server_state_icons(ui);
2859            redraw = true;
2860        }
2861
2862        let active_dirty = UNDOMANAGER.read().unwrap().has_unsaved()
2863            || DOCKMANAGER.read().unwrap().has_dock_changes();
2864        if self.active_session < self.sessions.len()
2865            && self.sessions[self.active_session].dirty != active_dirty
2866        {
2867            self.sessions[self.active_session].dirty = active_dirty;
2868            self.rebuild_project_tabs(ui);
2869            redraw = true;
2870        }
2871        if active_dirty != self.last_active_dirty {
2872            self.last_active_dirty = active_dirty;
2873            self.rebuild_project_tabs(ui);
2874            redraw = true;
2875        }
2876
2877        self.update_counter += 1;
2878        if self.update_counter > 2 {
2879            self.sidebar.startup = false;
2880        }
2881        redraw
2882    }
2883
2884    /// Returns true if there are changes
2885    fn has_changes(&self) -> bool {
2886        if self.active_session_has_changes() {
2887            return true;
2888        }
2889
2890        for (index, session) in self.sessions.iter().enumerate() {
2891            if index != self.active_session && session.dirty {
2892                return true;
2893            }
2894        }
2895
2896        false
2897    }
2898
2899    fn window_moved(&mut self, x: i32, y: i32) {
2900        self.window_state.x = Some(x);
2901        self.window_state.y = Some(y);
2902        self.save_window_state();
2903    }
2904
2905    fn window_resized(&mut self, width: usize, height: usize) {
2906        if width > 0 && height > 0 {
2907            self.window_state.width = Some(width);
2908            self.window_state.height = Some(height);
2909            self.save_window_state();
2910        }
2911    }
2912}
2913
2914pub trait EldironEditor {
2915    fn update_server_state_icons(&mut self, ui: &mut TheUI);
2916}
2917
2918impl EldironEditor for Editor {
2919    fn update_server_state_icons(&mut self, ui: &mut TheUI) {
2920        let rusterix = RUSTERIX.read().unwrap();
2921        if rusterix.server.state == rusterix::ServerState::Running {
2922            if let Some(button) = ui.get_widget("Play") {
2923                if let Some(button) = button.as_menubar_button() {
2924                    button.set_icon_name("play-fill".to_string());
2925                }
2926            }
2927            if let Some(button) = ui.get_widget("Pause") {
2928                if let Some(button) = button.as_menubar_button() {
2929                    button.set_icon_name("play-pause".to_string());
2930                }
2931            }
2932            if let Some(button) = ui.get_widget("Stop") {
2933                if let Some(button) = button.as_menubar_button() {
2934                    button.set_icon_name("stop".to_string());
2935                }
2936            }
2937        } else if rusterix.server.state == rusterix::ServerState::Paused {
2938            if let Some(button) = ui.get_widget("Play") {
2939                if let Some(button) = button.as_menubar_button() {
2940                    button.set_icon_name("play".to_string());
2941                }
2942            }
2943            if let Some(button) = ui.get_widget("Pause") {
2944                if let Some(button) = button.as_menubar_button() {
2945                    button.set_icon_name("play-pause-fill".to_string());
2946                }
2947            }
2948            if let Some(button) = ui.get_widget("Stop") {
2949                if let Some(button) = button.as_menubar_button() {
2950                    button.set_icon_name("stop".to_string());
2951                }
2952            }
2953        } else if rusterix.server.state == rusterix::ServerState::Off {
2954            if let Some(button) = ui.get_widget("Play") {
2955                if let Some(button) = button.as_menubar_button() {
2956                    button.set_icon_name("play".to_string());
2957                }
2958            }
2959            if let Some(button) = ui.get_widget("Pause") {
2960                if let Some(button) = button.as_menubar_button() {
2961                    button.set_icon_name("play-pause".to_string());
2962                }
2963            }
2964            if let Some(button) = ui.get_widget("Stop") {
2965                if let Some(button) = button.as_menubar_button() {
2966                    button.set_icon_name("stop-fill".to_string());
2967                }
2968            }
2969        }
2970    }
2971}