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()));
36pub 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::RegionSettings(_) => "docs/building_maps/region_settings".to_string(),
449 ProjectContext::CharacterPreviewRigging(_) => "docs/characters_items/rigging".into(),
450 ProjectContext::Character(_)
451 | ProjectContext::CharacterData(_)
452 | ProjectContext::Item(_)
453 | ProjectContext::ItemData(_) => "docs/characters_items/attributes".to_string(),
454 ProjectContext::Screen(_)
455 | ProjectContext::ScreenWidget(_, _)
456 | ProjectContext::RegionCharacterInstance(_, _)
457 | ProjectContext::RegionItemInstance(_, _) => "docs/screens/widgets".to_string(),
458 _ => "docs/creator/docks/attribute_editor".to_string(),
459 }
460 }
461
462 fn help_url_for_widget_name(&self, widget_name: &str) -> Option<String> {
463 match widget_name {
464 "Tiles" | "Tilemap" | "Tile Editor Dock RGBA Layout View" | "Tile Editor Tree" => {
465 Some("docs/creator/docks/tile_picker_editor".into())
466 }
467 "DockDataEditor" | "DockDataEditorMax" | "Data" => {
468 Some(self.help_url_for_data_context())
469 }
470 "DockCodeEditor" | "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
471 "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
472 "PolyView" => {
473 if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
474 Some("docs/building_maps/creating_2d".into())
475 } else {
476 Some("docs/building_maps/creating_3d_maps".into())
477 }
478 }
479 name if name.starts_with("DockVisualScripting") => {
480 Some("docs/creator/docks/visual_script_editor".into())
481 }
482 name if name.starts_with("Tile Editor ") => {
483 Some("docs/creator/docks/tile_picker_editor".into())
484 }
485 _ => None,
486 }
487 }
488
489 fn help_url_for_editor_event(&self, event: &TheEvent, ui: &mut TheUI) -> Option<String> {
490 let mut clicked = false;
491 let widget_name = match event {
492 TheEvent::StateChanged(id, state) if *state == TheWidgetState::Clicked => {
493 clicked = true;
494 Some(id.name.clone())
495 }
496 TheEvent::RenderViewClicked(id, _) => {
497 clicked = true;
498 Some(id.name.clone())
499 }
500 TheEvent::TilePicked(id, _) => {
501 clicked = true;
502 Some(id.name.clone())
503 }
504 TheEvent::TileEditorClicked(id, _) => {
505 clicked = true;
506 Some(id.name.clone())
507 }
508 TheEvent::MouseDown(coord) => {
509 clicked = true;
510 ui.get_widget_at_coord(*coord).map(|w| w.id().name.clone())
511 }
512 _ => None,
513 };
514
515 if let Some(widget_name) = widget_name
516 && let Some(url) = self.help_url_for_widget_name(&widget_name)
517 {
518 return Some(url);
519 }
520
521 if clicked {
522 let dm = DOCKMANAGER.read().unwrap();
523 if dm.state != DockManagerState::Minimized {
524 return match dm.dock.as_str() {
525 "Tiles" => Some("docs/creator/docks/tile_picker_editor".into()),
526 "Data" => Some(self.help_url_for_data_context()),
527 "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
528 "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
529 _ => None,
530 };
531 }
532 }
533 None
534 }
535}
536
537impl TheTrait for Editor {
538 fn new() -> Self
539 where
540 Self: Sized,
541 {
542 let mut project = Project::new();
543 if let Some(bytes) = crate::Embedded::get("toml/config.toml") {
544 if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
545 project.config = source.to_string();
546 }
547 }
548
549 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
550 let (self_update_tx, self_update_rx) = channel();
551
552 #[cfg(all(
553 not(target_arch = "wasm32"),
554 feature = "self-update",
555 not(target_os = "macos")
556 ))]
557 let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "eldiron-creator");
558 #[cfg(all(
559 not(target_arch = "wasm32"),
560 feature = "self-update",
561 target_os = "macos"
562 ))]
563 let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "Eldiron-Creator.app");
564
565 let initial_session = ProjectSession {
566 project: project.clone(),
567 project_path: None,
568 undo: UndoManager::default(),
569 dirty: false,
570 };
571
572 Self {
573 project,
574 project_path: None,
575 sessions: vec![initial_session],
576 active_session: 0,
577 replace_next_project_load_in_active_tab: false,
578 last_active_dirty: false,
579
580 sidebar: Sidebar::new(),
581 mapeditor: MapEditor::new(),
582
583 server_ctx: ServerContext::default(),
584
585 update_tracker: UpdateTracker::new(),
586 event_receiver: None,
587
588 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
589 self_update_rx,
590 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
591 self_update_tx,
592 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
593 self_updater: Arc::new(Mutex::new(self_updater)),
594
595 update_counter: 0,
596 last_processed_log_len: 0,
597
598 build_values: ValueContainer::default(),
599 window_state: Self::load_window_state(),
600 }
601 }
602
603 fn init(&mut self, _ctx: &mut TheContext) {
604 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
605 {
606 let updater = Arc::clone(&self.self_updater);
607 let tx = self.self_update_tx.clone();
608
609 thread::spawn(move || {
610 let mut updater = updater.lock().unwrap();
611
612 if let Err(err) = updater.fetch_release_list() {
613 tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
614 .unwrap();
615 };
616 });
617 }
618 }
619
620 fn window_title(&self) -> String {
621 "Eldiron Creator".to_string()
622 }
623
624 fn target_fps(&self) -> f64 {
625 1000.0 / self.redraw_interval_ms() as f64
626 }
627
628 fn fonts_to_load(&self) -> Vec<TheFontScript> {
629 vec![TheFontScript::Han]
630 }
631
632 fn default_window_size(&self) -> (usize, usize) {
633 (
634 self.window_state.width.unwrap_or(1200),
635 self.window_state.height.unwrap_or(720),
636 )
637 }
638
639 fn min_window_size(&self) -> (usize, usize) {
640 (1200, 720)
641 }
642
643 fn default_window_position(&self) -> Option<(i32, i32)> {
644 Some((self.window_state.x?, self.window_state.y?))
645 }
646
647 fn window_icon(&self) -> Option<(Vec<u8>, u32, u32)> {
648 if let Some(file) = Embedded::get("window_logo.png") {
649 let data = std::io::Cursor::new(file.data);
650
651 let decoder = png::Decoder::new(data);
652 if let Ok(mut reader) = decoder.read_info() {
653 if let Some(buffer_size) = reader.output_buffer_size() {
654 let mut buf = vec![0; buffer_size];
655 let info = reader.next_frame(&mut buf).unwrap();
656 let bytes = &buf[..info.buffer_size()];
657
658 Some((bytes.to_vec(), info.width, info.height))
659 } else {
660 None
661 }
662 } else {
663 None
664 }
665 } else {
666 None
667 }
668 }
669
670 fn init_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
671 RUSTERIX.write().unwrap().client.messages_font = ctx.ui.font.clone();
672
673 for file in Embedded::iter() {
675 let name = file.as_ref();
676
677 if name.ends_with(".png") {
678 if let Some(file) = Embedded::get(name) {
679 let data = std::io::Cursor::new(file.data);
680
681 let decoder = png::Decoder::new(data);
682 if let Ok(mut reader) = decoder.read_info() {
683 if let Some(buffer_size) = reader.output_buffer_size() {
684 let mut buf = vec![0; buffer_size];
685 let info = reader.next_frame(&mut buf).unwrap();
686 let bytes = &buf[..info.buffer_size()];
687
688 let mut cut_name = name.replace("icons/", "");
689 cut_name = cut_name.replace(".png", "");
690
691 ctx.ui.add_icon(
692 cut_name.to_string(),
693 TheRGBABuffer::from(bytes.to_vec(), info.width, info.height),
694 );
695 }
696 }
697 }
698 }
699 }
700
701 ui.set_statusbar_name("Statusbar".to_string());
704
705 let mut top_canvas = TheCanvas::new();
706 #[cfg(not(feature = "staticlib"))]
709 {
710 let mut menu_canvas = TheCanvas::new();
711 let mut menu = TheMenu::new(TheId::named("Menu"));
712
713 let mut file_menu = TheContextMenu::named(fl!("menu_file"));
714 file_menu.add(TheContextMenuItem::new_with_accel(
715 fl!("menu_new"),
716 TheId::named("New"),
717 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'n'),
718 ));
719 file_menu.add_separator();
720 file_menu.add(TheContextMenuItem::new_with_accel(
721 fl!("menu_open"),
722 TheId::named("Open"),
723 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
724 ));
725 file_menu.add(TheContextMenuItem::new_with_accel(
726 fl!("menu_close"),
727 TheId::named("Close"),
728 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'w'),
729 ));
730 file_menu.add_separator();
731 file_menu.add(TheContextMenuItem::new_with_accel(
732 fl!("menu_save"),
733 TheId::named("Save"),
734 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 's'),
735 ));
736 file_menu.add(TheContextMenuItem::new_with_accel(
737 fl!("menu_save_as"),
738 TheId::named("Save As"),
739 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'a'),
740 ));
741 let mut edit_menu = TheContextMenu::named(fl!("menu_edit"));
742 edit_menu.add(TheContextMenuItem::new_with_accel(
743 fl!("menu_undo"),
744 TheId::named("Undo"),
745 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'z'),
746 ));
747 edit_menu.add(TheContextMenuItem::new_with_accel(
748 fl!("menu_redo"),
749 TheId::named("Redo"),
750 TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'z'),
751 ));
752 edit_menu.add_separator();
753 edit_menu.add(TheContextMenuItem::new_with_accel(
754 fl!("menu_cut"),
755 TheId::named("Cut"),
756 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'x'),
757 ));
758 edit_menu.add(TheContextMenuItem::new_with_accel(
759 fl!("menu_copy"),
760 TheId::named("Copy"),
761 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'c'),
762 ));
763 edit_menu.add(TheContextMenuItem::new_with_accel(
764 fl!("menu_paste"),
765 TheId::named("Paste"),
766 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'v'),
767 ));
768 edit_menu.add_separator();
769 edit_menu.add(TheContextMenuItem::new_with_accel(
770 fl!("menu_apply_action"),
771 TheId::named("Action Apply"),
772 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
773 ));
774
775 let mut game_menu = TheContextMenu::named(fl!("game"));
776 game_menu.add(TheContextMenuItem::new_with_accel(
777 fl!("menu_play"),
778 TheId::named("Play"),
779 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
780 ));
781 game_menu.add(TheContextMenuItem::new_with_accel(
782 fl!("menu_pause"),
783 TheId::named("Pause"),
784 TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
785 ));
786 game_menu.add(TheContextMenuItem::new_with_accel(
787 fl!("menu_stop"),
788 TheId::named("Stop"),
789 TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'p'),
790 ));
791
792 file_menu.register_accel(ctx);
793 edit_menu.register_accel(ctx);
794 game_menu.register_accel(ctx);
795
796 menu.add_context_menu(file_menu);
797 menu.add_context_menu(edit_menu);
798 menu.add_context_menu(game_menu);
799 menu_canvas.set_widget(menu);
800 top_canvas.set_top(menu_canvas);
801 }
802
803 let mut menubar = TheMenubar::new(TheId::named("Menubar"));
804 #[cfg(feature = "staticlib")]
805 menubar.limiter_mut().set_max_height(43);
806 #[cfg(not(feature = "staticlib"))]
807 menubar.limiter_mut().set_max_height(43 + 22);
808
809 let mut logo_button = TheMenubarButton::new(TheId::named("Logo"));
810 logo_button.set_icon_name("logo".to_string());
811 logo_button.set_status_text(&fl!("status_logo_button"));
812
813 let mut open_button = TheMenubarButton::new(TheId::named("Open"));
814 open_button.set_icon_name("icon_role_load".to_string());
815 open_button.set_status_text(&fl!("status_open_button"));
816
817 let mut save_button = TheMenubarButton::new(TheId::named("Save"));
818 save_button.set_status_text(&fl!("status_save_button"));
819 save_button.set_icon_name("icon_role_save".to_string());
820
821 let mut save_as_button = TheMenubarButton::new(TheId::named("Save As"));
822 save_as_button.set_icon_name("icon_role_save_as".to_string());
823 save_as_button.set_status_text(&fl!("status_save_as_button"));
824 save_as_button.set_icon_offset(Vec2::new(2, -5));
825
826 let mut undo_button = TheMenubarButton::new(TheId::named("Undo"));
827 undo_button.set_status_text(&fl!("status_undo_button"));
828 undo_button.set_icon_name("icon_role_undo".to_string());
829
830 let mut redo_button = TheMenubarButton::new(TheId::named("Redo"));
831 redo_button.set_status_text(&fl!("status_redo_button"));
832 redo_button.set_icon_name("icon_role_redo".to_string());
833
834 let mut play_button = TheMenubarButton::new(TheId::named("Play"));
835 play_button.set_status_text(&fl!("status_play_button"));
836 play_button.set_icon_name("play".to_string());
837 let mut pause_button = TheMenubarButton::new(TheId::named("Pause"));
840 pause_button.set_status_text(&fl!("status_pause_button"));
841 pause_button.set_icon_name("play-pause".to_string());
842
843 let mut stop_button = TheMenubarButton::new(TheId::named("Stop"));
844 stop_button.set_status_text(&fl!("status_stop_button"));
845 stop_button.set_icon_name("stop-fill".to_string());
846
847 let mut input_button = TheMenubarButton::new(TheId::named("GameInput"));
848 input_button.set_status_text(&fl!("status_game_input_button"));
849 input_button.set_icon_name("keyboard".to_string());
850 input_button.set_has_state(true);
851
852 let mut time_slider = TheTimeSlider::new(TheId::named("Server Time Slider"));
853 time_slider.set_status_text(&fl!("status_time_slider"));
854 time_slider.set_continuous(true);
855 time_slider.limiter_mut().set_max_width(400);
856 time_slider.set_value(TheValue::Time(TheTime::default()));
857
858 let mut patreon_button = TheMenubarButton::new(TheId::named("Patreon"));
859 patreon_button.set_status_text(&fl!("status_patreon_button"));
860 patreon_button.set_icon_name("patreon".to_string());
861 patreon_button.set_icon_offset(Vec2::new(-4, -2));
863
864 let mut help_button = TheMenubarButton::new(TheId::named("Help"));
865 help_button.set_status_text(&fl!("status_help_button"));
866 help_button.set_icon_name("question-mark".to_string());
867 help_button.set_has_state(true);
868 help_button.set_icon_offset(Vec2::new(-2, -2));
870
871 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
872 let mut update_button = {
873 let mut button = TheMenubarButton::new(TheId::named("Update"));
874 button.set_status_text(&fl!("status_update_button"));
875 button.set_icon_name("arrows-clockwise".to_string());
876 button
877 };
878
879 let mut hlayout = TheHLayout::new(TheId::named("Menu Layout"));
880 hlayout.set_background_color(None);
881 hlayout.set_margin(Vec4::new(10, 2, 10, 1));
882 hlayout.add_widget(Box::new(logo_button));
883 hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
884 hlayout.add_widget(Box::new(open_button));
885 hlayout.add_widget(Box::new(save_button));
886 hlayout.add_widget(Box::new(save_as_button));
887 hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
888 hlayout.add_widget(Box::new(undo_button));
889 hlayout.add_widget(Box::new(redo_button));
890 hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
891 hlayout.add_widget(Box::new(play_button));
892 hlayout.add_widget(Box::new(pause_button));
893 hlayout.add_widget(Box::new(stop_button));
894 hlayout.add_widget(Box::new(input_button));
895 hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
896 hlayout.add_widget(Box::new(time_slider));
897 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
900 {
901 hlayout.add_widget(Box::new(update_button));
902 hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
903 hlayout.add_widget(Box::new(patreon_button));
904 hlayout.set_reverse_index(Some(3));
905 }
906
907 #[cfg(not(all(not(target_arch = "wasm32"), feature = "self-update")))]
908 {
909 hlayout.add_widget(Box::new(patreon_button));
910 hlayout.add_widget(Box::new(help_button));
911 hlayout.set_reverse_index(Some(2));
912 }
913
914 top_canvas.set_widget(menubar);
915 top_canvas.set_layout(hlayout);
916 ui.canvas.set_top(top_canvas);
917
918 self.sidebar.init_ui(ui, ctx, &mut self.server_ctx);
920
921 let bottom_panels = DOCKMANAGER.write().unwrap().init(ctx);
923
924 let mut editor_canvas: TheCanvas = TheCanvas::new();
925
926 let mut editor_stack = TheStackLayout::new(TheId::named("Editor Stack"));
927 let poly_canvas = self.mapeditor.init_ui(ui, ctx, &mut self.project);
928 editor_stack.add_canvas(poly_canvas);
929
930 DOCKMANAGER
932 .write()
933 .unwrap()
934 .add_editors_to_stack(&mut editor_stack, ctx);
935
936 editor_canvas.set_layout(editor_stack);
937
938 let mut vsplitlayout = TheSharedVLayout::new(TheId::named("Shared VLayout"));
940 vsplitlayout.add_canvas(editor_canvas);
941 vsplitlayout.add_canvas(bottom_panels);
942 vsplitlayout.set_shared_ratio(crate::DEFAULT_VLAYOUT_RATIO);
943 vsplitlayout.set_mode(TheSharedVLayoutMode::Shared);
944
945 let mut shared_canvas = TheCanvas::new();
946 shared_canvas.set_layout(vsplitlayout);
947
948 let mut tabs_canvas = TheCanvas::new();
949 let mut tabs = TheTabbar::new(TheId::named("Project Tabs"));
950 tabs.limiter_mut().set_max_height(22);
951 tabs_canvas.set_widget(tabs);
952 shared_canvas.set_top(tabs_canvas);
953
954 let mut tool_list_canvas: TheCanvas = TheCanvas::new();
956
957 let mut tool_list_bar_canvas = TheCanvas::new();
958 tool_list_bar_canvas.set_widget(TheToolListBar::new(TheId::empty()));
959 tool_list_canvas.set_top(tool_list_bar_canvas);
960
961 let mut v_tool_list_layout = TheVLayout::new(TheId::named("Tool List Layout"));
962 v_tool_list_layout.limiter_mut().set_max_width(51);
963 v_tool_list_layout.set_margin(Vec4::new(2, 2, 2, 2));
964 v_tool_list_layout.set_padding(1);
965
966 TOOLLIST
967 .write()
968 .unwrap()
969 .set_active_editor(&mut v_tool_list_layout, ctx);
970
971 tool_list_canvas.set_layout(v_tool_list_layout);
972
973 let mut tool_list_border_canvas = TheCanvas::new();
974 let mut border_widget = TheIconView::new(TheId::empty());
975 border_widget.set_border_color(Some([82, 82, 82, 255]));
976 border_widget.limiter_mut().set_max_width(1);
977 border_widget.limiter_mut().set_max_height(i32::MAX);
978 tool_list_border_canvas.set_widget(border_widget);
979
980 tool_list_canvas.set_right(tool_list_border_canvas);
981 shared_canvas.set_left(tool_list_canvas);
982
983 ui.canvas.set_center(shared_canvas);
984
985 let mut status_canvas = TheCanvas::new();
986 let mut statusbar = TheStatusbar::new(TheId::named("Statusbar"));
987 statusbar.set_text(fl!("info_welcome"));
988 status_canvas.set_widget(statusbar);
989
990 ui.canvas.set_bottom(status_canvas);
991
992 ctx.ui.set_disabled("Undo");
997 ctx.ui.set_disabled("Redo");
998
999 if let Some(icon) = ctx.ui.icon("light_on") {
1002 let texture = Texture::from_rgbabuffer(icon);
1003 self.build_values.set("light_on", Value::Texture(texture));
1004 }
1005 if let Some(icon) = ctx.ui.icon("light_off") {
1006 let texture = Texture::from_rgbabuffer(icon);
1007 self.build_values.set("light_off", Value::Texture(texture));
1008 }
1009 if let Some(icon) = ctx.ui.icon("character_on") {
1010 let texture = Texture::from_rgbabuffer(icon);
1011 self.build_values
1012 .set("character_on", Value::Texture(texture));
1013 }
1014 if let Some(icon) = ctx.ui.icon("character_off") {
1015 let texture = Texture::from_rgbabuffer(icon);
1016 self.build_values
1017 .set("character_off", Value::Texture(texture));
1018 }
1019 if let Some(icon) = ctx.ui.icon("treasure_on") {
1020 let texture = Texture::from_rgbabuffer(icon);
1021 self.build_values
1022 .set("treasure_on", Value::Texture(texture));
1023 }
1024 if let Some(icon) = ctx.ui.icon("treasure_off") {
1025 let texture = Texture::from_rgbabuffer(icon);
1026 self.build_values
1027 .set("treasure_off", Value::Texture(texture));
1028 }
1029
1030 RUSTERIX
1031 .write()
1032 .unwrap()
1033 .client
1034 .builder_d2
1035 .set_properties(&self.build_values);
1036 RUSTERIX.write().unwrap().set_d2();
1037 SCENEMANAGER
1038 .write()
1039 .unwrap()
1040 .set_apply_preview_filters(true);
1041 SCENEMANAGER.write().unwrap().startup();
1042
1043 self.event_receiver = Some(ui.add_state_listener("Main Receiver".into()));
1044 self.rebuild_project_tabs(ui);
1045 }
1046
1047 fn set_cmd_line_args(&mut self, args: Vec<String>, ctx: &mut TheContext) {
1049 if args.len() > 1 {
1050 let mut queued_any = false;
1051 for arg in args.iter().skip(1) {
1052 #[allow(irrefutable_let_patterns)]
1053 if let Ok(path) = PathBuf::from_str(arg) {
1054 if !queued_any {
1055 self.replace_next_project_load_in_active_tab = true;
1056 }
1057 ctx.ui.send(TheEvent::FileRequesterResult(
1058 TheId::named("Open"),
1059 vec![path],
1060 ));
1061 queued_any = true;
1062 }
1063 }
1064 if queued_any {
1065 return;
1066 }
1067 }
1068
1069 self.replace_next_project_load_in_active_tab = true;
1070 ctx.ui.send(TheEvent::StateChanged(
1071 TheId::named("New"),
1072 TheWidgetState::Clicked,
1073 ));
1074 }
1075
1076 fn update_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) -> bool {
1078 let mut redraw = false;
1079 let mut update_server_icons = false;
1080
1081 if self.update_counter == 0 {
1083 let mut toollist = TOOLLIST.write().unwrap();
1084 let id = toollist.get_current_tool().id().uuid;
1085
1086 toollist.set_tool(id, ui, ctx, &mut self.project, &mut self.server_ctx);
1087 }
1088
1089 while let Some(result) = SCENEMANAGER.write().unwrap().receive() {
1091 match result {
1092 SceneManagerResult::Startup => {
1093 println!("Scene manager has started up.");
1094 }
1095 SceneManagerResult::ProcessedHeights(coord, heights) => {
1096 if let Some(map) = &mut self.project.get_map_mut(&self.server_ctx) {
1097 let local = map.terrain.get_chunk_coords(coord.x, coord.y);
1098 if let Some(chunk) = &mut map.terrain.chunks.get_mut(&local) {
1099 chunk.processed_heights = Some(heights);
1100 }
1101 }
1102 }
1103 SceneManagerResult::Chunk(chunk, togo, total, billboards) => {
1104 if togo == 0 {
1105 self.server_ctx.background_progress = None;
1106 } else {
1107 self.server_ctx.background_progress = Some(format!("{togo}/{total}"));
1108 }
1109
1110 let mut rusterix = RUSTERIX.write().unwrap();
1111
1112 rusterix
1113 .scene_handler
1114 .vm
1115 .execute(scenevm::Atom::RemoveChunkAt {
1116 origin: chunk.origin,
1117 });
1118
1119 rusterix.scene_handler.vm.execute(scenevm::Atom::AddChunk {
1120 id: Uuid::new_v4(),
1121 chunk: chunk,
1122 });
1123
1124 for billboard in billboards {
1126 rusterix
1127 .scene_handler
1128 .billboards
1129 .insert(billboard.geo_id, billboard);
1130 }
1131
1132 ctx.ui.send(TheEvent::Custom(
1133 TheId::named("Update Minimap"),
1134 TheValue::Empty,
1135 ));
1136 }
1137 SceneManagerResult::UpdatedBatch3D(coord, batch) => {
1138 let mut rusterix = RUSTERIX.write().unwrap();
1139 if let Some(chunk) = rusterix.client.scene.chunks.get_mut(&coord) {
1140 chunk.terrain_batch3d = Some(batch);
1141 }
1142 }
1143 SceneManagerResult::Clear => {
1144 let mut rusterix = RUSTERIX.write().unwrap();
1145 rusterix
1146 .scene_handler
1147 .vm
1148 .execute(scenevm::Atom::ClearGeometry);
1149
1150 rusterix.scene_handler.billboards.clear();
1151 }
1152 SceneManagerResult::Quit => {
1153 println!("Scene manager has shutdown.");
1154 }
1155 }
1156 }
1157
1158 let redraw_ms = self.redraw_interval_ms();
1160 let tick_ms = CONFIGEDITOR.read().unwrap().game_tick_ms.max(1) as u64;
1161 let (mut redraw_update, tick_update) = self.update_tracker.update(redraw_ms, tick_ms);
1162
1163 let mut pending_events = Vec::new();
1165 if let Some(receiver) = &mut self.event_receiver {
1166 while let Ok(event) = receiver.try_recv() {
1167 pending_events.push(event);
1168 }
1169 }
1170 if !pending_events.is_empty() {
1171 redraw_update = true;
1172 }
1173
1174 if tick_update {
1175 RUSTERIX.write().unwrap().client.inc_animation_frame();
1176
1177 self.server_ctx.animation_counter = self.server_ctx.animation_counter.wrapping_add(1);
1178 if DOCKMANAGER
1180 .read()
1181 .unwrap()
1182 .current_dock_supports_minimap_animation()
1183 {
1184 ctx.ui.send(TheEvent::Custom(
1185 TheId::named("Soft Update Minimap"),
1186 TheValue::Empty,
1187 ));
1188 }
1189 }
1190
1191 if redraw_update && !self.project.regions.is_empty() {
1192 SCENEMANAGER.write().unwrap().tick_batch(8);
1194
1195 self.build_values.set(
1196 "no_rect_geo",
1197 Value::Bool(self.server_ctx.no_rect_geo_on_map),
1198 );
1199
1200 extract_build_values_from_config(&mut self.build_values);
1201
1202 let mut messages = Vec::new();
1203 let mut says = Vec::new();
1204 let mut choices = Vec::new();
1205
1206 {
1208 let rusterix = &mut RUSTERIX.write().unwrap();
1209 if rusterix.server.state == rusterix::ServerState::Running {
1210 if tick_update {
1212 rusterix.server.system_tick();
1213 }
1214
1215 if redraw_update {
1217 rusterix.server.redraw_tick();
1218 }
1219
1220 if let Some(new_region_name) = rusterix.update_server() {
1221 rusterix.client.current_map = new_region_name;
1222 }
1223 if rusterix.server.log_changed {
1224 let log_text = rusterix.server.get_log();
1225 ui.set_widget_value("LogEdit", ctx, TheValue::Text(log_text.clone()));
1226
1227 let mut start = if log_text.len() < self.last_processed_log_len {
1229 0
1230 } else {
1231 self.last_processed_log_len
1232 };
1233 while start < log_text.len() && !log_text.is_char_boundary(start) {
1234 start += 1;
1235 }
1236 let new_segment = &log_text[start..];
1237 if Self::log_segment_has_warning_or_error(new_segment) {
1238 ctx.ui.send(TheEvent::StateChanged(
1239 TheId::named("Debug Log"),
1240 TheWidgetState::Clicked,
1241 ));
1242 }
1243 self.last_processed_log_len = log_text.len();
1244 }
1245 for r in &mut self.project.regions {
1246 rusterix.server.apply_entities_items(&mut r.map);
1247
1248 if r.id == self.server_ctx.curr_region {
1249 if let Some(time) = rusterix.server.get_time(&r.map.id) {
1250 rusterix.client.set_server_time(time);
1251 if let Some(widget) = ui.get_widget("Server Time Slider") {
1252 widget.set_value(TheValue::Time(rusterix.client.server_time));
1253 }
1254 }
1255
1256 rusterix::tile_builder(&mut r.map, &mut rusterix.assets);
1257 messages = rusterix.server.get_messages(&r.map.id);
1258 says = rusterix.server.get_says(&r.map.id);
1259 choices = rusterix.server.get_choices(&r.map.id);
1260 for cmd in rusterix.server.get_audio_commands(&r.map.id) {
1261 match cmd {
1262 AudioCommand::Play {
1263 name,
1264 bus,
1265 gain,
1266 looping,
1267 } => {
1268 rusterix.play_audio_on_bus(&name, &bus, gain, looping);
1269 }
1270 AudioCommand::ClearBus { bus } => {
1271 rusterix.clear_audio_bus(&bus);
1272 }
1273 AudioCommand::ClearAll => {
1274 rusterix.clear_all_audio();
1275 }
1276 AudioCommand::SetBusVolume { bus, volume } => {
1277 rusterix.set_audio_bus_volume(&bus, volume);
1278 }
1279 }
1280 }
1281
1282 match &self.server_ctx.cc {
1284 ContentContext::CharacterInstance(uuid) => {
1285 for entity in r.map.entities.iter() {
1286 if entity.creator_id == *uuid {
1287 CODEGRIDFX.write().unwrap().redraw_debug(
1288 ui,
1289 ctx,
1290 entity.id,
1291 &rusterix.server.debug,
1292 );
1293 }
1294 }
1295 }
1296 _ => {}
1297 }
1298 }
1299 }
1300 }
1301 }
1302
1303 if let Some(render_view) = ui.get_render_view("PolyView") {
1305 let dim = *render_view.dim();
1306
1307 let buffer = render_view.render_buffer_mut();
1308 buffer.resize(dim.width, dim.height);
1309
1310 {
1311 if !self.server_ctx.game_mode
1313 && self.server_ctx.editor_view_mode != EditorViewMode::D2
1314 && self.server_ctx.curr_map_tool_type == MapToolType::Vertex
1315 {
1316 TOOLLIST
1317 .write()
1318 .unwrap()
1319 .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1320 }
1321
1322 let rusterix = &mut RUSTERIX.write().unwrap();
1323 let is_running = rusterix.server.state == rusterix::ServerState::Running;
1324
1325 if is_running && self.server_ctx.game_mode {
1326 for r in &mut self.project.regions {
1327 if r.map.name == rusterix.client.current_map {
1328 rusterix.draw_game(&r.map, messages, says, choices);
1329 break;
1330 }
1331 }
1332
1333 rusterix
1334 .client
1335 .insert_game_buffer(render_view.render_buffer_mut());
1336 } else {
1337 if self.server_ctx.editor_view_mode != EditorViewMode::D2
1338 && self.server_ctx.get_map_context() == MapContext::Region
1339 {
1340 if let Some(region) =
1341 self.project.get_region_ctx_mut(&mut self.server_ctx)
1342 {
1343 let follow_player_firstp = is_running
1344 && self.server_ctx.editor_view_mode == EditorViewMode::FirstP;
1345
1346 if follow_player_firstp
1347 && let Some(player) =
1348 region.map.entities.iter().find(|e| e.is_player())
1349 {
1350 let orientation =
1351 if player.orientation.magnitude_squared() > f32::EPSILON {
1352 player.orientation.normalized()
1353 } else {
1354 Vec2::new(1.0, 0.0)
1355 };
1356
1357 region.editing_position_3d = Vec3::new(
1358 player.position.x,
1359 player.position.y,
1360 player.position.z,
1361 );
1362 region.editing_look_at_3d = Vec3::new(
1363 player.position.x + orientation.x,
1364 player.position.y,
1365 player.position.z + orientation.y,
1366 );
1367 } else {
1368 EDITCAMERA
1369 .write()
1370 .unwrap()
1371 .update_action(region, &mut self.server_ctx);
1372 }
1373 EDITCAMERA.write().unwrap().update_camera(
1374 region,
1375 &mut self.server_ctx,
1376 rusterix,
1377 );
1378
1379 let animation_frame = rusterix.client.animation_frame;
1382 rusterix.build_dynamics_3d(®ion.map, animation_frame);
1383 rusterix.draw_d3(
1384 ®ion.map,
1385 render_view.render_buffer_mut().pixels_mut(),
1386 dim.width as usize,
1387 dim.height as usize,
1388 );
1389 }
1390 } else
1391 if self.server_ctx.get_map_context() == MapContext::Region
1393 && self.server_ctx.editing_surface.is_none()
1394 {
1395 if let Some(region) =
1396 self.project.get_region(&self.server_ctx.curr_region)
1397 {
1398 rusterix.client.builder_d2.set_clip_rect(None);
1399 rusterix
1400 .client
1401 .builder_d2
1402 .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1403 if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1404 rusterix.client.builder_d2.set_map_hover_info(
1405 self.server_ctx.hover,
1406 Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1407 );
1408 } else {
1409 rusterix
1410 .client
1411 .builder_d2
1412 .set_map_hover_info(self.server_ctx.hover, None);
1413 }
1414
1415 if let Some(camera_pos) = region.map.camera_xz {
1416 rusterix.client.builder_d2.set_camera_info(
1417 Some(Vec3::new(camera_pos.x, 0.0, camera_pos.y)),
1418 None,
1419 );
1420 }
1421
1422 if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1425 let mut map = region.map.clone();
1428 if let Some(hover) = self.server_ctx.hover_cursor {
1429 map.paste_at_position(clipboard, hover);
1430 }
1431
1432 rusterix.set_dirty();
1433 rusterix.apply_entities_items(
1440 Vec2::new(dim.width as f32, dim.height as f32),
1441 &map,
1442 &self.server_ctx.editing_surface,
1443 false,
1444 );
1445 } else {
1446 if let Some(map) = self.project.get_map(&self.server_ctx) {
1454 rusterix.apply_entities_items(
1455 Vec2::new(dim.width as f32, dim.height as f32),
1456 map,
1457 &self.server_ctx.editing_surface,
1458 false,
1459 );
1460 }
1461 }
1462
1463 rusterix.process_messages(®ion.map, says);
1465
1466 }
1469
1470 if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1471 if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
1472 rusterix.scene_handler.settings.backend_2d =
1473 RendererBackend::Raster;
1474 rusterix.set_d2();
1475 }
1476 if is_running
1477 && self.server_ctx.editor_view_mode == EditorViewMode::D2
1478 {
1479 let animation_frame = rusterix.client.animation_frame;
1480 rusterix.build_dynamics_2d(map, animation_frame);
1481 }
1482 if self.server_ctx.editor_view_mode == EditorViewMode::D2
1483 && rusterix.scene_handler.vm.vm_layer_count() > 1
1484 {
1485 rusterix.scene_handler.vm.set_layer_enabled(
1486 1,
1487 self.server_ctx.show_editing_geometry,
1488 );
1489 }
1490 rusterix.draw_scene(
1491 map,
1492 render_view.render_buffer_mut().pixels_mut(),
1493 dim.width as usize,
1494 dim.height as usize,
1495 );
1496 }
1497 } else if self.server_ctx.get_map_context() == MapContext::Region
1498 && self.server_ctx.editing_surface.is_some()
1499 {
1500 rusterix
1501 .client
1502 .builder_d2
1503 .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1504 if let Some(profile) = self.project.get_map_mut(&self.server_ctx) {
1505 if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1506 rusterix.scene_handler.vm.set_layer_enabled(1, true);
1508 }
1509 if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1510 rusterix.client.builder_d2.set_map_hover_info(
1511 self.server_ctx.hover,
1512 Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1513 );
1514 } else {
1515 rusterix
1516 .client
1517 .builder_d2
1518 .set_map_hover_info(self.server_ctx.hover, None);
1519 }
1520
1521 if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1522 let mut map = profile.clone();
1524 if let Some(hover) = self.server_ctx.hover_cursor {
1525 map.paste_at_position(clipboard, hover);
1526 }
1527 rusterix.set_dirty();
1528 rusterix.build_custom_scene_d2(
1529 Vec2::new(dim.width as f32, dim.height as f32),
1530 &map,
1531 &self.build_values,
1532 &self.server_ctx.editing_surface,
1533 true,
1534 );
1535 rusterix.draw_custom_d2(
1536 &map,
1537 render_view.render_buffer_mut().pixels_mut(),
1538 dim.width as usize,
1539 dim.height as usize,
1540 );
1541 } else {
1542 rusterix.build_custom_scene_d2(
1543 Vec2::new(dim.width as f32, dim.height as f32),
1544 profile,
1545 &self.build_values,
1546 &self.server_ctx.editing_surface,
1547 true,
1548 );
1549 rusterix.draw_custom_d2(
1550 profile,
1551 render_view.render_buffer_mut().pixels_mut(),
1552 dim.width as usize,
1553 dim.height as usize,
1554 );
1555 }
1556 }
1557 } else
1558 if self.server_ctx.get_map_context() == MapContext::Character
1560 || self.server_ctx.get_map_context() == MapContext::Item
1561 || self.server_ctx.get_map_context() == MapContext::Screen
1562 {
1563 rusterix
1564 .client
1565 .builder_d2
1566 .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1567 if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1568 if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1569 rusterix.scene_handler.vm.set_layer_enabled(
1571 1,
1572 self.server_ctx.show_editing_geometry,
1573 );
1574 }
1575 if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1576 rusterix.client.builder_d2.set_map_hover_info(
1577 self.server_ctx.hover,
1578 Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1579 );
1580 } else {
1581 rusterix
1582 .client
1583 .builder_d2
1584 .set_map_hover_info(self.server_ctx.hover, None);
1585 }
1586
1587 if self.server_ctx.get_map_context() != MapContext::Screen {
1588 rusterix.client.builder_d2.set_clip_rect(Some(
1589 rusterix::Rect {
1590 x: -5.0,
1591 y: -5.0,
1592 width: 10.0,
1593 height: 10.0,
1594 },
1595 ));
1596 } else {
1597 let viewport = CONFIGEDITOR.read().unwrap().viewport;
1598 let grid_size = CONFIGEDITOR.read().unwrap().grid_size as f32;
1599 let w = viewport.x as f32 / grid_size;
1600 let h = viewport.y as f32 / grid_size;
1601 rusterix.client.builder_d2.set_clip_rect(Some(
1602 rusterix::Rect {
1603 x: -w / 2.0,
1604 y: -h / 2.0,
1605 width: w,
1606 height: h,
1607 },
1608 ));
1609 }
1610
1611 if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1612 let mut map = map.clone();
1614 if let Some(hover) = self.server_ctx.hover_cursor {
1615 map.paste_at_position(clipboard, hover);
1616 }
1617 rusterix.set_dirty();
1618 rusterix.build_custom_scene_d2(
1619 Vec2::new(dim.width as f32, dim.height as f32),
1620 &map,
1621 &self.build_values,
1622 &self.server_ctx.editing_surface,
1623 true,
1624 );
1625 rusterix.draw_custom_d2(
1626 &map,
1627 render_view.render_buffer_mut().pixels_mut(),
1628 dim.width as usize,
1629 dim.height as usize,
1630 );
1631 } else {
1632 rusterix.build_custom_scene_d2(
1633 Vec2::new(dim.width as f32, dim.height as f32),
1634 map,
1635 &self.build_values,
1636 &None,
1637 true,
1638 );
1639 rusterix.draw_custom_d2(
1640 map,
1641 render_view.render_buffer_mut().pixels_mut(),
1642 dim.width as usize,
1643 dim.height as usize,
1644 );
1645 }
1646 }
1647 }
1648 }
1649 }
1650 if !self.server_ctx.game_mode {
1651 if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1652 TOOLLIST.write().unwrap().draw_hud(
1653 render_view.render_buffer_mut(),
1654 map,
1655 ctx,
1656 &mut self.server_ctx,
1657 &RUSTERIX.read().unwrap().assets,
1658 );
1659 }
1660 }
1661 }
1662
1663 redraw = true;
1676 }
1677
1678 for event in pending_events {
1679 if self.server_ctx.help_mode
1680 && let Some(url) = self.help_url_for_editor_event(&event, ui)
1681 {
1682 ctx.ui.send(TheEvent::Custom(
1683 TheId::named("Show Help"),
1684 TheValue::Text(url),
1685 ));
1686 redraw = true;
1687 continue;
1688 }
1689
1690 if self.server_ctx.game_input_mode && !self.server_ctx.game_mode {
1691 if let Some(game_tool) =
1693 TOOLLIST.write().unwrap().get_game_tool_of_name("Game Tool")
1694 {
1695 redraw = game_tool.handle_event(
1696 &event,
1697 ui,
1698 ctx,
1699 &mut self.project,
1700 &mut self.server_ctx,
1701 );
1702 }
1703 }
1704 if self
1705 .sidebar
1706 .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1707 {
1708 redraw = true;
1709 }
1710 if TOOLLIST.write().unwrap().handle_event(
1711 &event,
1712 ui,
1713 ctx,
1714 &mut self.project,
1715 &mut self.server_ctx,
1716 ) {
1717 redraw = true;
1718 }
1719 if DOCKMANAGER.write().unwrap().handle_event(
1720 &event,
1721 ui,
1722 ctx,
1723 &mut self.project,
1724 &mut self.server_ctx,
1725 ) {
1726 redraw = true;
1727 }
1728 if self
1729 .mapeditor
1730 .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1731 {
1732 redraw = true;
1733 }
1734 match event {
1735 TheEvent::IndexChanged(id, index) => {
1736 if id.name == "Project Tabs" {
1737 self.switch_to_session(
1738 index,
1739 ui,
1740 ctx,
1741 &mut update_server_icons,
1742 &mut redraw,
1743 );
1744 }
1745 }
1746 TheEvent::CustomUndo(id, p, n) => {
1747 if id.name == "ModuleUndo" {
1748 let _ = (&p, &n);
1749 }
1750 }
1751 TheEvent::Custom(id, value) => {
1752 if id.name == "Show Help" {
1753 if let TheValue::Text(url) = value {
1754 _ = open::that(format!("https://www.eldiron.com/{}", url));
1755 ctx.ui
1756 .set_widget_state("Help".to_string(), TheWidgetState::None);
1757 ctx.ui.clear_hover();
1758 self.server_ctx.help_mode = false;
1759 redraw = true;
1760 }
1761 }
1762 if id.name == "Set Project Undo State" {
1763 UNDOMANAGER.read().unwrap().set_undo_state_to_ui(ctx);
1764 } else if id.name == "Render SceneManager Map" {
1765 if self.server_ctx.pc.is_region() {
1766 if self.server_ctx.editor_view_mode == EditorViewMode::D2
1767 && self.server_ctx.profile_view.is_some()
1768 {
1769 } else {
1770 crate::utils::scenemanager_render_map(
1771 &self.project,
1772 &self.server_ctx,
1773 );
1774 if self.server_ctx.editor_view_mode != EditorViewMode::D2 {
1775 TOOLLIST.write().unwrap().update_geometry_overlay_3d(
1776 &mut self.project,
1777 &mut self.server_ctx,
1778 );
1779 }
1780 }
1781 }
1782 } else if id.name == "Tool Changed" {
1783 TOOLLIST
1784 .write()
1785 .unwrap()
1786 .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1787 } else if id.name == "Update Client Properties" {
1788 let mut rusterix = RUSTERIX.write().unwrap();
1789 self.build_values.set(
1790 "no_rect_geo",
1791 rusterix::Value::Bool(self.server_ctx.no_rect_geo_on_map),
1792 );
1793 self.build_values.set(
1794 "editing_slice",
1795 rusterix::Value::Float(self.server_ctx.editing_slice),
1796 );
1797 self.build_values.set(
1798 "editing_slice_height",
1799 rusterix::Value::Float(self.server_ctx.editing_slice_height),
1800 );
1801 rusterix
1802 .client
1803 .builder_d2
1804 .set_properties(&self.build_values);
1805 rusterix.set_dirty();
1806 }
1807 }
1808
1809 TheEvent::DialogValueOnClose(role, name, uuid, _value) => {
1810 if name == "Delete Character Instance ?" {
1811 if role == TheDialogButtonRole::Delete {
1812 if let Some(region) =
1813 self.project.get_region_mut(&self.server_ctx.curr_region)
1814 {
1815 let character_id = uuid;
1816 if region.characters.shift_remove(&character_id).is_some() {
1817 self.server_ctx.curr_region_content = ContentContext::Unknown;
1818 region.map.selected_entity_item = None;
1819 redraw = true;
1820
1821 if let Some(list) = ui.get_list_layout("Region Content List") {
1823 list.remove(TheId::named_with_id(
1824 "Region Content List Item",
1825 character_id,
1826 ));
1827 ui.select_first_list_item("Region Content List", ctx);
1828 ctx.ui.relayout = true;
1829 }
1830 insert_content_into_maps(&mut self.project);
1831 RUSTERIX.write().unwrap().set_dirty();
1832 }
1833 }
1834 }
1835 } else if name == "Delete Item Instance ?" {
1836 if role == TheDialogButtonRole::Delete {
1837 if let Some(region) =
1838 self.project.get_region_mut(&self.server_ctx.curr_region)
1839 {
1840 let item_id = uuid;
1841 if region.items.shift_remove(&item_id).is_some() {
1842 self.server_ctx.curr_region_content = ContentContext::Unknown;
1843 redraw = true;
1844
1845 if let Some(list) = ui.get_list_layout("Region Content List") {
1847 list.remove(TheId::named_with_id(
1848 "Region Content List Item",
1849 item_id,
1850 ));
1851 ui.select_first_list_item("Region Content List", ctx);
1852 ctx.ui.relayout = true;
1853 }
1854 insert_content_into_maps(&mut self.project);
1855 RUSTERIX.write().unwrap().set_dirty();
1856 }
1857 }
1858 }
1859 } else if name == "Close Project Tab" && role == TheDialogButtonRole::Accept {
1860 self.close_active_session(ui, ctx, &mut update_server_icons, &mut redraw);
1861 } else if name == "Update Eldiron" && role == TheDialogButtonRole::Accept {
1862 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
1863 {
1864 let updater = self.self_updater.lock().unwrap();
1865
1866 if updater.has_newer_release() {
1867 let release = updater.latest_release().cloned().unwrap();
1868
1869 let updater = Arc::clone(&self.self_updater);
1870 let tx = self.self_update_tx.clone();
1871
1872 self.self_update_tx
1873 .send(SelfUpdateEvent::UpdateStart(release.clone()))
1874 .unwrap();
1875
1876 thread::spawn(move || {
1877 match updater.lock().unwrap().update_latest() {
1878 Ok(status) => match status {
1879 self_update::Status::UpToDate(_) => {
1880 tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
1881 }
1882 self_update::Status::Updated(_) => {
1883 tx.send(SelfUpdateEvent::UpdateCompleted(release))
1884 .unwrap();
1885 }
1886 },
1887 Err(err) => {
1888 tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
1889 .unwrap();
1890 }
1891 }
1892 });
1893 } else {
1894 self.self_update_tx
1895 .send(SelfUpdateEvent::AlreadyUpToDate)
1896 .unwrap();
1897 }
1898 }
1899 }
1900 }
1901 TheEvent::RenderViewDrop(_id, location, drop) => {
1902 if drop.id.name.starts_with("Shader") {
1903 return true;
1904 }
1905
1906 let mut grid_pos = Vec2::zero();
1907 let mut spawn_y = 0.0;
1908
1909 if let Some(map) = self.project.get_map(&self.server_ctx) {
1910 if let Some(render_view) = ui.get_render_view("PolyView") {
1911 let dim = *render_view.dim();
1912 grid_pos = self.server_ctx.local_to_map_cell(
1913 Vec2::new(dim.width as f32, dim.height as f32),
1914 Vec2::new(location.x as f32, location.y as f32),
1915 map,
1916 map.subdivisions,
1917 );
1918 grid_pos += 0.5;
1919 let mut best_height: Option<f32> = None;
1920 for sector in map
1921 .sectors
1922 .iter()
1923 .filter(|s| s.layer.is_none() && s.is_inside(map, grid_pos))
1924 {
1925 let mut vertex_ids: Vec<u32> = Vec::new();
1926 let mut sum_y = 0.0f32;
1927 let mut count = 0usize;
1928 for linedef_id in §or.linedefs {
1929 if let Some(ld) = map.find_linedef(*linedef_id) {
1930 if !vertex_ids.contains(&ld.start_vertex) {
1931 vertex_ids.push(ld.start_vertex);
1932 if let Some(v) = map.get_vertex_3d(ld.start_vertex) {
1933 sum_y += v.y;
1934 count += 1;
1935 }
1936 }
1937 if !vertex_ids.contains(&ld.end_vertex) {
1938 vertex_ids.push(ld.end_vertex);
1939 if let Some(v) = map.get_vertex_3d(ld.end_vertex) {
1940 sum_y += v.y;
1941 count += 1;
1942 }
1943 }
1944 }
1945 }
1946 if count > 0 {
1947 let h = sum_y / count as f32;
1948 best_height = Some(best_height.map_or(h, |prev| prev.max(h)));
1949 }
1950 }
1951 if let Some(h) = best_height {
1952 spawn_y = h;
1953 }
1954 }
1955 }
1956
1957 if drop.id.name.starts_with("Character") {
1958 let mut instance = Character {
1959 character_id: drop.id.references,
1960 position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
1961 ..Default::default()
1962 };
1963
1964 if let Some(bytes) = crate::Embedded::get("python/instcharacter.py") {
1965 if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
1966 instance.source = source.to_string();
1967 }
1968 }
1969
1970 let mut name = "Character".to_string();
1971 if let Some(character) = self.project.characters.get(&drop.id.references) {
1972 name.clone_from(&character.name);
1973 }
1974 instance.name = name.clone();
1975
1976 let atom = ProjectUndoAtom::AddRegionCharacterInstance(
1977 self.server_ctx.curr_region,
1978 instance,
1979 );
1980 atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
1981 UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
1982 } else if drop.id.name.starts_with("Item") {
1983 let mut instance = Item {
1984 item_id: drop.id.references,
1985 position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
1986 ..Default::default()
1987 };
1988
1989 if let Some(bytes) = crate::Embedded::get("python/institem.py") {
1990 if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
1991 instance.source = source.to_string();
1992 }
1993 }
1994
1995 let mut name = "Item".to_string();
1996 if let Some(item) = self.project.items.get(&drop.id.references) {
1997 name.clone_from(&item.name);
1998 }
1999 instance.name = name;
2000
2001 let atom = ProjectUndoAtom::AddRegionItemInstance(
2002 self.server_ctx.curr_region,
2003 instance,
2004 );
2005 atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
2006 UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
2007 }
2008 }
2009 TheEvent::FileRequesterResult(id, paths) => {
2209 if id.name == "Palette Import" {
2211 for p in paths {
2212 let contents = std::fs::read_to_string(p).unwrap_or("".to_string());
2213 let prev = self.project.palette.clone();
2214 self.project.palette.load_from_txt(contents);
2215 *PALETTE.write().unwrap() = self.project.palette.clone();
2216
2217 if let Some(palette_picker) = ui.get_palette_picker("Palette Picker") {
2218 let index = palette_picker.index();
2219
2220 palette_picker.set_palette(self.project.palette.clone());
2221 if let Some(widget) = ui.get_widget("Palette Color Picker") {
2222 if let Some(color) = &self.project.palette[index] {
2223 widget.set_value(TheValue::ColorObject(color.clone()));
2224 }
2225 }
2226 if let Some(widget) = ui.get_widget("Palette Hex Edit") {
2227 if let Some(color) = &self.project.palette[index] {
2228 widget.set_value(TheValue::Text(color.to_hex()));
2229 }
2230 }
2231 }
2232 redraw = true;
2233
2234 let undo =
2235 ProjectUndoAtom::PaletteEdit(prev, self.project.palette.clone());
2236 UNDOMANAGER.write().unwrap().add_undo(undo, ctx);
2237 }
2238 } else
2239 if id.name == "Open" {
2241 for p in paths {
2242 if let Ok(contents) = std::fs::read_to_string(&p) {
2243 if let Ok(mut loaded) = serde_json::from_str::<Project>(&contents) {
2244 loaded.palette.current_index = 0;
2245 Self::sanitize_loaded_project(&mut loaded);
2246
2247 self.sync_active_session_from_editor();
2248 let new_index = if self.replace_next_project_load_in_active_tab
2249 {
2250 self.sessions[self.active_session] = ProjectSession {
2251 project: loaded,
2252 project_path: Some(p.clone()),
2253 undo: UndoManager::default(),
2254 dirty: false,
2255 };
2256 self.replace_next_project_load_in_active_tab = false;
2257 self.active_session
2258 } else {
2259 self.sessions.push(ProjectSession {
2260 project: loaded,
2261 project_path: Some(p.clone()),
2262 undo: UndoManager::default(),
2263 dirty: false,
2264 });
2265 self.sessions.len() - 1
2266 };
2267 self.switch_to_session(
2268 new_index,
2269 ui,
2270 ctx,
2271 &mut update_server_icons,
2272 &mut redraw,
2273 );
2274 ctx.ui.send(TheEvent::SetStatusText(
2275 TheId::empty(),
2276 "Project loaded successfully.".to_string(),
2277 ));
2278 } else {
2279 self.replace_next_project_load_in_active_tab = false;
2280 ctx.ui.send(TheEvent::SetStatusText(
2281 TheId::empty(),
2282 "Unable to load project!".to_string(),
2283 ));
2284 }
2285 }
2286 }
2287 } else if id.name == "Save As" {
2288 for p in paths {
2289 self.persist_active_region_view_state();
2290 let json = serde_json::to_string(&self.project);
2291 if let Ok(json) = json {
2292 if std::fs::write(p.clone(), json).is_ok() {
2293 self.project_path = Some(p);
2294 UNDOMANAGER.write().unwrap().mark_saved();
2295 DOCKMANAGER.write().unwrap().mark_saved();
2296 if self.active_session < self.sessions.len() {
2297 self.sessions[self.active_session].dirty = false;
2298 }
2299 self.sync_active_session_from_editor();
2300 self.rebuild_project_tabs(ui);
2301 ctx.ui.send(TheEvent::SetStatusText(
2302 TheId::empty(),
2303 "Project saved successfully.".to_string(),
2304 ))
2305 } else {
2306 ctx.ui.send(TheEvent::SetStatusText(
2307 TheId::empty(),
2308 "Unable to save project!".to_string(),
2309 ))
2310 }
2311 }
2312 }
2313 }
2314 }
2315 TheEvent::StateChanged(id, state) => {
2316 if id.name == "Help" {
2317 self.server_ctx.help_mode = state == TheWidgetState::Clicked;
2318 }
2319 if id.name == "GameInput" {
2320 self.server_ctx.game_input_mode = state == TheWidgetState::Clicked;
2321 } else if id.name == "New" {
2322 let mut project = Project::default();
2323 if let Some(bytes) = crate::Embedded::get("starter_project.eldiron")
2324 && let Ok(project_string) = std::str::from_utf8(bytes.data.as_ref())
2325 && let Ok(loaded) = serde_json::from_str(&project_string.to_string())
2326 {
2327 project = loaded;
2328 }
2329 Self::sanitize_loaded_project(&mut project);
2330
2331 self.sync_active_session_from_editor();
2332 let new_index = if self.replace_next_project_load_in_active_tab {
2333 self.sessions[self.active_session] = ProjectSession {
2334 project,
2335 project_path: None,
2336 undo: UndoManager::default(),
2337 dirty: false,
2338 };
2339 self.replace_next_project_load_in_active_tab = false;
2340 self.active_session
2341 } else {
2342 self.sessions.push(ProjectSession {
2343 project,
2344 project_path: None,
2345 undo: UndoManager::default(),
2346 dirty: false,
2347 });
2348 self.sessions.len() - 1
2349 };
2350 self.switch_to_session(
2351 new_index,
2352 ui,
2353 ctx,
2354 &mut update_server_icons,
2355 &mut redraw,
2356 );
2357
2358 ctx.ui.send(TheEvent::SetStatusText(
2359 TheId::empty(),
2360 "New project successfully initialized.".to_string(),
2361 ));
2362 } else if id.name == "Logo" {
2363 _ = open::that("https://eldiron.com");
2364 ctx.ui
2365 .set_widget_state("Logo".to_string(), TheWidgetState::None);
2366 ctx.ui.clear_hover();
2367 redraw = true;
2368 } else if id.name == "Patreon" {
2369 _ = open::that("https://www.patreon.com/eldiron");
2370 ctx.ui
2371 .set_widget_state("Patreon".to_string(), TheWidgetState::None);
2372 ctx.ui.clear_hover();
2373 redraw = true;
2374 } else if id.name == "Update" {
2375 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2376 {
2377 let updater = self.self_updater.lock().unwrap();
2378
2379 if updater.has_newer_release() {
2380 self.self_update_tx
2381 .send(SelfUpdateEvent::UpdateConfirm(
2382 updater.latest_release().cloned().unwrap(),
2383 ))
2384 .unwrap();
2385 } else {
2386 if let Some(statusbar) = ui.get_widget("Statusbar") {
2387 statusbar
2388 .as_statusbar()
2389 .unwrap()
2390 .set_text(fl!("info_update_check"));
2391 }
2392
2393 let updater = Arc::clone(&self.self_updater);
2394 let tx = self.self_update_tx.clone();
2395
2396 thread::spawn(move || {
2397 let mut updater = updater.lock().unwrap();
2398
2399 match updater.fetch_release_list() {
2400 Ok(_) => {
2401 if updater.has_newer_release() {
2402 tx.send(SelfUpdateEvent::UpdateConfirm(
2403 updater.latest_release().cloned().unwrap(),
2404 ))
2405 .unwrap();
2406 } else {
2407 tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
2408 }
2409 }
2410 Err(err) => {
2411 tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
2412 .unwrap();
2413 }
2414 }
2415 });
2416 }
2417
2418 ctx.ui
2419 .set_widget_state("Update".to_string(), TheWidgetState::None);
2420 ctx.ui.clear_hover();
2421 redraw = true;
2422 }
2423 } else if id.name == "Open" {
2424 ctx.ui.open_file_requester(
2425 TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2426 "Open".into(),
2427 TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2428 );
2429 ctx.ui
2430 .set_widget_state("Open".to_string(), TheWidgetState::None);
2431 ctx.ui.clear_hover();
2432 redraw = true;
2433 } else if id.name == "Close" {
2434 if self.active_session_has_changes() {
2435 let uuid = Uuid::new_v4();
2436 let width = 380;
2437 let height = 110;
2438
2439 let mut canvas = TheCanvas::new();
2440 canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2441
2442 let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2443 hlayout.limiter_mut().set_max_width(width);
2444
2445 let mut text_widget =
2446 TheText::new(TheId::named_with_id("Dialog Value", uuid));
2447 text_widget.set_text(
2448 "This tab has unsaved changes. Close it anyway?".to_string(),
2449 );
2450 text_widget.limiter_mut().set_max_width(280);
2451 hlayout.add_widget(Box::new(text_widget));
2452
2453 canvas.set_layout(hlayout);
2454 ui.show_dialog(
2455 "Close Project Tab",
2456 canvas,
2457 vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2458 ctx,
2459 );
2460 } else {
2461 self.close_active_session(
2462 ui,
2463 ctx,
2464 &mut update_server_icons,
2465 &mut redraw,
2466 );
2467 }
2468 ctx.ui
2469 .set_widget_state("Close".to_string(), TheWidgetState::None);
2470 ctx.ui.clear_hover();
2471 redraw = true;
2472 } else if id.name == "Save" {
2473 if let Some(path) = self.project_path.clone() {
2474 let mut success = false;
2475 self.persist_active_region_view_state();
2477 if let Ok(output) = serde_json::to_string(&self.project) {
2478 if std::fs::write(&path, output).is_ok() {
2479 UNDOMANAGER.write().unwrap().mark_saved();
2480 DOCKMANAGER.write().unwrap().mark_saved();
2481 if self.active_session < self.sessions.len() {
2482 self.sessions[self.active_session].dirty = false;
2483 }
2484 self.sync_active_session_from_editor();
2485 self.rebuild_project_tabs(ui);
2486 ctx.ui.send(TheEvent::SetStatusText(
2487 TheId::empty(),
2488 "Project saved successfully.".to_string(),
2489 ));
2490 success = true;
2491 }
2492 }
2493
2494 if !success {
2495 ctx.ui.send(TheEvent::SetStatusText(
2496 TheId::empty(),
2497 "Unable to save project!".to_string(),
2498 ))
2499 }
2500 } else {
2501 ctx.ui.send(TheEvent::StateChanged(
2502 TheId::named("Save As"),
2503 TheWidgetState::Clicked,
2504 ));
2505 ctx.ui
2506 .set_widget_state("Save".to_string(), TheWidgetState::None);
2507 }
2508 } else if id.name == "Save As" {
2509 ctx.ui.save_file_requester(
2510 TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2511 "Save".into(),
2512 TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2513 );
2514 ctx.ui
2515 .set_widget_state("Save As".to_string(), TheWidgetState::None);
2516 ctx.ui.clear_hover();
2517 redraw = true;
2518 }
2519 else if id.name == "Play" {
2521 let state = RUSTERIX.read().unwrap().server.state;
2522 if state == rusterix::ServerState::Paused {
2523 RUSTERIX.write().unwrap().server.continue_instances();
2524 update_server_icons = true;
2525 } else {
2526 if state == rusterix::ServerState::Off {
2527 start_server(
2528 &mut RUSTERIX.write().unwrap(),
2529 &mut self.project,
2530 false,
2531 );
2532 RUSTERIX.write().unwrap().clear_say_messages();
2533 let commands =
2534 setup_client(&mut RUSTERIX.write().unwrap(), &mut self.project);
2535 RUSTERIX
2536 .write()
2537 .unwrap()
2538 .server
2539 .process_client_commands(commands);
2540 ctx.ui.send(TheEvent::SetStatusText(
2541 TheId::empty(),
2542 "Server has been started.".to_string(),
2543 ));
2544 ui.set_widget_value("LogEdit", ctx, TheValue::Text(String::new()));
2545 self.last_processed_log_len = 0;
2546 RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2547 }
2548 update_server_icons = true;
2549 }
2550 } else if id.name == "Pause" {
2551 let state = RUSTERIX.read().unwrap().server.state;
2552 if state == rusterix::ServerState::Running {
2553 RUSTERIX.write().unwrap().server.pause();
2554 update_server_icons = true;
2555 }
2556 } else if id.name == "Stop" {
2557 RUSTERIX.write().unwrap().server.stop();
2558 RUSTERIX.write().unwrap().clear_say_messages();
2559 RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2560
2561 ui.set_widget_value("InfoView", ctx, TheValue::Text("".into()));
2562 insert_content_into_maps(&mut self.project);
2563 update_server_icons = true;
2564
2565 ctx.ui.send(TheEvent::Custom(
2566 TheId::named("Render SceneManager Map"),
2567 TheValue::Empty,
2568 ));
2569 } else if id.name == "Undo" || id.name == "Redo" {
2570 let mut refresh_action_ui = false;
2571 if ui.focus_widget_supports_undo_redo(ctx) {
2572 if id.name == "Undo" {
2573 ui.undo(ctx);
2574 } else {
2575 ui.redo(ctx);
2576 }
2577 } else if DOCKMANAGER.read().unwrap().current_dock_supports_undo() {
2578 if id.name == "Undo" {
2579 DOCKMANAGER.write().unwrap().undo(
2580 ui,
2581 ctx,
2582 &mut self.project,
2583 &mut self.server_ctx,
2584 );
2585 } else {
2586 DOCKMANAGER.write().unwrap().redo(
2587 ui,
2588 ctx,
2589 &mut self.project,
2590 &mut self.server_ctx,
2591 );
2592 }
2593 refresh_action_ui = true;
2594 } else {
2595 let mut manager = UNDOMANAGER.write().unwrap();
2596
2597 if id.name == "Undo" {
2598 manager.undo(&mut self.server_ctx, &mut self.project, ui, ctx);
2599 } else {
2600 manager.redo(&mut self.server_ctx, &mut self.project, ui, ctx);
2601 }
2602 refresh_action_ui = true;
2603 }
2604
2605 if refresh_action_ui {
2607 ctx.ui.clear_focus();
2609 {
2612 let mut module = CODEGRIDFX.write().unwrap();
2613 module.clear_toolbar_settings(ui, ctx);
2614 module.show_settings(ui, ctx);
2615 }
2616 ctx.ui.send(TheEvent::Custom(
2617 TheId::named("Update Action List"),
2618 TheValue::Empty,
2619 ));
2620 ctx.ui.send(TheEvent::Custom(
2621 TheId::named("Update Action Parameters"),
2622 TheValue::Empty,
2623 ));
2624 }
2625 } else if id.name == "Cut" {
2626 if ui.focus_widget_supports_clipboard(ctx) {
2627 ui.cut(ctx);
2629 } else {
2630 ctx.ui.send(TheEvent::Cut);
2632 }
2633 } else if id.name == "Copy" {
2634 if ui.focus_widget_supports_clipboard(ctx) {
2635 ui.copy(ctx);
2637 } else {
2638 ctx.ui.send(TheEvent::Copy);
2640 }
2641 } else if id.name == "Paste" {
2642 if ui.focus_widget_supports_clipboard(ctx) {
2643 ui.paste(ctx);
2645 } else {
2646 if let Some(value) = &ctx.ui.clipboard {
2648 ctx.ui.send(TheEvent::Paste(
2649 value.clone(),
2650 ctx.ui.clipboard_app_type.clone(),
2651 ));
2652 } else {
2653 ctx.ui.send(TheEvent::Paste(
2654 TheValue::Empty,
2655 ctx.ui.clipboard_app_type.clone(),
2656 ));
2657 }
2658 }
2659 }
2660 }
2661 TheEvent::ValueChanged(id, value) => {
2662 if id.name == "Server Time Slider" {
2663 if let TheValue::Time(time) = value {
2664 self.project.time = time;
2665 let mut rusterix = RUSTERIX.write().unwrap();
2666 rusterix.client.set_server_time(time);
2667
2668 if rusterix.server.state == rusterix::ServerState::Running {
2669 if let Some(map) = self.project.get_map(&self.server_ctx) {
2670 rusterix.server.set_time(&map.id, time);
2671 }
2672 }
2673 }
2674 }
2675 }
2676 _ => {}
2677 }
2678 }
2679
2680 #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2681 while let Ok(event) = self.self_update_rx.try_recv() {
2682 match event {
2683 SelfUpdateEvent::AlreadyUpToDate => {
2684 let text = str!("Eldiron is already up-to-date.");
2685 let uuid = Uuid::new_v4();
2686
2687 let width = 300;
2688 let height = 100;
2689
2690 let mut canvas = TheCanvas::new();
2691 canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2692
2693 let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2694 hlayout.limiter_mut().set_max_width(width);
2695
2696 let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2697 text_widget.set_text(text.to_string());
2698 text_widget.limiter_mut().set_max_width(200);
2699 hlayout.add_widget(Box::new(text_widget));
2700
2701 canvas.set_layout(hlayout);
2702
2703 ui.show_dialog(
2704 "Eldiron Up-to-Date",
2705 canvas,
2706 vec![TheDialogButtonRole::Accept],
2707 ctx,
2708 );
2709 }
2710 SelfUpdateEvent::UpdateCompleted(release) => {
2711 if let Some(statusbar) = ui.get_widget("Statusbar") {
2712 statusbar.as_statusbar().unwrap().set_text(format!(
2713 "Updated to version {}. Please restart the application to enjoy the new features.",
2714 release.version
2715 ));
2716 }
2717 }
2718 SelfUpdateEvent::UpdateConfirm(release) => {
2719 let text = &format!("Update to version {}?", release.version);
2720 let uuid = Uuid::new_v4();
2721
2722 let width = 300;
2723 let height = 100;
2724
2725 let mut canvas = TheCanvas::new();
2726 canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2727
2728 let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2729 hlayout.limiter_mut().set_max_width(width);
2730
2731 let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2732 text_widget.set_text(text.to_string());
2733 text_widget.limiter_mut().set_max_width(200);
2734 hlayout.add_widget(Box::new(text_widget));
2735
2736 canvas.set_layout(hlayout);
2737
2738 ui.show_dialog(
2739 "Update Eldiron",
2740 canvas,
2741 vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2742 ctx,
2743 );
2744 }
2745 SelfUpdateEvent::UpdateError(err) => {
2746 if let Some(statusbar) = ui.get_widget("Statusbar") {
2747 statusbar
2748 .as_statusbar()
2749 .unwrap()
2750 .set_text(format!("Failed to update Eldiron: {err}"));
2751 }
2752 }
2753 SelfUpdateEvent::UpdateStart(release) => {
2754 if let Some(statusbar) = ui.get_widget("Statusbar") {
2755 statusbar
2756 .as_statusbar()
2757 .unwrap()
2758 .set_text(format!("Updating to version {}...", release.version));
2759 }
2760 }
2761 }
2762 }
2763
2764 if update_server_icons {
2765 self.update_server_state_icons(ui);
2766 redraw = true;
2767 }
2768
2769 let active_dirty = UNDOMANAGER.read().unwrap().has_unsaved()
2770 || DOCKMANAGER.read().unwrap().has_dock_changes();
2771 if self.active_session < self.sessions.len()
2772 && self.sessions[self.active_session].dirty != active_dirty
2773 {
2774 self.sessions[self.active_session].dirty = active_dirty;
2775 self.rebuild_project_tabs(ui);
2776 redraw = true;
2777 }
2778 if active_dirty != self.last_active_dirty {
2779 self.last_active_dirty = active_dirty;
2780 self.rebuild_project_tabs(ui);
2781 redraw = true;
2782 }
2783
2784 self.update_counter += 1;
2785 if self.update_counter > 2 {
2786 self.sidebar.startup = false;
2787 }
2788 redraw
2789 }
2790
2791 fn has_changes(&self) -> bool {
2793 if self.active_session_has_changes() {
2794 return true;
2795 }
2796
2797 for (index, session) in self.sessions.iter().enumerate() {
2798 if index != self.active_session && session.dirty {
2799 return true;
2800 }
2801 }
2802
2803 false
2804 }
2805
2806 fn window_moved(&mut self, x: i32, y: i32) {
2807 self.window_state.x = Some(x);
2808 self.window_state.y = Some(y);
2809 self.save_window_state();
2810 }
2811
2812 fn window_resized(&mut self, width: usize, height: usize) {
2813 if width > 0 && height > 0 {
2814 self.window_state.width = Some(width);
2815 self.window_state.height = Some(height);
2816 self.save_window_state();
2817 }
2818 }
2819}
2820
2821pub trait EldironEditor {
2822 fn update_server_state_icons(&mut self, ui: &mut TheUI);
2823}
2824
2825impl EldironEditor for Editor {
2826 fn update_server_state_icons(&mut self, ui: &mut TheUI) {
2827 let rusterix = RUSTERIX.read().unwrap();
2828 if rusterix.server.state == rusterix::ServerState::Running {
2829 if let Some(button) = ui.get_widget("Play") {
2830 if let Some(button) = button.as_menubar_button() {
2831 button.set_icon_name("play-fill".to_string());
2832 }
2833 }
2834 if let Some(button) = ui.get_widget("Pause") {
2835 if let Some(button) = button.as_menubar_button() {
2836 button.set_icon_name("play-pause".to_string());
2837 }
2838 }
2839 if let Some(button) = ui.get_widget("Stop") {
2840 if let Some(button) = button.as_menubar_button() {
2841 button.set_icon_name("stop".to_string());
2842 }
2843 }
2844 } else if rusterix.server.state == rusterix::ServerState::Paused {
2845 if let Some(button) = ui.get_widget("Play") {
2846 if let Some(button) = button.as_menubar_button() {
2847 button.set_icon_name("play".to_string());
2848 }
2849 }
2850 if let Some(button) = ui.get_widget("Pause") {
2851 if let Some(button) = button.as_menubar_button() {
2852 button.set_icon_name("play-pause-fill".to_string());
2853 }
2854 }
2855 if let Some(button) = ui.get_widget("Stop") {
2856 if let Some(button) = button.as_menubar_button() {
2857 button.set_icon_name("stop".to_string());
2858 }
2859 }
2860 } else if rusterix.server.state == rusterix::ServerState::Off {
2861 if let Some(button) = ui.get_widget("Play") {
2862 if let Some(button) = button.as_menubar_button() {
2863 button.set_icon_name("play".to_string());
2864 }
2865 }
2866 if let Some(button) = ui.get_widget("Pause") {
2867 if let Some(button) = button.as_menubar_button() {
2868 button.set_icon_name("play-pause".to_string());
2869 }
2870 }
2871 if let Some(button) = ui.get_widget("Stop") {
2872 if let Some(button) = button.as_menubar_button() {
2873 button.set_icon_name("stop-fill".to_string());
2874 }
2875 }
2876 }
2877 }
2878}