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::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 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 ui.set_statusbar_name("Statusbar".to_string());
721
722 let mut top_canvas = TheCanvas::new();
723 #[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 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_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 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 #[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 self.sidebar.init_ui(ui, ctx, &mut self.server_ctx);
968
969 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 DOCKMANAGER
980 .write()
981 .unwrap()
982 .add_editors_to_stack(&mut editor_stack, ctx);
983
984 editor_canvas.set_layout(editor_stack);
985
986 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 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 ctx.ui.set_disabled("Undo");
1045 ctx.ui.set_disabled("Redo");
1046
1047 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 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 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 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 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 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 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 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 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_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 {
1245 let rusterix = &mut RUSTERIX.write().unwrap();
1246 if rusterix.server.state == rusterix::ServerState::Running {
1247 if tick_update {
1249 rusterix.server.system_tick();
1250 }
1251
1252 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 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 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 !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 let animation_frame = rusterix.client.animation_frame;
1413 rusterix.build_dynamics_3d(®ion.map, animation_frame);
1414 rusterix.draw_d3(
1415 ®ion.map,
1416 render_view.render_buffer_mut().pixels_mut(),
1417 dim.width as usize,
1418 dim.height as usize,
1419 );
1420 }
1421 } else
1422 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 if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1456 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.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 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 rusterix.process_messages(®ion.map, says);
1496
1497 }
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 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 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 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 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 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 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 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 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 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 §or.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 TheEvent::FileRequesterResult(id, paths) => {
2240 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 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 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 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 if refresh_action_ui {
2700 ctx.ui.clear_focus();
2702 {
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 ui.cut(ctx);
2722 } else {
2723 ctx.ui.send(TheEvent::Cut);
2725 }
2726 } else if id.name == "Copy" {
2727 if ui.focus_widget_supports_clipboard(ctx) {
2728 ui.copy(ctx);
2730 } else {
2731 ctx.ui.send(TheEvent::Copy);
2733 }
2734 } else if id.name == "Paste" {
2735 if ui.focus_widget_supports_clipboard(ctx) {
2736 ui.paste(ctx);
2738 } else {
2739 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 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}