From f4fa07890f5b374562239c2d6e695c6e29cf881f Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 17:34:04 +0100 Subject: [PATCH 01/54] save --- Cargo.toml | 3 +- README.md | 2 + TODO.txt | 4 + crates/joko_core/Cargo.toml | 1 + crates/joko_marker_format/Cargo.toml | 1 + .../joko_marker_format/src/io/deserialize.rs | 113 +++++++++++++----- crates/joko_marker_format/src/io/mod.rs | 2 + crates/joko_marker_format/src/io/serialize.rs | 13 +- .../src/manager/live_pack.rs | 82 ++++++++----- crates/joko_marker_format/src/pack/common.rs | 51 +++++--- crates/joko_marker_format/src/pack/mod.rs | 21 +++- 11 files changed, 199 insertions(+), 94 deletions(-) create mode 100644 TODO.txt diff --git a/Cargo.toml b/Cargo.toml index 1646b73..40fadf5 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ resolver = "2" [workspace.dependencies] -tracing = { version = "0.1" } +tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } ringbuffer = { version = "0.14" } egui = { version = "*" } egui_extras = { version = "*" } @@ -36,3 +36,4 @@ indexmap = { version = "2" } rfd = { version = "*" } smol_str = { version = "*" } itertools = { version = "*" } +ordered_hash_map = { version = "*", features= ["serde"] } diff --git a/README.md b/README.md index 5beccae..905eb9f 100755 --- a/README.md +++ b/README.md @@ -36,3 +36,5 @@ for now, just look at the github workflow file. I will primarily be testing `Jokolay` on `Endeavour (Arch) OS` / `KDE Plasma` latest. need more guinea pigs to test things on other DEs. +=> try winit +https://docs.rs/winit/latest/winit/index.html \ No newline at end of file diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..5a813cf --- /dev/null +++ b/TODO.txt @@ -0,0 +1,4 @@ +implement + +Window package may have files with backslash in the name, simply renaming the path shall not work, one need to also look up for alternative. + We may have packages flattened in one big folder with files with strange names. diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index fe8a74d..941a478 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -26,3 +26,4 @@ serde_json = { workspace = true } indexmap = { workspace = true } rfd = { workspace = true } glam = { workspace = true } +ordered_hash_map = { workspace = true } diff --git a/crates/joko_marker_format/Cargo.toml b/crates/joko_marker_format/Cargo.toml index c901644..0721c2c 100755 --- a/crates/joko_marker_format/Cargo.toml +++ b/crates/joko_marker_format/Cargo.toml @@ -36,6 +36,7 @@ itertools = { workspace = true } time = { workspace = true , features = ["serde"]} phf = { version = "*", features = ["macros"] } paste = { version = "*" } +ordered_hash_map = { workspace = true } joko_render = { path = "../joko_render" } jokolink = { path = "../jokolink" } jokoapi = { path = "../jokoapi" } diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index bd12fe8..85cfc8e 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -1,5 +1,5 @@ use crate::{ - pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, Trail}, + pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, Trail, Texture, MapData}, BASE64_ENGINE, }; use base64::Engine; @@ -8,7 +8,8 @@ use glam::Vec3; use indexmap::IndexMap; use miette::{bail, Context, IntoDiagnostic, Result}; use std::{collections::BTreeMap, io::Read}; -use tracing::{info, info_span, instrument, warn}; +use ordered_hash_map::OrderedHashMap; +use tracing::{info, info_span, instrument, trace, warn}; use uuid::Uuid; use xot::{Node, Xot}; @@ -41,7 +42,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { .wrap_err("map data entry name not utf-8")? .to_string(); - if name.ends_with("xml") { + if name.ends_with(".xml") { if let Some(name) = name.strip_suffix(".xml") { match name { "categories" => { @@ -74,15 +75,17 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { } } } - } + } + } else { + trace!("file ignored: {name}") } } Ok(pack) } fn recursive_walk_dir_and_read_images_and_tbins( dir: &Dir, - images: &mut BTreeMap>, - tbins: &mut BTreeMap, + images: &mut OrderedHashMap, + tbins: &mut OrderedHashMap, parent_path: &RelativePath, ) -> Result<()> { for entry in dir @@ -112,7 +115,12 @@ fn recursive_walk_dir_and_read_images_and_tbins( .into_diagnostic() .wrap_err("failed to read file contents")?; if name.ends_with("png") { - images.insert(path, bytes); + images.insert(path.clone(), Texture{ //it is the presence in the directory that defines the source of information + path: path.clone(), + original: parent_path.to_string(), + source: parent_path.to_string(), + bytes + }); } else if name.ends_with("trl") { if let Some(tbin) = parse_tbin_from_slice(&bytes) { tbins.insert(path, tbin); @@ -208,7 +216,7 @@ fn recursive_marker_category_parser( let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); + let display_name = ele.get_attribute(names.display_name).unwrap_or(name); let separator = ele .get_attribute(names.separator) @@ -313,6 +321,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) }) .ok_or_else(|| miette::miette!("invalid guid"))?; + //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail if child.name() == names.poi { if child .get_attribute(names.map_id) @@ -348,7 +357,10 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result guid, }; - pack.maps.entry(map_id).or_default().markers.push(marker); + if !pack.maps.contains_key(&map_id) { + pack.maps.insert(map_id, MapData::default()); + } + pack.maps.get_mut(&map_id).unwrap().markers.push(marker); } else if child.name() == names.trail { if child .get_attribute(names.map_id) @@ -367,7 +379,11 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result props: ca, guid, }; - pack.maps.entry(map_id).or_default().trails.push(trail); + + if !pack.maps.contains_key(&map_id) { + pack.maps.insert(map_id, MapData::default()); + } + pack.maps.get_mut(&map_id).unwrap().trails.push(trail); } } } @@ -384,6 +400,7 @@ fn recursive_marker_category_parser_categories_xml( for tag in tags { if let Some(ele) = tree.element(tag) { if ele.name() != names.marker_category { + let name = ele.name(); continue; } @@ -396,7 +413,7 @@ fn recursive_marker_category_parser_categories_xml( let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); + let display_name = ele.get_attribute(names.display_name).unwrap_or(name); let separator = match ele.get_attribute(names.separator).unwrap_or("0") { "0" => false, @@ -431,6 +448,8 @@ fn recursive_marker_category_parser_categories_xml( names, ); std::mem::drop(span_guard); + } else { + info!("ignore tag: {:?}", tag); } } } @@ -444,7 +463,7 @@ fn recursive_marker_category_parser_categories_xml( #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { // all the contents of ZPack - let mut pack = PackCore::default(); + let mut new_pack = PackCore::default(); // parse zip file let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) .into_diagnostic() @@ -457,25 +476,32 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { // we collect the names first, because reading a file from zip is a mutating operation. // So, we can't iterate AND read the file at the same time for name in zip_archive.file_names() { - if name.ends_with("png") { - images.push(name.to_string()); - } else if name.ends_with("trl") { - tbins.push(name.to_string()); - } else if name.ends_with("xml") { - xmls.push(name.to_string()); - } else if name.ends_with('/') { - // directory. so, we can ignore this. + let name_as_string = name.to_string(); + if name_as_string.ends_with("png") { + images.push(name_as_string); + } else if name_as_string.ends_with("trl") { + tbins.push(name_as_string); + } else if name_as_string.ends_with("xml") { + xmls.push(name_as_string); + } else if name_as_string.replace("\\", "/").ends_with('/') { + // directory. so, we can silently ignore this. } else { info!("ignoring file: {name}"); } } + xmls.sort();//build back the intended order in folder, since zip_archive may not give the files in order. for name in images { let span = info_span!("load image", name).entered(); - let file_path: RelativePath = name.parse().unwrap(); + let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { Ok(_) => assert!( - pack.textures.insert(file_path, bytes).is_none(), + new_pack.textures.insert(file_path.clone(), Texture{ + path: file_path.clone(), + original: name.clone(), + source: String::from(std::str::from_utf8(taco).unwrap_or_default()), + bytes + }).is_none(), "duplicate image file {name}" ), Err(e) => { @@ -489,11 +515,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { for name in tbins { let span = info_span!("load tbin {name}").entered(); - let file_path: RelativePath = name.parse().unwrap(); + let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { if let Some(tbin) = parse_tbin_from_slice(&bytes) { assert!( - pack.tbins.insert(file_path, tbin).is_none(), + new_pack.tbins.insert(file_path, tbin).is_none(), "duplicate tbin file {name}" ); } else { @@ -541,7 +567,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }; // parse_categories - recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names); + recursive_marker_category_parser(&tree, tree.children(od), &mut new_pack.categories, &names); let pois = match tree.children(od).find(|node| { tree.element(*node) @@ -605,8 +631,14 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(child, &names); if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - info!(%icon_file, "failed to find this texture in this pack"); + if !new_pack.textures.contains_key(icon_file) { + let alternative_icon_file = icon_file.alternative(); + if new_pack.textures.contains_key(&alternative_icon_file) { + info!(%alternative_icon_file, "renamed texture to alternative"); + common_attributes.set_icon_file(Some(alternative_icon_file)); + } else { + info!(%icon_file, "failed to find this texture in this pack"); + } } } else if let Some(icf) = child.get_attribute(names.icon_file) { info!(icf, "marker's icon file attribute failed to parse"); @@ -618,7 +650,10 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { attrs: common_attributes, guid, }; - pack.maps.entry(map_id).or_default().markers.push(marker); + if !new_pack.maps.contains_key(&map_id) { + new_pack.maps.insert(map_id, MapData::default()); + } + new_pack.maps.get_mut(&map_id).unwrap().markers.push(marker); } else { info!("missing map id") } @@ -627,14 +662,23 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { .get_attribute(names.trail_data) .and_then(|trail_data| { let path: RelativePath = trail_data.parse().unwrap(); - pack.tbins.get(&path).map(|tb| tb.map_id) + new_pack.tbins.get(&path).map(|tb| tb.map_id) }) { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(child, &names); if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) {} + if !new_pack.textures.contains_key(tex) { + let alternative_tex_file = tex.alternative(); + if new_pack.textures.contains_key(&alternative_tex_file) { + info!(%alternative_tex_file, "renamed texture to alternative"); + common_attributes.set_texture(Some(alternative_tex_file)); + } else { + info!(%tex, "failed to find this texture in this pack"); + } + } + } let trail = Trail { @@ -643,11 +687,14 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { props: common_attributes, guid, }; - pack.maps.entry(map_id).or_default().trails.push(trail); + if !new_pack.maps.contains_key(&map_id) { + new_pack.maps.insert(map_id, MapData::default()); + } + new_pack.maps.get_mut(&map_id).unwrap().trails.push(trail); } else { let td = child.get_attribute(names.trail_data); let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); + let tbin = new_pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); info!("missing map_id: {td:?} {rp} {tbin:?}"); } } else { @@ -658,7 +705,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { drop(span_guard); } - Ok(pack) + Ok(new_pack) } #[instrument(skip(zip_archive))] fn read_file_bytes_from_zip_by_name( diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs index d4e114e..937d5f1 100644 --- a/crates/joko_marker_format/src/io/mod.rs +++ b/crates/joko_marker_format/src/io/mod.rs @@ -16,6 +16,7 @@ pub(crate) struct XotAttributeNameIDs { pub pois: NameId, pub poi: NameId, pub trail: NameId, + pub route: NameId, // marker specific attributes pub category: NameId, pub guid: NameId, @@ -97,6 +98,7 @@ impl XotAttributeNameIDs { pois: tree.add_name("POIs"), poi: tree.add_name("POI"), trail: tree.add_name("Trail"), + route: tree.add_name("Route"), // non inheritable attributes category: tree.add_name("type"), xpos: tree.add_name("xpos"), diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index 51c2a8a..9dbc6ba 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -1,12 +1,13 @@ use crate::{ - pack::{Category, Marker, PackCore, RelativePath, Trail}, + pack::{Category, Marker, PackCore, RelativePath, Trail, Texture}, BASE64_ENGINE, }; use base64::Engine; use cap_std::fs_utf8::Dir; use indexmap::IndexMap; use miette::{Context, IntoDiagnostic, Result}; -use std::{collections::HashSet, io::Write}; +use std::{io::Write}; +use ordered_hash_map::{OrderedHashSet}; use tracing::info; use xot::{Element, Node, SerializeOptions, Xot}; @@ -16,9 +17,9 @@ pub(crate) fn save_pack_core_to_dir( pack_core: &PackCore, dir: &Dir, cats: bool, - mut maps: HashSet, - mut textures: HashSet, - mut tbins: HashSet, + mut maps: OrderedHashSet, + mut textures: OrderedHashSet, + mut tbins: OrderedHashSet, all: bool, ) -> Result<()> { if cats || all { @@ -117,7 +118,7 @@ pub(crate) fn save_pack_core_to_dir( dir.create(img_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(img) + .write(&img.bytes) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to write image bytes to file: {img_path}") diff --git a/crates/joko_marker_format/src/manager/live_pack.rs b/crates/joko_marker_format/src/manager/live_pack.rs index 8bc0ada..9acf348 100644 --- a/crates/joko_marker_format/src/manager/live_pack.rs +++ b/crates/joko_marker_format/src/manager/live_pack.rs @@ -1,7 +1,7 @@ use std::{ - collections::{HashMap, HashSet}, sync::Arc, }; +use ordered_hash_map::{OrderedHashMap, OrderedHashSet}; use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; @@ -9,7 +9,7 @@ use glam::{vec2, Vec2, Vec3}; use image::EncodableLayout; use indexmap::IndexMap; use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, trace}; use uuid::Uuid; use crate::{ @@ -30,7 +30,7 @@ pub(crate) struct LoadedPack { /// The actual xml pack. pub core: PackCore, /// The selection of categories which are "enabled" and markers belonging to these may be rendered - cats_selection: HashMap, + cats_selection: OrderedHashMap, dirty: Dirty, activation_data: ActivationData, current_map_data: CurrentMapData, @@ -44,11 +44,11 @@ struct Dirty { /// whether cats selection needs to be saved cats_selection: bool, /// Whether any mapdata needs saving - map_dirty: HashSet, + map_dirty: OrderedHashSet, /// whether any texture needs saving - texture: HashSet, + texture: OrderedHashSet, /// whether any tbin needs saving - tbin: HashSet, + tbin: OrderedHashSet, } impl Dirty { @@ -97,11 +97,16 @@ impl LoadedPack { } } pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { - CategorySelection::recursive_selection_ui( - &mut self.cats_selection, - ui, - &mut self.dirty.cats_selection, - ); + //TODO: find a way to merge categories (see LadyElyssa pack) + //it is important to generate a new id each time to avoid collision + //or to do a look up of an already existing menu + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + &mut self.cats_selection, + ui, + &mut self.dirty.cats_selection, + ); + }); } pub fn load_from_dir(dir: Arc) -> Result { if !dir @@ -224,7 +229,7 @@ impl LoadedPack { link: &MumbleLink, default_tex_id: &TextureHandle, ) { - info!( + trace!( self.current_map_data.map_id, link.map_id, "current map data is updated." ); @@ -241,6 +246,7 @@ impl LoadedPack { "", &Default::default(), ); + let mut failure_loading = false; for (index, marker) in self .core .maps @@ -302,7 +308,7 @@ impl LoadedPack { if let Some(tex_path) = attrs.get_icon_file() { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); + let img = image::load_from_memory(&tex.bytes).unwrap(); self.current_map_data.active_textures.insert( tex_path.clone(), etx.load_texture( @@ -315,11 +321,12 @@ impl LoadedPack { ), ); } else { - info!(%tex_path, ?self.core.textures, "failed to find this texture"); + info!(%tex_path, "failed to find this icon texture"); + failure_loading = true; } } } else { - info!("no texture attribute on this marker"); + trace!("no texture attribute on this marker"); } let th = attrs .get_icon_file() @@ -361,7 +368,7 @@ impl LoadedPack { if let Some(tex_path) = common_attributes.get_texture() { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); + let img = image::load_from_memory(&tex.bytes).unwrap(); self.current_map_data.active_textures.insert( tex_path.clone(), etx.load_texture( @@ -374,11 +381,12 @@ impl LoadedPack { ), ); } else { - info!(%tex_path, ?self.core.textures, "failed to find this texture"); + info!(%tex_path, "failed to find this trail texture"); + failure_loading = true; } } } else { - info!("no texture attribute on this marker"); + trace!("no texture attribute on this marker"); } let th = common_attributes .get_texture() @@ -388,13 +396,13 @@ impl LoadedPack { let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { tbin } else { - info!(?trail, "missing tbin path"); + trace!(?trail, "missing tbin path"); continue; }; let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { tbin } else { - info!(%tbin_path, "failed to find tbin"); + trace!(%tbin_path, "failed to find tbin"); continue; }; if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( @@ -408,6 +416,13 @@ impl LoadedPack { } } } + if failure_loading { + info!("Error when loading textures, here are the keys:"); + for k in self.core.textures.keys() { + info!(%k); + } + info!("end of keys"); + } } pub fn save_all(&mut self) -> Result<()> { self.dirty.all = true; @@ -457,7 +472,7 @@ pub(crate) struct CurrentMapData { /// the map to which the current map data belongs to pub map_id: u32, /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: HashMap, + pub active_textures: OrderedHashMap, /// The key is the index of the marker in the map markers /// Their position in the map markers serves as their "id" as uuids can be duplicates. pub active_markers: IndexMap, @@ -493,19 +508,19 @@ pub(crate) struct ActiveMarker { struct CategorySelection { pub selected: bool, pub display_name: String, - pub children: HashMap, + pub children: OrderedHashMap, } impl CategorySelection { - fn default_from_pack_core(pack: &PackCore) -> HashMap { - let mut selection = HashMap::new(); + fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { + let mut selection = OrderedHashMap::new(); Self::recursive_create_category_selection(&mut selection, &pack.categories); selection } fn recursive_get_full_names( - selection: &HashMap, + selection: &OrderedHashMap, cats: &IndexMap, - list: &mut HashMap, + list: &mut OrderedHashMap, parent_name: &str, parent_common_attributes: &CommonAttributes, ) { @@ -533,18 +548,22 @@ impl CategorySelection { } } fn recursive_create_category_selection( - selection: &mut HashMap, + selection: &mut OrderedHashMap, cats: &IndexMap, ) { for (cat_name, cat) in cats.iter() { - let s = selection.entry(cat_name.clone()).or_default(); - s.selected = cat.default_enabled; - s.display_name = cat.display_name.clone(); + if !selection.contains_key(cat_name) { + let mut to_insert = CategorySelection::default(); + to_insert.selected = cat.default_enabled; + to_insert.display_name = cat.display_name.clone(); + selection.insert(cat_name.clone(), to_insert); + } + let mut s = selection.get_mut(cat_name).unwrap(); Self::recursive_create_category_selection(&mut s.children, &cat.children); } } fn recursive_selection_ui( - selection: &mut HashMap, + selection: &mut OrderedHashMap, ui: &mut egui::Ui, changed: &mut bool, ) { @@ -560,6 +579,7 @@ impl CategorySelection { }); } else { ui.label(&cat.display_name); + info!("create category {:?}", cat.display_name); } }); } diff --git a/crates/joko_marker_format/src/pack/common.rs b/crates/joko_marker_format/src/pack/common.rs index 97fd40b..66de647 100644 --- a/crates/joko_marker_format/src/pack/common.rs +++ b/crates/joko_marker_format/src/pack/common.rs @@ -239,30 +239,43 @@ macro_rules! update_attribute_from_ele { /// } /// } /// ``` -macro_rules! update_attribute_bool_from_ele { - ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if let Some(value) = $ele.get_attribute($names.$field) { - match value.trim().parse::() { - Ok(value) => { - match value { + +fn parse_boolean(raw_value: &str) -> Option { + let trimmed = raw_value.trim().to_lowercase(); + match trimmed.as_ref() { + "true" => {Some(true)}, + "false" => {Some(false)}, + _ => { + match trimmed.parse::() {//might entirely get rid of parsing + Ok(parsed_value) => { + match parsed_value { 0 | 1 => { - $common_attributes - .active_attributes - .insert(ActiveAttributes::$field); - $common_attributes.bool_attributes.set( - BoolAttributes::$field, - if value == 0 { false } else { true }, - ); - } - _ => { - info!(value, "failed to parse {}", stringify!($field)); + Some(parsed_value == 1) } + _ => None } } - Err(e) => { - tracing::info!(?e, value, "failed to parse {}", stringify!($field)); + Err(_e) => { + None } } + }, + } +} +macro_rules! update_attribute_bool_from_ele { + ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if let Some(value) = $ele.get_attribute($names.$field) { + if let Some(found) = parse_boolean(value) { + $common_attributes + .active_attributes + .insert(ActiveAttributes::$field); + $common_attributes.bool_attributes.set( + BoolAttributes::$field, + found, + ); + } else { + tracing::info!(value, "failed to parse {}", stringify!($field)); + } })+ }; } @@ -285,7 +298,7 @@ macro_rules! update_attribute_bool_from_ele { /// match value.trim().parse::() { /// Ok(flag) => { /// ca -/// .active_attribus +/// .active_attributes /// .insert(ActiveAttributes::field1); /// ca.field1.set(flag); /// } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index f2fa977..34306e4 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -2,21 +2,30 @@ mod common; mod marker; mod trail; -use std::{collections::BTreeMap, str::FromStr}; +use std::{str::FromStr}; use indexmap::IndexMap; +use ordered_hash_map; pub use common::*; pub(crate) use marker::*; use smol_str::SmolStr; pub(crate) use trail::*; +#[derive(Default, Debug, Clone)] +pub(crate) struct Texture { + pub path: RelativePath, + pub original: String, //raw original name + pub source: String,//where this was defined for the first time + pub bytes: Vec, +} + #[derive(Default, Debug, Clone)] pub(crate) struct PackCore { - pub textures: BTreeMap>, - pub tbins: BTreeMap, + pub textures: ordered_hash_map::OrderedHashMap, + pub tbins: ordered_hash_map::OrderedHashMap, pub categories: IndexMap, - pub maps: BTreeMap, + pub maps: ordered_hash_map::OrderedHashMap, } #[derive(Default, Debug, Clone)] @@ -89,6 +98,9 @@ impl RelativePath { pub fn as_str(&self) -> &str { &self.0 } + pub fn alternative(&self) -> Self { + return Self(self.0.clone().replace("/", "\\").into()); + } } impl std::fmt::Display for RelativePath { @@ -96,6 +108,7 @@ impl std::fmt::Display for RelativePath { self.0.fmt(f) } } + impl From for String { fn from(val: RelativePath) -> String { val.0.into() From 14acb6b6755efc4165be655fffcc2ec0ec8047b9 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:13:23 +0100 Subject: [PATCH 02/54] minimize changes to master --- crates/joko_marker_format/src/io/deserialize.rs | 17 ++--------------- .../joko_marker_format/src/manager/live_pack.rs | 10 +++++----- crates/joko_marker_format/src/pack/mod.rs | 3 --- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 85cfc8e..a9f51d5 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -632,13 +632,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { common_attributes.update_common_attributes_from_element(child, &names); if let Some(icon_file) = common_attributes.get_icon_file() { if !new_pack.textures.contains_key(icon_file) { - let alternative_icon_file = icon_file.alternative(); - if new_pack.textures.contains_key(&alternative_icon_file) { - info!(%alternative_icon_file, "renamed texture to alternative"); - common_attributes.set_icon_file(Some(alternative_icon_file)); - } else { - info!(%icon_file, "failed to find this texture in this pack"); - } + info!(%icon_file, "failed to find this texture in this pack"); } } else if let Some(icf) = child.get_attribute(names.icon_file) { info!(icf, "marker's icon file attribute failed to parse"); @@ -670,15 +664,8 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if let Some(tex) = common_attributes.get_texture() { if !new_pack.textures.contains_key(tex) { - let alternative_tex_file = tex.alternative(); - if new_pack.textures.contains_key(&alternative_tex_file) { - info!(%alternative_tex_file, "renamed texture to alternative"); - common_attributes.set_texture(Some(alternative_tex_file)); - } else { - info!(%tex, "failed to find this texture in this pack"); - } + info!(%tex, "failed to find this texture in this pack"); } - } let trail = Trail { diff --git a/crates/joko_marker_format/src/manager/live_pack.rs b/crates/joko_marker_format/src/manager/live_pack.rs index 9acf348..fdb35b3 100644 --- a/crates/joko_marker_format/src/manager/live_pack.rs +++ b/crates/joko_marker_format/src/manager/live_pack.rs @@ -229,7 +229,7 @@ impl LoadedPack { link: &MumbleLink, default_tex_id: &TextureHandle, ) { - trace!( + info!( self.current_map_data.map_id, link.map_id, "current map data is updated." ); @@ -326,7 +326,7 @@ impl LoadedPack { } } } else { - trace!("no texture attribute on this marker"); + info!("no texture attribute on this marker"); } let th = attrs .get_icon_file() @@ -386,7 +386,7 @@ impl LoadedPack { } } } else { - trace!("no texture attribute on this marker"); + info!("no texture attribute on this marker"); } let th = common_attributes .get_texture() @@ -396,13 +396,13 @@ impl LoadedPack { let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { tbin } else { - trace!(?trail, "missing tbin path"); + info!(?trail, "missing tbin path"); continue; }; let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { tbin } else { - trace!(%tbin_path, "failed to find tbin"); + info!(%tbin_path, "failed to find tbin"); continue; }; if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index 34306e4..12ccbfa 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -98,9 +98,6 @@ impl RelativePath { pub fn as_str(&self) -> &str { &self.0 } - pub fn alternative(&self) -> Self { - return Self(self.0.clone().replace("/", "\\").into()); - } } impl std::fmt::Display for RelativePath { From 53089339653f4383ef294153d138e590d4fa5260 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:17:21 +0100 Subject: [PATCH 03/54] minimize changes to master --- README.md | 2 -- TODO.txt | 4 ---- crates/joko_marker_format/src/io/deserialize.rs | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 TODO.txt diff --git a/README.md b/README.md index 905eb9f..5beccae 100755 --- a/README.md +++ b/README.md @@ -36,5 +36,3 @@ for now, just look at the github workflow file. I will primarily be testing `Jokolay` on `Endeavour (Arch) OS` / `KDE Plasma` latest. need more guinea pigs to test things on other DEs. -=> try winit -https://docs.rs/winit/latest/winit/index.html \ No newline at end of file diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 5a813cf..0000000 --- a/TODO.txt +++ /dev/null @@ -1,4 +0,0 @@ -implement - -Window package may have files with backslash in the name, simply renaming the path shall not work, one need to also look up for alternative. - We may have packages flattened in one big folder with files with strange names. diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index a9f51d5..1951130 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -75,7 +75,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { } } } - } + } } else { trace!("file ignored: {name}") } From 93caac9b8c67f175573a206d1742b6e1d1bec783 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:18:49 +0100 Subject: [PATCH 04/54] minimize changes to master --- crates/joko_marker_format/src/io/deserialize.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 1951130..ade51e1 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -105,7 +105,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( .wrap_err("failed to get file type")? .is_file() { - if path.ends_with("png") || path.ends_with("trl") { + if path.ends_with(".png") || path.ends_with(".trl") { let mut bytes = vec![]; entry .open() @@ -114,14 +114,14 @@ fn recursive_walk_dir_and_read_images_and_tbins( .read_to_end(&mut bytes) .into_diagnostic() .wrap_err("failed to read file contents")?; - if name.ends_with("png") { + if name.ends_with(".png") { images.insert(path.clone(), Texture{ //it is the presence in the directory that defines the source of information path: path.clone(), original: parent_path.to_string(), source: parent_path.to_string(), bytes }); - } else if name.ends_with("trl") { + } else if name.ends_with(".trl") { if let Some(tbin) = parse_tbin_from_slice(&bytes) { tbins.insert(path, tbin); } else { @@ -477,11 +477,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { // So, we can't iterate AND read the file at the same time for name in zip_archive.file_names() { let name_as_string = name.to_string(); - if name_as_string.ends_with("png") { + if name_as_string.ends_with(".png") { images.push(name_as_string); - } else if name_as_string.ends_with("trl") { + } else if name_as_string.ends_with(".trl") { tbins.push(name_as_string); - } else if name_as_string.ends_with("xml") { + } else if name_as_string.ends_with(".xml") { xmls.push(name_as_string); } else if name_as_string.replace("\\", "/").ends_with('/') { // directory. so, we can silently ignore this. From d6772ef90449d5d5d65d90a9c00d72e0887573ee Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:20:44 +0100 Subject: [PATCH 05/54] minimize changes to master --- .../joko_marker_format/src/io/deserialize.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index ade51e1..09e347d 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -463,7 +463,7 @@ fn recursive_marker_category_parser_categories_xml( #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { // all the contents of ZPack - let mut new_pack = PackCore::default(); + let mut pack = PackCore::default(); // parse zip file let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) .into_diagnostic() @@ -496,7 +496,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { Ok(_) => assert!( - new_pack.textures.insert(file_path.clone(), Texture{ + pack.textures.insert(file_path.clone(), Texture{ path: file_path.clone(), original: name.clone(), source: String::from(std::str::from_utf8(taco).unwrap_or_default()), @@ -519,7 +519,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { if let Some(tbin) = parse_tbin_from_slice(&bytes) { assert!( - new_pack.tbins.insert(file_path, tbin).is_none(), + pack.tbins.insert(file_path, tbin).is_none(), "duplicate tbin file {name}" ); } else { @@ -631,7 +631,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(child, &names); if let Some(icon_file) = common_attributes.get_icon_file() { - if !new_pack.textures.contains_key(icon_file) { + if !pack.textures.contains_key(icon_file) { info!(%icon_file, "failed to find this texture in this pack"); } } else if let Some(icf) = child.get_attribute(names.icon_file) { @@ -644,10 +644,10 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { attrs: common_attributes, guid, }; - if !new_pack.maps.contains_key(&map_id) { - new_pack.maps.insert(map_id, MapData::default()); + if !pack.maps.contains_key(&map_id) { + pack.maps.insert(map_id, MapData::default()); } - new_pack.maps.get_mut(&map_id).unwrap().markers.push(marker); + pack.maps.get_mut(&map_id).unwrap().markers.push(marker); } else { info!("missing map id") } @@ -656,14 +656,14 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { .get_attribute(names.trail_data) .and_then(|trail_data| { let path: RelativePath = trail_data.parse().unwrap(); - new_pack.tbins.get(&path).map(|tb| tb.map_id) + pack.tbins.get(&path).map(|tb| tb.map_id) }) { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(child, &names); if let Some(tex) = common_attributes.get_texture() { - if !new_pack.textures.contains_key(tex) { + if !pack.textures.contains_key(tex) { info!(%tex, "failed to find this texture in this pack"); } } @@ -674,14 +674,14 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { props: common_attributes, guid, }; - if !new_pack.maps.contains_key(&map_id) { - new_pack.maps.insert(map_id, MapData::default()); + if !pack.maps.contains_key(&map_id) { + pack.maps.insert(map_id, MapData::default()); } new_pack.maps.get_mut(&map_id).unwrap().trails.push(trail); } else { let td = child.get_attribute(names.trail_data); let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = new_pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); + let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); info!("missing map_id: {td:?} {rp} {tbin:?}"); } } else { @@ -692,7 +692,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { drop(span_guard); } - Ok(new_pack) + Ok(pack) } #[instrument(skip(zip_archive))] fn read_file_bytes_from_zip_by_name( From 0e655885c1c97473b502f56dbfb07bcfd75000a7 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:21:00 +0100 Subject: [PATCH 06/54] minimize changes to master --- crates/joko_marker_format/src/io/deserialize.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 09e347d..1cd122b 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -567,7 +567,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }; // parse_categories - recursive_marker_category_parser(&tree, tree.children(od), &mut new_pack.categories, &names); + recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names); let pois = match tree.children(od).find(|node| { tree.element(*node) @@ -677,7 +677,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if !pack.maps.contains_key(&map_id) { pack.maps.insert(map_id, MapData::default()); } - new_pack.maps.get_mut(&map_id).unwrap().trails.push(trail); + pack.maps.get_mut(&map_id).unwrap().trails.push(trail); } else { let td = child.get_attribute(names.trail_data); let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); From c82e2fdd5ed54070bdc99eb0724c39d3e431c6c0 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:27:54 +0100 Subject: [PATCH 07/54] remove lock --- Cargo.lock | 3066 ---------------------------------------------------- 1 file changed, 3066 deletions(-) delete mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index d0414cf..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,3066 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - -[[package]] -name = "accesskit" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eb1adf08c5bcaa8490b9851fd53cca27fa9880076f178ea9d29f05196728a8" -dependencies = [ - "enumn", - "serde", -] - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "serde", - "version_check", - "zerocopy 0.7.25", -] - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arcdps" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e8e3e68ba99ea4d9fc0af6c26f7277c6a30f9fbd7a1884efd8d016dcdfdc39" -dependencies = [ - "arcdps_codegen", - "chrono", - "once_cell", -] - -[[package]] -name = "arcdps_codegen" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b73c6f84c5845e9eba3a232593d20ef3db434281848f5072a367edbcc1f3fee" -dependencies = [ - "paste", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "base64" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata 0.1.10", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "bytemuck" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" - -[[package]] -name = "cap-directories" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182588d07579a8ca97dbfbea2787d450341d068b16062c8caa2205158ddb269d" -dependencies = [ - "cap-std", - "directories-next", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "cap-primitives" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf30c373a3bee22c292b1b6a7a26736a38376840f1af3d2d806455edf8c3899" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix", - "windows-sys 0.48.0", - "winx", -] - -[[package]] -name = "cap-std" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bade423fa6403efeebeafe568fdb230e8c590a275fba2ba978dd112efcf6e9" -dependencies = [ - "camino", - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-expr" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx", - "num-traits", -] - -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.48.5", -] - -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "console" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "const_format" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "cxx" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.39", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "data-encoding" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" - -[[package]] -name = "deranged" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "ecolor" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "egui" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" -dependencies = [ - "accesskit", - "ahash", - "epaint", - "nohash-hasher", - "serde", -] - -[[package]] -name = "egui_extras" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ffe3fe5c00295f91c2a61a74ee271c32f74049c94ba0b1cea8f26eb478bc07" -dependencies = [ - "egui", - "enum-map", - "log", - "mime_guess", - "serde", -] - -[[package]] -name = "egui_render_glow" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df0cb60080432a2c025f00942fbd1a0f8b719338ab6a28adab5a1ca15013771" -dependencies = [ - "bytemuck", - "egui", - "getrandom", - "glow", - "js-sys", - "raw-window-handle", - "tracing", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "egui_render_three_d" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4038e7bac93f9356eb88ffabd20d9486070b79910584d662af1ac3bf64f01e2a" -dependencies = [ - "egui", - "egui_render_glow", - "raw-window-handle", - "three-d", -] - -[[package]] -name = "egui_window_glfw_passthrough" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecec3abb56e2be5104a35a4c1848f976add5167a8655f67ae7c84d45d35c8905" -dependencies = [ - "egui", - "glfw-passthrough", - "tracing", -] - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "emath" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "encoding_rs" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-map" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed40247825a1a0393b91b51d475ea1063a6cbbf0847592e7f13fb427aca6a716" -dependencies = [ - "enum-map-derive", - "serde", -] - -[[package]] -name = "enum-map-derive" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7933cd46e720348d29ed1493f89df9792563f272f96d8f13d18afe03b32f8cb8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "enumflags2" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "enumn" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "epaint" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" -dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "nohash-hasher", - "parking_lot", - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "fdeflate" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "filetime" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", -] - -[[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs-set-times" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" -dependencies = [ - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gethostname" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "getrandom" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glam" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "glfw-passthrough" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89ad199bb99922313a6e97b609dab23a88e3b68a6b0233d1fafdb5044a7728f" -dependencies = [ - "bitflags 1.3.2", - "glfw-sys-passthrough", - "objc", - "raw-window-handle", - "winapi", -] - -[[package]] -name = "glfw-sys-passthrough" -version = "4.0.3+3.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b2db4d361b9ebe743c3a542ddef5d605269bd1f93e1090440fff075e666ddf" -dependencies = [ - "cmake", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "glow" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "half" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy 0.6.5", -] - -[[package]] -name = "hashbrown" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "iana-time-zone" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "image" -version = "0.24.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-rational", - "num-traits", - "png", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown", - "serde", -] - -[[package]] -name = "indextree" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-extras" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3c230ee517ee76b1cc593b52939ff68deda3fae9e41eca426c6b4993df51c4" -dependencies = [ - "io-lifetimes", - "windows-sys 0.48.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "joko_core" -version = "0.2.1" -dependencies = [ - "cap-directories", - "cap-std", - "egui", - "egui_extras", - "glam", - "indexmap", - "miette", - "rayon", - "rfd", - "ringbuffer", - "serde", - "serde_json", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] -name = "joko_ext" -version = "0.1.0" - -[[package]] -name = "joko_marker_format" -version = "0.2.1" -dependencies = [ - "base64", - "cap-std", - "cxx", - "cxx-build", - "data-encoding", - "egui", - "enumflags2", - "glam", - "image", - "indexmap", - "itertools", - "joko_render", - "jokoapi", - "jokolink", - "miette", - "paste", - "phf", - "rayon", - "rfd", - "rstest", - "serde", - "serde_json", - "similar-asserts", - "smol_str", - "time", - "tracing", - "url", - "uuid", - "xot", - "zip", -] - -[[package]] -name = "joko_render" -version = "0.2.1" -dependencies = [ - "bytemuck", - "egui", - "egui_render_three_d", - "egui_window_glfw_passthrough", - "glam", - "jokolink", - "raw-window-handle", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "jokoapi" -version = "0.2.1" -dependencies = [ - "const_format", - "enumflags2", - "miette", - "serde", - "ureq", -] - -[[package]] -name = "jokolay" -version = "0.2.1" -dependencies = [ - "cap-directories", - "cap-std", - "egui", - "egui_extras", - "egui_window_glfw_passthrough", - "glam", - "indexmap", - "joko_core", - "joko_marker_format", - "joko_render", - "jokolink", - "miette", - "rayon", - "rfd", - "ringbuffer", - "serde", - "serde_json", - "tracing", - "tracing-appender", - "tracing-subscriber", - "url", -] - -[[package]] -name = "jokolink" -version = "0.2.1" -dependencies = [ - "arcdps", - "egui", - "enumflags2", - "glam", - "jokoapi", - "miette", - "notify", - "num-derive", - "num-traits", - "serde", - "serde_json", - "time", - "tracing", - "tracing-appender", - "tracing-subscriber", - "widestring", - "windows", - "x11rb", -] - -[[package]] -name = "js-sys" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kqueue" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libredox" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" -dependencies = [ - "bitflags 2.4.1", - "libc", - "redox_syscall 0.4.1", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - -[[package]] -name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "backtrace", - "backtrace-ext", - "is-terminal", - "miette-derive", - "once_cell", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "next-gen" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1962f0b64c859f27f9551c74afbdbec7090fa83518daf6c5eb5b31d153455beb" -dependencies = [ - "next-gen-proc_macros", - "unwind_safe", -] - -[[package]] -name = "next-gen-proc_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a59395d2ffdd03894479cdd1ce4b7e0700d379d517f2d396cee2a4828707c5a0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.4.1", - "filetime", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "owned_ttf_parser" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "png" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - -[[package]] -name = "rayon" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "relative-path" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" - -[[package]] -name = "rfd" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" -dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "ring" -version = "0.17.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" -dependencies = [ - "cc", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.48.0", -] - -[[package]] -name = "ringbuffer" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" - -[[package]] -name = "rstest" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" -dependencies = [ - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" -dependencies = [ - "cfg-if", - "glob", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.39", - "unicode-ident", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" -dependencies = [ - "bitflags 2.4.1", - "errno", - "itoa", - "libc", - "linux-raw-sys", - "once_cell", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustls" -version = "0.21.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scratch" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "semver" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" - -[[package]] -name = "serde" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "serde_json" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" -dependencies = [ - "serde", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "similar" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" -dependencies = [ - "bstr", - "unicode-segmentation", -] - -[[package]] -name = "similar-asserts" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" -dependencies = [ - "console", - "similar", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "smol_str" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" -dependencies = [ - "serde", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "supports-unicode" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - -[[package]] -name = "termcolor" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "three-d" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2db9010227411ab0aa5948e770304e807e5c9b6d5d0719c3de248bae7be7096" -dependencies = [ - "cgmath", - "glow", - "instant", - "thiserror", - "three-d-asset", -] - -[[package]] -name = "three-d-asset" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9959d4427b63958661828008f7470d6a8d2c0945b3df0dc7377d6aca38fb694" -dependencies = [ - "cgmath", - "half", - "thiserror", - "web-sys", -] - -[[package]] -name = "time" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" -dependencies = [ - "deranged", - "itoa", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" -dependencies = [ - "crossbeam-channel", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "time", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "unwind_safe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" - -[[package]] -name = "ureq" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-webpki", - "serde", - "serde_json", - "url", - "webpki-roots", -] - -[[package]] -name = "url" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "uuid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" -dependencies = [ - "getrandom", - "rand", - "serde", - "uuid-macro-internal", -] - -[[package]] -name = "uuid-macro-internal" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" - -[[package]] -name = "web-sys" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" -dependencies = [ - "windows-core", - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "winnow" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" -dependencies = [ - "memchr", -] - -[[package]] -name = "winx" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5" -dependencies = [ - "bitflags 2.4.1", - "windows-sys 0.48.0", -] - -[[package]] -name = "x11rb" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" -dependencies = [ - "gethostname", - "nix", - "winapi", - "winapi-wsapoll", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" -dependencies = [ - "nix", -] - -[[package]] -name = "xhtmlchardet" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc471704e8954f426350a7300e92a4da6932b762068ae8e6aa5dcacf141e133" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "xot" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55dc1c3603e452c78983b59f466cd8251695db1729b230f473d004d70b3d94d8" -dependencies = [ - "ahash", - "encoding_rs", - "indextree", - "next-gen", - "xhtmlchardet", - "xmlparser", -] - -[[package]] -name = "zerocopy" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96f8f25c15a0edc9b07eb66e7e6e97d124c0505435c382fde1ab7ceb188aa956" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.5", -] - -[[package]] -name = "zerocopy" -version = "0.7.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" -dependencies = [ - "zerocopy-derive 0.7.25", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "855e0f6af9cd72b87d8a6c586f3cb583f5cdcc62c2c80869d8cd7e96fdf7ee20" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", - "flate2", -] From d8bec95826ab5e25a1b8816f34be0016d4b27291 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 23 Feb 2024 18:29:40 +0100 Subject: [PATCH 08/54] ignore Cargo.lock --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 046f6bb..7957a17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /assets # the wasm build of crate **/dist -*.log \ No newline at end of file +*.log +Cargo.lock From 14edb117cc4845387f4b6d66e0139303767cc040 Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 18 Mar 2024 22:47:30 +0100 Subject: [PATCH 09/54] split long trails into subparts to have a progressive display and avoid seing nothing when far from both ends + implement routes --- Cargo.toml | 4 + .../joko_marker_format/src/io/deserialize.rs | 535 +++++++++++++----- crates/joko_marker_format/src/io/mod.rs | 10 + crates/joko_marker_format/src/io/serialize.rs | 39 +- .../src/manager/live_pack.rs | 115 +++- crates/joko_marker_format/src/manager/mod.rs | 18 +- crates/joko_marker_format/src/pack/mod.rs | 12 +- crates/joko_marker_format/src/pack/route.rs | 13 + crates/joko_marker_format/src/pack/trail.png | Bin 2293 -> 6896 bytes crates/joko_marker_format/src/pack/trail.rs | 9 + .../src/pack/trail_black.png | Bin 0 -> 2293 bytes crates/joko_render/src/lib.rs | 2 +- 12 files changed, 585 insertions(+), 172 deletions(-) create mode 100644 crates/joko_marker_format/src/pack/route.rs create mode 100644 crates/joko_marker_format/src/pack/trail_black.png diff --git a/Cargo.toml b/Cargo.toml index 40fadf5..93f15c6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,7 @@ rfd = { version = "*" } smol_str = { version = "*" } itertools = { version = "*" } ordered_hash_map = { version = "*", features= ["serde"] } + +[profile.release] +strip = "symbols" +lto = true diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 1cd122b..0a882a4 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -1,21 +1,24 @@ use crate::{ - pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, Trail, Texture, MapData}, + pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, TBinStatus, Trail, MapData, Route}, BASE64_ENGINE, }; use base64::Engine; use cap_std::fs_utf8::Dir; +use egui::lerp; use glam::Vec3; use indexmap::IndexMap; use miette::{bail, Context, IntoDiagnostic, Result}; -use std::{collections::BTreeMap, io::Read}; +use serde_json::map; +use std::{collections::{BTreeMap, VecDeque}, io::Read}; use ordered_hash_map::OrderedHashMap; -use tracing::{info, info_span, instrument, trace, warn}; +use tracing::{debug, info, info_span, instrument, trace, warn}; use uuid::Uuid; -use xot::{Node, Xot}; +use xot::{Node, Xot, Element}; use super::XotAttributeNameIDs; pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { + //called from already parsed data let mut pack = PackCore::default(); // walks the directory and loads all files into the hashmap recursive_walk_dir_and_read_images_and_tbins( @@ -43,8 +46,8 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { .to_string(); if name.ends_with(".xml") { - if let Some(name) = name.strip_suffix(".xml") { - match name { + if let Some(name_as_str) = name.strip_suffix(".xml") { + match name_as_str { "categories" => { // parse categories { @@ -52,13 +55,14 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { .read_to_string("categories.xml") .into_diagnostic() .wrap_err("failed to read categories.xml")?; - parse_categories_file(&cats_xml, &mut pack) + parse_categories_file(&name, &cats_xml, &mut pack) .wrap_err("failed to parse category file")?; } } map_id => { // parse map file - if let Ok(map_id) = map_id.parse() { + let span_guard = info_span!("map", map_id).entered(); + if let Ok(map_id) = map_id.parse::() { let mut xml_str = String::new(); entry .open() @@ -73,6 +77,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { } else { info!("unrecognized xml file {map_id}") } + std::mem::drop(span_guard); } } } @@ -82,9 +87,11 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { } Ok(pack) } + + fn recursive_walk_dir_and_read_images_and_tbins( dir: &Dir, - images: &mut OrderedHashMap, + images: &mut OrderedHashMap>, tbins: &mut OrderedHashMap, parent_path: &RelativePath, ) -> Result<()> { @@ -115,15 +122,16 @@ fn recursive_walk_dir_and_read_images_and_tbins( .into_diagnostic() .wrap_err("failed to read file contents")?; if name.ends_with(".png") { - images.insert(path.clone(), Texture{ //it is the presence in the directory that defines the source of information - path: path.clone(), - original: parent_path.to_string(), - source: parent_path.to_string(), - bytes - }); + images.insert(path.clone(), bytes); } else if name.ends_with(".trl") { - if let Some(tbin) = parse_tbin_from_slice(&bytes) { - tbins.insert(path, tbin); + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + } + tbins.insert(path, tbs.tbin); } else { info!("invalid tbin: {path}"); } @@ -140,7 +148,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( } Ok(()) } -fn parse_tbin_from_slice(bytes: &[u8]) -> Option { +fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let content_length = bytes.len(); // content_length must be atleast 8 to contain version + map_id if content_length < 8 { @@ -155,8 +163,10 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { map_id_bytes.copy_from_slice(&bytes[4..8]); let map_id = u32::from_ne_bytes(map_id_bytes); + let zero = Vec3{x:0.0, y:0.0, z:0.0}; + // this will either be empty vec or series of vec3s. - let nodes: Vec = bytes[8..] + let mut nodes: VecDeque = bytes[8..] .chunks_exact(12) .map(|float_bytes| { // make [f32 ;3] out of those 12 bytes @@ -187,10 +197,64 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { Vec3::from_array(arr) }) .collect(); - Some(TBin { - map_id, - version, - nodes, + + //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. + let mut iso_x = false; + let mut iso_y = false; + let mut iso_z = false; + let mut closed = false; + let mut resulting_nodes : Vec = Vec::new(); + if nodes.len() > 0 { + let ref_node = nodes[0]; + let mut c_iso_x = true; + let mut c_iso_y = true; + let mut c_iso_z = true; + // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts + resulting_nodes.push(ref_node); + for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { + //ignore zeroes since they would be separators + if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { + let distance_to_next_point = a.distance_squared(*b); + let mut current_cursor = distance_to_next_point; + while current_cursor > 400.0 { + let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); + resulting_nodes.push(c); + current_cursor -= 400.0; + } + } + resulting_nodes.push(*b); + } + for node in &nodes { + if resulting_nodes.len() > 1 { + //TODO: load epsilon from a configuration somewhere, with a default value + if (node.x - ref_node.x).abs() < 0.1 { + c_iso_x = false; + } + if (node.y - ref_node.y).abs() < 0.1 { + c_iso_y = false; + } + if (node.z - ref_node.z).abs() < 0.1 { + c_iso_z = false; + } + } + } + iso_x = c_iso_x; + iso_y = c_iso_y; + iso_z = c_iso_z; + if nodes.len() > 1 {// TODO: get this threshold from configuration + closed = nodes.front().unwrap().distance(*nodes.back().unwrap()).abs() < 0.1 + } + } + Some(TBinStatus{ + tbin: TBin { + map_id, + version, + nodes: resulting_nodes, + }, + iso_x, + iso_y, + iso_z, + closed }) } // a recursive function to parse the marker category tree. @@ -209,7 +273,7 @@ fn recursive_marker_category_parser( continue; } - let name = ele.get_attribute(names.name).unwrap_or_default(); + let name = ele.get_attribute(names.name).or(ele.get_attribute(names.CapitalName)).unwrap_or_default(); if name.is_empty() { continue; } @@ -249,7 +313,7 @@ fn recursive_marker_category_parser( } } -fn parse_categories_file(cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { +fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { let mut tree = xot::Xot::new(); let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); let root_node = tree @@ -265,6 +329,7 @@ fn parse_categories_file(cats_xml_str: &str, pack: &mut PackCore) -> Result<()> if let Some(od) = tree.element(overlay_data_node) { if od.name() == xot_names.overlay_data { recursive_marker_category_parser_categories_xml( + &file_name, &tree, tree.children(overlay_data_node), &mut pack.categories, @@ -278,6 +343,8 @@ fn parse_categories_file(cats_xml_str: &str, pack: &mut PackCore) -> Result<()> } Ok(()) } + + fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result<()> { let mut tree = Xot::new(); let root_node = tree @@ -304,25 +371,37 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result None => false, }) .ok_or_else(|| miette::miette!("missing pois node"))?; - for child in tree.children(pois) { - if let Some(child) = tree.element(child) { + + for poi_node in tree.children(pois) { + if let Some(child) = tree.element(poi_node) { let category = child .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); + let span_guard = info_span!("category", category).entered(); - let guid = child - .get_attribute(names.guid) - .and_then(|guid| { + let raw_uid = child.get_attribute(names.guid); + if raw_uid.is_none() { + info!("This POI is either invalid or inside a Route {:?}", child); + span_guard.exit(); + continue; + } + let guid = raw_uid.and_then(|guid| { let mut buffer = [0u8; 20]; BASE64_ENGINE .decode_slice(guid, &mut buffer) .ok() .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) }) - .ok_or_else(|| miette::miette!("invalid guid"))?; + .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; + //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail - if child.name() == names.poi { + if child.name() == names.route { + debug!("Found a route in core pack {:?}", child); + import_route_as_trail(pack, &names, &tree, &poi_node, child, category) + } + else if child.name() == names.poi { + debug!("Found a POI in core pack {:?}", child); if child .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -362,6 +441,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result } pack.maps.get_mut(&map_id).unwrap().markers.push(marker); } else if child.name() == names.trail { + debug!("Found a trail in core pack {:?}", child); if child .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -378,6 +458,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result map_id, props: ca, guid, + dynamic: false, }; if !pack.maps.contains_key(&map_id) { @@ -385,6 +466,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result } pack.maps.get_mut(&map_id).unwrap().trails.push(trail); } + span_guard.exit(); } } Ok(()) @@ -392,6 +474,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result // a temporary recursive function to parse the marker category tree. fn recursive_marker_category_parser_categories_xml( + file_name: &String, tree: &Xot, tags: impl Iterator, cats: &mut IndexMap, @@ -404,12 +487,16 @@ fn recursive_marker_category_parser_categories_xml( continue; } - let name = ele.get_attribute(names.name).unwrap_or_default(); + let name = ele.get_attribute(names.name) + .or(ele.get_attribute(names.display_name) + .or(ele.get_attribute(names.CapitalName) + ) + ).unwrap_or_default(); if name.is_empty() { info!("category doesn't have a name attribute: {ele:#?}"); continue; } - let span_guard = info_span!("category {name}").entered(); + let span_guard = info_span!("category", name).entered(); let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); @@ -433,6 +520,7 @@ fn recursive_marker_category_parser_categories_xml( } }; recursive_marker_category_parser_categories_xml( + file_name, tree, tree.children(tag), &mut cats @@ -449,7 +537,8 @@ fn recursive_marker_category_parser_categories_xml( ); std::mem::drop(span_guard); } else { - info!("ignore tag: {:?}", tag); + //it may be a comment, a space, anything + //info!("In file {}, ignore node {:?}", file_name, tag); } } } @@ -462,6 +551,7 @@ fn recursive_marker_category_parser_categories_xml( /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { + //called to import a new pack // all the contents of ZPack let mut pack = PackCore::default(); // parse zip file @@ -496,12 +586,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { Ok(_) => assert!( - pack.textures.insert(file_path.clone(), Texture{ - path: file_path.clone(), - original: name.clone(), - source: String::from(std::str::from_utf8(taco).unwrap_or_default()), - bytes - }).is_none(), + pack.textures.insert(file_path.clone(), bytes).is_none(), "duplicate image file {name}" ), Err(e) => { @@ -517,9 +602,15 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { - if let Some(tbin) = parse_tbin_from_slice(&bytes) { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + } assert!( - pack.tbins.insert(file_path, tbin).is_none(), + pack.tbins.insert(file_path, tbs.tbin).is_none(), "duplicate tbin file {name}" ); } else { @@ -590,110 +681,288 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); - let guid = child - .get_attribute(names.guid) - .and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - .or_else(|| { - info!(guid, "failed to deserialize guid"); - None - }) - }) - .unwrap_or_else(Uuid::new_v4); - if category.is_empty() { - info!(?guid, "missing category (type) attribute on marker"); - } + debug!("import element: {:?}", child); if child.name() == names.poi { - if let Some(map_id) = child + import_poi(&mut pack, &names, &child, category); + } else if child.name() == names.trail { + import_trail(&mut pack, &names, &child, category); + } else if child.name() == names.route { + import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category); + } else { + info!("unknown element: {:?}", child); + } + } + + drop(span_guard); + } + + Ok(pack) +} + +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ + child + .get_attribute(names.guid) + .and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + .or_else(|| { + info!(guid, "failed to deserialize guid"); + None + }) + }) + .unwrap_or_else(Uuid::new_v4) +} + +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String) -> Option { + if let Some(map_id) = poi_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + let xpos = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let ypos = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let zpos = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(poi_element, &names); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + info!(%icon_file, "failed to find this texture in this pack"); + } + } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { + info!(icf, "marker's icon file attribute failed to parse"); + } + Some(Marker { + position: [xpos, ypos, zpos].into(), + map_id, + category, + attrs: common_attributes, + guid: parse_guid(names, poi_element), + }) + } else { + info!("missing map id"); + None + } +} + +fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { + let x = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let y = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let z = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Vec3{x, y, z} +} + +fn parse_route(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) -> Option { + + let mut path: Vec = Vec::new(); + let resetposx = route_element + .get_attribute(names.resetposx) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposy = route_element + .get_attribute(names.resetposy) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposz = route_element + .get_attribute(names.resetposz) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let reset_position = Vec3::new(resetposx, resetposy, resetposz); + let reset_range = route_element.get_attribute(names.reset_range).and_then(|map_id| map_id.parse::().ok()); + let name = route_element.get_attribute(names.name).or(route_element.get_attribute(names.CapitalName)); + + if name.is_none() { + info!("route element is missing name: {route_element:?}"); + return None; + } + let mut category: String = category; + let mut map_id: Option = route_element.get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()); + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + let marker = parse_position(&names, child); + path.push(marker); + if let Some(cat) = child.get_attribute(names.category) { + if category.is_empty() { + category = cat.to_string(); + } + } + if map_id.is_none() { + if let Some(node_map_id) = child .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) { - let xpos = child - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let ypos = child - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let zpos = child - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(child, &names); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - info!(%icon_file, "failed to find this texture in this pack"); - } - } else if let Some(icf) = child.get_attribute(names.icon_file) { - info!(icf, "marker's icon file attribute failed to parse"); - } - let marker = Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category, - attrs: common_attributes, - guid, - }; - if !pack.maps.contains_key(&map_id) { - pack.maps.insert(map_id, MapData::default()); - } - pack.maps.get_mut(&map_id).unwrap().markers.push(marker); - } else { - info!("missing map id") + map_id = Some(node_map_id); } - } else if child.name() == names.trail { - if let Some(map_id) = child - .get_attribute(names.trail_data) - .and_then(|trail_data| { - let path: RelativePath = trail_data.parse().unwrap(); - pack.tbins.get(&path).map(|tb| tb.map_id) - }) - { - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(child, &names); + } + } + } + if category.is_empty() { + info!("Could not find a category for route element: {route_element:?}"); + return None; + } + if map_id.is_none() { + info!("Could not find a map_id for route element: {route_element:?}"); + return None; + } + debug!("found route with {:?} elements {route_element:?}", path.len()); - if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) { - info!(%tex, "failed to find this texture in this pack"); - } - } + Some(Route { + category, + path, + reset_position, + reset_range: reset_range.unwrap_or(0.0), + map_id: map_id.unwrap(), + name: name.unwrap().into(), + guid: parse_guid(names, &route_element) + }) +} - let trail = Trail { - category, - map_id, - props: common_attributes, - guid, - }; - if !pack.maps.contains_key(&map_id) { - pack.maps.insert(map_id, MapData::default()); - } - pack.maps.get_mut(&map_id).unwrap().trails.push(trail); - } else { - let td = child.get_attribute(names.trail_data); - let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); - info!("missing map_id: {td:?} {rp} {tbin:?}"); - } - } else { - info!("unknown tag: {:?}", child.name()); + +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String) -> Option { + //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html + if let Some(map_id) = trail_element + .get_attribute(names.trail_data) + .and_then(|trail_data| { + let path: RelativePath = trail_data.parse().unwrap(); + pack.tbins.get(&path).map(|tb| tb.map_id) + }) + { + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(trail_element, &names); + + if let Some(tex) = common_attributes.get_texture() { + if !pack.textures.contains_key(tex) { + info!(%tex, "failed to find this texture in this pack"); } } - drop(span_guard); + Some(Trail { + category, + map_id, + props: common_attributes, + guid: parse_guid(names, trail_element), + dynamic: false + }) + } else { + let td = trail_element.get_attribute(names.trail_data); + let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); + let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); + info!("missing map_id: {td:?} {rp} {tbin:?}"); + None } - Ok(pack) } + +fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String) { + if let Some(marker) = parse_marker(pack, names, poi_element, category) { + if !pack.maps.contains_key(&marker.map_id) { + pack.maps.insert(marker.map_id, MapData::default()); + } + pack.maps.get_mut(&marker.map_id).unwrap().markers.push(marker); + } else { + debug!("Could not parse POI"); + } +} + + +fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String) { + if let Some(trail) = parse_trail(pack, names, trail_element, category) { + if !pack.maps.contains_key(&trail.map_id) { + pack.maps.insert(trail.map_id, MapData::default()); + } + pack.maps.get_mut(&trail.map_id).unwrap().trails.push(trail); + } else { + debug!("Could not parse Trail"); + } + +} + +fn route_to_tbin(route: &Route) -> TBin { + assert!( route.path.len() > 1); + TBin { + map_id: route.map_id, + version: 0, + nodes: route.path.clone(), + } +} + +fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { + let mut props = CommonAttributes::default(); + let default_texture: RelativePath = "default_trail_texture.png".parse().unwrap(); + props.set_texture(None); + props.set_trail_data(Some(file_path.clone())); + debug!("Build dynamic trail {}", route.guid); + Trail { + map_id: route.map_id, + category: route.category.clone(), + guid: route.guid, + props: props, + dynamic: true + } +} + +fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) { + if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category) { + let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); + let file_path: RelativePath = file_name.parse().unwrap(); + let trail = route_to_trail(&route, &file_path); + let tbin = route_to_tbin(&route); + pack.tbins.insert(file_path, tbin);//there may be duplicates since we load and save each time + if !pack.maps.contains_key(&trail.map_id) { + pack.maps.insert(trail.map_id, MapData::default()); + } + pack.maps.get_mut(&trail.map_id).unwrap().trails.push(trail); + pack.maps.get_mut(&route.map_id).unwrap().routes.push(route); + } else { + info!("Could not parse route {:?}", route_element); + } +} + +fn import_route(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) { + if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category) { + if !pack.maps.contains_key(&route.map_id) { + pack.maps.insert(route.map_id, MapData::default()); + } + pack.maps.get_mut(&route.map_id).unwrap().routes.push(route); + } else { + info!("Could not parse Route"); + } +} + #[instrument(skip(zip_archive))] fn read_file_bytes_from_zip_by_name( name: &str, diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs index 937d5f1..e39f12a 100644 --- a/crates/joko_marker_format/src/io/mod.rs +++ b/crates/joko_marker_format/src/io/mod.rs @@ -28,6 +28,7 @@ pub(crate) struct XotAttributeNameIDs { pub default_enabled: NameId, pub display_name: NameId, pub name: NameId, + pub CapitalName: NameId,//same than "name" but with a starting capital letter pub separator: NameId, // inheritable attributes pub achievement_id: NameId, @@ -88,6 +89,10 @@ pub(crate) struct XotAttributeNameIDs { pub trail_data: NameId, pub trail_scale: NameId, pub trigger_range: NameId, + pub reset_range: NameId, + pub resetposx: NameId, + pub resetposy: NameId, + pub resetposz: NameId, } impl XotAttributeNameIDs { pub fn register_with_xot(tree: &mut Xot) -> Self { @@ -112,6 +117,7 @@ impl XotAttributeNameIDs { default_enabled: tree.add_name("defaulttoggle"), display_name: tree.add_name("DisplayName"), name: tree.add_name("name"), + CapitalName: tree.add_name("Name"), // inheritable attributes achievement_id: tree.add_name("achievementId"), achievement_bit: tree.add_name("achievementBit"), @@ -171,6 +177,10 @@ impl XotAttributeNameIDs { bounce: tree.add_name("bounce"), keep_on_map_edge: tree.add_name("keepOnMapEdge"), map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), + reset_range: tree.add_name("resetrange"), + resetposx: tree.add_name("resetposx"), + resetposy: tree.add_name("resetposy"), + resetposz: tree.add_name("resetposz"), } } } diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index 9dbc6ba..390bea4 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -1,5 +1,5 @@ use crate::{ - pack::{Category, Marker, PackCore, RelativePath, Trail, Texture}, + pack::{Category, Marker, PackCore, RelativePath, Trail, Route}, BASE64_ENGINE, }; use base64::Engine; @@ -75,7 +75,13 @@ pub(crate) fn save_pack_core_to_dir( let ele = tree.element_mut(poi).unwrap(); serialize_marker_to_element(marker, ele, &names); } + for route_path in &map_data.routes { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } for trail in &map_data.trails { + if trail.dynamic { + continue; + } let trail_node = tree.new_element(names.trail); tree.append(pois, trail_node) .into_diagnostic() @@ -118,7 +124,7 @@ pub(crate) fn save_pack_core_to_dir( dir.create(img_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(&img.bytes) + .write(img) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to write image bytes to file: {img_path}") @@ -213,3 +219,32 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.category, &marker.category); marker.attrs.serialize_to_element(ele, names); } + +fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .into_diagnostic() + .wrap_err("failed to append route to pois")?; + let ele = tree.element_mut(route_node).unwrap(); + + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + for pos in &route.path { + let child = tree.new_element(names.poi); + tree.append(route_node, child); + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} + diff --git a/crates/joko_marker_format/src/manager/live_pack.rs b/crates/joko_marker_format/src/manager/live_pack.rs index fdb35b3..9416a4d 100644 --- a/crates/joko_marker_format/src/manager/live_pack.rs +++ b/crates/joko_marker_format/src/manager/live_pack.rs @@ -6,7 +6,7 @@ use ordered_hash_map::{OrderedHashMap, OrderedHashSet}; use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; use glam::{vec2, Vec2, Vec3}; -use image::EncodableLayout; +use image::{flat, EncodableLayout}; use indexmap::IndexMap; use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; use tracing::{debug, error, info, trace}; @@ -97,9 +97,7 @@ impl LoadedPack { } } pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { - //TODO: find a way to merge categories (see LadyElyssa pack) //it is important to generate a new id each time to avoid collision - //or to do a look up of an already existing menu ui.push_id(ui.next_auto_id(), |ui| { CategorySelection::recursive_selection_ui( &mut self.cats_selection, @@ -108,22 +106,22 @@ impl LoadedPack { ); }); } - pub fn load_from_dir(dir: Arc) -> Result { - if !dir + pub fn load_from_dir(pack_dir: Arc) -> Result { + if !pack_dir .try_exists(Self::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to check if pack core exists")? { bail!("pack core doesn't exist in this pack"); } - let core_dir = dir + let core_dir = pack_dir .open_dir(Self::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to open core pack directory")?; let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - let cats_selection = (if dir.exists(Self::ACTIVATION_DATA_FILE_NAME) { - match dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { + let cats_selection = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { Ok(cd_json) => match serde_json::from_str(&cd_json) { Ok(cd) => Some(cd), Err(e) => { @@ -143,7 +141,7 @@ impl LoadedPack { .unwrap_or_else(|| { let cs = CategorySelection::default_from_pack_core(&core); match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { debug!("wrote cat selections to disk after creating a default from pack"); } @@ -157,8 +155,8 @@ impl LoadedPack { } cs }); - let activation_data = (if dir.exists(Self::ACTIVATION_DATA_FILE_NAME) { - match dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { + let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { + match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { Ok(contents) => match serde_json::from_str(&contents) { Ok(cd) => Some(cd), Err(e) => { @@ -177,7 +175,7 @@ impl LoadedPack { .flatten() .unwrap_or_default(); Ok(LoadedPack { - dir, + dir: pack_dir, core, cats_selection, dirty: Default::default(), @@ -192,6 +190,7 @@ impl LoadedPack { joko_renderer: &mut joko_render::JokoRenderer, link: &Option>, default_tex_id: &TextureHandle, + default_trail_id: &TextureHandle, ) { let categories_changed = self.dirty.cats_selection; if self.dirty.is_dirty() { @@ -208,7 +207,7 @@ impl LoadedPack { }; if self.current_map_data.map_id != link.map_id || categories_changed { - self.on_map_changed(etx, link, default_tex_id); + self.on_map_changed(etx, link, default_tex_id, default_trail_id); } let z_near = joko_renderer.get_z_near(); for marker in self.current_map_data.active_markers.values() { @@ -228,6 +227,7 @@ impl LoadedPack { etx: &egui::Context, link: &MumbleLink, default_tex_id: &TextureHandle, + default_trail_id: &TextureHandle, ) { info!( self.current_map_data.map_id, @@ -235,6 +235,7 @@ impl LoadedPack { ); self.current_map_data = Default::default(); if link.map_id == 0 { + info!("No map do not do anything"); return; } self.current_map_data.map_id = link.map_id; @@ -247,6 +248,8 @@ impl LoadedPack { &Default::default(), ); let mut failure_loading = false; + let mut nb_markers_attempt = 0; + let mut nb_markers_loaded = 0; for (index, marker) in self .core .maps @@ -256,6 +259,7 @@ impl LoadedPack { .iter() .enumerate() { + nb_markers_attempt += 1; if let Some(category_attributes) = enabled_cats_list.get(&marker.category) { let mut attrs = marker.attrs.clone(); attrs.inherit_if_attr_none(category_attributes); @@ -308,7 +312,7 @@ impl LoadedPack { if let Some(tex_path) = attrs.get_icon_file() { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(&tex.bytes).unwrap(); + let img = image::load_from_memory(tex).unwrap(); self.current_map_data.active_textures.insert( tex_path.clone(), etx.load_texture( @@ -350,9 +354,12 @@ impl LoadedPack { min_pixel_size, }, ); + nb_markers_loaded += 1; } } + let mut nb_trails_attempt = 0; + let mut nb_trails_loaded = 0; for (index, trail) in self .core .maps @@ -362,13 +369,14 @@ impl LoadedPack { .iter() .enumerate() { + nb_trails_attempt += 1; if let Some(category_attributes) = enabled_cats_list.get(&trail.category) { let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(&tex.bytes).unwrap(); + let img = image::load_from_memory(tex).unwrap(); self.current_map_data.active_textures.insert( tex_path.clone(), etx.load_texture( @@ -384,16 +392,19 @@ impl LoadedPack { info!(%tex_path, "failed to find this trail texture"); failure_loading = true; } + } else { + debug!("Trail texture alreadu loaded {:?}", tex_path); } } else { - info!("no texture attribute on this marker"); + info!("no texture attribute on this trail"); } - let th = common_attributes - .get_texture() + let texture_path = common_attributes.get_texture(); + let th = texture_path .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); + .unwrap_or(default_trail_id); let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); tbin } else { info!(?trail, "missing tbin path"); @@ -405,6 +416,7 @@ impl LoadedPack { info!(%tbin_path, "failed to find tbin"); continue; }; + //TODO: if iso and closed, split it as a polygon and fill it as a surface if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( &common_attributes, &tbin.nodes, @@ -413,9 +425,17 @@ impl LoadedPack { self.current_map_data .active_trails .insert(index, active_trail); + } else { + info!("Cannot display {texture_path:?}") } + nb_trails_loaded += 1; + } else { + info!("category {} is not enabled", trail.category); } } + info!("Loaded for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); + debug!("active categories: {:?}", enabled_cats_list.keys()); + if failure_loading { info!("Error when loading textures, here are the keys:"); for k in self.core.textures.keys() { @@ -444,6 +464,19 @@ impl LoadedPack { error!(?e, "failed to serialize cat selection"); } } + match serde_json::to_string_pretty(&self.activation_data) { + Ok(ad_json) => match self.dir.write(Self::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(_) => { + debug!("wrote activation to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write activation data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } } self.dir .create_dir_all(Self::CORE_PACK_DIR_NAME) @@ -507,6 +540,7 @@ pub(crate) struct ActiveMarker { #[derive(Debug, Default, Serialize, Deserialize, Clone)] struct CategorySelection { pub selected: bool, + pub separator: bool, pub display_name: String, pub children: OrderedHashMap, } @@ -533,7 +567,7 @@ impl CategorySelection { name.clone() } else { format!("{parent_name}.{name}") - }; + }.to_lowercase(); let mut common_attributes = cat.props.clone(); common_attributes.inherit_if_attr_none(parent_common_attributes); Self::recursive_get_full_names( @@ -555,6 +589,7 @@ impl CategorySelection { if !selection.contains_key(cat_name) { let mut to_insert = CategorySelection::default(); to_insert.selected = cat.default_enabled; + to_insert.separator = cat.separator; to_insert.display_name = cat.display_name.clone(); selection.insert(cat_name.clone(), to_insert); } @@ -562,24 +597,50 @@ impl CategorySelection { Self::recursive_create_category_selection(&mut s.children, &cat.children); } } + /*fn recursive_selection_data( + selection: &mut OrderedHashMap, + data: &mut json::object::Object, + current_key: &String + ) { + for (name, cat) in selection.iter_mut() { + let mut sub_key = current_key.clone(); + if sub_key.len() > 0 { + sub_key.push_str("."); + } + sub_key.push_str(name); + if cat.separator { + } else { + data.insert(sub_key.as_str(), json::JsonValue::Boolean(cat.selected)); + Self::recursive_selection_data(&mut cat.children, data, &sub_key); + } + } + }*/ + fn recursive_selection_ui( selection: &mut OrderedHashMap, ui: &mut egui::Ui, changed: &mut bool, ) { + if selection.is_empty() { + return; + } egui::ScrollArea::vertical().show(ui, |ui| { - for cat in selection.values_mut() { + for (name, cat) in selection.iter_mut() { ui.horizontal(|ui| { - if ui.checkbox(&mut cat.selected, "").changed() { - *changed = true; + if cat.separator { + ui.add_space(3.0); + } else { + let mut cb = ui.checkbox(&mut cat.selected, ""); + if cb.changed() { + *changed = true; + } } - if !cat.children.is_empty() { + if cat.children.is_empty() { + ui.label(&cat.display_name); + } else { ui.menu_button(&cat.display_name, |ui: &mut egui::Ui| { Self::recursive_selection_ui(&mut cat.children, ui, changed); }); - } else { - ui.label(&cat.display_name); - info!("create category {:?}", cat.display_name); } }); } diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index 93f3438..f24e54f 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -26,7 +26,7 @@ use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; use image::EncodableLayout; -use tracing::{error, info, info_span}; +use tracing::{debug, error, info, info_span}; use jokolink::MumbleLink; use miette::{Context, IntoDiagnostic, Result}; @@ -63,6 +63,7 @@ pub struct MarkerManager { /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. packs: BTreeMap, missing_texture: Option, + missing_trail: Option, /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. /// This allows us to avoid saving the pack too often. pub save_interval: f64, @@ -156,10 +157,12 @@ impl MarkerManager { ui_data: Default::default(), save_interval: 0.0, missing_texture: None, + missing_trail: None }) } fn pack_importer(import_status: Arc>) { + //called when a new pack is imported rayon::spawn(move || { *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; @@ -203,6 +206,18 @@ impl MarkerManager { }, )); } + if self.missing_trail.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/trail.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.missing_trail = Some(etx.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + }, + )); + } for pack in self.packs.values_mut() { pack.tick( @@ -211,6 +226,7 @@ impl MarkerManager { joko_renderer, link, self.missing_texture.as_ref().unwrap(), + self.missing_trail.as_ref().unwrap(), ); } } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index 12ccbfa..691adcb 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -1,6 +1,7 @@ mod common; mod marker; mod trail; +mod route; use std::{str::FromStr}; @@ -11,18 +12,12 @@ pub use common::*; pub(crate) use marker::*; use smol_str::SmolStr; pub(crate) use trail::*; +pub(crate) use route::*; -#[derive(Default, Debug, Clone)] -pub(crate) struct Texture { - pub path: RelativePath, - pub original: String, //raw original name - pub source: String,//where this was defined for the first time - pub bytes: Vec, -} #[derive(Default, Debug, Clone)] pub(crate) struct PackCore { - pub textures: ordered_hash_map::OrderedHashMap, + pub textures: ordered_hash_map::OrderedHashMap>, pub tbins: ordered_hash_map::OrderedHashMap, pub categories: IndexMap, pub maps: ordered_hash_map::OrderedHashMap, @@ -31,6 +26,7 @@ pub(crate) struct PackCore { #[derive(Default, Debug, Clone)] pub(crate) struct MapData { pub markers: Vec, + pub routes: Vec, pub trails: Vec, } diff --git a/crates/joko_marker_format/src/pack/route.rs b/crates/joko_marker_format/src/pack/route.rs new file mode 100644 index 0000000..758d4c5 --- /dev/null +++ b/crates/joko_marker_format/src/pack/route.rs @@ -0,0 +1,13 @@ +use uuid::Uuid; +use glam::Vec3; + +#[derive(Debug, Clone)] +pub(crate) struct Route { + pub category: String, + pub path: Vec, + pub reset_position: Vec3, + pub reset_range: f64, + pub map_id: u32, + pub guid: Uuid, + pub name: String, +} diff --git a/crates/joko_marker_format/src/pack/trail.png b/crates/joko_marker_format/src/pack/trail.png index d4326e68a6bdc79ef11a27f2deee592e3dbc4382..7529ba0fccf9f596a6515371d695d21ad15ef1ab 100755 GIT binary patch literal 6896 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 E19PVjH2?qr delta 2282 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi z!vFvd!vV){sAK>D2#QHWK~#8N?VU%6WknQ*$DA?eoE_=%sJ`T7Q{qZ}E^|8#ih&#{sMOUU7u5 zGS<1`3Gss%HIQ@Rd-0sWEMkeNPNnAnr-`$~zJj&8Ypp0Si#8SSi*H0#W>KjDz?#3M zxJ(=ziWm{?83cI-$5*=}wpJ{KQ}e?+k=B>-FL z41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LYiGP;mfF@IX2?|K~1;zLTIR*f0{(Nz% zVDLklJY9*za3jG?g;|6@;r6mIM7Ga>v*z<; zmXONi@gjTWv;<)Cu+Jw5Ru!eTW$~SEBDNP_3i5(%&7xca0BaS850fW{eg0fUPC#O~ zmVdx3qO6={PLMhPsOsVBT`jP8Q#yQIi&F9q;xqBFAg8feG^DKsRXx_tQR`_9kjvaD z7KrSWat5H8gL?j{g1vODt=Rlty9)9GdI{bZ1V$E06#%qxVf4_ zNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNwI$slb-`3p3Hjz?tP`PcX=W+n_ zTn>Pq%K?y|u!K2k7R4HN0OTj^;D1*JpTQL(0DQGtK?)H7Pl?wj{%DOM9v}|J@EKeo zj38>Ox4S7;LQ3&yegg*+yoQ9@M-;)f}c19Z2-%(|^$B z_oNtu^OkD)dRcHK9VBfM=|X(rz!JmYr(g@gRND5o@Y@A_ilhFn81P+rrhn{6-1yB^ z@w2&CfYUo%0Ck+`%|H-&wdoW=-{GNx!b;o0q&b75TwDstNgi6at3Sz8QvmS0>?kf0 zG+6M$h}nvp$EaeG3%ob@^resmi1m}+gZl)n)}yBQ*>auWI=B#9WvgS+O~V;OwxDg- z{Fz`kaF5{7YPG6i383waE`KBvzb@|;;JaZb!9Brr?6M>f7*(nC0N|Vf2_J_}xTB}* z>>UrGIp8<>#A5JC_+s7`PHxd$0A(c< z(>_l=gWpxc*B0vZTmV(gJBLfl0Z8~*^SL;hbvd?U$MjF@HpwAv+kagD;@iR_h`sn8 zf^46J54SFrFpF>)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<`i>iE~R4&>{knnYB@^l4H z{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m0f4hd!pGA^fhM=XZ`520 z$sLEytL2KRBIXzXtbeB*Ex%k)(jBL&7h71+>d5&(29GYj<6EF$4!&8N3+c&+RcP0M+5 z#hd}y;*k+^hkszrYknJXa2BJBOCXY(MX>?^lZSmi=k9!YcsQQOE!w%5lT+_OW+saQM0w%K)#(=P+vX&f%y% zh9Luhr+uNIutTInS**hUA(1M^?zcD?}zJk={RP47%edj5dhi(@0(iLx*+jC@Hf7C3>yOT?iDW`V#8rv5))`276L!uezNd`7&<4`L`(0I;-7 z#+q3??Lav=$_~J?EX%Si%d#xXvMkH8EX%U2+BI+9|700zjHJNuPyhe`07*qoM6N<$ Ef&$`4_y7O^ diff --git a/crates/joko_marker_format/src/pack/trail.rs b/crates/joko_marker_format/src/pack/trail.rs index e51f66b..635c20c 100644 --- a/crates/joko_marker_format/src/pack/trail.rs +++ b/crates/joko_marker_format/src/pack/trail.rs @@ -8,6 +8,7 @@ pub(crate) struct Trail { pub map_id: u32, pub category: String, pub props: CommonAttributes, + pub dynamic: bool, } #[derive(Debug, Clone)] @@ -16,5 +17,13 @@ pub(crate) struct TBin { pub version: u32, pub nodes: Vec, } +#[derive(Debug, Clone)] +pub(crate) struct TBinStatus { + pub tbin: TBin, + pub iso_x: bool, + pub iso_y: bool, + pub iso_z: bool, + pub closed: bool, +} impl TBin {} diff --git a/crates/joko_marker_format/src/pack/trail_black.png b/crates/joko_marker_format/src/pack/trail_black.png new file mode 100644 index 0000000000000000000000000000000000000000..d4326e68a6bdc79ef11a27f2deee592e3dbc4382 GIT binary patch literal 2293 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#QHWK~#8N?VU%6 zWknQ*$DA?eoE_=%sJa1|knwU2G`c6$D4`bAp<*yx2@UBEAsK zlRt`uVrQ|kuqqLQ|A6>Sj2iS|ZWDV6s{%3Y^W-!5Wci~8e3)MZdBTZeDPa{V627O! zOw;}-z7o%iqXjua^{OceWS@V4xKNPrZ6IbVasa#nzLyWg&*BeZWiGaH3^@Dz$y>d- zaEsVQkW5;cYj5$8U>i4TFvkI__+D{@urk)U;tBDC7&VY{;d}9%z${{ksZOQm0H=ww z#J+;HyKAi|FpD-7?~89lRc2AC0l=ESrMOHSC~)&aiQT~Ff*rz#g2eDIVI>p;e!pPv zK5FU?#jRp@VI|ZF0+WaBSzD8*D=?NG75pEnP-#oR1>ziWm{?83cI-$5*=}wpJ{KQ} ze?+k=B>-FL41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@ z;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LY ziI(MnCR2O~3P|_`#rOm{1^{dRd~vB@@I#tBU5UhSBf(6CS%g2~_OdZVw$FgG=JREa znsRf8D&lT}-^H4x$nqZ+JUzLt`7^)Kw`L-z$~JyoMldsIsmBZ;p$y2uy<2Bd|iuD@($uN z@v$JMu~{^vtp!y**3D7tX$_Fe+$k1_?38i_pqYbu{;7h!bgiw}{9d~X@&bAZ-WCK# z7D^QWv~gkd(9A($rPjf3gtvj2#f(Psx0B~sI27$lN)6i$J^s!6$U62zbQ>*~s zxE3dNjuA9c^j+A*6pt{A=rrbddXkwSMgUB8KELnE;(mgV0YK)vvA9CeFfqNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNw zI$slb-`3p3Hjz?tP`PcX=W+n_Tn>Pq%K?y|u!K2k7R4HN0OTj^;8zBp!4)C^e6?CZ z3K0NLiPtCoXpJErAP&ax8C)TZAZpyWT7V5q)yr<+%=$#UDxMYGdPZF!cpeaHV7Rvr zH;_Xf)VxF09HtW;Nc85@f6(Ulq!@$qmTLKWS#TvCByAGuLVV%C62su9U<<)i+V-~a z+Xa1!qyDZK@LhSP>`2`B%~kQUxmSSGJ6r&DoaoI!5P7xf6hYtNp@PCn+rgwcgQHwr z3du+Bs5p*iPI>oz+D=L8uDlZOUMwt6^d!_#-yFz7c6`t@)( zt;AyRN%&&k7EW%_TmWSy6w^LWK7-#?!q*n+^jrW{%{zxn%K=FESo66!nsqt0W5@JQ z>o&)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<` zi>iE~R4&>{knnYB@^l4H{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m z0f4hd!pGA^fhM=XZ`520$sLEytL2KRBIXzXtfw3;zg$q#9jB@nTUgNS47eJWAfSrq zOCiS;88&Nvskeo52^M)v5z1`_I9yyKnv0}zo%W~GeuAwc)x>xBLC(r408STN9(j}~ z`SM7*p~+b|-Ah*ySHG}($x^ul0Ok(2hw@2AUAmY7a4NYc$!5_sA^=RAPd2+>^0odY^5p#!N&1-%eac~x+i%TGqnnke!0F#G(KIiUydE`XB zgQgaiL-GOY2T9DL7y*F2i^InZz#3>xhyoIhC%FN$0OjQ9W>LrhAj)yW@bizC~KvM?`e{ P00000NkvXXu0mjf7Ck~> literal 0 HcmV?d00001 diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index bf710ab..a83ecda 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -70,7 +70,7 @@ impl JokoRenderer { Vector3::unit_y(), Deg(90.0), 1.0, - 5000.0, + 5000.0,//FIXME: trails may have points very far apart, when loading, one should fix those by putting intermediary points. ), link: Default::default(), gl: backend, From bbb9e50a2248fac188b5c12cebe7dcc6e9976c4f Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 20 Mar 2024 14:17:09 +0100 Subject: [PATCH 10/54] add source file + start to reorganize project into individual components - next goal is to have a view per element with definition/configuration/state in same file for each component --- .../joko_marker_format/src/io/deserialize.rs | 68 +- crates/joko_marker_format/src/io/mod.rs | 2 + crates/joko_marker_format/src/io/serialize.rs | 3 + crates/joko_marker_format/src/lib.rs | 1 + crates/joko_marker_format/src/manager/file.rs | 80 ++ .../src/manager/live_pack.rs | 879 ------------------ .../joko_marker_format/src/manager/marker.rs | 301 ++++++ crates/joko_marker_format/src/manager/mod.rs | 345 +------ .../src/manager/pack/activation.rs | 21 + .../src/manager/pack/active.rs | 281 ++++++ .../src/manager/pack/category_selection.rs | 102 ++ .../src/manager/pack/dirty.rs | 29 + .../src/manager/pack/entry.rs | 6 + .../src/manager/pack/import.rs | 37 + .../src/manager/pack/list.rs | 6 + .../src/manager/pack/loaded.rs | 467 ++++++++++ .../src/manager/pack/mod.rs | 7 + crates/joko_marker_format/src/pack/marker.rs | 1 + crates/joko_marker_format/src/pack/route.rs | 1 + crates/joko_marker_format/src/pack/trail.rs | 1 + crates/jokolay/src/app/mod.rs | 15 + 21 files changed, 1401 insertions(+), 1252 deletions(-) create mode 100644 crates/joko_marker_format/src/manager/file.rs delete mode 100644 crates/joko_marker_format/src/manager/live_pack.rs create mode 100644 crates/joko_marker_format/src/manager/marker.rs create mode 100644 crates/joko_marker_format/src/manager/pack/activation.rs create mode 100644 crates/joko_marker_format/src/manager/pack/active.rs create mode 100644 crates/joko_marker_format/src/manager/pack/category_selection.rs create mode 100644 crates/joko_marker_format/src/manager/pack/dirty.rs create mode 100644 crates/joko_marker_format/src/manager/pack/entry.rs create mode 100644 crates/joko_marker_format/src/manager/pack/import.rs create mode 100644 crates/joko_marker_format/src/manager/pack/list.rs create mode 100644 crates/joko_marker_format/src/manager/pack/loaded.rs create mode 100644 crates/joko_marker_format/src/manager/pack/mod.rs diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 0a882a4..38b2c36 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -166,7 +166,7 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let zero = Vec3{x:0.0, y:0.0, z:0.0}; // this will either be empty vec or series of vec3s. - let mut nodes: VecDeque = bytes[8..] + let nodes: VecDeque = bytes[8..] .chunks_exact(12) .map(|float_bytes| { // make [f32 ;3] out of those 12 bytes @@ -395,10 +395,11 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result }) .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; + let source_file_name = child.get_attribute(names._source_file_name).unwrap_or_default().to_string(); //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail if child.name() == names.route { debug!("Found a route in core pack {:?}", child); - import_route_as_trail(pack, &names, &tree, &poi_node, child, category) + import_route_as_trail(pack, &names, &tree, &poi_node, child, category, source_file_name) } else if child.name() == names.poi { debug!("Found a POI in core pack {:?}", child); @@ -434,6 +435,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result category, attrs: ca, guid, + source_file_name }; if !pack.maps.contains_key(&map_id) { @@ -459,6 +461,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result props: ca, guid, dynamic: false, + source_file_name }; if !pack.maps.contains_key(&map_id) { @@ -483,7 +486,6 @@ fn recursive_marker_category_parser_categories_xml( for tag in tags { if let Some(ele) = tree.element(tag) { if ele.name() != names.marker_category { - let name = ele.name(); continue; } @@ -621,12 +623,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } std::mem::drop(span); } - for name in xmls { + for source_file_name in xmls { let mut xml_str = String::new(); - let xml_file_name = name.clone(); - let span_guard = info_span!("deserialize xml", xml_file_name).entered(); + let span_guard = info_span!("deserialize xml", source_file_name).entered(); if zip_archive - .by_name(&name) + .by_name(&source_file_name) .ok() .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) .is_none() @@ -684,11 +685,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { debug!("import element: {:?}", child); if child.name() == names.poi { - import_poi(&mut pack, &names, &child, category); + import_poi(&mut pack, &names, &child, category, source_file_name.clone()); } else if child.name() == names.trail { - import_trail(&mut pack, &names, &child, category); + import_trail(&mut pack, &names, &child, category, source_file_name.clone()); } else if child.name() == names.route { - import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category); + import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category, source_file_name.clone()); } else { info!("unknown element: {:?}", child); } @@ -717,7 +718,7 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ .unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String) -> Option { +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String, source_file_name: String) -> Option { if let Some(map_id) = poi_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -752,6 +753,7 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & category, attrs: common_attributes, guid: parse_guid(names, poi_element), + source_file_name }) } else { info!("missing map id"); @@ -778,7 +780,15 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { Vec3{x, y, z} } -fn parse_route(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) -> Option { +fn parse_route( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, + category: String, + source_file_name: String +) -> Option { let mut path: Vec = Vec::new(); let resetposx = route_element @@ -847,12 +857,13 @@ fn parse_route(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, rou reset_range: reset_range.unwrap_or(0.0), map_id: map_id.unwrap(), name: name.unwrap().into(), - guid: parse_guid(names, &route_element) + guid: parse_guid(names, &route_element), + source_file_name, }) } -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String) -> Option { +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String, source_file_name: String) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html if let Some(map_id) = trail_element .get_attribute(names.trail_data) @@ -875,7 +886,8 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: map_id, props: common_attributes, guid: parse_guid(names, trail_element), - dynamic: false + dynamic: false, + source_file_name, }) } else { let td = trail_element.get_attribute(names.trail_data); @@ -887,8 +899,8 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } -fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String) { - if let Some(marker) = parse_marker(pack, names, poi_element, category) { +fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String, source_file_name: String) { + if let Some(marker) = parse_marker(pack, names, poi_element, category, source_file_name) { if !pack.maps.contains_key(&marker.map_id) { pack.maps.insert(marker.map_id, MapData::default()); } @@ -899,8 +911,8 @@ fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &El } -fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String) { - if let Some(trail) = parse_trail(pack, names, trail_element, category) { +fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String, source_file_name: String) { + if let Some(trail) = parse_trail(pack, names, trail_element, category, source_file_name) { if !pack.maps.contains_key(&trail.map_id) { pack.maps.insert(trail.map_id, MapData::default()); } @@ -931,12 +943,13 @@ fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { category: route.category.clone(), guid: route.guid, props: props, - dynamic: true + dynamic: true, + source_file_name: route.source_file_name.clone(), } } -fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) { - if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category) { +fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String, source_file_name: String) { + if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category, source_file_name) { let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); let file_path: RelativePath = file_name.parse().unwrap(); let trail = route_to_trail(&route, &file_path); @@ -952,17 +965,6 @@ fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: } } -fn import_route(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String) { - if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category) { - if !pack.maps.contains_key(&route.map_id) { - pack.maps.insert(route.map_id, MapData::default()); - } - pack.maps.get_mut(&route.map_id).unwrap().routes.push(route); - } else { - info!("Could not parse Route"); - } -} - #[instrument(skip(zip_archive))] fn read_file_bytes_from_zip_by_name( name: &str, diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs index e39f12a..5ee12b2 100644 --- a/crates/joko_marker_format/src/io/mod.rs +++ b/crates/joko_marker_format/src/io/mod.rs @@ -93,6 +93,7 @@ pub(crate) struct XotAttributeNameIDs { pub resetposx: NameId, pub resetposy: NameId, pub resetposz: NameId, + pub _source_file_name: NameId, } impl XotAttributeNameIDs { pub fn register_with_xot(tree: &mut Xot) -> Self { @@ -181,6 +182,7 @@ impl XotAttributeNameIDs { resetposx: tree.add_name("resetposx"), resetposy: tree.add_name("resetposy"), resetposz: tree.add_name("resetposz"), + _source_file_name: tree.add_name("_source_file_name"), } } } diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index 390bea4..28040ae 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -207,6 +207,7 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); ele.set_attribute(names.category, &trail.category); ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute(names._source_file_name, &trail.source_file_name); trail.props.serialize_to_element(ele, names); } @@ -217,6 +218,7 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); + ele.set_attribute(names._source_file_name, &marker.source_file_name); marker.attrs.serialize_to_element(ele, names); } @@ -236,6 +238,7 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); ele.set_attribute(names.map_id, format!("{}", route.map_id)); ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute(names._source_file_name, &route.source_file_name); for pos in &route.path { let child = tree.new_element(names.poi); tree.append(route_node, child); diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs index 14b1d3b..f225b44 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_marker_format/src/lib.rs @@ -7,6 +7,7 @@ pub(crate) mod manager; pub(crate) mod pack; pub use manager::MarkerManager; +pub use manager::FileManager; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); diff --git a/crates/joko_marker_format/src/manager/file.rs b/crates/joko_marker_format/src/manager/file.rs new file mode 100644 index 0000000..8303221 --- /dev/null +++ b/crates/joko_marker_format/src/manager/file.rs @@ -0,0 +1,80 @@ +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +use cap_std::fs_utf8::Dir; +use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; +use image::EncodableLayout; + +use tracing::{error, info, info_span}; + +use jokolink::MumbleLink; +use miette::{Context, IntoDiagnostic, Result}; + +use crate::manager::pack::loaded::LoadedPack; + +pub const FILE_MANAGER_DIRECTORY_NAME: &str = "file_manager"; + +pub struct FileManager { + /// holds data that is useful for the ui + ui_data: FileManagerUI, + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + packs: BTreeMap, + missing_texture: Option, + missing_trail: Option, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, +} + +#[derive(Debug, Default)] +pub(crate) struct FileManagerUI { + // tf is this type supposed to be? maybe we should have used a ECS for this reason. + +} + + +impl FileManager { + pub fn new(jdir: &Dir) -> Result { + jdir.create_dir_all(FILE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to create file manager directory")?; + let mut packs: BTreeMap = Default::default(); + + Ok(Self { + packs, + ui_data: Default::default(), + save_interval: 0.0, + missing_texture: None, + missing_trail: None + }) + } + + pub fn tick( + &mut self, + etx: &egui::Context, + timestamp: f64, + joko_renderer: &mut joko_render::JokoRenderer, + link: &Option>, + ) { + } + pub fn menu_ui(&mut self, ui: &mut egui::Ui) { + ui.menu_button("Files", |ui| { + for pack in self.packs.values_mut() { + pack.category_sub_menu(ui); + } + }); + } + + pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { + Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { + //TODO: display list of currently loaded files + Ok(()) + }); + } +} + diff --git a/crates/joko_marker_format/src/manager/live_pack.rs b/crates/joko_marker_format/src/manager/live_pack.rs deleted file mode 100644 index 9416a4d..0000000 --- a/crates/joko_marker_format/src/manager/live_pack.rs +++ /dev/null @@ -1,879 +0,0 @@ -use std::{ - sync::Arc, -}; -use ordered_hash_map::{OrderedHashMap, OrderedHashSet}; - -use cap_std::fs_utf8::Dir; -use egui::{ColorImage, TextureHandle}; -use glam::{vec2, Vec2, Vec3}; -use image::{flat, EncodableLayout}; -use indexmap::IndexMap; -use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; -use tracing::{debug, error, info, trace}; -use uuid::Uuid; - -use crate::{ - io::{load_pack_core_from_dir, save_pack_core_to_dir}, - pack::{Category, CommonAttributes, PackCore, RelativePath}, - INCHES_PER_METER, -}; -use jokolink::MumbleLink; -use miette::{bail, Context, IntoDiagnostic, Result}; -use serde::{Deserialize, Serialize}; - -pub(crate) struct LoadedPack { - /// The directory inside which the pack data is stored - /// There should be a subdirectory called `core` which stores the pack core - /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. - /// eg: Active categories, activation data etc.. - pub dir: Arc, - /// The actual xml pack. - pub core: PackCore, - /// The selection of categories which are "enabled" and markers belonging to these may be rendered - cats_selection: OrderedHashMap, - dirty: Dirty, - activation_data: ActivationData, - current_map_data: CurrentMapData, -} - -#[derive(Debug, Default, Clone)] -struct Dirty { - all: bool, - /// whether categories need to be saved - cats: bool, - /// whether cats selection needs to be saved - cats_selection: bool, - /// Whether any mapdata needs saving - map_dirty: OrderedHashSet, - /// whether any texture needs saving - texture: OrderedHashSet, - /// whether any tbin needs saving - tbin: OrderedHashSet, -} - -impl Dirty { - fn is_dirty(&self) -> bool { - self.cats - || self.cats_selection - || !self.map_dirty.is_empty() - || !self.texture.is_empty() - || !self.tbin.is_empty() - } -} -/// This is the activation data per pack -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -pub struct ActivationData { - /// this is for markers which are global and only activate once regardless of account - pub global: IndexMap, - /// this is the activation data per character - /// for markers which trigger once per character - pub character: IndexMap>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ActivationType { - /// clean these up when the map is changed - ReappearOnMapChange, - /// clean these up when the timestamp is reached - TimeStamp(time::OffsetDateTime), - Instance(std::net::IpAddr), -} -impl LoadedPack { - const CORE_PACK_DIR_NAME: &str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &str = "cats.json"; - const ACTIVATION_DATA_FILE_NAME: &str = "activation.json"; - - pub fn new(core: PackCore, dir: Arc) -> Self { - let cats_selection = CategorySelection::default_from_pack_core(&core); - LoadedPack { - core, - cats_selection, - dirty: Dirty { - all: true, - ..Default::default() - }, - current_map_data: Default::default(), - dir, - activation_data: Default::default(), - } - } - pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { - //it is important to generate a new id each time to avoid collision - ui.push_id(ui.next_auto_id(), |ui| { - CategorySelection::recursive_selection_ui( - &mut self.cats_selection, - ui, - &mut self.dirty.cats_selection, - ); - }); - } - pub fn load_from_dir(pack_dir: Arc) -> Result { - if !pack_dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? - { - bail!("pack core doesn't exist in this pack"); - } - let core_dir = pack_dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - - let cats_selection = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { - match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize category data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&core); - match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - cs - }); - let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { - match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); - Ok(LoadedPack { - dir: pack_dir, - core, - cats_selection, - dirty: Default::default(), - current_map_data: Default::default(), - activation_data, - }) - } - pub fn tick( - &mut self, - etx: &egui::Context, - _timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, - default_tex_id: &TextureHandle, - default_trail_id: &TextureHandle, - ) { - let categories_changed = self.dirty.cats_selection; - if self.dirty.is_dirty() { - match self.save() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to save marker pack"); - } - } - } - let link = match link { - Some(link) => link, - None => return, - }; - - if self.current_map_data.map_id != link.map_id || categories_changed { - self.on_map_changed(etx, link, default_tex_id, default_trail_id); - } - let z_near = joko_renderer.get_z_near(); - for marker in self.current_map_data.active_markers.values() { - if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { - joko_renderer.add_billboard(mo); - } - } - for trail in self.current_map_data.active_trails.values() { - joko_renderer.add_trail(TrailObject { - vertices: trail.trail_object.vertices.clone(), - texture: trail.trail_object.texture, - }); - } - } - fn on_map_changed( - &mut self, - etx: &egui::Context, - link: &MumbleLink, - default_tex_id: &TextureHandle, - default_trail_id: &TextureHandle, - ) { - info!( - self.current_map_data.map_id, - link.map_id, "current map data is updated." - ); - self.current_map_data = Default::default(); - if link.map_id == 0 { - info!("No map do not do anything"); - return; - } - self.current_map_data.map_id = link.map_id; - let mut enabled_cats_list = Default::default(); - CategorySelection::recursive_get_full_names( - &self.cats_selection, - &self.core.categories, - &mut enabled_cats_list, - "", - &Default::default(), - ); - let mut failure_loading = false; - let mut nb_markers_attempt = 0; - let mut nb_markers_loaded = 0; - for (index, marker) in self - .core - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .markers - .iter() - .enumerate() - { - nb_markers_attempt += 1; - if let Some(category_attributes) = enabled_cats_list.get(&marker.category) { - let mut attrs = marker.attrs.clone(); - attrs.inherit_if_attr_none(category_attributes); - let key = &marker.guid; - if let Some(behavior) = attrs.get_behavior() { - use crate::pack::Behavior; - if match behavior { - Behavior::AlwaysVisible => false, - Behavior::ReappearOnMapChange - | Behavior::ReappearOnDailyReset - | Behavior::OnlyVisibleBeforeActivation - | Behavior::ReappearAfterTimer - | Behavior::ReappearOnMapReset - | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), - Behavior::OncePerInstance => self - .activation_data - .global - .get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default(), - Behavior::DailyPerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| a.contains_key(key)) - .unwrap_or_default(), - Behavior::OncePerInstancePerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| { - a.get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default() - }) - .unwrap_or_default(), - Behavior::WvWObjective => { - false // ??? - } - } { - continue; - } - } - if let Some(tex_path) = attrs.get_icon_file() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this icon texture"); - failure_loading = true; - } - } - } else { - info!("no texture attribute on this marker"); - } - let th = attrs - .get_icon_file() - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - let texture_id = match th.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }; - - let max_pixel_size = attrs.get_max_size().copied().unwrap_or(2048.0); // default taco max size - let min_pixel_size = attrs.get_min_size().copied().unwrap_or(5.0); // default taco min size - self.current_map_data.active_markers.insert( - index, - ActiveMarker { - texture_id, - _texture: th.clone(), - attrs, - pos: marker.position, - max_pixel_size, - min_pixel_size, - }, - ); - nb_markers_loaded += 1; - } - } - - let mut nb_trails_attempt = 0; - let mut nb_trails_loaded = 0; - for (index, trail) in self - .core - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .trails - .iter() - .enumerate() - { - nb_trails_attempt += 1; - if let Some(category_attributes) = enabled_cats_list.get(&trail.category) { - let mut common_attributes = trail.props.clone(); - common_attributes.inherit_if_attr_none(category_attributes); - if let Some(tex_path) = common_attributes.get_texture() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this trail texture"); - failure_loading = true; - } - } else { - debug!("Trail texture alreadu loaded {:?}", tex_path); - } - } else { - info!("no texture attribute on this trail"); - } - let texture_path = common_attributes.get_texture(); - let th = texture_path - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_trail_id); - - let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { - debug!(?texture_path, "tbin path"); - tbin - } else { - info!(?trail, "missing tbin path"); - continue; - }; - let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { - tbin - } else { - info!(%tbin_path, "failed to find tbin"); - continue; - }; - //TODO: if iso and closed, split it as a polygon and fill it as a surface - if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( - &common_attributes, - &tbin.nodes, - th.clone(), - ) { - self.current_map_data - .active_trails - .insert(index, active_trail); - } else { - info!("Cannot display {texture_path:?}") - } - nb_trails_loaded += 1; - } else { - info!("category {} is not enabled", trail.category); - } - } - info!("Loaded for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); - debug!("active categories: {:?}", enabled_cats_list.keys()); - - if failure_loading { - info!("Error when loading textures, here are the keys:"); - for k in self.core.textures.keys() { - info!(%k); - } - info!("end of keys"); - } - } - pub fn save_all(&mut self) -> Result<()> { - self.dirty.all = true; - self.save() - } - #[tracing::instrument(skip(self))] - pub fn save(&mut self) -> Result<()> { - if std::mem::take(&mut self.dirty.cats_selection) || self.dirty.all { - match serde_json::to_string_pretty(&self.cats_selection) { - Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - match serde_json::to_string_pretty(&self.activation_data) { - Ok(ad_json) => match self.dir.write(Self::ACTIVATION_DATA_FILE_NAME, ad_json) { - Ok(_) => { - debug!("wrote activation to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write activation data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize activation"); - } - } - } - self.dir - .create_dir_all(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to create xmlpack directory")?; - let core_dir = self - .dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - save_pack_core_to_dir( - &self.core, - &core_dir, - std::mem::take(&mut self.dirty.cats), - std::mem::take(&mut self.dirty.map_dirty), - std::mem::take(&mut self.dirty.texture), - std::mem::take(&mut self.dirty.tbin), - std::mem::take(&mut self.dirty.all), - )?; - Ok(()) - } -} - -#[derive(Default)] -pub(crate) struct CurrentMapData { - /// the map to which the current map data belongs to - pub map_id: u32, - /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: OrderedHashMap, - /// The key is the index of the marker in the map markers - /// Their position in the map markers serves as their "id" as uuids can be duplicates. - pub active_markers: IndexMap, - /// The key is the position/index of this trail in the map trails. same as markers - pub active_trails: IndexMap, -} - -/* -- activation data with uuids and track the latest timestamp that will be activated -- category activation data -> track and changes to propagate to markers of this map -- current active markers, which will keep track of their original marker, so as to propagate any changes easily -*/ -pub struct ActiveTrail { - pub trail_object: TrailObject, - pub texture_handle: TextureHandle, -} -/// This is an active marker. -/// It stores all the info that we need to scan every frame -pub(crate) struct ActiveMarker { - /// texture id from managed textures - pub texture_id: u64, - /// owned texture handle to keep it alive - pub _texture: TextureHandle, - /// position - pub pos: Vec3, - /// billboard must not be bigger than this size in pixels - pub max_pixel_size: f32, - /// billboard must not be smaller than this size in pixels - pub min_pixel_size: f32, - pub attrs: CommonAttributes, -} -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -struct CategorySelection { - pub selected: bool, - pub separator: bool, - pub display_name: String, - pub children: OrderedHashMap, -} - -impl CategorySelection { - fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { - let mut selection = OrderedHashMap::new(); - Self::recursive_create_category_selection(&mut selection, &pack.categories); - selection - } - fn recursive_get_full_names( - selection: &OrderedHashMap, - cats: &IndexMap, - list: &mut OrderedHashMap, - parent_name: &str, - parent_common_attributes: &CommonAttributes, - ) { - for (name, cat) in cats { - if let Some(selected_cat) = selection.get(name) { - if !selected_cat.selected { - continue; - } - let full_name = if parent_name.is_empty() { - name.clone() - } else { - format!("{parent_name}.{name}") - }.to_lowercase(); - let mut common_attributes = cat.props.clone(); - common_attributes.inherit_if_attr_none(parent_common_attributes); - Self::recursive_get_full_names( - &selected_cat.children, - &cat.children, - list, - &full_name, - &common_attributes, - ); - list.insert(full_name, common_attributes); - } - } - } - fn recursive_create_category_selection( - selection: &mut OrderedHashMap, - cats: &IndexMap, - ) { - for (cat_name, cat) in cats.iter() { - if !selection.contains_key(cat_name) { - let mut to_insert = CategorySelection::default(); - to_insert.selected = cat.default_enabled; - to_insert.separator = cat.separator; - to_insert.display_name = cat.display_name.clone(); - selection.insert(cat_name.clone(), to_insert); - } - let mut s = selection.get_mut(cat_name).unwrap(); - Self::recursive_create_category_selection(&mut s.children, &cat.children); - } - } - /*fn recursive_selection_data( - selection: &mut OrderedHashMap, - data: &mut json::object::Object, - current_key: &String - ) { - for (name, cat) in selection.iter_mut() { - let mut sub_key = current_key.clone(); - if sub_key.len() > 0 { - sub_key.push_str("."); - } - sub_key.push_str(name); - if cat.separator { - } else { - data.insert(sub_key.as_str(), json::JsonValue::Boolean(cat.selected)); - Self::recursive_selection_data(&mut cat.children, data, &sub_key); - } - } - }*/ - - fn recursive_selection_ui( - selection: &mut OrderedHashMap, - ui: &mut egui::Ui, - changed: &mut bool, - ) { - if selection.is_empty() { - return; - } - egui::ScrollArea::vertical().show(ui, |ui| { - for (name, cat) in selection.iter_mut() { - ui.horizontal(|ui| { - if cat.separator { - ui.add_space(3.0); - } else { - let mut cb = ui.checkbox(&mut cat.selected, ""); - if cb.changed() { - *changed = true; - } - } - if cat.children.is_empty() { - ui.label(&cat.display_name); - } else { - ui.menu_button(&cat.display_name, |ui: &mut egui::Ui| { - Self::recursive_selection_ui(&mut cat.children, ui, changed); - }); - } - }); - } - }); - } -} - -pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; - -impl ActiveMarker { - pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { - let Self { - texture_id, - pos, - attrs, - _texture, - max_pixel_size, - min_pixel_size, - .. - } = self; - // let width = *width; - // let height = *height; - let texture_id = *texture_id; - let pos = *pos; - // filters - if let Some(mounts) = attrs.get_mount() { - if let Some(current) = link.mount { - if !mounts.contains(current) { - return None; - } - } else { - return None; - } - } - let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); - let player_distance = pos.distance(link.player_pos); - let camera_distance = pos.distance(link.cam_pos); - let fade_near_far = Vec2::new(fade_near, fade_far); - - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let color = attrs.get_color().copied().unwrap_or_default(); - /* - 1. we need to filter the markers - 1. statically - mapid, character, map_type, race, profession - 2. dynamically - achievement, behavior, mount, fade_far, cull - 3. force hide/show by user discretion - 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior - 3. store the state for these markers activation data, and temporary data like bounce - */ - /* - skip if: - alpha is 0.0 - achievement id/bit is done (maybe this should be at map filter level?) - behavior (activation) - cull - distance > fade_far - visibility (ingame/map/minimap) - mount - specialization - */ - if fade_far > 0.0 && player_distance > fade_far { - return None; - } - // markers are 1 meter in width/height by default - let mut pos = pos; - pos.y += height_offset; - let direction_to_marker = link.cam_pos - pos; - let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); - - let far_offset = { - let dpi = if link.dpi_scaling <= 0 { - 96.0 - } else { - link.dpi as f32 - } / 96.0; - let gw2_width = link.client_size.as_vec2().x / dpi; - - // offset (half width i.e. distance from center of the marker to the side of the marker) - const SIDE_OFFSET_FAR: f32 = 1.0; - // the size of the projected on to the near plane - let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); - // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width - let near_offset_in_pixels = near_offset * gw2_width; - - // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width - let near_offset_in_pixels = near_offset_in_pixels - .clamp(*min_pixel_size, *max_pixel_size) - .min(gw2_width / 2.0); - - let near_offset_of_marker = near_offset_in_pixels / gw2_width; - near_offset_of_marker * camera_distance / z_near - }; - // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; - // we want to map 100 pixels to one meter in game - // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard - // But, i will ignore that as that makes markers too small - let x_offset = far_offset; - let y_offset = x_offset; // seems all markers are squares - let bottom_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 1.0), - alpha, - color, - fade_near_far, - }; - - let top_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 0.0), - alpha, - color, - fade_near_far, - }; - let top_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 0.0), - alpha, - color, - fade_near_far, - }; - let bottom_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 1.0), - alpha, - color, - fade_near_far, - }; - let vertices = [ - top_left, - bottom_left, - bottom_right, - bottom_right, - top_right, - top_left, - ]; - Some(MarkerObject { - vertices, - texture: texture_id, - distance: player_distance, - }) - } -} - -impl ActiveTrail { - fn get_vertices_and_texture( - attrs: &CommonAttributes, - positions: &[Vec3], - texture: TextureHandle, - ) -> Option { - // can't have a trail without atleast two nodes - if positions.len() < 2 { - return None; - } - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_near_far = Vec2::new(fade_near, fade_far); - let color = attrs.get_color().copied().unwrap_or([0u8; 4]); - // default taco width - let horizontal_offset = 20.0 / INCHES_PER_METER; - // scale it trail scale - let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); - let height = horizontal_offset * 2.0; - - let mut vertices = vec![]; - // trail mesh is split by separating different parts with a [0, 0, 0] - // we will call each separate trail mesh as a "strip" of trail. - // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. - for strip in positions.split(|&v| v == Vec3::ZERO) { - let mut y_offset = 1.0; - for two_positions in strip.windows(2) { - let first = two_positions[0]; - let second = two_positions[1]; - // right side of the vector from first to second - let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); - - let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; - let first_left = MarkerVertex { - position: first - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, y_offset), - alpha, - color, - fade_near_far, - }; - let first_right = MarkerVertex { - position: first + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, y_offset), - alpha, - color, - fade_near_far, - }; - let second_left = MarkerVertex { - position: second - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, new_offset), - alpha, - color, - fade_near_far, - }; - let second_right = MarkerVertex { - position: second + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, new_offset), - alpha, - color, - fade_near_far, - }; - y_offset = if new_offset.is_sign_positive() { - new_offset - } else { - 1.0 - new_offset.fract().abs() - }; - vertices.extend([ - second_left, - first_left, - first_right, - first_right, - second_right, - second_left, - ]); - } - } - - Some(ActiveTrail { - trail_object: TrailObject { - vertices: vertices.into(), - texture: match texture.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }, - }, - texture_handle: texture, - }) - } -} diff --git a/crates/joko_marker_format/src/manager/marker.rs b/crates/joko_marker_format/src/manager/marker.rs new file mode 100644 index 0000000..d67be4c --- /dev/null +++ b/crates/joko_marker_format/src/manager/marker.rs @@ -0,0 +1,301 @@ +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +use cap_std::fs_utf8::Dir; +use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; +use image::EncodableLayout; + +use tracing::{error, info, info_span}; + +use jokolink::MumbleLink; +use miette::{Context, IntoDiagnostic, Result}; + +use crate::manager::pack::loaded::LoadedPack; +use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; + +pub const MARKER_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; +pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; +// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; + +/// It manage everything that has to do with marker packs. +/// 1. imports, loads, saves and exports marker packs. +/// 2. maintains the categories selection data for every pack +/// 3. contains activation data globally and per character +/// 4. When we load into a map, it filters the markers and runs the logic every frame +/// 1. If a marker needs to be activated (based on player position or whatever) +/// 2. marker needs to be drawn +/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture +/// 4. render that marker use joko_render +pub struct MarkerManager { + /// holds data that is useful for the ui + ui_data: MarkerManagerUI, + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + _marker_manager_dir: Arc, + /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. + /// The name of the child directory is the name of the pack + marker_packs_dir: Arc, + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + packs: BTreeMap, + missing_texture: Option, + missing_trail: Option, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, +} + +#[derive(Debug, Default)] +pub(crate) struct MarkerManagerUI { + // tf is this type supposed to be? maybe we should have used a ECS for this reason. + pub import_status: Option>>, +} + + +impl MarkerManager { + /// Creates a new instance of [MarkerManager]. + /// 1. It opens the marker manager directory + /// 2. loads its configuration + /// 3. opens the packs directory + /// 4. loads all the packs + /// 5. loads all the activation data + /// 6. returns self + pub fn new(jdir: &Dir) -> Result { + jdir.create_dir_all(MARKER_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to create marker manager directory")?; + let marker_manager_dir = jdir + .open_dir(MARKER_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to open marker manager directory")?; + marker_manager_dir + .create_dir_all(MARKER_PACKS_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to create marker packs directory")?; + let marker_packs_dir = marker_manager_dir + .open_dir(MARKER_PACKS_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to open marker packs dir")?; + let mut packs: BTreeMap = Default::default(); + + for entry in marker_packs_dir + .entries() + .into_diagnostic() + .wrap_err("failed to get entries of marker packs dir")? + { + let entry = entry.into_diagnostic()?; + if entry.metadata().into_diagnostic()?.is_file() { + continue; + } + if let Ok(name) = entry.file_name() { + let pack_dir = entry + .open_dir() + .into_diagnostic() + .wrap_err("failed to open pack entry as directory")?; + { + let span_guard = info_span!("loading pack from dir", name).entered(); + match LoadedPack::load_from_dir(pack_dir.into()) { + Ok(lp) => { + packs.insert(name, lp); + } + Err(e) => { + error!(?e, "failed to load pack from directory"); + } + } + drop(span_guard); + } + } + } + + Ok(Self { + packs, + marker_packs_dir: marker_packs_dir.into(), + _marker_manager_dir: marker_manager_dir.into(), + ui_data: Default::default(), + save_interval: 0.0, + missing_texture: None, + missing_trail: None + }) + } + + fn pack_importer(import_status: Arc>) { + //called when a new pack is imported + rayon::spawn(move || { + *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + + if let Some(file_path) = rfd::FileDialog::new() + .add_filter("taco", &["zip", "taco"]) + .pick_file() + { + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); + + let result = import_pack_from_zip_file_path(file_path); + match result { + Ok((name, pack)) => { + *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); + } + Err(e) => { + *import_status.lock().unwrap() = ImportStatus::PackError(e); + } + } + } else { + *import_status.lock().unwrap() = + ImportStatus::PackError(miette::miette!("file chooser was cancelled")); + } + }); + } + pub fn tick( + &mut self, + etx: &egui::Context, + timestamp: f64, + joko_renderer: &mut joko_render::JokoRenderer, + link: &Option>, + ) { + if self.missing_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.missing_texture = Some(etx.load_texture( + "default marker", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + }, + )); + } + if self.missing_trail.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/trail.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.missing_trail = Some(etx.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + }, + )); + } + + for pack in self.packs.values_mut() { + pack.tick( + etx, + timestamp, + joko_renderer, + link, + self.missing_texture.as_ref().unwrap(), + self.missing_trail.as_ref().unwrap(), + ); + } + } + pub fn menu_ui(&mut self, ui: &mut egui::Ui) { + ui.menu_button("Markers", |ui| { + for pack in self.packs.values_mut() { + pack.category_sub_menu(ui); + } + }); + } + pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { + Window::new("Marker Manager").open(open).show(etx, |ui| -> Result<()> { + CollapsingHeader::new("Loaded Packs").show(ui, |ui| { + egui::Grid::new("packs").striped(true).show(ui, |ui| { + let mut delete = vec![]; + for pack in self.packs.keys() { + ui.label(pack); + if ui.button("delete").clicked() { + delete.push(pack.clone()); + } + } + for pack_name in delete { + self.packs.remove(&pack_name); + if let Err(e) = self.marker_packs_dir.remove_dir_all(&pack_name) { + error!(?e, pack_name,"failed to remove pack"); + } else { + info!("deleted marker pack: {pack_name}"); + } + } + }); + }); + + if self.ui_data.import_status.is_some() { + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + self.ui_data.import_status = None; + } + } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + let import_status = Arc::new(Mutex::default()); + self.ui_data.import_status = Some(import_status.clone()); + Self::pack_importer(import_status); + } + if let Some(import_status) = self.ui_data.import_status.as_ref() { + if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + ui.label("import not started yet"); + } + ImportStatus::WaitingForFileChooser => { + ui.label( + "wailting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + + if !*saved { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + let name = name.as_str(); + if ui.button("save").clicked() { + + if self.marker_packs_dir.exists(name) { + self.marker_packs_dir + .remove_dir_all(name) + .into_diagnostic()?; + } + if let Err(e) = self.marker_packs_dir.create_dir_all(name) { + error!(?e, "failed to create directory for pack"); + + } + match self.marker_packs_dir.open_dir(name) { + Ok(dir) => { + let core = std::mem::take(pack); + let mut loaded_pack = LoadedPack::new(core, dir.into()); + match loaded_pack.save_all() { + Ok(_) => { + self.packs.insert(name.to_string(), loaded_pack); + *saved = true; + }, + Err(e) => { + error!(?e, "failed to save marker pack"); + }, + } + }, + Err(e) => { + error!(?e, "failed to open marker pack directory to save pack"); + } + }; + } + } else { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } + } + ImportStatus::PackError(e) => { + ui.colored_label( + egui::Color32::RED, + format!("failed to import pack due to error: {e:#?}"), + ); + } + } + } + } + + Ok(()) + }); + } +} + diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index f24e54f..9154585 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -15,346 +15,11 @@ editing a category's name/path means that you have to load all the maps that ref We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. */ -mod live_pack; -use std::{ - collections::BTreeMap, - io::Read, - sync::{Arc, Mutex}, -}; -use cap_std::fs_utf8::Dir; -use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; -use image::EncodableLayout; +mod marker; +mod pack; +mod file; -use tracing::{debug, error, info, info_span}; +pub use marker::MarkerManager; +pub use file::FileManager; -use jokolink::MumbleLink; -use miette::{Context, IntoDiagnostic, Result}; - -use self::live_pack::LoadedPack; - -use super::pack::PackCore; - -// pub const PACK_LIST_URL: &str = "https://packlist.jokolay.com/packlist.json"; - -pub const MARKER_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; -pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; -// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; - -/// It manage everything that has to do with marker packs. -/// 1. imports, loads, saves and exports marker packs. -/// 2. maintains the categories selection data for every pack -/// 3. contains activation data globally and per character -/// 4. When we load into a map, it filters the markers and runs the logic every frame -/// 1. If a marker needs to be activated (based on player position or whatever) -/// 2. marker needs to be drawn -/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture -/// 4. render that marker use joko_render -pub struct MarkerManager { - /// holds data that is useful for the ui - ui_data: MarkerManagerUI, - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - _marker_manager_dir: Arc, - /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. - /// The name of the child directory is the name of the pack - marker_packs_dir: Arc, - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - packs: BTreeMap, - missing_texture: Option, - missing_trail: Option, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, -} - -#[derive(Debug, Default)] -pub(crate) enum ImportStatus { - #[default] - UnInitialized, - WaitingForFileChooser, - LoadingPack(std::path::PathBuf), - PackDone(String, PackCore, bool), - PackError(miette::Report), -} -#[derive(Debug, Default)] -pub(crate) struct MarkerManagerUI { - // tf is this type supposed to be? maybe we should have used a ECS for this reason. - pub import_status: Option>>, -} - -#[derive(Debug, Default)] -pub struct PackList { - pub packs: BTreeMap, -} - -#[derive(Debug)] -pub struct PackEntry { - pub url: url::Url, - pub description: String, -} - -impl MarkerManager { - /// Creates a new instance of [MarkerManager]. - /// 1. It opens the marker manager directory - /// 2. loads its configuration - /// 3. opens the packs directory - /// 4. loads all the packs - /// 5. loads all the activation data - /// 6. returns self - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(MARKER_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker manager directory")?; - let marker_manager_dir = jdir - .open_dir(MARKER_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker manager directory")?; - marker_manager_dir - .create_dir_all(MARKER_PACKS_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker packs directory")?; - let marker_packs_dir = marker_manager_dir - .open_dir(MARKER_PACKS_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker packs dir")?; - let mut packs: BTreeMap = Default::default(); - - for entry in marker_packs_dir - .entries() - .into_diagnostic() - .wrap_err("failed to get entries of marker packs dir")? - { - let entry = entry.into_diagnostic()?; - if entry.metadata().into_diagnostic()?.is_file() { - continue; - } - if let Ok(name) = entry.file_name() { - let pack_dir = entry - .open_dir() - .into_diagnostic() - .wrap_err("failed to open pack entry as directory")?; - { - let span_guard = info_span!("loading pack from dir", name).entered(); - match LoadedPack::load_from_dir(pack_dir.into()) { - Ok(lp) => { - packs.insert(name, lp); - } - Err(e) => { - error!(?e, "failed to load pack from directory"); - } - } - drop(span_guard); - } - } - } - - Ok(Self { - packs, - marker_packs_dir: marker_packs_dir.into(), - _marker_manager_dir: marker_manager_dir.into(), - ui_data: Default::default(), - save_interval: 0.0, - missing_texture: None, - missing_trail: None - }) - } - - fn pack_importer(import_status: Arc>) { - //called when a new pack is imported - rayon::spawn(move || { - *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; - - if let Some(file_path) = rfd::FileDialog::new() - .add_filter("taco", &["zip", "taco"]) - .pick_file() - { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); - - let result = import_pack_from_zip_file_path(file_path); - match result { - Ok((name, pack)) => { - *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); - } - Err(e) => { - *import_status.lock().unwrap() = ImportStatus::PackError(e); - } - } - } else { - *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); - } - }); - } - pub fn tick( - &mut self, - etx: &egui::Context, - timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, - ) { - if self.missing_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.missing_texture = Some(etx.load_texture( - "default marker", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - }, - )); - } - if self.missing_trail.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/trail.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.missing_trail = Some(etx.load_texture( - "default trail", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - }, - )); - } - - for pack in self.packs.values_mut() { - pack.tick( - etx, - timestamp, - joko_renderer, - link, - self.missing_texture.as_ref().unwrap(), - self.missing_trail.as_ref().unwrap(), - ); - } - } - pub fn menu_ui(&mut self, ui: &mut egui::Ui) { - ui.menu_button("Markers", |ui| { - for pack in self.packs.values_mut() { - pack.category_sub_menu(ui); - } - }); - } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { - Window::new("Marker Manager").open(open).show(etx, |ui| -> Result<()> { - CollapsingHeader::new("Loaded Packs").show(ui, |ui| { - egui::Grid::new("packs").striped(true).show(ui, |ui| { - let mut delete = vec![]; - for pack in self.packs.keys() { - ui.label(pack); - if ui.button("delete").clicked() { - delete.push(pack.clone()); - } - } - for pack_name in delete { - self.packs.remove(&pack_name); - if let Err(e) = self.marker_packs_dir.remove_dir_all(&pack_name) { - error!(?e, pack_name,"failed to remove pack"); - } else { - info!("deleted marker pack: {pack_name}"); - } - } - }); - }); - - if self.ui_data.import_status.is_some() { - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - self.ui_data.import_status = None; - } - } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - let import_status = Arc::new(Mutex::default()); - self.ui_data.import_status = Some(import_status.clone()); - Self::pack_importer(import_status); - } - if let Some(import_status) = self.ui_data.import_status.as_ref() { - if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) => { - ui.label(format!("pack is being imported from {p:?}")); - } - ImportStatus::PackDone(name, pack, saved) => { - - if !*saved { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - let name = name.as_str(); - if ui.button("save").clicked() { - - if self.marker_packs_dir.exists(name) { - self.marker_packs_dir - .remove_dir_all(name) - .into_diagnostic()?; - } - if let Err(e) = self.marker_packs_dir.create_dir_all(name) { - error!(?e, "failed to create directory for pack"); - - } - match self.marker_packs_dir.open_dir(name) { - Ok(dir) => { - let core = std::mem::take(pack); - let mut loaded_pack = LoadedPack::new(core, dir.into()); - match loaded_pack.save_all() { - Ok(_) => { - self.packs.insert(name.to_string(), loaded_pack); - *saved = true; - }, - Err(e) => { - error!(?e, "failed to save marker pack"); - }, - } - }, - Err(e) => { - error!(?e, "failed to open marker pack directory to save pack"); - } - }; - } - } else { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); - } - } - ImportStatus::PackError(e) => { - ui.colored_label( - egui::Color32::RED, - format!("failed to import pack due to error: {e:#?}"), - ); - } - } - } - } - - Ok(()) - }); - } -} - -fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { - let mut taco_zip = vec![]; - std::fs::File::open(&file_path) - .into_diagnostic()? - .read_to_end(&mut taco_zip) - .into_diagnostic()?; - - info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { - ( - file_path - .file_name() - .map(|ostr| ostr.to_string_lossy().to_string()) - .unwrap_or_default(), - pack, - ) - }) -} diff --git a/crates/joko_marker_format/src/manager/pack/activation.rs b/crates/joko_marker_format/src/manager/pack/activation.rs new file mode 100644 index 0000000..a0a2a83 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/activation.rs @@ -0,0 +1,21 @@ +use indexmap::IndexMap; +use uuid::Uuid; + + +/// This is the activation data per pack +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct ActivationData { + /// this is for markers which are global and only activate once regardless of account + pub global: IndexMap, + /// this is the activation data per character + /// for markers which trigger once per character + pub character: IndexMap>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ActivationType { + /// clean these up when the map is changed + ReappearOnMapChange, + /// clean these up when the timestamp is reached + TimeStamp(time::OffsetDateTime), + Instance(std::net::IpAddr), +} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/active.rs b/crates/joko_marker_format/src/manager/pack/active.rs new file mode 100644 index 0000000..64ec9c8 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/active.rs @@ -0,0 +1,281 @@ +use ordered_hash_map::OrderedHashMap; + +use egui::TextureHandle; +use glam::{vec2, Vec2, Vec3}; +use indexmap::IndexMap; +use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; + +use crate::{ + pack::{CommonAttributes, RelativePath}, + INCHES_PER_METER, +}; +use jokolink::MumbleLink; + +/* +- activation data with uuids and track the latest timestamp that will be activated +- category activation data -> track and changes to propagate to markers of this map +- current active markers, which will keep track of their original marker, so as to propagate any changes easily +*/ +pub struct ActiveTrail { + pub trail_object: TrailObject, + pub texture_handle: TextureHandle, +} +/// This is an active marker. +/// It stores all the info that we need to scan every frame +pub(crate) struct ActiveMarker { + /// texture id from managed textures + pub texture_id: u64, + /// owned texture handle to keep it alive + pub _texture: TextureHandle, + /// position + pub pos: Vec3, + /// billboard must not be bigger than this size in pixels + pub max_pixel_size: f32, + /// billboard must not be smaller than this size in pixels + pub min_pixel_size: f32, + pub attrs: CommonAttributes, +} + +pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; + +impl ActiveMarker { + pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { + let Self { + texture_id, + pos, + attrs, + _texture, + max_pixel_size, + min_pixel_size, + .. + } = self; + // let width = *width; + // let height = *height; + let texture_id = *texture_id; + let pos = *pos; + // filters + if let Some(mounts) = attrs.get_mount() { + if let Some(current) = link.mount { + if !mounts.contains(current) { + return None; + } + } else { + return None; + } + } + let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); + let player_distance = pos.distance(link.player_pos); + let camera_distance = pos.distance(link.cam_pos); + let fade_near_far = Vec2::new(fade_near, fade_far); + + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let color = attrs.get_color().copied().unwrap_or_default(); + /* + 1. we need to filter the markers + 1. statically - mapid, character, map_type, race, profession + 2. dynamically - achievement, behavior, mount, fade_far, cull + 3. force hide/show by user discretion + 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior + 3. store the state for these markers activation data, and temporary data like bounce + */ + /* + skip if: + alpha is 0.0 + achievement id/bit is done (maybe this should be at map filter level?) + behavior (activation) + cull + distance > fade_far + visibility (ingame/map/minimap) + mount + specialization + */ + if fade_far > 0.0 && player_distance > fade_far { + return None; + } + // markers are 1 meter in width/height by default + let mut pos = pos; + pos.y += height_offset; + let direction_to_marker = link.cam_pos - pos; + let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); + + let far_offset = { + let dpi = if link.dpi_scaling <= 0 { + 96.0 + } else { + link.dpi as f32 + } / 96.0; + let gw2_width = link.client_size.as_vec2().x / dpi; + + // offset (half width i.e. distance from center of the marker to the side of the marker) + const SIDE_OFFSET_FAR: f32 = 1.0; + // the size of the projected on to the near plane + let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); + // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width + let near_offset_in_pixels = near_offset * gw2_width; + + // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width + let near_offset_in_pixels = near_offset_in_pixels + .clamp(*min_pixel_size, *max_pixel_size) + .min(gw2_width / 2.0); + + let near_offset_of_marker = near_offset_in_pixels / gw2_width; + near_offset_of_marker * camera_distance / z_near + }; + // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; + // we want to map 100 pixels to one meter in game + // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard + // But, i will ignore that as that makes markers too small + let x_offset = far_offset; + let y_offset = x_offset; // seems all markers are squares + let bottom_left = MarkerVertex { + position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), + texture_coordinates: vec2(0.0, 1.0), + alpha, + color, + fade_near_far, + }; + + let top_left = MarkerVertex { + position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), + texture_coordinates: vec2(0.0, 0.0), + alpha, + color, + fade_near_far, + }; + let top_right = MarkerVertex { + position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), + texture_coordinates: vec2(1.0, 0.0), + alpha, + color, + fade_near_far, + }; + let bottom_right = MarkerVertex { + position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), + texture_coordinates: vec2(1.0, 1.0), + alpha, + color, + fade_near_far, + }; + let vertices = [ + top_left, + bottom_left, + bottom_right, + bottom_right, + top_right, + top_left, + ]; + Some(MarkerObject { + vertices, + texture: texture_id, + distance: player_distance, + }) + } +} + +impl ActiveTrail { + pub fn get_vertices_and_texture( + attrs: &CommonAttributes, + positions: &[Vec3], + texture: TextureHandle, + ) -> Option { + // can't have a trail without atleast two nodes + if positions.len() < 2 { + return None; + } + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_near_far = Vec2::new(fade_near, fade_far); + let color = attrs.get_color().copied().unwrap_or([0u8; 4]); + // default taco width + let horizontal_offset = 20.0 / INCHES_PER_METER; + // scale it trail scale + let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); + let height = horizontal_offset * 2.0; + + let mut vertices = vec![]; + // trail mesh is split by separating different parts with a [0, 0, 0] + // we will call each separate trail mesh as a "strip" of trail. + // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. + for strip in positions.split(|&v| v == Vec3::ZERO) { + let mut y_offset = 1.0; + for two_positions in strip.windows(2) { + let first = two_positions[0]; + let second = two_positions[1]; + // right side of the vector from first to second + let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); + + let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; + let first_left = MarkerVertex { + position: first - (right_side * horizontal_offset), + texture_coordinates: vec2(0.0, y_offset), + alpha, + color, + fade_near_far, + }; + let first_right = MarkerVertex { + position: first + (right_side * horizontal_offset), + texture_coordinates: vec2(1.0, y_offset), + alpha, + color, + fade_near_far, + }; + let second_left = MarkerVertex { + position: second - (right_side * horizontal_offset), + texture_coordinates: vec2(0.0, new_offset), + alpha, + color, + fade_near_far, + }; + let second_right = MarkerVertex { + position: second + (right_side * horizontal_offset), + texture_coordinates: vec2(1.0, new_offset), + alpha, + color, + fade_near_far, + }; + y_offset = if new_offset.is_sign_positive() { + new_offset + } else { + 1.0 - new_offset.fract().abs() + }; + vertices.extend([ + second_left, + first_left, + first_right, + first_right, + second_right, + second_left, + ]); + } + } + + Some(ActiveTrail { + trail_object: TrailObject { + vertices: vertices.into(), + texture: match texture.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }, + }, + texture_handle: texture, + }) + } +} + +#[derive(Default)] +pub(crate) struct CurrentMapData { + /// the map to which the current map data belongs to + pub map_id: u32, + /// The textures that are being used by the markers, so must be kept alive by this hashmap + pub active_textures: OrderedHashMap, + /// The key is the index of the marker in the map markers + /// Their position in the map markers serves as their "id" as uuids can be duplicates. + pub active_markers: IndexMap, + /// The key is the position/index of this trail in the map trails. same as markers + pub active_trails: IndexMap, +} + diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs new file mode 100644 index 0000000..a26fff6 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -0,0 +1,102 @@ +use ordered_hash_map::{OrderedHashMap}; + +use indexmap::IndexMap; + +use crate::{ + pack::{Category, CommonAttributes, PackCore}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct CategorySelection { + pub selected: bool, + pub separator: bool, + pub display_name: String, + pub children: OrderedHashMap, +} + +impl CategorySelection { + pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { + let mut selection = OrderedHashMap::new(); + Self::recursive_create_category_selection(&mut selection, &pack.categories); + selection + } + pub fn recursive_get_full_names( + selection: &OrderedHashMap, + cats: &IndexMap, + list_of_enabled_categories: &mut OrderedHashMap, + parent_name: &str, + parent_common_attributes: &CommonAttributes, + ) { + for (name, cat) in cats { + if let Some(selected_cat) = selection.get(name) { + if !selected_cat.selected { + continue; + } + let full_name = if parent_name.is_empty() { + name.clone() + } else { + format!("{parent_name}.{name}") + }.to_lowercase(); + let mut common_attributes = cat.props.clone(); + common_attributes.inherit_if_attr_none(parent_common_attributes); + Self::recursive_get_full_names( + &selected_cat.children, + &cat.children, + list_of_enabled_categories, + &full_name, + &common_attributes, + ); + list_of_enabled_categories.insert(full_name, common_attributes); + } + } + } + fn recursive_create_category_selection( + selection: &mut OrderedHashMap, + cats: &IndexMap, + ) { + for (cat_name, cat) in cats.iter() { + if !selection.contains_key(cat_name) { + let mut to_insert = CategorySelection::default(); + to_insert.selected = cat.default_enabled; + to_insert.separator = cat.separator; + to_insert.display_name = cat.display_name.clone(); + selection.insert(cat_name.clone(), to_insert); + } + let s = selection.get_mut(cat_name).unwrap(); + Self::recursive_create_category_selection(&mut s.children, &cat.children); + } + } + + pub fn recursive_selection_ui( + selection: &mut OrderedHashMap, + ui: &mut egui::Ui, + changed: &mut bool, + ) { + if selection.is_empty() { + return; + } + egui::ScrollArea::vertical().show(ui, |ui| { + for (_name, cat) in selection.iter_mut() { + ui.horizontal(|ui| { + if cat.separator { + ui.add_space(3.0); + } else { + let cb = ui.checkbox(&mut cat.selected, ""); + if cb.changed() { + *changed = true; + } + } + if cat.children.is_empty() { + ui.label(&cat.display_name); + } else { + ui.menu_button(&cat.display_name, |ui: &mut egui::Ui| { + Self::recursive_selection_ui(&mut cat.children, ui, changed); + }); + } + }); + } + }); + } +} + diff --git a/crates/joko_marker_format/src/manager/pack/dirty.rs b/crates/joko_marker_format/src/manager/pack/dirty.rs new file mode 100644 index 0000000..fbb96c7 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/dirty.rs @@ -0,0 +1,29 @@ + +use ordered_hash_map::OrderedHashSet; + +use crate::pack::RelativePath; + +#[derive(Debug, Default, Clone)] +pub(crate) struct Dirty { + pub all: bool, + /// whether categories need to be saved + pub cats: bool, + /// whether cats selection needs to be saved + pub cats_selection: bool, + /// Whether any mapdata needs saving + pub map_dirty: OrderedHashSet, + /// whether any texture needs saving + pub texture: OrderedHashSet, + /// whether any tbin needs saving + pub tbin: OrderedHashSet, +} + +impl Dirty { + pub fn is_dirty(&self) -> bool { + self.cats + || self.cats_selection + || !self.map_dirty.is_empty() + || !self.texture.is_empty() + || !self.tbin.is_empty() + } +} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/entry.rs b/crates/joko_marker_format/src/manager/pack/entry.rs new file mode 100644 index 0000000..ad78681 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/entry.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub struct PackEntry { + pub url: url::Url, + pub description: String, +} + diff --git a/crates/joko_marker_format/src/manager/pack/import.rs b/crates/joko_marker_format/src/manager/pack/import.rs new file mode 100644 index 0000000..58baac4 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/import.rs @@ -0,0 +1,37 @@ +use std::{ + io::Read, +}; +use tracing::{info}; + +use miette::{IntoDiagnostic, Result}; +use crate::pack::PackCore; + + +#[derive(Debug, Default)] +pub enum ImportStatus { + #[default] + UnInitialized, + WaitingForFileChooser, + LoadingPack(std::path::PathBuf), + PackDone(String, PackCore, bool), + PackError(miette::Report), +} + +pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { + let mut taco_zip = vec![]; + std::fs::File::open(&file_path) + .into_diagnostic()? + .read_to_end(&mut taco_zip) + .into_diagnostic()?; + + info!("starting to get pack from taco"); + crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { + ( + file_path + .file_name() + .map(|ostr| ostr.to_string_lossy().to_string()) + .unwrap_or_default(), + pack, + ) + }) +} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/list.rs b/crates/joko_marker_format/src/manager/pack/list.rs new file mode 100644 index 0000000..499fe2f --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/list.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Default)] +pub struct PackList { + pub packs: BTreeMap, +} + + diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs new file mode 100644 index 0000000..47b2f19 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -0,0 +1,467 @@ +use std::{ + sync::Arc, +}; +use ordered_hash_map::{OrderedHashMap}; + +use cap_std::fs_utf8::Dir; +use egui::{ColorImage, TextureHandle}; +use image::{EncodableLayout}; +use joko_render::billboard::{TrailObject}; +use tracing::{debug, error, info}; + +use crate::{ + io::{load_pack_core_from_dir, save_pack_core_to_dir}, + pack::{PackCore}, +}; +use jokolink::MumbleLink; +use miette::{bail, Context, IntoDiagnostic, Result}; + +use super::dirty::Dirty; +use super::activation::{ActivationData, ActivationType}; +use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; +use crate::manager::pack::category_selection::CategorySelection; + +pub(crate) struct LoadedPack { + /// The directory inside which the pack data is stored + /// There should be a subdirectory called `core` which stores the pack core + /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. + /// eg: Active categories, activation data etc.. + pub dir: Arc, + /// The actual xml pack. + pub core: PackCore, + /// The selection of categories which are "enabled" and markers belonging to these may be rendered + cats_selection: OrderedHashMap, + dirty: Dirty, + activation_data: ActivationData, + current_map_data: CurrentMapData, +} + +impl LoadedPack { + const CORE_PACK_DIR_NAME: &str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &str = "cats.json"; + const ACTIVATION_DATA_FILE_NAME: &str = "activation.json"; + + pub fn new(core: PackCore, dir: Arc) -> Self { + let cats_selection = CategorySelection::default_from_pack_core(&core); + LoadedPack { + core, + cats_selection, + dirty: Dirty { + all: true, + ..Default::default() + }, + current_map_data: Default::default(), + dir, + activation_data: Default::default(), + } + } + pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { + //it is important to generate a new id each time to avoid collision + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + &mut self.cats_selection, + ui, + &mut self.dirty.cats_selection, + ); + }); + } + pub fn load_from_dir(pack_dir: Arc) -> Result { + if !pack_dir + .try_exists(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to check if pack core exists")? + { + bail!("pack core doesn't exist in this pack"); + } + let core_dir = pack_dir + .open_dir(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + + let cats_selection = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize category data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_else(|| { + let cs = CategorySelection::default_from_pack_core(&core); + match serde_json::to_string_pretty(&cs) { + Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + cs + }); + let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { + match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_default(); + Ok(LoadedPack { + dir: pack_dir, + core, + cats_selection, + dirty: Default::default(), + current_map_data: Default::default(), + activation_data, + }) + } + pub fn tick( + &mut self, + etx: &egui::Context, + _timestamp: f64, + joko_renderer: &mut joko_render::JokoRenderer, + link: &Option>, + default_tex_id: &TextureHandle, + default_trail_id: &TextureHandle, + ) { + let categories_changed = self.dirty.cats_selection; + if self.dirty.is_dirty() { + match self.save() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to save marker pack"); + } + } + } + let link = match link { + Some(link) => link, + None => return, + }; + + if self.current_map_data.map_id != link.map_id || categories_changed { + self.on_map_changed(etx, link, default_tex_id, default_trail_id); + } + let z_near = joko_renderer.get_z_near(); + for marker in self.current_map_data.active_markers.values() { + if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { + joko_renderer.add_billboard(mo); + } + } + for trail in self.current_map_data.active_trails.values() { + joko_renderer.add_trail(TrailObject { + vertices: trail.trail_object.vertices.clone(), + texture: trail.trail_object.texture, + }); + } + } + fn on_map_changed( + &mut self, + etx: &egui::Context, + link: &MumbleLink, + default_tex_id: &TextureHandle, + default_trail_id: &TextureHandle, + ) { + info!( + self.current_map_data.map_id, + link.map_id, "current map data is updated." + ); + self.current_map_data = Default::default(); + if link.map_id == 0 { + info!("No map do not do anything"); + return; + } + self.current_map_data.map_id = link.map_id; + let mut list_of_enabled_categories = Default::default(); + let mut list_of_enabled_files: OrderedHashMap = OrderedHashMap::new(); + //TODO: build list_of_enabled_files + CategorySelection::recursive_get_full_names( + &self.cats_selection, + &self.core.categories, + &mut list_of_enabled_categories, + "", + &Default::default(), + ); + + let mut failure_loading = false; + let mut nb_markers_attempt = 0; + let mut nb_markers_loaded = 0; + for (index, marker) in self + .core + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .markers + .iter() + .enumerate() + { + nb_markers_attempt += 1; + if let Some(source_file_name) = list_of_enabled_files.get(&marker.source_file_name) { + if let Some(category_attributes) = list_of_enabled_categories.get(&marker.category) { + let mut attrs = marker.attrs.clone(); + attrs.inherit_if_attr_none(category_attributes); + let key = &marker.guid; + if let Some(behavior) = attrs.get_behavior() { + use crate::pack::Behavior; + if match behavior { + Behavior::AlwaysVisible => false, + Behavior::ReappearOnMapChange + | Behavior::ReappearOnDailyReset + | Behavior::OnlyVisibleBeforeActivation + | Behavior::ReappearAfterTimer + | Behavior::ReappearOnMapReset + | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), + Behavior::OncePerInstance => self + .activation_data + .global + .get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default(), + Behavior::DailyPerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| a.contains_key(key)) + .unwrap_or_default(), + Behavior::OncePerInstancePerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| { + a.get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default() + }) + .unwrap_or_default(), + Behavior::WvWObjective => { + false // ??? + } + } { + continue; + } + } + if let Some(tex_path) = attrs.get_icon_file() { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.core.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + etx.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this icon texture"); + failure_loading = true; + } + } + } else { + info!("no texture attribute on this marker"); + } + let th = attrs + .get_icon_file() + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_tex_id); + let texture_id = match th.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }; + + let max_pixel_size = attrs.get_max_size().copied().unwrap_or(2048.0); // default taco max size + let min_pixel_size = attrs.get_min_size().copied().unwrap_or(5.0); // default taco min size + self.current_map_data.active_markers.insert( + index, + ActiveMarker { + texture_id, + _texture: th.clone(), + attrs, + pos: marker.position, + max_pixel_size, + min_pixel_size, + }, + ); + nb_markers_loaded += 1; + } + } + } + + let mut nb_trails_attempt = 0; + let mut nb_trails_loaded = 0; + for (index, trail) in self + .core + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .trails + .iter() + .enumerate() + { + nb_trails_attempt += 1; + if let Some(source_file_name) = list_of_enabled_files.get(&trail.source_file_name) { + if let Some(category_attributes) = list_of_enabled_categories.get(&trail.category) { + let mut common_attributes = trail.props.clone(); + common_attributes.inherit_if_attr_none(category_attributes); + if let Some(tex_path) = common_attributes.get_texture() { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.core.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + etx.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this trail texture"); + failure_loading = true; + } + } else { + debug!("Trail texture alreadu loaded {:?}", tex_path); + } + } else { + info!("no texture attribute on this trail"); + } + let texture_path = common_attributes.get_texture(); + let th = texture_path + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_trail_id); + + let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); + tbin + } else { + info!(?trail, "missing tbin path"); + continue; + }; + let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { + tbin + } else { + info!(%tbin_path, "failed to find tbin"); + continue; + }; + //TODO: if iso and closed, split it as a polygon and fill it as a surface + if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( + &common_attributes, + &tbin.nodes, + th.clone(), + ) { + self.current_map_data + .active_trails + .insert(index, active_trail); + } else { + info!("Cannot display {texture_path:?}") + } + nb_trails_loaded += 1; + } else { + info!("category {} is not enabled", trail.category); + } + } + } + info!("Loaded for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); + debug!("active categories: {:?}", list_of_enabled_categories.keys()); + + if failure_loading { + info!("Error when loading textures, here are the keys:"); + for k in self.core.textures.keys() { + info!(%k); + } + info!("end of keys"); + } + } + pub fn save_all(&mut self) -> Result<()> { + self.dirty.all = true; + self.save() + } + #[tracing::instrument(skip(self))] + pub fn save(&mut self) -> Result<()> { + if std::mem::take(&mut self.dirty.cats_selection) || self.dirty.all { + match serde_json::to_string_pretty(&self.cats_selection) { + Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + match serde_json::to_string_pretty(&self.activation_data) { + Ok(ad_json) => match self.dir.write(Self::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(_) => { + debug!("wrote activation to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write activation data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } + } + self.dir + .create_dir_all(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to create xmlpack directory")?; + let core_dir = self + .dir + .open_dir(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + save_pack_core_to_dir( + &self.core, + &core_dir, + std::mem::take(&mut self.dirty.cats), + std::mem::take(&mut self.dirty.map_dirty), + std::mem::take(&mut self.dirty.texture), + std::mem::take(&mut self.dirty.tbin), + std::mem::take(&mut self.dirty.all), + )?; + Ok(()) + } +} diff --git a/crates/joko_marker_format/src/manager/pack/mod.rs b/crates/joko_marker_format/src/manager/pack/mod.rs new file mode 100644 index 0000000..0922872 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/mod.rs @@ -0,0 +1,7 @@ +pub mod category_selection; +pub mod activation; +pub mod active; +pub mod loaded; +pub mod dirty; +pub mod import; + diff --git a/crates/joko_marker_format/src/pack/marker.rs b/crates/joko_marker_format/src/pack/marker.rs index 4dbc187..428aa2d 100644 --- a/crates/joko_marker_format/src/pack/marker.rs +++ b/crates/joko_marker_format/src/pack/marker.rs @@ -9,4 +9,5 @@ pub(crate) struct Marker { pub map_id: u32, pub category: String, pub attrs: CommonAttributes, + pub source_file_name: String, } diff --git a/crates/joko_marker_format/src/pack/route.rs b/crates/joko_marker_format/src/pack/route.rs index 758d4c5..6815f07 100644 --- a/crates/joko_marker_format/src/pack/route.rs +++ b/crates/joko_marker_format/src/pack/route.rs @@ -10,4 +10,5 @@ pub(crate) struct Route { pub map_id: u32, pub guid: Uuid, pub name: String, + pub source_file_name: String, } diff --git a/crates/joko_marker_format/src/pack/trail.rs b/crates/joko_marker_format/src/pack/trail.rs index 635c20c..02dc650 100644 --- a/crates/joko_marker_format/src/pack/trail.rs +++ b/crates/joko_marker_format/src/pack/trail.rs @@ -9,6 +9,7 @@ pub(crate) struct Trail { pub category: String, pub props: CommonAttributes, pub dynamic: bool, + pub source_file_name: String, } #[derive(Debug, Clone)] diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index fef6821..32d7b9d 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -6,11 +6,13 @@ mod init; mod wm; use init::get_jokolay_dir; use jmf::MarkerManager; +use jmf::FileManager; use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use joko_render::JokoRenderer; use jokolink::{MumbleChanges, MumbleManager}; use miette::{Context, Result}; use tracing::{error, info}; + #[allow(unused)] pub struct Jokolay { frame_stats: wm::WindowStatistics, @@ -19,10 +21,12 @@ pub struct Jokolay { mumble_manager: MumbleManager, marker_manager: MarkerManager, theme_manager: ThemeManager, + file_manager: FileManager, joko_renderer: JokoRenderer, egui_context: egui::Context, glfw_backend: GlfwBackend, } + impl Jokolay { pub fn new(jdir: Arc) -> Result { let mumble = @@ -31,6 +35,8 @@ impl Jokolay { MarkerManager::new(&jdir).wrap_err("failed to create marker manager")?; let mut theme_manager = ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; + let file_manager = + FileManager::new(&jdir).wrap_err("failed to create file manager")?; let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); let mut glfw_backend = GlfwBackend::new(GlfwConfig { @@ -62,6 +68,7 @@ impl Jokolay { jdir, egui_context, theme_manager, + file_manager, menu_panel: MenuPanel::default(), }) } @@ -77,6 +84,7 @@ impl Jokolay { mumble_manager, marker_manager, theme_manager, + file_manager, joko_renderer, egui_context, glfw_backend, @@ -135,6 +143,7 @@ impl Jokolay { }; joko_renderer.tick(link.clone()); marker_manager.tick(&etx, latest_time, joko_renderer, &link); + file_manager.tick(&etx, latest_time, joko_renderer, &link); menu_panel.tick(&etx, link.clone().as_ref().map(|m| m.as_ref())); // do the gui stuff now @@ -167,6 +176,10 @@ impl Jokolay { &mut menu_panel.show_theme_window, "Show Theme Manager", ); + ui.checkbox( + &mut menu_panel.show_file_manager_window, + "Show File Manager", + ); ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); if ui.button("exit").clicked() { info!("exiting jokolay"); @@ -181,6 +194,7 @@ impl Jokolay { mumble_manager.gui(&etx, &mut menu_panel.show_mumble_manager_winodw); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); + file_manager.gui(&etx, &mut menu_panel.show_file_manager_window); frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); // show notifications JokolayTracingLayer::show_notifications(&etx); @@ -327,6 +341,7 @@ pub struct MenuPanel { show_marker_manager_window: bool, show_mumble_manager_winodw: bool, show_window_manager: bool, + show_file_manager_window: bool, } impl MenuPanel { From a545ed09420e2ba5238cd22d33bbb551635831b3 Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 30 Mar 2024 19:03:07 +0100 Subject: [PATCH 11/54] first version to track displayed components and highlight them in the menu --- Cargo.lock | 3613 +++++++++++++++++ Cargo.toml | 5 +- crates/joko_core/src/manager/theme/mod.rs | 12 +- crates/joko_core/src/manager/trace/mod.rs | 4 +- .../joko_marker_format/src/io/deserialize.rs | 85 +- crates/joko_marker_format/src/io/serialize.rs | 236 +- crates/joko_marker_format/src/lib.rs | 1 - crates/joko_marker_format/src/manager/file.rs | 80 - .../joko_marker_format/src/manager/marker.rs | 131 +- crates/joko_marker_format/src/manager/mod.rs | 2 - .../src/manager/pack/active.rs | 5 +- .../src/manager/pack/category_selection.rs | 101 +- .../src/manager/pack/dirty.rs | 18 +- .../src/manager/pack/file_selection.rs | 46 + .../src/manager/pack/loaded.rs | 132 +- .../src/manager/pack/mod.rs | 1 + crates/joko_marker_format/src/pack/mod.rs | 27 +- crates/joko_render/src/billboard.rs | 2 +- crates/joko_render/src/lib.rs | 22 +- crates/jokolay/src/app/mod.rs | 68 +- crates/jokolink/src/lib.rs | 130 +- crates/jokolink/src/mumble/mod.rs | 8 +- 22 files changed, 4284 insertions(+), 445 deletions(-) create mode 100644 Cargo.lock delete mode 100644 crates/joko_marker_format/src/manager/file.rs create mode 100644 crates/joko_marker_format/src/manager/pack/file_selection.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2b07e84 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3613 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "accesskit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", + "zerocopy 0.7.32", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arcdps" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e8e3e68ba99ea4d9fc0af6c26f7277c6a30f9fbd7a1884efd8d016dcdfdc39" +dependencies = [ + "arcdps_codegen", + "chrono", + "once_cell", +] + +[[package]] +name = "arcdps_codegen" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b73c6f84c5845e9eba3a232593d20ef3db434281848f5072a367edbcc1f3fee" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.3.0", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.3.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 5.2.0", + "futures-lite", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.3.0", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata 0.1.10", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "cap-directories" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99bbc76e44242cd767689c33f5350c3646758edecdf1f8b7f4df5a8ea029" +dependencies = [ + "cap-std 2.0.1", + "directories-next", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-directories" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb706d254d585ce9ff8b716d169d948700ca7b3a4715272fd2bd1cb9a65210f1" +dependencies = [ + "cap-std 3.0.0", + "directories-next", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-primitives" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe16767ed8eee6d3f1f00d6a7576b81c226ab917eb54b96e5f77a5216ef67abb" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-primitives" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90a0b44fc796b1a84535a63753d50ba3972c4db55c7255c186f79140e63d56d0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-std" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330" +dependencies = [ + "camino", + "cap-primitives 2.0.1", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cap-std" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266626ce180cf9709f317d0bf9754e3a5006359d87f4bf792f06c9c5f1b63c0f" +dependencies = [ + "cap-primitives 3.0.0", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx", + "num-traits", +] + +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cxx" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dc7287237dd438b926a81a1a5605dad33d286870e5eee2db17bf2bcd9e92a" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47c6c8ad7c1a10d3ef0fe3ff6733f4db0d78f08ef0b13121543163ef327058b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.55", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "701a1ac7a697e249cdd8dc026d7a7dafbfd0dbcd8bd24ec55889f2bc13dd6287" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b404f596046b0bb2d903a9c786b875a126261b52b7c3a64bbb66382c41c771df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "ecolor" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03cfe80b1890e1a8cdbffc6044d6872e814aaf6011835a2a5e2db0e5c5c4ef4e" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "egui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180f595432a5b615fc6b74afef3955249b86cfea72607b40740a4cd60d5297d0" +dependencies = [ + "accesskit", + "ahash", + "epaint", + "nohash-hasher", + "serde", +] + +[[package]] +name = "egui_extras" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4a6962241a76da5be5e64e41b851ee1c95fda11f76635522a3c82b119b5475" +dependencies = [ + "egui", + "enum-map", + "log", + "mime_guess2", + "serde", +] + +[[package]] +name = "egui_render_glow" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21691a0388394a02b9352fb31edc7a008645ef1af13bc6eace5da06c2f599e60" +dependencies = [ + "bytemuck", + "egui", + "getrandom", + "glow", + "js-sys", + "raw-window-handle 0.6.0", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_render_three_d" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39bc7f5aab85ad422c53b2a1753a94a08bdca4b701346edc226ba015a0b2a7a8" +dependencies = [ + "egui", + "egui_render_glow", + "raw-window-handle 0.6.0", + "three-d", +] + +[[package]] +name = "egui_window_glfw_passthrough" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8cd7260410f069d82b31b188f66900336e054f839bbe24112dc2bb29acfc4" +dependencies = [ + "egui", + "glfw-passthrough", + "tracing", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "emath" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6916301ecf80448f786cdf3eb51d9dbdd831538732229d49119e2d4312eaaf09" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "enumflags2" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "enumn" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "epaint" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b9fdf617dd7f58b0c8e6e9e4a1281f730cde0831d40547da446b2bb76a47af" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "nohash-hasher", + "parking_lot", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "glfw-passthrough" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e0ee79341d32b6c490876d36f5e815bb10be943452cd3fff67d509d3143fb5" +dependencies = [ + "bitflags 1.3.2", + "glfw-sys-passthrough", + "objc", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", + "winapi", +] + +[[package]] +name = "glfw-sys-passthrough" +version = "4.0.3+3.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b2db4d361b9ebe743c3a542ddef5d605269bd1f93e1090440fff075e666ddf" +dependencies = [ + "cmake", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy 0.6.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "indextree" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-extras" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c301e73fb90e8a29e600a9f402d095765f74310d582916a952f618836a1bd1ed" +dependencies = [ + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "joko_core" +version = "0.2.1" +dependencies = [ + "cap-directories 3.0.0", + "cap-std 2.0.1", + "egui", + "egui_extras", + "glam", + "indexmap", + "miette", + "ordered_hash_map", + "rayon", + "rfd", + "ringbuffer", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + +[[package]] +name = "joko_ext" +version = "0.1.0" + +[[package]] +name = "joko_marker_format" +version = "0.2.1" +dependencies = [ + "base64", + "cap-std 2.0.1", + "cxx", + "cxx-build", + "data-encoding", + "egui", + "enumflags2", + "glam", + "image", + "indexmap", + "itertools", + "joko_render", + "jokoapi", + "jokolink", + "miette", + "ordered_hash_map", + "paste", + "phf", + "rayon", + "rfd", + "rstest", + "serde", + "serde_json", + "similar-asserts", + "smol_str", + "time", + "tracing", + "tribool", + "url", + "uuid", + "xot", + "zip", +] + +[[package]] +name = "joko_render" +version = "0.2.1" +dependencies = [ + "bytemuck", + "egui", + "egui_render_three_d", + "egui_window_glfw_passthrough", + "glam", + "jokolink", + "raw-window-handle 0.5.2", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "jokoapi" +version = "0.2.1" +dependencies = [ + "const_format", + "enumflags2", + "miette", + "serde", + "ureq", +] + +[[package]] +name = "jokolay" +version = "0.2.1" +dependencies = [ + "cap-directories 2.0.1", + "cap-std 2.0.1", + "egui", + "egui_extras", + "egui_window_glfw_passthrough", + "glam", + "indexmap", + "joko_core", + "joko_marker_format", + "joko_render", + "jokolink", + "miette", + "rayon", + "rfd", + "ringbuffer", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-subscriber", + "url", +] + +[[package]] +name = "jokolink" +version = "0.2.1" +dependencies = [ + "arcdps", + "egui", + "enumflags2", + "glam", + "jokoapi", + "miette", + "notify", + "num-derive", + "num-traits", + "serde", + "serde_json", + "time", + "tracing", + "tracing-appender", + "tracing-subscriber", + "widestring", + "windows", + "x11rb", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "next-gen" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1962f0b64c859f27f9551c74afbdbec7090fa83518daf6c5eb5b31d153455beb" +dependencies = [ + "next-gen-proc_macros", + "unwind_safe", +] + +[[package]] +name = "next-gen-proc_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59395d2ffdd03894479cdd1ce4b7e0700d379d517f2d396cee2a4828707c5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ordered_hash_map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" +dependencies = [ + "hashbrown 0.13.2", + "serde", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle 0.6.0", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuffer" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.55", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "itoa", + "libc", + "linux-raw-sys", + "once_cell", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +dependencies = [ + "console", + "similar", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "smol_str" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "supports-color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "three-d" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aecff785797175a2e56dca49da9836948eee41fab48b7b01dfcb64cae256ecb" +dependencies = [ + "cgmath", + "glow", + "instant", + "thiserror", + "three-d-asset", +] + +[[package]] +name = "three-d-asset" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9959d4427b63958661828008f7470d6a8d2c0945b3df0dc7377d6aca38fb694" +dependencies = [ + "cgmath", + "half", + "thiserror", + "web-sys", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tribool" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8660361502033a51e119386b47fbb811e5706722f2e91ccf867aa6b2b09f90" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unwind_safe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" + +[[package]] +name = "ureq" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "serde_json", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "rand", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" +dependencies = [ + "bitflags 2.5.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname", + "nix 0.26.4", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix 0.26.4", +] + +[[package]] +name = "xdg-home" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "xhtmlchardet" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc471704e8954f426350a7300e92a4da6932b762068ae8e6aa5dcacf141e133" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xot" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55dc1c3603e452c78983b59f466cd8251695db1729b230f473d004d70b3d94d8" +dependencies = [ + "ahash", + "encoding_rs", + "indextree", + "next-gen", + "xhtmlchardet", + "xmlparser", +] + +[[package]] +name = "zbus" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock 3.3.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "derivative", + "enumflags2", + "event-listener 5.2.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.28.0", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive 0.7.32", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zvariant" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index 93f15c6..84ae0fd 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,9 @@ resolver = "2" [workspace.dependencies] tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } ringbuffer = { version = "0.14" } -egui = { version = "*" } -egui_extras = { version = "*" } +egui = { version = "0.26" } +egui_extras = { version = "0.26" } +cap-directories = { version = "2" } cap-std = { version = "2", features = ["fs_utf8"] } serde = { version = "*", features = ["derive"] } miette = { version = "*", features = ["fancy"] } diff --git a/crates/joko_core/src/manager/theme/mod.rs b/crates/joko_core/src/manager/theme/mod.rs index 1759e2a..2121cac 100644 --- a/crates/joko_core/src/manager/theme/mod.rs +++ b/crates/joko_core/src/manager/theme/mod.rs @@ -46,12 +46,12 @@ impl Default for ThemeManagerConfig { } impl ThemeManager { - const THEME_MANAGER_DIR_NAME: &str = "theme_manager"; - const THEMES_DIR_NAME: &str = "themes"; - const FONTS_DIR_NAME: &str = "fonts"; - const DEFAULT_FONT_NAME: &str = "default"; - const DEFAULT_THEME_NAME: &str = "default"; - const THEME_MANAGER_CONFIG_NAME: &str = "theme_manager_config"; + const THEME_MANAGER_DIR_NAME: &'static str = "theme_manager"; + const THEMES_DIR_NAME: &'static str = "themes"; + const FONTS_DIR_NAME: &'static str = "fonts"; + const DEFAULT_FONT_NAME: &'static str = "default"; + const DEFAULT_THEME_NAME: &'static str = "default"; + const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; pub fn new(jdir: &Dir) -> Result { jdir.create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() diff --git a/crates/joko_core/src/manager/trace/mod.rs b/crates/joko_core/src/manager/trace/mod.rs index f92ad36..099db80 100644 --- a/crates/joko_core/src/manager/trace/mod.rs +++ b/crates/joko_core/src/manager/trace/mod.rs @@ -80,8 +80,8 @@ impl JokolayTracingLayer { }) .body(|body| { let events = &JKL_TRACING_DATA.get().unwrap().lock().unwrap().buffer; - body.rows(20.0, events.len(), |index, mut row| { - let ev = events.get(index as _).unwrap(); + body.rows(20.0, events.len(), |mut row| { + let ev = events.get(row.index() as _).unwrap(); ev.ui_row(&mut row); }); }); diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 38b2c36..dcf502c 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -4,12 +4,10 @@ use crate::{ }; use base64::Engine; use cap_std::fs_utf8::Dir; -use egui::lerp; use glam::Vec3; use indexmap::IndexMap; use miette::{bail, Context, IntoDiagnostic, Result}; -use serde_json::map; -use std::{collections::{BTreeMap, VecDeque}, io::Read}; +use std::{collections::{VecDeque}, io::Read}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, info, info_span, instrument, trace, warn}; use uuid::Uuid; @@ -29,6 +27,15 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { ) .wrap_err("failed to walk dir when loading a markerpack")?; + //categories are required to register other objects + let cats_xml = dir + .read_to_string("categories.xml") + .into_diagnostic() + .wrap_err("failed to read categories.xml")?; + let categories_file = String::from("categories.xml"); + parse_categories_file(&categories_file, &cats_xml, &mut pack) + .wrap_err("failed to parse category file")?; + // parse map data of the pack for entry in dir .entries() @@ -49,15 +56,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { if let Some(name_as_str) = name.strip_suffix(".xml") { match name_as_str { "categories" => { - // parse categories - { - let cats_xml = dir - .read_to_string("categories.xml") - .into_diagnostic() - .wrap_err("failed to read categories.xml")?; - parse_categories_file(&name, &cats_xml, &mut pack) - .wrap_err("failed to parse category file")?; - } + //already done } map_id => { // parse map file @@ -301,6 +300,7 @@ fn recursive_marker_category_parser( &mut cats .entry(name.to_string()) .or_insert_with(|| Category { + guid: parse_guid(names, ele), display_name: display_name.to_string(), separator, default_enabled, @@ -327,14 +327,16 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack .wrap_err("no doc element")?; if let Some(od) = tree.element(overlay_data_node) { + let mut categories: IndexMap = Default::default(); if od.name() == xot_names.overlay_data { recursive_marker_category_parser_categories_xml( &file_name, &tree, tree.children(overlay_data_node), - &mut pack.categories, + &mut categories, &xot_names, ); + pack.categories = categories; } else { bail!("root tag is not OverlayData") } @@ -396,6 +398,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; let source_file_name = child.get_attribute(names._source_file_name).unwrap_or_default().to_string(); + pack.source_files.insert(source_file_name.clone(), true); //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail if child.name() == names.route { debug!("Found a route in core pack {:?}", child); @@ -429,6 +432,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child, &names); + pack.register_uuid(&category, &guid); let marker = Marker { position: [xpos, ypos, zpos].into(), map_id, @@ -441,7 +445,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result if !pack.maps.contains_key(&map_id) { pack.maps.insert(map_id, MapData::default()); } - pack.maps.get_mut(&map_id).unwrap().markers.push(marker); + pack.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); } else if child.name() == names.trail { debug!("Found a trail in core pack {:?}", child); if child @@ -455,6 +459,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child, &names); + pack.register_uuid(&category, &guid); let trail = Trail { category, map_id, @@ -467,7 +472,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result if !pack.maps.contains_key(&map_id) { pack.maps.insert(map_id, MapData::default()); } - pack.maps.get_mut(&map_id).unwrap().trails.push(trail); + pack.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); } span_guard.exit(); } @@ -521,6 +526,7 @@ fn recursive_marker_category_parser_categories_xml( true } }; + let guid = parse_guid(names, ele); recursive_marker_category_parser_categories_xml( file_name, tree, @@ -528,6 +534,7 @@ fn recursive_marker_category_parser_categories_xml( &mut cats .entry(name.to_string()) .or_insert_with(|| Category { + guid, display_name: display_name.to_string(), separator, default_enabled, @@ -553,6 +560,7 @@ fn recursive_marker_category_parser_categories_xml( /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { + //FIXME: there is a problem where pack map files are not dump into the folders anymore //called to import a new pack // all the contents of ZPack let mut pack = PackCore::default(); @@ -678,18 +686,18 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { Some(ele) => ele, None => continue, }; - let category = child + let category_name = child .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); debug!("import element: {:?}", child); if child.name() == names.poi { - import_poi(&mut pack, &names, &child, category, source_file_name.clone()); + import_poi(&mut pack, &names, &child, category_name, source_file_name.clone()); } else if child.name() == names.trail { - import_trail(&mut pack, &names, &child, category, source_file_name.clone()); + import_trail(&mut pack, &names, &child, category_name, source_file_name.clone()); } else if child.name() == names.route { - import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category, source_file_name.clone()); + import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category_name, source_file_name.clone()); } else { info!("unknown element: {:?}", child); } @@ -697,7 +705,6 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { drop(span_guard); } - Ok(pack) } @@ -718,7 +725,7 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ .unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String, source_file_name: String) -> Option { +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, source_file_name: String) -> Option { if let Some(map_id) = poi_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -750,7 +757,7 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & Some(Marker { position: [xpos, ypos, zpos].into(), map_id, - category, + category: category_name.clone(), attrs: common_attributes, guid: parse_guid(names, poi_element), source_file_name @@ -781,12 +788,12 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { } fn parse_route( - pack: &mut PackCore, + _pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, - category: String, + category_name: &String, source_file_name: String ) -> Option { @@ -814,7 +821,7 @@ fn parse_route( info!("route element is missing name: {route_element:?}"); return None; } - let mut category: String = category; + let mut category: String = category_name.clone(); let mut map_id: Option = route_element.get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()); for child_node in tree.children(*route_node) { @@ -863,7 +870,7 @@ fn parse_route( } -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String, source_file_name: String) -> Option { +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, source_file_name: String) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html if let Some(map_id) = trail_element .get_attribute(names.trail_data) @@ -882,7 +889,7 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } Some(Trail { - category, + category: category_name.clone(), map_id, props: common_attributes, guid: parse_guid(names, trail_element), @@ -899,24 +906,26 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } -fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category: String, source_file_name: String) { - if let Some(marker) = parse_marker(pack, names, poi_element, category, source_file_name) { +fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: String, source_file_name: String) { + if let Some(marker) = parse_marker(pack, names, poi_element, &category_name, source_file_name) { + pack.register_uuid(&category_name, &marker.guid); if !pack.maps.contains_key(&marker.map_id) { pack.maps.insert(marker.map_id, MapData::default()); } - pack.maps.get_mut(&marker.map_id).unwrap().markers.push(marker); + pack.maps.get_mut(&marker.map_id).unwrap().markers.insert(marker.guid, marker); } else { debug!("Could not parse POI"); } } -fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category: String, source_file_name: String) { - if let Some(trail) = parse_trail(pack, names, trail_element, category, source_file_name) { +fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: String, source_file_name: String) { + if let Some(trail) = parse_trail(pack, names, trail_element, &category_name, source_file_name) { + pack.register_uuid(&category_name, &trail.guid); if !pack.maps.contains_key(&trail.map_id) { pack.maps.insert(trail.map_id, MapData::default()); } - pack.maps.get_mut(&trail.map_id).unwrap().trails.push(trail); + pack.maps.get_mut(&trail.map_id).unwrap().trails.insert(trail.guid, trail); } else { debug!("Could not parse Trail"); } @@ -934,7 +943,6 @@ fn route_to_tbin(route: &Route) -> TBin { fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { let mut props = CommonAttributes::default(); - let default_texture: RelativePath = "default_trail_texture.png".parse().unwrap(); props.set_texture(None); props.set_trail_data(Some(file_path.clone())); debug!("Build dynamic trail {}", route.guid); @@ -948,18 +956,19 @@ fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { } } -fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category: String, source_file_name: String) { - if let Some(route) = parse_route(pack, names, tree, route_node, route_element, category, source_file_name) { +fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category_name: String, source_file_name: String) { + if let Some(route) = parse_route(pack, names, tree, route_node, route_element, &category_name, source_file_name) { let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); let file_path: RelativePath = file_name.parse().unwrap(); let trail = route_to_trail(&route, &file_path); let tbin = route_to_tbin(&route); + pack.register_uuid(&category_name, &route.guid); pack.tbins.insert(file_path, tbin);//there may be duplicates since we load and save each time if !pack.maps.contains_key(&trail.map_id) { pack.maps.insert(trail.map_id, MapData::default()); } - pack.maps.get_mut(&trail.map_id).unwrap().trails.push(trail); - pack.maps.get_mut(&route.map_id).unwrap().routes.push(route); + pack.maps.get_mut(&trail.map_id).unwrap().trails.insert(trail.guid, trail); + pack.maps.get_mut(&route.map_id).unwrap().routes.insert(route.guid, route); } else { info!("Could not parse route {:?}", route_element); } diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index 28040ae..ef6ec4d 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -1,5 +1,5 @@ use crate::{ - pack::{Category, Marker, PackCore, RelativePath, Trail, Route}, + pack::{Category, Marker, PackCore, Trail, Route}, BASE64_ENGINE, }; use base64::Engine; @@ -7,7 +7,6 @@ use cap_std::fs_utf8::Dir; use indexmap::IndexMap; use miette::{Context, IntoDiagnostic, Result}; use std::{io::Write}; -use ordered_hash_map::{OrderedHashSet}; use tracing::info; use xot::{Element, Node, SerializeOptions, Xot}; @@ -16,166 +15,133 @@ use super::XotAttributeNameIDs; pub(crate) fn save_pack_core_to_dir( pack_core: &PackCore, dir: &Dir, - cats: bool, - mut maps: OrderedHashSet, - mut textures: OrderedHashSet, - mut tbins: OrderedHashSet, - all: bool, + is_dirty: bool, ) -> Result<()> { - if cats || all { - // save categories + if !is_dirty { + return Ok(()); + } + // save categories + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create new root with overlay data node")?; + recursive_cat_serializer(&mut tree, &names, &pack_core.categories, od) + .wrap_err("failed to serialize cats")?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to convert cats xot to string")?; + dir.create("categories.xml") + .into_diagnostic() + .wrap_err("failed to create categories.xml")? + .write_all(cats.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write to categories.xml")?; + // save maps + for (map_id, map_data) in pack_core.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); let od = tree.new_element(names.overlay_data); - let root_node = tree + let root_node: Node = tree .new_root(od) .into_diagnostic() - .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_core.categories, od) - .wrap_err("failed to serialize cats")?; - let cats = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to convert cats xot to string")?; - dir.create("categories.xml") + .wrap_err("failed to create root wiht overlay data for pois")?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) .into_diagnostic() - .wrap_err("failed to create categories.xml")? - .write_all(cats.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write to categories.xml")?; - } - // save maps - for (map_id, map_data) in pack_core.maps.iter() { - if maps.remove(map_id) || all { - if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node: Node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create root wiht overlay data for pois")?; - let pois = tree.new_element(names.pois); - tree.append(od, pois) + .wrap_err("faild to append pois to od node")?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) .into_diagnostic() - .wrap_err("faild to append pois to od node")?; - for marker in &map_data.markers { - let poi = tree.new_element(names.poi); - tree.append(pois, poi) - .into_diagnostic() - .wrap_err("failed to append poi (marker) to pois")?; - let ele = tree.element_mut(poi).unwrap(); - serialize_marker_to_element(marker, ele, &names); - } - for route_path in &map_data.routes { - serialize_route_to_element(&mut tree, route_path, &pois, &names)?; - } - for trail in &map_data.trails { - if trail.dynamic { - continue; - } - let trail_node = tree.new_element(names.trail); - tree.append(pois, trail_node) - .into_diagnostic() - .wrap_err("failed to append a trail node to pois")?; - let ele = tree.element_mut(trail_node).unwrap(); - serialize_trail_to_element(trail, ele, &names); + .wrap_err("failed to append poi (marker) to pois")?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; } - let map_xml = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to serialize map data to string")?; - dir.create(format!("{map_id}.xml")) + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) .into_diagnostic() - .wrap_err("failed to create map xml file")? - .write_all(map_xml.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write map data to file")?; - } - } - // if any other map remained in the maps, then it means the map was deleted from pack, so we remove the xml file too - for map_id in maps { - if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); + .wrap_err("failed to append a trail node to pois")?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to serialize map data to string")?; + dir.create(format!("{map_id}.xml")) + .into_diagnostic() + .wrap_err("failed to create map xml file")? + .write_all(map_xml.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write map data to file")?; } // save images for (img_path, img) in pack_core.textures.iter() { - if textures.remove(img_path) || all { - if let Some(parent) = img_path.parent() { - dir.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir for an image: {img_path}") - })?; - } - dir.create(img_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(img) + if let Some(parent) = img_path.parent() { + dir.create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { - miette::miette!("failed to write image bytes to file: {img_path}") + miette::miette!("failed to create parent dir for an image: {img_path}") })?; } - } - for img_path in textures { - if let Err(e) = dir.remove_file(img_path.as_str()) { - info!( - ?e, - %img_path, "failed to remove file" - ); - } + dir.create(img_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .write(img) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to write image bytes to file: {img_path}") + })?; } // save tbins for (tbin_path, tbin) in pack_core.tbins.iter() { - if tbins.remove(tbin_path) || all { - if let Some(parent) = tbin_path.parent() { - dir.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir of tbin: {tbin_path}") - })?; - } - let mut bytes: Vec = vec![]; - bytes.reserve(8 + tbin.nodes.len() * 12); - bytes.extend_from_slice(&tbin.version.to_ne_bytes()); - bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); - for node in &tbin.nodes { - bytes.extend_from_slice(&node[0].to_ne_bytes()); - bytes.extend_from_slice(&node[1].to_ne_bytes()); - bytes.extend_from_slice(&node[2].to_ne_bytes()); - } - dir.create(tbin_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? - .write_all(&bytes) + if let Some(parent) = tbin_path.parent() { + dir.create_dir_all(parent) .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + .wrap_err_with(|| { + miette::miette!("failed to create parent dir of tbin: {tbin_path}") + })?; } - } - for tbin_path in tbins { - if let Err(e) = dir.remove_file(tbin_path.as_str()) { - info!( - ?e, - %tbin_path, "failed to remove file" - ); + let mut bytes: Vec = vec![]; + bytes.reserve(8 + tbin.nodes.len() * 12); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); } + dir.create(tbin_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .write_all(&bytes) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; } Ok(()) } + fn recursive_cat_serializer( tree: &mut Xot, names: &XotAttributeNameIDs, diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs index f225b44..14b1d3b 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_marker_format/src/lib.rs @@ -7,7 +7,6 @@ pub(crate) mod manager; pub(crate) mod pack; pub use manager::MarkerManager; -pub use manager::FileManager; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); diff --git a/crates/joko_marker_format/src/manager/file.rs b/crates/joko_marker_format/src/manager/file.rs deleted file mode 100644 index 8303221..0000000 --- a/crates/joko_marker_format/src/manager/file.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{ - collections::BTreeMap, - sync::{Arc, Mutex}, -}; - -use cap_std::fs_utf8::Dir; -use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; -use image::EncodableLayout; - -use tracing::{error, info, info_span}; - -use jokolink::MumbleLink; -use miette::{Context, IntoDiagnostic, Result}; - -use crate::manager::pack::loaded::LoadedPack; - -pub const FILE_MANAGER_DIRECTORY_NAME: &str = "file_manager"; - -pub struct FileManager { - /// holds data that is useful for the ui - ui_data: FileManagerUI, - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - packs: BTreeMap, - missing_texture: Option, - missing_trail: Option, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, -} - -#[derive(Debug, Default)] -pub(crate) struct FileManagerUI { - // tf is this type supposed to be? maybe we should have used a ECS for this reason. - -} - - -impl FileManager { - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(FILE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create file manager directory")?; - let mut packs: BTreeMap = Default::default(); - - Ok(Self { - packs, - ui_data: Default::default(), - save_interval: 0.0, - missing_texture: None, - missing_trail: None - }) - } - - pub fn tick( - &mut self, - etx: &egui::Context, - timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, - ) { - } - pub fn menu_ui(&mut self, ui: &mut egui::Ui) { - ui.menu_button("Files", |ui| { - for pack in self.packs.values_mut() { - pack.category_sub_menu(ui); - } - }); - } - - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { - Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { - //TODO: display list of currently loaded files - Ok(()) - }); - } -} - diff --git a/crates/joko_marker_format/src/manager/marker.rs b/crates/joko_marker_format/src/manager/marker.rs index d67be4c..f791cf5 100644 --- a/crates/joko_marker_format/src/manager/marker.rs +++ b/crates/joko_marker_format/src/manager/marker.rs @@ -1,8 +1,8 @@ use std::{ - collections::BTreeMap, - sync::{Arc, Mutex}, + collections::BTreeMap, sync::{Arc, Mutex}, collections::HashSet }; +use tribool::Tribool; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; use image::EncodableLayout; @@ -11,6 +11,7 @@ use tracing::{error, info, info_span}; use jokolink::MumbleLink; use miette::{Context, IntoDiagnostic, Result}; +use uuid::Uuid; use crate::manager::pack::loaded::LoadedPack; use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; @@ -28,6 +29,7 @@ pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; /// 2. marker needs to be drawn /// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture /// 4. render that marker use joko_render +/// FIXME: it is a bad name, it does not manage Markers, but packages pub struct MarkerManager { /// holds data that is useful for the ui ui_data: MarkerManagerUI, @@ -45,6 +47,12 @@ pub struct MarkerManager { /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. /// This allows us to avoid saving the pack too often. pub save_interval: f64, + + all_files_tribool: Tribool, + all_files_toggle: bool, + currently_used_files: BTreeMap, + on_screen: HashSet, + is_dirty: bool } #[derive(Debug, Default)] @@ -116,7 +124,12 @@ impl MarkerManager { ui_data: Default::default(), save_interval: 0.0, missing_texture: None, - missing_trail: None + missing_trail: None, + all_files_tribool: Tribool::True, + all_files_toggle: false, + currently_used_files: Default::default(), + on_screen: Default::default(), + is_dirty: true, }) } @@ -151,7 +164,7 @@ impl MarkerManager { etx: &egui::Context, timestamp: f64, joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, + link: Option<&MumbleLink>, ) { if self.missing_texture.is_none() { let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); @@ -162,6 +175,7 @@ impl MarkerManager { egui::TextureOptions { magnification: egui::TextureFilter::Linear, minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, }, )); } @@ -174,29 +188,102 @@ impl MarkerManager { egui::TextureOptions { magnification: egui::TextureFilter::Linear, minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, }, )); } - for pack in self.packs.values_mut() { - pack.tick( - etx, - timestamp, - joko_renderer, - link, - self.missing_texture.as_ref().unwrap(), - self.missing_trail.as_ref().unwrap(), - ); - } + let mut currently_used_files: BTreeMap = Default::default(); + let mut next_on_screen: HashSet = Default::default(); + match link { + Some(link) => { + //FIXME: how to save/load the active files ? + let mut is_dirty = self.is_dirty; + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.core.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.core.source_files.get(&marker.source_file_name) { + currently_used_files.insert( + marker.source_file_name.clone(), + *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {is_dirty = true; is_active}) + ); + } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.core.source_files.get(&trail.source_file_name) { + currently_used_files.insert( + trail.source_file_name.clone(), + *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {is_dirty = true; is_active}) + ); + } + } + } + } + for pack in self.packs.values_mut() { + pack.tick( + etx, + timestamp, + link, + self.missing_texture.as_ref().unwrap(), + self.missing_trail.as_ref().unwrap(), + ¤tly_used_files, + is_dirty + ); + pack.render( + timestamp, + joko_renderer, + link, + &mut next_on_screen, + ); + } + std::mem::take(&mut self.is_dirty); + }, + None => {}, + }; + self.currently_used_files = currently_used_files; + self.on_screen = next_on_screen;//those are the elements displayed, not the categories, one would need to keep the link between the two } pub fn menu_ui(&mut self, ui: &mut egui::Ui) { ui.menu_button("Markers", |ui| { for pack in self.packs.values_mut() { - pack.category_sub_menu(ui); + pack.category_sub_menu(ui, &self.on_screen); } }); + } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { + fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool, link: Option<&MumbleLink>) { + Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Grid::new("link grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + if self.all_files_tribool.is_indeterminate(){ + ui.add(egui::Checkbox::new(&mut self.all_files_toggle, "File").indeterminate(true)); + } else { + ui.checkbox(&mut self.all_files_toggle, "File"); + } + ui.label("Trails"); + ui.label("Markers"); + ui.end_row(); + + for file in self.currently_used_files.iter_mut() { + let cb = ui.checkbox(file.1, file.0.clone()); + if cb.changed() { + self.is_dirty = true; + } + if ui.button("Edit").clicked() { + println!("click {}", file.0.clone()); + } + ui.end_row(); + } + ui.end_row(); + }) + }); + Ok(()) + }); + } + fn gui_marker_manager(&mut self, etx: &egui::Context, open: &mut bool) { Window::new("Marker Manager").open(open).show(etx, |ui| -> Result<()> { CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { @@ -297,5 +384,17 @@ impl MarkerManager { Ok(()) }); } + pub fn gui( + &mut self, + etx: &egui::Context, + is_marker_open: &mut bool, + is_file_open: &mut bool, + timestamp: f64, + joko_renderer: &mut joko_render::JokoRenderer, + link: Option<&MumbleLink> + ) { + self.gui_marker_manager(etx, is_marker_open); + self.gui_file_manager(etx, is_file_open, link); +} } diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index 9154585..3cd2514 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -18,8 +18,6 @@ We will make not having a valid category/texture/tbin path as allowed. So, users mod marker; mod pack; -mod file; pub use marker::MarkerManager; -pub use file::FileManager; diff --git a/crates/joko_marker_format/src/manager/pack/active.rs b/crates/joko_marker_format/src/manager/pack/active.rs index 64ec9c8..8fd27be 100644 --- a/crates/joko_marker_format/src/manager/pack/active.rs +++ b/crates/joko_marker_format/src/manager/pack/active.rs @@ -4,6 +4,7 @@ use egui::TextureHandle; use glam::{vec2, Vec2, Vec3}; use indexmap::IndexMap; use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; +use uuid::Uuid; use crate::{ pack::{CommonAttributes, RelativePath}, @@ -274,8 +275,8 @@ pub(crate) struct CurrentMapData { pub active_textures: OrderedHashMap, /// The key is the index of the marker in the map markers /// Their position in the map markers serves as their "id" as uuids can be duplicates. - pub active_markers: IndexMap, + pub active_markers: IndexMap, /// The key is the position/index of this trail in the map trails. same as markers - pub active_trails: IndexMap, + pub active_trails: IndexMap, } diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs index a26fff6..8cc5614 100644 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -1,6 +1,8 @@ +use std::collections::{HashSet, HashMap}; use ordered_hash_map::{OrderedHashMap}; use indexmap::IndexMap; +use uuid::Uuid; use crate::{ pack::{Category, CommonAttributes, PackCore}, @@ -9,26 +11,67 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { + #[serde(skip)] + pub guid: HashSet,//should be a HashSet of all the children markers/trails uuid and self (but not sub categories) pub selected: bool, pub separator: bool, pub display_name: String, pub children: OrderedHashMap, } +pub struct SelectedCategoryManager { + data: OrderedHashMap, + +} +impl<'a> SelectedCategoryManager { + pub fn new( + selected_categories: &OrderedHashMap, + core_categories: &IndexMap + ) -> Self { + let mut list_of_enabled_categories = Default::default(); + CategorySelection::recursive_get_full_names( + &selected_categories, + &core_categories, + &mut list_of_enabled_categories, + "", + &Default::default(), + ); + + Self { data: list_of_enabled_categories } + } + pub fn cloned_data(&self) -> OrderedHashMap { + self.data.clone() + } + pub fn is_selected(&self, category: &String) -> bool { + self.data.contains_key(category) + } + pub fn get(&self, key: &String) -> &CommonAttributes { + self.data.get(key).unwrap() + } + pub fn len(&self) -> usize { + self.data.len() + } + pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, String, CommonAttributes> { + self.data.keys() + } +} + +static mut once: bool = true; + impl CategorySelection { pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { let mut selection = OrderedHashMap::new(); Self::recursive_create_category_selection(&mut selection, &pack.categories); selection } - pub fn recursive_get_full_names( + fn recursive_get_full_names( selection: &OrderedHashMap, - cats: &IndexMap, + core_categories: &IndexMap, list_of_enabled_categories: &mut OrderedHashMap, parent_name: &str, parent_common_attributes: &CommonAttributes, ) { - for (name, cat) in cats { + for (name, cat) in core_categories { if let Some(selected_cat) = selection.get(name) { if !selected_cat.selected { continue; @@ -51,16 +94,42 @@ impl CategorySelection { } } } + pub fn recursive_populate_guids( + selection: &mut OrderedHashMap, + all_pack_guids: &HashMap>, + parent_name: Option, + ) { + for (cat_name, cat) in selection.iter_mut() { + let current_name = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, cat_name) + } else { + cat_name.clone() + }; + if let Some(other_existing_uuid) = all_pack_guids.get(¤t_name) { + cat.guid.extend(other_existing_uuid); + } + Self::recursive_populate_guids(&mut cat.children, all_pack_guids, Some(current_name)); + for child in cat.children.values() { + cat.guid.extend(&child.guid); + } + //assert!(cat.guid.len() > 0); + } + } fn recursive_create_category_selection( selection: &mut OrderedHashMap, cats: &IndexMap, ) { for (cat_name, cat) in cats.iter() { if !selection.contains_key(cat_name) { - let mut to_insert = CategorySelection::default(); - to_insert.selected = cat.default_enabled; - to_insert.separator = cat.separator; - to_insert.display_name = cat.display_name.clone(); + let mut all_uuids: HashSet = Default::default(); + all_uuids.insert(cat.guid); + let to_insert = CategorySelection { + guid: all_uuids, + selected: cat.default_enabled, + separator: cat.separator, + display_name: cat.display_name.clone(), + children: Default::default(), + }; selection.insert(cat_name.clone(), to_insert); } let s = selection.get_mut(cat_name).unwrap(); @@ -71,7 +140,8 @@ impl CategorySelection { pub fn recursive_selection_ui( selection: &mut OrderedHashMap, ui: &mut egui::Ui, - changed: &mut bool, + is_dirty: &mut bool, + on_screen: &HashSet, ) { if selection.is_empty() { return; @@ -84,14 +154,21 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.selected, ""); if cb.changed() { - *changed = true; + *is_dirty = true; } } + let mut is_current_branch_displayed = on_screen.intersection(&cat.guid).count() > 0; + let color = if is_current_branch_displayed { + egui::Color32::LIGHT_GREEN + } else { + egui::Color32::GRAY + }; + let label = egui::RichText::new(&cat.display_name).color(color); if cat.children.is_empty() { - ui.label(&cat.display_name); + ui.label(label); } else { - ui.menu_button(&cat.display_name, |ui: &mut egui::Ui| { - Self::recursive_selection_ui(&mut cat.children, ui, changed); + ui.menu_button(label, |ui: &mut egui::Ui| { + Self::recursive_selection_ui(&mut cat.children, ui, is_dirty, on_screen); }); } }); diff --git a/crates/joko_marker_format/src/manager/pack/dirty.rs b/crates/joko_marker_format/src/manager/pack/dirty.rs index fbb96c7..737186c 100644 --- a/crates/joko_marker_format/src/manager/pack/dirty.rs +++ b/crates/joko_marker_format/src/manager/pack/dirty.rs @@ -4,25 +4,25 @@ use ordered_hash_map::OrderedHashSet; use crate::pack::RelativePath; #[derive(Debug, Default, Clone)] -pub(crate) struct Dirty { +pub(crate) struct DirtyMarker { pub all: bool, /// whether categories need to be saved - pub cats: bool, - /// whether cats selection needs to be saved - pub cats_selection: bool, + pub categories: bool, + /// whether selected categories needs to be saved + pub selected_categories: bool, /// Whether any mapdata needs saving - pub map_dirty: OrderedHashSet, + pub map: OrderedHashSet, /// whether any texture needs saving pub texture: OrderedHashSet, /// whether any tbin needs saving pub tbin: OrderedHashSet, } -impl Dirty { +impl DirtyMarker { pub fn is_dirty(&self) -> bool { - self.cats - || self.cats_selection - || !self.map_dirty.is_empty() + self.categories + || self.selected_categories + || !self.map.is_empty() || !self.texture.is_empty() || !self.tbin.is_empty() } diff --git a/crates/joko_marker_format/src/manager/pack/file_selection.rs b/crates/joko_marker_format/src/manager/pack/file_selection.rs new file mode 100644 index 0000000..cc0e9e7 --- /dev/null +++ b/crates/joko_marker_format/src/manager/pack/file_selection.rs @@ -0,0 +1,46 @@ +use std::{ + collections::BTreeMap, +}; +use ordered_hash_map::{OrderedHashMap}; + +pub struct SelectedFileManager { + data: OrderedHashMap, + +} +impl<'a> SelectedFileManager { + pub fn new( + selected_files: &OrderedHashMap, + pack_source_files: &OrderedHashMap, + currently_used_files: &BTreeMap, + ) -> Self { + //TODO: build data + let mut list_of_enabled_files: OrderedHashMap = Default::default(); + SelectedFileManager::recursive_get_full_names( + &selected_files, + &pack_source_files, + ¤tly_used_files, + &mut list_of_enabled_files, + ); + Self { data: list_of_enabled_files } + } + fn recursive_get_full_names( + _selected_files: &OrderedHashMap, + _pack_source_files: &OrderedHashMap, + currently_used_files: &BTreeMap, + list_of_enabled_files: &mut OrderedHashMap + ){ + for (key, v) in currently_used_files.iter() { + list_of_enabled_files.insert(key.clone(), *v); + } + } + pub fn cloned_data(&self) -> OrderedHashMap { + self.data.clone() + } + pub fn is_selected(&self, source_file_name: &String) -> bool { + let default = false; + self.data.is_empty() || *self.data.get(source_file_name).unwrap_or(&default) + } + pub fn len(&self) -> usize { + self.data.len() + } +} diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 47b2f19..73830cb 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -1,5 +1,5 @@ use std::{ - sync::Arc, + collections::{BTreeMap, HashSet}, sync::Arc }; use ordered_hash_map::{OrderedHashMap}; @@ -8,15 +8,14 @@ use egui::{ColorImage, TextureHandle}; use image::{EncodableLayout}; use joko_render::billboard::{TrailObject}; use tracing::{debug, error, info}; +use uuid::Uuid; use crate::{ - io::{load_pack_core_from_dir, save_pack_core_to_dir}, - pack::{PackCore}, + io::{load_pack_core_from_dir, save_pack_core_to_dir}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, pack::{PackCore} }; use jokolink::MumbleLink; use miette::{bail, Context, IntoDiagnostic, Result}; -use super::dirty::Dirty; use super::activation::{ActivationData, ActivationType}; use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; use crate::manager::pack::category_selection::CategorySelection; @@ -30,38 +29,38 @@ pub(crate) struct LoadedPack { /// The actual xml pack. pub core: PackCore, /// The selection of categories which are "enabled" and markers belonging to these may be rendered - cats_selection: OrderedHashMap, - dirty: Dirty, + selected_categories: OrderedHashMap, + selected_files: OrderedHashMap, + is_dirty: bool, activation_data: ActivationData, current_map_data: CurrentMapData, } impl LoadedPack { - const CORE_PACK_DIR_NAME: &str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &str = "cats.json"; - const ACTIVATION_DATA_FILE_NAME: &str = "activation.json"; + const CORE_PACK_DIR_NAME: &'static str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; + const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; pub fn new(core: PackCore, dir: Arc) -> Self { - let cats_selection = CategorySelection::default_from_pack_core(&core); + let selected_categories = CategorySelection::default_from_pack_core(&core); LoadedPack { - core, - cats_selection, - dirty: Dirty { - all: true, - ..Default::default() - }, - current_map_data: Default::default(), dir, + core, + selected_categories, + selected_files: Default::default(), + is_dirty: true, activation_data: Default::default(), + current_map_data: Default::default(), } } - pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { + pub fn category_sub_menu(&mut self, ui: &mut egui::Ui, on_screen: &HashSet) { //it is important to generate a new id each time to avoid collision ui.push_id(ui.next_auto_id(), |ui| { CategorySelection::recursive_selection_ui( - &mut self.cats_selection, + &mut self.selected_categories, ui, - &mut self.dirty.cats_selection, + &mut self.is_dirty, + on_screen, ); }); } @@ -79,7 +78,7 @@ impl LoadedPack { .wrap_err("failed to open core pack directory")?; let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - let cats_selection = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + let selected_categories = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { Ok(cd_json) => match serde_json::from_str(&cd_json) { Ok(cd) => Some(cd), @@ -136,23 +135,26 @@ impl LoadedPack { Ok(LoadedPack { dir: pack_dir, core, - cats_selection, - dirty: Default::default(), - current_map_data: Default::default(), + selected_categories, + selected_files: Default::default(), + is_dirty: true, activation_data, + current_map_data: Default::default(), }) } + pub fn tick( &mut self, etx: &egui::Context, _timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, + link: &MumbleLink, default_tex_id: &TextureHandle, default_trail_id: &TextureHandle, + currently_used_files: &BTreeMap, + is_dirty: bool, ) { - let categories_changed = self.dirty.cats_selection; - if self.dirty.is_dirty() { + let is_dirty = self.is_dirty || is_dirty; + if self.is_dirty { match self.save() { Ok(_) => {} Err(e) => { @@ -160,25 +162,32 @@ impl LoadedPack { } } } - let link = match link { - Some(link) => link, - None => return, - }; - - if self.current_map_data.map_id != link.map_id || categories_changed { - self.on_map_changed(etx, link, default_tex_id, default_trail_id); + //FIXME: takes a lot of time when "is_dirty" is true (i.e.: the map of things to display changes). Everythings get reloaded => how to do partial version ? + if self.current_map_data.map_id != link.map_id || is_dirty { + self.on_map_changed(etx, link, default_tex_id, default_trail_id, currently_used_files); } + } + pub fn render( + &mut self, + _timestamp: f64, + joko_renderer: &mut joko_render::JokoRenderer, + link: &MumbleLink, + next_on_screen: &mut HashSet, + ) { let z_near = joko_renderer.get_z_near(); - for marker in self.current_map_data.active_markers.values() { + for (uuid, marker) in self.current_map_data.active_markers.iter() { + //FIXME: what's the difference between a Marker and an ActiveMarker ? rename second one in something more fitting ? if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { joko_renderer.add_billboard(mo); + next_on_screen.insert(*uuid); } } - for trail in self.current_map_data.active_trails.values() { + for (uuid, trail) in self.current_map_data.active_trails.iter() { joko_renderer.add_trail(TrailObject { vertices: trail.trail_object.vertices.clone(), texture: trail.trail_object.texture, }); + next_on_screen.insert(*uuid); } } fn on_map_changed( @@ -187,6 +196,7 @@ impl LoadedPack { link: &MumbleLink, default_tex_id: &TextureHandle, default_trail_id: &TextureHandle, + currently_used_files: &BTreeMap, ) { info!( self.current_map_data.map_id, @@ -198,16 +208,10 @@ impl LoadedPack { return; } self.current_map_data.map_id = link.map_id; - let mut list_of_enabled_categories = Default::default(); - let mut list_of_enabled_files: OrderedHashMap = OrderedHashMap::new(); - //TODO: build list_of_enabled_files - CategorySelection::recursive_get_full_names( - &self.cats_selection, - &self.core.categories, - &mut list_of_enabled_categories, - "", - &Default::default(), - ); + CategorySelection::recursive_populate_guids(&mut self.selected_categories, &self.core.all_guids, None); + let selected_categories_manager = SelectedCategoryManager::new(&self.selected_categories, &self.core.categories); + + let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.core.source_files, ¤tly_used_files); let mut failure_loading = false; let mut nb_markers_attempt = 0; @@ -218,13 +222,14 @@ impl LoadedPack { .get(&link.map_id) .unwrap_or(&Default::default()) .markers - .iter() + .values() .enumerate() { nb_markers_attempt += 1; - if let Some(source_file_name) = list_of_enabled_files.get(&marker.source_file_name) { - if let Some(category_attributes) = list_of_enabled_categories.get(&marker.category) { - let mut attrs = marker.attrs.clone(); + if selected_files_manager.is_selected(&marker.source_file_name) { + if selected_categories_manager.is_selected(&marker.category) { + let category_attributes = selected_categories_manager.get(&marker.category); + let mut attrs = marker.attrs.clone();// why a clone ? attrs.inherit_if_attr_none(category_attributes); let key = &marker.guid; if let Some(behavior) = attrs.get_behavior() { @@ -307,7 +312,7 @@ impl LoadedPack { let max_pixel_size = attrs.get_max_size().copied().unwrap_or(2048.0); // default taco max size let min_pixel_size = attrs.get_min_size().copied().unwrap_or(5.0); // default taco min size self.current_map_data.active_markers.insert( - index, + marker.guid, ActiveMarker { texture_id, _texture: th.clone(), @@ -330,12 +335,13 @@ impl LoadedPack { .get(&link.map_id) .unwrap_or(&Default::default()) .trails - .iter() + .values() .enumerate() { nb_trails_attempt += 1; - if let Some(source_file_name) = list_of_enabled_files.get(&trail.source_file_name) { - if let Some(category_attributes) = list_of_enabled_categories.get(&trail.category) { + if selected_files_manager.is_selected(&trail.source_file_name) { + if selected_categories_manager.is_selected(&trail.category) { + let category_attributes = selected_categories_manager.get(&trail.category); let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { @@ -389,7 +395,7 @@ impl LoadedPack { ) { self.current_map_data .active_trails - .insert(index, active_trail); + .insert(trail.guid, active_trail); } else { info!("Cannot display {texture_path:?}") } @@ -400,7 +406,7 @@ impl LoadedPack { } } info!("Loaded for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); - debug!("active categories: {:?}", list_of_enabled_categories.keys()); + debug!("active categories: {:?}", selected_categories_manager.keys()); if failure_loading { info!("Error when loading textures, here are the keys:"); @@ -411,13 +417,13 @@ impl LoadedPack { } } pub fn save_all(&mut self) -> Result<()> { - self.dirty.all = true; + self.is_dirty = true; self.save() } #[tracing::instrument(skip(self))] pub fn save(&mut self) -> Result<()> { - if std::mem::take(&mut self.dirty.cats_selection) || self.dirty.all { - match serde_json::to_string_pretty(&self.cats_selection) { + if std::mem::take(&mut self.is_dirty) { + match serde_json::to_string_pretty(&self.selected_categories) { Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { debug!("wrote cat selections to disk after creating a default from pack"); @@ -456,11 +462,7 @@ impl LoadedPack { save_pack_core_to_dir( &self.core, &core_dir, - std::mem::take(&mut self.dirty.cats), - std::mem::take(&mut self.dirty.map_dirty), - std::mem::take(&mut self.dirty.texture), - std::mem::take(&mut self.dirty.tbin), - std::mem::take(&mut self.dirty.all), + std::mem::take(&mut self.is_dirty), )?; Ok(()) } diff --git a/crates/joko_marker_format/src/manager/pack/mod.rs b/crates/joko_marker_format/src/manager/pack/mod.rs index 0922872..2833ae2 100644 --- a/crates/joko_marker_format/src/manager/pack/mod.rs +++ b/crates/joko_marker_format/src/manager/pack/mod.rs @@ -1,4 +1,5 @@ pub mod category_selection; +pub mod file_selection; pub mod activation; pub mod active; pub mod loaded; diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index 691adcb..d11a26b 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -3,16 +3,19 @@ mod marker; mod trail; mod route; -use std::{str::FromStr}; +use std::{collections::{HashMap, HashSet}, str::FromStr}; use indexmap::IndexMap; use ordered_hash_map; +use tracing::info; + pub use common::*; pub(crate) use marker::*; use smol_str::SmolStr; pub(crate) use trail::*; pub(crate) use route::*; +use uuid::Uuid; #[derive(Default, Debug, Clone)] @@ -20,18 +23,34 @@ pub(crate) struct PackCore { pub textures: ordered_hash_map::OrderedHashMap>, pub tbins: ordered_hash_map::OrderedHashMap, pub categories: IndexMap, + pub all_guids: HashMap>, + pub source_files: ordered_hash_map::OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: ordered_hash_map::OrderedHashMap, } +impl PackCore { + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) { + if !self.all_guids.contains_key(full_category_name) { + self.all_guids.insert(full_category_name.clone(), HashSet::default()); + } + if let Some(all_guid) = self.all_guids.get_mut(full_category_name) { + all_guid.insert(*uuid); + } else { + panic!("Can't register {} {}", full_category_name, uuid); + } + } +} + #[derive(Default, Debug, Clone)] pub(crate) struct MapData { - pub markers: Vec, - pub routes: Vec, - pub trails: Vec, + pub markers: IndexMap, + pub routes: IndexMap, + pub trails: IndexMap, } #[derive(Debug, Clone)] pub(crate) struct Category { + pub guid: Uuid, pub display_name: String, pub separator: bool, pub default_enabled: bool, diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 16880d7..58b1489 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -75,7 +75,7 @@ impl BillBoardRenderer { self.markers.clear(); self.trails.clear(); } - pub fn prepare_render_data(&mut self, _link: &jokolink::MumbleLink, gl: &Context) { + pub fn prepare_render_data(&mut self, gl: &Context) { unsafe { gl_error!(gl); } diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index a83ecda..5f83bd8 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -15,8 +15,6 @@ use egui_render_three_d::ThreeDConfig; use egui_window_glfw_passthrough::GlfwBackend; use glam::Mat4; use jokolink::MumbleLink; -use raw_window_handle::HasRawWindowHandle; -use std::sync::Arc; use three_d::prelude::*; #[macro_export] @@ -34,7 +32,7 @@ pub struct JokoRenderer { pub cam_pos: glam::Vec3, pub camera: Camera, pub viewport: Viewport, - pub link: Option>, + pub has_link: bool, pub billboard_renderer: BillBoardRenderer, pub gl: egui_render_three_d::ThreeDBackend, } @@ -47,7 +45,7 @@ impl JokoRenderer { glow_config: Default::default(), }, |s| glfw.get_proc_address_raw(s), - glfw_backend.window.raw_window_handle(), + //glfw_backend.window.raw_window_handle(), glfw_backend.framebuffer_size_physical, ); let viewport = Viewport { @@ -70,9 +68,9 @@ impl JokoRenderer { Vector3::unit_y(), Deg(90.0), 1.0, - 5000.0,//FIXME: trails may have points very far apart, when loading, one should fix those by putting intermediary points. + 5000.0, ), - link: Default::default(), + has_link: false, gl: backend, billboard_renderer, cam_pos: Default::default(), @@ -84,8 +82,8 @@ impl JokoRenderer { pub fn get_z_far(&self) -> f32 { 1000.0 } - pub fn tick(&mut self, link: Option>) { - if let Some(link) = link.as_ref() { + pub fn tick(&mut self, link: Option<&MumbleLink>) { + if let Some(link) = link { let center = link.cam_pos + link.f_camera_front; let camera = Camera::new_perspective( self.viewport, @@ -106,8 +104,10 @@ impl JokoRenderer { ); self.view_proj = proj * view; self.cam_pos = link.cam_pos; + self.has_link = true; + } else { + self.has_link = false; } - self.link = link; } pub fn add_billboard(&mut self, marker_object: MarkerObject) { self.billboard_renderer.markers.push(marker_object); @@ -140,9 +140,9 @@ impl JokoRenderer { textures_delta: egui::TexturesDelta, logical_screen_size: [f32; 2], ) { - if let Some(link) = self.link.as_ref() { + if self.has_link { self.billboard_renderer - .prepare_render_data(link, &self.gl.context); + .prepare_render_data(&self.gl.context); self.billboard_renderer.render( &self.gl.context, self.cam_pos, diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 32d7b9d..6acbe8c 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -6,7 +6,7 @@ mod init; mod wm; use init::get_jokolay_dir; use jmf::MarkerManager; -use jmf::FileManager; +//use jmf::FileManager; use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use joko_render::JokoRenderer; use jokolink::{MumbleChanges, MumbleManager}; @@ -21,7 +21,6 @@ pub struct Jokolay { mumble_manager: MumbleManager, marker_manager: MarkerManager, theme_manager: ThemeManager, - file_manager: FileManager, joko_renderer: JokoRenderer, egui_context: egui::Context, glfw_backend: GlfwBackend, @@ -35,8 +34,6 @@ impl Jokolay { MarkerManager::new(&jdir).wrap_err("failed to create marker manager")?; let mut theme_manager = ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; - let file_manager = - FileManager::new(&jdir).wrap_err("failed to create file manager")?; let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); let mut glfw_backend = GlfwBackend::new(GlfwConfig { @@ -68,7 +65,6 @@ impl Jokolay { jdir, egui_context, theme_manager, - file_manager, menu_panel: MenuPanel::default(), }) } @@ -84,7 +80,6 @@ impl Jokolay { mumble_manager, marker_manager, theme_manager, - file_manager, joko_renderer, egui_context, glfw_backend, @@ -141,10 +136,25 @@ impl Jokolay { None } }; - joko_renderer.tick(link.clone()); - marker_manager.tick(&etx, latest_time, joko_renderer, &link); - file_manager.tick(&etx, latest_time, joko_renderer, &link); - menu_panel.tick(&etx, link.clone().as_ref().map(|m| m.as_ref())); + // check if we need to change window position or size. + if let Some(link) = link { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + glfw_backend + .window + .set_pos(link.client_pos.x, link.client_pos.y); + // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. + // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) + // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + glfw_backend + .window + .set_size(link.client_size.x - 1, link.client_size.y - 1); + } + } + joko_renderer.tick(link); + marker_manager.tick(&etx, latest_time, joko_renderer, link); + menu_panel.tick(&etx, link); // do the gui stuff now egui::Area::new("menu panel") @@ -169,7 +179,7 @@ impl Jokolay { "Show Marker Manager", ); ui.checkbox( - &mut menu_panel.show_mumble_manager_winodw, + &mut menu_panel.show_mumble_manager_window, "Show Mumble Manager", ); ui.checkbox( @@ -190,37 +200,21 @@ impl Jokolay { marker_manager.menu_ui(ui); }); }); - marker_manager.gui(&etx, &mut menu_panel.show_marker_manager_window); - mumble_manager.gui(&etx, &mut menu_panel.show_mumble_manager_winodw); + marker_manager.gui( + &etx, + &mut menu_panel.show_marker_manager_window, + &mut menu_panel.show_file_manager_window, + latest_time, joko_renderer, + link + ); + mumble_manager.gui(&etx, &mut menu_panel.show_mumble_manager_window); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); - file_manager.gui(&etx, &mut menu_panel.show_file_manager_window); frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); // show notifications JokolayTracingLayer::show_notifications(&etx); // end gui stuff - // check if we need to change window position or size. - if let Some(link) = link.as_ref() { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - info!( - ?link.client_pos, ?link.client_size, - "resizing/repositioning to match gw2 window dimensions" - ); - - glfw_backend - .window - .set_pos(link.client_pos.x, link.client_pos.y); - // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. - // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) - // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - glfw_backend - .window - .set_size(link.client_size.x - 1, link.client_size.y - 1); - } - } etx.request_repaint(); let egui::FullOutput { @@ -241,7 +235,7 @@ impl Jokolay { .window .set_mouse_passthrough(!(etx.wants_keyboard_input() || etx.wants_pointer_input())); joko_renderer.render_egui( - etx.tessellate(shapes), + etx.tessellate(shapes, etx.pixels_per_point()), textures_delta, glfw_backend.window_size_logical, ); @@ -339,7 +333,7 @@ pub struct MenuPanel { show_theme_window: bool, // show_settings_window: bool, show_marker_manager_window: bool, - show_mumble_manager_winodw: bool, + show_mumble_manager_window: bool, show_window_manager: bool, show_file_manager_window: bool, } diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index 3d64eec..0c044d7 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -9,14 +9,13 @@ //! mod mumble; -use egui::DragValue; +use egui::{DragValue}; use enumflags2::BitFlags; use glam::IVec2; use jokoapi::end_point::mounts::Mount; use miette::{IntoDiagnostic, Result, WrapErr}; pub use mumble::*; use serde_json::from_str; -use std::sync::Arc; use tracing::error; /// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing @@ -43,33 +42,36 @@ pub struct MumbleManager { /// we use this to get the latest mumble link and latest window dimensions of the current mumble link backend: MumblePlatformImpl, /// latest mumble link - link: Arc, + link: MumbleLink, + } impl MumbleManager { pub fn new(name: &str, _jokolay_window_id: Option) -> Result { let backend = MumblePlatformImpl::new(name)?; Ok(Self { backend, - link: Arc::new(Default::default()), + link: Default::default(), }) } - pub fn tick(&mut self) -> Result>> { + pub fn is_alive(&self) -> bool { + self.backend.is_alive() + } + pub fn tick(&mut self) -> Result> { if let Err(e) = self.backend.tick() { error!(?e, "mumble backend tick error"); return Ok(None); } if !self.backend.is_alive() { - // reset link - if self.link.ui_tick != 0 { - self.link = Arc::new(Default::default()); - } - return Ok(None); + self.link.client_size.x = self.link.client_size.x.max(1024); + self.link.client_size.y = self.link.client_size.y.max(768); + self.link.changes = BitFlags::all(); + return Ok(Some(&self.link)); } // backend is alive and tick is successful. time to get link let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); if cml.ui_tick == 0 && self.link.ui_tick != 0 { - self.link = Arc::new(Default::default()); + self.link = Default::default(); } if cml.ui_tick == 0 || cml.context.client_pos_size == [0; 4] { @@ -136,7 +138,7 @@ impl MumbleManager { if self.link.client_size != client_size { changes.insert(MumbleChanges::WindowSize); } - let link = Arc::new(MumbleLink { + self.link = MumbleLink { ui_tick: cml.ui_tick, player_pos: cml.f_avatar_position.into(), f_avatar_front: cml.f_avatar_front.into(), @@ -171,22 +173,26 @@ impl MumbleManager { map_scale: cml.context.map_scale, process_id: cml.context.process_id, mount: Mount::try_from_mumble_link(cml.context.mount_index), - }); - self.link = link.clone(); + }; + //self.link = link.clone(); Ok(if self.link.ui_tick == 0 { None } else { - Some(link) + Some(&self.link) }) } pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { egui::Window::new("Mumble Manager") .open(open) .show(etx, |ui| { - if self.link.ui_tick == 0 { - ui.label("Mumble is not initialized"); + if !self.is_alive() { + ui.label( + egui::RichText::new("Mumble is not initialized, display dummy link instead.") + .color(egui::Color32::RED) + ); + editable_mumble_ui(ui, &mut self.link); } else { - let link: MumbleLink = self.link.as_ref().clone(); + let link: MumbleLink = self.link.clone(); mumble_ui(ui, link); } }); @@ -277,6 +283,94 @@ fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.label("dpi"); ui.add(DragValue::new(&mut link.dpi)); ui.end_row(); + }); +} + + +fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut dummy_link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.player_pos.x)); + ui.add(DragValue::new(&mut dummy_link.player_pos.y)); + ui.add(DragValue::new(&mut dummy_link.player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.x)); + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.y)); + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.cam_pos.x)); + ui.add(DragValue::new(&mut dummy_link.cam_pos.y)); + ui.add(DragValue::new(&mut dummy_link.cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.f_camera_front.x)); + ui.add(DragValue::new(&mut dummy_link.f_camera_front.y)); + ui.add(DragValue::new(&mut dummy_link.f_camera_front.z)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut dummy_link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = dummy_link.client_size.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.label(&dummy_link.name); + ui.end_row(); + ui.label("map id"); + ui.add(DragValue::new(&mut dummy_link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut dummy_link.map_type)); + ui.end_row(); + ui.label("address"); + ui.label(format!("{}", dummy_link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut dummy_link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut dummy_link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", dummy_link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.client_pos.x)); + ui.add(DragValue::new(&mut dummy_link.client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.client_size.x)); + ui.add(DragValue::new(&mut dummy_link.client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut dummy_link.dpi)); + ui.end_row(); // ui.label("position"); // ui.horizontal(|ui| { diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs index 9ded416..0acdbf8 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/jokolink/src/mumble/mod.rs @@ -88,7 +88,7 @@ impl Default for MumbleLink { f_avatar_front: Default::default(), cam_pos: Default::default(), f_camera_front: Default::default(), - name: Default::default(), + name: String::from("This Is Jokolay Dummy"), map_id: Default::default(), map_type: Default::default(), server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), @@ -106,12 +106,12 @@ impl Default for MumbleLink { map_scale: Default::default(), process_id: Default::default(), mount: Default::default(), - fov: Default::default(), + fov: 2.0, uisz: Default::default(), dpi: Default::default(), - dpi_scaling: Default::default(), + dpi_scaling: 96, client_pos: Default::default(), - client_size: Default::default(), + client_size: IVec2{x: 1024, y: 768}, changes: Default::default(), } } From 5b253da29da0a34a002b30dcf3a3adfccfe51f59 Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 30 Mar 2024 19:04:07 +0100 Subject: [PATCH 12/54] with updated packages --- crates/joko_marker_format/Cargo.toml | 3 ++- crates/joko_render/Cargo.toml | 2 +- crates/jokolay/Cargo.toml | 4 ++-- crates/jokolink/Cargo.toml | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/joko_marker_format/Cargo.toml b/crates/joko_marker_format/Cargo.toml index 0721c2c..7b6f936 100755 --- a/crates/joko_marker_format/Cargo.toml +++ b/crates/joko_marker_format/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] # jmf deps # for marker packs -xot = { version = "0" } +xot = { version = "0.16.0" } # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip indexmap = { workspace = true, features = ["serde"]} uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } @@ -40,6 +40,7 @@ ordered_hash_map = { workspace = true } joko_render = { path = "../joko_render" } jokolink = { path = "../jokolink" } jokoapi = { path = "../jokoapi" } +tribool = "0.3.0" [dev-dependencies] diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml index ddbe098..81e3d6f 100644 --- a/crates/joko_render/Cargo.toml +++ b/crates/joko_render/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] egui_render_three_d = { version = "*" } -egui_window_glfw_passthrough = { version = "0.5" } +egui_window_glfw_passthrough = { version = "0.8" } bytemuck = { version = "1", default-features = false } jokolink = { path = "../jokolink" } glam = { workspace = true, features = ["bytemuck"] } diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index a6e6cc4..952cd4d 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -18,10 +18,10 @@ joko_render = { path = "../joko_render" } jmf = { path = "../joko_marker_format", package = "joko_marker_format" } jokolink = { path = "../jokolink" } url = { workspace = true, features = ["serde"] } -egui_window_glfw_passthrough = { version = "0.5" } +egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users # and `Dir` from cap-dirs doesn't allow us to get the path. -cap-directories = { version = "*" } +cap-directories = { workspace = true } cap-std = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = [ diff --git a/crates/jokolink/Cargo.toml b/crates/jokolink/Cargo.toml index be3bbf7..a21ac1e 100644 --- a/crates/jokolink/Cargo.toml +++ b/crates/jokolink/Cargo.toml @@ -24,6 +24,7 @@ serde = { workspace = true } glam = { workspace = true } serde_json = { workspace = true } notify = { version = "*", default-features = false } + [target.'cfg(unix)'.dependencies] x11rb = { version = "0.12", default-features = false, features = [] } From 39a552cd585cfac74df9a708667930e4fda49b3c Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 31 Mar 2024 01:56:25 +0100 Subject: [PATCH 13/54] loading and saving the CategorySelection is the wrong abstraction, it needs to be build from reference which is the Category. Any future piece of code has to take this into account. If anything dynamic occurs in Category, no loading/saving can take place elsewhere --- .../joko_marker_format/src/io/deserialize.rs | 19 +++- crates/joko_marker_format/src/io/serialize.rs | 1 + crates/joko_marker_format/src/lib.rs | 2 +- crates/joko_marker_format/src/manager/mod.rs | 4 +- .../src/manager/pack/category_selection.rs | 44 ++++---- .../src/manager/pack/loaded.rs | 44 +------- .../src/manager/{marker.rs => package.rs} | 101 ++++++++++++++---- crates/joko_marker_format/src/pack/mod.rs | 42 ++++++-- crates/jokolay/src/app/mod.rs | 21 ++-- 9 files changed, 171 insertions(+), 107 deletions(-) rename crates/joko_marker_format/src/manager/{marker.rs => package.rs} (83%) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index dcf502c..10e4be2 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -262,6 +262,7 @@ fn recursive_marker_category_parser( tags: impl Iterator, cats: &mut IndexMap, names: &XotAttributeNameIDs, + parent_uuid: Option, ) { for tag in tags { let ele = match tree.element(tag) { @@ -294,13 +295,16 @@ fn recursive_marker_category_parser( .parse() .map(|u: u8| u != 0) .unwrap_or(true); + let guid = parse_guid(names, ele); + //println!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_uuid); recursive_marker_category_parser( tree, tree.children(tag), &mut cats .entry(name.to_string()) .or_insert_with(|| Category { - guid: parse_guid(names, ele), + guid, + parent: parent_uuid.clone(), display_name: display_name.to_string(), separator, default_enabled, @@ -309,6 +313,7 @@ fn recursive_marker_category_parser( }) .children, names, + Some(guid), ); } } @@ -333,10 +338,14 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack &file_name, &tree, tree.children(overlay_data_node), + pack, &mut categories, &xot_names, + None, ); + //println!("loaded categories: {:?}", categories); pack.categories = categories; + pack.register_categories(); } else { bail!("root tag is not OverlayData") } @@ -485,8 +494,10 @@ fn recursive_marker_category_parser_categories_xml( file_name: &String, tree: &Xot, tags: impl Iterator, + pack: &mut PackCore, cats: &mut IndexMap, names: &XotAttributeNameIDs, + parent_uuid: Option, ) { for tag in tags { if let Some(ele) = tree.element(tag) { @@ -527,14 +538,17 @@ fn recursive_marker_category_parser_categories_xml( } }; let guid = parse_guid(names, ele); + //println!("recursive_marker_category_parser_categories_xml {} {} {:?}", name, guid, parent_uuid); recursive_marker_category_parser_categories_xml( file_name, tree, tree.children(tag), + pack, &mut cats .entry(name.to_string()) .or_insert_with(|| Category { guid, + parent: parent_uuid.clone(), display_name: display_name.to_string(), separator, default_enabled, @@ -543,6 +557,7 @@ fn recursive_marker_category_parser_categories_xml( }) .children, names, + Some(guid), ); std::mem::drop(span_guard); } else { @@ -667,7 +682,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }; // parse_categories - recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names); + recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names, None); let pois = match tree.children(od).find(|node| { tree.element(*node) diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index ef6ec4d..329883c 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -154,6 +154,7 @@ fn recursive_cat_serializer( { let ele = tree.element_mut(cat_node).unwrap(); ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); // let cat_name = tree.add_name(cat_name); ele.set_attribute(names.name, cat_name); // no point in serializing default values diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs index 14b1d3b..d19f32d 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_marker_format/src/lib.rs @@ -6,7 +6,7 @@ pub(crate) mod io; pub(crate) mod manager; pub(crate) mod pack; -pub use manager::MarkerManager; +pub use manager::PackageManager; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index 3cd2514..bb85ded 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -16,8 +16,8 @@ We will make not having a valid category/texture/tbin path as allowed. So, users */ -mod marker; +mod package; mod pack; -pub use marker::MarkerManager; +pub use package::PackageManager; diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs index 8cc5614..2f0d932 100644 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -1,4 +1,4 @@ -use std::collections::{HashSet, HashMap}; +use std::collections::{HashSet, HashMap, BTreeSet}; use ordered_hash_map::{OrderedHashMap}; use indexmap::IndexMap; @@ -12,7 +12,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { #[serde(skip)] - pub guid: HashSet,//should be a HashSet of all the children markers/trails uuid and self (but not sub categories) + pub uuid: Uuid,//FIXME: there seems to be guid generated at several places leading to confusion in what is active or not (most likely in category, not saved versys categoryselection, saved) + #[serde(skip)] + pub parent: Option, pub selected: bool, pub separator: bool, pub display_name: String, @@ -56,8 +58,6 @@ impl<'a> SelectedCategoryManager { } } -static mut once: bool = true; - impl CategorySelection { pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { let mut selection = OrderedHashMap::new(); @@ -96,22 +96,18 @@ impl CategorySelection { } pub fn recursive_populate_guids( selection: &mut OrderedHashMap, - all_pack_guids: &HashMap>, - parent_name: Option, + entities_parents: &mut HashMap, + parent_uuid: Option, ) { for (cat_name, cat) in selection.iter_mut() { - let current_name = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, cat_name) - } else { - cat_name.clone() - }; - if let Some(other_existing_uuid) = all_pack_guids.get(¤t_name) { - cat.guid.extend(other_existing_uuid); + if cat.uuid.is_nil() { + cat.uuid = Uuid::new_v4(); + } + cat.parent = parent_uuid.clone(); + Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); + if parent_uuid.is_some() { + entities_parents.insert(cat.uuid, parent_uuid.unwrap().clone()); } - Self::recursive_populate_guids(&mut cat.children, all_pack_guids, Some(current_name)); - for child in cat.children.values() { - cat.guid.extend(&child.guid); - } //assert!(cat.guid.len() > 0); } } @@ -121,15 +117,15 @@ impl CategorySelection { ) { for (cat_name, cat) in cats.iter() { if !selection.contains_key(cat_name) { - let mut all_uuids: HashSet = Default::default(); - all_uuids.insert(cat.guid); let to_insert = CategorySelection { - guid: all_uuids, + uuid: cat.guid, + parent: cat.parent, selected: cat.default_enabled, separator: cat.separator, display_name: cat.display_name.clone(), children: Default::default(), }; + //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); selection.insert(cat_name.clone(), to_insert); } let s = selection.get_mut(cat_name).unwrap(); @@ -141,13 +137,13 @@ impl CategorySelection { selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, - on_screen: &HashSet, + on_screen: &BTreeSet ) { if selection.is_empty() { return; } egui::ScrollArea::vertical().show(ui, |ui| { - for (_name, cat) in selection.iter_mut() { + for (name, cat) in selection.iter_mut() { ui.horizontal(|ui| { if cat.separator { ui.add_space(3.0); @@ -157,8 +153,8 @@ impl CategorySelection { *is_dirty = true; } } - let mut is_current_branch_displayed = on_screen.intersection(&cat.guid).count() > 0; - let color = if is_current_branch_displayed { + //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); + let color = if on_screen.contains(&cat.uuid) { egui::Color32::LIGHT_GREEN } else { egui::Color32::GRAY diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 73830cb..1707ce1 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashSet}, sync::Arc + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::Arc }; use ordered_hash_map::{OrderedHashMap}; @@ -53,14 +53,14 @@ impl LoadedPack { current_map_data: Default::default(), } } - pub fn category_sub_menu(&mut self, ui: &mut egui::Ui, on_screen: &HashSet) { + pub fn category_sub_menu(&mut self, ui: &mut egui::Ui, on_screen: &BTreeSet) { //it is important to generate a new id each time to avoid collision ui.push_id(ui.next_auto_id(), |ui| { CategorySelection::recursive_selection_ui( &mut self.selected_categories, ui, &mut self.is_dirty, - on_screen, + on_screen ); }); } @@ -78,41 +78,7 @@ impl LoadedPack { .wrap_err("failed to open core pack directory")?; let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - let selected_categories = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { - match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize category data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&core); - match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - cs - }); + let selected_categories = CategorySelection::default_from_pack_core(&core); let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { Ok(contents) => match serde_json::from_str(&contents) { @@ -208,7 +174,7 @@ impl LoadedPack { return; } self.current_map_data.map_id = link.map_id; - CategorySelection::recursive_populate_guids(&mut self.selected_categories, &self.core.all_guids, None); + //CategorySelection::recursive_populate_guids(&mut self.selected_categories, &mut self.core.entities_parents, None); let selected_categories_manager = SelectedCategoryManager::new(&self.selected_categories, &self.core.categories); let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.core.source_files, ¤tly_used_files); diff --git a/crates/joko_marker_format/src/manager/marker.rs b/crates/joko_marker_format/src/manager/package.rs similarity index 83% rename from crates/joko_marker_format/src/manager/marker.rs rename to crates/joko_marker_format/src/manager/package.rs index f791cf5..bbc6dbd 100644 --- a/crates/joko_marker_format/src/manager/marker.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeMap, sync::{Arc, Mutex}, collections::HashSet + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex} }; use tribool::Tribool; @@ -16,8 +16,8 @@ use uuid::Uuid; use crate::manager::pack::loaded::LoadedPack; use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; -pub const MARKER_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; -pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; /// It manage everything that has to do with marker packs. @@ -30,9 +30,9 @@ pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; /// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture /// 4. render that marker use joko_render /// FIXME: it is a bad name, it does not manage Markers, but packages -pub struct MarkerManager { +pub struct PackageManager { /// holds data that is useful for the ui - ui_data: MarkerManagerUI, + ui_manager: PackageUIManager, /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. _marker_manager_dir: Arc, /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. @@ -51,18 +51,18 @@ pub struct MarkerManager { all_files_tribool: Tribool, all_files_toggle: bool, currently_used_files: BTreeMap, - on_screen: HashSet, + on_screen: BTreeSet, is_dirty: bool } #[derive(Debug, Default)] -pub(crate) struct MarkerManagerUI { +pub(crate) struct PackageUIManager { // tf is this type supposed to be? maybe we should have used a ECS for this reason. pub import_status: Option>>, + parents: HashMap, } - -impl MarkerManager { +impl PackageManager { /// Creates a new instance of [MarkerManager]. /// 1. It opens the marker manager directory /// 2. loads its configuration @@ -71,23 +71,24 @@ impl MarkerManager { /// 5. loads all the activation data /// 6. returns self pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(MARKER_MANAGER_DIRECTORY_NAME) + jdir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() .wrap_err("failed to create marker manager directory")?; let marker_manager_dir = jdir - .open_dir(MARKER_MANAGER_DIRECTORY_NAME) + .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() .wrap_err("failed to open marker manager directory")?; marker_manager_dir - .create_dir_all(MARKER_PACKS_DIRECTORY_NAME) + .create_dir_all(PACKAGES_DIRECTORY_NAME) .into_diagnostic() .wrap_err("failed to create marker packs directory")?; let marker_packs_dir = marker_manager_dir - .open_dir(MARKER_PACKS_DIRECTORY_NAME) + .open_dir(PACKAGES_DIRECTORY_NAME) .into_diagnostic() .wrap_err("failed to open marker packs dir")?; let mut packs: BTreeMap = Default::default(); + for entry in marker_packs_dir .entries() .into_diagnostic() @@ -121,7 +122,7 @@ impl MarkerManager { packs, marker_packs_dir: marker_packs_dir.into(), _marker_manager_dir: marker_manager_dir.into(), - ui_data: Default::default(), + ui_manager: PackageUIManager::new(), save_interval: 0.0, missing_texture: None, missing_trail: None, @@ -241,9 +242,19 @@ impl MarkerManager { None => {}, }; self.currently_used_files = currently_used_files; - self.on_screen = next_on_screen;//those are the elements displayed, not the categories, one would need to keep the link between the two + //those are the elements displayed, not the categories, one would need to keep the link between the two + self.on_screen = self.update_active_elements(next_on_screen); + } + fn update_active_elements(&mut self, on_screen: HashSet) -> BTreeSet { + let mut parents: HashMap = Default::default(); + for pack in self.packs.values() { + parents.extend(pack.core.entities_parents.clone()); + } + self.ui_manager.parents = parents; + self.ui_manager.get_parents(on_screen.iter()) } pub fn menu_ui(&mut self, ui: &mut egui::Ui) { + //println!("Elements on screen: {:?}", self.on_screen); ui.menu_button("Markers", |ui| { for pack in self.packs.values_mut() { pack.category_sub_menu(ui, &self.on_screen); @@ -283,8 +294,8 @@ impl MarkerManager { Ok(()) }); } - fn gui_marker_manager(&mut self, etx: &egui::Context, open: &mut bool) { - Window::new("Marker Manager").open(open).show(etx, |ui| -> Result<()> { + fn gui_package_loader(&mut self, etx: &egui::Context, open: &mut bool) { + Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { let mut delete = vec![]; @@ -305,17 +316,17 @@ impl MarkerManager { }); }); - if self.ui_data.import_status.is_some() { + if self.ui_manager.import_status.is_some() { if ui.button("clear").on_hover_text( "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - self.ui_data.import_status = None; + self.ui_manager.import_status = None; } } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { let import_status = Arc::new(Mutex::default()); - self.ui_data.import_status = Some(import_status.clone()); + self.ui_manager.import_status = Some(import_status.clone()); Self::pack_importer(import_status); } - if let Some(import_status) = self.ui_data.import_status.as_ref() { + if let Some(import_status) = self.ui_manager.import_status.as_ref() { if let Ok(mut status) = import_status.lock() { match &mut *status { ImportStatus::UnInitialized => { @@ -393,8 +404,54 @@ impl MarkerManager { joko_renderer: &mut joko_render::JokoRenderer, link: Option<&MumbleLink> ) { - self.gui_marker_manager(etx, is_marker_open); + self.gui_package_loader(etx, is_marker_open); self.gui_file_manager(etx, is_file_open, link); } } +impl PackageUIManager { + pub fn new() -> Self { + Self{ + import_status: Default::default(), + parents: Default::default() + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> BTreeSet + where I: Iterator + { + let iter = input.into_iter(); + let mut result: BTreeSet = BTreeSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; + } + next_gen.push(p.clone()); + } + } + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + unreachable!("The loop should always return"); + } +} + diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index d11a26b..cbb0507 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -23,20 +23,47 @@ pub(crate) struct PackCore { pub textures: ordered_hash_map::OrderedHashMap>, pub tbins: ordered_hash_map::OrderedHashMap, pub categories: IndexMap, - pub all_guids: HashMap>, + pub all_categories: HashMap, + pub entities_parents: HashMap, pub source_files: ordered_hash_map::OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: ordered_hash_map::OrderedHashMap, } impl PackCore { pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) { - if !self.all_guids.contains_key(full_category_name) { - self.all_guids.insert(full_category_name.clone(), HashSet::default()); - } - if let Some(all_guid) = self.all_guids.get_mut(full_category_name) { - all_guid.insert(*uuid); + if let Some(parent_uuid) = self.all_categories.get(full_category_name) { + self.entities_parents.insert(*uuid, *parent_uuid); } else { - panic!("Can't register {} {}", full_category_name, uuid); + println!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); + } + } + pub fn register_categories(&mut self) { + let mut entities_parents: HashMap = Default::default(); + let mut all_categories: HashMap = Default::default(); + self.recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories, None); + self.entities_parents.extend(entities_parents); + info!("Catepories registered: {}", all_categories.len()); + self.all_categories = all_categories; + } + fn recursive_register_categories( + &self, + entities_parents: &mut HashMap, + categories: &IndexMap, + all_categories: &mut HashMap, + parent_name: Option + ) { + for (name, cat) in categories.iter() { + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, name) + } else { + name.to_string() + }; + //println!("Register catepory {} {} {:?}", full_category_name, cat.guid, cat.parent); + all_categories.insert(full_category_name.clone(), cat.guid); + if let Some(parent) = cat.parent { + entities_parents.insert(cat.guid, parent); + } + self.recursive_register_categories(entities_parents, &cat.children, all_categories, Some(full_category_name)); } } } @@ -51,6 +78,7 @@ pub(crate) struct MapData { #[derive(Debug, Clone)] pub(crate) struct Category { pub guid: Uuid, + pub parent: Option, pub display_name: String, pub separator: bool, pub default_enabled: bool, diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 6acbe8c..1f3e591 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -5,7 +5,7 @@ use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; mod wm; use init::get_jokolay_dir; -use jmf::MarkerManager; +use jmf::PackageManager; //use jmf::FileManager; use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use joko_render::JokoRenderer; @@ -19,7 +19,7 @@ pub struct Jokolay { jdir: Arc, menu_panel: MenuPanel, mumble_manager: MumbleManager, - marker_manager: MarkerManager, + package_manager: PackageManager, theme_manager: ThemeManager, joko_renderer: JokoRenderer, egui_context: egui::Context, @@ -31,9 +31,10 @@ impl Jokolay { let mumble = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; let marker_manager = - MarkerManager::new(&jdir).wrap_err("failed to create marker manager")?; + PackageManager::new(&jdir).wrap_err("failed to create marker manager")?; let mut theme_manager = ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; + let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); let mut glfw_backend = GlfwBackend::new(GlfwConfig { @@ -58,7 +59,7 @@ impl Jokolay { let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); Ok(Self { mumble_manager: mumble, - marker_manager, + package_manager: marker_manager, frame_stats: wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _), joko_renderer, glfw_backend, @@ -71,14 +72,14 @@ impl Jokolay { pub fn enter_event_loop(mut self) { tracing::info!("entering glfw event loop"); self.menu_panel.show_theme_window = true; - self.menu_panel.show_marker_manager_window = true; + self.menu_panel.show_package_manager_window = true; loop { let Self { frame_stats, jdir: _, menu_panel, mumble_manager, - marker_manager, + package_manager: marker_manager, theme_manager, joko_renderer, egui_context, @@ -175,8 +176,8 @@ impl Jokolay { "Show Window Manager", ); ui.checkbox( - &mut menu_panel.show_marker_manager_window, - "Show Marker Manager", + &mut menu_panel.show_package_manager_window, + "Show Package Manager", ); ui.checkbox( &mut menu_panel.show_mumble_manager_window, @@ -202,7 +203,7 @@ impl Jokolay { }); marker_manager.gui( &etx, - &mut menu_panel.show_marker_manager_window, + &mut menu_panel.show_package_manager_window, &mut menu_panel.show_file_manager_window, latest_time, joko_renderer, link @@ -332,7 +333,7 @@ pub struct MenuPanel { show_tracing_window: bool, show_theme_window: bool, // show_settings_window: bool, - show_marker_manager_window: bool, + show_package_manager_window: bool, show_mumble_manager_window: bool, show_window_manager: bool, show_file_manager_window: bool, From fa2dd2e3c8b1512884e6d58e68535f2a4fbd850a Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 31 Mar 2024 03:15:49 +0200 Subject: [PATCH 14/54] fix save of packages and put comment on old version once this is fixed --- .../src/manager/pack/loaded.rs | 49 +++++++++++++++++-- crates/joko_marker_format/src/pack/mod.rs | 3 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 1707ce1..b0c3e8f 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -48,7 +48,7 @@ impl LoadedPack { core, selected_categories, selected_files: Default::default(), - is_dirty: true, + is_dirty: false, activation_data: Default::default(), current_map_data: Default::default(), } @@ -78,7 +78,47 @@ impl LoadedPack { .wrap_err("failed to open core pack directory")?; let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. let selected_categories = CategorySelection::default_from_pack_core(&core); + /*** + FIXME: Once this is saved properly, we can restore following block of code. + + let selected_categories = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize category data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_else(|| { + let cs = CategorySelection::default_from_pack_core(&core); + match serde_json::to_string_pretty(&cs) { + Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + cs + }); + **/ let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { Ok(contents) => match serde_json::from_str(&contents) { @@ -103,7 +143,7 @@ impl LoadedPack { core, selected_categories, selected_files: Default::default(), - is_dirty: true, + is_dirty: false, activation_data, current_map_data: Default::default(), }) @@ -388,7 +428,8 @@ impl LoadedPack { } #[tracing::instrument(skip(self))] pub fn save(&mut self) -> Result<()> { - if std::mem::take(&mut self.is_dirty) { + let is_dirty = std::mem::take(&mut self.is_dirty); + if is_dirty { match serde_json::to_string_pretty(&self.selected_categories) { Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -428,7 +469,7 @@ impl LoadedPack { save_pack_core_to_dir( &self.core, &core_dir, - std::mem::take(&mut self.is_dirty), + is_dirty, )?; Ok(()) } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index cbb0507..08e9d18 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -34,7 +34,8 @@ impl PackCore { if let Some(parent_uuid) = self.all_categories.get(full_category_name) { self.entities_parents.insert(*uuid, *parent_uuid); } else { - println!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); + //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. + info!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); } } pub fn register_categories(&mut self) { From 1b4bcd574ca4b7c4418cc267836878e7c28c4545 Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 3 Apr 2024 11:27:06 +0200 Subject: [PATCH 15/54] working asynchronous version - now there are two loops: ui and back - works with signals (messages) - need to review all features --- Cargo.lock | 76 +- Cargo.toml | 11 +- crates/joko_core/Cargo.toml | 7 +- crates/joko_core/src/lib.rs | 94 ++ crates/joko_core/src/manager/theme/mod.rs | 1 + crates/joko_core/src/manager/trace/mod.rs | 8 +- crates/joko_core/src/task/mod.rs | 72 ++ crates/joko_marker_format/Cargo.toml | 48 +- .../joko_marker_format/src/io/deserialize.rs | 363 ++++++-- crates/joko_marker_format/src/io/mod.rs | 2 +- crates/joko_marker_format/src/io/serialize.rs | 51 +- crates/joko_marker_format/src/lib.rs | 4 +- crates/joko_marker_format/src/manager/mod.rs | 4 +- .../src/manager/pack/active.rs | 18 +- .../src/manager/pack/category_selection.rs | 150 +++- .../src/manager/pack/dirty.rs | 2 +- .../src/manager/pack/loaded.rs | 836 +++++++++++++----- .../joko_marker_format/src/manager/package.rs | 641 ++++++++------ crates/joko_marker_format/src/message.rs | 75 ++ crates/joko_marker_format/src/pack/common.rs | 2 +- crates/joko_marker_format/src/pack/marker.rs | 4 +- crates/joko_marker_format/src/pack/mod.rs | 344 +++++-- crates/joko_marker_format/src/pack/route.rs | 1 + crates/joko_marker_format/src/pack/trail.rs | 1 + .../src/pack/trail_rainbow.png | Bin 0 -> 16987 bytes crates/joko_render/Cargo.toml | 16 +- crates/joko_render/src/billboard.rs | 40 +- crates/joko_render/src/lib.rs | 34 +- crates/jokolay/Cargo.toml | 15 +- crates/jokolay/src/app/mod.rs | 442 +++++++-- crates/jokolay/src/lib.rs | 1 + crates/jokolink/src/lib.rs | 49 +- crates/jokolink/src/mumble/mod.rs | 2 + 33 files changed, 2427 insertions(+), 987 deletions(-) create mode 100644 crates/joko_core/src/task/mod.rs create mode 100644 crates/joko_marker_format/src/message.rs create mode 100644 crates/joko_marker_format/src/pack/trail_rainbow.png diff --git a/Cargo.lock b/Cargo.lock index 2b07e84..e8672d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,19 +441,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97ba99bbc76e44242cd767689c33f5350c3646758edecdf1f8b7f4df5a8ea029" dependencies = [ - "cap-std 2.0.1", - "directories-next", - "rustix", - "windows-sys 0.52.0", -] - -[[package]] -name = "cap-directories" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb706d254d585ce9ff8b716d169d948700ca7b3a4715272fd2bd1cb9a65210f1" -dependencies = [ - "cap-std 3.0.0", + "cap-std", "directories-next", "rustix", "windows-sys 0.52.0", @@ -476,23 +464,6 @@ dependencies = [ "winx", ] -[[package]] -name = "cap-primitives" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a0b44fc796b1a84535a63753d50ba3972c4db55c7255c186f79140e63d56d0" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix", - "windows-sys 0.52.0", - "winx", -] - [[package]] name = "cap-std" version = "2.0.1" @@ -500,19 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330" dependencies = [ "camino", - "cap-primitives 2.0.1", - "io-extras", - "io-lifetimes", - "rustix", -] - -[[package]] -name = "cap-std" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266626ce180cf9709f317d0bf9754e3a5006359d87f4bf792f06c9c5f1b63c0f" -dependencies = [ - "cap-primitives 3.0.0", + "cap-primitives", "io-extras", "io-lifetimes", "rustix", @@ -1452,22 +1411,24 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" name = "joko_core" version = "0.2.1" dependencies = [ - "cap-directories 3.0.0", - "cap-std 2.0.1", + "cap-std", "egui", "egui_extras", "glam", "indexmap", + "jokolink", "miette", - "ordered_hash_map", "rayon", "rfd", "ringbuffer", + "scopeguard", "serde", "serde_json", + "smol_str", "tracing", "tracing-appender", "tracing-subscriber", + "uuid", ] [[package]] @@ -1479,7 +1440,8 @@ name = "joko_marker_format" version = "0.2.1" dependencies = [ "base64", - "cap-std 2.0.1", + "bytemuck", + "cap-std", "cxx", "cxx-build", "data-encoding", @@ -1489,7 +1451,7 @@ dependencies = [ "image", "indexmap", "itertools", - "joko_render", + "joko_core", "jokoapi", "jokolink", "miette", @@ -1521,10 +1483,8 @@ dependencies = [ "egui_render_three_d", "egui_window_glfw_passthrough", "glam", + "joko_marker_format", "jokolink", - "raw-window-handle 0.5.2", - "serde", - "serde_json", "tracing", ] @@ -1543,27 +1503,17 @@ dependencies = [ name = "jokolay" version = "0.2.1" dependencies = [ - "cap-directories 2.0.1", - "cap-std 2.0.1", + "cap-directories", + "cap-std", "egui", - "egui_extras", "egui_window_glfw_passthrough", - "glam", - "indexmap", "joko_core", "joko_marker_format", "joko_render", "jokolink", "miette", "rayon", - "rfd", - "ringbuffer", - "serde", - "serde_json", "tracing", - "tracing-appender", - "tracing-subscriber", - "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 84ae0fd..30dc30c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ members = [ resolver = "2" [workspace.dependencies] +#https://docs.rs/tracing/latest/tracing/level_filters/index.html + +bytemuck = { version = "1", features = ["derive"] } tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } ringbuffer = { version = "0.14" } egui = { version = "0.26" } @@ -23,12 +26,7 @@ miette = { version = "*", features = ["fancy"] } url = { version = "*", features = ["serde"] } serde_json = { version = "*" } rayon = { version = "*" } -# tokio = { version = "*", default-features = false, features = [ -# "rt-multi-thread", -# "sync", -# "time", -# "parking_lot" -# ]} +paste = { version = "*" } glam = { version = "*", features = ["fast-math"] } time = { version = "*" } ureq = { version = "*" } @@ -36,6 +34,7 @@ enumflags2 = { version = "*" } indexmap = { version = "2" } rfd = { version = "*" } smol_str = { version = "*" } +uuid = { version = "*" } itertools = { version = "*" } ordered_hash_map = { version = "*", features= ["serde"] } diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index 941a478..6818224 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cap-directories = { version = "*" } cap-std = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = [ @@ -26,4 +25,8 @@ serde_json = { workspace = true } indexmap = { workspace = true } rfd = { workspace = true } glam = { workspace = true } -ordered_hash_map = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } +uuid = { workspace = true } + +jokolink = { path = "../jokolink" } diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 415a8dc..2c5d369 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,3 +1,8 @@ +use std::str::FromStr; + +use smol_str::SmolStr; +use uuid::Uuid; + pub mod manager; /* each manager must have @@ -7,3 +12,92 @@ each manager must have 4. a public api for other managers to access */ + +pub mod task; + + +use glam::{Vec2, Vec3}; +use jokolink::MumbleLink; + + +/// This newtype is used to represents relative paths in marker packs +/// 1. It won't start with `/` or `C:` like roots, because its a relative path +/// 2. It can be empty to represent current directory +/// 3. No expansion of special characters like `.` or `..` stuff. +/// 4. It is always lowercase to avoid platform specific quirks. +/// 5. It will use `/` as the path separator. +/// 6. It doesn't mean that the path is valid. It may contain many of the utf-8 characters which are not valid path names on linux/windows +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RelativePath(SmolStr); +#[allow(unused)] +impl RelativePath { + pub fn join_str(&self, path: &str) -> Self { + let path = path.trim_start_matches('/'); + if path.is_empty() { + return Self(self.0.clone()); + } + let lower_case = path.to_lowercase(); + if self.0.is_empty() { + // no need to push `/` if we are empty, as that would make it an absolute path + return Self(lower_case.into()); + } + + let mut new = self.0.to_string(); + if !self.0.ends_with('/') { + new.push('/'); + } + new.push_str(&lower_case); + Self(new.into()) + } + + pub fn ends_with(&self, ext: &str) -> bool { + self.0.ends_with(ext) + } + pub fn is_png(&self) -> bool { + self.ends_with(".png") + } + pub fn is_tbin(&self) -> bool { + self.ends_with(".trl") + } + pub fn is_xml(&self) -> bool { + self.ends_with(".xml") + } + pub fn is_dir(&self) -> bool { + self.ends_with("/") + } + pub fn parent(&self) -> Option<&str> { + let path = self.0.trim_end_matches('/'); + if path.is_empty() { + return None; + } + path.rfind('/').map(|index| &path[..=index]) + } + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for RelativePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl From for String { + fn from(val: RelativePath) -> String { + val.0.into() + } +} +impl FromStr for RelativePath { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let path = s.trim_start_matches('/'); + if path.is_empty() { + return Ok(Self::default()); + } + Ok(Self(path.to_lowercase().into())) + } +} + + diff --git a/crates/joko_core/src/manager/theme/mod.rs b/crates/joko_core/src/manager/theme/mod.rs index 2121cac..59d1a56 100644 --- a/crates/joko_core/src/manager/theme/mod.rs +++ b/crates/joko_core/src/manager/theme/mod.rs @@ -209,6 +209,7 @@ impl ThemeManager { error!(%self.config.default_theme, "failed to find the default theme in the loaded themes :("); } } + pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { egui::Window::new("Theme Manager") .open(open) diff --git a/crates/joko_core/src/manager/trace/mod.rs b/crates/joko_core/src/manager/trace/mod.rs index 099db80..b27a99d 100644 --- a/crates/joko_core/src/manager/trace/mod.rs +++ b/crates/joko_core/src/manager/trace/mod.rs @@ -60,9 +60,8 @@ impl JokolayTracingLayer { .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::exact(40.0)) - .column(Column::initial(100.0).range(40.0..=300.0).clip(true)) - .column(Column::exact(40.0)) - .column(Column::initial(100.0).clip(true)) + .column(Column::initial(200.0).range(40.0..=300.0).clip(true)) + .column(Column::initial(400.0).clip(true)) .min_scrolled_height(0.0) .header(20.0, |mut header| { header.col(|ui| { @@ -71,9 +70,6 @@ impl JokolayTracingLayer { header.col(|ui| { ui.strong("target"); }); - header.col(|ui| { - ui.strong("line"); - }); header.col(|ui| { ui.strong("message"); }); diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs new file mode 100644 index 0000000..e072fc9 --- /dev/null +++ b/crates/joko_core/src/task/mod.rs @@ -0,0 +1,72 @@ +use std::{ + sync::{mpsc::SendError, Arc, Mutex}, + thread::JoinHandle, + result::Result +}; + +//TODO: could this be a wrapper only and a move/copy would not impact content ? +pub struct AsyncTaskGuard +{ + task_sender: std::sync::mpsc::Sender, + result_receiver: std::sync::mpsc::Receiver, + thread_task: Option>, + thread_nb: Option>, + nb: Arc, +} + +pub type AsyncTask = Arc>>; + +impl AsyncTaskGuard +where + TaskItem: Send + 'static, + ResultItem: Send + 'static, +{ + pub fn new(f: F) -> AsyncTask + where + F: Fn(TaskItem) -> ResultItem + Send + 'static, + { + //https://doc.rust-lang.org/rust-by-example/std_misc/channels.html + let (task_sender, th_task_receiver) = std::sync::mpsc::channel(); + let (th_result_sender, result_receiver) = std::sync::mpsc::channel(); + let (nb_sender, nb_receiver) = std::sync::mpsc::channel(); + let nb = Arc::new(std::sync::atomic::AtomicI32::new(0)); + let mut res = Arc::new(Mutex::new(Self { + task_sender, + result_receiver, + thread_task: None, + thread_nb: None, + nb: Arc::clone(&nb) + })); + let thread_task = std::thread::spawn(move || { + while let Ok(elt) = th_task_receiver.recv() { + let _guard = scopeguard::guard(0, |_| { + nb_sender.send(-1); + }); + nb_sender.send(1); + th_result_sender.send(f(elt)); + } + }); + let thread_nb = std::thread::spawn(move || { + while let Ok(elt) = nb_receiver.recv() { + { + nb.fetch_add(elt, std::sync::atomic::Ordering::Relaxed); + } + } + }); + + { + let mut t = res.lock().unwrap(); + t.thread_task = Some(thread_task); + t.thread_nb = Some(thread_nb); + } + res + } + pub fn send(&self, value: TaskItem) -> Result<(), SendError> { + self.task_sender.send(value) + } + + pub fn is_running(&self) -> bool { + let nb = self.nb.load(std::sync::atomic::Ordering::Relaxed); + nb != 0 + } +} \ No newline at end of file diff --git a/crates/joko_marker_format/Cargo.toml b/crates/joko_marker_format/Cargo.toml index 7b6f936..1eb9c48 100755 --- a/crates/joko_marker_format/Cargo.toml +++ b/crates/joko_marker_format/Cargo.toml @@ -6,41 +6,37 @@ edition = "2021" [dependencies] # jmf deps # for marker packs -xot = { version = "0.16.0" } -# to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip -indexmap = { workspace = true, features = ["serde"]} -uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } - - -# for easier extraction to folers and compression of folders into zip files (.taco format alias) -zip = { version = "0.6", default-features = false, features = ["deflate"] } -# for dealing with png files in marker packs. -image = { version = "0.24", default-features = false, features = ["png"] } -# for rapid xml bindings -cxx = { version = "1.0", features = ["std"] } base64 = "0.21.2" +bytemuck = { workspace = true } +cap-std = { workspace = true } +cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings data-encoding = "2.4.0" +egui = { workspace = true } enumflags2 = { workspace = true } -cap-std = { workspace = true } -tracing = { workspace = true } -miette = { workspace = true } glam = { workspace = true } -egui = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -url = { workspace = true } +image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +jokoapi = { path = "../jokoapi" } +jokolink = { path = "../jokolink" } +miette = { workspace = true } +ordered_hash_map = { workspace = true } +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } rayon = { workspace = true } rfd = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } smol_str = { workspace = true } -itertools = { workspace = true } time = { workspace = true , features = ["serde"]} -phf = { version = "*", features = ["macros"] } -paste = { version = "*" } -ordered_hash_map = { workspace = true } -joko_render = { path = "../joko_render" } -jokolink = { path = "../jokolink" } -jokoapi = { path = "../jokoapi" } +tracing = { workspace = true } tribool = "0.3.0" +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } +zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) + [dev-dependencies] diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 10e4be2..389f7ee 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -1,5 +1,7 @@ +use joko_core::RelativePath; + use crate::{ - pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, TBinStatus, Trail, MapData, Route}, + pack::{Category, RawCategory, CommonAttributes, Marker, PackCore, TBin, TBinStatus, Trail, MapData, Route, prefix_parent}, BASE64_ENGINE, }; use base64::Engine; @@ -7,7 +9,7 @@ use cap_std::fs_utf8::Dir; use glam::Vec3; use indexmap::IndexMap; use miette::{bail, Context, IntoDiagnostic, Result}; -use std::{collections::{VecDeque}, io::Read}; +use std::{collections::{VecDeque, HashMap}, io::Read, sync::Arc}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, info, info_span, instrument, trace, warn}; use uuid::Uuid; @@ -16,6 +18,7 @@ use xot::{Node, Xot, Element}; use super::XotAttributeNameIDs; pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { + //FIXME: this should return two elements: //called from already parsed data let mut pack = PackCore::default(); // walks the directory and loads all files into the hashmap @@ -84,6 +87,10 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { trace!("file ignored: {name}") } } + info!("Entities registered: {}", pack.entities_parents.len()); + info!("Maps registered: {}", pack.maps.len()); + info!("Textures registered: {}", pack.textures.len()); + info!("Trail binaries registered: {}", pack.tbins.len()); Ok(pack) } @@ -256,13 +263,26 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { closed }) } + +fn parse_categories( + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut OrderedHashMap, + names: &XotAttributeNameIDs, +) { + //called once per file + parse_categories_recursive(tree, tags, first_pass_categories, names, None); + +} + + // a recursive function to parse the marker category tree. -fn recursive_marker_category_parser( +fn parse_categories_recursive( tree: &Xot, tags: impl Iterator, - cats: &mut IndexMap, + first_pass_categories: &mut OrderedHashMap, names: &XotAttributeNameIDs, - parent_uuid: Option, + parent_name: Option, ) { for tag in tags { let ele = match tree.element(tag) { @@ -273,14 +293,25 @@ fn recursive_marker_category_parser( continue; } - let name = ele.get_attribute(names.name).or(ele.get_attribute(names.CapitalName)).unwrap_or_default(); + let name = ele + .get_attribute(names.name) + .or(ele.get_attribute(names.CapitalName)) + .unwrap_or_default() + .to_lowercase(); if name.is_empty() { continue; } let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); - let display_name = ele.get_attribute(names.display_name).unwrap_or(name); + /* + FIXME: how to handle both + orphans + out of order evaluation => mark the current marker category to be skipped and not inserted, this is an orphan for later reinsertion + if the category has a Display name, then the name is relative, if not, it means this is defined somewhere else and name is absolute. + => have a "late insertion" container + */ + let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); let separator = ele .get_attribute(names.separator) @@ -296,24 +327,30 @@ fn recursive_marker_category_parser( .map(|u: u8| u != 0) .unwrap_or(true); let guid = parse_guid(names, ele); - //println!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_uuid); - recursive_marker_category_parser( + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, name) + } else { + name.to_string() + }; + trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); + if !first_pass_categories.contains_key(&full_category_name) { + first_pass_categories.insert(full_category_name.clone(), RawCategory { + guid, + parent_name: parent_name.clone(), + display_name: display_name.to_string(), + relative_category_name: name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: ca, + }); + } + parse_categories_recursive( tree, tree.children(tag), - &mut cats - .entry(name.to_string()) - .or_insert_with(|| Category { - guid, - parent: parent_uuid.clone(), - display_name: display_name.to_string(), - separator, - default_enabled, - props: ca, - children: Default::default(), - }) - .children, + first_pass_categories, names, - Some(guid), + Some(full_category_name), ); } } @@ -332,9 +369,9 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack .wrap_err("no doc element")?; if let Some(od) = tree.element(overlay_data_node) { - let mut categories: IndexMap = Default::default(); + let mut categories: IndexMap = Default::default(); if od.name() == xot_names.overlay_data { - recursive_marker_category_parser_categories_xml( + parse_category_categories_xml_recursive( &file_name, &tree, tree.children(overlay_data_node), @@ -342,8 +379,9 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack &mut categories, &xot_names, None, + None, ); - //println!("loaded categories: {:?}", categories); + trace!("loaded categories: {:?}", categories); pack.categories = categories; pack.register_categories(); } else { @@ -385,11 +423,16 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result for poi_node in tree.children(pois) { if let Some(child) = tree.element(poi_node) { - let category = child + let full_category_name = child .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); - let span_guard = info_span!("category", category).entered(); + if full_category_name.is_empty() { + panic!("full_category_name is empty {:?} {:?}", map_xml_str, child); + } + let span_guard = info_span!("category", full_category_name).entered(); + + let category_uuid = pack.get_or_create_category_uuid(&full_category_name); let raw_uid = child.get_attribute(names.guid); if raw_uid.is_none() { @@ -411,7 +454,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail if child.name() == names.route { debug!("Found a route in core pack {:?}", child); - import_route_as_trail(pack, &names, &tree, &poi_node, child, category, source_file_name) + import_route_as_trail(pack, &names, &tree, &poi_node, child, full_category_name, &category_uuid, source_file_name) } else if child.name() == names.poi { debug!("Found a POI in core pack {:?}", child); @@ -441,11 +484,12 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child, &names); - pack.register_uuid(&category, &guid); + pack.register_uuid(&full_category_name, &guid); let marker = Marker { position: [xpos, ypos, zpos].into(), map_id, - category, + category: full_category_name, + parent: category_uuid.clone(), attrs: ca, guid, source_file_name @@ -468,9 +512,10 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child, &names); - pack.register_uuid(&category, &guid); + pack.register_uuid(&full_category_name, &guid); let trail = Trail { - category, + category: full_category_name, + parent: category_uuid.clone(), map_id, props: ca, guid, @@ -490,14 +535,15 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result } // a temporary recursive function to parse the marker category tree. -fn recursive_marker_category_parser_categories_xml( +fn parse_category_categories_xml_recursive( file_name: &String, tree: &Xot, tags: impl Iterator, pack: &mut PackCore, - cats: &mut IndexMap, + cats: &mut IndexMap, names: &XotAttributeNameIDs, parent_uuid: Option, + parent_name: Option, ) { for tag in tags { if let Some(ele) = tree.element(tag) { @@ -505,20 +551,21 @@ fn recursive_marker_category_parser_categories_xml( continue; } - let name = ele.get_attribute(names.name) + //TODO: if no display name, only keep the parent/enfant relationship + let relative_category_name = ele.get_attribute(names.name) .or(ele.get_attribute(names.display_name) .or(ele.get_attribute(names.CapitalName) ) - ).unwrap_or_default(); - if name.is_empty() { + ).unwrap_or_default().to_lowercase(); + if relative_category_name.is_empty() { info!("category doesn't have a name attribute: {ele:#?}"); continue; } - let span_guard = info_span!("category", name).entered(); + let span_guard = info_span!("category", relative_category_name).entered(); let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); - let display_name = ele.get_attribute(names.display_name).unwrap_or(name); + let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); let separator = match ele.get_attribute(names.separator).unwrap_or("0") { "0" => false, @@ -537,28 +584,51 @@ fn recursive_marker_category_parser_categories_xml( true } }; + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, relative_category_name) + } else { + relative_category_name.to_string() + }; let guid = parse_guid(names, ele); - //println!("recursive_marker_category_parser_categories_xml {} {} {:?}", name, guid, parent_uuid); - recursive_marker_category_parser_categories_xml( - file_name, - tree, - tree.children(tag), - pack, - &mut cats - .entry(name.to_string()) + trace!("recursive_marker_category_parser_categories_xml {} {} {:?}", full_category_name, guid, parent_uuid); + if display_name.is_empty() { + assert!(parent_name.is_none()); + parse_category_categories_xml_recursive( + file_name, + tree, + tree.children(tag), + pack, + cats, + names, + Some(guid), + Some(full_category_name), + ); + } else { + let current_category = cats + .entry(guid) .or_insert_with(|| Category { guid, parent: parent_uuid.clone(), display_name: display_name.to_string(), + relative_category_name: relative_category_name.to_string(), + full_category_name: full_category_name.clone(), separator, default_enabled, props: ca, children: Default::default(), - }) - .children, - names, - Some(guid), - ); + }); + parse_category_categories_xml_recursive( + file_name, + tree, + tree.children(tag), + pack, + &mut current_category.children, + names, + Some(guid), + Some(full_category_name), + ); + }; + std::mem::drop(span_guard); } else { //it may be a comment, a space, anything @@ -575,7 +645,7 @@ fn recursive_marker_category_parser_categories_xml( /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { - //FIXME: there is a problem where pack map files are not dump into the folders anymore + //FIXME: there might be a problem where the elements are not displayed immediately after save //called to import a new pack // all the contents of ZPack let mut pack = PackCore::default(); @@ -646,9 +716,57 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } std::mem::drop(span); } - for source_file_name in xmls { + + let span_guard_categories = info_span!("deserialize xml: categories").entered(); + + //first pass: categories only + let span_guard_first_pass = info_span!("deserialize xml first pass: load MarkerCategory").entered(); + let mut first_pass_categories: OrderedHashMap = Default::default(); + for source_file_name in xmls.iter() { + let mut xml_str = String::new(); + let span_guard = info_span!("deserialize xml first pass: load file", source_file_name).entered(); + if zip_archive + .by_name(&source_file_name) + .ok() + .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) + .is_none() + { + info!("failed to read file from zip"); + continue; + }; + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + parse_categories(&tree, tree.children(od), &mut first_pass_categories, &names); + drop(span_guard); + } + span_guard_first_pass.exit(); + + //second pass: orphan categories + let span_guard_second_pass = info_span!("deserialize xml second pass: orphan categories").entered(); + for source_file_name in xmls.iter() { let mut xml_str = String::new(); - let span_guard = info_span!("deserialize xml", source_file_name).entered(); + let span_guard = info_span!("deserialize xml second pass: load file", source_file_name).entered(); if zip_archive .by_name(&source_file_name) .ok() @@ -680,9 +798,90 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { continue; } }; + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + info!("missing pois tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let full_category_name = child + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + if full_category_name.is_empty() { + //ignore it silently since it might be a Route + //info!("full_category_name is empty {:?}", child); + continue; + } + if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { + let category_uuid = Uuid::new_v4(); + first_pass_categories.insert(full_category_name.clone(), RawCategory{ + default_enabled: true, + guid: category_uuid, + parent_name: prefix_parent(&full_category_name, '.'), + display_name: full_category_name.clone(), + full_category_name: full_category_name.clone(), + relative_category_name: full_category_name.clone(), + props: Default::default(), + separator: false + }); + info!("There is an orphan missing category '{}' which was created", full_category_name); + } + } + drop(span_guard); + } + span_guard_second_pass.exit(); + + pack.categories = Category::reassemble(&first_pass_categories, &mut pack.late_discovery_categories); + pack.register_categories(); + + //third and last pass: elements + let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); + for source_file_name in xmls.iter() { + let mut xml_str = String::new(); + let span_guard = info_span!("deserialize xml third pass load file ", source_file_name).entered(); + if zip_archive + .by_name(&source_file_name) + .ok() + .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) + .is_none() + { + info!("failed to read file from zip"); + continue; + }; - // parse_categories - recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names, None); + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; let pois = match tree.children(od).find(|node| { tree.element(*node) @@ -701,18 +900,26 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { Some(ele) => ele, None => continue, }; - let category_name = child + let full_category_name = child .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); + if full_category_name.is_empty() { + info!("full_category_name is empty {:?}", child); + continue; + } + if ! pack.category_exists(&full_category_name) { + panic!("Missing category {}, previous pass should have taken care of this", full_category_name); + } + let category_uuid = pack.get_or_create_category_uuid(&full_category_name); debug!("import element: {:?}", child); if child.name() == names.poi { - import_poi(&mut pack, &names, &child, category_name, source_file_name.clone()); + import_poi(&mut pack, &names, &child, full_category_name, &category_uuid, source_file_name.clone()); } else if child.name() == names.trail { - import_trail(&mut pack, &names, &child, category_name, source_file_name.clone()); + import_trail(&mut pack, &names, &child, full_category_name, &category_uuid, source_file_name.clone()); } else if child.name() == names.route { - import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, category_name, source_file_name.clone()); + import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, full_category_name, &category_uuid, source_file_name.clone()); } else { info!("unknown element: {:?}", child); } @@ -720,6 +927,8 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { drop(span_guard); } + span_guard_third_pass.exit(); + span_guard_categories.exit(); Ok(pack) } @@ -740,7 +949,7 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ .unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, source_file_name: String) -> Option { +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { if let Some(map_id) = poi_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -773,6 +982,7 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & position: [xpos, ypos, zpos].into(), map_id, category: category_name.clone(), + parent: category_uuid.clone(), attrs: common_attributes, guid: parse_guid(names, poi_element), source_file_name @@ -809,6 +1019,7 @@ fn parse_route( route_node: &Node, route_element: &Element, category_name: &String, + category_uuid: &Uuid, source_file_name: String ) -> Option { @@ -874,6 +1085,7 @@ fn parse_route( Some(Route { category, + parent: category_uuid.clone(), path, reset_position, reset_range: reset_range.unwrap_or(0.0), @@ -885,7 +1097,7 @@ fn parse_route( } -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, source_file_name: String) -> Option { +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html if let Some(map_id) = trail_element .get_attribute(names.trail_data) @@ -905,6 +1117,7 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: Some(Trail { category: category_name.clone(), + parent: category_uuid.clone(), map_id, props: common_attributes, guid: parse_guid(names, trail_element), @@ -921,8 +1134,8 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } -fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: String, source_file_name: String) { - if let Some(marker) = parse_marker(pack, names, poi_element, &category_name, source_file_name) { +fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: String, category_uuid: &Uuid, source_file_name: String) { + if let Some(marker) = parse_marker(pack, names, poi_element, &category_name, category_uuid, source_file_name) { pack.register_uuid(&category_name, &marker.guid); if !pack.maps.contains_key(&marker.map_id) { pack.maps.insert(marker.map_id, MapData::default()); @@ -934,8 +1147,8 @@ fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &El } -fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: String, source_file_name: String) { - if let Some(trail) = parse_trail(pack, names, trail_element, &category_name, source_file_name) { +fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: String, category_uuid: &Uuid, source_file_name: String) { + if let Some(trail) = parse_trail(pack, names, trail_element, &category_name, category_uuid, source_file_name) { pack.register_uuid(&category_name, &trail.guid); if !pack.maps.contains_key(&trail.map_id) { pack.maps.insert(trail.map_id, MapData::default()); @@ -964,6 +1177,7 @@ fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { Trail { map_id: route.map_id, category: route.category.clone(), + parent: route.parent.clone(), guid: route.guid, props: props, dynamic: true, @@ -971,8 +1185,17 @@ fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { } } -fn import_route_as_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category_name: String, source_file_name: String) { - if let Some(route) = parse_route(pack, names, tree, route_node, route_element, &category_name, source_file_name) { +fn import_route_as_trail( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, + category_name: String, + category_uuid: &Uuid, + source_file_name: String +) { + if let Some(route) = parse_route(pack, names, tree, route_node, route_element, &category_name, category_uuid, source_file_name) { let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); let file_path: RelativePath = file_name.parse().unwrap(); let trail = route_to_trail(&route, &file_path); @@ -1014,6 +1237,8 @@ fn read_file_bytes_from_zip_by_name( } None } + + // #[cfg(test)] // mod test { diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs index 5ee12b2..2640b97 100644 --- a/crates/joko_marker_format/src/io/mod.rs +++ b/crates/joko_marker_format/src/io/mod.rs @@ -8,7 +8,7 @@ mod error; mod serialize; pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; -pub(crate) use serialize::save_pack_core_to_dir; +pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; pub(crate) struct XotAttributeNameIDs { // xml tags pub overlay_data: NameId, diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index 329883c..e14bfbe 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -1,25 +1,23 @@ use crate::{ - pack::{Category, Marker, PackCore, Trail, Route}, + pack::{Category, Marker, Trail, Route}, + manager::{LoadedPackData, LoadedPackTexture}, BASE64_ENGINE, }; use base64::Engine; use cap_std::fs_utf8::Dir; use indexmap::IndexMap; use miette::{Context, IntoDiagnostic, Result}; -use std::{io::Write}; +use std::io::Write; use tracing::info; +use uuid::Uuid; use xot::{Element, Node, SerializeOptions, Xot}; use super::XotAttributeNameIDs; /// Save the pack core as xml pack using the given directory as pack root path. -pub(crate) fn save_pack_core_to_dir( - pack_core: &PackCore, - dir: &Dir, - is_dirty: bool, +pub(crate) fn save_pack_data_to_dir( + pack_data: &LoadedPackData, + writing_directory: &Dir, ) -> Result<()> { - if !is_dirty { - return Ok(()); - } // save categories let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); @@ -28,23 +26,23 @@ pub(crate) fn save_pack_core_to_dir( .new_root(od) .into_diagnostic() .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_core.categories, od) + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) .wrap_err("failed to serialize cats")?; let cats = tree .with_serialize_options(SerializeOptions { pretty: true }) .to_string(root_node) .into_diagnostic() .wrap_err("failed to convert cats xot to string")?; - dir.create("categories.xml") + writing_directory.create("categories.xml") .into_diagnostic() .wrap_err("failed to create categories.xml")? .write_all(cats.as_bytes()) .into_diagnostic() .wrap_err("failed to write to categories.xml")?; // save maps - for (map_id, map_data) in pack_core.maps.iter() { + for (map_id, map_data) in pack_data.maps.iter() { if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { info!( ?e, map_id, "failed to remove xml file that had nothing to write to" @@ -89,23 +87,30 @@ pub(crate) fn save_pack_core_to_dir( .to_string(root_node) .into_diagnostic() .wrap_err("failed to serialize map data to string")?; - dir.create(format!("{map_id}.xml")) + writing_directory.create(format!("{map_id}.xml")) .into_diagnostic() .wrap_err("failed to create map xml file")? .write_all(map_xml.as_bytes()) .into_diagnostic() .wrap_err("failed to write map data to file")?; } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<()> { + // save images - for (img_path, img) in pack_core.textures.iter() { + for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { - dir.create_dir_all(parent) + writing_directory.create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir for an image: {img_path}") })?; } - dir.create(img_path.as_str()) + writing_directory.create(img_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? .write(img) @@ -115,9 +120,9 @@ pub(crate) fn save_pack_core_to_dir( })?; } // save tbins - for (tbin_path, tbin) in pack_core.tbins.iter() { + for (tbin_path, tbin) in pack_texture.tbins.iter() { if let Some(parent) = tbin_path.parent() { - dir.create_dir_all(parent) + writing_directory.create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir of tbin: {tbin_path}") @@ -132,7 +137,7 @@ pub(crate) fn save_pack_core_to_dir( bytes.extend_from_slice(&node[1].to_ne_bytes()); bytes.extend_from_slice(&node[2].to_ne_bytes()); } - dir.create(tbin_path.as_str()) + writing_directory.create(tbin_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? .write_all(&bytes) @@ -145,10 +150,10 @@ pub(crate) fn save_pack_core_to_dir( fn recursive_cat_serializer( tree: &mut Xot, names: &XotAttributeNameIDs, - cats: &IndexMap, + cats: &IndexMap, parent: Node, ) -> Result<()> { - for (cat_name, cat) in cats { + for (_, cat) in cats { let cat_node = tree.new_element(names.marker_category); tree.append(parent, cat_node).into_diagnostic()?; { @@ -156,7 +161,7 @@ fn recursive_cat_serializer( ele.set_attribute(names.display_name, &cat.display_name); ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); // let cat_name = tree.add_name(cat_name); - ele.set_attribute(names.name, cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); // no point in serializing default values if !cat.default_enabled { ele.set_attribute(names.default_enabled, "0"); diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs index d19f32d..57eab99 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_marker_format/src/lib.rs @@ -5,8 +5,10 @@ pub(crate) mod io; pub(crate) mod manager; pub(crate) mod pack; +pub mod message; + +pub use manager::{PackageDataManager, PackageUIManager, LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; -pub use manager::PackageManager; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index bb85ded..6335145 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -19,5 +19,5 @@ We will make not having a valid category/texture/tbin path as allowed. So, users mod package; mod pack; -pub use package::PackageManager; - +pub use package::{PackageDataManager, PackageUIManager}; +pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; diff --git a/crates/joko_marker_format/src/manager/pack/active.rs b/crates/joko_marker_format/src/manager/pack/active.rs index 8fd27be..a274352 100644 --- a/crates/joko_marker_format/src/manager/pack/active.rs +++ b/crates/joko_marker_format/src/manager/pack/active.rs @@ -1,13 +1,16 @@ +use std::collections::HashSet; + use ordered_hash_map::OrderedHashMap; use egui::TextureHandle; use glam::{vec2, Vec2, Vec3}; use indexmap::IndexMap; -use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; +use crate::message::{MarkerObject, MarkerVertex, TrailObject}; use uuid::Uuid; +use joko_core::RelativePath; use crate::{ - pack::{CommonAttributes, RelativePath}, + pack::CommonAttributes, INCHES_PER_METER, }; use jokolink::MumbleLink; @@ -17,12 +20,14 @@ use jokolink::MumbleLink; - category activation data -> track and changes to propagate to markers of this map - current active markers, which will keep track of their original marker, so as to propagate any changes easily */ +#[derive(Clone)] pub struct ActiveTrail { pub trail_object: TrailObject, pub texture_handle: TextureHandle, } /// This is an active marker. /// It stores all the info that we need to scan every frame +#[derive(Clone)] pub(crate) struct ActiveMarker { /// texture id from managed textures pub texture_id: u64, @@ -34,7 +39,7 @@ pub(crate) struct ActiveMarker { pub max_pixel_size: f32, /// billboard must not be smaller than this size in pixels pub min_pixel_size: f32, - pub attrs: CommonAttributes, + pub common_attributes: CommonAttributes, } pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; @@ -44,7 +49,7 @@ impl ActiveMarker { let Self { texture_id, pos, - attrs, + common_attributes: attrs, _texture, max_pixel_size, min_pixel_size, @@ -267,16 +272,19 @@ impl ActiveTrail { } } -#[derive(Default)] +#[derive(Default, Clone)] pub(crate) struct CurrentMapData { /// the map to which the current map data belongs to pub map_id: u32, + //pub active_elements: HashSet, /// The textures that are being used by the markers, so must be kept alive by this hashmap pub active_textures: OrderedHashMap, /// The key is the index of the marker in the map markers /// Their position in the map markers serves as their "id" as uuids can be duplicates. pub active_markers: IndexMap, + pub wip_markers: IndexMap, /// The key is the position/index of this trail in the map trails. same as markers pub active_trails: IndexMap, + pub wip_trails: IndexMap, } diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs index 2f0d932..728f38a 100644 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -1,11 +1,11 @@ -use std::collections::{HashSet, HashMap, BTreeSet}; -use ordered_hash_map::{OrderedHashMap}; +use std::collections::{HashSet, HashMap}; +use ordered_hash_map::OrderedHashMap; use indexmap::IndexMap; use uuid::Uuid; use crate::{ - pack::{Category, CommonAttributes, PackCore}, + message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, PackCore} }; use serde::{Deserialize, Serialize}; @@ -15,82 +15,77 @@ pub struct CategorySelection { pub uuid: Uuid,//FIXME: there seems to be guid generated at several places leading to confusion in what is active or not (most likely in category, not saved versys categoryselection, saved) #[serde(skip)] pub parent: Option, - pub selected: bool, + pub is_selected: bool,//has it been selected in configuration to be displayed + pub is_active: bool,//currently being displayed (i.e.: active) pub separator: bool, pub display_name: String, pub children: OrderedHashMap, } pub struct SelectedCategoryManager { - data: OrderedHashMap, + data: OrderedHashMap, } impl<'a> SelectedCategoryManager { pub fn new( selected_categories: &OrderedHashMap, - core_categories: &IndexMap + categories: &IndexMap ) -> Self { let mut list_of_enabled_categories = Default::default(); - CategorySelection::recursive_get_full_names( + CategorySelection::get_list_of_enabled_categories( &selected_categories, - &core_categories, + &categories, &mut list_of_enabled_categories, - "", &Default::default(), ); Self { data: list_of_enabled_categories } } - pub fn cloned_data(&self) -> OrderedHashMap { + pub fn cloned_data(&self) -> OrderedHashMap { self.data.clone() } - pub fn is_selected(&self, category: &String) -> bool { + pub fn is_selected(&self, category: &Uuid) -> bool { self.data.contains_key(category) } - pub fn get(&self, key: &String) -> &CommonAttributes { + pub fn get(&self, key: &Uuid) -> &CommonAttributes { self.data.get(key).unwrap() } pub fn len(&self) -> usize { self.data.len() } - pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, String, CommonAttributes> { + pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { self.data.keys() } } + + impl CategorySelection { pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { - let mut selection = OrderedHashMap::new(); - Self::recursive_create_category_selection(&mut selection, &pack.categories); - selection + let mut selectable_categories = OrderedHashMap::new(); + Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); + selectable_categories } - fn recursive_get_full_names( + fn get_list_of_enabled_categories( selection: &OrderedHashMap, - core_categories: &IndexMap, - list_of_enabled_categories: &mut OrderedHashMap, - parent_name: &str, + categories: &IndexMap, + list_of_enabled_categories: &mut OrderedHashMap, parent_common_attributes: &CommonAttributes, ) { - for (name, cat) in core_categories { - if let Some(selected_cat) = selection.get(name) { - if !selected_cat.selected { + for (_, cat) in categories { + if let Some(selectable_category) = selection.get(&cat.relative_category_name) { + if !selectable_category.is_selected { continue; } - let full_name = if parent_name.is_empty() { - name.clone() - } else { - format!("{parent_name}.{name}") - }.to_lowercase(); let mut common_attributes = cat.props.clone(); common_attributes.inherit_if_attr_none(parent_common_attributes); - Self::recursive_get_full_names( - &selected_cat.children, + Self::get_list_of_enabled_categories( + &selectable_category.children, &cat.children, list_of_enabled_categories, - &full_name, &common_attributes, ); - list_of_enabled_categories.insert(full_name, common_attributes); + list_of_enabled_categories.insert(cat.guid, common_attributes); } } } @@ -111,50 +106,107 @@ impl CategorySelection { //assert!(cat.guid.len() > 0); } } - fn recursive_create_category_selection( - selection: &mut OrderedHashMap, - cats: &IndexMap, + fn recursive_create_selectable_categories( + selectable_categories: &mut OrderedHashMap, + cats: &IndexMap, ) { - for (cat_name, cat) in cats.iter() { - if !selection.contains_key(cat_name) { + for (_, cat) in cats.iter() { + if !selectable_categories.contains_key(&cat.relative_category_name) { let to_insert = CategorySelection { uuid: cat.guid, parent: cat.parent, - selected: cat.default_enabled, + is_selected: cat.default_enabled, + is_active: !cat.separator,//by default separators are not considered active since they contain nothing separator: cat.separator, display_name: cat.display_name.clone(), children: Default::default(), }; //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); - selection.insert(cat_name.clone(), to_insert); + selectable_categories.insert(cat.relative_category_name.clone(), to_insert); } - let s = selection.get_mut(cat_name).unwrap(); - Self::recursive_create_category_selection(&mut s.children, &cat.children); + let s = selectable_categories.get_mut(&cat.relative_category_name).unwrap(); + Self::recursive_create_selectable_categories(&mut s.children, &cat.children); } } + pub fn recursive_set(selection: &mut OrderedHashMap, uuid: Uuid, status: bool) -> bool { + if selection.is_empty() { + return false; + } else { + for cat in selection.values_mut() { + if cat.separator { + continue; + } + if cat.uuid == uuid { + cat.is_selected = status; + return true; + } + if Self::recursive_set(&mut cat.children, uuid, status) { + return true; + } + } + return false; + } + } + pub fn recursive_set_all(selection: &mut OrderedHashMap, status: bool) { + if selection.is_empty() { + return; + } + for cat in selection.values_mut() { + if cat.separator { + continue; + } + cat.is_selected = status; + Self::recursive_set_all(&mut cat.children, status); + } + } + + pub fn recursive_update_active_categories(selection: &mut OrderedHashMap, active_elements: &HashSet) -> bool { + let mut is_active = false; + if selection.is_empty() { + //println!("recursive_update_active_categories is_empty"); + return is_active; + } + for cat in selection.values_mut() { + cat.is_active = active_elements.contains(&cat.uuid) || Self::recursive_update_active_categories(&mut cat.children, active_elements); + if cat.is_active { + is_active = true; + } + } + return is_active; + } + pub fn recursive_selection_ui( + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, - on_screen: &BTreeSet + show_only_active: bool, + late_discovery_categories: &HashSet, ) { if selection.is_empty() { return; } egui::ScrollArea::vertical().show(ui, |ui| { for (name, cat) in selection.iter_mut() { + if !cat.is_active && show_only_active && !cat.separator { + continue; + } ui.horizontal(|ui| { if cat.separator { ui.add_space(3.0); } else { - let cb = ui.checkbox(&mut cat.selected, ""); + let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { + u2b_sender.send(UIToBackMessage::CategoryActivationStatusChange(cat.uuid, cat.is_selected)); *is_dirty = true; } } //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); - let color = if on_screen.contains(&cat.uuid) { + let color = if late_discovery_categories.contains(&cat.uuid) { + egui::Color32::LIGHT_RED + } else if cat.is_active { egui::Color32::LIGHT_GREEN } else { egui::Color32::GRAY @@ -164,7 +216,15 @@ impl CategorySelection { ui.label(label); } else { ui.menu_button(label, |ui: &mut egui::Ui| { - Self::recursive_selection_ui(&mut cat.children, ui, is_dirty, on_screen); + Self::recursive_selection_ui( + u2b_sender, + u2u_sender, + &mut cat.children, + ui, + is_dirty, + show_only_active, + late_discovery_categories + ); }); } }); diff --git a/crates/joko_marker_format/src/manager/pack/dirty.rs b/crates/joko_marker_format/src/manager/pack/dirty.rs index 737186c..3dd900c 100644 --- a/crates/joko_marker_format/src/manager/pack/dirty.rs +++ b/crates/joko_marker_format/src/manager/pack/dirty.rs @@ -1,7 +1,7 @@ use ordered_hash_map::OrderedHashSet; -use crate::pack::RelativePath; +use joko_core::RelativePath; #[derive(Debug, Default, Clone)] pub(crate) struct DirtyMarker { diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index b0c3e8f..6af96f3 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -1,70 +1,193 @@ use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::Arc + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc }; -use ordered_hash_map::{OrderedHashMap}; + +use indexmap::IndexMap; +use ordered_hash_map::OrderedHashMap; use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; -use image::{EncodableLayout}; -use joko_render::billboard::{TrailObject}; -use tracing::{debug, error, info}; +use image::EncodableLayout; +use tracing::{debug, error, info, info_span}; use uuid::Uuid; use crate::{ - io::{load_pack_core_from_dir, save_pack_core_to_dir}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, pack::{PackCore} + io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} }; use jokolink::MumbleLink; +use joko_core::{ + task::{AsyncTask, AsyncTaskGuard}, + RelativePath +}; +use crate::message::{ + BackToUIMessage, TrailObject +}; use miette::{bail, Context, IntoDiagnostic, Result}; use super::activation::{ActivationData, ActivationType}; use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; use crate::manager::pack::category_selection::CategorySelection; +use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; + -pub(crate) struct LoadedPack { +pub (crate) struct PackTasks { + //TODO: the tasks should be in GUI and not in package + //an object that can handle such tasks should be passed as argument of any function that may required an async action + save_ui_task: AsyncTask>, + save_data_task: AsyncTask>, +} + +#[derive(Clone)] +pub struct LoadedPackData { + pub name: String, + pub uuid: Uuid, + pub dir: Arc, + /// The actual xml pack. + //pub core: PackCore, + pub categories: IndexMap, + pub all_categories: HashMap, + pub source_files: OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: OrderedHashMap, + selected_files: OrderedHashMap, + _is_dirty: bool,//there was an edition in the package itself + + // loca copy in the data side of what is exposed in UI + selectable_categories: OrderedHashMap, + pub entities_parents: HashMap, + activation_data: ActivationData, + active_elements: HashSet,//keep track of which elements are active +} +/* +TODO: LoadedPack is in fact the perfect tool to handle GUI if there was no "core" member inside it + it means dig out a CorePack into multiple parts +*/ +#[derive(Clone)] +pub struct LoadedPackTexture { + pub name: String, + pub uuid: Uuid, /// The directory inside which the pack data is stored /// There should be a subdirectory called `core` which stores the pack core /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. /// eg: Active categories, activation data etc.. pub dir: Arc, - /// The actual xml pack. - pub core: PackCore, + pub tbins: OrderedHashMap, + pub textures: OrderedHashMap>, + /// The selection of categories which are "enabled" and markers belonging to these may be rendered - selected_categories: OrderedHashMap, - selected_files: OrderedHashMap, - is_dirty: bool, - activation_data: ActivationData, + selectable_categories: OrderedHashMap, current_map_data: CurrentMapData, + activation_data: ActivationData, + active_elements: HashSet,//which are the active elements (loaded) + pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + _is_dirty: bool, } -impl LoadedPack { - const CORE_PACK_DIR_NAME: &'static str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; - const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; +impl PackTasks { + pub fn new() -> Self { + Self { + save_ui_task: AsyncTaskGuard::new(PackTasks::async_save_ui), + save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + } + } + pub fn is_running(&self) -> bool { + self.save_ui_task.lock().unwrap().is_running() + } - pub fn new(core: PackCore, dir: Arc) -> Self { - let selected_categories = CategorySelection::default_from_pack_core(&core); - LoadedPack { - dir, - core, - selected_categories, - selected_files: Default::default(), - is_dirty: false, - activation_data: Default::default(), - current_map_data: Default::default(), + fn save_ui(&self, pack: &mut LoadedPackTexture) { + if pack._is_dirty { + std::mem::take(&mut pack._is_dirty); + self.save_ui_task.lock().unwrap().send( + pack.clone() + ); } } - pub fn category_sub_menu(&mut self, ui: &mut egui::Ui, on_screen: &BTreeSet) { - //it is important to generate a new id each time to avoid collision - ui.push_id(ui.next_auto_id(), |ui| { - CategorySelection::recursive_selection_ui( - &mut self.selected_categories, - ui, - &mut self.is_dirty, - on_screen + + fn save_data(&self, pack: &mut LoadedPackData) { + if pack._is_dirty { + std::mem::take(&mut pack._is_dirty); + self.save_data_task.lock().unwrap().send( + pack.clone() ); - }); + } + } + + fn change_map( + &self, + pack: &mut LoadedPackData, + b2u_sender: &std::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap + ) { + //TODO + //self.load_map_task.lock().unwrap().send(pack); + } + + fn async_save_ui( + pack_texture: LoadedPackTexture + ) -> Result<()> { + //let (dir, selectable_categories, activation_data, core) = pack; + info!("Save package {:?}", pack_texture.dir);//FIXME: the context is no more since this is another thread entirely, we do not know which package this is about + match serde_json::to_string_pretty(&pack_texture.selectable_categories) { + Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + match serde_json::to_string_pretty(&pack_texture.activation_data) { + Ok(ad_json) => match pack_texture.dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(_) => { + debug!("wrote activation to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write activation data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } + let writing_directory = pack_texture.dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + save_pack_texture_to_dir(&pack_texture, &writing_directory)?; + Ok(()) } - pub fn load_from_dir(pack_dir: Arc) -> Result { + + fn async_save_data( + pack_data: LoadedPackData + ) -> Result<()> { + pack_data.dir + .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to create xmlpack directory")?; + let writing_directory = pack_data.dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + save_pack_data_to_dir( + &pack_data, + &writing_directory, + )?; + Ok(()) + } + +} + + +impl LoadedPackData { + const CORE_PACK_DIR_NAME: &'static str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; + + pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { if !pack_dir .try_exists(Self::CORE_PACK_DIR_NAME) .into_diagnostic() @@ -79,7 +202,7 @@ impl LoadedPack { let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. - let selected_categories = CategorySelection::default_from_pack_core(&core); + let selectable_categories = CategorySelection::default_from_pack_core(&core); /*** FIXME: Once this is saved properly, we can restore following block of code. @@ -119,111 +242,99 @@ impl LoadedPack { cs }); **/ - let activation_data = (if pack_dir.is_file(Self::ACTIVATION_DATA_FILE_NAME) { - match pack_dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); - Ok(LoadedPack { + Ok(LoadedPackData { + name, + uuid: core.uuid, dir: pack_dir, - core, - selected_categories, selected_files: Default::default(), - is_dirty: false, - activation_data, - current_map_data: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.source_files, + _is_dirty: false, + active_elements: Default::default(), + activation_data: Default::default(), + selectable_categories, + entities_parents: core.entities_parents, }) } + pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { + if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { + self._is_dirty = true; + true + } else { + false + } + } + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + pub fn tick( &mut self, - etx: &egui::Context, - _timestamp: f64, + b2u_sender: &std::sync::mpsc::Sender, + loop_index: u128, link: &MumbleLink, - default_tex_id: &TextureHandle, - default_trail_id: &TextureHandle, currently_used_files: &BTreeMap, - is_dirty: bool, + list_of_active_or_selected_elements_changed: bool, + map_changed: bool, + tasks: &PackTasks, + next_loaded: &mut HashSet, ) { - let is_dirty = self.is_dirty || is_dirty; - if self.is_dirty { - match self.save() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to save marker pack"); - } - } - } + /* + TODO: + need to be used for redraw from last known copy (should be an argument): + selectable_categories + active_elements + activation_data + */ + //we are in a GUI drawing, save in background. + tasks.save_data(self); //FIXME: takes a lot of time when "is_dirty" is true (i.e.: the map of things to display changes). Everythings get reloaded => how to do partial version ? - if self.current_map_data.map_id != link.map_id || is_dirty { - self.on_map_changed(etx, link, default_tex_id, default_trail_id, currently_used_files); - } - } - pub fn render( - &mut self, - _timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &MumbleLink, - next_on_screen: &mut HashSet, - ) { - let z_near = joko_renderer.get_z_near(); - for (uuid, marker) in self.current_map_data.active_markers.iter() { - //FIXME: what's the difference between a Marker and an ActiveMarker ? rename second one in something more fitting ? - if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { - joko_renderer.add_billboard(mo); - next_on_screen.insert(*uuid); - } - } - for (uuid, trail) in self.current_map_data.active_trails.iter() { - joko_renderer.add_trail(TrailObject { - vertices: trail.trail_object.vertices.clone(), - texture: trail.trail_object.texture, - }); - next_on_screen.insert(*uuid); + if map_changed || list_of_active_or_selected_elements_changed { + tasks.change_map(self, b2u_sender, link, currently_used_files); + let mut active_elements: HashSet = Default::default(); + self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); + b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); + self.active_elements = active_elements.clone(); + next_loaded.extend(active_elements); } } + fn on_map_changed( &mut self, - etx: &egui::Context, + b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, - default_tex_id: &TextureHandle, - default_trail_id: &TextureHandle, currently_used_files: &BTreeMap, - ) { - info!( - self.current_map_data.map_id, - link.map_id, "current map data is updated." - ); - self.current_map_data = Default::default(); + //selectable_categories: OrderedHashMap, + //activation_data: ActivationData, + active_elements: &mut HashSet, + ){ + /* + FIXME: + this is processing too much information + one need to process more at first load and not on map change + + ensure load of every texture regardless of status + + */ + info!(link.map_id, "current map data is updated."); if link.map_id == 0 { info!("No map do not do anything"); return; } - self.current_map_data.map_id = link.map_id; - //CategorySelection::recursive_populate_guids(&mut self.selected_categories, &mut self.core.entities_parents, None); - let selected_categories_manager = SelectedCategoryManager::new(&self.selected_categories, &self.core.categories); + debug!("Start building SelectedCategoryManager {}", self.selectable_categories.len()); + let selected_categories_manager = SelectedCategoryManager::new(&self.selectable_categories, &self.categories); - let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.core.source_files, ¤tly_used_files); + debug!("Start building SelectedFileManager"); + let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.source_files, ¤tly_used_files); - let mut failure_loading = false; + debug!("Start loading markers"); let mut nb_markers_attempt = 0; let mut nb_markers_loaded = 0; - for (index, marker) in self - .core + for (_index, marker) in self .maps .get(&link.map_id) .unwrap_or(&Default::default()) @@ -233,12 +344,14 @@ impl LoadedPack { { nb_markers_attempt += 1; if selected_files_manager.is_selected(&marker.source_file_name) { - if selected_categories_manager.is_selected(&marker.category) { - let category_attributes = selected_categories_manager.get(&marker.category); - let mut attrs = marker.attrs.clone();// why a clone ? - attrs.inherit_if_attr_none(category_attributes); + active_elements.insert(marker.guid); + active_elements.insert(marker.parent); + if selected_categories_manager.is_selected(&marker.parent) { + let category_attributes = selected_categories_manager.get(&marker.parent); + let mut common_attributes = marker.attrs.clone();// why a clone ? + common_attributes.inherit_if_attr_none(category_attributes); let key = &marker.guid; - if let Some(behavior) = attrs.get_behavior() { + if let Some(behavior) = common_attributes.get_behavior() { use crate::pack::Behavior; if match behavior { Behavior::AlwaysVisible => false, @@ -257,8 +370,8 @@ impl LoadedPack { _ => false, }) .unwrap_or_default(), - Behavior::DailyPerChar => self - .activation_data + Behavior::DailyPerChar => + self.activation_data .character .get(&link.name) .map(|a| a.contains_key(key)) @@ -283,60 +396,28 @@ impl LoadedPack { continue; } } - if let Some(tex_path) = attrs.get_icon_file() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this icon texture"); - failure_loading = true; - } - } + /* + TODO: purely make it a notification ? + + what are the pro and cons to have a map data per package + */ + if let Some(tex_path) = common_attributes.get_icon_file() { + b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); } else { - info!("no texture attribute on this marker"); + debug!("no texture attribute on this marker"); } - let th = attrs - .get_icon_file() - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - let texture_id = match th.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }; - - let max_pixel_size = attrs.get_max_size().copied().unwrap_or(2048.0); // default taco max size - let min_pixel_size = attrs.get_min_size().copied().unwrap_or(5.0); // default taco min size - self.current_map_data.active_markers.insert( - marker.guid, - ActiveMarker { - texture_id, - _texture: th.clone(), - attrs, - pos: marker.position, - max_pixel_size, - min_pixel_size, - }, - ); + nb_markers_loaded += 1; + } else { + debug!("category {} = {} is not enabled", marker.category, marker.parent); } } } + debug!("Start loading trails"); let mut nb_trails_attempt = 0; let mut nb_trails_loaded = 0; - for (index, trail) in self - .core + for (_index, trail) in self .maps .get(&link.map_id) .unwrap_or(&Default::default()) @@ -346,91 +427,40 @@ impl LoadedPack { { nb_trails_attempt += 1; if selected_files_manager.is_selected(&trail.source_file_name) { - if selected_categories_manager.is_selected(&trail.category) { - let category_attributes = selected_categories_manager.get(&trail.category); + active_elements.insert(trail.guid); + active_elements.insert(trail.parent); + if selected_categories_manager.is_selected(&trail.parent) { + let category_attributes = selected_categories_manager.get(&trail.parent); let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this trail texture"); - failure_loading = true; - } - } else { - debug!("Trail texture alreadu loaded {:?}", tex_path); - } + b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); } else { - info!("no texture attribute on this trail"); - } - let texture_path = common_attributes.get_texture(); - let th = texture_path - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_trail_id); - - let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { - debug!(?texture_path, "tbin path"); - tbin - } else { - info!(?trail, "missing tbin path"); - continue; - }; - let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { - tbin - } else { - info!(%tbin_path, "failed to find tbin"); - continue; - }; - //TODO: if iso and closed, split it as a polygon and fill it as a surface - if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( - &common_attributes, - &tbin.nodes, - th.clone(), - ) { - self.current_map_data - .active_trails - .insert(trail.guid, active_trail); - } else { - info!("Cannot display {texture_path:?}") + debug!("no texture attribute on this trail"); } nb_trails_loaded += 1; } else { - info!("category {} is not enabled", trail.category); + debug!("category {} = {} is not enabled", trail.category, trail.parent); } } } - info!("Loaded for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); + info!("Load notifications for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); debug!("active categories: {:?}", selected_categories_manager.keys()); - - if failure_loading { - info!("Error when loading textures, here are the keys:"); - for k in self.core.textures.keys() { - info!(%k); - } - info!("end of keys"); - } } + pub fn save_all(&mut self) -> Result<()> { - self.is_dirty = true; + unimplemented!("Replace by a save on both data and ui"); + /* + self._is_dirty = true; self.save() + */ } - #[tracing::instrument(skip(self))] + + /* pub fn save(&mut self) -> Result<()> { - let is_dirty = std::mem::take(&mut self.is_dirty); + let is_dirty = std::mem::take(&mut self._is_dirty); if is_dirty { - match serde_json::to_string_pretty(&self.selected_categories) { + match serde_json::to_string_pretty(&self.selectable_categories) { Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { debug!("wrote cat selections to disk after creating a default from pack"); @@ -467,10 +497,338 @@ impl LoadedPack { .into_diagnostic() .wrap_err("failed to open core pack directory")?; save_pack_core_to_dir( - &self.core, + &self, &core_dir, is_dirty, )?; Ok(()) + }*/ +} + + + +impl LoadedPackTexture { + const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; + + /*pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub fn active_elements(&self) -> HashSet { + self.current_map_data.active_elements.clone() + }*/ + + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + CategorySelection::recursive_update_active_categories(&mut self.selectable_categories, active_elements); + } + pub fn category_sub_menu( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, + ui: &mut egui::Ui, + show_only_active: bool, + ) { + //it is important to generate a new id each time to avoid collision + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + u2b_sender, + u2u_sender, + &mut self.selectable_categories, + ui, + &mut self._is_dirty, + show_only_active, + &self.late_discovery_categories + ); + }); + if self._is_dirty { + u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); + } + } + + pub fn tick( + &mut self, + u2u_sender: &std::sync::mpsc::Sender, + _timestamp: f64, + link: &MumbleLink, + //next_on_screen: &mut HashSet, + z_near: f32, + tasks: &PackTasks, + ) { + tasks.save_ui(self); + //FIXME: how to reset state correctly to only display what is necessary ? + println!("LoadedPackTexture.tick: {}-{} {}-{}", + self.current_map_data.active_markers.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.active_trails.len(), + self.current_map_data.wip_trails.len(), + ); + //FIXME: SelectedCategoryManager works with categories, not elements + let mut marker_objects = Vec::new(); + for (uuid, marker) in self.current_map_data.active_markers.iter() { + if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { + marker_objects.push(mo); + } + } + println!("LoadedPackTexture.tick: markers {}", marker_objects.len()); + u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); + let mut trail_objects = Vec::new(); + for (uuid, trail) in self.current_map_data.active_trails.iter() { + trail_objects.push(TrailObject { + vertices: trail.trail_object.vertices.clone(), + texture: trail.trail_object.texture, + }); + //next_on_screen.insert(*uuid); + } + println!("LoadedPackTexture.tick: trails {}", trail_objects.len()); + u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); + u2u_sender.send(UIToUIMessage::RenderSwapChain); + } + + pub fn swap(&mut self) { + std::mem::swap(&mut self.current_map_data.active_markers, &mut self.current_map_data.wip_markers); + std::mem::swap(&mut self.current_map_data.active_trails, &mut self.current_map_data.wip_trails); + self.current_map_data.wip_markers.clear(); + self.current_map_data.wip_trails.clear(); + } + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + marker_uuid: Uuid, + position: glam::Vec3, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this icon texture"); + } + } + let th = self.current_map_data.active_textures.get(tex_path) + .unwrap_or(default_tex_id); + let texture_id = match th.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }; + + let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size + let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size + let am = ActiveMarker { + texture_id, + _texture: th.clone(), + common_attributes, + pos: position, + max_pixel_size, + min_pixel_size, + }; + self.current_map_data + .wip_markers + .insert(marker_uuid, am); + } + + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this trail texture"); + } + } else { + debug!("Trail texture alreadu loaded {:?}", tex_path); + } + let texture_path = common_attributes.get_texture(); + let th = texture_path + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_tex_id); + + let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); + tbin + } else { + info!(?trail_uuid, "missing tbin path"); + return; + }; + let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { + tbin + } else { + info!(%tbin_path, "failed to find tbin"); + return; + }; + //TODO: if iso and closed, split it as a polygon and fill it as a surface + if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( + &common_attributes, + &tbin.nodes, + th.clone(), + ) { + self.current_map_data + .wip_trails + .insert(trail_uuid, active_trail); + } else { + info!("Cannot display {texture_path:?}") + } + + } + +} + + +pub fn load_all_from_dir(pack_dir: &Arc) -> Result<(BTreeMap, BTreeMap)>{ + pack_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to create marker manager directory")?; + let marker_manager_dir = pack_dir + .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to open marker manager directory")?; + marker_manager_dir + .create_dir_all(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to create marker packs directory")?; + let marker_packs_dir = marker_manager_dir + .open_dir(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err("failed to open marker packs dir")?; + let mut data_packs: BTreeMap = Default::default(); + let mut texture_packs: BTreeMap = Default::default(); + + + for entry in marker_packs_dir + .entries() + .into_diagnostic() + .wrap_err("failed to get entries of marker packs dir")? + { + let entry = entry.into_diagnostic()?; + if entry.metadata().into_diagnostic()?.is_file() { + continue; + } + if let Ok(name) = entry.file_name() { + let pack_dir = entry + .open_dir() + .into_diagnostic() + .wrap_err("failed to open pack entry as directory")?; + { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name, pack_dir.into()) { + Ok(lp) => { + let (data, tex) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + } + Err(e) => { + error!(?e, "failed to load pack from directory"); + } + } + drop(span_guard); + } + } + } + Ok((data_packs, texture_packs)) +} + +fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, LoadedPackTexture)> { + if !pack_dir + .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to check if pack core exists")? + { + bail!("pack core doesn't exist in this pack"); + } + let core_dir = pack_dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + Ok(build_from_core(name, pack_dir, core)) +} + + +pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { + let selectable_categories = CategorySelection::default_from_pack_core(&core); + let data = LoadedPackData { + name, + uuid: core.uuid, + dir: Arc::clone(&dir), + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.source_files, + _is_dirty: false, + activation_data: Default::default(), + active_elements: Default::default(), + selectable_categories: selectable_categories.clone(), + entities_parents: core.entities_parents, + }; + let activation_data = (if dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + match dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_default(); + let tex = LoadedPackTexture { + uuid: core.uuid, + selectable_categories, + textures: core.textures, + current_map_data: Default::default(), + _is_dirty: true, + activation_data, + dir: Arc::clone(&dir), + late_discovery_categories: core.late_discovery_categories, + name: core.name, + tbins: core.tbins, + active_elements: Default::default(), + }; + (data, tex) } + diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index bbc6dbd..fe479b1 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -2,18 +2,22 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex} }; +use glam::Vec3; use tribool::Tribool; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; use image::EncodableLayout; -use tracing::{error, info, info_span}; +use tracing::{error, info, info_span, trace}; +use joko_core::RelativePath; use jokolink::MumbleLink; use miette::{Context, IntoDiagnostic, Result}; use uuid::Uuid; +use crate::message::{UIToBackMessage, UIToUIMessage}; -use crate::manager::pack::loaded::LoadedPack; +use crate::{message::BackToUIMessage, pack::CommonAttributes}; +use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose @@ -30,39 +34,44 @@ pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility /// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture /// 4. render that marker use joko_render /// FIXME: it is a bad name, it does not manage Markers, but packages -pub struct PackageManager { - /// holds data that is useful for the ui - ui_manager: PackageUIManager, +#[must_use] +pub struct PackageDataManager { /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - _marker_manager_dir: Arc, + //_marker_manager_dir: Arc, /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. /// The name of the child directory is the name of the pack - marker_packs_dir: Arc, + pub marker_packs_dir: Arc, /// These are the marker packs /// The key is the name of the pack /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - packs: BTreeMap, - missing_texture: Option, - missing_trail: Option, + pub packs: BTreeMap, + tasks: PackTasks, + current_map_id: u32, + show_only_active: bool, /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. /// This allows us to avoid saving the pack too often. pub save_interval: f64, - all_files_tribool: Tribool, - all_files_toggle: bool, - currently_used_files: BTreeMap, + pub currently_used_files: BTreeMap, + parents: HashMap, + loaded_elements: HashSet, on_screen: BTreeSet, - is_dirty: bool } - -#[derive(Debug, Default)] -pub(crate) struct PackageUIManager { - // tf is this type supposed to be? maybe we should have used a ECS for this reason. +#[must_use] +pub struct PackageUIManager { pub import_status: Option>>, - parents: HashMap, + default_marker_texture: Option, + default_trail_texture: Option, + packs: BTreeMap, + tasks: PackTasks, + + currently_used_files: BTreeMap, + all_files_tribool: Tribool, + all_files_toggle: bool, + show_only_active: bool, } -impl PackageManager { +impl PackageDataManager { /// Creates a new instance of [MarkerManager]. /// 1. It opens the marker manager directory /// 2. loads its configuration @@ -70,199 +79,389 @@ impl PackageManager { /// 4. loads all the packs /// 5. loads all the activation data /// 6. returns self - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker manager directory")?; - let marker_manager_dir = jdir - .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker manager directory")?; - marker_manager_dir - .create_dir_all(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker packs directory")?; - let marker_packs_dir = marker_manager_dir - .open_dir(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker packs dir")?; - let mut packs: BTreeMap = Default::default(); - - - for entry in marker_packs_dir - .entries() - .into_diagnostic() - .wrap_err("failed to get entries of marker packs dir")? - { - let entry = entry.into_diagnostic()?; - if entry.metadata().into_diagnostic()?.is_file() { - continue; - } - if let Ok(name) = entry.file_name() { - let pack_dir = entry - .open_dir() - .into_diagnostic() - .wrap_err("failed to open pack entry as directory")?; - { - let span_guard = info_span!("loading pack from dir", name).entered(); - match LoadedPack::load_from_dir(pack_dir.into()) { - Ok(lp) => { - packs.insert(name, lp); - } - Err(e) => { - error!(?e, "failed to load pack from directory"); - } - } - drop(span_guard); - } - } - } - - Ok(Self { + pub fn new(packs: BTreeMap, marker_packs_dir: &Arc) -> Self { + Self { packs, - marker_packs_dir: marker_packs_dir.into(), - _marker_manager_dir: marker_manager_dir.into(), - ui_manager: PackageUIManager::new(), + tasks: PackTasks::new(), + marker_packs_dir: marker_packs_dir.clone(), + //_marker_manager_dir: marker_manager_dir.into(), + current_map_id: 0, save_interval: 0.0, - missing_texture: None, - missing_trail: None, - all_files_tribool: Tribool::True, - all_files_toggle: false, + show_only_active: true, currently_used_files: Default::default(), + parents: Default::default(), + loaded_elements: Default::default(), on_screen: Default::default(), - is_dirty: true, - }) + } } - fn pack_importer(import_status: Arc>) { - //called when a new pack is imported - rayon::spawn(move || { - *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } - if let Some(file_path) = rfd::FileDialog::new() - .add_filter("taco", &["zip", "taco"]) - .pick_file() - { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); + pub fn category_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_set(uuid, status) { + break; + } + } + } - let result = import_pack_from_zip_file_path(file_path); - match result { - Ok((name, pack)) => { - *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); - } - Err(e) => { - *import_status.lock().unwrap() = ImportStatus::PackError(e); + pub fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> HashSet + where I: Iterator + { + let iter = input.into_iter(); + let mut result: HashSet = HashSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; } + next_gen.push(p.clone()); } - } else { - *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); } - }); + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + unreachable!("The loop should always return"); } + + pub fn get_active_elements_parents(&mut self, categories_and_elements_to_be_loaded: HashSet) { + trace!("There are {} active elements", categories_and_elements_to_be_loaded.len()); + + //first merge the parents to iterate overit + let mut parents: HashMap = Default::default(); + for pack in self.packs.values_mut() { + parents.extend(pack.entities_parents.clone()); + } + self.parents = parents; + //then climb up the tree of parent's categories + self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); + } + pub fn tick( &mut self, - etx: &egui::Context, - timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, + b2u_sender: &std::sync::mpsc::Sender, + loop_index: u128, link: Option<&MumbleLink>, + choice_of_category_changed: bool, ) { - if self.missing_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.missing_texture = Some(etx.load_texture( - "default marker", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - if self.missing_trail.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/trail.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.missing_trail = Some(etx.load_texture( - "default trail", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - let mut currently_used_files: BTreeMap = Default::default(); - let mut next_on_screen: HashSet = Default::default(); + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + match link { Some(link) => { //FIXME: how to save/load the active files ? - let mut is_dirty = self.is_dirty; + //TODO: find an efficient way to propagate the file deactivation + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; for pack in self.packs.values_mut() { - if let Some(current_map) = pack.core.maps.get(&link.map_id) { + if let Some(current_map) = pack.maps.get(&link.map_id) { for marker in current_map.markers.values() { - if let Some(is_active) = pack.core.source_files.get(&marker.source_file_name) { + if let Some(is_active) = pack.source_files.get(&marker.source_file_name) { currently_used_files.insert( marker.source_file_name.clone(), - *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {is_dirty = true; is_active}) + *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) ); } } for trail in current_map.trails.values() { - if let Some(is_active) = pack.core.source_files.get(&trail.source_file_name) { + if let Some(is_active) = pack.source_files.get(&trail.source_file_name) { currently_used_files.insert( trail.source_file_name.clone(), - *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {is_dirty = true; is_active}) + *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) ); } } } } - for pack in self.packs.values_mut() { + for (uuid, pack) in self.packs.iter_mut() { + let span_guard = info_span!("Updating package status").entered(); pack.tick( - etx, - timestamp, + &b2u_sender, + loop_index, link, - self.missing_texture.as_ref().unwrap(), - self.missing_trail.as_ref().unwrap(), ¤tly_used_files, - is_dirty - ); - pack.render( - timestamp, - joko_renderer, - link, - &mut next_on_screen, + have_used_files_list_changed || choice_of_category_changed, + map_changed, + &self.tasks, + &mut categories_and_elements_to_be_loaded, ); + std::mem::drop(span_guard); + } + if map_changed { + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); + } + if map_changed || have_used_files_list_changed || choice_of_category_changed { + //there is no point in sending a new list if nothing changed + b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); + self.currently_used_files = currently_used_files; + b2u_sender.send(BackToUIMessage::TextureSwapChain); } - std::mem::take(&mut self.is_dirty); }, None => {}, }; - self.currently_used_files = currently_used_files; + //TODO: state_sender.send(BackToUIMessage::ActiveElements(active_elements)); + + //those are the elements displayed, not the categories, one would need to keep the link between the two - self.on_screen = self.update_active_elements(next_on_screen); + /*if is_one_package_reloaded { + for pack in self.packs.values() { + next_loaded.extend(pack.active_elements()); + } + info!("Loaded {} elements", next_loaded.len()); + self.loaded_elements = self.update_active_elements(next_loaded); + }*/ + //self.on_screen = self.update_active_elements(next_on_screen); } - fn update_active_elements(&mut self, on_screen: HashSet) -> BTreeSet { - let mut parents: HashMap = Default::default(); - for pack in self.packs.values() { - parents.extend(pack.core.entities_parents.clone()); + +} + + +impl PackageUIManager { + pub fn new(packs: BTreeMap) -> Self { + Self { + packs, + tasks: PackTasks::new(), + default_marker_texture: None, + default_trail_texture: None, + import_status: Default::default(), + + all_files_tribool: Tribool::True, + all_files_toggle: false, + show_only_active: true, + currently_used_files: Default::default()// UI copy to (de-)activate files + } + } + + pub fn late_init( + &mut self, + etx: &egui::Context, + ) { + if self.default_marker_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_marker_texture = Some(etx.load_texture( + "default marker", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + if self.default_trail_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/trail_rainbow.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_trail_texture = Some(etx.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + } + + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for pack in self.packs.values_mut() { + pack.update_active_categories(active_elements); + } + } + + pub fn update_pack_active_categories(&mut self, pack_uuid: Uuid, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for (uuid, pack) in self.packs.iter_mut() { + if uuid == &pack_uuid { + pack.update_active_categories(active_elements); + break; + } + } + } + pub fn swap(&mut self) { + for pack in self.packs.values_mut() { + pack.swap(); + } + } + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + self.packs + .get_mut(&pack_uuid) + .map( |pack| { + pack.load_marker_texture( + egui_context, + self.default_marker_texture.as_ref().unwrap(), + &tex_path, + marker_uuid, + position, + common_attributes, + ); + }); + } + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + self.packs + .get_mut(&pack_uuid) + .map( |pack| { + pack.load_trail_texture( + egui_context, + &self.default_trail_texture.as_ref().unwrap(), + &tex_path, + trail_uuid, + common_attributes, + ); + }); + } + + fn pack_importer(import_status: Arc>) { + //called when a new pack is imported + rayon::spawn(move || { + *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + + if let Some(file_path) = rfd::FileDialog::new() + .add_filter("taco", &["zip", "taco"]) + .pick_file() + { + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); + + let result = import_pack_from_zip_file_path(file_path); + match result { + Ok((name, pack)) => { + *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); + } + Err(e) => { + *import_status.lock().unwrap() = ImportStatus::PackError(e); + } + } + } else { + *import_status.lock().unwrap() = + ImportStatus::PackError(miette::miette!("file chooser was cancelled")); + } + }); + } + + fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); } - self.ui_manager.parents = parents; - self.ui_manager.get_parents(on_screen.iter()) } - pub fn menu_ui(&mut self, ui: &mut egui::Ui) { - //println!("Elements on screen: {:?}", self.on_screen); + + pub fn tick( + &mut self, + u2u_sender: &std::sync::mpsc::Sender, + timestamp: f64, + link: &MumbleLink, + z_near: f32, + ) { + trace!("nb packs: {}", self.packs.len()); + for (uuid, pack) in self.packs.iter_mut() { + let span_guard = info_span!("Updating package status").entered(); + pack.tick( + &u2u_sender, + timestamp, + link, + //&mut next_on_screen, + z_near, + &self.tasks + ); + std::mem::drop(span_guard); + } + u2u_sender.send(UIToUIMessage::Present); + } + + pub fn menu_ui( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, + ui: &mut egui::Ui + ) { ui.menu_button("Markers", |ui| { + if self.show_only_active { + if ui.button("Show everything").clicked() { + self.show_only_active = false; + } + } else { + if ui.button("Show only active").clicked() { + self.show_only_active = true; + } + } + if ui.button("Activate all elements").clicked() { + self.category_set_all(true); + u2b_sender.send(UIToBackMessage::CategorySetAll(true)); + } + if ui.button("Deactivate all elements").clicked() { + self.category_set_all(false); + u2b_sender.send(UIToBackMessage::CategorySetAll(false)); + } + for pack in self.packs.values_mut() { - pack.category_sub_menu(ui, &self.on_screen); + //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; + //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active); } + }); - + if self.tasks.is_running() { + ui.spinner(); + } } - fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool, link: Option<&MumbleLink>) { + + fn gui_file_manager( + &mut self, + event_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + open: &mut bool, + link: Option<&MumbleLink> + ) { + let mut files_changed = false; Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { egui::ScrollArea::vertical().show(ui, |ui| { egui::Grid::new("link grid") @@ -281,7 +480,7 @@ impl PackageManager { for file in self.currently_used_files.iter_mut() { let cb = ui.checkbox(file.1, file.0.clone()); if cb.changed() { - self.is_dirty = true; + files_changed = true; } if ui.button("Edit").clicked() { println!("click {}", file.0.clone()); @@ -293,40 +492,45 @@ impl PackageManager { }); Ok(()) }); + if files_changed { + event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); + } } - fn gui_package_loader(&mut self, etx: &egui::Context, open: &mut bool) { + fn gui_package_loader( + &mut self, + event_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + open: &mut bool + ) { Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { let mut delete = vec![]; - for pack in self.packs.keys() { - ui.label(pack); + for pack in self.packs.values() { + ui.label(pack.name.clone()); if ui.button("delete").clicked() { - delete.push(pack.clone()); + delete.push(pack.uuid); } } - for pack_name in delete { - self.packs.remove(&pack_name); - if let Err(e) = self.marker_packs_dir.remove_dir_all(&pack_name) { - error!(?e, pack_name,"failed to remove pack"); - } else { - info!("deleted marker pack: {pack_name}"); - } + if !delete.is_empty() { + //TODO: send message to background thread, UIToBackMessage::DeletePack + event_sender.send(UIToBackMessage::DeletePacks(delete)); } }); }); - if self.ui_manager.import_status.is_some() { + if self.import_status.is_some() { if ui.button("clear").on_hover_text( "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - self.ui_manager.import_status = None; + self.import_status = None; } } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + //TODO: send message to background thread, UIToBackMessage::ImportPack let import_status = Arc::new(Mutex::default()); - self.ui_manager.import_status = Some(import_status.clone()); + self.import_status = Some(import_status.clone()); Self::pack_importer(import_status); } - if let Some(import_status) = self.ui_manager.import_status.as_ref() { + if let Some(import_status) = self.import_status.as_ref() { if let Ok(mut status) = import_status.lock() { match &mut *status { ImportStatus::UnInitialized => { @@ -347,36 +551,8 @@ impl PackageManager { ui.label("choose a pack name: "); ui.text_edit_singleline(name); }); - let name = name.as_str(); if ui.button("save").clicked() { - - if self.marker_packs_dir.exists(name) { - self.marker_packs_dir - .remove_dir_all(name) - .into_diagnostic()?; - } - if let Err(e) = self.marker_packs_dir.create_dir_all(name) { - error!(?e, "failed to create directory for pack"); - - } - match self.marker_packs_dir.open_dir(name) { - Ok(dir) => { - let core = std::mem::take(pack); - let mut loaded_pack = LoadedPack::new(core, dir.into()); - match loaded_pack.save_all() { - Ok(_) => { - self.packs.insert(name.to_string(), loaded_pack); - *saved = true; - }, - Err(e) => { - error!(?e, "failed to save marker pack"); - }, - } - }, - Err(e) => { - error!(?e, "failed to open marker pack directory to save pack"); - } - }; + event_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); } } else { ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); @@ -397,61 +573,16 @@ impl PackageManager { } pub fn gui( &mut self, + event_sender: &std::sync::mpsc::Sender, etx: &egui::Context, is_marker_open: &mut bool, is_file_open: &mut bool, timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, link: Option<&MumbleLink> ) { - self.gui_package_loader(etx, is_marker_open); - self.gui_file_manager(etx, is_file_open, link); -} -} - -impl PackageUIManager { - pub fn new() -> Self { - Self{ - import_status: Default::default(), - parents: Default::default() - } - } - - pub fn register(&mut self, element: Uuid, parent: Uuid) { - self.parents.insert(element, parent); - } - pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { - self.parents.get(element) - } - pub fn get_parents<'a, I>(&self, input: I) -> BTreeSet - where I: Iterator - { - let iter = input.into_iter(); - let mut result: BTreeSet = BTreeSet::new(); - let mut current_generation: Vec = Vec::new(); - for elt in iter { - current_generation.push(*elt) - } - //info!("starts with {}", current_generation.len()); - loop { - if current_generation.is_empty() { - //info!("ends with {}", result.len()); - return result; - } - let mut next_gen: Vec = Vec::new(); - for elt in current_generation.iter() { - if let Some(p) = self.get_parent(elt) { - if result.contains(p) { - //avoid duplicate, redundancy or loop - continue; - } - next_gen.push(p.clone()); - } - } - let to_insert = std::mem::replace(&mut current_generation, next_gen); - result.extend(to_insert); - } - unreachable!("The loop should always return"); + self.gui_package_loader(event_sender, etx, is_marker_open); + self.gui_file_manager(event_sender, etx, is_file_open, link); } } + diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs new file mode 100644 index 0000000..546752c --- /dev/null +++ b/crates/joko_marker_format/src/message.rs @@ -0,0 +1,75 @@ +use std::hash::Hash; +use std::sync::Arc; +use std::collections::{BTreeMap, HashSet}; + +use uuid::Uuid; + +use glam::{Vec2, Vec3}; + +use jokolink::MumbleLink; +use joko_core::RelativePath; + +use crate::{pack::{CommonAttributes, PackCore}, LoadedPackTexture}; + + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct MarkerVertex { + pub position: Vec3, + pub alpha: f32, + pub texture_coordinates: Vec2, + pub fade_near_far: Vec2, + pub color: [u8; 4], +} + +#[derive(Debug)] +pub struct MarkerObject { + /// The six vertices that make up the marker quad + pub vertices: [MarkerVertex; 6], + /// The (managed) texture id from egui data + pub texture: u64, + /// The distance from camera + /// As markers have transparency, we need to render them from far -> near order + /// So, we will sort them using this distance just before rendering + pub distance: f32, +} + +#[derive(Debug, Clone)] +pub struct TrailObject { + pub vertices: Arc<[MarkerVertex]>, + pub texture: u64, +} + +pub enum BackToUIMessage { + ActiveElements(HashSet),//list of all elements that are loaded for current map + CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display + LoadedPack(LoadedPackTexture),//push a loaded pack to UI + Loading, + MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), + MumbleLink(Option), + MumbleLinkChanged,//tell there is a need to resize + PackageActiveElements(Uuid, HashSet),// first is the package reference, second is the list of active elements in the package. + TextureSwapChain,// The list of texture to load was changed, will be soon followed by a RenderSwapChain + TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), +} + +pub enum UIToBackMessage { + ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. + CategoryActivationStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationStatusChanged,//something happened that needs to reload the whole set + CategorySetAll(bool),//signal all categories should be now at this status + DeletePacks(Vec),//uuid of the pack to delete + ImportPack, + ReloadPack, + SavePack(String, PackCore), +} + +pub enum UIToUIMessage { + BulkMarkerObject(Vec), + BulkTrailObject(Vec), + Present,// a render loop is finished and we can present it + MarkerObject(MarkerObject), + RenderSwapChain,// The list of elements to display was changed + TrailObject(TrailObject), +} + diff --git a/crates/joko_marker_format/src/pack/common.rs b/crates/joko_marker_format/src/pack/common.rs index 66de647..aea6dff 100644 --- a/crates/joko_marker_format/src/pack/common.rs +++ b/crates/joko_marker_format/src/pack/common.rs @@ -378,7 +378,7 @@ macro_rules! setters_for_bool_attributes { common_attributes_struct_macro!( /// the struct we use for inheritance from category/other markers. #[derive(Debug, Clone, Default)] - pub(crate) struct CommonAttributes { + pub struct CommonAttributes { /// An ID for an achievement from the GW2 API. Markers with the corresponding achievement ID will be hidden if the ID is marked as "done" for the API key that's entered in TacO. achievement_id: u32, /// This is similar to achievementId, but works for partially completed achievements as well, if the achievement has "bits", they can be individually referenced with this. diff --git a/crates/joko_marker_format/src/pack/marker.rs b/crates/joko_marker_format/src/pack/marker.rs index 428aa2d..79c6898 100644 --- a/crates/joko_marker_format/src/pack/marker.rs +++ b/crates/joko_marker_format/src/pack/marker.rs @@ -2,12 +2,14 @@ use super::CommonAttributes; use glam::Vec3; use uuid::Uuid; + #[derive(Debug, Clone)] pub(crate) struct Marker { pub guid: Uuid, + pub parent: Uuid, pub position: Vec3, pub map_id: u32, pub category: String, - pub attrs: CommonAttributes, pub source_file_name: String, + pub attrs: CommonAttributes, } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index 08e9d18..a2a4871 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -3,68 +3,111 @@ mod marker; mod trail; mod route; -use std::{collections::{HashMap, HashSet}, str::FromStr}; +use std::collections::{HashMap, HashSet}; use indexmap::IndexMap; -use ordered_hash_map; +use ordered_hash_map::OrderedHashMap; -use tracing::info; +use tracing::{info, debug}; +use joko_core::RelativePath; pub use common::*; pub(crate) use marker::*; -use smol_str::SmolStr; pub(crate) use trail::*; pub(crate) use route::*; use uuid::Uuid; #[derive(Default, Debug, Clone)] -pub(crate) struct PackCore { - pub textures: ordered_hash_map::OrderedHashMap>, - pub tbins: ordered_hash_map::OrderedHashMap, - pub categories: IndexMap, +pub struct PackCore { + /* + TODO: + this is a temporary holder of data + it might be deleted at some point to avoid copy of data => into parts, break it down as fields and reassemble those. + it mean the "new" functions have to be rewritten to take those fields instead of a PackCore as a whole. + The "new" functions might even be removed to not be able to build them independantly. + Once built the UI version would be sent to UI. + */ + pub name: String, + pub uuid: Uuid, + pub textures: OrderedHashMap>, + pub tbins: OrderedHashMap, + pub categories: IndexMap, pub all_categories: HashMap, + pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. pub entities_parents: HashMap, - pub source_files: ordered_hash_map::OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: ordered_hash_map::OrderedHashMap, + pub source_files: OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: OrderedHashMap, } + impl PackCore { + pub fn category_exists(&self, full_category_name: &String) -> bool { + self.all_categories.contains_key(full_category_name) + } + + pub fn get_category_uuid(&mut self, full_category_name: &String) -> Option<&Uuid> { + self.all_categories.get(full_category_name) + } + + pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { + if let Some(category_uuid) = self.all_categories.get(full_category_name) { + category_uuid.clone() + } else { + //TODO: if import is "dirty", create missing category + //default import mode is "strict" (get inspiration from HTML modes) + debug!("There is no defined category for {}", full_category_name); + + let mut n = 0; + let mut last_uuid: Option = None; + while let Some(category_name) = prefix_until_nth_char(&full_category_name, '.', n) { + n += 1; + if let Some(parent_uuid) = self.all_categories.get(&category_name) { + //FIXME: might want to make the difference between impacted parents and actual missing category + self.late_discovery_categories.insert(*parent_uuid); + last_uuid = Some(*parent_uuid); + } else { + let new_uuid = Uuid::new_v4(); + debug!("Partial create missing parent category: {} {}", category_name, new_uuid); + self.all_categories.insert(category_name.clone(), new_uuid); + self.late_discovery_categories.insert(new_uuid); + last_uuid = Some(new_uuid); + } + } + info!("{} uuid: {:?}", full_category_name, last_uuid); + assert!(last_uuid.is_some()); + last_uuid.unwrap() + } + } + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) { if let Some(parent_uuid) = self.all_categories.get(full_category_name) { self.entities_parents.insert(*uuid, *parent_uuid); } else { //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - info!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); + debug!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); } } pub fn register_categories(&mut self) { let mut entities_parents: HashMap = Default::default(); let mut all_categories: HashMap = Default::default(); - self.recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories, None); - self.entities_parents.extend(entities_parents); + Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); info!("Catepories registered: {}", all_categories.len()); + self.entities_parents.extend(entities_parents); self.all_categories = all_categories; } fn recursive_register_categories( - &self, entities_parents: &mut HashMap, - categories: &IndexMap, + categories: &IndexMap, all_categories: &mut HashMap, - parent_name: Option ) { - for (name, cat) in categories.iter() { - let full_category_name: String = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, name) - } else { - name.to_string() - }; - //println!("Register catepory {} {} {:?}", full_category_name, cat.guid, cat.parent); - all_categories.insert(full_category_name.clone(), cat.guid); + for (_, cat) in categories.iter() { + debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); + all_categories.insert(cat.full_category_name.clone(), cat.guid); if let Some(parent) = cat.parent { entities_parents.insert(cat.guid, parent); } - self.recursive_register_categories(entities_parents, &cat.children, all_categories, Some(full_category_name)); + Self::recursive_register_categories(entities_parents, &cat.children, all_categories); } } } @@ -76,93 +119,204 @@ pub(crate) struct MapData { pub trails: IndexMap, } +#[derive(Debug, Clone)] +pub(crate) struct RawCategory { + pub guid: Uuid, + pub parent_name: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, +} + #[derive(Debug, Clone)] pub(crate) struct Category { pub guid: Uuid, pub parent: Option, pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub children: IndexMap, + pub children: IndexMap, } -/// This newtype is used to represents relative paths in marker packs -/// 1. It won't start with `/` or `C:` like roots, because its a relative path -/// 2. It can be empty to represent current directory -/// 3. No expansion of special characters like `.` or `..` stuff. -/// 4. It is always lowercase to avoid platform specific quirks. -/// 5. It will use `/` as the path separator. -/// 6. It doesn't mean that the path is valid. It may contain many of the utf-8 characters which are not valid path names on linux/windows -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RelativePath(SmolStr); -#[allow(unused)] -impl RelativePath { - pub fn join_str(&self, path: &str) -> Self { - let path = path.trim_start_matches('/'); - if path.is_empty() { - return Self(self.0.clone()); - } - let lower_case = path.to_lowercase(); - if self.0.is_empty() { - // no need to push `/` if we are empty, as that would make it an absolute path - return Self(lower_case.into()); - } - - let mut new = self.0.to_string(); - if !self.0.ends_with('/') { - new.push('/'); - } - new.push_str(&lower_case); - Self(new.into()) - } - pub fn ends_with(&self, ext: &str) -> bool { - self.0.ends_with(ext) - } - pub fn is_png(&self) -> bool { - self.ends_with(".png") - } - pub fn is_tbin(&self) -> bool { - self.ends_with(".trl") - } - pub fn is_xml(&self) -> bool { - self.ends_with(".xml") - } - pub fn is_dir(&self) -> bool { - self.ends_with("/") - } - pub fn parent(&self) -> Option<&str> { - let path = self.0.trim_end_matches('/'); - if path.is_empty() { - return None; - } - path.rfind('/').map(|index| &path[..=index]) - } - pub fn as_str(&self) -> &str { - &self.0 - } +pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { + let res = s.match_indices(pat) + .nth(n) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_until_nth_char {} {} {:?}", s, n, res); + res } -impl std::fmt::Display for RelativePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } +pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { + let nb_matches = s.matches(pat).count(); + assert!(nb_matches + 1 > n); + let res = s.split(pat) + .nth(n) + ; + debug!("nth_chunk {} {} {:?}", s, n, res); + res.unwrap().to_string() } -impl From for String { - fn from(val: RelativePath) -> String { - val.0.into() - } +pub fn prefix_parent(s: &str, pat: char) -> Option { + let n = s.matches(pat).count(); + assert!(n > 0); + let res = s.match_indices(pat) + .nth(n - 1) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_parent {} {} {:?}", s, n, res); + res } -impl FromStr for RelativePath { - type Err = &'static str; - fn from_str(s: &str) -> Result { - let path = s.trim_start_matches('/'); - if path.is_empty() { - return Ok(Self::default()); +impl Category { + // Required method + pub fn from(value: &RawCategory, parent: Option) -> Self { + Self { + guid: value.guid.clone(), + props: value.props.clone(), + separator: value.separator, + default_enabled: value.default_enabled, + display_name: value.display_name.clone(), + relative_category_name: value.relative_category_name.clone(), + full_category_name: value.full_category_name.clone(), + parent: parent, + children: Default::default() + } + } + pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + for (_, cat) in categories { + if &cat.guid == uuid { + return Some(cat); + } + let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); + if sub_res.is_some() { + return sub_res; + } } - Ok(Self(path.to_lowercase().into())) + return None; } + pub fn reassemble( + input_first_pass_categories: &OrderedHashMap, + late_discovered_categories: &mut HashSet, + ) -> IndexMap { + let mut first_pass_categories = input_first_pass_categories.clone(); + let mut second_pass_categories: OrderedHashMap = Default::default(); + let mut need_a_pass: bool = true; + + let mut third_pass_categories: IndexMap = Default::default(); + let mut third_pass_categories_ref: Vec = Default::default(); + let mut root: IndexMap = Default::default(); + while need_a_pass { + need_a_pass = false; + for (key, value) in first_pass_categories.iter() { + debug!("reassemble_categories {:?}", value); + let mut to_insert = value.clone(); + if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { + let mut n = 0; + let mut last_name: Option = None; + // This is an almost duplication of code of pack/mod.rs + while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { + debug!("{} {}", parent_name, n); + if let Some(parent_category) = first_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + }else{ + let new_uuid = Uuid::new_v4(); + let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); + let to_insert = RawCategory { + default_enabled: value.default_enabled, + guid: new_uuid, + relative_category_name: relative_category_name.clone(), + display_name: relative_category_name.clone(), + parent_name: prefix_until_nth_char(&parent_name, '.', n-1), + props: value.props.clone(), + separator: false, + full_category_name: parent_name.clone() + }; + last_name = Some(to_insert.full_category_name.clone()); + second_pass_categories.insert(parent_name.clone(), to_insert); + late_discovered_categories.insert(new_uuid); + need_a_pass = true; + } + n += 1; + } + late_discovered_categories.insert(value.guid); + to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + to_insert.display_name = to_insert.relative_category_name.clone(); + debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); + assert!(last_name.is_some()); + to_insert.parent_name = last_name; + } else { + to_insert.parent_name = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.full_category_name.clone()) + } else { + None + } + }else { + None + }; + debug!("insert as is {:?}", to_insert); + } + second_pass_categories.insert(key.clone(), to_insert); + } + if need_a_pass { + std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); + second_pass_categories.clear(); + } + } + for (key, value) in second_pass_categories { + let parent = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.guid.clone()) + } else { + None + } + } else { + None + }; + + debug!("{} parent is {:?}", key , parent); + let cat = Category::from(&value, parent); + let ref_uuid = cat.guid.clone(); + if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { + third_pass_categories_ref.push(ref_uuid); + } + } + + for full_category_name in third_pass_categories_ref { + if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { + if let Some(parent) = cat.parent { + //FIXME: this only look for top level + if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else { + panic!("Could not find parent {} for {:?}", parent, cat); + } + } else { + root.insert(cat.guid.clone(), cat); + } + } else { + panic!("Some bad logic at works"); + } + } + debug!("reassemble_categories {:?}", root); + root + } + + } + diff --git a/crates/joko_marker_format/src/pack/route.rs b/crates/joko_marker_format/src/pack/route.rs index 6815f07..e10d42b 100644 --- a/crates/joko_marker_format/src/pack/route.rs +++ b/crates/joko_marker_format/src/pack/route.rs @@ -4,6 +4,7 @@ use glam::Vec3; #[derive(Debug, Clone)] pub(crate) struct Route { pub category: String, + pub parent: Uuid, pub path: Vec, pub reset_position: Vec3, pub reset_range: f64, diff --git a/crates/joko_marker_format/src/pack/trail.rs b/crates/joko_marker_format/src/pack/trail.rs index 02dc650..71908f1 100644 --- a/crates/joko_marker_format/src/pack/trail.rs +++ b/crates/joko_marker_format/src/pack/trail.rs @@ -5,6 +5,7 @@ use super::CommonAttributes; #[derive(Debug, Clone)] pub(crate) struct Trail { pub guid: Uuid, + pub parent: Uuid, pub map_id: u32, pub category: String, pub props: CommonAttributes, diff --git a/crates/joko_marker_format/src/pack/trail_rainbow.png b/crates/joko_marker_format/src/pack/trail_rainbow.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3ff6d305a60bb6a05c6418b6417c832ac14c16 GIT binary patch literal 16987 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 literal 0 HcmV?d00001 diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml index 81e3d6f..dfaa519 100644 --- a/crates/joko_render/Cargo.toml +++ b/crates/joko_render/Cargo.toml @@ -1,3 +1,5 @@ +# Define all structures that can be sent through asynchronous messages + [package] name = "joko_render" version = "0.2.1" @@ -6,13 +8,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -egui_render_three_d = { version = "*" } -egui_window_glfw_passthrough = { version = "0.8" } -bytemuck = { version = "1", default-features = false } -jokolink = { path = "../jokolink" } +bytemuck = { workspace = true } glam = { workspace = true, features = ["bytemuck"] } tracing = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } egui = { workspace = true } -raw-window-handle = { version = "0.5" } +egui_render_three_d = { version = "*" } +egui_window_glfw_passthrough = { version = "0.8" } + + +jokolink = { path = "../jokolink" } +joko_marker_format = { path = "../joko_marker_format" } diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 58b1489..2745056 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -5,7 +5,7 @@ use egui_render_three_d::{ three_d::{context::*, Context, HasContext}, GpuTexture, }; -use glam::{Vec2, Vec3}; +use joko_marker_format::message::{MarkerVertex, MarkerObject, TrailObject}; use tracing::{error, info, warn}; use crate::gl_error; @@ -14,15 +14,14 @@ const MARKER_VERTEX_STRIDE: i32 = std::mem::size_of::() as _; pub struct BillBoardRenderer { pub markers: Vec, pub trails: Vec, + pub markers_wip: Vec,//work in progress: this is where the markers are inserted + pub trails_wip: Vec,//work in progress: this is where the markers are inserted marker_program: NativeProgram, vao: NativeVertexArray, vb: NativeBuffer, trail_buffers: Vec, } -pub struct TrailObject { - pub vertices: Arc<[MarkerVertex]>, - pub texture: u64, -} + const MARKER_VS: &str = include_str!("../shaders/marker.vs"); const MARKER_FS: &str = include_str!("../shaders/marker.fs"); @@ -64,17 +63,23 @@ impl BillBoardRenderer { Self { markers: Vec::new(), marker_program, + markers_wip: Vec::new(), vb, trails: Vec::new(), + trails_wip: Vec::new(), trail_buffers: Default::default(), vao, } } } - pub fn prepare_frame(&mut self) { - self.markers.clear(); - self.trails.clear(); + + pub fn swap(&mut self) { + std::mem::swap(&mut self.markers, &mut self.markers_wip); + std::mem::swap(&mut self.trails, &mut self.trails_wip); + self.markers_wip.clear(); + self.trails_wip.clear(); } + pub fn prepare_render_data(&mut self, gl: &Context) { unsafe { gl_error!(gl); @@ -169,26 +174,7 @@ impl BillBoardRenderer { } } -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct MarkerVertex { - pub position: Vec3, - pub alpha: f32, - pub texture_coordinates: Vec2, - pub fade_near_far: Vec2, - pub color: [u8; 4], -} -pub struct MarkerObject { - /// The six vertices that make up the marker quad - pub vertices: [MarkerVertex; 6], - /// The (managed) texture id from egui data - pub texture: u64, - /// The distance from camera - /// As markers have transparency, we need to render them from far -> near order - /// So, we will sort them using this distance just before rendering - pub distance: f32, -} /// takes in strings containing vertex/fragment shaders and returns a Shaderprogram with them attached #[tracing::instrument(skip(gl))] diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index 5f83bd8..5c193a3 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -1,7 +1,5 @@ pub mod billboard; use billboard::BillBoardRenderer; -use billboard::MarkerObject; -use billboard::TrailObject; use egui_render_three_d::three_d; use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; @@ -17,6 +15,9 @@ use glam::Mat4; use jokolink::MumbleLink; use three_d::prelude::*; + +use joko_marker_format::message::{MarkerObject, TrailObject}; + #[macro_export] macro_rules! gl_error { ($gl:expr) => {{ @@ -76,12 +77,15 @@ impl JokoRenderer { cam_pos: Default::default(), } } - pub fn get_z_near(&self) -> f32 { + pub fn get_z_near() -> f32 { 1.0 } - pub fn get_z_far(&self) -> f32 { + pub fn get_z_far() -> f32 { 1000.0 } + pub fn swap(&mut self) { + self.billboard_renderer.swap(); + } pub fn tick(&mut self, link: Option<&MumbleLink>) { if let Some(link) = link { let center = link.cam_pos + link.f_camera_front; @@ -91,16 +95,16 @@ impl JokoRenderer { center.to_array().into(), Vector3::unit_y(), Rad(link.fov), - self.get_z_near(), - self.get_z_far(), + Self::get_z_near(), + Self::get_z_far(), ); self.camera = camera; let view = Mat4::look_at_lh(link.cam_pos, center, glam::Vec3::Y); let proj = Mat4::perspective_lh( link.fov, self.viewport.aspect(), - self.get_z_near(), - self.get_z_far(), + Self::get_z_near(), + Self::get_z_far(), ); self.view_proj = proj * view; self.cam_pos = link.cam_pos; @@ -109,14 +113,22 @@ impl JokoRenderer { self.has_link = false; } } + pub fn set_billboard(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip = marker_objects; + } pub fn add_billboard(&mut self, marker_object: MarkerObject) { - self.billboard_renderer.markers.push(marker_object); + self.billboard_renderer.markers_wip.push(marker_object); + } + + pub fn set_trail(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip = trail_objects; } pub fn add_trail(&mut self, trail_object: TrailObject) { - self.billboard_renderer.trails.push(trail_object); + self.billboard_renderer.trails_wip.push(trail_object); } + pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - self.billboard_renderer.prepare_frame(); + //FIXME: have a double buffering self.gl.prepare_frame(latest_framebuffer_size_getter); unsafe { let gl = self.gl.context.clone(); diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index 952cd4d..bfb4f1d 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -17,28 +17,15 @@ joko_core = { path = "../joko_core" } joko_render = { path = "../joko_render" } jmf = { path = "../joko_marker_format", package = "joko_marker_format" } jokolink = { path = "../jokolink" } -url = { workspace = true, features = ["serde"] } egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users # and `Dir` from cap-dirs doesn't allow us to get the path. cap-directories = { workspace = true } cap-std = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "time", -] } # for ErrorLayer -tracing-appender = { version = "*" } miette = { workspace = true } egui = { workspace = true, features = ["serde"] } -egui_extras = { workspace = true } -ringbuffer = { workspace = true } rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -indexmap = { workspace = true } -rfd = { workspace = true } -glam = { workspace = true } -# sea-orm ={ version = "*", features = ["sqlx-sqlite"]} + diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 1f3e591..a9eb1ab 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,39 +1,61 @@ -use std::sync::Arc; +use std::{ops::DerefMut, sync::{mpsc::channel, Arc, Mutex}, thread}; use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; mod wm; use init::get_jokolay_dir; -use jmf::PackageManager; +use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; //use jmf::FileManager; use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; +use jmf::message::BackToUIMessage; use joko_render::JokoRenderer; -use jokolink::{MumbleChanges, MumbleManager}; -use miette::{Context, Result}; -use tracing::{error, info}; - -#[allow(unused)] -pub struct Jokolay { +use jokolink::{MumbleChanges, MumbleLink, MumbleManager, mumble_gui}; +use miette::{Context, IntoDiagnostic, Result}; +use tracing::{error, info, info_span, span}; +use jmf::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; + +#[derive(Clone)] +struct JokolayState { + link: Option, + window_changed: bool, + choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI + list_of_textures_changed: bool//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display +} +struct JokolayApp { + mumble_manager: MumbleManager, + package_manager: PackageDataManager, +} +struct JokolayGui { frame_stats: wm::WindowStatistics, - jdir: Arc, menu_panel: MenuPanel, - mumble_manager: MumbleManager, - package_manager: PackageManager, - theme_manager: ThemeManager, joko_renderer: JokoRenderer, egui_context: egui::Context, glfw_backend: GlfwBackend, + theme_manager: ThemeManager, + package_manager: PackageUIManager, } +#[allow(unused)] +pub struct Jokolay { + jdir: Arc, + gui: Arc>, + app: Arc>>, + state: Arc>>, +} + impl Jokolay { pub fn new(jdir: Arc) -> Result { - let mumble = + //TODO: we could have two mumble_manager, one for UI, one for backend, each keeping its own copy + //this would allow overwriting from gui to back ? + //if we want to be able to edit the link, one need to put a "form submission" logic. + let mumble_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - let marker_manager = - PackageManager::new(&jdir).wrap_err("failed to create marker manager")?; - let mut theme_manager = - ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; + + let (data_packages, texture_packages) = load_all_from_dir(&jdir).wrap_err("failed to load packages")?; + let mut package_data_manager = PackageDataManager::new(data_packages, &jdir); + let mut package_ui_manager = PackageUIManager::new(texture_packages); + let mut theme_manager = ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); @@ -57,34 +79,300 @@ impl Jokolay { glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); + let frame_stats = wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _); + + let mut menu_panel = MenuPanel::default(); + menu_panel.show_theme_window = true; + menu_panel.show_package_manager_window = true; + + package_ui_manager.late_init(&egui_context); Ok(Self { - mumble_manager: mumble, - package_manager: marker_manager, - frame_stats: wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _), - joko_renderer, - glfw_backend, jdir, - egui_context, - theme_manager, - menu_panel: MenuPanel::default(), + gui: Arc::new(Mutex::new(JokolayGui { + frame_stats, + joko_renderer, + glfw_backend, + egui_context, + menu_panel, + theme_manager, + package_manager: package_ui_manager + })), + app: Arc::new(Mutex::new(Box::new(JokolayApp{ + mumble_manager, + package_manager: package_data_manager, + }))), + state: Arc::new(Mutex::new(Box::new(JokolayState{ + link: None, + window_changed: true, + choice_of_category_changed: false, + list_of_textures_changed: false, + }))) }) } + + fn start_background_loop( + app: Arc>>, + state: Arc>>, + b2u_sender: std::sync::mpsc::Sender, + u2b_receiver: std::sync::mpsc::Receiver, + ) { + let background_thread = std::thread::spawn(move || { + Self::background_loop(Arc::clone(&app), Arc::clone(&state), b2u_sender, u2b_receiver); + }); + } + + fn handle_u2b_message( + package_manager: &mut PackageDataManager, + local_state: &mut JokolayState, + state_sender: &std::sync::mpsc::Sender, + msg: UIToBackMessage + ) { + match msg { + UIToBackMessage::ActiveFiles(currently_used_files) => { + tracing::trace!("Handling of UIToBackMessage::ActiveFiles"); + package_manager.set_currently_used_files(currently_used_files); + } + UIToBackMessage::CategoryActivationStatusChange(category_uuid, status) => { + tracing::trace!("Handling of UIToBackMessage::CategoryActivationStatusChange"); + package_manager.category_set(category_uuid, status); + } + UIToBackMessage::CategoryActivationStatusChanged => { + tracing::trace!("Handling of UIToBackMessage::CategoryActivationStatusChanged"); + local_state.choice_of_category_changed = true; + } + UIToBackMessage::CategorySetAll(status) => { + tracing::trace!("Handling of UIToBackMessage::CategorySetAll"); + package_manager.category_set_all(status); + local_state.choice_of_category_changed = true; + } + UIToBackMessage::DeletePacks(to_delete) => { + tracing::trace!("Handling of UIToBackMessage::DeletePacks"); + for pack_uuid in to_delete { + let pack = package_manager.packs.remove(&pack_uuid).unwrap(); + if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) { + error!(?e, pack.name,"failed to remove pack"); + } else { + info!("deleted marker pack: {}", pack.name); + } + } + } + UIToBackMessage::ImportPack => { + unimplemented!("Handling of UIToBackMessage::ImportPack has not been implemented yet"); + } + UIToBackMessage::ReloadPack => { + unimplemented!("Handling of UIToBackMessage::ReloadPack has not been implemented yet"); + } + UIToBackMessage::SavePack(name, pack) => { + //TODO: send message to background thread, UIToBackMessage::SavePack + let name = name.as_str(); + match package_manager.marker_packs_dir.open_dir(name) { + Ok(dir) => { + let (mut data_pack, texture_pack) = build_from_core(name.to_string(), dir.into(), pack); + match data_pack.save_all() { + Ok(_) => { + package_manager.packs.insert(data_pack.uuid, data_pack); + }, + Err(e) => { + error!(?e, "failed to save marker pack"); + }, + } + state_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + }, + Err(e) => { + error!(?e, "failed to open marker pack directory to save pack"); + } + }; + tracing::trace!("Handling of UIToBackMessage::SavePack"); + } + _ => { + unimplemented!("Handling BackToUIMessage has not been implemented yet"); + } + } + } + fn background_loop( + mut app: Arc>>, + state: Arc>>, + b2u_sender: std::sync::mpsc::Sender, + u2b_receiver: std::sync::mpsc::Receiver, + ) { + tracing::info!("entering background event loop"); + let span_guard = info_span!("background event loop").entered(); + let mut local_state = state.lock().unwrap().as_mut().clone(); + let mut loop_index: u128 = 0; + let mut nb_messages: u128 = 0; + loop { + tracing::trace!("background loop tick: {} {}", loop_index, nb_messages); + let mut app = app.lock().unwrap(); + let JokolayApp { + mumble_manager, + package_manager + } = &mut app.deref_mut().as_mut(); + while let Ok(msg) = u2b_receiver.try_recv() { + Self::handle_u2b_message(package_manager, &mut local_state,&b2u_sender, msg); + nb_messages += 1; + } + let link = match mumble_manager.tick() { + Ok(ml) => { + if let Some(link) = ml { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + b2u_sender.send(BackToUIMessage::MumbleLinkChanged); + } + } + ml + }, + Err(e) => { + error!(?e, "mumble manager tick error"); + None + } + }; + println!("choice_of_category_changed: {}", local_state.choice_of_category_changed); + package_manager.tick( + &b2u_sender, + loop_index, + link, + local_state.choice_of_category_changed + ); + local_state.choice_of_category_changed = false; + + let link_clone = link.cloned(); + b2u_sender.send(BackToUIMessage::MumbleLink(link_clone)); + thread::sleep(std::time::Duration::from_millis(10)); + loop_index += 1; + } + drop(span_guard); + } + + fn handle_u2u_message( + gui: &mut JokolayGui, + state: &mut JokolayState, + msg: UIToUIMessage + ) { + match msg { + UIToUIMessage::BulkMarkerObject(marker_objects) => { + tracing::trace!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); + gui.joko_renderer.set_billboard(marker_objects); + } + UIToUIMessage::BulkTrailObject(trail_objects) => { + tracing::trace!("Handling of UIToUIMessage::BulkTrailObject"); + gui.joko_renderer.set_trail(trail_objects); + } + UIToUIMessage::MarkerObject(mo) => { + tracing::trace!("Handling of UIToUIMessage::MarkerObject"); + gui.joko_renderer.add_billboard(mo); + } + UIToUIMessage::TrailObject(to) => { + tracing::trace!("Handling of UIToUIMessage::TrailObject"); + gui.joko_renderer.add_trail(to); + } + UIToUIMessage::Present => { + tracing::trace!("Handling of UIToUIMessage::Present"); + //gui.package_manager.swap(); + } + UIToUIMessage::RenderSwapChain => { + tracing::trace!("Handling of UIToUIMessage::RenderSwapChain"); + gui.joko_renderer.swap(); + } + _ => { + unimplemented!("Handling UIToUIMessage has not been implemented yet"); + } + } + } + fn handle_b2u_message( + gui: &mut JokolayGui, + state: &mut JokolayState, + msg: BackToUIMessage + ) { + match msg { + + BackToUIMessage::ActiveElements(active_elements) => { + tracing::trace!("Handling of BackToUIMessage::ActiveElements"); + gui.package_manager.update_active_categories(&active_elements); + } + BackToUIMessage::CurrentlyUsedFiles(currently_used_files) => { + tracing::trace!("Handling of BackToUIMessage::CurrentlyUsedFiles"); + gui.package_manager.set_currently_used_files(currently_used_files); + } + BackToUIMessage::LoadedPack(pack_texture) => { + unimplemented!("Handling of BackToUIMessage::LoadedPack has not been implemented yet"); + } + BackToUIMessage::Loading => { + unimplemented!("Handling of BackToUIMessage::Loading has not been implemented yet"); + } + BackToUIMessage::MarkerTexture(pack_uuid, tex_path, marker_uuid, position, common_attributes) => { + tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); + gui.package_manager.load_marker_texture(&gui.egui_context, pack_uuid, tex_path, marker_uuid, position, common_attributes); + } + BackToUIMessage::MumbleLink(link) => { + tracing::trace!("Handling of BackToUIMessage::MumbleLink"); + state.link = link; + } + BackToUIMessage::MumbleLinkChanged => { + //too verbose to trace + state.window_changed = true; + } + BackToUIMessage::PackageActiveElements(pack_uuid, active_elements) => { + tracing::trace!("Handling of BackToUIMessage::PackageActiveElements"); + gui.package_manager.update_pack_active_categories(pack_uuid, &active_elements); + } + BackToUIMessage::TextureSwapChain => { + tracing::trace!("Handling of BackToUIMessage::TextureSwapChain"); + gui.package_manager.swap(); + state.list_of_textures_changed = true; + } + BackToUIMessage::TrailTexture(pack_uuid, tex_path, trail_uuid, common_attributes) => { + tracing::trace!("Handling of BackToUIMessage::TrailTexture"); + gui.package_manager.load_trail_texture(&gui.egui_context, pack_uuid, tex_path, trail_uuid, common_attributes); + } + _ => { + unimplemented!("Handling BackToUIMessage has not been implemented yet"); + } + } + } + pub fn enter_event_loop(mut self) { + let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); + let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); + let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); + Self::start_background_loop(Arc::clone(&self.app), Arc::clone(&self.state), b2u_sender, u2b_receiver); + tracing::info!("entering glfw event loop"); - self.menu_panel.show_theme_window = true; - self.menu_panel.show_package_manager_window = true; + let span_guard = info_span!("glfw event loop").entered(); + let mut nb_frames: u128 = 0; + let mut nb_messages: u128 = 0; + //u2u_sender.send(UIToUIMessage::Present);// force a first drawing loop { - let Self { + { + tracing::trace!("glfw event loop, {} frames, {} messages", nb_frames, nb_messages); + if let Ok(mut state) = self.state.lock() { + //untested and might crash due to .unwrap() + let mut gui = self.gui.lock().unwrap(); + while let Ok(msg) = b2u_receiver.try_recv() { + nb_messages += 1; + Self::handle_b2u_message(gui.deref_mut(), state.deref_mut(), msg); + } + while let Ok(msg) = u2u_receiver.try_recv() { + nb_messages += 1; + Self::handle_u2u_message(gui.deref_mut(), state.deref_mut(), msg); + } + } else { + error!("Failed to aquire lock on state"); + } + } + + let mut gui = self.gui.lock().unwrap(); + let JokolayGui { frame_stats, - jdir: _, menu_panel, - mumble_manager, - package_manager: marker_manager, - theme_manager, joko_renderer, egui_context, glfw_backend, - } = &mut self; + theme_manager, + package_manager + } = &mut gui.deref_mut(); + let latest_time = glfw_backend.glfw.get_time(); + let etx = egui_context.clone(); // gather events @@ -123,40 +411,46 @@ impl Jokolay { latest_size }); - let latest_time = glfw_backend.glfw.get_time(); let mut input = glfw_backend.take_raw_input(); input.time = Some(latest_time); etx.begin_frame(input); + // do all the non-gui stuff first frame_stats.tick(latest_time); - let link = match mumble_manager.tick() { - Ok(ml) => ml, - Err(e) => { - error!(?e, "mumble manager tick error"); - None - } - }; - // check if we need to change window position or size. - if let Some(link) = link { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - glfw_backend - .window - .set_pos(link.client_pos.x, link.client_pos.y); - // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. - // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) - // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - glfw_backend - .window - .set_size(link.client_size.x - 1, link.client_size.y - 1); + { + let state = Arc::clone(&self.state); + let mut state = state.lock().unwrap(); + // check if we need to change window position or size. + if let Some(link) = state.link.as_ref() { + if state.window_changed { + glfw_backend + .window + .set_pos(link.client_pos.x, link.client_pos.y); + // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. + // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) + // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + glfw_backend + .window + .set_size(link.client_size.x - 1, link.client_size.y - 1); + } + if state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map){ + package_manager.tick( + &u2u_sender, + latest_time, + link, + JokoRenderer::get_z_near() + ); + state.list_of_textures_changed = false; + } + state.window_changed = false; } + + joko_renderer.tick(state.link.as_ref()); + menu_panel.tick(&etx, state.link.as_ref()); + } - joko_renderer.tick(link); - marker_manager.tick(&etx, latest_time, joko_renderer, link); - menu_panel.tick(&etx, link); - + // do the gui stuff now egui::Area::new("menu panel") .fixed_pos(menu_panel.pos) @@ -198,17 +492,27 @@ impl Jokolay { } }, ); - marker_manager.menu_ui(ui); + package_manager.menu_ui(&u2b_sender, &u2u_sender, ui); }); }); - marker_manager.gui( - &etx, - &mut menu_panel.show_package_manager_window, - &mut menu_panel.show_file_manager_window, - latest_time, joko_renderer, - link - ); - mumble_manager.gui(&etx, &mut menu_panel.show_mumble_manager_window); + + { + let state = Arc::clone(&self.state); + if let Some(link) = state.lock().unwrap().link.as_mut() { + //updates need to be sent to the background state + mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, true, link); + package_manager.gui( + &u2b_sender, + &etx, + &mut menu_panel.show_package_manager_window, + &mut menu_panel.show_file_manager_window, + latest_time, + Some(link) + ); + } else { + //no mumble link available + }; + } JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); @@ -242,7 +546,9 @@ impl Jokolay { ); joko_renderer.present(); glfw_backend.window.swap_buffers(); + nb_frames += 1; } + drop(span_guard); } } diff --git a/crates/jokolay/src/lib.rs b/crates/jokolay/src/lib.rs index 33beac7..2894e1e 100644 --- a/crates/jokolay/src/lib.rs +++ b/crates/jokolay/src/lib.rs @@ -1,3 +1,4 @@ mod app; pub use app::start_jokolay; + diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index 0c044d7..4e41b67 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -9,7 +9,7 @@ //! mod mumble; -use egui::{DragValue}; +use egui::DragValue; use enumflags2::BitFlags; use glam::IVec2; use jokoapi::end_point::mounts::Mount; @@ -138,11 +138,21 @@ impl MumbleManager { if self.link.client_size != client_size { changes.insert(MumbleChanges::WindowSize); } + let cam_pos = cml.f_camera_position.into(); + if self.link.cam_pos != cam_pos { + changes.insert(MumbleChanges::Camera); + } + + let player_pos = cml.f_avatar_position.into(); + if self.link.player_pos != player_pos { + changes.insert(MumbleChanges::Position); + } + self.link = MumbleLink { ui_tick: cml.ui_tick, - player_pos: cml.f_avatar_position.into(), + player_pos, f_avatar_front: cml.f_avatar_front.into(), - cam_pos: cml.f_camera_position.into(), + cam_pos, f_camera_front: cml.f_camera_front.into(), name: identity.name, map_id: cml.context.map_id, @@ -181,22 +191,23 @@ impl MumbleManager { Some(&self.link) }) } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { - egui::Window::new("Mumble Manager") - .open(open) - .show(etx, |ui| { - if !self.is_alive() { - ui.label( - egui::RichText::new("Mumble is not initialized, display dummy link instead.") - .color(egui::Color32::RED) - ); - editable_mumble_ui(ui, &mut self.link); - } else { - let link: MumbleLink = self.link.clone(); - mumble_ui(ui, link); - } - }); - } +} + +pub fn mumble_gui(etx: &egui::Context, open: &mut bool, is_alive: bool, link: &mut MumbleLink) { + egui::Window::new("Mumble Manager") + .open(open) + .show(etx, |ui| { + if !is_alive { + ui.label( + egui::RichText::new("Mumble is not initialized, display dummy link instead.") + .color(egui::Color32::RED) + ); + editable_mumble_ui(ui, link); + } else { + let link: MumbleLink = link.clone(); + mumble_ui(ui, link); + } + }); } fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs index 0acdbf8..0bb2366 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/jokolink/src/mumble/mod.rs @@ -126,6 +126,8 @@ pub enum MumbleChanges { Character = 1 << 2, WindowPosition = 1 << 3, WindowSize = 1 << 4, + Camera = 1 << 5, + Position = 1 << 6, } /// represents the ui scale set in settings -> graphics options -> interface size From 32edc708a7a170ae8f3b03aec781fed7ae7e94d8 Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 3 Apr 2024 11:33:28 +0200 Subject: [PATCH 16/54] fix ugly println --- crates/joko_marker_format/src/manager/pack/loaded.rs | 6 +++--- crates/jokolay/src/app/mod.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 6af96f3..d6940fc 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -560,7 +560,7 @@ impl LoadedPackTexture { ) { tasks.save_ui(self); //FIXME: how to reset state correctly to only display what is necessary ? - println!("LoadedPackTexture.tick: {}-{} {}-{}", + tracing::trace!("LoadedPackTexture.tick: {}-{} {}-{}", self.current_map_data.active_markers.len(), self.current_map_data.wip_markers.len(), self.current_map_data.active_trails.len(), @@ -573,7 +573,7 @@ impl LoadedPackTexture { marker_objects.push(mo); } } - println!("LoadedPackTexture.tick: markers {}", marker_objects.len()); + tracing::trace!("LoadedPackTexture.tick: markers {}", marker_objects.len()); u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); let mut trail_objects = Vec::new(); for (uuid, trail) in self.current_map_data.active_trails.iter() { @@ -583,7 +583,7 @@ impl LoadedPackTexture { }); //next_on_screen.insert(*uuid); } - println!("LoadedPackTexture.tick: trails {}", trail_objects.len()); + tracing::trace!("LoadedPackTexture.tick: trails {}", trail_objects.len()); u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); u2u_sender.send(UIToUIMessage::RenderSwapChain); } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index a9eb1ab..79729fa 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -227,7 +227,7 @@ impl Jokolay { None } }; - println!("choice_of_category_changed: {}", local_state.choice_of_category_changed); + tracing::trace!("choice_of_category_changed: {}", local_state.choice_of_category_changed); package_manager.tick( &b2u_sender, loop_index, From dde3b7f1c81a8b3ede7743c59d399efd18223209 Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 3 Apr 2024 15:49:53 +0200 Subject: [PATCH 17/54] fix overwrite when multiple packages are rendered --- crates/joko_core/src/manager/theme/mod.rs | 22 +- .../joko_marker_format/src/io/deserialize.rs | 1 + crates/joko_marker_format/src/io/serialize.rs | 2 + .../src/manager/pack/loaded.rs | 106 ++++---- .../joko_marker_format/src/manager/package.rs | 61 +++-- crates/joko_marker_format/src/message.rs | 3 +- crates/joko_render/src/billboard.rs | 10 +- crates/joko_render/src/lib.rs | 8 +- crates/jokolay/src/app/init.rs | 3 +- crates/jokolay/src/app/mod.rs | 240 ++++++++++-------- 10 files changed, 257 insertions(+), 199 deletions(-) diff --git a/crates/joko_core/src/manager/theme/mod.rs b/crates/joko_core/src/manager/theme/mod.rs index 59d1a56..f49efea 100644 --- a/crates/joko_core/src/manager/theme/mod.rs +++ b/crates/joko_core/src/manager/theme/mod.rs @@ -52,28 +52,28 @@ impl ThemeManager { const DEFAULT_FONT_NAME: &'static str = "default"; const DEFAULT_THEME_NAME: &'static str = "default"; const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(Self::THEME_MANAGER_DIR_NAME) + pub fn new(jokolay_dir: Arc) -> Result { + jokolay_dir.create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() .wrap_err("failed to create theme manager dir")?; - let dir: Arc = jdir + let theme_manager_dir: Arc = jokolay_dir .open_dir(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() .wrap_err("failed to open theme_manager dir")? .into(); - dir.create_dir_all(Self::THEMES_DIR_NAME) + theme_manager_dir.create_dir_all(Self::THEMES_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; - let themes_dir: Arc = dir + let themes_dir: Arc = theme_manager_dir .open_dir(Self::THEMES_DIR_NAME) .into_diagnostic() .wrap_err("failed to open themes dir")? .into(); - dir.create_dir_all(Self::FONTS_DIR_NAME) + theme_manager_dir.create_dir_all(Self::FONTS_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; - let fonts_dir: Arc = dir + let fonts_dir: Arc = theme_manager_dir .open_dir(Self::FONTS_DIR_NAME) .into_diagnostic() .wrap_err("failed to open themes dir")? @@ -167,8 +167,8 @@ impl ThemeManager { fonts.insert(theme_name, font_bytes); } } - if !dir.exists(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) { - dir.write( + if !theme_manager_dir.exists(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) { + theme_manager_dir.write( format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME), serde_json::to_vec_pretty(&ThemeManagerConfig::default()) .into_diagnostic() @@ -178,14 +178,14 @@ impl ThemeManager { .wrap_err("failed to write theme manager config to the theme manager dir")?; } let config = serde_json::from_str( - &dir.read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) + &theme_manager_dir.read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) .into_diagnostic() .wrap_err("failed to read theme manager config file")?, ) .into_diagnostic() .wrap_err("failed to deserialize theme manager config file")?; Ok(Self { - dir, + dir: theme_manager_dir, themes_dir, fonts_dir, themes, diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 389f7ee..dda1385 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -21,6 +21,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { //FIXME: this should return two elements: //called from already parsed data let mut pack = PackCore::default(); + pack.uuid = Uuid::new_v4(); // walks the directory and loads all files into the hashmap recursive_walk_dir_and_read_images_and_tbins( dir, diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs index e14bfbe..2155f20 100644 --- a/crates/joko_marker_format/src/io/serialize.rs +++ b/crates/joko_marker_format/src/io/serialize.rs @@ -19,6 +19,7 @@ pub(crate) fn save_pack_data_to_dir( writing_directory: &Dir, ) -> Result<()> { // save categories + info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); let od = tree.new_element(names.overlay_data); @@ -101,6 +102,7 @@ pub(crate) fn save_pack_texture_to_dir( writing_directory: &Dir, ) -> Result<()> { + info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); // save images for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index d6940fc..f23b47c 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -34,7 +34,7 @@ use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY pub (crate) struct PackTasks { //TODO: the tasks should be in GUI and not in package //an object that can handle such tasks should be passed as argument of any function that may required an async action - save_ui_task: AsyncTask>, + save_texture_task: AsyncTask>, save_data_task: AsyncTask>, } @@ -86,28 +86,28 @@ pub struct LoadedPackTexture { impl PackTasks { pub fn new() -> Self { Self { - save_ui_task: AsyncTaskGuard::new(PackTasks::async_save_ui), + save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), } } pub fn is_running(&self) -> bool { - self.save_ui_task.lock().unwrap().is_running() + self.save_texture_task.lock().unwrap().is_running() } - fn save_ui(&self, pack: &mut LoadedPackTexture) { - if pack._is_dirty { - std::mem::take(&mut pack._is_dirty); - self.save_ui_task.lock().unwrap().send( - pack.clone() + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { + if status { + std::mem::take(&mut texture_pack._is_dirty); + self.save_texture_task.lock().unwrap().send( + texture_pack.clone() ); } } - fn save_data(&self, pack: &mut LoadedPackData) { - if pack._is_dirty { - std::mem::take(&mut pack._is_dirty); + pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { + if status { + std::mem::take(&mut data_pack._is_dirty); self.save_data_task.lock().unwrap().send( - pack.clone() + data_pack.clone() ); } } @@ -123,11 +123,11 @@ impl PackTasks { //self.load_map_task.lock().unwrap().send(pack); } - fn async_save_ui( + fn async_save_texture( pack_texture: LoadedPackTexture ) -> Result<()> { //let (dir, selectable_categories, activation_data, core) = pack; - info!("Save package {:?}", pack_texture.dir);//FIXME: the context is no more since this is another thread entirely, we do not know which package this is about + info!("Save texture package {:?}", pack_texture.dir);//FIXME: the context is no more since this is another thread entirely, we do not know which package this is about match serde_json::to_string_pretty(&pack_texture.selectable_categories) { Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -165,6 +165,7 @@ impl PackTasks { fn async_save_data( pack_data: LoadedPackData ) -> Result<()> { + info!("Save data package {:?}", pack_data.dir); pack_data.dir .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() @@ -272,6 +273,10 @@ impl LoadedPackData { self._is_dirty = true; } + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub fn tick( &mut self, b2u_sender: &std::sync::mpsc::Sender, @@ -291,7 +296,7 @@ impl LoadedPackData { activation_data */ //we are in a GUI drawing, save in background. - tasks.save_data(self); + //tasks.save_data(self); //FIXME: takes a lot of time when "is_dirty" is true (i.e.: the map of things to display changes). Everythings get reloaded => how to do partial version ? if map_changed || list_of_active_or_selected_elements_changed { tasks.change_map(self, b2u_sender, link, currently_used_files); @@ -320,7 +325,7 @@ impl LoadedPackData { ensure load of every texture regardless of status */ - info!(link.map_id, "current map data is updated."); + info!(link.map_id, "current map data is updated. {}", self.name); if link.map_id == 0 { info!("No map do not do anything"); return; @@ -444,18 +449,10 @@ impl LoadedPackData { } } } - info!("Load notifications for {}: {}/{} markers and {}/{} trails", link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); + info!("Load notifications for {} on map {}: {}/{} markers and {}/{} trails", self.name, link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); debug!("active categories: {:?}", selected_categories_manager.keys()); } - pub fn save_all(&mut self) -> Result<()> { - unimplemented!("Replace by a save on both data and ui"); - /* - self._is_dirty = true; - self.save() - */ - } - /* pub fn save(&mut self) -> Result<()> { let is_dirty = std::mem::take(&mut self._is_dirty); @@ -549,7 +546,10 @@ impl LoadedPackTexture { } } - pub fn tick( + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub fn tick( &mut self, u2u_sender: &std::sync::mpsc::Sender, _timestamp: f64, @@ -558,9 +558,10 @@ impl LoadedPackTexture { z_near: f32, tasks: &PackTasks, ) { - tasks.save_ui(self); + //tasks.save_texture(self); //FIXME: how to reset state correctly to only display what is necessary ? - tracing::trace!("LoadedPackTexture.tick: {}-{} {}-{}", + tracing::trace!("LoadedPackTexture.tick: {} {}-{} {}-{}", + self.name, self.current_map_data.active_markers.len(), self.current_map_data.wip_markers.len(), self.current_map_data.active_trails.len(), @@ -573,7 +574,7 @@ impl LoadedPackTexture { marker_objects.push(mo); } } - tracing::trace!("LoadedPackTexture.tick: markers {}", marker_objects.len()); + tracing::info!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); let mut trail_objects = Vec::new(); for (uuid, trail) in self.current_map_data.active_trails.iter() { @@ -583,16 +584,19 @@ impl LoadedPackTexture { }); //next_on_screen.insert(*uuid); } - tracing::trace!("LoadedPackTexture.tick: trails {}", trail_objects.len()); + tracing::info!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); - u2u_sender.send(UIToUIMessage::RenderSwapChain); } pub fn swap(&mut self) { - std::mem::swap(&mut self.current_map_data.active_markers, &mut self.current_map_data.wip_markers); - std::mem::swap(&mut self.current_map_data.active_trails, &mut self.current_map_data.wip_trails); - self.current_map_data.wip_markers.clear(); - self.current_map_data.wip_trails.clear(); + info!("swap {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.wip_trails.len() + ); + self.current_map_data.active_markers = std::mem::take(&mut self.current_map_data.wip_markers); + self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); } pub fn load_marker_texture( @@ -708,23 +712,27 @@ impl LoadedPackTexture { } - -pub fn load_all_from_dir(pack_dir: &Arc) -> Result<(BTreeMap, BTreeMap)>{ - pack_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) +pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { + jokolay_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() - .wrap_err("failed to create marker manager directory")?; - let marker_manager_dir = pack_dir + .wrap_err(format!("failed to create marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + let marker_manager_dir = jokolay_dir .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() - .wrap_err("failed to open marker manager directory")?; + .wrap_err(format!("failed to open marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; marker_manager_dir .create_dir_all(PACKAGES_DIRECTORY_NAME) .into_diagnostic() - .wrap_err("failed to create marker packs directory")?; + .wrap_err(format!("failed to create marker packs directory {}", PACKAGES_DIRECTORY_NAME))?; let marker_packs_dir = marker_manager_dir .open_dir(PACKAGES_DIRECTORY_NAME) .into_diagnostic() - .wrap_err("failed to open marker packs dir")?; + .wrap_err(format!("failed to open marker packs dir {}", PACKAGES_DIRECTORY_NAME))?; + Ok(marker_packs_dir) +} + +pub fn load_all_from_dir(jokolay_dir: &Arc) -> Result<(BTreeMap, BTreeMap)>{ + let marker_packs_dir = jokolay_to_marker_dir(jokolay_dir)?; let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); @@ -742,18 +750,18 @@ pub fn load_all_from_dir(pack_dir: &Arc) -> Result<(BTreeMap { let (data, tex) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); } Err(e) => { - error!(?e, "failed to load pack from directory"); + error!(?e, "failed to load pack from directory: {}", name); } } drop(span_guard); @@ -783,7 +791,7 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, L pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { let selectable_categories = CategorySelection::default_from_pack_core(&core); let data = LoadedPackData { - name, + name: name.clone(), uuid: core.uuid, dir: Arc::clone(&dir), selected_files: Default::default(), @@ -821,11 +829,11 @@ pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPa selectable_categories, textures: core.textures, current_map_data: Default::default(), - _is_dirty: true, + _is_dirty: false, activation_data, dir: Arc::clone(&dir), late_discovery_categories: core.late_discovery_categories, - name: core.name, + name: name, tbins: core.tbins, active_elements: Default::default(), }; diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index fe479b1..4c83826 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -20,6 +20,8 @@ use crate::{message::BackToUIMessage, pack::CommonAttributes}; use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; +use super::pack::loaded::jokolay_to_marker_dir; + pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; @@ -79,11 +81,12 @@ impl PackageDataManager { /// 4. loads all the packs /// 5. loads all the activation data /// 6. returns self - pub fn new(packs: BTreeMap, marker_packs_dir: &Arc) -> Self { - Self { + pub fn new(packs: BTreeMap, jokolay_dir: Arc) -> Result { + let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; + Ok(Self { packs, tasks: PackTasks::new(), - marker_packs_dir: marker_packs_dir.clone(), + marker_packs_dir: Arc::new(marker_packs_dir), //_marker_manager_dir: marker_manager_dir.into(), current_map_id: 0, save_interval: 0.0, @@ -92,7 +95,7 @@ impl PackageDataManager { parents: Default::default(), loaded_elements: Default::default(), on_screen: Default::default(), - } + }) } pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { @@ -200,8 +203,10 @@ impl PackageDataManager { } } } + let mut tasks = &self.tasks; for (uuid, pack) in self.packs.iter_mut() { let span_guard = info_span!("Updating package status").entered(); + tasks.save_data(pack, pack.is_dirty()); pack.tick( &b2u_sender, loop_index, @@ -209,7 +214,7 @@ impl PackageDataManager { ¤tly_used_files, have_used_files_list_changed || choice_of_category_changed, map_changed, - &self.tasks, + &tasks, &mut categories_and_elements_to_be_loaded, ); std::mem::drop(span_guard); @@ -241,6 +246,11 @@ impl PackageDataManager { //self.on_screen = self.update_active_elements(next_on_screen); } + pub fn save(&mut self, mut data_pack: LoadedPackData) { + self.tasks.save_data(&mut data_pack, true); + self.packs.insert(data_pack.uuid, data_pack); + } + } @@ -292,6 +302,11 @@ impl PackageUIManager { } } + pub fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { self.currently_used_files = currently_used_files; } @@ -401,20 +416,21 @@ impl PackageUIManager { link: &MumbleLink, z_near: f32, ) { - trace!("nb packs: {}", self.packs.len()); + let mut tasks = &self.tasks; for (uuid, pack) in self.packs.iter_mut() { let span_guard = info_span!("Updating package status").entered(); + tasks.save_texture(pack, pack.is_dirty()); pack.tick( &u2u_sender, timestamp, link, - //&mut next_on_screen, z_near, - &self.tasks + &tasks ); std::mem::drop(span_guard); } - u2u_sender.send(UIToUIMessage::Present); + u2u_sender.send(UIToUIMessage::RenderSwapChain); + //u2u_sender.send(UIToUIMessage::Present); } pub fn menu_ui( @@ -505,18 +521,18 @@ impl PackageUIManager { Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { - let mut delete = vec![]; - for pack in self.packs.values() { - ui.label(pack.name.clone()); - if ui.button("delete").clicked() { - delete.push(pack.uuid); + let mut to_delete = vec![]; + for pack in self.packs.values() { + ui.label(pack.name.clone()); + if ui.button("delete").clicked() { + to_delete.push(pack.uuid); + } } - } - if !delete.is_empty() { - //TODO: send message to background thread, UIToBackMessage::DeletePack - event_sender.send(UIToBackMessage::DeletePacks(delete)); - } - }); + if !to_delete.is_empty() { + //TODO: send message to background thread, UIToBackMessage::DeletePack + event_sender.send(UIToBackMessage::DeletePacks(to_delete)); + } + }); }); if self.import_status.is_some() { @@ -583,6 +599,11 @@ impl PackageUIManager { self.gui_package_loader(event_sender, etx, is_marker_open); self.gui_file_manager(event_sender, etx, is_file_open, link); } + + pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { + self.tasks.save_texture(&mut texture_pack, true); + self.packs.insert(texture_pack.uuid, texture_pack); + } } diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs index 546752c..bd4c489 100644 --- a/crates/joko_marker_format/src/message.rs +++ b/crates/joko_marker_format/src/message.rs @@ -44,6 +44,7 @@ pub enum BackToUIMessage { ActiveElements(HashSet),//list of all elements that are loaded for current map CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display LoadedPack(LoadedPackTexture),//push a loaded pack to UI + DeletedPacks(Vec),//push a deleted set of packs to UI Loading, MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), MumbleLink(Option), @@ -67,7 +68,7 @@ pub enum UIToBackMessage { pub enum UIToUIMessage { BulkMarkerObject(Vec), BulkTrailObject(Vec), - Present,// a render loop is finished and we can present it + //Present,// a render loop is finished and we can present it MarkerObject(MarkerObject), RenderSwapChain,// The list of elements to display was changed TrailObject(TrailObject), diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 2745056..ea9e01c 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -74,10 +74,12 @@ impl BillBoardRenderer { } pub fn swap(&mut self) { - std::mem::swap(&mut self.markers, &mut self.markers_wip); - std::mem::swap(&mut self.trails, &mut self.trails_wip); - self.markers_wip.clear(); - self.trails_wip.clear(); + info!("swap UI to display {} markers, {} trails", + self.markers_wip.len(), + self.trails_wip.len() + ); + self.markers = std::mem::take(&mut self.markers_wip); + self.trails = std::mem::take(&mut self.trails_wip); } pub fn prepare_render_data(&mut self, gl: &Context) { diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index 5c193a3..e073bf9 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -113,15 +113,15 @@ impl JokoRenderer { self.has_link = false; } } - pub fn set_billboard(&mut self, marker_objects: Vec) { - self.billboard_renderer.markers_wip = marker_objects; + pub fn extend_markers(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip.extend(marker_objects); } pub fn add_billboard(&mut self, marker_object: MarkerObject) { self.billboard_renderer.markers_wip.push(marker_object); } - pub fn set_trail(&mut self, trail_objects: Vec) { - self.billboard_renderer.trails_wip = trail_objects; + pub fn extend_trails(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip.extend(trail_objects); } pub fn add_trail(&mut self, trail_object: TrailObject) { self.billboard_renderer.trails_wip.push(trail_object); diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index 78a09bd..63dfeea 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -20,7 +20,8 @@ pub fn get_jokolay_dir() -> Result { .wrap_err(jkl_path) .wrap_err("failed to open jokolay data dir")? } else { - let dir = cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah) + let project_dir = cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah); + let dir = project_dir .ok_or(miette::miette!( "getting project dirs failed for some reason" ))? diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 79729fa..b5e8b32 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -33,6 +33,7 @@ struct JokolayGui { egui_context: egui::Context, glfw_backend: GlfwBackend, theme_manager: ThemeManager, + mumble_manager: MumbleManager, package_manager: PackageUIManager, } #[allow(unused)] @@ -40,22 +41,24 @@ pub struct Jokolay { jdir: Arc, gui: Arc>, app: Arc>>, - state: Arc>>, + state: JokolayState, } impl Jokolay { - pub fn new(jdir: Arc) -> Result { + pub fn new(jokolay_dir: Arc) -> Result { //TODO: we could have two mumble_manager, one for UI, one for backend, each keeping its own copy //this would allow overwriting from gui to back ? //if we want to be able to edit the link, one need to put a "form submission" logic. - let mumble_manager = + let mumble_data_manager = + MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; + let mumble_ui_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - let (data_packages, texture_packages) = load_all_from_dir(&jdir).wrap_err("failed to load packages")?; - let mut package_data_manager = PackageDataManager::new(data_packages, &jdir); + let (data_packages, texture_packages) = load_all_from_dir(&jokolay_dir).wrap_err("failed to load packages")?; + let mut package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); - let mut theme_manager = ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; + let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); @@ -87,7 +90,7 @@ impl Jokolay { package_ui_manager.late_init(&egui_context); Ok(Self { - jdir, + jdir: jokolay_dir, gui: Arc::new(Mutex::new(JokolayGui { frame_stats, joko_renderer, @@ -95,36 +98,37 @@ impl Jokolay { egui_context, menu_panel, theme_manager, + mumble_manager: mumble_ui_manager, package_manager: package_ui_manager })), app: Arc::new(Mutex::new(Box::new(JokolayApp{ - mumble_manager, + mumble_manager: mumble_data_manager, package_manager: package_data_manager, }))), - state: Arc::new(Mutex::new(Box::new(JokolayState{ + state: JokolayState{ link: None, window_changed: true, choice_of_category_changed: false, list_of_textures_changed: false, - }))) + } }) } fn start_background_loop( app: Arc>>, - state: Arc>>, + state: JokolayState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, ) { let background_thread = std::thread::spawn(move || { - Self::background_loop(Arc::clone(&app), Arc::clone(&state), b2u_sender, u2b_receiver); + Self::background_loop(Arc::clone(&app), state, b2u_sender, u2b_receiver); }); } fn handle_u2b_message( package_manager: &mut PackageDataManager, local_state: &mut JokolayState, - state_sender: &std::sync::mpsc::Sender, + b2u_sender: &std::sync::mpsc::Sender, msg: UIToBackMessage ) { match msg { @@ -147,14 +151,17 @@ impl Jokolay { } UIToBackMessage::DeletePacks(to_delete) => { tracing::trace!("Handling of UIToBackMessage::DeletePacks"); + let mut deleted = Vec::new(); for pack_uuid in to_delete { let pack = package_manager.packs.remove(&pack_uuid).unwrap(); if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) { error!(?e, pack.name,"failed to remove pack"); } else { info!("deleted marker pack: {}", pack.name); + deleted.push(pack_uuid); } } + b2u_sender.send(BackToUIMessage::DeletedPacks(deleted)); } UIToBackMessage::ImportPack => { unimplemented!("Handling of UIToBackMessage::ImportPack has not been implemented yet"); @@ -163,26 +170,32 @@ impl Jokolay { unimplemented!("Handling of UIToBackMessage::ReloadPack has not been implemented yet"); } UIToBackMessage::SavePack(name, pack) => { - //TODO: send message to background thread, UIToBackMessage::SavePack + tracing::trace!("Handling of UIToBackMessage::SavePack"); let name = name.as_str(); + if package_manager.marker_packs_dir.exists(name) { + match package_manager.marker_packs_dir + .remove_dir_all(name) + .into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to delete already existing marker pack"); + } + } + } + if let Err(e) = package_manager.marker_packs_dir.create_dir_all(name) { + error!(?e, "failed to create directory for pack"); + } match package_manager.marker_packs_dir.open_dir(name) { Ok(dir) => { let (mut data_pack, texture_pack) = build_from_core(name.to_string(), dir.into(), pack); - match data_pack.save_all() { - Ok(_) => { - package_manager.packs.insert(data_pack.uuid, data_pack); - }, - Err(e) => { - error!(?e, "failed to save marker pack"); - }, - } - state_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + tracing::trace!("Package loaded into data and texture"); + package_manager.save(data_pack); + b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); }, Err(e) => { - error!(?e, "failed to open marker pack directory to save pack"); + error!(?e, "failed to open marker pack directory to save pack {:?} {}", package_manager.marker_packs_dir, name); } }; - tracing::trace!("Handling of UIToBackMessage::SavePack"); } _ => { unimplemented!("Handling BackToUIMessage has not been implemented yet"); @@ -191,13 +204,12 @@ impl Jokolay { } fn background_loop( mut app: Arc>>, - state: Arc>>, + mut local_state: JokolayState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, ) { tracing::info!("entering background event loop"); let span_guard = info_span!("background event loop").entered(); - let mut local_state = state.lock().unwrap().as_mut().clone(); let mut loop_index: u128 = 0; let mut nb_messages: u128 = 0; loop { @@ -207,12 +219,11 @@ impl Jokolay { mumble_manager, package_manager } = &mut app.deref_mut().as_mut(); - while let Ok(msg) = u2b_receiver.try_recv() { - Self::handle_u2b_message(package_manager, &mut local_state,&b2u_sender, msg); - nb_messages += 1; - } + //very first thing to do is to read the mumble link let link = match mumble_manager.tick() { Ok(ml) => { + //let link_clone = ml.cloned(); + //b2u_sender.send(BackToUIMessage::MumbleLink(link_clone)); if let Some(link) = ml { if link.changes.contains(MumbleChanges::WindowPosition) || link.changes.contains(MumbleChanges::WindowSize) @@ -227,6 +238,10 @@ impl Jokolay { None } }; + while let Ok(msg) = u2b_receiver.try_recv() { + Self::handle_u2b_message(package_manager, &mut local_state,&b2u_sender, msg); + nb_messages += 1; + } tracing::trace!("choice_of_category_changed: {}", local_state.choice_of_category_changed); package_manager.tick( &b2u_sender, @@ -236,8 +251,6 @@ impl Jokolay { ); local_state.choice_of_category_changed = false; - let link_clone = link.cloned(); - b2u_sender.send(BackToUIMessage::MumbleLink(link_clone)); thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; } @@ -251,12 +264,12 @@ impl Jokolay { ) { match msg { UIToUIMessage::BulkMarkerObject(marker_objects) => { - tracing::trace!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); - gui.joko_renderer.set_billboard(marker_objects); + tracing::info!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); + gui.joko_renderer.extend_markers(marker_objects); } UIToUIMessage::BulkTrailObject(trail_objects) => { - tracing::trace!("Handling of UIToUIMessage::BulkTrailObject"); - gui.joko_renderer.set_trail(trail_objects); + tracing::info!("Handling of UIToUIMessage::BulkTrailObject {}", trail_objects.len()); + gui.joko_renderer.extend_trails(trail_objects); } UIToUIMessage::MarkerObject(mo) => { tracing::trace!("Handling of UIToUIMessage::MarkerObject"); @@ -266,12 +279,8 @@ impl Jokolay { tracing::trace!("Handling of UIToUIMessage::TrailObject"); gui.joko_renderer.add_trail(to); } - UIToUIMessage::Present => { - tracing::trace!("Handling of UIToUIMessage::Present"); - //gui.package_manager.swap(); - } UIToUIMessage::RenderSwapChain => { - tracing::trace!("Handling of UIToUIMessage::RenderSwapChain"); + tracing::info!("Handling of UIToUIMessage::RenderSwapChain"); gui.joko_renderer.swap(); } _ => { @@ -281,7 +290,7 @@ impl Jokolay { } fn handle_b2u_message( gui: &mut JokolayGui, - state: &mut JokolayState, + local_state: &mut JokolayState, msg: BackToUIMessage ) { match msg { @@ -294,8 +303,14 @@ impl Jokolay { tracing::trace!("Handling of BackToUIMessage::CurrentlyUsedFiles"); gui.package_manager.set_currently_used_files(currently_used_files); } + BackToUIMessage::DeletedPacks(to_delete) => { + tracing::trace!("Handling of BackToUIMessage::DeletedPacks"); + gui.package_manager.delete_packs(to_delete); + } BackToUIMessage::LoadedPack(pack_texture) => { - unimplemented!("Handling of BackToUIMessage::LoadedPack has not been implemented yet"); + tracing::trace!("Handling of BackToUIMessage::LoadedPack"); + gui.package_manager.save(pack_texture); + gui.package_manager.import_status = None; } BackToUIMessage::Loading => { unimplemented!("Handling of BackToUIMessage::Loading has not been implemented yet"); @@ -306,20 +321,20 @@ impl Jokolay { } BackToUIMessage::MumbleLink(link) => { tracing::trace!("Handling of BackToUIMessage::MumbleLink"); - state.link = link; + local_state.link = link; } BackToUIMessage::MumbleLinkChanged => { //too verbose to trace - state.window_changed = true; + local_state.window_changed = true; } BackToUIMessage::PackageActiveElements(pack_uuid, active_elements) => { tracing::trace!("Handling of BackToUIMessage::PackageActiveElements"); gui.package_manager.update_pack_active_categories(pack_uuid, &active_elements); } BackToUIMessage::TextureSwapChain => { - tracing::trace!("Handling of BackToUIMessage::TextureSwapChain"); + tracing::info!("Handling of BackToUIMessage::TextureSwapChain"); gui.package_manager.swap(); - state.list_of_textures_changed = true; + local_state.list_of_textures_changed = true; } BackToUIMessage::TrailTexture(pack_uuid, tex_path, trail_uuid, common_attributes) => { tracing::trace!("Handling of BackToUIMessage::TrailTexture"); @@ -335,29 +350,26 @@ impl Jokolay { let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); - Self::start_background_loop(Arc::clone(&self.app), Arc::clone(&self.state), b2u_sender, u2b_receiver); + Self::start_background_loop(Arc::clone(&self.app), self.state.clone(), b2u_sender, u2b_receiver); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); + let mut local_state = self.state.clone(); let mut nb_frames: u128 = 0; let mut nb_messages: u128 = 0; //u2u_sender.send(UIToUIMessage::Present);// force a first drawing loop { { tracing::trace!("glfw event loop, {} frames, {} messages", nb_frames, nb_messages); - if let Ok(mut state) = self.state.lock() { - //untested and might crash due to .unwrap() - let mut gui = self.gui.lock().unwrap(); - while let Ok(msg) = b2u_receiver.try_recv() { - nb_messages += 1; - Self::handle_b2u_message(gui.deref_mut(), state.deref_mut(), msg); - } - while let Ok(msg) = u2u_receiver.try_recv() { - nb_messages += 1; - Self::handle_u2u_message(gui.deref_mut(), state.deref_mut(), msg); - } - } else { - error!("Failed to aquire lock on state"); + //untested and might crash due to .unwrap() + let mut gui = self.gui.lock().unwrap(); + while let Ok(msg) = b2u_receiver.try_recv() { + nb_messages += 1; + Self::handle_b2u_message(gui.deref_mut(), &mut local_state, msg); + } + while let Ok(msg) = u2u_receiver.try_recv() { + nb_messages += 1; + Self::handle_u2u_message(gui.deref_mut(), &mut local_state, msg); } } @@ -369,6 +381,7 @@ impl Jokolay { egui_context, glfw_backend, theme_manager, + mumble_manager, package_manager } = &mut gui.deref_mut(); let latest_time = glfw_backend.glfw.get_time(); @@ -418,39 +431,52 @@ impl Jokolay { // do all the non-gui stuff first frame_stats.tick(latest_time); - { - let state = Arc::clone(&self.state); - let mut state = state.lock().unwrap(); - // check if we need to change window position or size. - if let Some(link) = state.link.as_ref() { - if state.window_changed { - glfw_backend - .window - .set_pos(link.client_pos.x, link.client_pos.y); - // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. - // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) - // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - glfw_backend - .window - .set_size(link.client_size.x - 1, link.client_size.y - 1); - } - if state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map){ - package_manager.tick( - &u2u_sender, - latest_time, - link, - JokoRenderer::get_z_near() - ); - state.list_of_textures_changed = false; + let link = match mumble_manager.tick() { + Ok(ml) => { + if let Some(link) = ml { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + local_state.window_changed = true; + } + local_state.link = Some(link.clone()); } - state.window_changed = false; + ml + }, + Err(e) => { + error!(?e, "mumble manager tick error"); + None + } + }; + + // check if we need to change window position or size. + if let Some(link) = link { + if local_state.window_changed { + glfw_backend + .window + .set_pos(link.client_pos.x, link.client_pos.y); + // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. + // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) + // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + glfw_backend + .window + .set_size(link.client_size.x - 1, link.client_size.y - 1); } - - joko_renderer.tick(state.link.as_ref()); - menu_panel.tick(&etx, state.link.as_ref()); - + if local_state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map) { + package_manager.tick( + &u2u_sender, + latest_time, + link, + JokoRenderer::get_z_near() + ); + local_state.list_of_textures_changed = false; + } + local_state.window_changed = false; } + joko_renderer.tick(link); + menu_panel.tick(&etx, link); + // do the gui stuff now egui::Area::new("menu panel") .fixed_pos(menu_panel.pos) @@ -494,25 +520,21 @@ impl Jokolay { ); package_manager.menu_ui(&u2b_sender, &u2u_sender, ui); }); - }); + } + ); - { - let state = Arc::clone(&self.state); - if let Some(link) = state.lock().unwrap().link.as_mut() { - //updates need to be sent to the background state - mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, true, link); - package_manager.gui( - &u2b_sender, - &etx, - &mut menu_panel.show_package_manager_window, - &mut menu_panel.show_file_manager_window, - latest_time, - Some(link) - ); - } else { - //no mumble link available - }; - } + if let Some(link) = local_state.link.as_mut() { + //updates need to be sent to the background state + mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, true, link); + }; + package_manager.gui( + &u2b_sender, + &etx, + &mut menu_panel.show_package_manager_window, + &mut menu_panel.show_file_manager_window, + latest_time, + link + ); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); From ff34b75b2cde5c7f794e05080598d42b4aab4f4d Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 3 Apr 2024 15:59:28 +0200 Subject: [PATCH 18/54] tuned down the info since issues is identified and fixed --- crates/joko_marker_format/src/io/deserialize.rs | 4 +++- crates/joko_marker_format/src/manager/pack/loaded.rs | 4 ++-- crates/joko_marker_format/src/pack/mod.rs | 1 - crates/jokolay/src/app/mod.rs | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index dda1385..343bd9c 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -88,7 +88,9 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { trace!("file ignored: {name}") } } - info!("Entities registered: {}", pack.entities_parents.len()); + info!("Entities registered (category + markers): {}", pack.entities_parents.len()); + info!("Categories registered: {}", pack.all_categories.len()); + info!("Markers registered: {}", pack.entities_parents.len() - pack.all_categories.len()); info!("Maps registered: {}", pack.maps.len()); info!("Textures registered: {}", pack.textures.len()); info!("Trail binaries registered: {}", pack.tbins.len()); diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index f23b47c..2b0be81 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -574,7 +574,7 @@ impl LoadedPackTexture { marker_objects.push(mo); } } - tracing::info!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); + tracing::trace!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); let mut trail_objects = Vec::new(); for (uuid, trail) in self.current_map_data.active_trails.iter() { @@ -584,7 +584,7 @@ impl LoadedPackTexture { }); //next_on_screen.insert(*uuid); } - tracing::info!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); + tracing::trace!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index a2a4871..a7765f7 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -92,7 +92,6 @@ impl PackCore { let mut entities_parents: HashMap = Default::default(); let mut all_categories: HashMap = Default::default(); Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); - info!("Catepories registered: {}", all_categories.len()); self.entities_parents.extend(entities_parents); self.all_categories = all_categories; } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index b5e8b32..d439ee4 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -264,11 +264,11 @@ impl Jokolay { ) { match msg { UIToUIMessage::BulkMarkerObject(marker_objects) => { - tracing::info!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); + tracing::debug!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); gui.joko_renderer.extend_markers(marker_objects); } UIToUIMessage::BulkTrailObject(trail_objects) => { - tracing::info!("Handling of UIToUIMessage::BulkTrailObject {}", trail_objects.len()); + tracing::debug!("Handling of UIToUIMessage::BulkTrailObject {}", trail_objects.len()); gui.joko_renderer.extend_trails(trail_objects); } UIToUIMessage::MarkerObject(mo) => { @@ -280,7 +280,7 @@ impl Jokolay { gui.joko_renderer.add_trail(to); } UIToUIMessage::RenderSwapChain => { - tracing::info!("Handling of UIToUIMessage::RenderSwapChain"); + tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); gui.joko_renderer.swap(); } _ => { @@ -332,7 +332,7 @@ impl Jokolay { gui.package_manager.update_pack_active_categories(pack_uuid, &active_elements); } BackToUIMessage::TextureSwapChain => { - tracing::info!("Handling of BackToUIMessage::TextureSwapChain"); + tracing::debug!("Handling of BackToUIMessage::TextureSwapChain"); gui.package_manager.swap(); local_state.list_of_textures_changed = true; } From 7361004231a29bfd6b722357308e4b082446141c Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 3 Apr 2024 23:55:21 +0200 Subject: [PATCH 19/54] allow throttling of messages + TODO/FIXME cleanup --- crates/joko_core/src/task/mod.rs | 7 +- .../joko_marker_format/src/io/deserialize.rs | 12 +- .../src/manager/pack/category_selection.rs | 4 +- .../src/manager/pack/file_selection.rs | 1 - .../src/manager/pack/loaded.rs | 175 +++++------------- .../joko_marker_format/src/manager/package.rs | 44 +++-- crates/joko_marker_format/src/pack/mod.rs | 11 +- crates/joko_render/src/lib.rs | 1 - crates/jokolay/src/app/mod.rs | 32 +++- 9 files changed, 111 insertions(+), 176 deletions(-) diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs index e072fc9..b413bdf 100644 --- a/crates/joko_core/src/task/mod.rs +++ b/crates/joko_core/src/task/mod.rs @@ -1,7 +1,5 @@ use std::{ - sync::{mpsc::SendError, Arc, Mutex}, - thread::JoinHandle, - result::Result + result::Result, sync::{mpsc::{RecvError, SendError}, Arc, Mutex}, thread::JoinHandle }; //TODO: could this be a wrapper only and a move/copy would not impact content ? @@ -64,6 +62,9 @@ where pub fn send(&self, value: TaskItem) -> Result<(), SendError> { self.task_sender.send(value) } + pub fn recv(&self) -> Result { + self.result_receiver.recv() + } pub fn is_running(&self) -> bool { let nb = self.nb.load(std::sync::atomic::Ordering::Relaxed); diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 343bd9c..5a61a09 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -18,7 +18,6 @@ use xot::{Node, Xot, Element}; use super::XotAttributeNameIDs; pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { - //FIXME: this should return two elements: //called from already parsed data let mut pack = PackCore::default(); pack.uuid = Uuid::new_v4(); @@ -307,13 +306,6 @@ fn parse_categories_recursive( let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(ele, names); - /* - FIXME: how to handle both - orphans - out of order evaluation => mark the current marker category to be skipped and not inserted, this is an orphan for later reinsertion - if the category has a Display name, then the name is relative, if not, it means this is defined somewhere else and name is absolute. - => have a "late insertion" container - */ let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); let separator = ele @@ -454,7 +446,7 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result let source_file_name = child.get_attribute(names._source_file_name).unwrap_or_default().to_string(); pack.source_files.insert(source_file_name.clone(), true); - //TODO: route, difference with trail: trail is binary format while route is text => convert route into a trail + if child.name() == names.route { debug!("Found a route in core pack {:?}", child); import_route_as_trail(pack, &names, &tree, &poi_node, child, full_category_name, &category_uuid, source_file_name) @@ -554,7 +546,6 @@ fn parse_category_categories_xml_recursive( continue; } - //TODO: if no display name, only keep the parent/enfant relationship let relative_category_name = ele.get_attribute(names.name) .or(ele.get_attribute(names.display_name) .or(ele.get_attribute(names.CapitalName) @@ -648,7 +639,6 @@ fn parse_category_categories_xml_recursive( /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { - //FIXME: there might be a problem where the elements are not displayed immediately after save //called to import a new pack // all the contents of ZPack let mut pack = PackCore::default(); diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs index 728f38a..ef9869a 100644 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { - #[serde(skip)] - pub uuid: Uuid,//FIXME: there seems to be guid generated at several places leading to confusion in what is active or not (most likely in category, not saved versys categoryselection, saved) + //#[serde(skip)] + pub uuid: Uuid,//FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information #[serde(skip)] pub parent: Option, pub is_selected: bool,//has it been selected in configuration to be displayed diff --git a/crates/joko_marker_format/src/manager/pack/file_selection.rs b/crates/joko_marker_format/src/manager/pack/file_selection.rs index cc0e9e7..5a8013a 100644 --- a/crates/joko_marker_format/src/manager/pack/file_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/file_selection.rs @@ -13,7 +13,6 @@ impl<'a> SelectedFileManager { pack_source_files: &OrderedHashMap, currently_used_files: &BTreeMap, ) -> Self { - //TODO: build data let mut list_of_enabled_files: OrderedHashMap = Default::default(); SelectedFileManager::recursive_get_full_names( &selected_files, diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 2b0be81..38cf1b9 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -1,6 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, - sync::Arc + collections::{BTreeMap, HashMap, HashSet}, sync::Arc }; use indexmap::IndexMap; @@ -32,10 +31,10 @@ use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY pub (crate) struct PackTasks { - //TODO: the tasks should be in GUI and not in package //an object that can handle such tasks should be passed as argument of any function that may required an async action save_texture_task: AsyncTask>, save_data_task: AsyncTask>, + load_pack_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> } #[derive(Clone)] @@ -58,10 +57,7 @@ pub struct LoadedPackData { activation_data: ActivationData, active_elements: HashSet,//keep track of which elements are active } -/* -TODO: LoadedPack is in fact the perfect tool to handle GUI if there was no "core" member inside it - it means dig out a CorePack into multiple parts -*/ + #[derive(Clone)] pub struct LoadedPackTexture { pub name: String, @@ -88,10 +84,22 @@ impl PackTasks { Self { save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + load_pack_task: AsyncTaskGuard::new(load_all_from_dir), } } pub fn is_running(&self) -> bool { - self.save_texture_task.lock().unwrap().is_running() + self.save_texture_task.lock().unwrap().is_running() || + self.save_data_task.lock().unwrap().is_running() + } + pub fn status_as_color(&self) -> egui::Color32 { + //we can choose whatever color code we want to focus on load, save, network queries, anything. + let max_nb_saving = 2; + let nb_saving = + self.save_texture_task.lock().unwrap().is_running() as u8 + + self.save_data_task.lock().unwrap().is_running() as u8 + ; + let color_saving = nb_saving * 0xff / max_nb_saving; + egui::Color32::from_rgb(color_saving, 0, 0) } pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { @@ -126,8 +134,7 @@ impl PackTasks { fn async_save_texture( pack_texture: LoadedPackTexture ) -> Result<()> { - //let (dir, selectable_categories, activation_data, core) = pack; - info!("Save texture package {:?}", pack_texture.dir);//FIXME: the context is no more since this is another thread entirely, we do not know which package this is about + info!("Save texture package {:?}", pack_texture.dir); match serde_json::to_string_pretty(&pack_texture.selectable_categories) { Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -188,26 +195,9 @@ impl LoadedPackData { const CORE_PACK_DIR_NAME: &'static str = "core"; const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; - pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { - if !pack_dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? - { - bail!("pack core doesn't exist in this pack"); - } - let core_dir = pack_dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - - //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. - let selectable_categories = CategorySelection::default_from_pack_core(&core); - /*** - FIXME: Once this is saved properly, we can restore following block of code. - - let selected_categories = (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + fn load_selectable_categories(pack_dir: &Arc, pack: &PackCore) -> OrderedHashMap { + //FIXME: we need to patch those categories from the one in the files + (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { Ok(cd_json) => match serde_json::from_str(&cd_json) { Ok(cd) => Some(cd), @@ -226,7 +216,7 @@ impl LoadedPackData { }) .flatten() .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&core); + let cs = CategorySelection::default_from_pack_core(&pack); match serde_json::to_string_pretty(&cs) { Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -241,8 +231,26 @@ impl LoadedPackData { } } cs - }); - **/ + }) + } + pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { + if !pack_dir + .try_exists(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to check if pack core exists")? + { + bail!("pack core doesn't exist in this pack"); + } + let core_dir = pack_dir + .open_dir(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. + //let selectable_categories = CategorySelection::default_from_pack_core(&core); + let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); + Ok(LoadedPackData { name, uuid: core.uuid, @@ -288,16 +296,7 @@ impl LoadedPackData { tasks: &PackTasks, next_loaded: &mut HashSet, ) { - /* - TODO: - need to be used for redraw from last known copy (should be an argument): - selectable_categories - active_elements - activation_data - */ - //we are in a GUI drawing, save in background. - //tasks.save_data(self); - //FIXME: takes a lot of time when "is_dirty" is true (i.e.: the map of things to display changes). Everythings get reloaded => how to do partial version ? + //since the loading of texture is lazy, there is no problem when calling this regularly if map_changed || list_of_active_or_selected_elements_changed { tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); @@ -313,18 +312,8 @@ impl LoadedPackData { b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, - //selectable_categories: OrderedHashMap, - //activation_data: ActivationData, active_elements: &mut HashSet, ){ - /* - FIXME: - this is processing too much information - one need to process more at first load and not on map change - - ensure load of every texture regardless of status - - */ info!(link.map_id, "current map data is updated. {}", self.name); if link.map_id == 0 { info!("No map do not do anything"); @@ -401,11 +390,6 @@ impl LoadedPackData { continue; } } - /* - TODO: purely make it a notification ? - - what are the pro and cons to have a map data per package - */ if let Some(tex_path) = common_attributes.get_icon_file() { b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); } else { @@ -452,54 +436,6 @@ impl LoadedPackData { info!("Load notifications for {} on map {}: {}/{} markers and {}/{} trails", self.name, link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); debug!("active categories: {:?}", selected_categories_manager.keys()); } - - /* - pub fn save(&mut self) -> Result<()> { - let is_dirty = std::mem::take(&mut self._is_dirty); - if is_dirty { - match serde_json::to_string_pretty(&self.selectable_categories) { - Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - match serde_json::to_string_pretty(&self.activation_data) { - Ok(ad_json) => match self.dir.write(Self::ACTIVATION_DATA_FILE_NAME, ad_json) { - Ok(_) => { - debug!("wrote activation to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write activation data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize activation"); - } - } - } - self.dir - .create_dir_all(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to create xmlpack directory")?; - let core_dir = self - .dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - save_pack_core_to_dir( - &self, - &core_dir, - is_dirty, - )?; - Ok(()) - }*/ } @@ -507,13 +443,6 @@ impl LoadedPackData { impl LoadedPackTexture { const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; - /*pub fn is_dirty(&self) -> bool { - self._is_dirty - } - pub fn active_elements(&self) -> HashSet { - self.current_map_data.active_elements.clone() - }*/ - pub fn category_set_all(&mut self, status: bool) { CategorySelection::recursive_set_all(&mut self.selectable_categories, status); self._is_dirty = true; @@ -558,8 +487,6 @@ impl LoadedPackTexture { z_near: f32, tasks: &PackTasks, ) { - //tasks.save_texture(self); - //FIXME: how to reset state correctly to only display what is necessary ? tracing::trace!("LoadedPackTexture.tick: {} {}-{} {}-{}", self.name, self.current_map_data.active_markers.len(), @@ -567,7 +494,6 @@ impl LoadedPackTexture { self.current_map_data.active_trails.len(), self.current_map_data.wip_trails.len(), ); - //FIXME: SelectedCategoryManager works with categories, not elements let mut marker_objects = Vec::new(); for (uuid, marker) in self.current_map_data.active_markers.iter() { if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { @@ -695,7 +621,6 @@ impl LoadedPackTexture { info!(%tbin_path, "failed to find tbin"); return; }; - //TODO: if iso and closed, split it as a polygon and fill it as a surface if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( &common_attributes, &tbin.nodes, @@ -731,8 +656,8 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { Ok(marker_packs_dir) } -pub fn load_all_from_dir(jokolay_dir: &Arc) -> Result<(BTreeMap, BTreeMap)>{ - let marker_packs_dir = jokolay_to_marker_dir(jokolay_dir)?; +pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap, BTreeMap)>{ + let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); @@ -788,12 +713,12 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, L } -pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { - let selectable_categories = CategorySelection::default_from_pack_core(&core); +pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { + let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); let data = LoadedPackData { name: name.clone(), uuid: core.uuid, - dir: Arc::clone(&dir), + dir: Arc::clone(&pack_dir), selected_files: Default::default(), all_categories: core.all_categories, categories: core.categories, @@ -805,8 +730,8 @@ pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPa selectable_categories: selectable_categories.clone(), entities_parents: core.entities_parents, }; - let activation_data = (if dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - match dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { Ok(contents) => match serde_json::from_str(&contents) { Ok(cd) => Some(cd), Err(e) => { @@ -831,7 +756,7 @@ pub fn build_from_core(name: String, dir: Arc, core: PackCore) -> (LoadedPa current_map_data: Default::default(), _is_dirty: false, activation_data, - dir: Arc::clone(&dir), + dir: Arc::clone(&pack_dir), late_discovery_categories: core.late_discovery_categories, name: name, tbins: core.tbins, diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index 4c83826..3bed4dd 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -35,7 +35,6 @@ pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility /// 2. marker needs to be drawn /// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture /// 4. render that marker use joko_render -/// FIXME: it is a bad name, it does not manage Markers, but packages #[must_use] pub struct PackageDataManager { /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. @@ -178,7 +177,7 @@ impl PackageDataManager { match link { Some(link) => { - //FIXME: how to save/load the active files ? + //TODO: how to save/load the active files ? //TODO: find an efficient way to propagate the file deactivation let mut have_used_files_list_changed = false; let map_changed = self.current_map_id != link.map_id; @@ -232,21 +231,21 @@ impl PackageDataManager { }, None => {}, }; - //TODO: state_sender.send(BackToUIMessage::ActiveElements(active_elements)); - - - //those are the elements displayed, not the categories, one would need to keep the link between the two - /*if is_one_package_reloaded { - for pack in self.packs.values() { - next_loaded.extend(pack.active_elements()); - } - info!("Loaded {} elements", next_loaded.len()); - self.loaded_elements = self.update_active_elements(next_loaded); - }*/ - //self.on_screen = self.update_active_elements(next_on_screen); } + fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } pub fn save(&mut self, mut data_pack: LoadedPackData) { + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == data_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); self.tasks.save_data(&mut data_pack, true); self.packs.insert(data_pack.uuid, data_pack); } @@ -466,7 +465,8 @@ impl PackageUIManager { }); if self.tasks.is_running() { - ui.spinner(); + let sp = egui::Spinner::new().color(self.tasks.status_as_color()); + ui.add(sp); } } @@ -529,7 +529,6 @@ impl PackageUIManager { } } if !to_delete.is_empty() { - //TODO: send message to background thread, UIToBackMessage::DeletePack event_sender.send(UIToBackMessage::DeletePacks(to_delete)); } }); @@ -541,7 +540,7 @@ impl PackageUIManager { self.import_status = None; } } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - //TODO: send message to background thread, UIToBackMessage::ImportPack + //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? let import_status = Arc::new(Mutex::default()); self.import_status = Some(import_status.clone()); Self::pack_importer(import_status); @@ -601,6 +600,17 @@ impl PackageUIManager { } pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { + /* + We save in a file with the name of the package, while we keep track of it from a uuid point of view. + It means we can have duplicates unless package with same name is deleted. + */ + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == texture_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); self.tasks.save_texture(&mut texture_pack, true); self.packs.insert(texture_pack.uuid, texture_pack); } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index a7765f7..a1cdeb5 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -21,12 +21,8 @@ use uuid::Uuid; #[derive(Default, Debug, Clone)] pub struct PackCore { /* - TODO: - this is a temporary holder of data - it might be deleted at some point to avoid copy of data => into parts, break it down as fields and reassemble those. - it mean the "new" functions have to be rewritten to take those fields instead of a PackCore as a whole. - The "new" functions might even be removed to not be able to build them independantly. - Once built the UI version would be sent to UI. + PackCore is a temporary holder of data + It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. */ pub name: String, pub uuid: Uuid, @@ -55,7 +51,7 @@ impl PackCore { category_uuid.clone() } else { //TODO: if import is "dirty", create missing category - //default import mode is "strict" (get inspiration from HTML modes) + //TODO: default import mode is "strict" (get inspiration from HTML modes) debug!("There is no defined category for {}", full_category_name); let mut n = 0; @@ -297,7 +293,6 @@ impl Category { for full_category_name in third_pass_categories_ref { if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { if let Some(parent) = cat.parent { - //FIXME: this only look for top level if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { parent_category.children.insert(cat.guid.clone(), cat); } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index e073bf9..b6c5ba3 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -128,7 +128,6 @@ impl JokoRenderer { } pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - //FIXME: have a double buffering self.gl.prepare_frame(latest_framebuffer_size_getter); unsafe { let gl = self.gl.context.clone(); diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index d439ee4..d9236f9 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -47,15 +47,15 @@ pub struct Jokolay { impl Jokolay { pub fn new(jokolay_dir: Arc) -> Result { - //TODO: we could have two mumble_manager, one for UI, one for backend, each keeping its own copy - //this would allow overwriting from gui to back ? - //if we want to be able to edit the link, one need to put a "form submission" logic. + //We have two mumble_managers, one for UI, one for backend, each keeping its own copy + //this avoid transmition between threads to read same data from system + //TODO: if we want to be able to edit the link, one need to put a "form submission" logic. let mumble_data_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; let mumble_ui_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - let (data_packages, texture_packages) = load_all_from_dir(&jokolay_dir).wrap_err("failed to load packages")?; + let (data_packages, texture_packages) = load_all_from_dir(Arc::clone(&jokolay_dir)).wrap_err("failed to load packages")?; let mut package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; @@ -121,6 +121,7 @@ impl Jokolay { u2b_receiver: std::sync::mpsc::Receiver, ) { let background_thread = std::thread::spawn(move || { + //TODO here, load the directory with packages Self::background_loop(Arc::clone(&app), state, b2u_sender, u2b_receiver); }); } @@ -291,6 +292,7 @@ impl Jokolay { fn handle_b2u_message( gui: &mut JokolayGui, local_state: &mut JokolayState, + u2b_sender: &std::sync::mpsc::Sender, msg: BackToUIMessage ) { match msg { @@ -311,6 +313,7 @@ impl Jokolay { tracing::trace!("Handling of BackToUIMessage::LoadedPack"); gui.package_manager.save(pack_texture); gui.package_manager.import_status = None; + u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); } BackToUIMessage::Loading => { unimplemented!("Handling of BackToUIMessage::Loading has not been implemented yet"); @@ -357,22 +360,35 @@ impl Jokolay { let mut local_state = self.state.clone(); let mut nb_frames: u128 = 0; let mut nb_messages: u128 = 0; + let max_nb_messages_per_loop: u128 = 100; //u2u_sender.send(UIToUIMessage::Present);// force a first drawing loop { { + let mut nb_message_on_curent_loop: u128 = 0; tracing::trace!("glfw event loop, {} frames, {} messages", nb_frames, nb_messages); //untested and might crash due to .unwrap() let mut gui = self.gui.lock().unwrap(); - while let Ok(msg) = b2u_receiver.try_recv() { - nb_messages += 1; - Self::handle_b2u_message(gui.deref_mut(), &mut local_state, msg); - } while let Ok(msg) = u2u_receiver.try_recv() { nb_messages += 1; Self::handle_u2u_message(gui.deref_mut(), &mut local_state, msg); + nb_message_on_curent_loop += 1; + if nb_message_on_curent_loop == max_nb_messages_per_loop { + break; + } + } + if nb_message_on_curent_loop < max_nb_messages_per_loop { + while let Ok(msg) = b2u_receiver.try_recv() { + nb_messages += 1; + Self::handle_b2u_message(gui.deref_mut(), &mut local_state, &u2b_sender, msg); + nb_message_on_curent_loop += 1; + if nb_message_on_curent_loop == max_nb_messages_per_loop { + break; + } + } } } + let mut gui = self.gui.lock().unwrap(); let JokolayGui { frame_stats, From b3a43562c21835b7ce3e273e2ee180fea40518d2 Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 4 Apr 2024 19:09:26 +0200 Subject: [PATCH 20/54] add uuid collision avoidance + preparation work for lazy loading of maps --- Cargo.toml | 13 +- crates/joko_core/src/task/mod.rs | 3 + .../joko_marker_format/src/io/deserialize.rs | 276 ++++++++---------- .../src/manager/pack/file_selection.rs | 17 +- .../src/manager/pack/loaded.rs | 50 ++-- .../joko_marker_format/src/manager/package.rs | 72 ++++- crates/joko_marker_format/src/message.rs | 1 + crates/joko_marker_format/src/pack/mod.rs | 197 +++++++++---- crates/joko_marker_format/src/pack/route.rs | 5 + crates/joko_render/src/billboard.rs | 4 +- crates/jokolay/Cargo.toml | 1 + crates/jokolay/src/app/mod.rs | 53 +++- 12 files changed, 439 insertions(+), 253 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30dc30c..0cf5de7 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,17 @@ uuid = { version = "*" } itertools = { version = "*" } ordered_hash_map = { version = "*", features= ["serde"] } + +#https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build +[profile.dev] +split-debuginfo = "unpacked" + +[profile.dev.build-override] +opt-level = 3 + + [profile.release] strip = "symbols" -lto = true + +#lto make the build very slow +#lto = true diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs index b413bdf..a165200 100644 --- a/crates/joko_core/src/task/mod.rs +++ b/crates/joko_core/src/task/mod.rs @@ -66,6 +66,9 @@ where self.result_receiver.recv() } + pub fn count(&self) -> i32 { + self.nb.load(std::sync::atomic::Ordering::Relaxed) + } pub fn is_running(&self) -> bool { let nb = self.nb.load(std::sync::atomic::Ordering::Relaxed); nb != 0 diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 5a61a09..6d85309 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -1,15 +1,15 @@ use joko_core::RelativePath; +use miette::{bail, Context, IntoDiagnostic, Result}; use crate::{ - pack::{Category, RawCategory, CommonAttributes, Marker, PackCore, TBin, TBinStatus, Trail, MapData, Route, prefix_parent}, + pack::{prefix_parent, Category, CommonAttributes, MapData, Marker, PackCore, RawCategory, Route, TBin, TBinStatus, Trail}, BASE64_ENGINE, }; use base64::Engine; -use cap_std::fs_utf8::Dir; +use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; use indexmap::IndexMap; -use miette::{bail, Context, IntoDiagnostic, Result}; -use std::{collections::{VecDeque, HashMap}, io::Read, sync::Arc}; +use std::{collections::{VecDeque, HashMap}, io::Read}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, info, info_span, instrument, trace, warn}; use uuid::Uuid; @@ -19,16 +19,18 @@ use super::XotAttributeNameIDs; pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { //called from already parsed data - let mut pack = PackCore::default(); - pack.uuid = Uuid::new_v4(); + let mut core_pack = PackCore::new(); // walks the directory and loads all files into the hashmap + let start = std::time::SystemTime::now(); recursive_walk_dir_and_read_images_and_tbins( dir, - &mut pack.textures, - &mut pack.tbins, + &mut core_pack.textures, + &mut core_pack.tbins, &RelativePath::default(), ) .wrap_err("failed to walk dir when loading a markerpack")?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of core package textures from disk took {} ms", elaspsed.as_millis()); //categories are required to register other objects let cats_xml = dir @@ -36,8 +38,10 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { .into_diagnostic() .wrap_err("failed to read categories.xml")?; let categories_file = String::from("categories.xml"); - parse_categories_file(&categories_file, &cats_xml, &mut pack) + let parse_categories_file_start = std::time::SystemTime::now(); + parse_categories_file(&categories_file, &cats_xml, &mut core_pack) .wrap_err("failed to parse category file")?; + info!("parse_categories_file took {} ms", parse_categories_file_start.elapsed().unwrap_or_default().as_millis()); // parse map data of the pack for entry in dir @@ -45,11 +49,11 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { .into_diagnostic() .wrap_err("failed to read entries of pack dir")? { - let entry = entry + let dir_entry = entry .into_diagnostic() .wrap_err("entry error whiel reading xml files")?; - let name = entry + let name = dir_entry .file_name() .into_diagnostic() .wrap_err("map data entry name not utf-8")? @@ -63,19 +67,11 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { } map_id => { // parse map file - let span_guard = info_span!("map", map_id).entered(); + let span_guard = info_span!("load map", map_id).entered(); if let Ok(map_id) = map_id.parse::() { - let mut xml_str = String::new(); - entry - .open() - .into_diagnostic() - .wrap_err("failed to open xml file")? - .read_to_string(&mut xml_str) - .into_diagnostic() - .wrap_err("faield to read xml string")?; - parse_map_file(map_id, &xml_str, &mut pack).wrap_err_with(|| { - miette::miette!("error parsing map file: {map_id}") - })?; + //let mut partial_pack = PackCore::partial(&core_pack.all_categories); + load_map_file(map_id, &dir_entry, &mut core_pack)?; + //core_pack.merge_partial(partial_pack); } else { info!("unrecognized xml file {map_id}") } @@ -87,20 +83,20 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { trace!("file ignored: {name}") } } - info!("Entities registered (category + markers): {}", pack.entities_parents.len()); - info!("Categories registered: {}", pack.all_categories.len()); - info!("Markers registered: {}", pack.entities_parents.len() - pack.all_categories.len()); - info!("Maps registered: {}", pack.maps.len()); - info!("Textures registered: {}", pack.textures.len()); - info!("Trail binaries registered: {}", pack.tbins.len()); - Ok(pack) + info!("Entities registered (category + markers): {}", core_pack.entities_parents.len()); + info!("Categories registered: {}", core_pack.all_categories.len()); + info!("Markers registered: {}", core_pack.entities_parents.len() - core_pack.all_categories.len()); + info!("Maps registered: {}", core_pack.maps.len()); + info!("Textures registered: {}", core_pack.textures.len()); + info!("Trail binaries registered: {}", core_pack.tbins.len()); + Ok(core_pack) } fn recursive_walk_dir_and_read_images_and_tbins( dir: &Dir, - images: &mut OrderedHashMap>, - tbins: &mut OrderedHashMap, + images: &mut HashMap>, + tbins: &mut HashMap, parent_path: &RelativePath, ) -> Result<()> { for entry in dir @@ -389,7 +385,34 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack } -fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result<()> { +fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Result<()> { + let mut xml_str = String::new(); + dir_entry + .open() + .into_diagnostic() + .wrap_err("failed to open xml file")? + .read_to_string(&mut xml_str) + .into_diagnostic() + .wrap_err("faield to read xml string")?; + //TODO: launch an async load of the file + make a priority queue to have current map first + parse_map_xml_string(map_id, &xml_str, target).wrap_err_with(|| { + miette::miette!("error parsing map file: {map_id}") + }) +} + +fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) -> Result<()> { + /* + fields read: + all_categories + + fields modified: + maps + all_categories + late_discovery_categories + source_files + tbins + entities_parents + */ let mut tree = Xot::new(); let root_node = tree .parse(map_xml_str) @@ -417,24 +440,27 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result .ok_or_else(|| miette::miette!("missing pois node"))?; for poi_node in tree.children(pois) { - if let Some(child) = tree.element(poi_node) { - let full_category_name = child + if let Some(child_element) = tree.element(poi_node) { + let full_category_name = child_element .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); if full_category_name.is_empty() { - panic!("full_category_name is empty {:?} {:?}", map_xml_str, child); + panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); } let span_guard = info_span!("category", full_category_name).entered(); - let category_uuid = pack.get_or_create_category_uuid(&full_category_name); - - let raw_uid = child.get_attribute(names.guid); + let raw_uid = child_element.get_attribute(names.guid); if raw_uid.is_none() { - info!("This POI is either invalid or inside a Route {:?}", child); + info!("This POI is either invalid or inside a Route {:?}", child_element); span_guard.exit(); continue; } + let source_file_name = child_element.get_attribute(names._source_file_name).unwrap_or_default().to_string(); + target.source_files.insert(source_file_name.clone(), true); + + //FIXME: this needs to be changed for partial load + let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed let guid = raw_uid.and_then(|guid| { let mut buffer = [0u8; 20]; BASE64_ENGINE @@ -444,16 +470,19 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result }) .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; - let source_file_name = child.get_attribute(names._source_file_name).unwrap_or_default().to_string(); - pack.source_files.insert(source_file_name.clone(), true); - if child.name() == names.route { - debug!("Found a route in core pack {:?}", child); - import_route_as_trail(pack, &names, &tree, &poi_node, child, full_category_name, &category_uuid, source_file_name) + if child_element.name() == names.route { + debug!("Found a route in core pack {:?}", child_element); + let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, &category_uuid, source_file_name.clone()); + if let Some(route) = route { + target.register_route(full_category_name, route); + } else { + info!("Could not parse route {:?}", child_element); + } } - else if child.name() == names.poi { - debug!("Found a POI in core pack {:?}", child); - if child + else if child_element.name() == names.poi { + debug!("Found a POI in core pack {:?}", child_element); + if child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) .ok_or_else(|| miette::miette!("invalid mapid"))? @@ -461,25 +490,25 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result { bail!("mapid doesn't match the file name"); } - let xpos = child + let xpos = child_element .get_attribute(names.xpos) .unwrap_or_default() .parse::() .into_diagnostic()?; - let ypos = child + let ypos = child_element .get_attribute(names.ypos) .unwrap_or_default() .parse::() .into_diagnostic()?; - let zpos = child + let zpos = child_element .get_attribute(names.zpos) .unwrap_or_default() .parse::() .into_diagnostic()?; let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child, &names); + ca.update_common_attributes_from_element(child_element, &names); - pack.register_uuid(&full_category_name, &guid); + target.register_uuid(&full_category_name, &guid); let marker = Marker { position: [xpos, ypos, zpos].into(), map_id, @@ -490,13 +519,13 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result source_file_name }; - if !pack.maps.contains_key(&map_id) { - pack.maps.insert(map_id, MapData::default()); + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); } - pack.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); - } else if child.name() == names.trail { - debug!("Found a trail in core pack {:?}", child); - if child + target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); + } else if child_element.name() == names.trail { + debug!("Found a trail in core pack {:?}", child_element); + if child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) .ok_or_else(|| miette::miette!("invalid mapid"))? @@ -505,9 +534,9 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result bail!("mapid doesn't match the file name"); } let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child, &names); + ca.update_common_attributes_from_element(child_element, &names); - pack.register_uuid(&full_category_name, &guid); + target.register_uuid(&full_category_name, &guid); let trail = Trail { category: full_category_name, parent: category_uuid.clone(), @@ -518,10 +547,10 @@ fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result source_file_name }; - if !pack.maps.contains_key(&map_id) { - pack.maps.insert(map_id, MapData::default()); + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); } - pack.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); + target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); } span_guard.exit(); } @@ -641,7 +670,7 @@ fn parse_category_categories_xml_recursive( pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { //called to import a new pack // all the contents of ZPack - let mut pack = PackCore::default(); + let mut pack = PackCore::new(); // parse zip file let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) .into_diagnostic() @@ -668,6 +697,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } } xmls.sort();//build back the intended order in folder, since zip_archive may not give the files in order. + let start = std::time::SystemTime::now(); for name in images { let span = info_span!("load image", name).entered(); let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); @@ -709,6 +739,8 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } std::mem::drop(span); } + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of taco package textures from disk took {} ms", elaspsed.as_millis()); let span_guard_categories = info_span!("deserialize xml: categories").entered(); @@ -889,16 +921,16 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }; for child_node in tree.children(pois) { - let child = match tree.element(child_node) { + let child_element = match tree.element(child_node) { Some(ele) => ele, None => continue, }; - let full_category_name = child + let full_category_name = child_element .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); if full_category_name.is_empty() { - info!("full_category_name is empty {:?}", child); + info!("full_category_name is empty {:?}", child_element); continue; } if ! pack.category_exists(&full_category_name) { @@ -906,15 +938,28 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } let category_uuid = pack.get_or_create_category_uuid(&full_category_name); - debug!("import element: {:?}", child); - if child.name() == names.poi { - import_poi(&mut pack, &names, &child, full_category_name, &category_uuid, source_file_name.clone()); - } else if child.name() == names.trail { - import_trail(&mut pack, &names, &child, full_category_name, &category_uuid, source_file_name.clone()); - } else if child.name() == names.route { - import_route_as_trail(&mut pack, &names, &tree, &child_node, &child, full_category_name, &category_uuid, source_file_name.clone()); + debug!("import element: {:?}", child_element); + if child_element.name() == names.poi { + if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_marker(full_category_name, marker)?; + } else { + debug!("Could not parse POI"); + } + } else if child_element.name() == names.trail { + if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_trail(full_category_name, trail)?; + } else { + debug!("Could not parse Trail"); + } + } else if child_element.name() == names.route { + let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, &category_uuid, source_file_name.clone()); + if let Some(route) = route { + pack.register_route(full_category_name, route)?; + } else { + info!("Could not parse route {:?}", child_element); + } } else { - info!("unknown element: {:?}", child); + info!("unknown element: {:?}", child_element); } } @@ -1006,7 +1051,6 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { } fn parse_route( - _pack: &mut PackCore, names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, @@ -1127,84 +1171,6 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } -fn import_poi(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: String, category_uuid: &Uuid, source_file_name: String) { - if let Some(marker) = parse_marker(pack, names, poi_element, &category_name, category_uuid, source_file_name) { - pack.register_uuid(&category_name, &marker.guid); - if !pack.maps.contains_key(&marker.map_id) { - pack.maps.insert(marker.map_id, MapData::default()); - } - pack.maps.get_mut(&marker.map_id).unwrap().markers.insert(marker.guid, marker); - } else { - debug!("Could not parse POI"); - } -} - - -fn import_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: String, category_uuid: &Uuid, source_file_name: String) { - if let Some(trail) = parse_trail(pack, names, trail_element, &category_name, category_uuid, source_file_name) { - pack.register_uuid(&category_name, &trail.guid); - if !pack.maps.contains_key(&trail.map_id) { - pack.maps.insert(trail.map_id, MapData::default()); - } - pack.maps.get_mut(&trail.map_id).unwrap().trails.insert(trail.guid, trail); - } else { - debug!("Could not parse Trail"); - } - -} - -fn route_to_tbin(route: &Route) -> TBin { - assert!( route.path.len() > 1); - TBin { - map_id: route.map_id, - version: 0, - nodes: route.path.clone(), - } -} - -fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { - let mut props = CommonAttributes::default(); - props.set_texture(None); - props.set_trail_data(Some(file_path.clone())); - debug!("Build dynamic trail {}", route.guid); - Trail { - map_id: route.map_id, - category: route.category.clone(), - parent: route.parent.clone(), - guid: route.guid, - props: props, - dynamic: true, - source_file_name: route.source_file_name.clone(), - } -} - -fn import_route_as_trail( - pack: &mut PackCore, - names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, - category_name: String, - category_uuid: &Uuid, - source_file_name: String -) { - if let Some(route) = parse_route(pack, names, tree, route_node, route_element, &category_name, category_uuid, source_file_name) { - let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); - let file_path: RelativePath = file_name.parse().unwrap(); - let trail = route_to_trail(&route, &file_path); - let tbin = route_to_tbin(&route); - pack.register_uuid(&category_name, &route.guid); - pack.tbins.insert(file_path, tbin);//there may be duplicates since we load and save each time - if !pack.maps.contains_key(&trail.map_id) { - pack.maps.insert(trail.map_id, MapData::default()); - } - pack.maps.get_mut(&trail.map_id).unwrap().trails.insert(trail.guid, trail); - pack.maps.get_mut(&route.map_id).unwrap().routes.insert(route.guid, route); - } else { - info!("Could not parse route {:?}", route_element); - } -} - #[instrument(skip(zip_archive))] fn read_file_bytes_from_zip_by_name( name: &str, diff --git a/crates/joko_marker_format/src/manager/pack/file_selection.rs b/crates/joko_marker_format/src/manager/pack/file_selection.rs index 5a8013a..73d27cd 100644 --- a/crates/joko_marker_format/src/manager/pack/file_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/file_selection.rs @@ -1,19 +1,18 @@ use std::{ collections::BTreeMap, }; -use ordered_hash_map::{OrderedHashMap}; pub struct SelectedFileManager { - data: OrderedHashMap, + data: BTreeMap, } impl<'a> SelectedFileManager { pub fn new( - selected_files: &OrderedHashMap, - pack_source_files: &OrderedHashMap, + selected_files: &BTreeMap, + pack_source_files: &BTreeMap, currently_used_files: &BTreeMap, ) -> Self { - let mut list_of_enabled_files: OrderedHashMap = Default::default(); + let mut list_of_enabled_files: BTreeMap = Default::default(); SelectedFileManager::recursive_get_full_names( &selected_files, &pack_source_files, @@ -23,16 +22,16 @@ impl<'a> SelectedFileManager { Self { data: list_of_enabled_files } } fn recursive_get_full_names( - _selected_files: &OrderedHashMap, - _pack_source_files: &OrderedHashMap, + _selected_files: &BTreeMap, + _pack_source_files: &BTreeMap, currently_used_files: &BTreeMap, - list_of_enabled_files: &mut OrderedHashMap + list_of_enabled_files: &mut BTreeMap ){ for (key, v) in currently_used_files.iter() { list_of_enabled_files.insert(key.clone(), *v); } } - pub fn cloned_data(&self) -> OrderedHashMap { + pub fn cloned_data(&self) -> BTreeMap { self.data.clone() } pub fn is_selected(&self, source_file_name: &String) -> bool { diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index 38cf1b9..b064c95 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -30,11 +30,12 @@ use crate::manager::pack::category_selection::CategorySelection; use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; +//TODO: separate in front and back tasks pub (crate) struct PackTasks { //an object that can handle such tasks should be passed as argument of any function that may required an async action save_texture_task: AsyncTask>, save_data_task: AsyncTask>, - load_pack_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> + load_all_packs_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> } #[derive(Clone)] @@ -46,9 +47,9 @@ pub struct LoadedPackData { //pub core: PackCore, pub categories: IndexMap, pub all_categories: HashMap, - pub source_files: OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: OrderedHashMap, - selected_files: OrderedHashMap, + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, + selected_files: BTreeMap, _is_dirty: bool,//there was an edition in the package itself // loca copy in the data side of what is exposed in UI @@ -67,8 +68,8 @@ pub struct LoadedPackTexture { /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. /// eg: Active categories, activation data etc.. pub dir: Arc, - pub tbins: OrderedHashMap, - pub textures: OrderedHashMap>, + pub tbins: HashMap, + pub textures: HashMap>, /// The selection of categories which are "enabled" and markers belonging to these may be rendered selectable_categories: OrderedHashMap, @@ -84,24 +85,20 @@ impl PackTasks { Self { save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), - load_pack_task: AsyncTaskGuard::new(load_all_from_dir), + load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), } } pub fn is_running(&self) -> bool { self.save_texture_task.lock().unwrap().is_running() || self.save_data_task.lock().unwrap().is_running() } - pub fn status_as_color(&self) -> egui::Color32 { - //we can choose whatever color code we want to focus on load, save, network queries, anything. - let max_nb_saving = 2; - let nb_saving = - self.save_texture_task.lock().unwrap().is_running() as u8 - + self.save_data_task.lock().unwrap().is_running() as u8 - ; - let color_saving = nb_saving * 0xff / max_nb_saving; - egui::Color32::from_rgb(color_saving, 0, 0) + pub fn count(&self) -> i32 { + 0 + + self.save_texture_task.lock().unwrap().count() + + self.save_data_task.lock().unwrap().count() + + self.load_all_packs_task.lock().unwrap().count() } - + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { if status { std::mem::take(&mut texture_pack._is_dirty); @@ -119,6 +116,14 @@ impl PackTasks { ); } } + pub fn load_all_packs(&self, jokolay_dir: Arc) { + self.load_all_packs_task.lock().unwrap().send( + jokolay_dir + ); + } + pub fn wait_for_load_all_packs(&self) -> Result<(BTreeMap, BTreeMap)> { + self.load_all_packs_task.lock().unwrap().recv().unwrap() + } fn change_map( &self, @@ -245,8 +250,11 @@ impl LoadedPackData { .open_dir(Self::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to open core pack directory")?; + let start = std::time::SystemTime::now(); let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. //let selectable_categories = CategorySelection::default_from_pack_core(&core); let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); @@ -708,8 +716,12 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, L .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to open core pack directory")?; + let start = std::time::SystemTime::now(); let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - Ok(build_from_core(name, pack_dir, core)) + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); + let res = build_from_core(name.clone(), pack_dir, core); + Ok(res) } diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index 3bed4dd..4441b9d 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -14,7 +14,7 @@ use joko_core::RelativePath; use jokolink::MumbleLink; use miette::{Context, IntoDiagnostic, Result}; use uuid::Uuid; -use crate::message::{UIToBackMessage, UIToUIMessage}; +use crate::{load_all_from_dir, message::{UIToBackMessage, UIToUIMessage}}; use crate::{message::BackToUIMessage, pack::CommonAttributes}; use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; @@ -205,6 +205,7 @@ impl PackageDataManager { let mut tasks = &self.tasks; for (uuid, pack) in self.packs.iter_mut() { let span_guard = info_span!("Updating package status").entered(); + b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); tasks.save_data(pack, pack.is_dirty()); pack.tick( &b2u_sender, @@ -238,7 +239,7 @@ impl PackageDataManager { self.packs.remove(&uuid); } } - pub fn save(&mut self, mut data_pack: LoadedPackData) { + pub fn save(&mut self, mut data_pack: LoadedPackData) -> Uuid { let mut to_delete: Vec = Vec::new(); for (uuid, pack) in self.packs.iter() { if pack.name == data_pack.name { @@ -247,7 +248,34 @@ impl PackageDataManager { } self.delete_packs(to_delete); self.tasks.save_data(&mut data_pack, true); - self.packs.insert(data_pack.uuid, data_pack); + let mut uuid_to_insert = data_pack.uuid.clone(); + while self.packs.contains_key(&uuid_to_insert) {//collision avoidance + trace!("Uuid collision detected for {} for package {}", uuid_to_insert, data_pack.name); + uuid_to_insert = Uuid::new_v4(); + } + data_pack.uuid = uuid_to_insert; + self.packs.insert(uuid_to_insert, data_pack); + uuid_to_insert + } + + pub fn load_all( + &mut self, + jokolay_dir: Arc, + b2u_sender: &std::sync::mpsc::Sender, + ) { + // Called only once at application start. + b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + self.tasks.load_all_packs(jokolay_dir); + if let Ok((data_packages, texture_packages)) = self.tasks.wait_for_load_all_packs() { + for (uuid, data_pack) in data_packages { + self.packs.insert(uuid, data_pack); + } + for (uuid, texture_pack) in texture_packages { + b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + } + b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); + } + } } @@ -436,7 +464,9 @@ impl PackageUIManager { &mut self, u2b_sender: &std::sync::mpsc::Sender, u2u_sender: &std::sync::mpsc::Sender, - ui: &mut egui::Ui + ui: &mut egui::Ui, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, ) { ui.menu_button("Markers", |ui| { if self.show_only_active { @@ -464,11 +494,41 @@ impl PackageUIManager { } }); - if self.tasks.is_running() { - let sp = egui::Spinner::new().color(self.tasks.status_as_color()); + if self.tasks.is_running() || nb_running_tasks_on_back > 0 || nb_running_tasks_on_network > 0{ + let sp = egui::Spinner::new().color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); ui.add(sp); } } + pub fn status_as_color(&self, nb_running_tasks_on_back: i32, nb_running_tasks_on_network: i32) -> egui::Color32 { + //we can choose whatever color code we want to focus on load, save, network queries, anything. + let nb_running_tasks_on_ui = self.tasks.count(); + //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 + let color_ui = if nb_running_tasks_on_ui > 0 { + let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; + let res = nb_ui_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_back = if nb_running_tasks_on_back > 0 { + let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; + let res = nb_bask_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_network = if nb_running_tasks_on_network > 0 { + let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; + let res = nb_network_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + egui::Color32::from_rgb(color_ui, color_back, color_network) + } fn gui_file_manager( &mut self, diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs index bd4c489..e82be67 100644 --- a/crates/joko_marker_format/src/message.rs +++ b/crates/joko_marker_format/src/message.rs @@ -49,6 +49,7 @@ pub enum BackToUIMessage { MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), MumbleLink(Option), MumbleLinkChanged,//tell there is a need to resize + NbTasksRunning(i32),//tell the number of taks running in background PackageActiveElements(Uuid, HashSet),// first is the package reference, second is the list of active elements in the package. TextureSwapChain,// The list of texture to load was changed, will be soon followed by a RenderSwapChain TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index a1cdeb5..d0a5940 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -3,12 +3,12 @@ mod marker; mod trail; mod route; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use indexmap::IndexMap; use ordered_hash_map::OrderedHashMap; -use tracing::{info, debug}; +use tracing::{debug, info, trace}; use joko_core::RelativePath; pub use common::*; @@ -17,32 +17,117 @@ pub(crate) use trail::*; pub(crate) use route::*; use uuid::Uuid; - #[derive(Default, Debug, Clone)] +pub(crate) struct MapData { + pub markers: IndexMap, + pub routes: IndexMap, + pub trails: IndexMap, +} + +#[derive(Debug, Clone)] +pub(crate) struct RawCategory { + pub guid: Uuid, + pub parent_name: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, +} + +#[derive(Debug, Clone)] +pub(crate) struct Category { + pub guid: Uuid, + pub parent: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, + pub children: IndexMap, +} + +#[derive(Debug, Clone)] pub struct PackCore { /* PackCore is a temporary holder of data It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. */ - pub name: String, pub uuid: Uuid, - pub textures: OrderedHashMap>, - pub tbins: OrderedHashMap, - pub categories: IndexMap, + pub textures: HashMap>, + pub(crate) tbins: HashMap, + pub(crate) categories: IndexMap, pub all_categories: HashMap, pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. pub entities_parents: HashMap, - pub source_files: OrderedHashMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: OrderedHashMap, + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, } +fn route_to_tbin(route: &Route) -> TBin { + assert!( route.path.len() > 1); + TBin { + map_id: route.map_id, + version: 0, + nodes: route.path.clone(), + } +} + +fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { + let mut props = CommonAttributes::default(); + props.set_texture(None); + props.set_trail_data(Some(file_path.clone())); + Trail { + map_id: route.map_id, + category: route.category.clone(), + parent: route.parent.clone(), + guid: route.guid, + props: props, + dynamic: true, + source_file_name: route.source_file_name.clone(), + } +} + impl PackCore { + + pub fn new() -> Self { + let mut res = Self { + all_categories: Default::default(), + categories: Default::default(), + entities_parents: Default::default(), + late_discovery_categories: Default::default(), + maps: Default::default(), + source_files: Default::default(), + tbins: Default::default(), + textures: Default::default(), + uuid: Default::default(), + }; + res.uuid = Uuid::new_v4(); + res + } + pub fn partial(all_categories: &HashMap) -> Self { + // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. + let mut res: Self = Self::new(); + res.all_categories = all_categories.clone(); + res + } + + pub fn merge_partial(&mut self, partial_pack: PackCore) { + self.maps.extend(partial_pack.maps); + self.all_categories = partial_pack.all_categories; + self.late_discovery_categories.extend(partial_pack.late_discovery_categories); + self.source_files.extend(partial_pack.source_files); + self.tbins.extend(partial_pack.tbins); + self.entities_parents.extend(partial_pack.entities_parents); + } pub fn category_exists(&self, full_category_name: &String) -> bool { self.all_categories.contains_key(full_category_name) } - pub fn get_category_uuid(&mut self, full_category_name: &String) -> Option<&Uuid> { + pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { self.all_categories.get(full_category_name) } @@ -56,34 +141,78 @@ impl PackCore { let mut n = 0; let mut last_uuid: Option = None; - while let Some(category_name) = prefix_until_nth_char(&full_category_name, '.', n) { + while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { n += 1; - if let Some(parent_uuid) = self.all_categories.get(&category_name) { + if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { //FIXME: might want to make the difference between impacted parents and actual missing category self.late_discovery_categories.insert(*parent_uuid); last_uuid = Some(*parent_uuid); } else { let new_uuid = Uuid::new_v4(); - debug!("Partial create missing parent category: {} {}", category_name, new_uuid); - self.all_categories.insert(category_name.clone(), new_uuid); + debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); + self.all_categories.insert(parent_full_category_name.clone(), new_uuid); self.late_discovery_categories.insert(new_uuid); last_uuid = Some(new_uuid); } } - info!("{} uuid: {:?}", full_category_name, last_uuid); + trace!("{} uuid: {:?}", full_category_name, last_uuid); assert!(last_uuid.is_some()); last_uuid.unwrap() } } - pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) { + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ if let Some(parent_uuid) = self.all_categories.get(full_category_name) { - self.entities_parents.insert(*uuid, *parent_uuid); + let mut uuid_to_insert = uuid.clone(); + while self.entities_parents.contains_key(&uuid_to_insert) { + trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); + uuid_to_insert = Uuid::new_v4(); + } + self.entities_parents.insert(uuid_to_insert, *parent_uuid); + Ok(uuid_to_insert) } else { //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - debug!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid); + Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) } } + + pub(crate) fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; + marker.guid = uuid_to_insert; + if !self.maps.contains_key(&marker.map_id) { + self.maps.insert(marker.map_id, MapData::default()); + } + self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); + Ok(()) + } + + pub(crate) fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; + trail.guid = uuid_to_insert; + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + Ok(()) + } + + pub(crate) fn register_route(&mut self, full_category_name: String , mut route: Route) -> Result<(), miette::Error> { + let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); + let tbin_path: RelativePath = file_name.parse().unwrap(); + let uuid_to_insert = self.register_uuid(&full_category_name, &route.guid)?; + route.guid = uuid_to_insert; + let trail = route_to_trail(&route, &tbin_path); + let tbin = route_to_tbin(&route); + + self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); + Ok(()) + } + pub fn register_categories(&mut self) { let mut entities_parents: HashMap = Default::default(); let mut all_categories: HashMap = Default::default(); @@ -107,38 +236,6 @@ impl PackCore { } } -#[derive(Default, Debug, Clone)] -pub(crate) struct MapData { - pub markers: IndexMap, - pub routes: IndexMap, - pub trails: IndexMap, -} - -#[derive(Debug, Clone)] -pub(crate) struct RawCategory { - pub guid: Uuid, - pub parent_name: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, -} - -#[derive(Debug, Clone)] -pub(crate) struct Category { - pub guid: Uuid, - pub parent: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, - pub children: IndexMap, -} - pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { let res = s.match_indices(pat) diff --git a/crates/joko_marker_format/src/pack/route.rs b/crates/joko_marker_format/src/pack/route.rs index e10d42b..2bef53b 100644 --- a/crates/joko_marker_format/src/pack/route.rs +++ b/crates/joko_marker_format/src/pack/route.rs @@ -1,6 +1,11 @@ +use joko_core::RelativePath; use uuid::Uuid; use glam::Vec3; +use crate::pack::CommonAttributes; + +use super::{TBin, Trail}; + #[derive(Debug, Clone)] pub(crate) struct Route { pub category: String, diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index ea9e01c..b0077b6 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -6,7 +6,7 @@ use egui_render_three_d::{ GpuTexture, }; use joko_marker_format::message::{MarkerVertex, MarkerObject, TrailObject}; -use tracing::{error, info, warn}; +use tracing::{error, info, trace, warn}; use crate::gl_error; @@ -74,7 +74,7 @@ impl BillBoardRenderer { } pub fn swap(&mut self) { - info!("swap UI to display {} markers, {} trails", + trace!("swap UI to display {} markers, {} trails", self.markers_wip.len(), self.trails_wip.len() ); diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index bfb4f1d..5883649 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -28,4 +28,5 @@ miette = { workspace = true } egui = { workspace = true, features = ["serde"] } rayon = { workspace = true } +uuid = { workspace = true } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index d9236f9..0c1655e 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,9 +1,15 @@ -use std::{ops::DerefMut, sync::{mpsc::channel, Arc, Mutex}, thread}; +use std::{ + collections::BTreeMap, + ops::DerefMut, + sync::{Arc, Mutex}, + thread +}; use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; mod wm; +use uuid::Uuid; use init::get_jokolay_dir; use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; //use jmf::FileManager; @@ -20,7 +26,9 @@ struct JokolayState { link: Option, window_changed: bool, choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI - list_of_textures_changed: bool//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + list_of_textures_changed: bool,//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + nb_running_tasks_on_back: i32,// store the number of running tasks in background thread + nb_running_tasks_on_network: i32,// store the number of running tasks (requests) in progress } struct JokolayApp { mumble_manager: MumbleManager, @@ -38,7 +46,7 @@ struct JokolayGui { } #[allow(unused)] pub struct Jokolay { - jdir: Arc, + jokolay_dir: Arc, gui: Arc>, app: Arc>>, state: JokolayState, @@ -55,7 +63,8 @@ impl Jokolay { let mumble_ui_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - let (data_packages, texture_packages) = load_all_from_dir(Arc::clone(&jokolay_dir)).wrap_err("failed to load packages")?; + let data_packages: BTreeMap = Default::default(); + let texture_packages: BTreeMap = Default::default(); let mut package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; @@ -90,7 +99,7 @@ impl Jokolay { package_ui_manager.late_init(&egui_context); Ok(Self { - jdir: jokolay_dir, + jokolay_dir, gui: Arc::new(Mutex::new(JokolayGui { frame_stats, joko_renderer, @@ -110,18 +119,30 @@ impl Jokolay { window_changed: true, choice_of_category_changed: false, list_of_textures_changed: false, + nb_running_tasks_on_back: 0, + nb_running_tasks_on_network: 0, } }) } fn start_background_loop( + jokolay_dir: Arc, app: Arc>>, state: JokolayState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, ) { let background_thread = std::thread::spawn(move || { - //TODO here, load the directory with packages + // Load the directory with packages in the background process + { + //TODO: lazy loading to load maps only when on it + let mut app = app.lock().unwrap(); + let JokolayApp { + mumble_manager, + package_manager + } = &mut app.deref_mut().as_mut(); + package_manager.load_all(Arc::clone(&jokolay_dir), &b2u_sender); + } Self::background_loop(Arc::clone(&app), state, b2u_sender, u2b_receiver); }); } @@ -188,9 +209,9 @@ impl Jokolay { } match package_manager.marker_packs_dir.open_dir(name) { Ok(dir) => { - let (mut data_pack, texture_pack) = build_from_core(name.to_string(), dir.into(), pack); + let (mut data_pack, mut texture_pack) = build_from_core(name.to_string(), dir.into(), pack); tracing::trace!("Package loaded into data and texture"); - package_manager.save(data_pack); + texture_pack.uuid = package_manager.save(data_pack); b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); }, Err(e) => { @@ -240,7 +261,7 @@ impl Jokolay { } }; while let Ok(msg) = u2b_receiver.try_recv() { - Self::handle_u2b_message(package_manager, &mut local_state,&b2u_sender, msg); + Self::handle_u2b_message(package_manager, &mut local_state, &b2u_sender, msg); nb_messages += 1; } tracing::trace!("choice_of_category_changed: {}", local_state.choice_of_category_changed); @@ -330,6 +351,10 @@ impl Jokolay { //too verbose to trace local_state.window_changed = true; } + BackToUIMessage::NbTasksRunning(nb_tasks) => { + tracing::trace!("Handling of BackToUIMessage::NbTasksRunning"); + local_state.nb_running_tasks_on_back = nb_tasks; + } BackToUIMessage::PackageActiveElements(pack_uuid, active_elements) => { tracing::trace!("Handling of BackToUIMessage::PackageActiveElements"); gui.package_manager.update_pack_active_categories(pack_uuid, &active_elements); @@ -353,7 +378,7 @@ impl Jokolay { let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); - Self::start_background_loop(Arc::clone(&self.app), self.state.clone(), b2u_sender, u2b_receiver); + Self::start_background_loop(Arc::clone(&self.jokolay_dir), Arc::clone(&self.app), self.state.clone(), b2u_sender, u2b_receiver); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); @@ -534,7 +559,13 @@ impl Jokolay { } }, ); - package_manager.menu_ui(&u2b_sender, &u2u_sender, ui); + package_manager.menu_ui( + &u2b_sender, + &u2u_sender, + ui, + local_state.nb_running_tasks_on_back, + local_state.nb_running_tasks_on_network, + ); }); } ); From 1cb79fde56d9d0e5a6eb281147fe390e589835bd Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 5 Apr 2024 01:22:13 +0200 Subject: [PATCH 21/54] few fix on import of packages (tasks + routes) --- .../joko_marker_format/src/io/deserialize.rs | 306 ++++++++++-------- crates/joko_marker_format/src/lib.rs | 11 +- crates/joko_marker_format/src/manager/mod.rs | 1 + .../src/manager/pack/import.rs | 1 + .../src/manager/pack/loaded.rs | 2 +- .../joko_marker_format/src/manager/package.rs | 114 +++---- crates/joko_marker_format/src/message.rs | 6 +- crates/joko_marker_format/src/pack/mod.rs | 4 +- crates/jokolay/src/app/mod.rs | 81 +++-- 9 files changed, 308 insertions(+), 218 deletions(-) diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs index 6d85309..a2ac83d 100644 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ b/crates/joko_marker_format/src/io/deserialize.rs @@ -445,112 +445,116 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); - if full_category_name.is_empty() { - panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); - } + let span_guard = info_span!("category", full_category_name).entered(); - - let raw_uid = child_element.get_attribute(names.guid); - if raw_uid.is_none() { - info!("This POI is either invalid or inside a Route {:?}", child_element); - span_guard.exit(); - continue; - } + let source_file_name = child_element.get_attribute(names._source_file_name).unwrap_or_default().to_string(); target.source_files.insert(source_file_name.clone(), true); - //FIXME: this needs to be changed for partial load - let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed - let guid = raw_uid.and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - }) - .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; - - if child_element.name() == names.route { debug!("Found a route in core pack {:?}", child_element); - let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, &category_uuid, source_file_name.clone()); - if let Some(route) = route { - target.register_route(full_category_name, route); + let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_name.clone()); + if let Some(mut route) = route { + //TODO: make sure there is no "very late" discovery + //let category_uuid = target.get_or_create_category_uuid(&route.category); + //route.parent = category_uuid; + target.register_route(route)?; } else { info!("Could not parse route {:?}", child_element); } - } - else if child_element.name() == names.poi { - debug!("Found a POI in core pack {:?}", child_element); - if child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); + } else { + if full_category_name.is_empty() { + panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); } - let xpos = child_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let ypos = child_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let zpos = child_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - target.register_uuid(&full_category_name, &guid); - let marker = Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category: full_category_name, - parent: category_uuid.clone(), - attrs: ca, - guid, - source_file_name - }; - - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); + let raw_uid = child_element.get_attribute(names.guid); + if raw_uid.is_none() { + info!("This POI is either invalid or inside a Route {:?}", child_element); + span_guard.exit(); + continue; } - target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); - } else if child_element.name() == names.trail { - debug!("Found a trail in core pack {:?}", child_element); - if child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - target.register_uuid(&full_category_name, &guid); - let trail = Trail { - category: full_category_name, - parent: category_uuid.clone(), - map_id, - props: ca, - guid, - dynamic: false, - source_file_name - }; + //FIXME: this needs to be changed for partial load + let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed + let guid = raw_uid.and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + }) + .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); + if child_element.name() == names.poi { + debug!("Found a POI in core pack {:?}", child_element); + if child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or_else(|| miette::miette!("invalid mapid"))? + != map_id + { + bail!("mapid doesn't match the file name"); + } + let xpos = child_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let ypos = child_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let zpos = child_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + target.register_uuid(&full_category_name, &guid); + let marker = Marker { + position: [xpos, ypos, zpos].into(), + map_id, + category: full_category_name, + parent: category_uuid.clone(), + attrs: ca, + guid, + source_file_name + }; + + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); + } + target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); + } else if child_element.name() == names.trail { + debug!("Found a trail in core pack {:?}", child_element); + if child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or_else(|| miette::miette!("invalid mapid"))? + != map_id + { + bail!("mapid doesn't match the file name"); + } + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + target.register_uuid(&full_category_name, &guid); + let trail = Trail { + category: full_category_name, + parent: category_uuid.clone(), + map_id, + props: ca, + guid, + dynamic: false, + source_file_name + }; + + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); + } + target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); } - target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); } span_guard.exit(); } @@ -836,18 +840,28 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }; for child_node in tree.children(pois) { - let child = match tree.element(child_node) { + let child_element = match tree.element(child_node) { Some(ele) => ele, None => continue, }; - let full_category_name = child + let mut full_category_name = child_element .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); if full_category_name.is_empty() { - //ignore it silently since it might be a Route - //info!("full_category_name is empty {:?}", child); - continue; + if child_element.name() == names.route { + // If route, take the first element inside + if let Some(category) = parse_route_category(&names, &tree, &child_node, child_element) { + if category.is_empty() { + continue; + } + full_category_name = category; + } else { + continue; + } + } else { + continue; + } } if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { let category_uuid = Uuid::new_v4(); @@ -929,37 +943,41 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); - if full_category_name.is_empty() { - info!("full_category_name is empty {:?}", child_element); - continue; - } - if ! pack.category_exists(&full_category_name) { - panic!("Missing category {}, previous pass should have taken care of this", full_category_name); - } - let category_uuid = pack.get_or_create_category_uuid(&full_category_name); debug!("import element: {:?}", child_element); - if child_element.name() == names.poi { - if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { - pack.register_marker(full_category_name, marker)?; + if child_element.name() == names.route { + let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, source_file_name.clone()); + if let Some(mut route) = route { + //one must not create category anymore + route.parent = pack.get_category_uuid(&route.category).unwrap().clone(); + pack.register_route(route)?; } else { - debug!("Could not parse POI"); + info!("Could not parse route {:?}", child_element); } - } else if child_element.name() == names.trail { - if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { - pack.register_trail(full_category_name, trail)?; - } else { - debug!("Could not parse Trail"); + } else { + if full_category_name.is_empty() { + info!("full_category_name is empty {:?}", child_element); + continue; + } + if ! pack.category_exists(&full_category_name) { + panic!("Missing category {}, previous pass should have taken care of this", full_category_name); } - } else if child_element.name() == names.route { - let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, &category_uuid, source_file_name.clone()); - if let Some(route) = route { - pack.register_route(full_category_name, route)?; + let category_uuid = pack.get_or_create_category_uuid(&full_category_name); + if child_element.name() == names.poi { + if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_marker(full_category_name, marker)?; + } else { + debug!("Could not parse POI"); + } + } else if child_element.name() == names.trail { + if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_trail(full_category_name, trail)?; + } else { + debug!("Could not parse Trail"); + } } else { - info!("Could not parse route {:?}", child_element); + info!("unknown element: {:?}", child_element); } - } else { - info!("unknown element: {:?}", child_element); } } @@ -970,7 +988,8 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { Ok(pack) } -fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ + +fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { child .get_attribute(names.guid) .and_then(|guid| { @@ -984,7 +1003,9 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ None }) }) - .unwrap_or_else(Uuid::new_v4) +} +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ + parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) } fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { @@ -1050,13 +1071,34 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { Vec3{x, y, z} } + +fn parse_route_category( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, +) -> Option { + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + if let Some(cat) = child.get_attribute(names.category) { + return Some(cat.to_string()); + } + } + } + info!("Could not find a category for route element: {route_element:?}"); + None +} + fn parse_route( names: &XotAttributeNameIDs, tree: &Xot, route_node: &Node, route_element: &Element, category_name: &String, - category_uuid: &Uuid, source_file_name: String ) -> Option { @@ -1085,6 +1127,7 @@ fn parse_route( return None; } let mut category: String = category_name.clone(); + let mut category_uuid: Option = parse_optional_guid(names, route_element); let mut map_id: Option = route_element.get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()); for child_node in tree.children(*route_node) { @@ -1095,11 +1138,14 @@ fn parse_route( if child.name() == names.poi { let marker = parse_position(&names, child); path.push(marker); - if let Some(cat) = child.get_attribute(names.category) { - if category.is_empty() { + if category.is_empty() { + if let Some(cat) = child.get_attribute(names.category) { category = cat.to_string(); } } + if category_uuid.is_none() { + category_uuid = parse_optional_guid(names, &child) + } if map_id.is_none() { if let Some(node_map_id) = child .get_attribute(names.map_id) @@ -1118,11 +1164,15 @@ fn parse_route( info!("Could not find a map_id for route element: {route_element:?}"); return None; } + if category_uuid.is_none() { + info!("Could not find a uuid for route element: {route_element:?}"); + return None; + } debug!("found route with {:?} elements {route_element:?}", path.len()); Some(Route { category, - parent: category_uuid.clone(), + parent: category_uuid.unwrap(), path, reset_position, reset_range: reset_range.unwrap_or(0.0), diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs index 57eab99..de5b736 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_marker_format/src/lib.rs @@ -7,7 +7,16 @@ pub(crate) mod manager; pub(crate) mod pack; pub mod message; -pub use manager::{PackageDataManager, PackageUIManager, LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +pub use manager::{ + PackageDataManager, + PackageUIManager, + LoadedPackData, + LoadedPackTexture, + load_all_from_dir, + build_from_core, + ImportStatus, + import_pack_from_zip_file_path +}; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs index 6335145..c6064dd 100644 --- a/crates/joko_marker_format/src/manager/mod.rs +++ b/crates/joko_marker_format/src/manager/mod.rs @@ -21,3 +21,4 @@ mod pack; pub use package::{PackageDataManager, PackageUIManager}; pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +pub use pack::import::{ImportStatus, import_pack_from_zip_file_path}; \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/import.rs b/crates/joko_marker_format/src/manager/pack/import.rs index 58baac4..4100841 100644 --- a/crates/joko_marker_format/src/manager/pack/import.rs +++ b/crates/joko_marker_format/src/manager/pack/import.rs @@ -13,6 +13,7 @@ pub enum ImportStatus { UnInitialized, WaitingForFileChooser, LoadingPack(std::path::PathBuf), + WaitingLoading(std::path::PathBuf), PackDone(String, PackCore, bool), PackError(miette::Report), } diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index b064c95..a06d117 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -12,7 +12,7 @@ use tracing::{debug, error, info, info_span}; use uuid::Uuid; use crate::{ - io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} + io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} }; use jokolink::MumbleLink; use joko_core::{ diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index 4441b9d..18e1077 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -60,7 +60,6 @@ pub struct PackageDataManager { } #[must_use] pub struct PackageUIManager { - pub import_status: Option>>, default_marker_texture: Option, default_trail_texture: Option, packs: BTreeMap, @@ -288,7 +287,6 @@ impl PackageUIManager { tasks: PackTasks::new(), default_marker_texture: None, default_trail_texture: None, - import_status: Default::default(), all_files_tribool: Tribool::True, all_files_toggle: false, @@ -403,26 +401,18 @@ impl PackageUIManager { }); } - fn pack_importer(import_status: Arc>) { + fn pack_importer( + import_status: Arc>, + ) { //called when a new pack is imported - rayon::spawn(move || { + rayon::spawn( move || { *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; if let Some(file_path) = rfd::FileDialog::new() .add_filter("taco", &["zip", "taco"]) .pick_file() { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); - - let result = import_pack_from_zip_file_path(file_path); - match result { - Ok((name, pack)) => { - *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); - } - Err(e) => { - *import_status.lock().unwrap() = ImportStatus::PackError(e); - } - } + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); } else { *import_status.lock().unwrap() = ImportStatus::PackError(miette::miette!("file chooser was cancelled")); @@ -574,8 +564,9 @@ impl PackageUIManager { } fn gui_package_loader( &mut self, - event_sender: &std::sync::mpsc::Sender, + u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, + import_status: &Arc>, open: &mut bool ) { Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { @@ -587,58 +578,58 @@ impl PackageUIManager { if ui.button("delete").clicked() { to_delete.push(pack.uuid); } + if ui.button("Details").clicked() { + //TODO + } + ui.end_row(); } if !to_delete.is_empty() { - event_sender.send(UIToBackMessage::DeletePacks(to_delete)); + u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); } }); }); - if self.import_status.is_some() { - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - self.import_status = None; - } - } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? - let import_status = Arc::new(Mutex::default()); - self.import_status = Some(import_status.clone()); - Self::pack_importer(import_status); - } - if let Some(import_status) = self.import_status.as_ref() { - if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) => { - ui.label(format!("pack is being imported from {p:?}")); + if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? + //let import_status = import_status.lock().unwrap(); + Self::pack_importer(Arc::clone(import_status)); } - ImportStatus::PackDone(name, pack, saved) => { - - if !*saved { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - if ui.button("save").clicked() { - event_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); - } - } else { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + ui.label("import not started yet"); + } + ImportStatus::WaitingForFileChooser => { + ui.label( + "wailting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + if *saved { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } else { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + if ui.button("save").clicked() { + u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); } } - ImportStatus::PackError(e) => { - ui.colored_label( - egui::Color32::RED, - format!("failed to import pack due to error: {e:#?}"), - ); + } + ImportStatus::PackError(e) => { + let error_msg = format!("failed to import pack due to error: {e:#?}"); + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + *status = ImportStatus::UnInitialized; } + ui.colored_label( + egui::Color32::RED, + error_msg, + ); } } } @@ -648,15 +639,16 @@ impl PackageUIManager { } pub fn gui( &mut self, - event_sender: &std::sync::mpsc::Sender, + u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, is_marker_open: &mut bool, + import_status: &Arc>, is_file_open: &mut bool, timestamp: f64, link: Option<&MumbleLink> ) { - self.gui_package_loader(event_sender, etx, is_marker_open); - self.gui_file_manager(event_sender, etx, is_file_open, link); + self.gui_package_loader(u2b_sender, etx, import_status, is_marker_open); + self.gui_file_manager(u2b_sender, etx, is_file_open, link); } pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs index e82be67..067b3d4 100644 --- a/crates/joko_marker_format/src/message.rs +++ b/crates/joko_marker_format/src/message.rs @@ -1,4 +1,3 @@ -use std::hash::Hash; use std::sync::Arc; use std::collections::{BTreeMap, HashSet}; @@ -45,7 +44,8 @@ pub enum BackToUIMessage { CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display LoadedPack(LoadedPackTexture),//push a loaded pack to UI DeletedPacks(Vec),//push a deleted set of packs to UI - Loading, + ImportedPack(String, PackCore), + ImportFailure(miette::Report), MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), MumbleLink(Option), MumbleLinkChanged,//tell there is a need to resize @@ -61,7 +61,7 @@ pub enum UIToBackMessage { CategoryActivationStatusChanged,//something happened that needs to reload the whole set CategorySetAll(bool),//signal all categories should be now at this status DeletePacks(Vec),//uuid of the pack to delete - ImportPack, + ImportPack(std::path::PathBuf), ReloadPack, SavePack(String, PackCore), } diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs index d0a5940..d7a5df6 100644 --- a/crates/joko_marker_format/src/pack/mod.rs +++ b/crates/joko_marker_format/src/pack/mod.rs @@ -196,10 +196,10 @@ impl PackCore { Ok(()) } - pub(crate) fn register_route(&mut self, full_category_name: String , mut route: Route) -> Result<(), miette::Error> { + pub(crate) fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); let tbin_path: RelativePath = file_name.parse().unwrap(); - let uuid_to_insert = self.register_uuid(&full_category_name, &route.guid)?; + let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; route.guid = uuid_to_insert; let trail = route_to_trail(&route, &tbin_path); let tbin = route_to_tbin(&route); diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 0c1655e..47a051f 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -20,15 +20,20 @@ use jokolink::{MumbleChanges, MumbleLink, MumbleManager, mumble_gui}; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span, span}; use jmf::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +use jmf::{ImportStatus, import_pack_from_zip_file_path}; #[derive(Clone)] -struct JokolayState { +struct JokolayUIState { link: Option, window_changed: bool, - choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI list_of_textures_changed: bool,//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display nb_running_tasks_on_back: i32,// store the number of running tasks in background thread nb_running_tasks_on_network: i32,// store the number of running tasks (requests) in progress + import_status: Arc>, +} + +struct JokolayBackState { + choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI } struct JokolayApp { mumble_manager: MumbleManager, @@ -49,7 +54,8 @@ pub struct Jokolay { jokolay_dir: Arc, gui: Arc>, app: Arc>>, - state: JokolayState, + state_ui: JokolayUIState, + state_back: JokolayBackState, } @@ -94,8 +100,8 @@ impl Jokolay { let frame_stats = wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _); let mut menu_panel = MenuPanel::default(); - menu_panel.show_theme_window = true; - menu_panel.show_package_manager_window = true; + //menu_panel.show_theme_window = true; + //menu_panel.show_package_manager_window = true; package_ui_manager.late_init(&egui_context); Ok(Self { @@ -114,13 +120,16 @@ impl Jokolay { mumble_manager: mumble_data_manager, package_manager: package_data_manager, }))), - state: JokolayState{ + state_ui: JokolayUIState { link: None, window_changed: true, - choice_of_category_changed: false, list_of_textures_changed: false, nb_running_tasks_on_back: 0, nb_running_tasks_on_network: 0, + import_status: Default::default(), + }, + state_back: JokolayBackState { + choice_of_category_changed: false, } }) } @@ -128,17 +137,17 @@ impl Jokolay { fn start_background_loop( jokolay_dir: Arc, app: Arc>>, - state: JokolayState, + state: JokolayBackState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, ) { - let background_thread = std::thread::spawn(move || { + let _background_thread = std::thread::spawn(move || { // Load the directory with packages in the background process { //TODO: lazy loading to load maps only when on it let mut app = app.lock().unwrap(); let JokolayApp { - mumble_manager, + mumble_manager: _, package_manager } = &mut app.deref_mut().as_mut(); package_manager.load_all(Arc::clone(&jokolay_dir), &b2u_sender); @@ -149,7 +158,7 @@ impl Jokolay { fn handle_u2b_message( package_manager: &mut PackageDataManager, - local_state: &mut JokolayState, + local_state: &mut JokolayBackState, b2u_sender: &std::sync::mpsc::Sender, msg: UIToBackMessage ) { @@ -185,8 +194,19 @@ impl Jokolay { } b2u_sender.send(BackToUIMessage::DeletedPacks(deleted)); } - UIToBackMessage::ImportPack => { - unimplemented!("Handling of UIToBackMessage::ImportPack has not been implemented yet"); + UIToBackMessage::ImportPack(file_path) => { + tracing::trace!("Handling of UIToBackMessage::ImportPack"); + b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + let result = import_pack_from_zip_file_path(file_path); + match result { + Ok((file_name, pack)) => { + b2u_sender.send(BackToUIMessage::ImportedPack(file_name, pack)); + } + Err(e) => { + b2u_sender.send(BackToUIMessage::ImportFailure(e)); + } + } + b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } UIToBackMessage::ReloadPack => { unimplemented!("Handling of UIToBackMessage::ReloadPack has not been implemented yet"); @@ -226,7 +246,7 @@ impl Jokolay { } fn background_loop( mut app: Arc>>, - mut local_state: JokolayState, + mut local_state: JokolayBackState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, ) { @@ -281,7 +301,7 @@ impl Jokolay { fn handle_u2u_message( gui: &mut JokolayGui, - state: &mut JokolayState, + state: &mut JokolayUIState, msg: UIToUIMessage ) { match msg { @@ -312,7 +332,7 @@ impl Jokolay { } fn handle_b2u_message( gui: &mut JokolayGui, - local_state: &mut JokolayState, + local_state: &mut JokolayUIState, u2b_sender: &std::sync::mpsc::Sender, msg: BackToUIMessage ) { @@ -330,15 +350,21 @@ impl Jokolay { tracing::trace!("Handling of BackToUIMessage::DeletedPacks"); gui.package_manager.delete_packs(to_delete); } + BackToUIMessage::ImportedPack(file_name, pack) => { + tracing::trace!("Handling of BackToUIMessage::ImportedPack"); + *local_state.import_status.lock().unwrap() = ImportStatus::PackDone(file_name, pack, false); + } + BackToUIMessage::ImportFailure(error) => { + tracing::trace!("Handling of BackToUIMessage::ImportFailure"); + *local_state.import_status.lock().unwrap() = ImportStatus::PackError(error); + + } BackToUIMessage::LoadedPack(pack_texture) => { tracing::trace!("Handling of BackToUIMessage::LoadedPack"); gui.package_manager.save(pack_texture); - gui.package_manager.import_status = None; + local_state.import_status = Default::default(); u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); } - BackToUIMessage::Loading => { - unimplemented!("Handling of BackToUIMessage::Loading has not been implemented yet"); - } BackToUIMessage::MarkerTexture(pack_uuid, tex_path, marker_uuid, position, common_attributes) => { tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); gui.package_manager.load_marker_texture(&gui.egui_context, pack_uuid, tex_path, marker_uuid, position, common_attributes); @@ -378,11 +404,11 @@ impl Jokolay { let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); - Self::start_background_loop(Arc::clone(&self.jokolay_dir), Arc::clone(&self.app), self.state.clone(), b2u_sender, u2b_receiver); + Self::start_background_loop(Arc::clone(&self.jokolay_dir), Arc::clone(&self.app), self.state_back, b2u_sender, u2b_receiver); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); - let mut local_state = self.state.clone(); + let mut local_state = self.state_ui; let mut nb_frames: u128 = 0; let mut nb_messages: u128 = 0; let max_nb_messages_per_loop: u128 = 100; @@ -391,6 +417,16 @@ impl Jokolay { { let mut nb_message_on_curent_loop: u128 = 0; tracing::trace!("glfw event loop, {} frames, {} messages", nb_frames, nb_messages); + + if let Ok(mut import_status) = local_state.import_status.lock() { + match &mut *import_status { + ImportStatus::LoadingPack(file_path) => { + u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); + } + _ => {} + } + } //untested and might crash due to .unwrap() let mut gui = self.gui.lock().unwrap(); while let Ok(msg) = u2u_receiver.try_recv() { @@ -578,6 +614,7 @@ impl Jokolay { &u2b_sender, &etx, &mut menu_panel.show_package_manager_window, + &local_state.import_status, &mut menu_panel.show_file_manager_window, latest_time, link From b1f965133e2b683aa4343fd3f53dac393f5b22c2 Mon Sep 17 00:00:00 2001 From: moi Date: Fri, 5 Apr 2024 02:05:32 +0200 Subject: [PATCH 22/54] add option to (de-)activate whole branches of categories --- .../src/manager/pack/category_selection.rs | 38 ++++++++++++++++++- .../src/manager/pack/loaded.rs | 10 +++++ .../joko_marker_format/src/manager/package.rs | 8 ++++ crates/joko_marker_format/src/message.rs | 3 +- crates/jokolay/src/app/mod.rs | 8 +++- 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs index ef9869a..367b79d 100644 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ b/crates/joko_marker_format/src/manager/pack/category_selection.rs @@ -89,6 +89,21 @@ impl CategorySelection { } } } + pub fn get(selection: &mut OrderedHashMap, uuid: Uuid) -> Option<&mut CategorySelection> { + if selection.is_empty() { + return None; + } else { + for cat in selection.values_mut() { + if cat.uuid == uuid { + return Some(cat); + } + if let Some(res) = Self::get(&mut cat.children, uuid) { + return Some(res); + } + } + return None; + } + } pub fn recursive_populate_guids( selection: &mut OrderedHashMap, entities_parents: &mut HashMap, @@ -176,6 +191,25 @@ impl CategorySelection { return is_active; } + fn context_menu( + u2b_sender: &std::sync::mpsc::Sender, + cs: &mut CategorySelection, + ui: &mut egui::Ui + ) { + if ui.button("Activate branch").clicked() { + cs.is_selected = true; + CategorySelection::recursive_set_all(&mut cs.children, true); + u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); + ui.close_menu(); + } + if ui.button("Deactivate branch").clicked() { + CategorySelection::recursive_set_all(&mut cs.children, false); + cs.is_selected = false; + u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); + ui.close_menu(); + } + } + pub fn recursive_selection_ui( u2b_sender: &std::sync::mpsc::Sender, u2u_sender: &std::sync::mpsc::Sender, @@ -199,7 +233,7 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { - u2b_sender.send(UIToBackMessage::CategoryActivationStatusChange(cat.uuid, cat.is_selected)); + u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); *is_dirty = true; } } @@ -225,7 +259,7 @@ impl CategorySelection { show_only_active, late_discovery_categories ); - }); + }).response.context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); } }); } diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs index a06d117..8427dc2 100644 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ b/crates/joko_marker_format/src/manager/pack/loaded.rs @@ -284,6 +284,16 @@ impl LoadedPackData { false } } + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { + if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { + cs.is_selected = status; + self._is_dirty = true; + if CategorySelection::recursive_set(&mut cs.children, uuid, status) { + return true; + } + } + false + } pub fn category_set_all(&mut self, status: bool) { CategorySelection::recursive_set_all(&mut self.selectable_categories, status); self._is_dirty = true; diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs index 18e1077..05f5dea 100644 --- a/crates/joko_marker_format/src/manager/package.rs +++ b/crates/joko_marker_format/src/manager/package.rs @@ -108,6 +108,14 @@ impl PackageDataManager { } } + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_branch_set(uuid, status) { + break; + } + } + } + pub fn category_set_all(&mut self, status: bool) { for pack in self.packs.values_mut() { pack.category_set_all(status); diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs index 067b3d4..189d543 100644 --- a/crates/joko_marker_format/src/message.rs +++ b/crates/joko_marker_format/src/message.rs @@ -57,7 +57,8 @@ pub enum BackToUIMessage { pub enum UIToBackMessage { ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. - CategoryActivationStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationElementStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationBranchStatusChange(Uuid, bool),//same, for a whole branch CategoryActivationStatusChanged,//something happened that needs to reload the whole set CategorySetAll(bool),//signal all categories should be now at this status DeletePacks(Vec),//uuid of the pack to delete diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 47a051f..d72275e 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -167,10 +167,14 @@ impl Jokolay { tracing::trace!("Handling of UIToBackMessage::ActiveFiles"); package_manager.set_currently_used_files(currently_used_files); } - UIToBackMessage::CategoryActivationStatusChange(category_uuid, status) => { - tracing::trace!("Handling of UIToBackMessage::CategoryActivationStatusChange"); + UIToBackMessage::CategoryActivationElementStatusChange(category_uuid, status) => { + tracing::trace!("Handling of UIToBackMessage::CategoryActivationElementStatusChange"); package_manager.category_set(category_uuid, status); } + UIToBackMessage::CategoryActivationBranchStatusChange(category_uuid, status) => { + tracing::trace!("Handling of UIToBackMessage::CategoryActivationBranchStatusChange"); + package_manager.category_branch_set(category_uuid, status); + } UIToBackMessage::CategoryActivationStatusChanged => { tracing::trace!("Handling of UIToBackMessage::CategoryActivationStatusChanged"); local_state.choice_of_category_changed = true; From 7f1b5f6ec4fed583ab02c0acca3c04c3538cf59d Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 10 Apr 2024 13:15:52 +0200 Subject: [PATCH 23/54] more steps to separate structures from controler and have simpler rust packages dependancies (faster compilation intended in the end) --- Cargo.lock | 156 +- Cargo.lock.backup_20240318 | 3087 +++++++++++++++++ Cargo.toml | 8 +- crates/joko_core/Cargo.toml | 22 - crates/joko_core/src/lib.rs | 6 - crates/joko_core/src/task/mod.rs | 2 +- crates/joko_package/Cargo.toml | 53 + crates/joko_package/README.md | 87 + crates/joko_package/build.rs | 14 + crates/joko_package/src/io/deserialize.rs | 1399 ++++++++ crates/joko_package/src/io/error.rs | 1 + crates/joko_package/src/io/mod.rs | 188 + crates/joko_package/src/io/serialize.rs | 227 ++ crates/joko_package/src/io/test.xml | 12 + crates/joko_package/src/io/xmlfile_schema.xsd | 394 +++ crates/joko_package/src/lib.rs | 47 + crates/joko_package/src/manager/mod.rs | 24 + .../src/manager/pack/activation.rs | 21 + .../joko_package/src/manager/pack/active.rs | 292 ++ .../src/manager/pack/category_selection.rs | 269 ++ crates/joko_package/src/manager/pack/dirty.rs | 29 + crates/joko_package/src/manager/pack/entry.rs | 6 + .../src/manager/pack/file_selection.rs | 42 + .../joko_package/src/manager/pack/import.rs | 36 + crates/joko_package/src/manager/pack/list.rs | 6 + .../joko_package/src/manager/pack/loaded.rs | 788 +++++ crates/joko_package/src/manager/pack/mod.rs | 8 + crates/joko_package/src/manager/package.rs | 680 ++++ crates/joko_package/src/message.rs | 55 + crates/joko_package/src/pack/common.rs | 2274 ++++++++++++ crates/joko_package/src/pack/marker.png | Bin 0 -> 173015 bytes crates/joko_package/src/pack/marker.rs | 15 + crates/joko_package/src/pack/mod.rs | 413 +++ crates/joko_package/src/pack/question.png | Bin 0 -> 4248 bytes crates/joko_package/src/pack/route.rs | 20 + crates/joko_package/src/pack/trail.png | Bin 0 -> 6896 bytes crates/joko_package/src/pack/trail.rs | 31 + crates/joko_package/src/pack/trail_black.png | Bin 0 -> 2293 bytes .../joko_package/src/pack/trail_rainbow.png | Bin 0 -> 16987 bytes crates/joko_package/vendor/rapid/license.txt | 52 + crates/joko_package/vendor/rapid/rapid.cpp | 66 + crates/joko_package/vendor/rapid/rapid.hpp | 7 + crates/joko_package/vendor/rapid/rapidxml.hpp | 2645 ++++++++++++++ .../vendor/rapid/rapidxml_iterators.hpp | 295 ++ .../vendor/rapid/rapidxml_print.hpp | 422 +++ .../vendor/rapid/rapidxml_utils.hpp | 56 + crates/joko_render/Cargo.toml | 3 +- crates/joko_render/src/billboard.rs | 7 +- crates/joko_render/src/gl.rs | 9 + crates/joko_render/src/lib.rs | 182 +- crates/joko_render/src/renderer.rs | 280 ++ crates/joko_render_models/Cargo.toml | 13 + crates/joko_render_models/src/lib.rs | 2 + crates/joko_render_models/src/marker.rs | 23 + crates/joko_render_models/src/trail.rs | 9 + crates/jokoapi/src/end_point/races/mod.rs | 47 +- crates/jokolay/Cargo.toml | 20 +- crates/jokolay/src/app/mod.rs | 127 +- crates/jokolay/src/lib.rs | 1 + .../{joko_core => jokolay}/src/manager/mod.rs | 0 .../src/manager/theme/mod.rs | 0 .../src/manager/theme/roboto.ttf | Bin .../src/manager/trace/mod.rs | 30 +- crates/jokolink/Cargo.toml | 4 - crates/jokolink/src/lib.rs | 107 +- crates/jokolink/src/mumble/ctypes.rs | 27 - crates/jokolink/src/mumble/mod.rs | 9 +- 67 files changed, 14672 insertions(+), 483 deletions(-) create mode 100644 Cargo.lock.backup_20240318 create mode 100644 crates/joko_package/Cargo.toml create mode 100644 crates/joko_package/README.md create mode 100644 crates/joko_package/build.rs create mode 100644 crates/joko_package/src/io/deserialize.rs create mode 100644 crates/joko_package/src/io/error.rs create mode 100644 crates/joko_package/src/io/mod.rs create mode 100644 crates/joko_package/src/io/serialize.rs create mode 100644 crates/joko_package/src/io/test.xml create mode 100644 crates/joko_package/src/io/xmlfile_schema.xsd create mode 100644 crates/joko_package/src/lib.rs create mode 100644 crates/joko_package/src/manager/mod.rs create mode 100644 crates/joko_package/src/manager/pack/activation.rs create mode 100644 crates/joko_package/src/manager/pack/active.rs create mode 100644 crates/joko_package/src/manager/pack/category_selection.rs create mode 100644 crates/joko_package/src/manager/pack/dirty.rs create mode 100644 crates/joko_package/src/manager/pack/entry.rs create mode 100644 crates/joko_package/src/manager/pack/file_selection.rs create mode 100644 crates/joko_package/src/manager/pack/import.rs create mode 100644 crates/joko_package/src/manager/pack/list.rs create mode 100644 crates/joko_package/src/manager/pack/loaded.rs create mode 100644 crates/joko_package/src/manager/pack/mod.rs create mode 100644 crates/joko_package/src/manager/package.rs create mode 100644 crates/joko_package/src/message.rs create mode 100644 crates/joko_package/src/pack/common.rs create mode 100644 crates/joko_package/src/pack/marker.png create mode 100644 crates/joko_package/src/pack/marker.rs create mode 100644 crates/joko_package/src/pack/mod.rs create mode 100644 crates/joko_package/src/pack/question.png create mode 100644 crates/joko_package/src/pack/route.rs create mode 100644 crates/joko_package/src/pack/trail.png create mode 100644 crates/joko_package/src/pack/trail.rs create mode 100644 crates/joko_package/src/pack/trail_black.png create mode 100644 crates/joko_package/src/pack/trail_rainbow.png create mode 100644 crates/joko_package/vendor/rapid/license.txt create mode 100644 crates/joko_package/vendor/rapid/rapid.cpp create mode 100644 crates/joko_package/vendor/rapid/rapid.hpp create mode 100644 crates/joko_package/vendor/rapid/rapidxml.hpp create mode 100644 crates/joko_package/vendor/rapid/rapidxml_iterators.hpp create mode 100644 crates/joko_package/vendor/rapid/rapidxml_print.hpp create mode 100644 crates/joko_package/vendor/rapid/rapidxml_utils.hpp create mode 100644 crates/joko_render/src/gl.rs create mode 100644 crates/joko_render/src/renderer.rs create mode 100644 crates/joko_render_models/Cargo.toml create mode 100644 crates/joko_render_models/src/lib.rs create mode 100644 crates/joko_render_models/src/marker.rs create mode 100644 crates/joko_render_models/src/trail.rs rename crates/{joko_core => jokolay}/src/manager/mod.rs (100%) rename crates/{joko_core => jokolay}/src/manager/theme/mod.rs (100%) rename crates/{joko_core => jokolay}/src/manager/theme/roboto.ttf (100%) rename crates/{joko_core => jokolay}/src/manager/trace/mod.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index e8672d7..e0b958c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1028,18 +1028,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "filetime" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.52.0", -] - [[package]] name = "flate2" version = "1.0.28" @@ -1335,26 +1323,6 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "instant" version = "0.1.12" @@ -1411,24 +1379,8 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" name = "joko_core" version = "0.2.1" dependencies = [ - "cap-std", - "egui", - "egui_extras", - "glam", - "indexmap", - "jokolink", - "miette", - "rayon", - "rfd", - "ringbuffer", "scopeguard", - "serde", - "serde_json", "smol_str", - "tracing", - "tracing-appender", - "tracing-subscriber", - "uuid", ] [[package]] @@ -1436,7 +1388,7 @@ name = "joko_ext" version = "0.1.0" [[package]] -name = "joko_marker_format" +name = "joko_package" version = "0.2.1" dependencies = [ "base64", @@ -1452,9 +1404,11 @@ dependencies = [ "indexmap", "itertools", "joko_core", + "joko_render_models", "jokoapi", "jokolink", "miette", + "once", "ordered_hash_map", "paste", "phf", @@ -1483,11 +1437,19 @@ dependencies = [ "egui_render_three_d", "egui_window_glfw_passthrough", "glam", - "joko_marker_format", + "joko_render_models", "jokolink", "tracing", ] +[[package]] +name = "joko_render_models" +version = "0.2.1" +dependencies = [ + "bytemuck", + "glam", +] + [[package]] name = "jokoapi" version = "0.2.1" @@ -1506,14 +1468,26 @@ dependencies = [ "cap-directories", "cap-std", "egui", + "egui_extras", "egui_window_glfw_passthrough", - "joko_core", - "joko_marker_format", + "enumflags2", + "glam", + "indexmap", + "joko_package", "joko_render", "jokolink", "miette", "rayon", + "rfd", + "ringbuffer", + "scopeguard", + "serde", + "serde_json", + "smol_str", "tracing", + "tracing-appender", + "tracing-subscriber", + "uuid", ] [[package]] @@ -1524,17 +1498,13 @@ dependencies = [ "egui", "enumflags2", "glam", - "jokoapi", "miette", - "notify", "num-derive", "num-traits", "serde", "serde_json", "time", "tracing", - "tracing-appender", - "tracing-subscriber", "widestring", "windows", "x11rb", @@ -1549,26 +1519,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kqueue" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1734,18 +1684,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "next-gen" version = "0.1.1" @@ -1798,23 +1736,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.5.0", - "filetime", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1890,6 +1811,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60bfe75a40f755f162b794140436c57845cb106fd1467598631c76c6fff08e28" + [[package]] name = "once_cell" version = "1.19.0" @@ -2386,15 +2313,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3017,16 +2935,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.lock.backup_20240318 b/Cargo.lock.backup_20240318 new file mode 100644 index 0000000..3951136 --- /dev/null +++ b/Cargo.lock.backup_20240318 @@ -0,0 +1,3087 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "accesskit" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eb1adf08c5bcaa8490b9851fd53cca27fa9880076f178ea9d29f05196728a8" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", + "zerocopy 0.7.25", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arcdps" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e8e3e68ba99ea4d9fc0af6c26f7277c6a30f9fbd7a1884efd8d016dcdfdc39" +dependencies = [ + "arcdps_codegen", + "chrono", + "once_cell", +] + +[[package]] +name = "arcdps_codegen" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b73c6f84c5845e9eba3a232593d20ef3db434281848f5072a367edbcc1f3fee" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata 0.1.10", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "cap-directories" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "182588d07579a8ca97dbfbea2787d450341d068b16062c8caa2205158ddb269d" +dependencies = [ + "cap-std", + "directories-next", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "cap-primitives" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf30c373a3bee22c292b1b6a7a26736a38376840f1af3d2d806455edf8c3899" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "windows-sys 0.48.0", + "winx", +] + +[[package]] +name = "cap-std" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bade423fa6403efeebeafe568fdb230e8c590a275fba2ba978dd112efcf6e9" +dependencies = [ + "camino", + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx", + "num-traits", +] + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "cxx" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.39", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "ecolor" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "egui" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" +dependencies = [ + "accesskit", + "ahash", + "epaint", + "nohash-hasher", + "serde", +] + +[[package]] +name = "egui_extras" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ffe3fe5c00295f91c2a61a74ee271c32f74049c94ba0b1cea8f26eb478bc07" +dependencies = [ + "egui", + "enum-map", + "log", + "mime_guess", + "serde", +] + +[[package]] +name = "egui_render_glow" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df0cb60080432a2c025f00942fbd1a0f8b719338ab6a28adab5a1ca15013771" +dependencies = [ + "bytemuck", + "egui", + "getrandom", + "glow", + "js-sys", + "raw-window-handle", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_render_three_d" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4038e7bac93f9356eb88ffabd20d9486070b79910584d662af1ac3bf64f01e2a" +dependencies = [ + "egui", + "egui_render_glow", + "raw-window-handle", + "three-d", +] + +[[package]] +name = "egui_window_glfw_passthrough" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecec3abb56e2be5104a35a4c1848f976add5167a8655f67ae7c84d45d35c8905" +dependencies = [ + "egui", + "glfw-passthrough", + "tracing", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "emath" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-map" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed40247825a1a0393b91b51d475ea1063a6cbbf0847592e7f13fb427aca6a716" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7933cd46e720348d29ed1493f89df9792563f272f96d8f13d18afe03b32f8cb8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "enumflags2" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "enumn" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "epaint" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "nohash-hasher", + "parking_lot", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "glfw-passthrough" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89ad199bb99922313a6e97b609dab23a88e3b68a6b0233d1fafdb5044a7728f" +dependencies = [ + "bitflags 1.3.2", + "glfw-sys-passthrough", + "objc", + "raw-window-handle", + "winapi", +] + +[[package]] +name = "glfw-sys-passthrough" +version = "4.0.3+3.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b2db4d361b9ebe743c3a542ddef5d605269bd1f93e1090440fff075e666ddf" +dependencies = [ + "cmake", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy 0.6.5", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", + "serde", +] + +[[package]] +name = "indextree" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-extras" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3c230ee517ee76b1cc593b52939ff68deda3fae9e41eca426c6b4993df51c4" +dependencies = [ + "io-lifetimes", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "joko_core" +version = "0.2.1" +dependencies = [ + "cap-directories", + "cap-std", + "egui", + "egui_extras", + "glam", + "indexmap", + "miette", + "ordered_hash_map", + "rayon", + "rfd", + "ringbuffer", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + +[[package]] +name = "joko_ext" +version = "0.1.0" + +[[package]] +name = "joko_marker_format" +version = "0.2.1" +dependencies = [ + "base64", + "cap-std", + "cxx", + "cxx-build", + "data-encoding", + "egui", + "enumflags2", + "glam", + "image", + "indexmap", + "itertools", + "joko_render", + "jokoapi", + "jokolink", + "miette", + "ordered_hash_map", + "paste", + "phf", + "rayon", + "rfd", + "rstest", + "serde", + "serde_json", + "similar-asserts", + "smol_str", + "time", + "tracing", + "url", + "uuid", + "xot", + "zip", +] + +[[package]] +name = "joko_render" +version = "0.2.1" +dependencies = [ + "bytemuck", + "egui", + "egui_render_three_d", + "egui_window_glfw_passthrough", + "glam", + "jokolink", + "raw-window-handle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "jokoapi" +version = "0.2.1" +dependencies = [ + "const_format", + "enumflags2", + "miette", + "serde", + "ureq", +] + +[[package]] +name = "jokolay" +version = "0.2.1" +dependencies = [ + "cap-directories", + "cap-std", + "egui", + "egui_extras", + "egui_window_glfw_passthrough", + "glam", + "indexmap", + "joko_core", + "joko_marker_format", + "joko_render", + "jokolink", + "miette", + "rayon", + "rfd", + "ringbuffer", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-subscriber", + "url", +] + +[[package]] +name = "jokolink" +version = "0.2.1" +dependencies = [ + "arcdps", + "egui", + "enumflags2", + "glam", + "jokoapi", + "miette", + "notify", + "num-derive", + "num-traits", + "serde", + "serde_json", + "time", + "tracing", + "tracing-appender", + "tracing-subscriber", + "widestring", + "windows", + "x11rb", +] + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "backtrace", + "backtrace-ext", + "is-terminal", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "next-gen" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1962f0b64c859f27f9551c74afbdbec7090fa83518daf6c5eb5b31d153455beb" +dependencies = [ + "next-gen-proc_macros", + "unwind_safe", +] + +[[package]] +name = "next-gen-proc_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59395d2ffdd03894479cdd1ce4b7e0700d379d517f2d396cee2a4828707c5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "ordered_hash_map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" +dependencies = [ + "hashbrown 0.13.2", + "serde", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + +[[package]] +name = "rfd" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "ringbuffer" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.39", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "itoa", + "libc", + "linux-raw-sys", + "once_cell", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +dependencies = [ + "console", + "similar", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "smol_str" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "supports-unicode" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "three-d" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2db9010227411ab0aa5948e770304e807e5c9b6d5d0719c3de248bae7be7096" +dependencies = [ + "cgmath", + "glow", + "instant", + "thiserror", + "three-d-asset", +] + +[[package]] +name = "three-d-asset" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9959d4427b63958661828008f7470d6a8d2c0945b3df0dc7377d6aca38fb694" +dependencies = [ + "cgmath", + "half", + "thiserror", + "web-sys", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unwind_safe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" + +[[package]] +name = "ureq" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", + "rand", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5" +dependencies = [ + "bitflags 2.4.1", + "windows-sys 0.48.0", +] + +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname", + "nix", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix", +] + +[[package]] +name = "xhtmlchardet" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc471704e8954f426350a7300e92a4da6932b762068ae8e6aa5dcacf141e133" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xot" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55dc1c3603e452c78983b59f466cd8251695db1729b230f473d004d70b3d94d8" +dependencies = [ + "ahash", + "encoding_rs", + "indextree", + "next-gen", + "xhtmlchardet", + "xmlparser", +] + +[[package]] +name = "zerocopy" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96f8f25c15a0edc9b07eb66e7e6e97d124c0505435c382fde1ab7ceb188aa956" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.5", +] + +[[package]] +name = "zerocopy" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +dependencies = [ + "zerocopy-derive 0.7.25", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "855e0f6af9cd72b87d8a6c586f3cb583f5cdcc62c2c80869d8cd7e96fdf7ee20" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/Cargo.toml b/Cargo.toml index 0cf5de7..b7b8e73 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ [workspace] members = [ "crates/joko_render", - "crates/joko_marker_format", + "crates/joko_render_models", + "crates/joko_package", "crates/jokolink", "crates/jokoapi", "crates/jokolay", @@ -37,6 +38,11 @@ smol_str = { version = "*" } uuid = { version = "*" } itertools = { version = "*" } ordered_hash_map = { version = "*", features= ["serde"] } +tracing-appender = { version = "*" } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", + "time", +] } # for ErrorLayer #https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index 6818224..8569d10 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -6,27 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cap-std = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "time", -] } # for ErrorLayer -tracing-appender = { version = "*" } -miette = { workspace = true } - -egui = { workspace = true, features = ["serde"] } -egui_extras = { workspace = true } - -ringbuffer = { workspace = true } -rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -indexmap = { workspace = true } -rfd = { workspace = true } -glam = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } -uuid = { workspace = true } - -jokolink = { path = "../jokolink" } diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 2c5d369..60e0737 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,9 +1,7 @@ use std::str::FromStr; use smol_str::SmolStr; -use uuid::Uuid; -pub mod manager; /* each manager must have 1. a main thread struct @@ -16,10 +14,6 @@ each manager must have pub mod task; -use glam::{Vec2, Vec3}; -use jokolink::MumbleLink; - - /// This newtype is used to represents relative paths in marker packs /// 1. It won't start with `/` or `C:` like roots, because its a relative path /// 2. It can be empty to represent current directory diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs index a165200..89afa4d 100644 --- a/crates/joko_core/src/task/mod.rs +++ b/crates/joko_core/src/task/mod.rs @@ -28,7 +28,7 @@ where let (th_result_sender, result_receiver) = std::sync::mpsc::channel(); let (nb_sender, nb_receiver) = std::sync::mpsc::channel(); let nb = Arc::new(std::sync::atomic::AtomicI32::new(0)); - let mut res = Arc::new(Mutex::new(Self { + let res = Arc::new(Mutex::new(Self { task_sender, result_receiver, thread_task: None, diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml new file mode 100644 index 0000000..060f953 --- /dev/null +++ b/crates/joko_package/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "joko_package" +version = "0.2.1" +edition = "2021" + +[dependencies] +# jmf deps +# for marker packs +base64 = "0.21.2" +bytemuck = { workspace = true } +cap-std = { workspace = true } +cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings +data-encoding = "2.4.0" +egui = { workspace = true } +enumflags2 = { workspace = true } +glam = { workspace = true } +image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +joko_render_models = { path = "../joko_render_models" } +jokoapi = { path = "../jokoapi" } +jokolink = { path = "../jokolink" } +miette = { workspace = true } +once = "0.3.4" +ordered_hash_map = { workspace = true } +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } +rayon = { workspace = true } +rfd = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +smol_str = { workspace = true } +time = { workspace = true , features = ["serde"]} +tracing = { workspace = true } +tribool = "0.3.0" +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } +zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) + + + +[dev-dependencies] +# jmf deps +rstest = { version = "0", default-features = false } +# rstest_reuse = "0.3.0" +similar-asserts = "1" + + +[build-dependencies] +# for rapidxml +cxx-build = { version = "1" } diff --git a/crates/joko_package/README.md b/crates/joko_package/README.md new file mode 100644 index 0000000..cbbf8d6 --- /dev/null +++ b/crates/joko_package/README.md @@ -0,0 +1,87 @@ + +## Status +still in early stages of development + + + + +### RapidXML Integration +Taco uses RapidXML, which is very very lenient in its parsing. +this led to marker packs not caring about their xml being valid xml. +Blish instead created a custom parsing library to deal with this and have workarounds for known issues. + +rapidxml does fix these issues itself when we roundtrip xml through it. so, we have a function called `rapid_filter` which takes in xml string and returns a "filtered" xml string that fixes a bunch of issues like escaping special characters like +ampersand, gt, lt etc.. with proper xml formatting i.e `&`, `>` etc.. + +Sources of rapidxml are in the vendor folder. it is a custom fork from https://github.com/timniederhausen/rapidxml which +added some fixes / enhancements. its stil a mess with compiler warnings, but whatever. + +we use cxxbridge crate. +`rapid.hpp` is our header with declaration for `rapid_filter` inside `rapid` namespace. (includes `joko_marker_format/src/lib.rs.h`) +`lib.rs` has extern declaration which has the same signature but in rust. (includes `joko_marker_format/vendor/rapid/rapid.hpp`) +`build.rs` has the compilation instructions. it uses `lib.rs` extern declaration, `rapid.cpp` as compilation unit as it + contains the definition of `rapid_filter` and finally outputs a `librapid.a` for linking. + +with this, we now filter the xml with `rapid_filter` before deserializing it in rust. if we still have errors we just +complain about it. + + + +### XML Marker Format +Marker Pack + +1. Textures + 1. identified by the relative path. case sensitive. But to accommodate case-insensitive MS windows packs, we will convert all paths to lowercase when importing. + 2. png format. + 3. need to convert to a srgba texture and upload to gpu to use it + 4. mostly tiny images. here's the composition of tekkit's pack textures + +| count | dimensions | +|-------|---------------| +| 630 | 100x100 | +| 7 | 150x150 | +| 89 | 200x200 | +| 683 | 250x250 | +| 42 | 256x256 | +| 435 | 500x500 | + +2. Tbins + 1. binary data of a series of vec3 positions. + mapid + a version (just ver 2 for now) + 2. need to generate a mesh to be usable to upload on gpu. different mesh for 2d map / minimap. trail_scale an affect width of the generated mesh + 3. anim_speed attr needs dynamic texture coords (probably based on time delta offset) + 4. color attribute requires blending. + 5. uses texture + 6. can be statically or dynamically filtered (culled). but no cooldowns. + +3. MarkerCategories + 1. create a tree structure of menu to be displayed. + 2. identified by their name (and parents in the hierarchy) as a unique path. + 3. can be enabled or disabled. need to persist this data in activation data or somewhere else. + 4. enabled / disabled categories act as dynamic filters for markers / trails. + 5. attributes get inherited by children unless overrided. and also inehrited by the markers / trails. + 6. can be enabled / disabled by a marker action (toggle_category attribute) +4. Markers + 1. render a quad. either billbaord or static rotation. + 2. needs texture + alpha attribute + color attribute for blending. + 3. alpha is also affected by fadenear and fadefar attributes. + 4. static filters like ingamevisibility or map visibility or minimap visibility. + 5. can display text via info / tip-description. + 6. dynamic filters like behavior + race + profession + specialization + mount + map type + category + festival + achievement. + 7. size is determined by texture + minSize / maxSize + scale. map quad rendering affected by scale on map and mapdisplaysize attribute + 8. triggers actions of behavior + copy-message (copy clipboard) + bounce?? + toggling category based on player proximity and pressing of a special action key (usually F) +5. Trails + 1. render the tbin mesh. + 2. same filters as marker + 3. no triggering / activation / cooldowns though. + + +3D: +1. can match blish +2. need to ignore certain attributes like minSize and maxSize. + + +2D: +1. can match taco +2. more performance because 2d? + + diff --git a/crates/joko_package/build.rs b/crates/joko_package/build.rs new file mode 100644 index 0000000..062e89b --- /dev/null +++ b/crates/joko_package/build.rs @@ -0,0 +1,14 @@ +fn main() { + cxx_build::bridge("src/lib.rs") // our extern declaration in rust for rapid_filter + .file("vendor/rapid/rapid.cpp") // our compilation unit containing definition + .warnings(false) + .extra_warnings(false) + .compile("rapid"); // name of library = librapid.a + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=vendor/rapid/rapid.cpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapid.hpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapidxml.hpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapidxml_print.hpp"); + // shadow_rs::new().expect("failed to run shadow"); +} diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs new file mode 100644 index 0000000..7219c5e --- /dev/null +++ b/crates/joko_package/src/io/deserialize.rs @@ -0,0 +1,1399 @@ +use joko_core::RelativePath; +use miette::{bail, Context, IntoDiagnostic, Result}; + +use crate::{ + pack::{prefix_parent, Category, CommonAttributes, MapData, Marker, PackCore, RawCategory, Route, TBin, TBinStatus, Trail}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::{Dir, DirEntry}; +use glam::Vec3; +use indexmap::IndexMap; +use std::{collections::{VecDeque, HashMap}, io::Read}; +use ordered_hash_map::OrderedHashMap; +use tracing::{debug, info, info_span, instrument, trace, warn}; +use uuid::Uuid; +use xot::{Node, Xot, Element}; + +use super::XotAttributeNameIDs; + +pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { + //called from already parsed data + let mut core_pack = PackCore::new(); + // walks the directory and loads all files into the hashmap + let start = std::time::SystemTime::now(); + recursive_walk_dir_and_read_images_and_tbins( + dir, + &mut core_pack.textures, + &mut core_pack.tbins, + &RelativePath::default(), + ) + .wrap_err("failed to walk dir when loading a markerpack")?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of core package textures from disk took {} ms", elaspsed.as_millis()); + + //categories are required to register other objects + let cats_xml = dir + .read_to_string("categories.xml") + .into_diagnostic() + .wrap_err("failed to read categories.xml")?; + let categories_file = String::from("categories.xml"); + let parse_categories_file_start = std::time::SystemTime::now(); + parse_categories_file(&categories_file, &cats_xml, &mut core_pack) + .wrap_err("failed to parse category file")?; + info!("parse_categories_file took {} ms", parse_categories_file_start.elapsed().unwrap_or_default().as_millis()); + + // parse map data of the pack + for entry in dir + .entries() + .into_diagnostic() + .wrap_err("failed to read entries of pack dir")? + { + let dir_entry = entry + .into_diagnostic() + .wrap_err("entry error whiel reading xml files")?; + + let name = dir_entry + .file_name() + .into_diagnostic() + .wrap_err("map data entry name not utf-8")? + .to_string(); + + if name.ends_with(".xml") { + if let Some(name_as_str) = name.strip_suffix(".xml") { + match name_as_str { + "categories" => { + //already done + } + map_id => { + // parse map file + let span_guard = info_span!("load map", map_id).entered(); + if let Ok(map_id) = map_id.parse::() { + //let mut partial_pack = PackCore::partial(&core_pack.all_categories); + load_map_file(map_id, &dir_entry, &mut core_pack)?; + //core_pack.merge_partial(partial_pack); + } else { + info!("unrecognized xml file {map_id}") + } + std::mem::drop(span_guard); + } + } + } + } else { + trace!("file ignored: {name}") + } + } + info!("Entities registered (category + markers): {}", core_pack.entities_parents.len()); + info!("Categories registered: {}", core_pack.all_categories.len()); + info!("Markers registered: {}", core_pack.entities_parents.len() - core_pack.all_categories.len()); + info!("Maps registered: {}", core_pack.maps.len()); + info!("Textures registered: {}", core_pack.textures.len()); + info!("Trail binaries registered: {}", core_pack.tbins.len()); + Ok(core_pack) +} + + +fn recursive_walk_dir_and_read_images_and_tbins( + dir: &Dir, + images: &mut HashMap>, + tbins: &mut HashMap, + parent_path: &RelativePath, +) -> Result<()> { + for entry in dir + .entries() + .into_diagnostic() + .wrap_err("failed to get directory entries")? + { + let entry = entry + .into_diagnostic() + .wrap_err("dir entry error when iterating dir entries")?; + let name = entry.file_name().into_diagnostic()?; + let path = parent_path.join_str(&name); + + if entry + .file_type() + .into_diagnostic() + .wrap_err("failed to get file type")? + .is_file() + { + if path.ends_with(".png") || path.ends_with(".trl") { + let mut bytes = vec![]; + entry + .open() + .into_diagnostic() + .wrap_err("failed to open file")? + .read_to_end(&mut bytes) + .into_diagnostic() + .wrap_err("failed to read file contents")?; + if name.ends_with(".png") { + images.insert(path.clone(), bytes); + } else if name.ends_with(".trl") { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + } + tbins.insert(path, tbs.tbin); + } else { + info!("invalid tbin: {path}"); + } + } + } + } else { + recursive_walk_dir_and_read_images_and_tbins( + &entry.open_dir().into_diagnostic()?, + images, + tbins, + &path, + )?; + } + } + Ok(()) +} +fn parse_tbin_from_slice(bytes: &[u8]) -> Option { + let content_length = bytes.len(); + // content_length must be atleast 8 to contain version + map_id + if content_length < 8 { + info!("failed to parse tbin because the len is less than 8"); + return None; + } + + let mut version_bytes = [0_u8; 4]; + version_bytes.copy_from_slice(&bytes[4..8]); + let version = u32::from_ne_bytes(version_bytes); + let mut map_id_bytes = [0_u8; 4]; + map_id_bytes.copy_from_slice(&bytes[4..8]); + let map_id = u32::from_ne_bytes(map_id_bytes); + + let zero = Vec3{x:0.0, y:0.0, z:0.0}; + + // this will either be empty vec or series of vec3s. + let nodes: VecDeque = bytes[8..] + .chunks_exact(12) + .map(|float_bytes| { + // make [f32 ;3] out of those 12 bytes + let arr = [ + f32::from_le_bytes([ + // first float + float_bytes[0], + float_bytes[1], + float_bytes[2], + float_bytes[3], + ]), + f32::from_le_bytes([ + // second float + float_bytes[4], + float_bytes[5], + float_bytes[6], + float_bytes[7], + ]), + f32::from_le_bytes([ + // third float + float_bytes[8], + float_bytes[9], + float_bytes[10], + float_bytes[11], + ]), + ]; + + Vec3::from_array(arr) + }) + .collect(); + + //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. + let mut iso_x = false; + let mut iso_y = false; + let mut iso_z = false; + let mut closed = false; + let mut resulting_nodes : Vec = Vec::new(); + if nodes.len() > 0 { + let ref_node = nodes[0]; + let mut c_iso_x = true; + let mut c_iso_y = true; + let mut c_iso_z = true; + // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts + resulting_nodes.push(ref_node); + for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { + //ignore zeroes since they would be separators + if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { + let distance_to_next_point = a.distance_squared(*b); + let mut current_cursor = distance_to_next_point; + while current_cursor > 400.0 { + let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); + resulting_nodes.push(c); + current_cursor -= 400.0; + } + } + resulting_nodes.push(*b); + } + for node in &nodes { + if resulting_nodes.len() > 1 { + //TODO: load epsilon from a configuration somewhere, with a default value + if (node.x - ref_node.x).abs() < 0.1 { + c_iso_x = false; + } + if (node.y - ref_node.y).abs() < 0.1 { + c_iso_y = false; + } + if (node.z - ref_node.z).abs() < 0.1 { + c_iso_z = false; + } + } + } + iso_x = c_iso_x; + iso_y = c_iso_y; + iso_z = c_iso_z; + if nodes.len() > 1 {// TODO: get this threshold from configuration + closed = nodes.front().unwrap().distance(*nodes.back().unwrap()).abs() < 0.1 + } + } + Some(TBinStatus{ + tbin: TBin { + map_id, + version, + nodes: resulting_nodes, + }, + iso_x, + iso_y, + iso_z, + closed + }) +} + +fn parse_categories( + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut OrderedHashMap, + names: &XotAttributeNameIDs, +) { + //called once per file + parse_categories_recursive(tree, tags, first_pass_categories, names, None); + +} + + +// a recursive function to parse the marker category tree. +fn parse_categories_recursive( + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut OrderedHashMap, + names: &XotAttributeNameIDs, + parent_name: Option, +) { + for tag in tags { + let ele = match tree.element(tag) { + Some(ele) => ele, + None => continue, + }; + if ele.name() != names.marker_category { + continue; + } + + let name = ele + .get_attribute(names.name) + .or(ele.get_attribute(names.capital_name)) + .unwrap_or_default() + .to_lowercase(); + if name.is_empty() { + continue; + } + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(ele, names); + + let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); + + let separator = ele + .get_attribute(names.separator) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or_default(); + + let default_enabled = ele + .get_attribute(names.default_enabled) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or(true); + let guid = parse_guid(names, ele); + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, name) + } else { + name.to_string() + }; + trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); + if !first_pass_categories.contains_key(&full_category_name) { + first_pass_categories.insert(full_category_name.clone(), RawCategory { + guid, + parent_name: parent_name.clone(), + display_name: display_name.to_string(), + relative_category_name: name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: ca, + }); + } + parse_categories_recursive( + tree, + tree.children(tag), + first_pass_categories, + names, + Some(full_category_name), + ); + } +} + +fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { + let mut tree = xot::Xot::new(); + let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); + let root_node = tree + .parse(cats_xml_str) + .into_diagnostic() + .wrap_err("invalid xml")?; + + let overlay_data_node = tree + .document_element(root_node) + .into_diagnostic() + .wrap_err("no doc element")?; + + if let Some(od) = tree.element(overlay_data_node) { + let mut categories: IndexMap = Default::default(); + if od.name() == xot_names.overlay_data { + parse_category_categories_xml_recursive( + &file_name, + &tree, + tree.children(overlay_data_node), + pack, + &mut categories, + &xot_names, + None, + None, + ); + trace!("loaded categories: {:?}", categories); + pack.categories = categories; + pack.register_categories(); + } else { + bail!("root tag is not OverlayData") + } + } else { + bail!("doc element is not element???"); + } + Ok(()) +} + + +fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Result<()> { + let mut xml_str = String::new(); + dir_entry + .open() + .into_diagnostic() + .wrap_err("failed to open xml file")? + .read_to_string(&mut xml_str) + .into_diagnostic() + .wrap_err("faield to read xml string")?; + //TODO: launch an async load of the file + make a priority queue to have current map first + parse_map_xml_string(map_id, &xml_str, target).wrap_err_with(|| { + miette::miette!("error parsing map file: {map_id}") + }) +} + +fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) -> Result<()> { + /* + fields read: + all_categories + + fields modified: + maps + all_categories + late_discovery_categories + source_files + tbins + entities_parents + */ + let mut tree = Xot::new(); + let root_node = tree + .parse(map_xml_str) + .into_diagnostic() + .wrap_err("invalid xml")?; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let overlay_data_node = tree + .document_element(root_node) + .into_diagnostic() + .wrap_err("missing doc element")?; + + let overlay_data_element = tree + .element(overlay_data_node) + .ok_or_else(|| miette::miette!("no doc ele"))?; + + if overlay_data_element.name() != names.overlay_data { + bail!("root tag is not OverlayData"); + } + let pois = tree + .children(overlay_data_node) + .find(|node| match tree.element(*node) { + Some(ele) => ele.name() == names.pois, + None => false, + }) + .ok_or_else(|| miette::miette!("missing pois node"))?; + + for poi_node in tree.children(pois) { + if let Some(child_element) = tree.element(poi_node) { + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + let span_guard = info_span!("category", full_category_name).entered(); + + let source_file_name = child_element.get_attribute(names._source_file_name).unwrap_or_default().to_string(); + target.source_files.insert(source_file_name.clone(), true); + + if child_element.name() == names.route { + debug!("Found a route in core pack {:?}", child_element); + let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_name.clone()); + if let Some(route) = route { + //TODO: make sure there is no "very late" discovery + //let category_uuid = target.get_or_create_category_uuid(&route.category); + //route.parent = category_uuid; + target.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); + } + let raw_uid = child_element.get_attribute(names.guid); + if raw_uid.is_none() { + info!("This POI is either invalid or inside a Route {:?}", child_element); + span_guard.exit(); + continue; + } + //FIXME: this needs to be changed for partial load + let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed + let guid = raw_uid.and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + }) + .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; + + if child_element.name() == names.poi { + debug!("Found a POI in core pack {:?}", child_element); + if child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or_else(|| miette::miette!("invalid mapid"))? + != map_id + { + bail!("mapid doesn't match the file name"); + } + let xpos = child_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let ypos = child_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let zpos = child_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .into_diagnostic()?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + target.register_uuid(&full_category_name, &guid); + let marker = Marker { + position: [xpos, ypos, zpos].into(), + map_id, + category: full_category_name, + parent: category_uuid.clone(), + attrs: ca, + guid, + source_file_name + }; + + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); + } + target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); + } else if child_element.name() == names.trail { + debug!("Found a trail in core pack {:?}", child_element); + if child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or_else(|| miette::miette!("invalid mapid"))? + != map_id + { + bail!("mapid doesn't match the file name"); + } + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + target.register_uuid(&full_category_name, &guid); + let trail = Trail { + category: full_category_name, + parent: category_uuid.clone(), + map_id, + props: ca, + guid, + dynamic: false, + source_file_name + }; + + if !target.maps.contains_key(&map_id) { + target.maps.insert(map_id, MapData::default()); + } + target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); + } + } + span_guard.exit(); + } + } + Ok(()) +} + +// a temporary recursive function to parse the marker category tree. +fn parse_category_categories_xml_recursive( + file_name: &String, + tree: &Xot, + tags: impl Iterator, + pack: &mut PackCore, + cats: &mut IndexMap, + names: &XotAttributeNameIDs, + parent_uuid: Option, + parent_name: Option, +) { + for tag in tags { + if let Some(ele) = tree.element(tag) { + if ele.name() != names.marker_category { + continue; + } + + let relative_category_name = ele.get_attribute(names.name) + .or(ele.get_attribute(names.display_name) + .or(ele.get_attribute(names.capital_name) + ) + ).unwrap_or_default().to_lowercase(); + if relative_category_name.is_empty() { + info!("category doesn't have a name attribute: {ele:#?}"); + continue; + } + let span_guard = info_span!("category", relative_category_name).entered(); + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(ele, names); + + let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); + + let separator = match ele.get_attribute(names.separator).unwrap_or("0") { + "0" => false, + "1" => true, + ors => { + info!("separator attribute has invalid value: {ors}"); + false + } + }; + + let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { + "0" => false, + "1" => true, + ors => { + info!("default_enabled attribute has invalid value: {ors}"); + true + } + }; + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, relative_category_name) + } else { + relative_category_name.to_string() + }; + let guid = parse_guid(names, ele); + trace!("recursive_marker_category_parser_categories_xml {} {} {:?}", full_category_name, guid, parent_uuid); + if display_name.is_empty() { + assert!(parent_name.is_none()); + parse_category_categories_xml_recursive( + file_name, + tree, + tree.children(tag), + pack, + cats, + names, + Some(guid), + Some(full_category_name), + ); + } else { + let current_category = cats + .entry(guid) + .or_insert_with(|| Category { + guid, + parent: parent_uuid.clone(), + display_name: display_name.to_string(), + relative_category_name: relative_category_name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: ca, + children: Default::default(), + }); + parse_category_categories_xml_recursive( + file_name, + tree, + tree.children(tag), + pack, + &mut current_category.children, + names, + Some(guid), + Some(full_category_name), + ); + }; + + std::mem::drop(span_guard); + } else { + //it may be a comment, a space, anything + //info!("In file {}, ignore node {:?}", file_name, tag); + } + } +} + +/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. +/// will return error if there's an issue with zipfile. +/// +/// but any other errors like invalid attributes or missing markers etc.. will just be logged. +/// the intention is "best effort" parsing and not "validating" xml marker packs. +/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. +#[instrument(skip_all)] +pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { + //called to import a new pack + // all the contents of ZPack + let mut pack = PackCore::new(); + // parse zip file + let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) + .into_diagnostic() + .wrap_err("failed to read zip archive")?; + + // file paths of different file types + let mut images = vec![]; + let mut tbins = vec![]; + let mut xmls = vec![]; + // we collect the names first, because reading a file from zip is a mutating operation. + // So, we can't iterate AND read the file at the same time + for name in zip_archive.file_names() { + let name_as_string = name.to_string(); + if name_as_string.ends_with(".png") { + images.push(name_as_string); + } else if name_as_string.ends_with(".trl") { + tbins.push(name_as_string); + } else if name_as_string.ends_with(".xml") { + xmls.push(name_as_string); + } else if name_as_string.replace("\\", "/").ends_with('/') { + // directory. so, we can silently ignore this. + } else { + info!("ignoring file: {name}"); + } + } + xmls.sort();//build back the intended order in folder, since zip_archive may not give the files in order. + let start = std::time::SystemTime::now(); + for name in images { + let span = info_span!("load image", name).entered(); + let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); + if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { + match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { + Ok(_) => assert!( + pack.textures.insert(file_path.clone(), bytes).is_none(), + "duplicate image file {name}" + ), + Err(e) => { + info!(?e, "failed to parse image file"); + } + } + } + std::mem::drop(span); + } + + for name in tbins { + let span = info_span!("load tbin {name}").entered(); + + let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); + if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + } + assert!( + pack.tbins.insert(file_path, tbs.tbin).is_none(), + "duplicate tbin file {name}" + ); + } else { + info!("failed to parse tbin from slice: {file_path}"); + } + } else { + info!(name, "failed to read tbin from zipfile"); + } + std::mem::drop(span); + } + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of taco package textures from disk took {} ms", elaspsed.as_millis()); + + let span_guard_categories = info_span!("deserialize xml: categories").entered(); + + //first pass: categories only + let span_guard_first_pass = info_span!("deserialize xml first pass: load MarkerCategory").entered(); + let mut first_pass_categories: OrderedHashMap = Default::default(); + for source_file_name in xmls.iter() { + let mut xml_str = String::new(); + let span_guard = info_span!("deserialize xml first pass: load file", source_file_name).entered(); + if zip_archive + .by_name(&source_file_name) + .ok() + .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) + .is_none() + { + info!("failed to read file from zip"); + continue; + }; + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + parse_categories(&tree, tree.children(od), &mut first_pass_categories, &names); + drop(span_guard); + } + span_guard_first_pass.exit(); + + //second pass: orphan categories + let span_guard_second_pass = info_span!("deserialize xml second pass: orphan categories").entered(); + for source_file_name in xmls.iter() { + let mut xml_str = String::new(); + let span_guard = info_span!("deserialize xml second pass: load file", source_file_name).entered(); + if zip_archive + .by_name(&source_file_name) + .ok() + .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) + .is_none() + { + info!("failed to read file from zip"); + continue; + }; + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + info!("missing pois tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let mut full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + if full_category_name.is_empty() { + if child_element.name() == names.route { + // If route, take the first element inside + if let Some(category) = parse_route_category(&names, &tree, &child_node, child_element) { + if category.is_empty() { + continue; + } + full_category_name = category; + } else { + continue; + } + } else { + continue; + } + } + if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { + let category_uuid = Uuid::new_v4(); + first_pass_categories.insert(full_category_name.clone(), RawCategory{ + default_enabled: true, + guid: category_uuid, + parent_name: prefix_parent(&full_category_name, '.'), + display_name: full_category_name.clone(), + full_category_name: full_category_name.clone(), + relative_category_name: full_category_name.clone(), + props: Default::default(), + separator: false + }); + info!("There is an orphan missing category '{}' which was created", full_category_name); + } + } + drop(span_guard); + } + span_guard_second_pass.exit(); + + pack.categories = Category::reassemble(&first_pass_categories, &mut pack.late_discovery_categories); + pack.register_categories(); + + //third and last pass: elements + let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); + for source_file_name in xmls.iter() { + let mut xml_str = String::new(); + let span_guard = info_span!("deserialize xml third pass load file ", source_file_name).entered(); + if zip_archive + .by_name(&source_file_name) + .ok() + .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) + .is_none() + { + info!("failed to read file from zip"); + continue; + }; + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + info!("missing pois tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + debug!("import element: {:?}", child_element); + if child_element.name() == names.route { + let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, source_file_name.clone()); + if let Some(mut route) = route { + //one must not create category anymore + route.parent = pack.get_category_uuid(&route.category).unwrap().clone(); + pack.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + info!("full_category_name is empty {:?}", child_element); + continue; + } + if ! pack.category_exists(&full_category_name) { + panic!("Missing category {}, previous pass should have taken care of this", full_category_name); + } + let category_uuid = pack.get_or_create_category_uuid(&full_category_name); + if child_element.name() == names.poi { + if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_marker(full_category_name, marker)?; + } else { + debug!("Could not parse POI"); + } + } else if child_element.name() == names.trail { + if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + pack.register_trail(full_category_name, trail)?; + } else { + debug!("Could not parse Trail"); + } + } else { + info!("unknown element: {:?}", child_element); + } + } + } + + drop(span_guard); + } + span_guard_third_pass.exit(); + span_guard_categories.exit(); + Ok(pack) +} + + +fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { + child + .get_attribute(names.guid) + .and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + .or_else(|| { + info!(guid, "failed to deserialize guid"); + None + }) + }) +} +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ + parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) +} + +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { + if let Some(map_id) = poi_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + let xpos = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let ypos = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let zpos = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(poi_element, &names); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + info!(%icon_file, "failed to find this texture in this pack"); + } + } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { + info!(icf, "marker's icon file attribute failed to parse"); + } + Some(Marker { + position: [xpos, ypos, zpos].into(), + map_id, + category: category_name.clone(), + parent: category_uuid.clone(), + attrs: common_attributes, + guid: parse_guid(names, poi_element), + source_file_name + }) + } else { + info!("missing map id"); + None + } +} + +fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { + let x = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let y = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let z = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Vec3{x, y, z} +} + + +fn parse_route_category( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, +) -> Option { + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + if let Some(cat) = child.get_attribute(names.category) { + return Some(cat.to_string()); + } + } + } + info!("Could not find a category for route element: {route_element:?}"); + None +} + +fn parse_route( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, + category_name: &String, + source_file_name: String +) -> Option { + + let mut path: Vec = Vec::new(); + let resetposx = route_element + .get_attribute(names.resetposx) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposy = route_element + .get_attribute(names.resetposy) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposz = route_element + .get_attribute(names.resetposz) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let reset_position = Vec3::new(resetposx, resetposy, resetposz); + let reset_range = route_element.get_attribute(names.reset_range).and_then(|map_id| map_id.parse::().ok()); + let name = route_element.get_attribute(names.name).or(route_element.get_attribute(names.capital_name)); + + if name.is_none() { + info!("route element is missing name: {route_element:?}"); + return None; + } + let mut category: String = category_name.clone(); + let mut category_uuid: Option = parse_optional_guid(names, route_element); + let mut map_id: Option = route_element.get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()); + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + let marker = parse_position(&names, child); + path.push(marker); + if category.is_empty() { + if let Some(cat) = child.get_attribute(names.category) { + category = cat.to_string(); + } + } + if category_uuid.is_none() { + category_uuid = parse_optional_guid(names, &child) + } + if map_id.is_none() { + if let Some(node_map_id) = child + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + map_id = Some(node_map_id); + } + } + } + } + if category.is_empty() { + info!("Could not find a category for route element: {route_element:?}"); + return None; + } + if map_id.is_none() { + info!("Could not find a map_id for route element: {route_element:?}"); + return None; + } + if category_uuid.is_none() { + info!("Could not find a uuid for route element: {route_element:?}"); + return None; + } + debug!("found route with {:?} elements {route_element:?}", path.len()); + + Some(Route { + category, + parent: category_uuid.unwrap(), + path, + reset_position, + reset_range: reset_range.unwrap_or(0.0), + map_id: map_id.unwrap(), + name: name.unwrap().into(), + guid: parse_guid(names, &route_element), + source_file_name, + }) +} + + +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { + //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html + if let Some(map_id) = trail_element + .get_attribute(names.trail_data) + .and_then(|trail_data| { + let path: RelativePath = trail_data.parse().unwrap(); + pack.tbins.get(&path).map(|tb| tb.map_id) + }) + { + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(trail_element, &names); + + if let Some(tex) = common_attributes.get_texture() { + if !pack.textures.contains_key(tex) { + info!(%tex, "failed to find this texture in this pack"); + } + } + + Some(Trail { + category: category_name.clone(), + parent: category_uuid.clone(), + map_id, + props: common_attributes, + guid: parse_guid(names, trail_element), + dynamic: false, + source_file_name, + }) + } else { + let td = trail_element.get_attribute(names.trail_data); + let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); + let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); + info!("missing map_id: {td:?} {rp} {tbin:?}"); + None + } + +} + +#[instrument(skip(zip_archive))] +fn read_file_bytes_from_zip_by_name( + name: &str, + zip_archive: &mut zip::ZipArchive, +) -> Option> { + let mut bytes = vec![]; + match zip_archive.by_name(name) { + Ok(mut file) => match file.read_to_end(&mut bytes) { + Ok(size) => { + if size == 0 { + info!("empty file {name}"); + } else { + return Some(bytes); + } + } + Err(e) => { + info!(?e, "failed to read file"); + } + }, + Err(e) => { + info!(?e, "failed to get file from zip"); + } + } + None +} + + +// #[cfg(test)] +// mod test { + +// use indexmap::IndexMap; +// use rstest::*; + +// use semver::Version; +// use similar_asserts::assert_eq; +// use std::io::Write; +// use std::sync::Arc; + +// use zip::write::FileOptions; +// use zip::ZipWriter; + +// use crate::{ +// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, +// INCHES_PER_METER, +// }; + +// const TEST_XML: &str = include_str!("test.xml"); +// const TEST_MARKER_PNG_NAME: &str = "marker.png"; +// const TEST_TRL_NAME: &str = "basic.trl"; + +// #[fixture] +// #[once] +// fn test_zip() -> Vec { +// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); +// // category.xml +// writer +// .start_file("category.xml", FileOptions::default()) +// .expect("failed to create category.xml"); +// writer +// .write_all(TEST_XML.as_bytes()) +// .expect("failed to write category.xml"); +// // marker.png +// writer +// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) +// .expect("failed to create marker.png"); +// writer +// .write_all(MARKER_PNG) +// .expect("failed to write marker.png"); +// // basic.trl +// writer +// .start_file(TEST_TRL_NAME, FileOptions::default()) +// .expect("failed to create basic trail"); +// writer +// .write_all(&0u32.to_ne_bytes()) +// .expect("failed to write version"); +// writer +// .write_all(&15u32.to_ne_bytes()) +// .expect("failed to write mapid "); +// writer +// .write_all(bytemuck::cast_slice(&[0f32; 3])) +// .expect("failed to write first node"); +// // done +// writer +// .finish() +// .expect("failed to finalize zip") +// .into_inner() +// } + +// #[fixture] +// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { +// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); +// assert_eq!(file_entries.len(), 3); +// let test_xml = std::str::from_utf8( +// file_entries +// .get(String::new("category.xml")) +// .expect("failed to get category.xml"), +// ) +// .expect("failed to get str from category.xml contents"); +// assert_eq!(test_xml, TEST_XML); +// let test_marker_png = file_entries +// .get(String::new("marker.png")) +// .expect("failed to get marker.png"); +// assert_eq!(test_marker_png, MARKER_PNG); +// file_entries +// } +// #[fixture] +// #[once] +// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { +// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); +// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); +// assert_eq!(pack.tbins.len(), 1); +// assert_eq!(pack.textures.len(), 1); +// assert_eq!( +// pack.textures +// .get(String::new(TEST_MARKER_PNG_NAME)) +// .expect("failed to get marker.png from textures"), +// MARKER_PNG +// ); + +// let tbin = pack +// .tbins +// .get(String::new(TEST_TRL_NAME)) +// .expect("failed to get basic trail") +// .clone(); + +// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); +// pack +// } + +// // #[rstest] +// // fn test_tag(test_pack: &Pack) { +// // let mut test_category_menu = CategoryMenu::default(); +// // let parent_path = String::new("parent"); +// // let child1_path = String::new("parent/child1"); +// // let subchild_path = String::new("parent/child1/subchild"); +// // let child2_path = String::new("parent/child2"); +// // test_category_menu.create_category(subchild_path); +// // test_category_menu.create_category(child2_path); +// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); +// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); +// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); +// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); + +// // assert_eq!(test_category_menu, test_pack.category_menu) +// // } + +// #[rstest] +// fn test_markers(test_pack: &Pack) { +// let marker = test_pack +// .markers +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// marker.props.texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); +// } +// #[rstest] +// fn test_trails(test_pack: &Pack) { +// let trail = test_pack +// .trails +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// trail.props.tbin.as_ref().unwrap(), +// String::new(TEST_TRL_NAME) +// ); +// assert_eq!( +// trail.props.trail_texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// } +// } diff --git a/crates/joko_package/src/io/error.rs b/crates/joko_package/src/io/error.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/joko_package/src/io/error.rs @@ -0,0 +1 @@ + diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs new file mode 100644 index 0000000..de2bd5c --- /dev/null +++ b/crates/joko_package/src/io/mod.rs @@ -0,0 +1,188 @@ +//! This modules primarily deals with serializing and deserializing xml data from marker packs +//! + +use xot::{NameId, Xot}; + +mod deserialize; +mod error; +mod serialize; + +pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; +pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; +pub(crate) struct XotAttributeNameIDs { + // xml tags + pub overlay_data: NameId, + pub marker_category: NameId, + pub pois: NameId, + pub poi: NameId, + pub trail: NameId, + pub route: NameId, + // marker specific attributes + pub category: NameId, + pub guid: NameId, + pub map_id: NameId, + pub xpos: NameId, + pub ypos: NameId, + pub zpos: NameId, + // marker category specific attributes + pub default_enabled: NameId, + pub display_name: NameId, + pub name: NameId, + pub capital_name: NameId,//same than "name" but with a starting capital letter + pub separator: NameId, + // inheritable attributes + pub achievement_id: NameId, + pub achievement_bit: NameId, + pub alpha: NameId, + pub anim_speed: NameId, + pub auto_trigger: NameId, + pub behavior: NameId, + pub bounce: NameId, + pub bounce_delay: NameId, + pub bounce_duration: NameId, + pub bounce_height: NameId, + pub can_fade: NameId, + pub color: NameId, + pub copy: NameId, + pub copy_message: NameId, + pub cull: NameId, + pub fade_far: NameId, + pub fade_near: NameId, + pub festival: NameId, + pub has_countdown: NameId, + pub height_offset: NameId, + pub hide: NameId, + pub icon_file: NameId, + pub icon_size: NameId, + pub in_game_visibility: NameId, + pub info: NameId, + pub info_range: NameId, + pub invert_behavior: NameId, + pub is_wall: NameId, + pub keep_on_map_edge: NameId, + pub map_display_size: NameId, + pub map_fade_out_scale_level: NameId, + pub map_type: NameId, + pub map_visibility: NameId, + pub max_size: NameId, + pub min_size: NameId, + pub mini_map_visibility: NameId, + pub mount: NameId, + pub profession: NameId, + pub race: NameId, + pub reset_length: NameId, + pub reset_offset: NameId, + pub rotate: NameId, + pub rotate_x: NameId, + pub rotate_y: NameId, + pub rotate_z: NameId, + pub scale_on_map_with_zoom: NameId, + pub show: NameId, + pub specialization: NameId, + pub text: NameId, + pub texture: NameId, + pub tip_name: NameId, + pub tip_description: NameId, + pub title: NameId, + pub title_color: NameId, + pub toggle_category: NameId, + pub trail_data: NameId, + pub trail_scale: NameId, + pub trigger_range: NameId, + pub reset_range: NameId, + pub resetposx: NameId, + pub resetposy: NameId, + pub resetposz: NameId, + pub _source_file_name: NameId, +} +impl XotAttributeNameIDs { + pub fn register_with_xot(tree: &mut Xot) -> Self { + Self { + // tags + overlay_data: tree.add_name("OverlayData"), + marker_category: tree.add_name("MarkerCategory"), + pois: tree.add_name("POIs"), + poi: tree.add_name("POI"), + trail: tree.add_name("Trail"), + route: tree.add_name("Route"), + // non inheritable attributes + category: tree.add_name("type"), + xpos: tree.add_name("xpos"), + ypos: tree.add_name("ypos"), + zpos: tree.add_name("zpos"), + map_id: tree.add_name("MapID"), + guid: tree.add_name("GUID"), + + // marker category specific attrs + separator: tree.add_name("IsSeparator"), + default_enabled: tree.add_name("defaulttoggle"), + display_name: tree.add_name("DisplayName"), + name: tree.add_name("name"), + capital_name: tree.add_name("Name"), + // inheritable attributes + achievement_id: tree.add_name("achievementId"), + achievement_bit: tree.add_name("achievementBit"), + alpha: tree.add_name("alpha"), + anim_speed: tree.add_name("animSpeed"), + auto_trigger: tree.add_name("autotrigger"), + behavior: tree.add_name("behavior"), + color: tree.add_name("color"), + copy: tree.add_name("copy"), + copy_message: tree.add_name("copy-message"), + fade_near: tree.add_name("fadeNear"), + fade_far: tree.add_name("fadeFar"), + festival: tree.add_name("festival"), + has_countdown: tree.add_name("hasCountdown"), + height_offset: tree.add_name("heightOffset"), + icon_file: tree.add_name("iconFile"), + icon_size: tree.add_name("iconSize"), + in_game_visibility: tree.add_name("inGameVisibility"), + info: tree.add_name("info"), + info_range: tree.add_name("infoRange"), + map_display_size: tree.add_name("mapDisplaySize"), + map_visibility: tree.add_name("mapVisibility"), + max_size: tree.add_name("maxSize"), + min_size: tree.add_name("minSize"), + mini_map_visibility: tree.add_name("miniMapVisibility"), + mount: tree.add_name("mount"), + profession: tree.add_name("profession"), + race: tree.add_name("race"), + reset_length: tree.add_name("resetLength"), + reset_offset: tree.add_name("resetOffset"), + scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), + tip_name: tree.add_name("tip-name"), + tip_description: tree.add_name("tip-description"), + toggle_category: tree.add_name("togglecateogry"), + texture: tree.add_name("texture"), + trail_data: tree.add_name("trailData"), + trail_scale: tree.add_name("trailScale"), + trigger_range: tree.add_name("triggerRange"), + bounce_delay: tree.add_name("bounce-delay"), + bounce_duration: tree.add_name("bounce-duration"), + bounce_height: tree.add_name("bounce-height"), + can_fade: tree.add_name("canfade"), + cull: tree.add_name("cull"), + hide: tree.add_name("hide"), + is_wall: tree.add_name("iswall"), + invert_behavior: tree.add_name("invertbehavior"), + map_type: tree.add_name("maptype"), + rotate: tree.add_name("rotate"), + rotate_x: tree.add_name("rotate-x"), + rotate_y: tree.add_name("rotate-y"), + rotate_z: tree.add_name("rotate-z"), + show: tree.add_name("show"), + specialization: tree.add_name("specialization"), + title: tree.add_name("title"), + title_color: tree.add_name("title-color"), + text: tree.add_name("text"), + bounce: tree.add_name("bounce"), + keep_on_map_edge: tree.add_name("keepOnMapEdge"), + map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), + reset_range: tree.add_name("resetrange"), + resetposx: tree.add_name("resetposx"), + resetposy: tree.add_name("resetposy"), + resetposz: tree.add_name("resetposz"), + _source_file_name: tree.add_name("_source_file_name"), + } + } +} diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs new file mode 100644 index 0000000..2155f20 --- /dev/null +++ b/crates/joko_package/src/io/serialize.rs @@ -0,0 +1,227 @@ +use crate::{ + pack::{Category, Marker, Trail, Route}, + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::Dir; +use indexmap::IndexMap; +use miette::{Context, IntoDiagnostic, Result}; +use std::io::Write; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +use super::XotAttributeNameIDs; +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn save_pack_data_to_dir( + pack_data: &LoadedPackData, + writing_directory: &Dir, +) -> Result<()> { + // save categories + info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create new root with overlay data node")?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) + .wrap_err("failed to serialize cats")?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to convert cats xot to string")?; + writing_directory.create("categories.xml") + .into_diagnostic() + .wrap_err("failed to create categories.xml")? + .write_all(cats.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write to categories.xml")?; + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create root wiht overlay data for pois")?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .into_diagnostic() + .wrap_err("faild to append pois to od node")?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .into_diagnostic() + .wrap_err("failed to append poi (marker) to pois")?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .into_diagnostic() + .wrap_err("failed to append a trail node to pois")?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to serialize map data to string")?; + writing_directory.create(format!("{map_id}.xml")) + .into_diagnostic() + .wrap_err("failed to create map xml file")? + .write_all(map_xml.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write map data to file")?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<()> { + + info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + writing_directory.create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir for an image: {img_path}") + })?; + } + writing_directory.create(img_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .write(img) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to write image bytes to file: {img_path}") + })?; + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + writing_directory.create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir of tbin: {tbin_path}") + })?; + } + let mut bytes: Vec = vec![]; + bytes.reserve(8 + tbin.nodes.len() * 12); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + writing_directory.create(tbin_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .write_all(&bytes) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &IndexMap, + parent: Node, +) -> Result<()> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node).into_diagnostic()?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute(names._source_file_name, &trail.source_file_name); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.xpos, format!("{}", marker.position[0])); + ele.set_attribute(names.ypos, format!("{}", marker.position[1])); + ele.set_attribute(names.zpos, format!("{}", marker.position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute(names._source_file_name, &marker.source_file_name); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .into_diagnostic() + .wrap_err("failed to append route to pois")?; + let ele = tree.element_mut(route_node).unwrap(); + + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute(names._source_file_name, &route.source_file_name); + for pos in &route.path { + let child = tree.new_element(names.poi); + tree.append(route_node, child); + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} + diff --git a/crates/joko_package/src/io/test.xml b/crates/joko_package/src/io/test.xml new file mode 100644 index 0000000..3b50657 --- /dev/null +++ b/crates/joko_package/src/io/test.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/joko_package/src/io/xmlfile_schema.xsd b/crates/joko_package/src/io/xmlfile_schema.xsd new file mode 100644 index 0000000..895a0ac --- /dev/null +++ b/crates/joko_package/src/io/xmlfile_schema.xsd @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/joko_package/src/lib.rs b/crates/joko_package/src/lib.rs new file mode 100644 index 0000000..dc4c68a --- /dev/null +++ b/crates/joko_package/src/lib.rs @@ -0,0 +1,47 @@ +//! ReadOnly XML marker packs support for Jokolay +//! +//! + +pub(crate) mod io; +pub(crate) mod manager; +pub(crate) mod pack; +pub mod message; + +pub use manager::{ + PackageDataManager, + PackageUIManager, + LoadedPackData, + LoadedPackTexture, + load_all_from_dir, + build_from_core, + ImportStatus, + import_pack_from_zip_file_path +}; + +// for compile time build info like pkg version or build timestamp or git hash etc.. +// shadow_rs::shadow!(build); + +// to filter the xml with rapidxml first +#[cxx::bridge(namespace = "rapid")] +mod ffi { + unsafe extern "C++" { + include!("joko_package/vendor/rapid/rapid.hpp"); + pub fn rapid_filter(src_xml: String) -> String; + + } +} + +pub fn rapid_filter_rust(src_xml: String) -> String { + ffi::rapid_filter(src_xml) +} + +pub const INCHES_PER_METER: f32 = 39.37; + +pub fn is_default(t: &T) -> bool { + t == &T::default() +} + +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); diff --git a/crates/joko_package/src/manager/mod.rs b/crates/joko_package/src/manager/mod.rs new file mode 100644 index 0000000..c6064dd --- /dev/null +++ b/crates/joko_package/src/manager/mod.rs @@ -0,0 +1,24 @@ +//! How should the pack be stored by jokolay? +//! 1. Inside a directory called packs, we will have a separate directory for each pack. +//! 2. the name of the directory will serve as an ID for each pack. +//! 3. Inside the directory, we will have +//! 1. categories.xml -> The xml file which contains the whole category tree +//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. +//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. + +/* +expensive: +categories being a tree with order among siblings (better to use a tree crate?) +markers/trails referring to a category via full path. +editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. + +We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. + +*/ + +mod package; +mod pack; + +pub use package::{PackageDataManager, PackageUIManager}; +pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +pub use pack::import::{ImportStatus, import_pack_from_zip_file_path}; \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/activation.rs b/crates/joko_package/src/manager/pack/activation.rs new file mode 100644 index 0000000..a0a2a83 --- /dev/null +++ b/crates/joko_package/src/manager/pack/activation.rs @@ -0,0 +1,21 @@ +use indexmap::IndexMap; +use uuid::Uuid; + + +/// This is the activation data per pack +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct ActivationData { + /// this is for markers which are global and only activate once regardless of account + pub global: IndexMap, + /// this is the activation data per character + /// for markers which trigger once per character + pub character: IndexMap>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ActivationType { + /// clean these up when the map is changed + ReappearOnMapChange, + /// clean these up when the timestamp is reached + TimeStamp(time::OffsetDateTime), + Instance(std::net::IpAddr), +} \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs new file mode 100644 index 0000000..4745e33 --- /dev/null +++ b/crates/joko_package/src/manager/pack/active.rs @@ -0,0 +1,292 @@ +use jokoapi::end_point::mounts::Mount; +use ordered_hash_map::OrderedHashMap; + +use egui::TextureHandle; +use glam::{vec2, Vec2, Vec3}; +use indexmap::IndexMap; +use uuid::Uuid; + +use joko_core::RelativePath; +use crate::{ + pack::CommonAttributes, + INCHES_PER_METER, +}; +use jokolink::MumbleLink; +use joko_render_models::{ + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject +}; + +/* +- activation data with uuids and track the latest timestamp that will be activated +- category activation data -> track and changes to propagate to markers of this map +- current active markers, which will keep track of their original marker, so as to propagate any changes easily +*/ +#[derive(Clone)] +pub struct ActiveTrail { + pub trail_object: TrailObject, + pub texture_handle: TextureHandle, +} +/// This is an active marker. +/// It stores all the info that we need to scan every frame +#[derive(Clone)] +pub(crate) struct ActiveMarker { + /// texture id from managed textures + pub texture_id: u64, + /// owned texture handle to keep it alive + pub _texture: TextureHandle, + /// position + pub pos: Vec3, + /// billboard must not be bigger than this size in pixels + pub max_pixel_size: f32, + /// billboard must not be smaller than this size in pixels + pub min_pixel_size: f32, + pub common_attributes: CommonAttributes, +} + +pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; + +impl ActiveMarker { + pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { + let Self { + texture_id, + pos, + common_attributes: attrs, + _texture, + max_pixel_size, + min_pixel_size, + .. + } = self; + // let width = *width; + // let height = *height; + let texture_id = *texture_id; + let pos = *pos; + // filters + if let Some(mounts) = attrs.get_mount() { + if let Some(current) = Mount::try_from_mumble_link(link.mount) { + if !mounts.contains(current) { + return None; + } + } else { + return None; + } + } + let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); + let player_distance = pos.distance(link.player_pos); + let camera_distance = pos.distance(link.cam_pos); + let fade_near_far = Vec2::new(fade_near, fade_far); + + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let color = attrs.get_color().copied().unwrap_or_default(); + /* + 1. we need to filter the markers + 1. statically - mapid, character, map_type, race, profession + 2. dynamically - achievement, behavior, mount, fade_far, cull + 3. force hide/show by user discretion + 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior + 3. store the state for these markers activation data, and temporary data like bounce + */ + /* + skip if: + alpha is 0.0 + achievement id/bit is done (maybe this should be at map filter level?) + behavior (activation) + cull + distance > fade_far + visibility (ingame/map/minimap) + mount + specialization + */ + if fade_far > 0.0 && player_distance > fade_far { + return None; + } + // markers are 1 meter in width/height by default + let mut pos = pos; + pos.y += height_offset; + let direction_to_marker = link.cam_pos - pos; + let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); + + let far_offset = { + let dpi = if link.dpi_scaling <= 0 { + 96.0 + } else { + link.dpi as f32 + } / 96.0; + let gw2_width = link.client_size.as_vec2().x / dpi; + + // offset (half width i.e. distance from center of the marker to the side of the marker) + const SIDE_OFFSET_FAR: f32 = 1.0; + // the size of the projected on to the near plane + let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); + // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width + let near_offset_in_pixels = near_offset * gw2_width; + + // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width + let near_offset_in_pixels = near_offset_in_pixels + .clamp(*min_pixel_size, *max_pixel_size) + .min(gw2_width / 2.0); + + let near_offset_of_marker = near_offset_in_pixels / gw2_width; + near_offset_of_marker * camera_distance / z_near + }; + // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; + // we want to map 100 pixels to one meter in game + // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard + // But, i will ignore that as that makes markers too small + let x_offset = far_offset; + let y_offset = x_offset; // seems all markers are squares + let bottom_left = MarkerVertex { + position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), + texture_coordinates: vec2(0.0, 1.0), + alpha, + color, + fade_near_far, + }; + + let top_left = MarkerVertex { + position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), + texture_coordinates: vec2(0.0, 0.0), + alpha, + color, + fade_near_far, + }; + let top_right = MarkerVertex { + position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), + texture_coordinates: vec2(1.0, 0.0), + alpha, + color, + fade_near_far, + }; + let bottom_right = MarkerVertex { + position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), + texture_coordinates: vec2(1.0, 1.0), + alpha, + color, + fade_near_far, + }; + let vertices = [ + top_left, + bottom_left, + bottom_right, + bottom_right, + top_right, + top_left, + ]; + Some(MarkerObject { + vertices, + texture: texture_id, + distance: player_distance, + }) + } +} + +impl ActiveTrail { + pub fn get_vertices_and_texture( + attrs: &CommonAttributes, + positions: &[Vec3], + texture: TextureHandle, + ) -> Option { + // can't have a trail without atleast two nodes + if positions.len() < 2 { + return None; + } + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_near_far = Vec2::new(fade_near, fade_far); + let color = attrs.get_color().copied().unwrap_or([0u8; 4]); + // default taco width + let horizontal_offset = 20.0 / INCHES_PER_METER; + // scale it trail scale + let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); + let height = horizontal_offset * 2.0; + + let mut vertices = vec![]; + // trail mesh is split by separating different parts with a [0, 0, 0] + // we will call each separate trail mesh as a "strip" of trail. + // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. + for strip in positions.split(|&v| v == Vec3::ZERO) { + let mut y_offset = 1.0; + for two_positions in strip.windows(2) { + let first = two_positions[0]; + let second = two_positions[1]; + // right side of the vector from first to second + let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); + + let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; + let first_left = MarkerVertex { + position: first - (right_side * horizontal_offset), + texture_coordinates: vec2(0.0, y_offset), + alpha, + color, + fade_near_far, + }; + let first_right = MarkerVertex { + position: first + (right_side * horizontal_offset), + texture_coordinates: vec2(1.0, y_offset), + alpha, + color, + fade_near_far, + }; + let second_left = MarkerVertex { + position: second - (right_side * horizontal_offset), + texture_coordinates: vec2(0.0, new_offset), + alpha, + color, + fade_near_far, + }; + let second_right = MarkerVertex { + position: second + (right_side * horizontal_offset), + texture_coordinates: vec2(1.0, new_offset), + alpha, + color, + fade_near_far, + }; + y_offset = if new_offset.is_sign_positive() { + new_offset + } else { + 1.0 - new_offset.fract().abs() + }; + vertices.extend([ + second_left, + first_left, + first_right, + first_right, + second_right, + second_left, + ]); + } + } + + Some(ActiveTrail { + trail_object: TrailObject { + vertices: vertices.into(), + texture: match texture.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }, + }, + texture_handle: texture, + }) + } +} + +#[derive(Default, Clone)] +pub(crate) struct CurrentMapData { + /// the map to which the current map data belongs to + pub map_id: u32, + //pub active_elements: HashSet, + /// The textures that are being used by the markers, so must be kept alive by this hashmap + pub active_textures: OrderedHashMap, + /// The key is the index of the marker in the map markers + /// Their position in the map markers serves as their "id" as uuids can be duplicates. + pub active_markers: IndexMap, + pub wip_markers: IndexMap, + /// The key is the position/index of this trail in the map trails. same as markers + pub active_trails: IndexMap, + pub wip_trails: IndexMap, +} + diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs new file mode 100644 index 0000000..367b79d --- /dev/null +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -0,0 +1,269 @@ +use std::collections::{HashSet, HashMap}; +use ordered_hash_map::OrderedHashMap; + +use indexmap::IndexMap; +use uuid::Uuid; + +use crate::{ + message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, PackCore} +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct CategorySelection { + //#[serde(skip)] + pub uuid: Uuid,//FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information + #[serde(skip)] + pub parent: Option, + pub is_selected: bool,//has it been selected in configuration to be displayed + pub is_active: bool,//currently being displayed (i.e.: active) + pub separator: bool, + pub display_name: String, + pub children: OrderedHashMap, +} + +pub struct SelectedCategoryManager { + data: OrderedHashMap, + +} +impl<'a> SelectedCategoryManager { + pub fn new( + selected_categories: &OrderedHashMap, + categories: &IndexMap + ) -> Self { + let mut list_of_enabled_categories = Default::default(); + CategorySelection::get_list_of_enabled_categories( + &selected_categories, + &categories, + &mut list_of_enabled_categories, + &Default::default(), + ); + + Self { data: list_of_enabled_categories } + } + pub fn cloned_data(&self) -> OrderedHashMap { + self.data.clone() + } + pub fn is_selected(&self, category: &Uuid) -> bool { + self.data.contains_key(category) + } + pub fn get(&self, key: &Uuid) -> &CommonAttributes { + self.data.get(key).unwrap() + } + pub fn len(&self) -> usize { + self.data.len() + } + pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { + self.data.keys() + } +} + + + +impl CategorySelection { + pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { + let mut selectable_categories = OrderedHashMap::new(); + Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); + selectable_categories + } + fn get_list_of_enabled_categories( + selection: &OrderedHashMap, + categories: &IndexMap, + list_of_enabled_categories: &mut OrderedHashMap, + parent_common_attributes: &CommonAttributes, + ) { + for (_, cat) in categories { + if let Some(selectable_category) = selection.get(&cat.relative_category_name) { + if !selectable_category.is_selected { + continue; + } + let mut common_attributes = cat.props.clone(); + common_attributes.inherit_if_attr_none(parent_common_attributes); + Self::get_list_of_enabled_categories( + &selectable_category.children, + &cat.children, + list_of_enabled_categories, + &common_attributes, + ); + list_of_enabled_categories.insert(cat.guid, common_attributes); + } + } + } + pub fn get(selection: &mut OrderedHashMap, uuid: Uuid) -> Option<&mut CategorySelection> { + if selection.is_empty() { + return None; + } else { + for cat in selection.values_mut() { + if cat.uuid == uuid { + return Some(cat); + } + if let Some(res) = Self::get(&mut cat.children, uuid) { + return Some(res); + } + } + return None; + } + } + pub fn recursive_populate_guids( + selection: &mut OrderedHashMap, + entities_parents: &mut HashMap, + parent_uuid: Option, + ) { + for (cat_name, cat) in selection.iter_mut() { + if cat.uuid.is_nil() { + cat.uuid = Uuid::new_v4(); + } + cat.parent = parent_uuid.clone(); + Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); + if parent_uuid.is_some() { + entities_parents.insert(cat.uuid, parent_uuid.unwrap().clone()); + } + //assert!(cat.guid.len() > 0); + } + } + fn recursive_create_selectable_categories( + selectable_categories: &mut OrderedHashMap, + cats: &IndexMap, + ) { + for (_, cat) in cats.iter() { + if !selectable_categories.contains_key(&cat.relative_category_name) { + let to_insert = CategorySelection { + uuid: cat.guid, + parent: cat.parent, + is_selected: cat.default_enabled, + is_active: !cat.separator,//by default separators are not considered active since they contain nothing + separator: cat.separator, + display_name: cat.display_name.clone(), + children: Default::default(), + }; + //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); + selectable_categories.insert(cat.relative_category_name.clone(), to_insert); + } + let s = selectable_categories.get_mut(&cat.relative_category_name).unwrap(); + Self::recursive_create_selectable_categories(&mut s.children, &cat.children); + } + } + + pub fn recursive_set(selection: &mut OrderedHashMap, uuid: Uuid, status: bool) -> bool { + if selection.is_empty() { + return false; + } else { + for cat in selection.values_mut() { + if cat.separator { + continue; + } + if cat.uuid == uuid { + cat.is_selected = status; + return true; + } + if Self::recursive_set(&mut cat.children, uuid, status) { + return true; + } + } + return false; + } + } + pub fn recursive_set_all(selection: &mut OrderedHashMap, status: bool) { + if selection.is_empty() { + return; + } + for cat in selection.values_mut() { + if cat.separator { + continue; + } + cat.is_selected = status; + Self::recursive_set_all(&mut cat.children, status); + } + } + + pub fn recursive_update_active_categories(selection: &mut OrderedHashMap, active_elements: &HashSet) -> bool { + let mut is_active = false; + if selection.is_empty() { + //println!("recursive_update_active_categories is_empty"); + return is_active; + } + for cat in selection.values_mut() { + cat.is_active = active_elements.contains(&cat.uuid) || Self::recursive_update_active_categories(&mut cat.children, active_elements); + if cat.is_active { + is_active = true; + } + } + return is_active; + } + + fn context_menu( + u2b_sender: &std::sync::mpsc::Sender, + cs: &mut CategorySelection, + ui: &mut egui::Ui + ) { + if ui.button("Activate branch").clicked() { + cs.is_selected = true; + CategorySelection::recursive_set_all(&mut cs.children, true); + u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); + ui.close_menu(); + } + if ui.button("Deactivate branch").clicked() { + CategorySelection::recursive_set_all(&mut cs.children, false); + cs.is_selected = false; + u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); + ui.close_menu(); + } + } + + pub fn recursive_selection_ui( + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, + selection: &mut OrderedHashMap, + ui: &mut egui::Ui, + is_dirty: &mut bool, + show_only_active: bool, + late_discovery_categories: &HashSet, + ) { + if selection.is_empty() { + return; + } + egui::ScrollArea::vertical().show(ui, |ui| { + for (name, cat) in selection.iter_mut() { + if !cat.is_active && show_only_active && !cat.separator { + continue; + } + ui.horizontal(|ui| { + if cat.separator { + ui.add_space(3.0); + } else { + let cb = ui.checkbox(&mut cat.is_selected, ""); + if cb.changed() { + u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); + *is_dirty = true; + } + } + //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); + let color = if late_discovery_categories.contains(&cat.uuid) { + egui::Color32::LIGHT_RED + } else if cat.is_active { + egui::Color32::LIGHT_GREEN + } else { + egui::Color32::GRAY + }; + let label = egui::RichText::new(&cat.display_name).color(color); + if cat.children.is_empty() { + ui.label(label); + } else { + ui.menu_button(label, |ui: &mut egui::Ui| { + Self::recursive_selection_ui( + u2b_sender, + u2u_sender, + &mut cat.children, + ui, + is_dirty, + show_only_active, + late_discovery_categories + ); + }).response.context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); + } + }); + } + }); + } +} + diff --git a/crates/joko_package/src/manager/pack/dirty.rs b/crates/joko_package/src/manager/pack/dirty.rs new file mode 100644 index 0000000..3dd900c --- /dev/null +++ b/crates/joko_package/src/manager/pack/dirty.rs @@ -0,0 +1,29 @@ + +use ordered_hash_map::OrderedHashSet; + +use joko_core::RelativePath; + +#[derive(Debug, Default, Clone)] +pub(crate) struct DirtyMarker { + pub all: bool, + /// whether categories need to be saved + pub categories: bool, + /// whether selected categories needs to be saved + pub selected_categories: bool, + /// Whether any mapdata needs saving + pub map: OrderedHashSet, + /// whether any texture needs saving + pub texture: OrderedHashSet, + /// whether any tbin needs saving + pub tbin: OrderedHashSet, +} + +impl DirtyMarker { + pub fn is_dirty(&self) -> bool { + self.categories + || self.selected_categories + || !self.map.is_empty() + || !self.texture.is_empty() + || !self.tbin.is_empty() + } +} \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/entry.rs b/crates/joko_package/src/manager/pack/entry.rs new file mode 100644 index 0000000..ad78681 --- /dev/null +++ b/crates/joko_package/src/manager/pack/entry.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub struct PackEntry { + pub url: url::Url, + pub description: String, +} + diff --git a/crates/joko_package/src/manager/pack/file_selection.rs b/crates/joko_package/src/manager/pack/file_selection.rs new file mode 100644 index 0000000..0d4a4fa --- /dev/null +++ b/crates/joko_package/src/manager/pack/file_selection.rs @@ -0,0 +1,42 @@ +use std::collections::BTreeMap; + +pub struct SelectedFileManager { + data: BTreeMap, + +} +impl<'a> SelectedFileManager { + pub fn new( + selected_files: &BTreeMap, + pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + ) -> Self { + let mut list_of_enabled_files: BTreeMap = Default::default(); + SelectedFileManager::recursive_get_full_names( + &selected_files, + &pack_source_files, + ¤tly_used_files, + &mut list_of_enabled_files, + ); + Self { data: list_of_enabled_files } + } + fn recursive_get_full_names( + _selected_files: &BTreeMap, + _pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + list_of_enabled_files: &mut BTreeMap + ){ + for (key, v) in currently_used_files.iter() { + list_of_enabled_files.insert(key.clone(), *v); + } + } + pub fn cloned_data(&self) -> BTreeMap { + self.data.clone() + } + pub fn is_selected(&self, source_file_name: &String) -> bool { + let default = false; + self.data.is_empty() || *self.data.get(source_file_name).unwrap_or(&default) + } + pub fn len(&self) -> usize { + self.data.len() + } +} diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs new file mode 100644 index 0000000..58f50a1 --- /dev/null +++ b/crates/joko_package/src/manager/pack/import.rs @@ -0,0 +1,36 @@ +use std::io::Read; +use tracing::info; + +use miette::{IntoDiagnostic, Result}; +use crate::pack::PackCore; + + +#[derive(Debug, Default)] +pub enum ImportStatus { + #[default] + UnInitialized, + WaitingForFileChooser, + LoadingPack(std::path::PathBuf), + WaitingLoading(std::path::PathBuf), + PackDone(String, PackCore, bool), + PackError(miette::Report), +} + +pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { + let mut taco_zip = vec![]; + std::fs::File::open(&file_path) + .into_diagnostic()? + .read_to_end(&mut taco_zip) + .into_diagnostic()?; + + info!("starting to get pack from taco"); + crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { + ( + file_path + .file_name() + .map(|ostr| ostr.to_string_lossy().to_string()) + .unwrap_or_default(), + pack, + ) + }) +} \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/list.rs b/crates/joko_package/src/manager/pack/list.rs new file mode 100644 index 0000000..499fe2f --- /dev/null +++ b/crates/joko_package/src/manager/pack/list.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Default)] +pub struct PackList { + pub packs: BTreeMap, +} + + diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs new file mode 100644 index 0000000..58c0f7b --- /dev/null +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -0,0 +1,788 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, sync::Arc +}; + +use indexmap::IndexMap; +use ordered_hash_map::OrderedHashMap; + +use cap_std::fs_utf8::Dir; +use egui::{ColorImage, TextureHandle}; +use image::EncodableLayout; +use tracing::{debug, error, info, info_span}; +use uuid::Uuid; + +use crate::{ + io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} +}; +use jokolink::MumbleLink; +use joko_core::{ + task::{AsyncTask, AsyncTaskGuard}, + RelativePath +}; +use joko_render_models::trail::TrailObject; +use crate::message::BackToUIMessage; +use miette::{bail, Context, IntoDiagnostic, Result}; + +use super::activation::{ActivationData, ActivationType}; +use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; +use crate::manager::pack::category_selection::CategorySelection; +use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; + + +//TODO: separate in front and back tasks +pub (crate) struct PackTasks { + //an object that can handle such tasks should be passed as argument of any function that may required an async action + save_texture_task: AsyncTask>, + save_data_task: AsyncTask>, + load_all_packs_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> +} + +#[derive(Clone)] +pub struct LoadedPackData { + pub name: String, + pub uuid: Uuid, + pub dir: Arc, + /// The actual xml pack. + //pub core: PackCore, + pub categories: IndexMap, + pub all_categories: HashMap, + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, + selected_files: BTreeMap, + _is_dirty: bool,//there was an edition in the package itself + + // loca copy in the data side of what is exposed in UI + selectable_categories: OrderedHashMap, + pub entities_parents: HashMap, + activation_data: ActivationData, + active_elements: HashSet,//keep track of which elements are active +} + +#[derive(Clone)] +pub struct LoadedPackTexture { + pub name: String, + pub uuid: Uuid, + /// The directory inside which the pack data is stored + /// There should be a subdirectory called `core` which stores the pack core + /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. + /// eg: Active categories, activation data etc.. + pub dir: Arc, + pub tbins: HashMap, + pub textures: HashMap>, + + /// The selection of categories which are "enabled" and markers belonging to these may be rendered + selectable_categories: OrderedHashMap, + current_map_data: CurrentMapData, + activation_data: ActivationData, + active_elements: HashSet,//which are the active elements (loaded) + pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + _is_dirty: bool, +} + +impl PackTasks { + pub fn new() -> Self { + Self { + save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), + save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), + } + } + pub fn is_running(&self) -> bool { + self.save_texture_task.lock().unwrap().is_running() || + self.save_data_task.lock().unwrap().is_running() + } + pub fn count(&self) -> i32 { + 0 + + self.save_texture_task.lock().unwrap().count() + + self.save_data_task.lock().unwrap().count() + + self.load_all_packs_task.lock().unwrap().count() + } + + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { + if status { + std::mem::take(&mut texture_pack._is_dirty); + self.save_texture_task.lock().unwrap().send( + texture_pack.clone() + ); + } + } + + pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { + if status { + std::mem::take(&mut data_pack._is_dirty); + self.save_data_task.lock().unwrap().send( + data_pack.clone() + ); + } + } + pub fn load_all_packs(&self, jokolay_dir: Arc) { + self.load_all_packs_task.lock().unwrap().send( + jokolay_dir + ); + } + pub fn wait_for_load_all_packs(&self) -> Result<(BTreeMap, BTreeMap)> { + self.load_all_packs_task.lock().unwrap().recv().unwrap() + } + + fn change_map( + &self, + pack: &mut LoadedPackData, + b2u_sender: &std::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap + ) { + //TODO + //self.load_map_task.lock().unwrap().send(pack); + } + + fn async_save_texture( + pack_texture: LoadedPackTexture + ) -> Result<()> { + info!("Save texture package {:?}", pack_texture.dir); + match serde_json::to_string_pretty(&pack_texture.selectable_categories) { + Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + match serde_json::to_string_pretty(&pack_texture.activation_data) { + Ok(ad_json) => match pack_texture.dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(_) => { + debug!("wrote activation to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write activation data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } + let writing_directory = pack_texture.dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + save_pack_texture_to_dir(&pack_texture, &writing_directory)?; + Ok(()) + } + + fn async_save_data( + pack_data: LoadedPackData + ) -> Result<()> { + info!("Save data package {:?}", pack_data.dir); + pack_data.dir + .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to create xmlpack directory")?; + let writing_directory = pack_data.dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + save_pack_data_to_dir( + &pack_data, + &writing_directory, + )?; + Ok(()) + } + +} + + +impl LoadedPackData { + const CORE_PACK_DIR_NAME: &'static str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; + + fn load_selectable_categories(pack_dir: &Arc, pack: &PackCore) -> OrderedHashMap { + //FIXME: we need to patch those categories from the one in the files + (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize category data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_else(|| { + let cs = CategorySelection::default_from_pack_core(&pack); + match serde_json::to_string_pretty(&cs) { + Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + cs + }) + } + pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { + if !pack_dir + .try_exists(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to check if pack core exists")? + { + bail!("pack core doesn't exist in this pack"); + } + let core_dir = pack_dir + .open_dir(Self::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + let start = std::time::SystemTime::now(); + let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); + + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. + //let selectable_categories = CategorySelection::default_from_pack_core(&core); + let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); + + Ok(LoadedPackData { + name, + uuid: core.uuid, + dir: pack_dir, + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.source_files, + _is_dirty: false, + active_elements: Default::default(), + activation_data: Default::default(), + selectable_categories, + entities_parents: core.entities_parents, + }) + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { + if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { + self._is_dirty = true; + true + } else { + false + } + } + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { + if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { + cs.is_selected = status; + self._is_dirty = true; + if CategorySelection::recursive_set(&mut cs.children, uuid, status) { + return true; + } + } + false + } + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + + pub fn tick( + &mut self, + b2u_sender: &std::sync::mpsc::Sender, + loop_index: u128, + link: &MumbleLink, + currently_used_files: &BTreeMap, + list_of_active_or_selected_elements_changed: bool, + map_changed: bool, + tasks: &PackTasks, + next_loaded: &mut HashSet, + ) { + //since the loading of texture is lazy, there is no problem when calling this regularly + if map_changed || list_of_active_or_selected_elements_changed { + tasks.change_map(self, b2u_sender, link, currently_used_files); + let mut active_elements: HashSet = Default::default(); + self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); + b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); + self.active_elements = active_elements.clone(); + next_loaded.extend(active_elements); + } + } + + fn on_map_changed( + &mut self, + b2u_sender: &std::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + active_elements: &mut HashSet, + ){ + info!(link.map_id, "current map data is updated. {}", self.name); + if link.map_id == 0 { + info!("No map do not do anything"); + return; + } + debug!("Start building SelectedCategoryManager {}", self.selectable_categories.len()); + let selected_categories_manager = SelectedCategoryManager::new(&self.selectable_categories, &self.categories); + + debug!("Start building SelectedFileManager"); + let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.source_files, ¤tly_used_files); + + debug!("Start loading markers"); + let mut nb_markers_attempt = 0; + let mut nb_markers_loaded = 0; + for (_index, marker) in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .markers + .values() + .enumerate() + { + nb_markers_attempt += 1; + if selected_files_manager.is_selected(&marker.source_file_name) { + active_elements.insert(marker.guid); + active_elements.insert(marker.parent); + if selected_categories_manager.is_selected(&marker.parent) { + let category_attributes = selected_categories_manager.get(&marker.parent); + let mut common_attributes = marker.attrs.clone();// why a clone ? + common_attributes.inherit_if_attr_none(category_attributes); + let key = &marker.guid; + if let Some(behavior) = common_attributes.get_behavior() { + use crate::pack::Behavior; + if match behavior { + Behavior::AlwaysVisible => false, + Behavior::ReappearOnMapChange + | Behavior::ReappearOnDailyReset + | Behavior::OnlyVisibleBeforeActivation + | Behavior::ReappearAfterTimer + | Behavior::ReappearOnMapReset + | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), + Behavior::OncePerInstance => self + .activation_data + .global + .get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default(), + Behavior::DailyPerChar => + self.activation_data + .character + .get(&link.name) + .map(|a| a.contains_key(key)) + .unwrap_or_default(), + Behavior::OncePerInstancePerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| { + a.get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default() + }) + .unwrap_or_default(), + Behavior::WvWObjective => { + false // ??? + } + } { + continue; + } + } + if let Some(tex_path) = common_attributes.get_icon_file() { + b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); + } else { + debug!("no texture attribute on this marker"); + } + + nb_markers_loaded += 1; + } else { + debug!("category {} = {} is not enabled", marker.category, marker.parent); + } + } + } + + debug!("Start loading trails"); + let mut nb_trails_attempt = 0; + let mut nb_trails_loaded = 0; + for (_index, trail) in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .trails + .values() + .enumerate() + { + nb_trails_attempt += 1; + if selected_files_manager.is_selected(&trail.source_file_name) { + active_elements.insert(trail.guid); + active_elements.insert(trail.parent); + if selected_categories_manager.is_selected(&trail.parent) { + let category_attributes = selected_categories_manager.get(&trail.parent); + let mut common_attributes = trail.props.clone(); + common_attributes.inherit_if_attr_none(category_attributes); + if let Some(tex_path) = common_attributes.get_texture() { + b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); + } else { + debug!("no texture attribute on this trail"); + } + nb_trails_loaded += 1; + } else { + debug!("category {} = {} is not enabled", trail.category, trail.parent); + } + } + } + info!("Load notifications for {} on map {}: {}/{} markers and {}/{} trails", self.name, link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); + debug!("active categories: {:?}", selected_categories_manager.keys()); + } +} + + + +impl LoadedPackTexture { + const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; + + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + CategorySelection::recursive_update_active_categories(&mut self.selectable_categories, active_elements); + } + pub fn category_sub_menu( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, + ui: &mut egui::Ui, + show_only_active: bool, + ) { + //it is important to generate a new id each time to avoid collision + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + u2b_sender, + u2u_sender, + &mut self.selectable_categories, + ui, + &mut self._is_dirty, + show_only_active, + &self.late_discovery_categories + ); + }); + if self._is_dirty { + u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); + } + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub fn tick( + &mut self, + u2u_sender: &std::sync::mpsc::Sender, + _timestamp: f64, + link: &MumbleLink, + //next_on_screen: &mut HashSet, + z_near: f32, + tasks: &PackTasks, + ) { + tracing::trace!("LoadedPackTexture.tick: {} {}-{} {}-{}", + self.name, + self.current_map_data.active_markers.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.active_trails.len(), + self.current_map_data.wip_trails.len(), + ); + let mut marker_objects = Vec::new(); + for (uuid, marker) in self.current_map_data.active_markers.iter() { + if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { + marker_objects.push(mo); + } + } + tracing::trace!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); + u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); + let mut trail_objects = Vec::new(); + for (uuid, trail) in self.current_map_data.active_trails.iter() { + trail_objects.push(TrailObject { + vertices: trail.trail_object.vertices.clone(), + texture: trail.trail_object.texture, + }); + //next_on_screen.insert(*uuid); + } + tracing::trace!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); + u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); + } + + pub fn swap(&mut self) { + info!("swap {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.wip_trails.len() + ); + self.current_map_data.active_markers = std::mem::take(&mut self.current_map_data.wip_markers); + self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); + } + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + marker_uuid: Uuid, + position: glam::Vec3, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this icon texture"); + } + } + let th = self.current_map_data.active_textures.get(tex_path) + .unwrap_or(default_tex_id); + let texture_id = match th.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }; + + let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size + let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size + let am = ActiveMarker { + texture_id, + _texture: th.clone(), + common_attributes, + pos: position, + max_pixel_size, + min_pixel_size, + }; + self.current_map_data + .wip_markers + .insert(marker_uuid, am); + } + + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + info!(%tex_path, "failed to find this trail texture"); + } + } else { + debug!("Trail texture alreadu loaded {:?}", tex_path); + } + let texture_path = common_attributes.get_texture(); + let th = texture_path + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_tex_id); + + let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); + tbin + } else { + info!(?trail_uuid, "missing tbin path"); + return; + }; + let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { + tbin + } else { + info!(%tbin_path, "failed to find tbin"); + return; + }; + if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( + &common_attributes, + &tbin.nodes, + th.clone(), + ) { + self.current_map_data + .wip_trails + .insert(trail_uuid, active_trail); + } else { + info!("Cannot display {texture_path:?}") + } + + } + +} + +pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { + jokolay_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!("failed to create marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + let marker_manager_dir = jokolay_dir + .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!("failed to open marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + marker_manager_dir + .create_dir_all(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!("failed to create marker packs directory {}", PACKAGES_DIRECTORY_NAME))?; + let marker_packs_dir = marker_manager_dir + .open_dir(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!("failed to open marker packs dir {}", PACKAGES_DIRECTORY_NAME))?; + Ok(marker_packs_dir) +} + +pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap, BTreeMap)>{ + let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; + let mut data_packs: BTreeMap = Default::default(); + let mut texture_packs: BTreeMap = Default::default(); + + + for entry in marker_packs_dir + .entries() + .into_diagnostic() + .wrap_err("failed to get entries of marker packs dir")? + { + let entry = entry.into_diagnostic()?; + if entry.metadata().into_diagnostic()?.is_file() { + continue; + } + if let Ok(name) = entry.file_name() { + let pack_dir = entry + .open_dir() + .into_diagnostic() + .wrap_err(format!("failed to open pack entry as directory: {}", name))?; + { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name.clone(), pack_dir.into()) { + Ok(lp) => { + let (data, tex) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + } + Err(e) => { + error!(?e, "failed to load pack from directory: {}", name); + } + } + drop(span_guard); + } + } + } + Ok((data_packs, texture_packs)) +} + +fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, LoadedPackTexture)> { + if !pack_dir + .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to check if pack core exists")? + { + bail!("pack core doesn't exist in this pack"); + } + let core_dir = pack_dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .into_diagnostic() + .wrap_err("failed to open core pack directory")?; + let start = std::time::SystemTime::now(); + let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); + let res = build_from_core(name.clone(), pack_dir, core); + Ok(res) +} + + +pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { + let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); + let data = LoadedPackData { + name: name.clone(), + uuid: core.uuid, + dir: Arc::clone(&pack_dir), + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.source_files, + _is_dirty: false, + activation_data: Default::default(), + active_elements: Default::default(), + selectable_categories: selectable_categories.clone(), + entities_parents: core.entities_parents, + }; + let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_default(); + let tex = LoadedPackTexture { + uuid: core.uuid, + selectable_categories, + textures: core.textures, + current_map_data: Default::default(), + _is_dirty: false, + activation_data, + dir: Arc::clone(&pack_dir), + late_discovery_categories: core.late_discovery_categories, + name: name, + tbins: core.tbins, + active_elements: Default::default(), + }; + (data, tex) +} + diff --git a/crates/joko_package/src/manager/pack/mod.rs b/crates/joko_package/src/manager/pack/mod.rs new file mode 100644 index 0000000..2833ae2 --- /dev/null +++ b/crates/joko_package/src/manager/pack/mod.rs @@ -0,0 +1,8 @@ +pub mod category_selection; +pub mod file_selection; +pub mod activation; +pub mod active; +pub mod loaded; +pub mod dirty; +pub mod import; + diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs new file mode 100644 index 0000000..059a3f5 --- /dev/null +++ b/crates/joko_package/src/manager/package.rs @@ -0,0 +1,680 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex} +}; + +use glam::Vec3; +use tribool::Tribool; +use cap_std::fs_utf8::Dir; +use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; +use image::EncodableLayout; + +use tracing::{info_span, trace}; + +use joko_core::RelativePath; +use jokolink::MumbleLink; +use miette::Result; +use uuid::Uuid; +use crate::message::{UIToBackMessage, UIToUIMessage}; + +use crate::{message::BackToUIMessage, pack::CommonAttributes}; +use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; +use crate::manager::pack::import::ImportStatus; + +use super::pack::loaded::jokolay_to_marker_dir; + +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose +// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; + +/// It manage everything that has to do with marker packs. +/// 1. imports, loads, saves and exports marker packs. +/// 2. maintains the categories selection data for every pack +/// 3. contains activation data globally and per character +/// 4. When we load into a map, it filters the markers and runs the logic every frame +/// 1. If a marker needs to be activated (based on player position or whatever) +/// 2. marker needs to be drawn +/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture +/// 4. render that marker use joko_render +#[must_use] +pub struct PackageDataManager { + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + //_marker_manager_dir: Arc, + /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. + /// The name of the child directory is the name of the pack + pub marker_packs_dir: Arc, + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + pub packs: BTreeMap, + tasks: PackTasks, + current_map_id: u32, + show_only_active: bool, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, + + pub currently_used_files: BTreeMap, + parents: HashMap, + loaded_elements: HashSet, + on_screen: BTreeSet, +} +#[must_use] +pub struct PackageUIManager { + default_marker_texture: Option, + default_trail_texture: Option, + packs: BTreeMap, + tasks: PackTasks, + + currently_used_files: BTreeMap, + all_files_tribool: Tribool, + all_files_toggle: bool, + show_only_active: bool, +} + +impl PackageDataManager { + /// Creates a new instance of [MarkerManager]. + /// 1. It opens the marker manager directory + /// 2. loads its configuration + /// 3. opens the packs directory + /// 4. loads all the packs + /// 5. loads all the activation data + /// 6. returns self + pub fn new(packs: BTreeMap, jokolay_dir: Arc) -> Result { + let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; + Ok(Self { + packs, + tasks: PackTasks::new(), + marker_packs_dir: Arc::new(marker_packs_dir), + //_marker_manager_dir: marker_manager_dir.into(), + current_map_id: 0, + save_interval: 0.0, + show_only_active: true, + currently_used_files: Default::default(), + parents: Default::default(), + loaded_elements: Default::default(), + on_screen: Default::default(), + }) + } + + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_set(uuid, status) { + break; + } + } + } + + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_branch_set(uuid, status) { + break; + } + } + } + + pub fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> HashSet + where I: Iterator + { + let iter = input.into_iter(); + let mut result: HashSet = HashSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; + } + next_gen.push(p.clone()); + } + } + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + unreachable!("The loop should always return"); + } + + pub fn get_active_elements_parents(&mut self, categories_and_elements_to_be_loaded: HashSet) { + trace!("There are {} active elements", categories_and_elements_to_be_loaded.len()); + + //first merge the parents to iterate overit + let mut parents: HashMap = Default::default(); + for pack in self.packs.values_mut() { + parents.extend(pack.entities_parents.clone()); + } + self.parents = parents; + //then climb up the tree of parent's categories + self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); + } + + pub fn tick( + &mut self, + b2u_sender: &std::sync::mpsc::Sender, + loop_index: u128, + link: Option<&MumbleLink>, + choice_of_category_changed: bool, + ) { + let mut currently_used_files: BTreeMap = Default::default(); + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + + match link { + Some(link) => { + //TODO: how to save/load the active files ? + //TODO: find an efficient way to propagate the file deactivation + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.source_files.get(&marker.source_file_name) { + currently_used_files.insert( + marker.source_file_name.clone(), + *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + ); + } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.source_files.get(&trail.source_file_name) { + currently_used_files.insert( + trail.source_file_name.clone(), + *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + ); + } + } + } + } + let mut tasks = &self.tasks; + for (uuid, pack) in self.packs.iter_mut() { + let span_guard = info_span!("Updating package status").entered(); + b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + &b2u_sender, + loop_index, + link, + ¤tly_used_files, + have_used_files_list_changed || choice_of_category_changed, + map_changed, + &tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + if map_changed { + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); + } + if map_changed || have_used_files_list_changed || choice_of_category_changed { + //there is no point in sending a new list if nothing changed + b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); + self.currently_used_files = currently_used_files; + b2u_sender.send(BackToUIMessage::TextureSwapChain); + } + }, + None => {}, + }; + } + + fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } + pub fn save(&mut self, mut data_pack: LoadedPackData) -> Uuid { + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == data_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks.save_data(&mut data_pack, true); + let mut uuid_to_insert = data_pack.uuid.clone(); + while self.packs.contains_key(&uuid_to_insert) {//collision avoidance + trace!("Uuid collision detected for {} for package {}", uuid_to_insert, data_pack.name); + uuid_to_insert = Uuid::new_v4(); + } + data_pack.uuid = uuid_to_insert; + self.packs.insert(uuid_to_insert, data_pack); + uuid_to_insert + } + + pub fn load_all( + &mut self, + jokolay_dir: Arc, + b2u_sender: &std::sync::mpsc::Sender, + ) { + once::assert_has_not_been_called!("Early load must happen only once"); + // Called only once at application start. + b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + self.tasks.load_all_packs(jokolay_dir); + if let Ok((data_packages, texture_packages)) = self.tasks.wait_for_load_all_packs() { + for (uuid, data_pack) in data_packages { + self.packs.insert(uuid, data_pack); + } + for (uuid, texture_pack) in texture_packages { + b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + } + b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); + } + + } + +} + + +impl PackageUIManager { + pub fn new(packs: BTreeMap) -> Self { + Self { + packs, + tasks: PackTasks::new(), + default_marker_texture: None, + default_trail_texture: None, + + all_files_tribool: Tribool::True, + all_files_toggle: false, + show_only_active: true, + currently_used_files: Default::default()// UI copy to (de-)activate files + } + } + + pub fn late_init( + &mut self, + etx: &egui::Context, + ) { + if self.default_marker_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_marker_texture = Some(etx.load_texture( + "default marker", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + if self.default_trail_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../pack/trail_rainbow.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_trail_texture = Some(etx.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + } + + pub fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for pack in self.packs.values_mut() { + pack.update_active_categories(active_elements); + } + } + + pub fn update_pack_active_categories(&mut self, pack_uuid: Uuid, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for (uuid, pack) in self.packs.iter_mut() { + if uuid == &pack_uuid { + pack.update_active_categories(active_elements); + break; + } + } + } + pub fn swap(&mut self) { + for pack in self.packs.values_mut() { + pack.swap(); + } + } + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + self.packs + .get_mut(&pack_uuid) + .map( |pack| { + pack.load_marker_texture( + egui_context, + self.default_marker_texture.as_ref().unwrap(), + &tex_path, + marker_uuid, + position, + common_attributes, + ); + }); + } + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + self.packs + .get_mut(&pack_uuid) + .map( |pack| { + pack.load_trail_texture( + egui_context, + &self.default_trail_texture.as_ref().unwrap(), + &tex_path, + trail_uuid, + common_attributes, + ); + }); + } + + fn pack_importer( + import_status: Arc>, + ) { + //called when a new pack is imported + rayon::spawn( move || { + *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + + if let Some(file_path) = rfd::FileDialog::new() + .add_filter("taco", &["zip", "taco"]) + .pick_file() + { + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); + } else { + *import_status.lock().unwrap() = + ImportStatus::PackError(miette::miette!("file chooser was cancelled")); + } + }); + } + + fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn tick( + &mut self, + u2u_sender: &std::sync::mpsc::Sender, + timestamp: f64, + link: &MumbleLink, + z_near: f32, + ) { + let mut tasks = &self.tasks; + for (uuid, pack) in self.packs.iter_mut() { + let span_guard = info_span!("Updating package status").entered(); + tasks.save_texture(pack, pack.is_dirty()); + pack.tick( + &u2u_sender, + timestamp, + link, + z_near, + &tasks + ); + std::mem::drop(span_guard); + } + u2u_sender.send(UIToUIMessage::RenderSwapChain); + //u2u_sender.send(UIToUIMessage::Present); + } + + pub fn menu_ui( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + u2u_sender: &std::sync::mpsc::Sender, + ui: &mut egui::Ui, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, + ) { + ui.menu_button("Markers", |ui| { + if self.show_only_active { + if ui.button("Show everything").clicked() { + self.show_only_active = false; + } + } else { + if ui.button("Show only active").clicked() { + self.show_only_active = true; + } + } + if ui.button("Activate all elements").clicked() { + self.category_set_all(true); + u2b_sender.send(UIToBackMessage::CategorySetAll(true)); + } + if ui.button("Deactivate all elements").clicked() { + self.category_set_all(false); + u2b_sender.send(UIToBackMessage::CategorySetAll(false)); + } + + for pack in self.packs.values_mut() { + //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; + //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active); + } + + }); + if self.tasks.is_running() || nb_running_tasks_on_back > 0 || nb_running_tasks_on_network > 0{ + let sp = egui::Spinner::new().color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); + ui.add(sp); + } + } + pub fn status_as_color(&self, nb_running_tasks_on_back: i32, nb_running_tasks_on_network: i32) -> egui::Color32 { + //we can choose whatever color code we want to focus on load, save, network queries, anything. + let nb_running_tasks_on_ui = self.tasks.count(); + //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 + let color_ui = if nb_running_tasks_on_ui > 0 { + let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; + let res = nb_ui_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_back = if nb_running_tasks_on_back > 0 { + let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; + let res = nb_bask_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_network = if nb_running_tasks_on_network > 0 { + let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; + let res = nb_network_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + egui::Color32::from_rgb(color_ui, color_back, color_network) + } + + fn gui_file_manager( + &mut self, + event_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + open: &mut bool, + link: Option<&MumbleLink> + ) { + let mut files_changed = false; + Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Grid::new("link grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + if self.all_files_tribool.is_indeterminate(){ + ui.add(egui::Checkbox::new(&mut self.all_files_toggle, "File").indeterminate(true)); + } else { + ui.checkbox(&mut self.all_files_toggle, "File"); + } + ui.label("Trails"); + ui.label("Markers"); + ui.end_row(); + + for file in self.currently_used_files.iter_mut() { + let cb = ui.checkbox(file.1, file.0.clone()); + if cb.changed() { + files_changed = true; + } + if ui.button("Edit").clicked() { + println!("click {}", file.0.clone()); + } + ui.end_row(); + } + ui.end_row(); + }) + }); + Ok(()) + }); + if files_changed { + event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); + } + } + fn gui_package_loader( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + import_status: &Arc>, + open: &mut bool + ) { + Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { + CollapsingHeader::new("Loaded Packs").show(ui, |ui| { + egui::Grid::new("packs").striped(true).show(ui, |ui| { + let mut to_delete = vec![]; + for pack in self.packs.values() { + ui.label(pack.name.clone()); + if ui.button("delete").clicked() { + to_delete.push(pack.uuid); + } + if ui.button("Details").clicked() { + //TODO + } + ui.end_row(); + } + if !to_delete.is_empty() { + u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); + } + }); + }); + + if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? + //let import_status = import_status.lock().unwrap(); + Self::pack_importer(Arc::clone(import_status)); + } + ui.label("import not started yet"); + } + ImportStatus::WaitingForFileChooser => { + ui.label( + "wailting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + if *saved { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } else { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + if ui.button("save").clicked() { + u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); + } + } + } + ImportStatus::PackError(e) => { + let error_msg = format!("failed to import pack due to error: {e:#?}"); + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + *status = ImportStatus::UnInitialized; + } + ui.colored_label( + egui::Color32::RED, + error_msg, + ); + } + } + } + + Ok(()) + }); + } + pub fn gui( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + is_marker_open: &mut bool, + import_status: &Arc>, + is_file_open: &mut bool, + timestamp: f64, + link: Option<&MumbleLink> + ) { + self.gui_package_loader(u2b_sender, etx, import_status, is_marker_open); + self.gui_file_manager(u2b_sender, etx, is_file_open, link); + } + + pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { + /* + We save in a file with the name of the package, while we keep track of it from a uuid point of view. + It means we can have duplicates unless package with same name is deleted. + */ + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == texture_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks.save_texture(&mut texture_pack, true); + self.packs.insert(texture_pack.uuid, texture_pack); + } +} + + diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs new file mode 100644 index 0000000..9279f3c --- /dev/null +++ b/crates/joko_package/src/message.rs @@ -0,0 +1,55 @@ +use std::collections::{BTreeMap, HashSet}; + +use uuid::Uuid; + +use glam::Vec3; + +use jokolink::MumbleLink; +use joko_core::RelativePath; +use joko_render_models::{ + marker::MarkerObject, + trail::TrailObject +}; + +use crate::{pack::{CommonAttributes, PackCore}, LoadedPackTexture}; + +pub enum BackToUIMessage { + ActiveElements(HashSet),//list of all elements that are loaded for current map + CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display + LoadedPack(LoadedPackTexture),//push a loaded pack to UI + DeletedPacks(Vec),//push a deleted set of packs to UI + ImportedPack(String, PackCore), + ImportFailure(miette::Report), + MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), + //MumbleLink(Option), + //MumbleLinkChanged,//tell there is a need to resize + NbTasksRunning(i32),//tell the number of taks running in background + PackageActiveElements(Uuid, HashSet),// first is the package reference, second is the list of active elements in the package. + TextureSwapChain,// The list of texture to load was changed, will be soon followed by a RenderSwapChain + TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), +} + +pub enum UIToBackMessage { + ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. + CategoryActivationElementStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationBranchStatusChange(Uuid, bool),//same, for a whole branch + CategoryActivationStatusChanged,//something happened that needs to reload the whole set + CategorySetAll(bool),//signal all categories should be now at this status + DeletePacks(Vec),//uuid of the pack to delete + ImportPack(std::path::PathBuf), + MumbleLinkBindedOnUI, + MumbleLinkAutonomous, + MumbleLink(Option),//pushed from a value imposed by UI. Either a form or a traveling for demo. + ReloadPack, + SavePack(String, PackCore), +} + +pub enum UIToUIMessage { + BulkMarkerObject(Vec), + BulkTrailObject(Vec), + //Present,// a render loop is finished and we can present it + MarkerObject(MarkerObject), + RenderSwapChain,// The list of elements to display was changed + TrailObject(TrailObject), +} + diff --git a/crates/joko_package/src/pack/common.rs b/crates/joko_package/src/pack/common.rs new file mode 100644 index 0000000..aea6dff --- /dev/null +++ b/crates/joko_package/src/pack/common.rs @@ -0,0 +1,2274 @@ +use std::str::FromStr; + +use enumflags2::{bitflags, BitFlags}; +use glam::Vec3; +use itertools::Itertools; +use tracing::info; +use xot::Element; + +use crate::io::XotAttributeNameIDs; + +use super::RelativePath; +use jokoapi::end_point::mounts::Mount; +use jokoapi::end_point::races::Race; +use smol_str::SmolStr; +/// This is a onetime macro to reduce code duplication +/// It basically takes the CommmonAttributes struct, adds the active_attributes and bool_attributes fields to it. +/// Then, it creates a method call `inherit_if_attr_none`, which will clone fields from other struct, if its own fields are not active (set) +/// Finally, it derives a getter and setter for all of the fields. +/// +/// Once we are close to releasing a 1.0 version of this crate, we should just expand all these macros to raw code as its never going to change again. +macro_rules! common_attributes_struct_macro { + ( + $( #[$attr:meta] )* + $vis:vis struct $name:ident { + $( $( #[$field_attr:meta] )* $field_vis:vis $field:ident : $ty:ty ),* $(,)? + } + ) => { + $( #[$attr] )* + $vis struct $name { + active_attributes: BitFlags, + bool_attributes: BitFlags, + $( $( #[$field_attr] )* $field : $ty ),* + } + impl $name { + $vis fn inherit_if_attr_none(&mut self, other: &$name) { + $(if !self.active_attributes.contains(ActiveAttributes::$field) + && other.active_attributes.contains(ActiveAttributes::$field) { + self.active_attributes.insert(ActiveAttributes::$field); + self.$field = other.$field.clone(); + })+ + } + $( + paste::paste!( + /// This gets the value IF the attribute is set. Otherwise returns None. + #[allow(unused)] + $vis fn [](&self) -> Option<&$ty> { + self.active_attributes.contains(ActiveAttributes::$field).then_some(&self.$field) + } + /// This directly sets the field to value IF the value is Some. Otherwise deactivates the attribute. + /// + /// Warning: This simply overwrites the value of the existing field. + /// So, if you wanted to combine them (an array or bitflags), then do get -> combine it smh -> set. + #[allow(unused)] + $vis fn [](&mut self, value: Option<$ty>) { + if let Some(value) = value { + self.active_attributes.insert(ActiveAttributes::$field); + self.$field = value; + } else { + self.active_attributes.remove(ActiveAttributes::$field); + } + } + ); + )+ + } + } +} +/// uses the [ToString] impl of attributes to serialize them (only if the relevant active attribute flag is set) +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. +/// ```rust +/// set_attribute_to_ele!(ca, ele, names, [field1, field2, field3]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if ca.active_attributes.contains(ActiveAttributes::field1) { +/// ele.set_attribute(names.field1, ca.field1.to_string()); +/// } +/// ``` +macro_rules! set_attribute_to_ele { + ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if $ca.active_attributes.contains(ActiveAttributes::$field) { + $ele.set_attribute($names.$field, $ca.$field.to_string()); + })+ + }; +} +/// true -> 1 and 0 -> false. (only if the relevant active attribute flag is set) +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. +/// ```rust +/// set_attribute_bool_to_ele!(ca, ele, names, [field1, field2, field3]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if ca.active_attributes.contains(ActiveAttributes::field1) { +/// ele.set_attribute(names.field1, +/// ca +/// .bool_attributes +/// .contains(BoolAttributes::field1) +/// .then_some(1) +/// .unwrap_or(0u8) +/// .to_string() +/// ); +/// } +/// ``` +macro_rules! set_attribute_bool_to_ele { + ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if $ca.active_attributes.contains(ActiveAttributes::$field) { + $ele.set_attribute( + $names.can_fade, + $ca.bool_attributes + .contains(BoolAttributes::$field) + .then_some(1) + .unwrap_or(0u8) + .to_string(), + ); + })+ + }; +} +/// iterates over a bitflags field and joins the enabled flags (as str) with comma. (only if the relevant active attribute flag is set) +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. +/// ```rust +/// set_attribute_bitflags_as_array_to_ele!(ca, ele, names, [field1, field2, field3]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if ca.active_attributes.contains(ActiveAttributes::field1) { +/// ele.set_attribute( +/// names.field1, +/// ca.field1.iter().map(|s| s.as_ref()).join(","), +/// ); +/// } +/// ``` +macro_rules! set_attribute_bitflags_as_array_to_ele { + ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if $ca.active_attributes.contains(ActiveAttributes::$field) { + $ele.set_attribute( + $names.$field, + $ca.$field.iter().map(|s| s.to_string()).join(","), + ); + })+ + }; +} +/// uses the [FromStr] impl of attributes to deserialize them (and set the relevant active attribute flag if successful) +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. +/// ```rust +/// update_attribute_from_ele!(ca, ele, names, [field1, field2, field3]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if let Some(value) = ele.get_attribute(names.field1) { +/// match value.trim().parse() { +/// Ok(value) => { +/// ca +/// .active_attributes +/// .insert(ActiveAttributes::fiel1); +/// ca.field1 = value; +/// } +/// Err(e) => { +/// tracing::info!(?e, value, "failed to parse {}", "field1"); +/// } +/// } +/// } +/// ``` +macro_rules! update_attribute_from_ele { + ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if let Some(value) = $ele.get_attribute($names.$field) { + match value.trim().parse() { + Ok(value) => { + $ca + .active_attributes + .insert(ActiveAttributes::$field); + $ca.$field = value; + } + Err(e) => { + tracing::info!(?e, value, "failed to parse {}", stringify!($field)); + } + } + })+ + }; +} + +/// deserializes an [i8] and matches that as 1 -> true and 0 -> false. +/// On success, set the relevant active attribute flag. +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. +/// ```rust +/// update_attribute_bool_from_ele!(ca, ele, names, [field1, field2, field3]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if let Some(value) = ele.get_attribute(names.field1) { +/// match value.trim().parse::() { +/// Ok(value) => { +/// match value { +/// 0 | 1 => { +/// ca +/// .active_attributes +/// .insert(ActiveAttributes::field1); +/// ca.bool_attributes.set( +/// BoolAttributes::field1, +/// if value == 0 { false } else { true }, +/// ); +/// } +/// _ => { +/// info!(value, "failed to parse {}", "field1"); +/// } +/// } +/// } +/// Err(e) => { +/// tracing::info!(?e, value, "failed to parse {}", "field1"); +/// } +/// } +/// } +/// ``` + +fn parse_boolean(raw_value: &str) -> Option { + let trimmed = raw_value.trim().to_lowercase(); + match trimmed.as_ref() { + "true" => {Some(true)}, + "false" => {Some(false)}, + _ => { + match trimmed.parse::() {//might entirely get rid of parsing + Ok(parsed_value) => { + match parsed_value { + 0 | 1 => { + Some(parsed_value == 1) + } + _ => None + } + } + Err(_e) => { + None + } + } + }, + } +} +macro_rules! update_attribute_bool_from_ele { + ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { + $(if let Some(value) = $ele.get_attribute($names.$field) { + if let Some(found) = parse_boolean(value) { + $common_attributes + .active_attributes + .insert(ActiveAttributes::$field); + $common_attributes.bool_attributes.set( + BoolAttributes::$field, + found, + ); + } else { + tracing::info!(value, "failed to parse {}", stringify!($field)); + } + })+ + }; +} +/// deserializes an [i8] and matches that as 1 -> true and 0 -> false. +/// On success, set the relevant active attribute flag. +/// +/// #### Args: +/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) +/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) +/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) +/// - [f1,t1; f2,t2;...]: an array of field identifiers which will be serialized and their enum type. +/// ```rust +/// update_attribute_bitflags_array_from_ele!(ca, ele, names, [f1, t1; f2, t2]); +/// ``` +/// +/// The expansion for each field is like this +/// ```rust +/// if let Some(field1_str) = ele.get_attribute(names.field1) { +/// for value in field1_str.split(',') { +/// match value.trim().parse::() { +/// Ok(flag) => { +/// ca +/// .active_attributes +/// .insert(ActiveAttributes::field1); +/// ca.field1.set(flag); +/// } +/// Err(e) => { +/// tracing::info!(value, e); +/// } +/// } +/// } +/// } +/// ``` +macro_rules! update_attribute_bitflags_array_from_ele { + ($ca: ident, $ele: ident,$xot_names: ident, [$($field: ident, $ty: ty);+]) => { + $(if let Some(value) = $ele.get_attribute($xot_names.$field) { + for item in value.trim().split(',') { + match item.trim().parse::<$ty>() { + Ok(flag) => { + $ca.active_attributes.insert(ActiveAttributes::$field); + $ca.$field.insert(flag); + } + Err(e) => { + info!(item, e); + } + } + } + })+ + }; +} +/// generates getters for bool attributes +/// ```rust +/// getters_for_bool_attributes!([field1, field2, field3]); +/// ``` +/// +/// This generates a `fn get_field1(&self) -> Option` +/// if attribute is not active, we return None. Otherwise, the value of the boolean attribute +macro_rules! getters_for_bool_attributes { + ([$($field: ident),+]) => { + paste::paste!{ + $( + /// If the attribute is not set, then we return None. + /// Otherwise, we return the boolean value of the attribute. + #[allow(unused)] + fn [](&self) -> Option { + self.active_attributes.contains(ActiveAttributes::$field).then_some( + self.bool_attributes.contains(BoolAttributes::$field) + ) + } + )+ + } + }; +} +/// generates setters for bool attributes +/// ```rust +/// setters_for_bool_attributes!([field1, field2, field3]); +/// ``` +/// +/// This generates a `fn set_field1(&mut self, value: Option)` +/// if attribute is not active, we return None. Otherwise, the value of the boolean attribute +macro_rules! setters_for_bool_attributes { + ([$($field: ident),+]) => { + paste::paste!{ + $( + /// If the attribute is not set, then we return None. + /// Otherwise, we return the boolean value of the attribute. + #[allow(unused)] + fn [](&mut self, value: Option) { + if let Some(value) = value { + self.active_attributes.insert(ActiveAttributes::$field); + self.bool_attributes.set(BoolAttributes::$field, value); + } else { + self.active_attributes.remove(ActiveAttributes::$field); + } + } + )+ + } + }; +} +common_attributes_struct_macro!( + /// the struct we use for inheritance from category/other markers. + #[derive(Debug, Clone, Default)] + pub struct CommonAttributes { + /// An ID for an achievement from the GW2 API. Markers with the corresponding achievement ID will be hidden if the ID is marked as "done" for the API key that's entered in TacO. + achievement_id: u32, + /// This is similar to achievementId, but works for partially completed achievements as well, if the achievement has "bits", they can be individually referenced with this. + achievement_bit: u32, + /// How opaque the displayed icon should be. The default is 1.0 + alpha: f32, + anim_speed: f32, + /// it describes the way the marker will behave when a player presses 'F' over it. + behavior: Behavior, + bounce: SmolStr, + bounce_delay: f32, + bounce_duration: f32, + bounce_height: f32, + /// hex value. The color tint of the marker. sRGBA8 + color: [u8; 4], + copy: SmolStr, + copy_message: SmolStr, + cull: Cull, + /// Determines how far the marker will completely disappear. If below 0, the marker won't disappear at any distance. Default is -1. FadeFar needs to be higher than fadeNear for sane results. This value is in game units (inches). + // #[serde(rename = "fadeFar")] + fade_far: f32, + /// Determines how far the marker will start to fade out. If below 0, the marker won't disappear at any distance. Default is -1. This value is in game units (inches). + // #[serde(rename = "fadeNear")] + fade_near: f32, + festival: BitFlags, + /// Specifies how high above the ground the marker is displayed. Default value is 1.5. in meters + height_offset: f32, + hide: SmolStr, + /// The icon to be displayed for the marker. If not given, this defaults to the image shown at the start of this article. This should point to a .png file. The overlay looks for the image files both starting from the root directory and the POIs directory for convenience. Make sure you don't use too high resolution (above 128x128) images because the texture atlas used for these is limited in size and it's a needless waste of resources to fill it quickly.Default value: 20 + icon_file: RelativePath, + /// The size of the icon in the game world. Default is 1.0 if this is not defined. Note that the "screen edges herd icons" option will limit the size of the displayed images for technical reasons. + icon_size: f32, + /// his can be a multiline string, it will show up on screen as a text when the player is inside of infoRange of the marker + info: SmolStr, + /// This determines how far away from the marker the info string will be visible. in meters. + info_range: f32, + /// The size of the marker at normal UI scale, at zoom level 1 on the miniMap, in Pixels. For trails this value can be used to tweak the width + // #[serde(rename = "mapDisplaySize")] + map_display_size: f32, + map_fade_out_scale_level: f32, + map_type: BitFlags, + /// Determines the maximum size of a marker on the screen, in pixels. + // #[serde(rename = "maxSize")] + max_size: f32, + /// Determines the minimum size of a marker on the screen, in pixels. + // #[serde(rename = "minSize")] + min_size: f32, + mount: BitFlags, + profession: BitFlags, + race: BitFlags, + /// For behavior 4 this tells how long the marker should be invisible after pressing 'F'. For behavior 5 this will tell how long a map cycle is. in seconds. + // #[serde(rename = "resetLength")] + reset_length: f32, + /// this will supply data for behavior 5. The data will be given in seconds. + // #[serde(rename = "resetOffset")] + reset_offset: f32, + rotate: Vec3, + rotate_x: f32, + rotate_y: f32, + rotate_z: f32, + show: SmolStr, + specialization: Vec, + text: SmolStr, + texture: RelativePath, + tip_name: SmolStr, + tip_description: SmolStr, + title: SmolStr, + title_color: [u8; 4], + /// will toggle the specified category on or off when triggered with the action key. or with auto_trigger/trigger_range + // #[serde(rename = "toggleCategory")] + toggle_category: SmolStr, + trail_data: RelativePath, + trail_scale: f32, + /// Determines the range from where the marker is triggered. in meters. + trigger_range: f32, + } +); + +impl CommonAttributes { + getters_for_bool_attributes!([ + auto_trigger, + can_fade, + has_countdown, + in_game_visibility, + invert_behavior, + is_wall, + keep_on_map_edge, + map_visibility, + mini_map_visibility, + scale_on_map_with_zoom + ]); + setters_for_bool_attributes!([ + auto_trigger, + can_fade, + has_countdown, + in_game_visibility, + invert_behavior, + is_wall, + keep_on_map_edge, + map_visibility, + mini_map_visibility, + scale_on_map_with_zoom + ]); + pub(crate) fn update_common_attributes_from_element( + &mut self, + ele: &Element, + names: &XotAttributeNameIDs, + ) { + if let Some(input_str) = ele.get_attribute(names.color) { + use data_encoding::HEXLOWER_PERMISSIVE; + let mut output = [0u8; 4]; + match HEXLOWER_PERMISSIVE.decode_len(input_str.len()) { + Ok(len) => { + match HEXLOWER_PERMISSIVE.decode_mut(input_str.as_bytes(), &mut output[0..len]) + { + Ok(_) => { + self.active_attributes.insert(ActiveAttributes::color); + self.color = output; + } + Err(e) => { + info!(?e, input_str, "failed to decode hex bytes of the attribute"); + } + } + } + Err(e) => { + info!(?e, input_str, "failed to get decode len for hex attribute"); + } + } + } + if let Some(input_str) = ele.get_attribute(names.title_color) { + use data_encoding::HEXLOWER_PERMISSIVE; + let mut output = [0u8; 4]; + match HEXLOWER_PERMISSIVE.decode_len(input_str.len()) { + Ok(len) => { + match HEXLOWER_PERMISSIVE.decode_mut(input_str.as_bytes(), &mut output[0..len]) + { + Ok(_) => { + self.active_attributes.insert(ActiveAttributes::title_color); + self.title_color = output; + } + Err(e) => { + info!(?e, input_str, "failed to decode hex bytes of the attribute"); + } + } + } + Err(e) => { + info!(?e, input_str, "failed to get decode len for hex attribute"); + } + } + } + if let Some(rotate_str) = ele.get_attribute(names.rotate) { + let mut array = [0f32; 3]; + for (index, value) in rotate_str.trim().split(',').enumerate() { + match value.parse::() { + Ok(f) => { + if let Some(x) = array.get_mut(index) { + *x = f; + self.rotate = array.into(); + self.active_attributes.insert(ActiveAttributes::rotate); + } + } + Err(e) => { + info!(?e, rotate_str, value, "failed to parse rotate attribute"); + } + } + } + } + if let Some(specs) = ele.get_attribute(names.specialization) { + for spec in specs.trim().split(',') { + match spec.parse() { + Ok(s) => { + self.active_attributes + .insert(ActiveAttributes::specialization); + self.specialization.push(s); + } + Err(e) => { + info!(specs, spec, e); + } + } + } + } + // bitflags with multiple elements + update_attribute_bitflags_array_from_ele!(self, ele, names, [ + festival, Festival; + map_type, MapType; + mount, Mount; + profession, Profession; + race, Race + ]); + + // bools + update_attribute_bool_from_ele!( + self, + ele, + names, + [ + auto_trigger, + can_fade, + has_countdown, + in_game_visibility, + invert_behavior, + is_wall, + keep_on_map_edge, + map_visibility, + mini_map_visibility, + scale_on_map_with_zoom + ] + ); + update_attribute_from_ele!( + self, + ele, + names, + [ + icon_file, + texture, + trail_data, + achievement_id, + achievement_bit, + bounce, + copy, + hide, + info, + copy_message, + show, + text, + tip_name, + tip_description, + title, + toggle_category, + alpha, + anim_speed, + bounce_delay, + bounce_duration, + bounce_height, + fade_near, + fade_far, + height_offset, + icon_size, + info_range, + map_display_size, + map_fade_out_scale_level, + max_size, + min_size, + reset_length, + reset_offset, + rotate_x, + rotate_y, + rotate_z, + trail_scale, + trigger_range, + cull, + behavior + ] + ); + } + + pub(crate) fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { + // color arrays + if self.active_attributes.contains(ActiveAttributes::color) { + ele.set_attribute(names.color, data_encoding::HEXLOWER.encode(&self.color)); + } + if self + .active_attributes + .contains(ActiveAttributes::title_color) + { + ele.set_attribute( + names.title_color, + data_encoding::HEXLOWER.encode(&self.title_color), + ); + } + // rotate array + if self.active_attributes.contains(ActiveAttributes::rotate) { + ele.set_attribute( + names.rotate, + format!("{},{},{}", self.rotate.x, self.rotate.y, self.rotate.z), + ); + } + // spec vector + if self + .active_attributes + .contains(ActiveAttributes::specialization) + { + ele.set_attribute( + names.specialization, + self.specialization + .iter() + .copied() + .map(|s| s as u8) + .join(","), + ); + } + // bitflags arrays + set_attribute_bitflags_as_array_to_ele!( + self, + ele, + names, + [festival, map_type, mount, profession, race] + ); + // bools + set_attribute_bool_to_ele!( + self, + ele, + names, + [ + auto_trigger, + can_fade, + has_countdown, + in_game_visibility, + invert_behavior, + is_wall, + keep_on_map_edge, + map_visibility, + mini_map_visibility, + scale_on_map_with_zoom + ] + ); + // tostrings + set_attribute_to_ele!( + self, + ele, + names, + [ + icon_file, + texture, + trail_data, + achievement_id, + achievement_bit, + bounce, + copy, + hide, + info, + copy_message, + show, + text, + tip_name, + tip_description, + title, + toggle_category, + alpha, + anim_speed, + bounce_delay, + bounce_duration, + bounce_height, + fade_near, + fade_far, + height_offset, + icon_size, + info_range, + map_display_size, + map_fade_out_scale_level, + max_size, + min_size, + reset_length, + reset_offset, + rotate_x, + rotate_y, + rotate_z, + trail_scale, + trigger_range + ] + ); + } + /* + + TF32 height = 1.5f; + TF32 triggerRange = 2.0f; + TF32 animSpeed = 1; + TS32 miniMapSize = 20; + TF32 miniMapFadeOutLevel = 100.0f; + TF32 infoRange = 2.0f; + CColor color = CColor( 0xffffffff ); + + TS16 resetLength = 0; + TS16 minSize = 5; + TS16 maxSize = 2048; + + */ +} + +#[allow(non_camel_case_types)] +#[bitflags] +#[repr(u16)] +#[derive(Debug, Clone, Copy)] +pub enum BoolAttributes { + /// should the trigger activate when within trigger range + auto_trigger = 1, + can_fade = 1 << 1, + /// should we show the countdown timers for markers that are sleeping + has_countdown = 1 << 2, + /// whether the marker is drawn ingame + in_game_visibility = 1 << 3, + invert_behavior = 1 << 4, + is_wall = 1 << 5, + keep_on_map_edge = 1 << 6, + /// whether draw on map + map_visibility = 1 << 7, + /// draw on minimap + mini_map_visibility = 1 << 8, + /// scaling of marker on 2d map (or minimap) + scale_on_map_with_zoom = 1 << 9, +} +#[allow(non_camel_case_types)] +#[bitflags] +#[repr(u64)] +#[derive(Debug, Clone, Copy)] +pub enum ActiveAttributes { + achievement_id = 1, + achievement_bit = 1 << 1, + alpha = 1 << 2, + anim_speed = 1 << 3, + auto_trigger = 1 << 4, + behavior = 1 << 5, + bounce = 1 << 6, + bounce_delay = 1 << 7, + bounce_duration = 1 << 8, + bounce_height = 1 << 9, + can_fade = 1 << 10, + color = 1 << 11, + copy = 1 << 12, + copy_message = 1 << 13, + cull = 1 << 14, + fade_far = 1 << 15, + fade_near = 1 << 16, + festival = 1 << 17, + has_countdown = 1 << 18, + height_offset = 1 << 19, + hide = 1 << 20, + icon_file = 1 << 21, + icon_size = 1 << 22, + in_game_visibility = 1 << 23, + info = 1 << 24, + info_range = 1 << 25, + invert_behavior = 1 << 26, + is_wall = 1 << 27, + keep_on_map_edge = 1 << 28, + map_display_size = 1 << 29, + map_fade_out_scale_level = 1 << 30, + map_type = 1 << 31, + map_visibility = 1 << 32, + max_size = 1 << 33, + min_size = 1 << 34, + mini_map_visibility = 1 << 35, + mount = 1 << 36, + profession = 1 << 37, + race = 1 << 38, + reset_length = 1 << 39, + reset_offset = 1 << 40, + rotate = 1 << 41, + rotate_x = 1 << 42, + rotate_y = 1 << 43, + rotate_z = 1 << 44, + scale_on_map_with_zoom = 1 << 45, + show = 1 << 46, + specialization = 1 << 47, + text = 1 << 48, + texture = 1 << 49, + tip_name = 1 << 50, + tip_description = 1 << 51, + title = 1 << 52, + title_color = 1 << 53, + toggle_category = 1 << 54, + trail_data = 1 << 55, + trail_scale = 1 << 56, + trigger_range = 1 << 57, +} +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum Behavior { + #[default] + AlwaysVisible, + /// live. marker_id + ReappearOnMapChange, + /// store. marker_id + next reset timestamp + ReappearOnDailyReset, + /// store. marker_id + OnlyVisibleBeforeActivation, + /// store. marker_id + timestamp of when to wakeup + ReappearAfterTimer, + /// store. marker_id + timestamp of next reset of map + ReappearOnMapReset, + /// live. marker_id + instance ip / shard id + OncePerInstance, + /// store. marker_id + next reset. character data + DailyPerChar, + /// live. marker_id + instance_id + character_name + OncePerInstancePerChar, + /// I have no idea. + WvWObjective, + WeeklyReset = 101, +} +impl FromStr for Behavior { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "0" => Self::AlwaysVisible, + "1" => Self::ReappearOnMapChange, + "2" => Self::ReappearOnDailyReset, + "3" => Self::OnlyVisibleBeforeActivation, + "4" => Self::ReappearAfterTimer, + "5" => Self::ReappearOnMapReset, + "6" => Self::OncePerInstance, + "7" => Self::DailyPerChar, + "8" => Self::OncePerInstancePerChar, + "9" => Self::WvWObjective, + "101" => Self::WeeklyReset, + _ => return Err("invalid behavior value"), + }) + } +} +/// Filter which professions the marker should be active for. if its null, its available for all professions +#[bitflags] +#[repr(u16)] +#[derive(Debug, Clone, Copy)] +pub enum Profession { + Elementalist = 1 << 0, + Engineer = 1 << 1, + Guardian = 1 << 2, + Mesmer = 1 << 3, + Necromancer = 1 << 4, + Ranger = 1 << 5, + Revenant = 1 << 6, + Thief = 1 << 7, + Warrior = 1 << 8, +} +impl FromStr for Profession { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "guardian" => Profession::Guardian, + "warrior" => Profession::Warrior, + "engineer" => Profession::Engineer, + "ranger" => Profession::Ranger, + "thief" => Profession::Thief, + "elementalist" => Profession::Elementalist, + "mesmer" => Profession::Mesmer, + "necromancer" => Profession::Necromancer, + "revenant" => Profession::Revenant, + _ => return Err("invalid profession"), + }) + } +} +impl AsRef for Profession { + fn as_ref(&self) -> &str { + match self { + Profession::Guardian => "guardian", + Profession::Warrior => "warrior", + Profession::Engineer => "engineer", + Profession::Ranger => "ranger", + Profession::Thief => "thief", + Profession::Elementalist => "elementalist", + Profession::Mesmer => "mesmer", + Profession::Necromancer => "necromancer", + Profession::Revenant => "revenant", + } + } +} +impl ToString for Profession { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} +#[derive(Debug, Clone, Copy, Default)] +pub enum Cull { + #[default] + None, + ClockWise, + CounterClockWise, +} +impl FromStr for Cull { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "None" => Cull::None, + "Clockwise" => Cull::ClockWise, + "CounterClockwise" => Cull::CounterClockWise, + _ => { + return Err("invalid value for cull attribute"); + } + }) + } +} +impl AsRef for Cull { + fn as_ref(&self) -> &'static str { + match self { + Cull::None => "None", + Cull::ClockWise => "Clockwise", + Cull::CounterClockWise => "CounterClockwise", + } + } +} +impl ToString for Cull { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} +/// Filter for which festivals will the marker be active for +#[bitflags] +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +pub enum Festival { + DragonBash = 1 << 0, + #[allow(clippy::enum_variant_names)] + FestivalOfTheFourWinds = 1 << 1, + Halloween = 1 << 2, + LunarNewYear = 1 << 3, + SuperAdventureBox = 1 << 4, + Wintersday = 1 << 5, +} +impl FromStr for Festival { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "halloween" => Festival::Halloween, + "wintersday" => Festival::Wintersday, + "superadventurefestival" => Festival::SuperAdventureBox, + "lunarnewyear" => Festival::LunarNewYear, + "festivalofthefourwinds" => Festival::FestivalOfTheFourWinds, + "dragonbash" => Festival::DragonBash, + _ => return Err("unrecognized festival"), + }) + } +} +impl AsRef for Festival { + fn as_ref(&self) -> &'static str { + match self { + Festival::Halloween => "halloween", + Festival::Wintersday => "wintersday", + Festival::SuperAdventureBox => "superadventurefestival", + Festival::LunarNewYear => "lunarnewyear", + Festival::FestivalOfTheFourWinds => "festivalofthefourwinds", + Festival::DragonBash => "dragonbash", + } + } +} +impl ToString for Festival { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} +/// Filter for which specializations (the third traitline) will the marker be active for +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum Specialization { + Dueling = 0, + DeathMagic = 1, + Invocation = 2, + Strength = 3, + Druid = 4, + Explosives = 5, + Daredevil = 6, + Marksmanship = 7, + Retribution = 8, + Domination = 9, + Tactics = 10, + Salvation = 11, + Valor = 12, + Corruption = 13, + Devastation = 14, + Radiance = 15, + Water = 16, + Berserker = 17, + BloodMagic = 18, + ShadowArts = 19, + Tools = 20, + Defense = 21, + Inspiration = 22, + Illusions = 23, + NatureMagic = 24, + Earth = 25, + Dragonhunter = 26, + DeadlyArts = 27, + Alchemy = 28, + Skirmishing = 29, + Fire = 30, + BeastMastery = 31, + WildernessSurvival = 32, + Reaper = 33, + CriticalStrikes = 34, + Arms = 35, + Arcane = 36, + Firearms = 37, + Curses = 38, + Chronomancer = 39, + Air = 40, + Zeal = 41, + Scrapper = 42, + Trickery = 43, + Chaos = 44, + Virtues = 45, + Inventions = 46, + Tempest = 47, + Honor = 48, + SoulReaping = 49, + Discipline = 50, + Herald = 51, + Spite = 52, + Acrobatics = 53, + Soulbeast = 54, + Weaver = 55, + Holosmith = 56, + Deadeye = 57, + Mirage = 58, + Scourge = 59, + Spellbreaker = 60, + Firebrand = 61, + Renegade = 62, + Harbinger = 63, + Willbender = 64, + Virtuoso = 65, + Catalyst = 66, + Bladesworn = 67, + Vindicator = 68, + Mechanist = 69, + Specter = 70, + Untamed = 71, +} + +impl FromStr for Specialization { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "dueling" => Self::Dueling, + "deathmagic" => Self::DeathMagic, + "invocation" => Self::Invocation, + "strength" => Self::Strength, + "druid" => Self::Druid, + "explosives" => Self::Explosives, + "daredevil" => Self::Daredevil, + "marksmanship" => Self::Marksmanship, + "retribution" => Self::Retribution, + "domination" => Self::Domination, + "tactics" => Self::Tactics, + "salvation" => Self::Salvation, + "valor" => Self::Valor, + "corruption" => Self::Corruption, + "devastation" => Self::Devastation, + "radiance" => Self::Radiance, + "water" => Self::Water, + "berserker" => Self::Berserker, + "bloodmagic" => Self::BloodMagic, + "shadowarts" => Self::ShadowArts, + "tools" => Self::Tools, + "defense" => Self::Defense, + "inspiration" => Self::Inspiration, + "illusions" => Self::Illusions, + "naturemagic" => Self::NatureMagic, + "earth" => Self::Earth, + "dragonhunter" => Self::Dragonhunter, + "deadlyarts" => Self::DeadlyArts, + "alchemy" => Self::Alchemy, + "skirmishing" => Self::Skirmishing, + "fire" => Self::Fire, + "beastmastery" => Self::BeastMastery, + "wildernesssurvival" => Self::WildernessSurvival, + "reaper" => Self::Reaper, + "criticalstrikes" => Self::CriticalStrikes, + "arms" => Self::Arms, + "arcane" => Self::Arcane, + "firearms" => Self::Firearms, + "curses" => Self::Curses, + "chronomancer" => Self::Chronomancer, + "air" => Self::Air, + "zeal" => Self::Zeal, + "scrapper" => Self::Scrapper, + "trickery" => Self::Trickery, + "chaos" => Self::Chaos, + "virtues" => Self::Virtues, + "inventions" => Self::Inventions, + "tempest" => Self::Tempest, + "honor" => Self::Honor, + "soulreaping" => Self::SoulReaping, + "discipline" => Self::Discipline, + "herald" => Self::Herald, + "spite" => Self::Spite, + "acrobatics" => Self::Acrobatics, + "soulbeast" => Self::Soulbeast, + "weaver" => Self::Weaver, + "holosmith" => Self::Holosmith, + "deadeye" => Self::Deadeye, + "mirage" => Self::Mirage, + "scourge" => Self::Scourge, + "spellbreaker" => Self::Spellbreaker, + "firebrand" => Self::Firebrand, + "renegade" => Self::Renegade, + "harbinger" => Self::Harbinger, + "willbender" => Self::Willbender, + "virtuoso" => Self::Virtuoso, + "catalyst" => Self::Catalyst, + "bladesworn" => Self::Bladesworn, + "vindicator" => Self::Vindicator, + "mechanist" => Self::Mechanist, + "specter" => Self::Specter, + "untamed" => Self::Untamed, + _ => return Err("invalid specialization"), + }) + } +} +impl AsRef for Specialization { + fn as_ref(&self) -> &str { + match self { + Self::Dueling => "dueling", + Self::DeathMagic => "deathmagic", + Self::Invocation => "invocation", + Self::Strength => "strength", + Self::Druid => "druid", + Self::Explosives => "explosives", + Self::Daredevil => "daredevil", + Self::Marksmanship => "marksmanship", + Self::Retribution => "retribution", + Self::Domination => "domination", + Self::Tactics => "tactics", + Self::Salvation => "salvation", + Self::Valor => "valor", + Self::Corruption => "corruption", + Self::Devastation => "devastation", + Self::Radiance => "radiance", + Self::Water => "water", + Self::Berserker => "berserker", + Self::BloodMagic => "bloodmagic", + Self::ShadowArts => "shadowarts", + Self::Tools => "tools", + Self::Defense => "defense", + Self::Inspiration => "inspiration", + Self::Illusions => "illusions", + Self::NatureMagic => "naturemagic", + Self::Earth => "earth", + Self::Dragonhunter => "dragonhunter", + Self::DeadlyArts => "deadlyarts", + Self::Alchemy => "alchemy", + Self::Skirmishing => "skirmishing", + Self::Fire => "fire", + Self::BeastMastery => "beastmastery", + Self::WildernessSurvival => "wildernesssurvival", + Self::Reaper => "reaper", + Self::CriticalStrikes => "criticalstrikes", + Self::Arms => "arms", + Self::Arcane => "arcane", + Self::Firearms => "firearms", + Self::Curses => "curses", + Self::Chronomancer => "chronomancer", + Self::Air => "air", + Self::Zeal => "zeal", + Self::Scrapper => "scrapper", + Self::Trickery => "trickery", + Self::Chaos => "chaos", + Self::Virtues => "virtues", + Self::Inventions => "inventions", + Self::Tempest => "tempest", + Self::Honor => "honor", + Self::SoulReaping => "soulreaping", + Self::Discipline => "discipline", + Self::Herald => "herald", + Self::Spite => "spite", + Self::Acrobatics => "acrobatics", + Self::Soulbeast => "soulbeast", + Self::Weaver => "weaver", + Self::Holosmith => "holosmith", + Self::Deadeye => "deadeye", + Self::Mirage => "mirage", + Self::Scourge => "scourge", + Self::Spellbreaker => "spellbreaker", + Self::Firebrand => "firebrand", + Self::Renegade => "renegade", + Self::Harbinger => "harbinger", + Self::Willbender => "willbender", + Self::Virtuoso => "virtuoso", + Self::Catalyst => "catalyst", + Self::Bladesworn => "bladesworn", + Self::Vindicator => "vindicator", + Self::Mechanist => "mechanist", + Self::Specter => "specter", + Self::Untamed => "untamed", + } + } +} + +impl ToString for Specialization { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} +/// Most of this data is stolen from BlishHUD. +#[bitflags] +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum MapType { + Unknown = 1 << 0, + /// Redirect map type, e.g. when logging in while in a PvP match. + Redirect = 1 << 1, + /// Character create map type. + CharacterCreate = 1 << 2, + /// PvP map type. + PvP = 1 << 3, + /// GvG map type. Unused. + /// Quote from lye: "lol unused ;_;". + GvG = 1 << 4, + /// Instance map type, e.g. dungeons and story content. + Instance = 1 << 5, + /// Public map type, e.g. open world. + Public = 1 << 6, + /// Tournament map type. Probably unused. + Tournament = 1 << 7, + /// Tutorial map type. + Tutorial = 1 << 8, + /// User tournament map type. Probably unused. + UserTournament = 1 << 9, + /// Eternal Battlegrounds (WvW) map type. + EternalBattlegrounds = 1 << 10, + /// Blue Borderlands (WvW) map type. + BlueBorderlands = 1 << 11, + /// Green Borderlands (WvW) map type. + GreenBorderlands = 1 << 12, + /// Red Borderlands (WvW) map type. + RedBorderlands = 1 << 13, + /// Fortune's Vale. Unused. + FortunesVale = 1 << 14, + /// Obsidian Sanctum (WvW) map type. + ObsidianSanctum = 1 << 15, + /// Edge of the Mists (WvW) map type. + EdgeOfTheMists = 1 << 16, + /// Mini public map type, e.g. Dry Top, the Silverwastes and Mistlock Sanctuary. + PublicMini = 1 << 17, + /// WvW lounge map type, e.g. Armistice Bastion. + WvwLounge = 1 << 18, +} +impl FromStr for MapType { + type Err = &'static str; + fn from_str(_s: &str) -> Result { + unimplemented!("needs research to verify the map type values") + } +} +impl AsRef for MapType { + fn as_ref(&self) -> &str { + unimplemented!("needs research to verify the maptype values") + } +} +impl ToString for MapType { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} +/// made it using multi cursor (ctrl + shift + L) by copy-pasting json from api +#[allow(unused)] +pub static MAP_ID_TO_NAME: phf::OrderedMap = phf::phf_ordered_map! { + 15u16 => "Queensdale", + 17u16 => "Harathi Hinterlands", + 18u16 => "Divinity's Reach", + 19u16 => "Plains of Ashford", + 20u16 => "Blazeridge Steppes", + 21u16 => "Fields of Ruin", + 22u16 => "Fireheart Rise", + 23u16 => "Kessex Hills", + 24u16 => "Gendarran Fields", + 25u16 => "Iron Marches", + 26u16 => "Dredgehaunt Cliffs", + 27u16 => "Lornar's Pass", + 28u16 => "Wayfarer Foothills", + 29u16 => "Timberline Falls", + 30u16 => "Frostgorge Sound", + 31u16 => "Snowden Drifts", + 32u16 => "Diessa Plateau", + 33u16 => "Ascalonian Catacombs", + 34u16 => "Caledon Forest", + 35u16 => "Metrica Province", + 36u16 => "Ascalonian Catacombs", + 37u16 => "Arson at the Orphanage", + 38u16 => "Eternal Battlegrounds", + 39u16 => "Mount Maelstrom", + 50u16 => "Lion's Arch", + 51u16 => "Straits of Devastation", + 53u16 => "Sparkfly Fen", + 54u16 => "Brisban Wildlands", + 55u16 => "The Hospital in Jeopardy", + 61u16 => "Infiltration", + 62u16 => "Cursed Shore", + 63u16 => "Sorrow's Embrace", + 64u16 => "Sorrow's Embrace", + 65u16 => "Malchor's Leap", + 66u16 => "Citadel of Flame", + 67u16 => "Twilight Arbor", + 68u16 => "Twilight Arbor", + 69u16 => "Citadel of Flame", + 70u16 => "Honor of the Waves", + 71u16 => "Honor of the Waves", + 73u16 => "Bloodtide Coast", + 75u16 => "Caudecus's Manor", + 76u16 => "Caudecus's Manor", + 77u16 => "Search the Premises", + 79u16 => "The Informant", + 80u16 => "A Society Function", + 81u16 => "Crucible of Eternity", + 82u16 => "Crucible of Eternity", + 89u16 => "Chasing the Culprits", + 91u16 => "The Grove", + 92u16 => "The Trial of Julius Zamon", + 95u16 => " Alpine Borderlands", + 96u16 => " Alpine Borderlands", + 97u16 => "Infiltration", + 110u16 => "The Perils of Friendship", + 111u16 => "Victory or Death", + 112u16 => "The Ruined City of Arah", + 113u16 => "Desperate Medicine", + 120u16 => "The Commander", + 138u16 => "Defense of Shaemoor", + 139u16 => "Rata Sum", + 140u16 => "The Apothecary", + 142u16 => "Going Undercover", + 143u16 => "Going Undercover", + 144u16 => "The Greater Good", + 145u16 => "The Rescue", + 147u16 => "Breaking the Blade", + 148u16 => "The Fall of Falcon Company", + 149u16 => "The Fall of Falcon Company", + 152u16 => "Confronting Captain Tervelan", + 153u16 => "Seek Logan's Aid", + 154u16 => "Seek Logan's Aid", + 157u16 => "Accusation", + 159u16 => "Accusation", + 161u16 => "Liberation", + 162u16 => "Voices From the Past", + 163u16 => "Voices From the Past", + 171u16 => "Rending the Mantle", + 172u16 => "Rending the Mantle", + 178u16 => "The Floating Grizwhirl", + 179u16 => "The Floating Grizwhirl", + 180u16 => "The Floating Grizwhirl", + 182u16 => "Clown College", + 184u16 => "The Artist's Workshop", + 185u16 => "Into the Woods", + 186u16 => "The Ringmaster", + 190u16 => "The Orders of Tyria", + 191u16 => "The Orders of Tyria", + 192u16 => "Brute Force", + 193u16 => "Mortus Virge", + 195u16 => "Triskell Quay", + 196u16 => "Track the Seraph", + 198u16 => "Speaker of the Dead", + 199u16 => "The Sad Tale of the \"Ravenous\"", + 201u16 => "Kellach's Attack", + 202u16 => "The Queen's Justice", + 203u16 => "The Trap", + 211u16 => "Best Laid Plans", + 212u16 => "Welcome Home", + 215u16 => "The Tribune's Call", + 216u16 => "The Tribune's Call", + 217u16 => "The Tribune's Call", + 218u16 => "Black Citadel", + 222u16 => "A Spy for a Spy", + 224u16 => "Scrapyard Dogs", + 225u16 => "A Spy for a Spy", + 226u16 => "On the Mend", + 232u16 => "Spilled Blood", + 234u16 => "Ghostbore Musket", + 237u16 => "Iron Grip of the Legion", + 238u16 => "The Flame Advances", + 239u16 => "The Flame Advances", + 242u16 => "Test Your Metal", + 244u16 => "Quick and Quiet", + 248u16 => "Salma District (Home)", + 249u16 => "An Unusual Inheritance", + 250u16 => "Windrock Maze", + 251u16 => "Mired Deep", + 252u16 => "Mired Deep", + 254u16 => "Deadly Force", + 255u16 => "Ghostbore Artillery", + 256u16 => "No Negotiations", + 257u16 => "Salvaging Scrap", + 258u16 => "Salvaging Scrap", + 259u16 => "In the Ruins", + 260u16 => "In the Ruins", + 262u16 => "Chain of Command", + 263u16 => "Chain of Command", + 264u16 => "Time for a Promotion", + 267u16 => "The End of the Line", + 269u16 => "Magic Users", + 271u16 => "Rage Suppression", + 272u16 => "Rage Suppression", + 274u16 => "Operation: Bulwark", + 275u16 => "AWOL", + 276u16 => "Human's Lament", + 282u16 => "Misplaced Faith", + 283u16 => "Thicker Than Water", + 284u16 => "Dishonorable Discharge", + 287u16 => "Searching for the Truth", + 288u16 => "Lighting the Beacons", + 290u16 => "Stoking the Flame", + 294u16 => "A Fork in the Road", + 295u16 => "Sins of the Father", + 297u16 => "Graveyard Ornaments", + 326u16 => "Hoelbrak", + 327u16 => "Desperate Medicine", + 330u16 => "Seraph Headquarters", + 334u16 => "Keg Brawl", + 335u16 => "Claw Island", + 336u16 => "Chantry of Secrets", + 350u16 => "Heart of the Mists", + 363u16 => "The Sting", + 364u16 => "Drawing Out the Cult", + 365u16 => "Ashes of the Past", + 371u16 => "Hero's Canton (Home)", + 372u16 => "Blood Tribune Quarters", + 373u16 => "The Command Core", + 374u16 => "Knut Whitebear's Loft", + 375u16 => "Hunter's Hearth (Home)", + 376u16 => "Stonewright's Steading", + 378u16 => "Queen's Throne Room", + 379u16 => "The Great Hunt", + 380u16 => "A Weapon of Legend", + 381u16 => "The Last of the Giant-Kings", + 382u16 => "Disciples of the Dragon", + 385u16 => "A Weapon of Legend", + 386u16 => "Echoes of Ages Past", + 387u16 => "Wild Spirits", + 388u16 => "Out of the Skies", + 389u16 => "Echoes of Ages Past", + 390u16 => "Twilight of the Wolf", + 391u16 => "Rage of the Minotaurs", + 392u16 => "A Pup's Illness", + 393u16 => "Through the Veil", + 394u16 => "A Trap Foiled", + 396u16 => "Raven's Revered", + 397u16 => "One Good Drink Deserves Another", + 399u16 => "Shape of the Spirit", + 400u16 => "Into the Mists", + 401u16 => "Through the Veil", + 405u16 => "Blessed of Bear", + 407u16 => "The Wolf Havroun", + 410u16 => "Minotaur Rampant", + 411u16 => "Minotaur Rampant", + 412u16 => "Unexpected Visitors", + 413u16 => "Rumors of Trouble", + 414u16 => "A New Challenger", + 415u16 => "Unexpected Visitors", + 416u16 => "Roadblock", + 417u16 => "Assault on Moledavia", + 418u16 => "Don't Leave Your Toys Out", + 419u16 => "A New Challenger", + 420u16 => "First Attack", + 421u16 => "The Finishing Blow", + 422u16 => "The Semifinals", + 423u16 => "The Championship Fight", + 424u16 => "The Championship Fight", + 425u16 => "The Machine in Action", + 427u16 => "Among the Kodan", + 428u16 => "Rumors of Trouble", + 429u16 => "Rage of the Minotaurs", + 430u16 => "Darkness at Drakentelt", + 432u16 => "Fighting the Nightmare", + 434u16 => "Preserving the Balance", + 435u16 => "Means to an End", + 436u16 => "Dredge Technology", + 439u16 => "Underground Scholar", + 440u16 => "Dredge Assault", + 441u16 => "The Dredge Hideout", + 444u16 => "Sabotage", + 447u16 => "Codebreaker", + 449u16 => "Armaments", + 453u16 => "Assault the Hill", + 454u16 => "Silent Warfare", + 455u16 => "Sever the Head", + 458u16 => "Fury of the Dead", + 459u16 => "A Fork in the Road", + 460u16 => "Citadel Stockade", + 464u16 => "Tribunes in Effigy", + 465u16 => "Sins of the Father", + 466u16 => "Misplaced Faith", + 470u16 => "Graveyard Ornaments", + 471u16 => "Undead Infestation", + 474u16 => "Whispers in the Dark", + 476u16 => "Dangerous Research", + 477u16 => "Digging Up Answers", + 480u16 => "Defending the Keep", + 481u16 => "Undead Detection", + 483u16 => "Ever Vigilant", + 485u16 => "Research and Destroy", + 487u16 => "Whispers of Vengeance", + 488u16 => "Killer Instinct", + 489u16 => "Meeting my Mentor", + 490u16 => "A Fragile Peace", + 492u16 => "Don't Shoot the Messenger", + 496u16 => "Meeting my Mentor", + 497u16 => "Dredging Up the Past", + 498u16 => "Dredging Up the Past", + 499u16 => "Scrapyard Dogs", + 502u16 => "Quaestor's Siege", + 503u16 => "Minister's Defense", + 504u16 => "Called to Service", + 505u16 => "Called to Service", + 507u16 => "Mockery of Death", + 509u16 => "Discovering Darkness", + 511u16 => "Hounds and the Hunted", + 512u16 => "Hounds and the Hunted", + 513u16 => "Loved and Lost", + 514u16 => "Saving the Stag", + 515u16 => "Hidden in Darkness", + 516u16 => "Good Work Spoiled", + 517u16 => "Black Night, White Stag", + 518u16 => "The Omphalos Chamber", + 519u16 => "Weakness of the Heart", + 520u16 => "Awakening", + 521u16 => "Holding Back the Darkness", + 522u16 => "A Sly Trick", + 523u16 => "Deep Tangled Roots", + 524u16 => "The Heart of Nightmare", + 525u16 => "Beneath a Cold Moon", + 527u16 => "The Knight's Duel", + 528u16 => "Hammer and Steel", + 529u16 => "Where Life Goes", + 532u16 => "After the Storm", + 533u16 => "After the Storm", + 534u16 => "Beneath the Waves", + 535u16 => "Mirror, Mirror", + 536u16 => "A Vision of Darkness", + 537u16 => "Shattered Light", + 538u16 => "An Unknown Soul", + 539u16 => "An Unknown Soul", + 540u16 => "Where Life Goes", + 542u16 => "Source of the Issue", + 543u16 => "Wild Growth", + 544u16 => "Wild Growth", + 545u16 => "Seeking the Zalisco", + 546u16 => "The Direct Approach", + 547u16 => "Trading Trickery", + 548u16 => "Eye of the Sun", + 549u16 => "Battle of Kyhlo", + 552u16 => "Seeking the Zalisco", + 554u16 => "Forest of Niflhel", + 556u16 => "A Different Dream", + 557u16 => "A Splinter in the Flesh", + 558u16 => "Shadow of the Tree", + 559u16 => "Eye of the Sun", + 560u16 => "Sharpened Thorns", + 561u16 => "Bramble Walls", + 563u16 => "Secrets in the Earth", + 564u16 => "The Blossom of Youth", + 566u16 => "The Bad Apple", + 567u16 => "Trouble at the Roots", + 569u16 => "Flower of Death", + 570u16 => "Dead of Winter", + 571u16 => "A Tangle of Weeds", + 573u16 => "Explosive Intellect", + 574u16 => "In Snaff's Footsteps", + 575u16 => "Golem Positioning System", + 576u16 => "Monkey Wrench", + 577u16 => "Defusing the Problem", + 578u16 => "The Things We Do For Love", + 579u16 => "The Snaff Prize", + 581u16 => "A Sparkling Rescue", + 582u16 => "High Maintenance", + 583u16 => "Snaff Would Be Proud", + 584u16 => "Taking Credit Back", + 586u16 => "Political Homicide", + 587u16 => "Here, There, Everywhere", + 588u16 => "Piece Negotiations", + 589u16 => "Readings On the Rise", + 590u16 => "Snaff Would Be Proud", + 591u16 => "Readings On the Rise", + 592u16 => "Unscheduled Delay", + 594u16 => "Stand By Your Krewe", + 595u16 => "Unwelcome Visitors", + 596u16 => "Where Credit Is Due", + 597u16 => "Where Credit Is Due", + 598u16 => "Short Fuse", + 599u16 => "Short Fuse", + 606u16 => "Salt in the Wound", + 607u16 => "Free Rein", + 608u16 => "Serving Up Trouble", + 609u16 => "Serving Up Trouble", + 610u16 => "Flash Flood", + 611u16 => "I Smell a Rat", + 613u16 => "Magnum Opus", + 614u16 => "Magnum Opus", + 617u16 => "Bad Business", + 618u16 => "Beta Test", + 619u16 => "Beta Test", + 620u16 => "Any Sufficiently Advanced Science", + 621u16 => "Any Sufficiently Advanced Science", + 622u16 => "Bad Forecast", + 623u16 => "Industrial Espionage", + 624u16 => "Split Second", + 625u16 => "Carry a Big Stick", + 627u16 => "Meeting my Mentor", + 628u16 => "Stealing Secrets", + 629u16 => "A Bold New Theory", + 630u16 => "Forging Permission", + 631u16 => "Forging Permission", + 633u16 => "Setting the Stage", + 634u16 => "Containment", + 635u16 => "Containment", + 636u16 => "Hazardous Environment", + 638u16 => "Down the Hatch", + 639u16 => "Down the Hatch", + 642u16 => "The Stone Sheath", + 643u16 => "Bad Blood", + 644u16 => "Test Subject", + 645u16 => "Field Test", + 646u16 => "The House of Caithe", + 647u16 => "Dreamer's Terrace (Home)", + 648u16 => "The Omphalos Chamber", + 649u16 => "Snaff Memorial Lab", + 650u16 => "Applied Development Lab (Home)", + 651u16 => "Council Level", + 652u16 => "A Meeting of the Minds", + 653u16 => "Mightier than the Sword", + 654u16 => "They Went Thataway", + 655u16 => "Lines of Communication", + 656u16 => "Untamed Wilds", + 657u16 => "An Apple a Day", + 658u16 => "Base of Operations", + 659u16 => "The Lost Chieftain's Return", + 660u16 => "Thrown Off Guard", + 662u16 => "Pets and Walls Make Stronger Kraals", + 663u16 => "Doubt", + 664u16 => "The False God's Lair", + 666u16 => "Bad Ice", + 667u16 => "Bad Ice", + 668u16 => "Pets and Walls Make Stronger Kraals", + 669u16 => "Attempted Deicide", + 670u16 => "Doubt", + 672u16 => "Rat-Tastrophe", + 673u16 => "Salvation Through Heresy", + 674u16 => "Enraged and Unashamed", + 675u16 => "Pastkeeper", + 676u16 => "Protest Too Much", + 677u16 => "Prying the Eye Open", + 678u16 => "The Hatchery", + 680u16 => "Convincing the Faithful", + 681u16 => "Evacuation", + 682u16 => "Untamed Wilds", + 683u16 => "Champion's Sacrifice", + 684u16 => "Thieving from Thieves", + 685u16 => "Crusader's Return", + 686u16 => "Unholy Grounds", + 687u16 => "Chosen of the Sun", + 691u16 => "Set to Blow", + 692u16 => "Gadd's Last Gizmo", + 693u16 => "Library Science", + 694u16 => "Rakt and Ruin", + 695u16 => "Suspicious Activity", + 696u16 => "Reconnaissance", + 697u16 => "Critical Blowback", + 698u16 => "The Battle of Claw Island", + 699u16 => "Suspicious Activity", + 700u16 => "Priory Library", + 701u16 => "On Red Alert", + 702u16 => "Forearmed Is Forewarned", + 703u16 => "The Oratory", + 704u16 => "Killing Fields", + 705u16 => "The Ghost Rite", + 706u16 => "The Good Fight", + 707u16 => "Defense Contract", + 708u16 => "Shards of Orr", + 709u16 => "The Sound of Psi-Lance", + 710u16 => "Early Parole", + 711u16 => "Magic Sucks", + 712u16 => "A Light in the Darkness", + 713u16 => "The Priory Assailed", + 714u16 => "Under Siege", + 715u16 => "Retribution", + 716u16 => "Retribution", + 719u16 => "The Sound of Psi-Lance", + 726u16 => "Wet Work", + 727u16 => "Shell Shock", + 728u16 => "Volcanic Extraction", + 729u16 => "Munition Acquisition", + 730u16 => "To the Core", + 731u16 => "The Battle of Fort Trinity", + 732u16 => "Tower Down", + 733u16 => "Forging the Pact", + 735u16 => "Willing Captives", + 736u16 => "Marshaling the Truth", + 737u16 => "Breaking the Bone Ship", + 738u16 => "Liberating Apatia", + 739u16 => "Liberating Apatia", + 743u16 => "Fixing the Blame", + 744u16 => "A Sad Duty", + 745u16 => "Striking off the Chains", + 746u16 => "Delivering Justice", + 747u16 => "Intercepting the Orb", + 750u16 => "Close the Eye", + 751u16 => "Through the Looking Glass", + 758u16 => "The Cathedral of Silence", + 760u16 => "Starving the Beast", + 761u16 => "Stealing Light", + 762u16 => "Hunters and Prey", + 763u16 => "Romke's Final Voyage", + 764u16 => "Marching Orders", + 766u16 => "Air Drop", + 767u16 => "Estate of Decay", + 768u16 => "What the Eye Beholds", + 769u16 => "Conscript the Dead Ships", + 772u16 => "Ossuary of Unquiet Dead", + 775u16 => "Temple of the Forgotten God", + 776u16 => "Temple of the Forgotten God", + 777u16 => "Temple of the Forgotten God", + 778u16 => "Through the Looking Glass", + 779u16 => "Starving the Beast", + 780u16 => "Against the Corruption", + 781u16 => "The Source of Orr", + 782u16 => "Armor Guard", + 783u16 => "Blast from the Past", + 784u16 => "The Steel Tide", + 785u16 => "Further Into Orr", + 786u16 => "Ships of the Line", + 787u16 => "Source of Orr", + 788u16 => "Victory or Death", + 789u16 => "A Grisly Shipment", + 790u16 => "Blast from the Past", + 792u16 => "A Pup's Illness", + 793u16 => "Hunters and Prey", + 795u16 => "Legacy of the Foefire", + 796u16 => "The Informant", + 797u16 => "A Traitor's Testimony", + 799u16 => "Follow the Trail", + 806u16 => "Awakening", + 807u16 => "Eye of the North", + 820u16 => "The Omphalos Chamber", + 821u16 => "The Omphalos Chamber", + 825u16 => "Codebreaker", + 827u16 => "Caer Aval", + 828u16 => "The Durmand Priory", + 830u16 => "Vigil Headquarters", + 833u16 => "Ash Tribune Quarters", + 845u16 => "Shattered Light", + 862u16 => "Reaper's Rumble", + 863u16 => "Ascent to Madness", + 864u16 => "Lunatic Inquisition", + 865u16 => "Mad King's Clock Tower", + 866u16 => "Mad King's Labyrinth", + 872u16 => "Fractals of the Mists", + 873u16 => "Southsun Cove", + 875u16 => "Temple of the Silent Storm", + 877u16 => "Snowball Mayhem", + 878u16 => "Tixx's Infinirarium", + 880u16 => "Toypocalypse", + 881u16 => "Bell Choir Ensemble", + 882u16 => "Winter Wonderland", + 894u16 => "Spirit Watch", + 895u16 => "Super Adventure Box", + 896u16 => "North Nolan Hatchery", + 897u16 => "Cragstead", + 899u16 => "Obsidian Sanctum", + 900u16 => "Skyhammer", + 901u16 => "Molten Furnace", + 905u16 => "Crab Toss", + 911u16 => "Dragon Ball Arena", + 912u16 => "Ceremony and Acrimony—Memorials on the Pyre", + 913u16 => "Hard Boiled—The Scene of the Crime", + 914u16 => "The Dead End", + 915u16 => "Aetherblade Retreat", + 917u16 => "No More Secrets—The Scene of the Crime", + 918u16 => "Aspect Arena", + 919u16 => "Sanctum Sprint", + 920u16 => "Southsun Survival", + 922u16 => "Labyrinthine Cliffs", + 924u16 => "Grandmaster of Om", + 929u16 => "The Crown Pavilion", + 930u16 => "Opening Ceremony", + 931u16 => "Scarlet's Playhouse", + 932u16 => "Closing Ceremony", + 934u16 => "Super Adventure Box", + 935u16 => "Super Adventure Box", + 937u16 => "Scarlet's End", + 943u16 => "The Tower of Nightmares (Public)", + 945u16 => "The Nightmare Ends", + 947u16 => "Fractals of the Mists", + 948u16 => "Fractals of the Mists", + 949u16 => "Fractals of the Mists", + 950u16 => "Fractals of the Mists", + 951u16 => "Fractals of the Mists", + 952u16 => "Fractals of the Mists", + 953u16 => "Fractals of the Mists", + 954u16 => "Fractals of the Mists", + 955u16 => "Fractals of the Mists", + 956u16 => "Fractals of the Mists", + 957u16 => "Fractals of the Mists", + 958u16 => "Fractals of the Mists", + 959u16 => "Fractals of the Mists", + 960u16 => "Fractals of the Mists", + 964u16 => "Scarlet's Secret Lair", + 965u16 => "The Origins of Madness: A Moment's Peace", + 968u16 => "Edge of the Mists", + 971u16 => "The Dead End: A Study in Scarlet", + 973u16 => "The Evacuation of Lion's Arch", + 980u16 => "The Dead End: Celebration", + 984u16 => "Courtyard", + 987u16 => "Lion's Arch: Honored Guests", + 988u16 => "Dry Top", + 989u16 => "Prosperity's Mystery", + 990u16 => "Cornered", + 991u16 => "Disturbance in Brisban Wildlands", + 992u16 => "Fallen Hopes", + 993u16 => "Scarlet's Secret Room", + 994u16 => "The Concordia Incident", + 997u16 => "Discovering Scarlet's Breakthrough", + 998u16 => "The Machine", + 999u16 => "Trouble at Fort Salma", + 1000u16 => "The Waypoint Conundrum", + 1001u16 => "Summit Invitations", + 1002u16 => "Mission Accomplished", + 1003u16 => "Rallying Call", + 1004u16 => "Plan of Attack", + 1005u16 => "Party Politics", + 1006u16 => "Foefire Cleansing", + 1007u16 => "Recalibrating the Waypoints", + 1008u16 => "The Ghosts of Fort Salma", + 1009u16 => "Taimi's Device", + 1010u16 => "The World Summit", + 1011u16 => "Battle of Champion's Dusk", + 1015u16 => "The Silverwastes", + 1016u16 => "Hidden Arcana", + 1017u16 => "Reunion with the Pact", + 1018u16 => "Caithe's Reconnaissance Squad", + 1019u16 => "Fort Trinity", + 1021u16 => "Into the Labyrinth", + 1022u16 => "Return to Camp Resolve", + 1023u16 => "Tracking the Aspect Masters", + 1024u16 => "No Refuge", + 1025u16 => "The Newly Awakened", + 1026u16 => "Meeting the Asura", + 1027u16 => "Pact Assaulted", + 1028u16 => "The Mystery Cave", + 1029u16 => "Arcana Obscura", + 1032u16 => "Prized Possessions", + 1033u16 => "Buried Insight", + 1037u16 => "The Jungle Provides", + 1040u16 => "Hearts and Minds", + 1041u16 => "Dragon's Stand", + 1042u16 => "Verdant Brink", + 1043u16 => "Auric Basin", + 1045u16 => "Tangled Depths", + 1046u16 => "Roots of Terror", + 1048u16 => "City of Hope", + 1050u16 => "Torn from the Sky", + 1051u16 => "Prisoners of the Dragon", + 1052u16 => "Verdant Brink", + 1054u16 => "Bitter Harvest", + 1057u16 => "Strange Observations", + 1058u16 => "Prologue: Rally to Maguuma", + 1062u16 => "Spirit Vale", + 1063u16 => "Southsun Crab Toss", + 1064u16 => "Claiming the Lost Precipice", + 1065u16 => "Angvar's Trove", + 1066u16 => "Claiming the Gilded Hollow", + 1067u16 => "Angvar's Trove", + 1068u16 => "Gilded Hollow", + 1069u16 => "Lost Precipice", + 1070u16 => "Claiming the Lost Precipice", + 1071u16 => "Lost Precipice", + 1072u16 => "Southsun Crab Toss", + 1073u16 => "Guild Initiative Office", + 1074u16 => "Blightwater Shatterstrike", + 1075u16 => "Proxemics Lab", + 1076u16 => "Lost Precipice", + 1078u16 => "Claiming the Gilded Hollow", + 1079u16 => "Deep Trouble", + 1080u16 => "Branded for Termination", + 1081u16 => "Langmar Estate", + 1082u16 => "Langmar Estate", + 1083u16 => "Deep Trouble", + 1084u16 => "Southsun Crab Toss", + 1086u16 => "Save Our Supplies", + 1087u16 => "Proxemics Lab", + 1088u16 => "Claiming the Gilded Hollow", + 1089u16 => "Angvar's Trove", + 1090u16 => "Langmar Estate", + 1091u16 => "Save Our Supplies", + 1092u16 => "Scratch Sentry Defense", + 1093u16 => "Angvar's Trove", + 1094u16 => "Save Our Supplies", + 1095u16 => "Dragon's Stand (Heart of Thorns)", + 1097u16 => "Proxemics Lab", + 1098u16 => "Claiming the Gilded Hollow", + 1099u16 => " Desert Borderlands", + 1100u16 => "Scratch Sentry Defense", + 1101u16 => "Gilded Hollow", + 1104u16 => "Lost Precipice", + 1105u16 => "Langmar Estate", + 1106u16 => "Deep Trouble", + 1107u16 => "Gilded Hollow", + 1108u16 => "Gilded Hollow", + 1109u16 => "Angvar's Trove", + 1110u16 => "Scrap Rifle Field Test", + 1111u16 => "Scratch Sentry Defense", + 1112u16 => "Branded for Termination", + 1113u16 => "Scratch Sentry Defense", + 1115u16 => "Haywire Punch-o-Matic Battle", + 1116u16 => "Deep Trouble", + 1117u16 => "Claiming the Lost Precipice", + 1118u16 => "Save Our Supplies", + 1121u16 => "Gilded Hollow", + 1122u16 => "Claiming the Gilded Hollow", + 1123u16 => "Blightwater Shatterstrike", + 1124u16 => "Lost Precipice", + 1126u16 => "Southsun Crab Toss", + 1128u16 => "Scratch Sentry Defense", + 1129u16 => "Langmar Estate", + 1130u16 => "Deep Trouble", + 1131u16 => "Blightwater Shatterstrike", + 1132u16 => "Claiming the Lost Precipice", + 1133u16 => "Branded for Termination", + 1134u16 => "Blightwater Shatterstrike", + 1135u16 => "Branded for Termination", + 1136u16 => "Proxemics Lab", + 1137u16 => "Proxemics Lab", + 1138u16 => "Save Our Supplies", + 1139u16 => "Southsun Crab Toss", + 1140u16 => "Claiming the Lost Precipice", + 1142u16 => "Blightwater Shatterstrike", + 1146u16 => "Branded for Termination", + 1147u16 => "Spirit Vale", + 1149u16 => "Salvation Pass", + 1153u16 => "Tiger Den", + 1154u16 => "Special Forces Training Area", + 1155u16 => "Lion's Arch Aerodrome", + 1156u16 => "Stronghold of the Faithful", + 1158u16 => "Noble's Folly", + 1159u16 => "Research in Rata Novus", + 1161u16 => "Eir's Homestead", + 1163u16 => "Revenge of the Capricorn", + 1164u16 => "Fractals of the Mists", + 1165u16 => "Bloodstone Fen", + 1166u16 => "Confessor's Stronghold", + 1167u16 => "A Shadow's Deeds", + 1169u16 => "Rata Novus", + 1170u16 => "Taimi's Game", + 1171u16 => "Eternal Coliseum", + 1172u16 => "Dragon Vigil", + 1173u16 => "Taimi's Game", + 1175u16 => "Ember Bay", + 1176u16 => "Taimi's Game", + 1177u16 => "Fractals of the Mists", + 1178u16 => "Bitterfrost Frontier", + 1180u16 => "The Bitter Cold", + 1181u16 => "Frozen Out", + 1182u16 => "Precocious Aurene", + 1185u16 => "Lake Doric", + 1188u16 => "Bastion of the Penitent", + 1189u16 => "Regrouping with the Queen", + 1190u16 => "A Meeting of Ministers", + 1191u16 => "Confessor's End", + 1192u16 => "The Second Vision", + 1193u16 => "The First Vision", + 1194u16 => "The Sword Regrown", + 1195u16 => "Draconis Mons", + 1196u16 => "Heart of the Volcano", + 1198u16 => "Taimi's Pet Project", + 1200u16 => "Hall of the Mists", + 1201u16 => "Asura Arena", + 1202u16 => "White Mantle Hideout", + 1203u16 => "Siren's Landing", + 1204u16 => "Palace Temple", + 1205u16 => "Fractals of the Mists", + 1206u16 => "Mistlock Sanctuary", + 1207u16 => "The Last Chance", + 1208u16 => "Shining Blade Headquarters", + 1209u16 => "The Sacrifice", + 1210u16 => "Crystal Oasis", + 1211u16 => "Desert Highlands", + 1212u16 => "Office of the Chief Councilor", + 1214u16 => "Windswept Haven", + 1215u16 => "Windswept Haven", + 1217u16 => "Sparking the Flame", + 1219u16 => "Enemy of My Enemy: The Beastmarshal", + 1220u16 => "Sparking the Flame (Prologue)", + 1221u16 => "The Way Forward", + 1222u16 => "Claiming Windswept Haven", + 1223u16 => "Small Victory (Epilogue)", + 1224u16 => "Windswept Haven", + 1226u16 => "The Desolation", + 1227u16 => "Hallowed Ground: Tomb of Primeval Kings", + 1228u16 => "Elon Riverlands", + 1230u16 => "Facing the Truth: The Sanctum", + 1231u16 => "Claiming Windswept Haven", + 1232u16 => "Windswept Haven", + 1234u16 => "To Kill a God", + 1236u16 => "Claiming Windswept Haven", + 1240u16 => "Blazing a Trail", + 1241u16 => "Night of Fires", + 1242u16 => "Zalambur's Office", + 1243u16 => "Windswept Haven", + 1244u16 => "Claiming Windswept Haven", + 1245u16 => "The Departing", + 1246u16 => "Captain Kiel's Office", + 1247u16 => "Enemy of My Enemy", + 1248u16 => "Domain of Vabbi", + 1250u16 => "Windswept Haven", + 1252u16 => "Crystalline Memories", + 1253u16 => "Beast of War", + 1255u16 => "Enemy of My Enemy: The Troopmarshal", + 1256u16 => "The Dark Library", + 1257u16 => "Spearmarshal's Lament", + 1260u16 => "Eye of the Brandstorm", + 1263u16 => "Domain of Istan", + 1264u16 => "Hall of Chains", + 1265u16 => "The Hero of Istan", + 1266u16 => "Cave of the Sunspear Champion", + 1267u16 => "Fractals of the Mists", + 1268u16 => "Fahranur, the First City", + 1270u16 => "Toypocalypse", + 1271u16 => "Sandswept Isles", + 1274u16 => "The Charge", + 1275u16 => "Courtyard", + 1276u16 => "The Test Subject", + 1277u16 => "The Charge", + 1278u16 => "???", + 1279u16 => "ERROR: SIGNAL LOST", + 1281u16 => "A Kindness Repaid", + 1282u16 => "Tracking the Scientist", + 1283u16 => "???", + 1285u16 => "???", + 1288u16 => "Domain of Kourna", + 1289u16 => "Seized", + 1290u16 => "Fractals of the Mists", + 1291u16 => "Forearmed Is Forewarned", + 1292u16 => "Be My Guest", + 1294u16 => "Sun's Refuge", + 1295u16 => "Legacy", + 1296u16 => "Storm Tracking", + 1297u16 => "A Shattered Nation", + 1299u16 => "Storm Tracking", + 1300u16 => "From the Ashes—The Deadeye", + 1301u16 => "Jahai Bluffs", + 1302u16 => "Storm Tracking", + 1303u16 => "Mythwright Gambit", + 1304u16 => "Mad King's Raceway", + 1305u16 => "Djinn's Dominion", + 1306u16 => "Secret Lair of the Snowmen (Squad)", + 1308u16 => "Scion & Champion", + 1309u16 => "Fractals of the Mists", + 1310u16 => "Thunderhead Peaks", + 1313u16 => "The Crystal Dragon", + 1314u16 => "The Crystal Blooms", + 1315u16 => "Armistice Bastion", + 1316u16 => "Mists Rift", + 1317u16 => "Dragonfall", + 1318u16 => "Dragonfall", + 1319u16 => "Descent", + 1320u16 => "The End", + 1321u16 => "Dragonflight", + 1322u16 => "Epilogue", + 1323u16 => "The Key of Ahdashim", + 1326u16 => "Dragon Bash Arena", + 1327u16 => "Dragon Arena Survival", + 1328u16 => "Auric Span", + 1329u16 => "Coming Home", + 1330u16 => "Grothmar Valley", + 1331u16 => "Strike Mission: Shiverpeaks Pass (Public)", + 1332u16 => "Strike Mission: Shiverpeaks Pass (Squad)", + 1334u16 => "Deeper and Deeper", + 1336u16 => "A Race to Arms", + 1338u16 => "Bad Blood", + 1339u16 => "Weekly Strike Mission: Boneskinner (Squad)", + 1340u16 => "Weekly Strike Mission: Voice of the Fallen and Claw of the Fallen (Public)", + 1341u16 => "Weekly Strike Mission: Fraenir of Jormag (Squad)", + 1342u16 => "The Invitation", + 1343u16 => "Bjora Marches", + 1344u16 => "Weekly Strike Mission: Fraenir of Jormag (Public)", + 1345u16 => "What's Left Behind", + 1346u16 => "Weekly Strike Mission: Voice of the Fallen and Claw of the Fallen (Squad)", + 1349u16 => "Silence", + 1351u16 => "Weekly Strike Mission: Boneskinner (Public)", + 1352u16 => "Secret Lair of the Snowmen (Public)", + 1353u16 => "Celestial Challenge", + 1355u16 => "Voice in the Deep", + 1356u16 => "Chasing Ghosts", + 1357u16 => "Strike Mission: Whisper of Jormag (Public)", + 1358u16 => "Eye of the North", + 1359u16 => "Strike Mission: Whisper of Jormag (Squad)", + 1361u16 => "The Nightmare Incarnate", + 1362u16 => "Forging Steel (Public)", + 1363u16 => "New Friends, New Enemies—North Nolan Hatchery", + 1364u16 => "The Battle for Cragstead", + 1366u16 => "Darkrime Delves", + 1368u16 => "Forging Steel (Squad)", + 1369u16 => "Canach's Lair", + 1370u16 => "Eye of the North", + 1371u16 => "Drizzlewood Coast", + 1372u16 => "Turnabout", + 1373u16 => "Pointed Parley", + 1374u16 => "Strike Mission: Cold War (Squad)", + 1375u16 => "Snapping Steel", + 1376u16 => "Strike Mission: Cold War (Public)", + 1378u16 => "Behind Enemy Lines", + 1379u16 => "One Charr, One Dragon, One Champion", + 1380u16 => "Epilogue", + 1382u16 => "Arena of the Wolverine", + 1383u16 => "A Simple Negotiation", + 1384u16 => "Fractals of the Mists", + 1385u16 => "Caledon Forest (Private)", + 1386u16 => "Thunderhead Peaks (Private)", + 1387u16 => "Bloodtide Coast (Public)", + 1388u16 => "Snowden Drifts (Private)", + 1389u16 => "Snowden Drifts (Public)", + 1390u16 => "Fireheart Rise (Public)", + 1391u16 => "Brisban Wildlands (Private)", + 1392u16 => "Primordus Rising", + 1393u16 => "Lake Doric (Public)", + 1394u16 => "Bloodtide Coast (Private)", + 1395u16 => "Thunderhead Peaks (Public)", + 1396u16 => "Gendarran Fields (Public)", + 1397u16 => "Metrica Province (Public)", + 1398u16 => "Fields of Ruin (Public)", + 1399u16 => "Brisban Wildlands (Public)", + 1400u16 => "Fields of Ruin (Private)", + 1401u16 => "Metrica Province (Private)", + 1402u16 => "Lake Doric (Private)", + 1403u16 => "Caledon Forest (Public)", + 1404u16 => "Fireheart Rise (Private)", + 1405u16 => "Gendarran Fields (Private)", + 1407u16 => "Council Level", + 1408u16 => "Wildfire", + 1409u16 => "Dragonstorm (Private Squad)", + 1410u16 => "Champion's End", + 1411u16 => "Dragonstorm (Public)", + 1412u16 => "Dragonstorm", + 1413u16 => "The Twisted Marionette (Public)", + 1414u16 => "The Twisted Marionette (Private Squad)", + 1415u16 => "The Future in Jade: Power Plant", + 1416u16 => "Deepest Secrets: Yong Reactor", + 1419u16 => "Isle of Reflection", + 1420u16 => "Fallout: Nika's Blade", + 1421u16 => "???", + 1422u16 => "Dragon's End", + 1426u16 => "Isle of Reflection", + 1427u16 => "Weight of the World: Lady Joon's Estate", + 1428u16 => "Arborstone", + 1429u16 => "The Cycle, Reborn: Arborstone", + 1430u16 => "Claiming the Isle of Reflection", + 1432u16 => "Strike Mission: Aetherblade Hideout", + 1433u16 => "Old Friends", + 1434u16 => "Empty", + 1435u16 => "Isle of Reflection", + 1436u16 => "Extraction Point: Command Quarters", + 1437u16 => "Strike Mission: Harvest Temple", + 1438u16 => "New Kaineng City", + 1439u16 => "The Only One", + 1440u16 => "Laying to Rest", + 1442u16 => "Seitung Province", + 1444u16 => "Isle of Reflection", + 1445u16 => "The Future in Jade: Nahpui Lab", + 1446u16 => "Aetherblade Armada", + 1448u16 => "The Cycle, Reborn: The Dead End Bar", + 1449u16 => "Aurene's Sanctuary", + 1450u16 => "Strike Mission: Xunlai Jade Junkyard", + 1451u16 => "Strike Mission: Kaineng Overlook", + 1452u16 => "The Echovald Wilds", + 1453u16 => "Ministry of Security: Main Office", + 1454u16 => "The Scenic Route: Kaineng Docks", + 1456u16 => "Claiming the Isle of Reflection", + 1457u16 => "Detention Facility", + 1458u16 => "Aurene's Sanctuary", + 1459u16 => "Claiming the Isle of Reflection", + 1460u16 => "Empress Ihn's Court", + 1461u16 => "Zen Daijun Hideaway", + 1462u16 => "Isle of Reflection", + 1463u16 => "Claiming the Isle of Reflection", + 1464u16 => "Fallout: Arborstone", + 1465u16 => "Thousand Seas Pavilion", + 1466u16 => "A Quiet Celebration—Knut Whitebear's Loft", + 1467u16 => "New Friends, New Enemies—The Command Core", + 1468u16 => "The Battle for Cragstead—Knut Whitebear's Loft", + 1469u16 => "New Friends, New Enemies—Blood Tribune Quarters", + 1470u16 => "A Quiet Celebration—Citadel Stockade", + 1471u16 => "Case Closed—The Dead End", + 1472u16 => "Hard Boiled—The Dead End", + 1474u16 => "Picking Up the Pieces", + 1477u16 => "The Tower of Nightmares (Private Squad)", + 1478u16 => "The Battle for Lion's Arch (Private Squad)", + 1480u16 => "The Twisted Marionette", + 1481u16 => "Battle on the Breachmaker", + 1482u16 => "The Battle For Lion's Arch (Public)", + 1483u16 => "Memory of Old Lion's Arch", + 1484u16 => "North Evacuation Camp", + 1485u16 => "Strike Mission: Old Lion's Court", + 1487u16 => "The Aether Escape", + 1488u16 => "On the Case: Excavation Yard", + 1489u16 => "A Raw Deal: Red Duck Tea House", + 1490u16 => "Gyala Delve", + 1491u16 => "Deep Trouble: Excavation Yard", + 1492u16 => "Deep Trouble: The Deep", + 1494u16 => "Entrapment: The Deep", + 1495u16 => "A Plan Emerges: Power Plant", + 1496u16 => "Emotional Release: Jade Pools", + 1497u16 => "Emotional Release: Command Quarters", + 1498u16 => "Full Circle: Red Duck Tea House", + 1499u16 => "Forward", + 1500u16 => "Fractals of the Mists", +}; diff --git a/crates/joko_package/src/pack/marker.png b/crates/joko_package/src/pack/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..294a322e8475b221dbee3297868b719baa40f656 GIT binary patch literal 173015 zcmd3MRZ|>H(r6+1RAkYRiIAb7pwQ&yq|~9Hp#Oo;P>AsVI(?T4%l|M}by*3hnn{wQe+ZnF zxUx7D)UO1TR};WL9LY)Uiz^fqdf$HyddRWD913a;RbEP5)64MGAEAI$`tBk1TT8~m z%b4OtT4?^X;$bJfax@T2N>fVgJ(wObvgnpo?tDkpx^mLrnn9UnS(Tudl9{8_uYVL5_sBbkAk@AFs8=&JorjlDM6&YF1sXM@(R9 zHLd?gL=5C?4A3j7Y`r%M+lh_={%PxMeNOAqS~X~Mby9M6$J{Xa-(0BV0bxwrtJ}wc zXlW~R%4WJ_Eq;j?%nr|^oBTUysgx18ItW*!g9R{;=_Y5t?b#w1RqCBL6%_>tjqBh-is^Ny5=LW-dXOZ_XdBmKCn7Gx71DHmWCL_e8AfOK{h^y)_ULmzVR6``jIn?Pc|c#840(=pE9)SbCX}H z0#HXZq*5a(x~(;i|8TyB#-MXx;sXy+yq0pZt3LTzX!v5fEmoPi27UX3y4ePh=-%Ld zSnp^>cv!U%8-~a@{9@w#=b*?`Lb`3UpYh+u41S!#Onwno&)dM|eOrM*UxjB4B8DN|F0_Vb!lkBh;{x^ez-*kz!&3S5m#+KpLx%@a=iDtx5PrSZGB9K==ZC$UHG z?>Aum21QL$6RmytY0lyv*E0~gbHcmarSXEI^*kPyc)T0P8_2>PoeT?BtkZOL2yp)M zq4PH(p+DGtFzX8b+sEN_m8Zr8Lv5>F4&?Jss86P7%it01gR_4J0$-^2;T9JCcE>JL z6+bvX(dcljAKJP~w*pXLY`O{N=`tE{?IO~`ZD`%Zt%)(yq@aZ}4DsKNV>e;5nDz2p ziwKYc25da@;vAGNPtbg)d&SPW*XKpNd6tVhUnZNx#H}vXl3|^(SsIW8O|%|{CKswv zFQqG^Z7{Iwqg;pBmcy;v8<{%LlU-|p6(d3~{t|YV)zWMjt=@kCl)21yikhDH^dmJ2 zG{4*Vi@L5f)4pQ2K6Xr)4v$Y?@KfpVOexdEtfFd5y&FR88Gl1uMQ&bY== zbb+m@#F6|(mj*8`LtktUitLRBDi-N|j=SF#|U~y8WhV43yweF@i`p<+TyV z*i9o$W`v?Ur9|J>_0Ypqq8nG&`VJ=oR8qbdsFX z8c}0!nCjKuO2{*A&t7D4{ODEu`FA@5H$|nN^FoH{c#>ITQ*DzQ&B_nY@N+zmxq{(c zfq||35Lskmw7B^v3^Z+`KI%OmkvSVrT-6uUGL^h4OXuTX`l3%>#%BAD|1=nv`wW*J zW9aV_sN%C4Au={=I4BLwC;+Hl)#**cKEFX`&YhadA3$9PS{%$?8layIlsyxLa z)48Y3E)S~cSrZeQSBG&C@$uOgjq?ms5IAuqFO0`x>GGIr@BSwz1;i^t9L$ZTwU+7z^2St z4zyMnYTl{9Z_o{0N9DH~2oDKR0+~!3>@n1hnt>weX?0nLeT9 zWdM0UWI6m~{NRNymU<;+DOsNZ+d82?mIc$P_cV9=i!@Q9JJac|>ADdSUiO7WV?^u6lmOD?{$`7&sg@YS$^5Ob`5E zk~t&XSfAMO#O?m0R?*&9rQLyd$!d+;n*V8x^r$8+)G+kW@e z$-eL+Ufl#!P+4BEprH3va?;4aLXC6|ar-+C0apXF9I9{NiWWu-=eFD=sC<9N6)@!Z z7-G&sFp*@LuA&wB2!++ykY*uBXd*<;c;?zWlbtj=h@G&otFiu~ZJ4*wQtx;(y;jeW zBp+IMvk5Dr_*z0*O+|hgMfr~WEyQ8JBiZKeTTW784>s65<$F#DwQ%BBXhZT>d0;4H zKrGke>BoxXpt|TILFt?{a5MT8OlBm~!poHK-Q8nvtCID$!VK9wbeJ-R z?AWi$sW1?>_@3b>O!%#2)g@P*BqNFVEJ?9wG+(#F8+DZCRh{@N%0DmP2;sy3QD_xa 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%0D>~L^060CsHx=NV^<)QR_ycX_mn5bPDsp8-9 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�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, + pub routes: IndexMap, + pub trails: IndexMap, +} + +#[derive(Debug, Clone)] +pub(crate) struct RawCategory { + pub guid: Uuid, + pub parent_name: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, +} + +#[derive(Debug, Clone)] +pub(crate) struct Category { + pub guid: Uuid, + pub parent: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, + pub children: IndexMap, +} + +#[derive(Debug, Clone)] +pub struct PackCore { + /* + PackCore is a temporary holder of data + It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. + */ + pub uuid: Uuid, + pub textures: HashMap>, + pub(crate) tbins: HashMap, + pub(crate) categories: IndexMap, + pub all_categories: HashMap, + pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + pub entities_parents: HashMap, + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, +} + + +fn route_to_tbin(route: &Route) -> TBin { + assert!( route.path.len() > 1); + TBin { + map_id: route.map_id, + version: 0, + nodes: route.path.clone(), + } +} + +fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { + let mut props = CommonAttributes::default(); + props.set_texture(None); + props.set_trail_data(Some(file_path.clone())); + Trail { + map_id: route.map_id, + category: route.category.clone(), + parent: route.parent.clone(), + guid: route.guid, + props: props, + dynamic: true, + source_file_name: route.source_file_name.clone(), + } +} + +impl PackCore { + + pub fn new() -> Self { + let mut res = Self { + all_categories: Default::default(), + categories: Default::default(), + entities_parents: Default::default(), + late_discovery_categories: Default::default(), + maps: Default::default(), + source_files: Default::default(), + tbins: Default::default(), + textures: Default::default(), + uuid: Default::default(), + }; + res.uuid = Uuid::new_v4(); + res + } + pub fn partial(all_categories: &HashMap) -> Self { + // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. + let mut res: Self = Self::new(); + res.all_categories = all_categories.clone(); + res + } + + pub fn merge_partial(&mut self, partial_pack: PackCore) { + self.maps.extend(partial_pack.maps); + self.all_categories = partial_pack.all_categories; + self.late_discovery_categories.extend(partial_pack.late_discovery_categories); + self.source_files.extend(partial_pack.source_files); + self.tbins.extend(partial_pack.tbins); + self.entities_parents.extend(partial_pack.entities_parents); + } + pub fn category_exists(&self, full_category_name: &String) -> bool { + self.all_categories.contains_key(full_category_name) + } + + pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { + self.all_categories.get(full_category_name) + } + + pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { + if let Some(category_uuid) = self.all_categories.get(full_category_name) { + category_uuid.clone() + } else { + //TODO: if import is "dirty", create missing category + //TODO: default import mode is "strict" (get inspiration from HTML modes) + debug!("There is no defined category for {}", full_category_name); + + let mut n = 0; + let mut last_uuid: Option = None; + while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { + n += 1; + if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { + //FIXME: might want to make the difference between impacted parents and actual missing category + self.late_discovery_categories.insert(*parent_uuid); + last_uuid = Some(*parent_uuid); + } else { + let new_uuid = Uuid::new_v4(); + debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); + self.all_categories.insert(parent_full_category_name.clone(), new_uuid); + self.late_discovery_categories.insert(new_uuid); + last_uuid = Some(new_uuid); + } + } + trace!("{} uuid: {:?}", full_category_name, last_uuid); + assert!(last_uuid.is_some()); + last_uuid.unwrap() + } + } + + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ + if let Some(parent_uuid) = self.all_categories.get(full_category_name) { + let mut uuid_to_insert = uuid.clone(); + while self.entities_parents.contains_key(&uuid_to_insert) { + trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); + uuid_to_insert = Uuid::new_v4(); + } + self.entities_parents.insert(uuid_to_insert, *parent_uuid); + Ok(uuid_to_insert) + } else { + //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. + Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) + } + } + + pub(crate) fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; + marker.guid = uuid_to_insert; + if !self.maps.contains_key(&marker.map_id) { + self.maps.insert(marker.map_id, MapData::default()); + } + self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); + Ok(()) + } + + pub(crate) fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; + trail.guid = uuid_to_insert; + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + Ok(()) + } + + pub(crate) fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { + let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); + let tbin_path: RelativePath = file_name.parse().unwrap(); + let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; + route.guid = uuid_to_insert; + let trail = route_to_trail(&route, &tbin_path); + let tbin = route_to_tbin(&route); + + self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); + Ok(()) + } + + pub fn register_categories(&mut self) { + let mut entities_parents: HashMap = Default::default(); + let mut all_categories: HashMap = Default::default(); + Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); + self.entities_parents.extend(entities_parents); + self.all_categories = all_categories; + } + fn recursive_register_categories( + entities_parents: &mut HashMap, + categories: &IndexMap, + all_categories: &mut HashMap, + ) { + for (_, cat) in categories.iter() { + debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); + all_categories.insert(cat.full_category_name.clone(), cat.guid); + if let Some(parent) = cat.parent { + entities_parents.insert(cat.guid, parent); + } + Self::recursive_register_categories(entities_parents, &cat.children, all_categories); + } + } +} + + +pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { + let res = s.match_indices(pat) + .nth(n) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_until_nth_char {} {} {:?}", s, n, res); + res +} + +pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { + let nb_matches = s.matches(pat).count(); + assert!(nb_matches + 1 > n); + let res = s.split(pat) + .nth(n) + ; + debug!("nth_chunk {} {} {:?}", s, n, res); + res.unwrap().to_string() +} + +pub fn prefix_parent(s: &str, pat: char) -> Option { + let n = s.matches(pat).count(); + assert!(n > 0); + let res = s.match_indices(pat) + .nth(n - 1) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_parent {} {} {:?}", s, n, res); + res +} + +impl Category { + // Required method + pub fn from(value: &RawCategory, parent: Option) -> Self { + Self { + guid: value.guid.clone(), + props: value.props.clone(), + separator: value.separator, + default_enabled: value.default_enabled, + display_name: value.display_name.clone(), + relative_category_name: value.relative_category_name.clone(), + full_category_name: value.full_category_name.clone(), + parent: parent, + children: Default::default() + } + } + pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + for (_, cat) in categories { + if &cat.guid == uuid { + return Some(cat); + } + let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); + if sub_res.is_some() { + return sub_res; + } + } + return None; + } + pub fn reassemble( + input_first_pass_categories: &OrderedHashMap, + late_discovered_categories: &mut HashSet, + ) -> IndexMap { + let mut first_pass_categories = input_first_pass_categories.clone(); + let mut second_pass_categories: OrderedHashMap = Default::default(); + let mut need_a_pass: bool = true; + + let mut third_pass_categories: IndexMap = Default::default(); + let mut third_pass_categories_ref: Vec = Default::default(); + let mut root: IndexMap = Default::default(); + while need_a_pass { + need_a_pass = false; + for (key, value) in first_pass_categories.iter() { + debug!("reassemble_categories {:?}", value); + let mut to_insert = value.clone(); + if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { + let mut n = 0; + let mut last_name: Option = None; + // This is an almost duplication of code of pack/mod.rs + while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { + debug!("{} {}", parent_name, n); + if let Some(parent_category) = first_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + }else{ + let new_uuid = Uuid::new_v4(); + let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); + let to_insert = RawCategory { + default_enabled: value.default_enabled, + guid: new_uuid, + relative_category_name: relative_category_name.clone(), + display_name: relative_category_name.clone(), + parent_name: prefix_until_nth_char(&parent_name, '.', n-1), + props: value.props.clone(), + separator: false, + full_category_name: parent_name.clone() + }; + last_name = Some(to_insert.full_category_name.clone()); + second_pass_categories.insert(parent_name.clone(), to_insert); + late_discovered_categories.insert(new_uuid); + need_a_pass = true; + } + n += 1; + } + late_discovered_categories.insert(value.guid); + to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + to_insert.display_name = to_insert.relative_category_name.clone(); + debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); + assert!(last_name.is_some()); + to_insert.parent_name = last_name; + } else { + to_insert.parent_name = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.full_category_name.clone()) + } else { + None + } + }else { + None + }; + debug!("insert as is {:?}", to_insert); + } + second_pass_categories.insert(key.clone(), to_insert); + } + if need_a_pass { + std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); + second_pass_categories.clear(); + } + } + for (key, value) in second_pass_categories { + let parent = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.guid.clone()) + } else { + None + } + } else { + None + }; + + debug!("{} parent is {:?}", key , parent); + let cat = Category::from(&value, parent); + let ref_uuid = cat.guid.clone(); + if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { + third_pass_categories_ref.push(ref_uuid); + } + } + + for full_category_name in third_pass_categories_ref { + if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { + if let Some(parent) = cat.parent { + if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else { + panic!("Could not find parent {} for {:?}", parent, cat); + } + } else { + root.insert(cat.guid.clone(), cat); + } + } else { + panic!("Some bad logic at works"); + } + } + debug!("reassemble_categories {:?}", root); + root + } + + +} + diff --git a/crates/joko_package/src/pack/question.png b/crates/joko_package/src/pack/question.png new file mode 100644 index 0000000000000000000000000000000000000000..02851711a0a83c17291f68672e13a7d4cb26f586 GIT binary patch literal 4248 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, + pub reset_position: Vec3, + pub reset_range: f64, + pub map_id: u32, + pub guid: Uuid, + pub name: String, + pub source_file_name: String, +} diff --git a/crates/joko_package/src/pack/trail.png b/crates/joko_package/src/pack/trail.png new file mode 100644 index 0000000000000000000000000000000000000000..7529ba0fccf9f596a6515371d695d21ad15ef1ab GIT binary patch literal 6896 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 E19PVjH2?qr literal 0 HcmV?d00001 diff --git a/crates/joko_package/src/pack/trail.rs b/crates/joko_package/src/pack/trail.rs new file mode 100644 index 0000000..71908f1 --- /dev/null +++ b/crates/joko_package/src/pack/trail.rs @@ -0,0 +1,31 @@ +use uuid::Uuid; + +use super::CommonAttributes; + +#[derive(Debug, Clone)] +pub(crate) struct Trail { + pub guid: Uuid, + pub parent: Uuid, + pub map_id: u32, + pub category: String, + pub props: CommonAttributes, + pub dynamic: bool, + pub source_file_name: String, +} + +#[derive(Debug, Clone)] +pub(crate) struct TBin { + pub map_id: u32, + pub version: u32, + pub nodes: Vec, +} +#[derive(Debug, Clone)] +pub(crate) struct TBinStatus { + pub tbin: TBin, + pub iso_x: bool, + pub iso_y: bool, + pub iso_z: bool, + pub closed: bool, +} + +impl TBin {} diff --git a/crates/joko_package/src/pack/trail_black.png b/crates/joko_package/src/pack/trail_black.png new file mode 100644 index 0000000000000000000000000000000000000000..d4326e68a6bdc79ef11a27f2deee592e3dbc4382 GIT binary patch literal 2293 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#QHWK~#8N?VU%6 zWknQ*$DA?eoE_=%sJa1|knwU2G`c6$D4`bAp<*yx2@UBEAsK zlRt`uVrQ|kuqqLQ|A6>Sj2iS|ZWDV6s{%3Y^W-!5Wci~8e3)MZdBTZeDPa{V627O! zOw;}-z7o%iqXjua^{OceWS@V4xKNPrZ6IbVasa#nzLyWg&*BeZWiGaH3^@Dz$y>d- zaEsVQkW5;cYj5$8U>i4TFvkI__+D{@urk)U;tBDC7&VY{;d}9%z${{ksZOQm0H=ww z#J+;HyKAi|FpD-7?~89lRc2AC0l=ESrMOHSC~)&aiQT~Ff*rz#g2eDIVI>p;e!pPv zK5FU?#jRp@VI|ZF0+WaBSzD8*D=?NG75pEnP-#oR1>ziWm{?83cI-$5*=}wpJ{KQ} ze?+k=B>-FL41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@ z;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LY ziI(MnCR2O~3P|_`#rOm{1^{dRd~vB@@I#tBU5UhSBf(6CS%g2~_OdZVw$FgG=JREa znsRf8D&lT}-^H4x$nqZ+JUzLt`7^)Kw`L-z$~JyoMldsIsmBZ;p$y2uy<2Bd|iuD@($uN z@v$JMu~{^vtp!y**3D7tX$_Fe+$k1_?38i_pqYbu{;7h!bgiw}{9d~X@&bAZ-WCK# z7D^QWv~gkd(9A($rPjf3gtvj2#f(Psx0B~sI27$lN)6i$J^s!6$U62zbQ>*~s zxE3dNjuA9c^j+A*6pt{A=rrbddXkwSMgUB8KELnE;(mgV0YK)vvA9CeFfqNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNw zI$slb-`3p3Hjz?tP`PcX=W+n_Tn>Pq%K?y|u!K2k7R4HN0OTj^;8zBp!4)C^e6?CZ z3K0NLiPtCoXpJErAP&ax8C)TZAZpyWT7V5q)yr<+%=$#UDxMYGdPZF!cpeaHV7Rvr zH;_Xf)VxF09HtW;Nc85@f6(Ulq!@$qmTLKWS#TvCByAGuLVV%C62su9U<<)i+V-~a z+Xa1!qyDZK@LhSP>`2`B%~kQUxmSSGJ6r&DoaoI!5P7xf6hYtNp@PCn+rgwcgQHwr z3du+Bs5p*iPI>oz+D=L8uDlZOUMwt6^d!_#-yFz7c6`t@)( zt;AyRN%&&k7EW%_TmWSy6w^LWK7-#?!q*n+^jrW{%{zxn%K=FESo66!nsqt0W5@JQ z>o&)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<` zi>iE~R4&>{knnYB@^l4H{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m z0f4hd!pGA^fhM=XZ`520$sLEytL2KRBIXzXtfw3;zg$q#9jB@nTUgNS47eJWAfSrq zOCiS;88&Nvskeo52^M)v5z1`_I9yyKnv0}zo%W~GeuAwc)x>xBLC(r408STN9(j}~ z`SM7*p~+b|-Ah*ySHG}($x^ul0Ok(2hw@2AUAmY7a4NYc$!5_sA^=RAPd2+>^0odY^5p#!N&1-%eac~x+i%TGqnnke!0F#G(KIiUydE`XB zgQgaiL-GOY2T9DL7y*F2i^InZz#3>xhyoIhC%FN$0OjQ9W>LrhAj)yW@bizC~KvM?`e{ P00000NkvXXu0mjf7Ck~> literal 0 HcmV?d00001 diff --git a/crates/joko_package/src/pack/trail_rainbow.png b/crates/joko_package/src/pack/trail_rainbow.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3ff6d305a60bb6a05c6418b6417c832ac14c16 GIT binary patch literal 16987 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 literal 0 HcmV?d00001 diff --git a/crates/joko_package/vendor/rapid/license.txt b/crates/joko_package/vendor/rapid/license.txt new file mode 100644 index 0000000..e5ecd23 --- /dev/null +++ b/crates/joko_package/vendor/rapid/license.txt @@ -0,0 +1,52 @@ +Use of this software is granted under one of the following two licenses, +to be chosen freely by the user. + +1. Boost Software License - Version 1.0 - August 17th, 2003 +=============================================================================== + +Copyright (c) 2006, 2007 Marcin Kalicinski + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +2. The MIT License +=============================================================================== + +Copyright (c) 2006, 2007 Marcin Kalicinski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/crates/joko_package/vendor/rapid/rapid.cpp b/crates/joko_package/vendor/rapid/rapid.cpp new file mode 100644 index 0000000..7c0aceb --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapid.cpp @@ -0,0 +1,66 @@ +#include "joko_package/vendor/rapid/rapid.hpp" +#include "joko_package/vendor/rapid/rapidxml.hpp" +#include "joko_package/vendor/rapid/rapidxml_print.hpp" +#include "joko_package/src/lib.rs.h" +#include +#include +#include +#include +void remove_duplicate_nodes(rapidxml::xml_node *node) +{ + + std::set duplicates; + rapidxml::xml_attribute *attr = node->first_attribute(); + while (attr) + { + std::string name(attr->name(), attr->name_size()); + if (duplicates.count(name) == 1) + { + rapidxml::xml_attribute *prev = attr; + attr = attr->next_attribute(); + node->remove_attribute(prev); + } + else + { + duplicates.insert(name); + attr = attr->next_attribute(); + } + } + for (rapidxml::xml_node *child = node->first_node(); child; child = child->next_sibling()) + { + remove_duplicate_nodes(child); + } +} + +namespace rapid +{ + + rust::String rapid_filter(rust::String src_xml) + { + // return std::string(src_xml); + std::string src = static_cast(src_xml); + std::string dst; + using namespace rapidxml; + // create document + xml_document doc; + // rapid xml throws exception if there's a parsing error + try + { + // parse the xml text. if there's exceptions we go to catch block from here + doc.parse<0>((char *)src.c_str()); + // delete all the duplicate attributes, so that there's no obvious errors for rust deserializers + for (rapidxml::xml_node *child = doc.first_node(); child; child = child->next_sibling()) + { + remove_duplicate_nodes(child); + } + std::ostringstream oss; + oss << doc; + dst = oss.str(); + } + catch (const parse_error &e) + { + return ""; + } + return dst; + } +} diff --git a/crates/joko_package/vendor/rapid/rapid.hpp b/crates/joko_package/vendor/rapid/rapid.hpp new file mode 100644 index 0000000..3652609 --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapid.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "joko_package/src/lib.rs.h" +#include "rust/cxx.h" + +namespace rapid { + rust::String rapid_filter(rust::String src_xml); +} \ No newline at end of file diff --git a/crates/joko_package/vendor/rapid/rapidxml.hpp b/crates/joko_package/vendor/rapid/rapidxml.hpp new file mode 100644 index 0000000..d025eef --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapidxml.hpp @@ -0,0 +1,2645 @@ +#ifndef RAPIDXML_HPP_INCLUDED +#define RAPIDXML_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/13 01:46:17 $ +//! \file rapidxml.hpp This file contains rapidxml parser and DOM implementation + +// If standard library is disabled, user must provide implementations of required functions and typedefs +#if !defined(RAPIDXML_NO_STDLIB) + #include // For std::size_t + #include // (Optional.) For std::strlen, ... + #include // (Optional.) For std::wcslen, ... + #include // For assert + #include // For placement new +#endif + +// RAPIDXML_NOEXCEPT: Expands to 'noexcept' on supported compilers. +#if !defined(RAPIDXML_NOEXCEPT) +# if !defined(RAPIDXML_DISABLE_NOEXCEPT) +# if defined(__clang__) +# if __has_feature(__cxx_noexcept__) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# elif defined(__GNUC__) +# if ((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4) +# if defined(__GXX_EXPERIMENTAL_CXX0X__) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# endif +# elif defined(_MSC_VER) && (_MSC_VER >= 1900) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# endif +# if !defined(RAPIDXML_NOEXCEPT) +# define RAPIDXML_NOEXCEPT +# endif +#endif + +// On MSVC, disable "conditional expression is constant" warning (level 4). +// This warning is almost impossible to avoid with certain types of templated code +#ifdef _MSC_VER + #pragma warning(push) + #pragma warning(disable:4127) // Conditional expression is constant +#endif + +/////////////////////////////////////////////////////////////////////////// +// RAPIDXML_PARSE_ERROR + +#if defined(RAPIDXML_NO_EXCEPTIONS) + +#define RAPIDXML_PARSE_ERROR(what, where) { parse_error_handler(what, where); assert(0); } + +namespace rapidxml +{ + //! When exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, + //! this function is called to notify user about the error. + //! It must be defined by the user. + //!

+ //! This function cannot return. If it does, the results are undefined. + //!

+ //! A very simple definition might look like that: + //!

+    //! void %rapidxml::%parse_error_handler(const char *what, void *where)
+    //! {
+    //!     std::cout << "Parse error: " << what << "\n";
+    //!     std::abort();
+    //! }
+    //! 
+ //! \param what Human readable description of the error. + //! \param where Pointer to character data where error was detected. + void parse_error_handler(const char *what, void *where); +} + +#else + +#include // For std::exception + +#define RAPIDXML_PARSE_ERROR(what, where) throw parse_error(what, where) + +namespace rapidxml +{ + + //! Parse error exception. + //! This exception is thrown by the parser when an error occurs. + //! Use what() function to get human-readable error message. + //! Use where() function to get a pointer to position within source text where error was detected. + //!

+ //! If throwing exceptions by the parser is undesirable, + //! it can be disabled by defining RAPIDXML_NO_EXCEPTIONS macro before rapidxml.hpp is included. + //! This will cause the parser to call rapidxml::parse_error_handler() function instead of throwing an exception. + //! This function must be defined by the user. + //!

+ //! This class derives from std::exception class. + class parse_error: public std::exception + { + public: + //! Constructs parse error + parse_error(const char *what, void *where) + : m_what(what) + , m_where(where) + { + } + + //! Gets human readable description of error. + //! \return Pointer to null terminated description of the error. + virtual const char *what() const throw() + { + return m_what; + } + + //! Gets pointer to character data where error happened. + //! Ch should be the same as char type of xml_document that produced the error. + //! \return Pointer to location within the parsed string where error occured. + template + Ch *where() const + { + return reinterpret_cast(m_where); + } + + private: + const char *m_what; + void *m_where; + }; +} + +#endif + +/////////////////////////////////////////////////////////////////////////// +// Pool sizes + +#ifndef RAPIDXML_STATIC_POOL_SIZE + // Size of static memory block of memory_pool. + // Define RAPIDXML_STATIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. + // No dynamic memory allocations are performed by memory_pool until static memory is exhausted. + #define RAPIDXML_STATIC_POOL_SIZE (64 * 1024) +#endif + +#ifndef RAPIDXML_DYNAMIC_POOL_SIZE + // Size of dynamic memory block of memory_pool. + // Define RAPIDXML_DYNAMIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. + // After the static block is exhausted, dynamic blocks with approximately this size are allocated by memory_pool. + #define RAPIDXML_DYNAMIC_POOL_SIZE (64 * 1024) +#endif + +#ifndef RAPIDXML_ALIGNMENT + // Memory allocation alignment. + // Define RAPIDXML_ALIGNMENT before including rapidxml.hpp if you want to override the default value, which is the size of pointer. + // All memory allocations for nodes, attributes and strings will be aligned to this value. + // This must be a power of 2 and at least 1, otherwise memory_pool will not work. + #define RAPIDXML_ALIGNMENT sizeof(void *) +#endif + +namespace rapidxml +{ + // Forward declarations + template class xml_node; + template class xml_attribute; + template class xml_document; + + //! Enumeration listing all node types produced by the parser. + //! Use xml_node::type() function to query node type. + enum node_type + { + node_document, //!< A document node. Name and value are empty. + node_element, //!< An element node. Name contains element name. Value contains text of first data node. + node_data, //!< A data node. Name is empty. Value contains data text. + node_cdata, //!< A CDATA node. Name is empty. Value contains data text. + node_comment, //!< A comment node. Name is empty. Value contains comment text. + node_declaration, //!< A declaration node. Name and value are empty. Declaration parameters (version, encoding and standalone) are in node attributes. + node_doctype, //!< A DOCTYPE node. Name is empty. Value contains DOCTYPE text. + node_pi //!< A PI node. Name contains target. Value contains instructions. + }; + + /////////////////////////////////////////////////////////////////////// + // Parsing flags + + //! Parse flag instructing the parser to not create data nodes. + //! Text of first data node will still be placed in value of parent element, unless rapidxml::parse_no_element_values flag is also specified. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_data_nodes = 0x1; + + //! Parse flag instructing the parser to not use text of first data node as a value of parent element. + //! Can be combined with other flags by use of | operator. + //! Note that child data nodes of element node take precendence over its value when printing. + //! That is, if element has one or more child data nodes and a value, the value will be ignored. + //! Use rapidxml::parse_no_data_nodes flag to prevent creation of data nodes if you want to manipulate data using values of elements. + //!

+ //! See xml_document::parse() function. + const int parse_no_element_values = 0x2; + + //! Parse flag instructing the parser to not place zero terminators after strings in the source text. + //! By default zero terminators are placed, modifying source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_string_terminators = 0x4; + + //! Parse flag instructing the parser to not translate entities in the source text. + //! By default entities are translated, modifying source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_entity_translation = 0x8; + + //! Parse flag instructing the parser to disable UTF-8 handling and assume plain 8 bit characters. + //! By default, UTF-8 handling is enabled. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_utf8 = 0x10; + + //! Parse flag instructing the parser to create XML declaration node. + //! By default, declaration node is not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_declaration_node = 0x20; + + //! Parse flag instructing the parser to create comments nodes. + //! By default, comment nodes are not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_comment_nodes = 0x40; + + //! Parse flag instructing the parser to create DOCTYPE node. + //! By default, doctype node is not created. + //! Although W3C specification allows at most one DOCTYPE node, RapidXml will silently accept documents with more than one. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_doctype_node = 0x80; + + //! Parse flag instructing the parser to create PI nodes. + //! By default, PI nodes are not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_pi_nodes = 0x100; + + //! Parse flag instructing the parser to validate closing tag names. + //! If not set, name inside closing tag is irrelevant to the parser. + //! By default, closing tags are not validated. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_validate_closing_tags = 0x200; + + //! Parse flag instructing the parser to trim all leading and trailing whitespace of data nodes. + //! By default, whitespace is not trimmed. + //! This flag does not cause the parser to modify source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_trim_whitespace = 0x400; + + //! Parse flag instructing the parser to condense all whitespace runs of data nodes to a single space character. + //! Trimming of leading and trailing whitespace of data is controlled by rapidxml::parse_trim_whitespace flag. + //! By default, whitespace is not normalized. + //! If this flag is specified, source text will be modified. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_normalize_whitespace = 0x800; + + // Compound flags + + //! Parse flags which represent default behaviour of the parser. + //! This is always equal to 0, so that all other flags can be simply ored together. + //! Normally there is no need to inconveniently disable flags by anding with their negated (~) values. + //! This also means that meaning of each flag is a negation of the default setting. + //! For example, if flag name is rapidxml::parse_no_utf8, it means that utf-8 is enabled by default, + //! and using the flag will disable it. + //!

+ //! See xml_document::parse() function. + const int parse_default = 0; + + //! A combination of parse flags that forbids any modifications of the source text. + //! This also results in faster parsing. However, note that the following will occur: + //!
    + //!
  • names and values of nodes will not be zero terminated, you have to use xml_base::name_size() and xml_base::value_size() functions to determine where name and value ends
  • + //!
  • entities will not be translated
  • + //!
  • whitespace will not be normalized
  • + //!
+ //! See xml_document::parse() function. + const int parse_non_destructive = parse_no_string_terminators | parse_no_entity_translation; + + //! A combination of parse flags resulting in fastest possible parsing, without sacrificing important data. + //!

+ //! See xml_document::parse() function. + const int parse_fastest = parse_non_destructive | parse_no_data_nodes; + + //! A combination of parse flags resulting in largest amount of data being extracted. + //! This usually results in slowest parsing. + //!

+ //! See xml_document::parse() function. + const int parse_full = parse_declaration_node | parse_comment_nodes | parse_doctype_node | parse_pi_nodes | parse_validate_closing_tags; + + /////////////////////////////////////////////////////////////////////// + // Internals + + //! \cond internal + namespace internal + { + + // Struct that contains lookup tables for the parser + // It must be a template to allow correct linking (because it has static data members, which are defined in a header file). + template + struct lookup_tables + { + static const unsigned char lookup_whitespace[256]; // Whitespace table + static const unsigned char lookup_node_name[256]; // Node name table + static const unsigned char lookup_text[256]; // Text table + static const unsigned char lookup_text_pure_no_ws[256]; // Text table + static const unsigned char lookup_text_pure_with_ws[256]; // Text table + static const unsigned char lookup_attribute_name[256]; // Attribute name table + static const unsigned char lookup_attribute_data_1[256]; // Attribute data table with single quote + static const unsigned char lookup_attribute_data_1_pure[256]; // Attribute data table with single quote + static const unsigned char lookup_attribute_data_2[256]; // Attribute data table with double quotes + static const unsigned char lookup_attribute_data_2_pure[256]; // Attribute data table with double quotes + static const unsigned char lookup_digits[256]; // Digits + static const unsigned char lookup_upcase[256]; // To uppercase conversion table for ASCII characters + }; + + // Find length of the string + template + inline std::size_t measure(const Ch *p) RAPIDXML_NOEXCEPT + { + const Ch *tmp = p; + while (*tmp) + ++tmp; + return tmp - p; + } + +#if !defined(RAPIDXML_NO_STDLIB) + inline std::size_t measure(const char* p) RAPIDXML_NOEXCEPT + { return std::strlen(p); } + + inline std::size_t measure(const wchar_t* p) RAPIDXML_NOEXCEPT + { return std::wcslen(p); } +#endif + + // Compare strings for equality + template + inline bool compare(const Ch *p1, std::size_t size1, const Ch *p2, + std::size_t size2, bool case_sensitive) RAPIDXML_NOEXCEPT + { + if (size1 != size2) + return false; + if (case_sensitive) + { + for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) + if (*p1 != *p2) + return false; + } + else + { + for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) + if (lookup_tables<0>::lookup_upcase[static_cast(*p1)] != lookup_tables<0>::lookup_upcase[static_cast(*p2)]) + return false; + } + return true; + } + } + //! \endcond + + /////////////////////////////////////////////////////////////////////// + // Memory pool + + //! This class is used by the parser to create new nodes and attributes, without overheads of dynamic memory allocation. + //! In most cases, you will not need to use this class directly. + //! However, if you need to create nodes manually or modify names/values of nodes, + //! you are encouraged to use memory_pool of relevant xml_document to allocate the memory. + //! Not only is this faster than allocating them by using new operator, + //! but also their lifetime will be tied to the lifetime of document, + //! possibly simplyfing memory management. + //!

+ //! Call allocate_node() or allocate_attribute() functions to obtain new nodes or attributes from the pool. + //! You can also call allocate_string() function to allocate strings. + //! Such strings can then be used as names or values of nodes without worrying about their lifetime. + //! Note that there is no free() function -- all allocations are freed at once when clear() function is called, + //! or when the pool is destroyed. + //!

+ //! It is also possible to create a standalone memory_pool, and use it + //! to allocate nodes, whose lifetime will not be tied to any document. + //!

+ //! Pool maintains RAPIDXML_STATIC_POOL_SIZE bytes of statically allocated memory. + //! Until static memory is exhausted, no dynamic memory allocations are done. + //! When static memory is exhausted, pool allocates additional blocks of memory of size RAPIDXML_DYNAMIC_POOL_SIZE each, + //! by using global new[] and delete[] operators. + //! This behaviour can be changed by setting custom allocation routines. + //! Use set_allocator() function to set them. + //!

+ //! Allocations for nodes, attributes and strings are aligned at RAPIDXML_ALIGNMENT bytes. + //! This value defaults to the size of pointer on target architecture. + //!

+ //! To obtain absolutely top performance from the parser, + //! it is important that all nodes are allocated from a single, contiguous block of memory. + //! Otherwise, cache misses when jumping between two (or more) disjoint blocks of memory can slow down parsing quite considerably. + //! If required, you can tweak RAPIDXML_STATIC_POOL_SIZE, RAPIDXML_DYNAMIC_POOL_SIZE and RAPIDXML_ALIGNMENT + //! to obtain best wasted memory to performance compromise. + //! To do it, define their values before rapidxml.hpp file is included. + //! \param Ch Character type of created nodes. + template + class memory_pool + { + + public: + + //! \cond internal + typedef void *(alloc_func)(std::size_t); // Type of user-defined function used to allocate memory + typedef void (free_func)(void *); // Type of user-defined function used to free memory + //! \endcond + + //! Constructs empty pool with default allocator functions. + memory_pool() RAPIDXML_NOEXCEPT + : m_alloc_func(0) + , m_free_func(0) + { + init(); + } + + //! Destroys pool and frees all the memory. + //! This causes memory occupied by nodes allocated by the pool to be freed. + //! Nodes allocated from the pool are no longer valid. + ~memory_pool() + { + clear(); + } + + //! Allocates a new node from the pool, and optionally assigns name and value to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param type Type of node to create. + //! \param name Name to assign to the node, or 0 to assign no name. + //! \param value Value to assign to the node, or 0 to assign no value. + //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. + //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. + //! \return Pointer to allocated node. This pointer will never be NULL. + xml_node *allocate_node(node_type type, + const Ch *name = 0, const Ch *value = 0, + std::size_t name_size = 0, std::size_t value_size = 0) + { + void *memory = allocate_aligned(sizeof(xml_node)); + xml_node *node = new(memory) xml_node(type); + if (name) + { + if (name_size > 0) + node->name(name, name_size); + else + node->name(name); + } + if (value) + { + if (value_size > 0) + node->value(value, value_size); + else + node->value(value); + } + return node; + } + + //! Allocates a new attribute from the pool, and optionally assigns name and value to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param name Name to assign to the attribute, or 0 to assign no name. + //! \param value Value to assign to the attribute, or 0 to assign no value. + //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. + //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. + //! \return Pointer to allocated attribute. This pointer will never be NULL. + xml_attribute *allocate_attribute(const Ch *name = 0, const Ch *value = 0, + std::size_t name_size = 0, std::size_t value_size = 0) + { + void *memory = allocate_aligned(sizeof(xml_attribute)); + xml_attribute *attribute = new(memory) xml_attribute; + if (name) + { + if (name_size > 0) + attribute->name(name, name_size); + else + attribute->name(name); + } + if (value) + { + if (value_size > 0) + attribute->value(value, value_size); + else + attribute->value(value); + } + return attribute; + } + + //! Allocates a char array of given size from the pool, and optionally copies a given string to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param source String to initialize the allocated memory with, or 0 to not initialize it. + //! \param size Number of characters to allocate, or zero to calculate it automatically from source string length; if size is 0, source string must be specified and null terminated. + //! \return Pointer to allocated char array. This pointer will never be NULL. + Ch *allocate_string(const Ch *source = 0, std::size_t size = 0) + { + assert(source || size); // Either source or size (or both) must be specified + if (size == 0) + size = internal::measure(source) + 1; + Ch *result = static_cast(allocate_aligned(size * sizeof(Ch))); + if (source) + for (std::size_t i = 0; i < size; ++i) + result[i] = source[i]; + return result; + } + + //! Clones an xml_node and its hierarchy of child nodes and attributes. + //! Nodes and attributes are allocated from this memory pool. + //! Names and values are not cloned, they are shared between the clone and the source. + //! Result node can be optionally specified as a second parameter, + //! in which case its contents will be replaced with cloned source node. + //! This is useful when you want to clone entire document. + //! \param source Node to clone. + //! \param result Node to put results in, or 0 to automatically allocate result node + //! \return Pointer to cloned node. This pointer will never be NULL. + xml_node *clone_node(const xml_node *source, xml_node *result = 0) + { + // Prepare result node + if (result) + { + result->remove_all_attributes(); + result->remove_all_nodes(); + result->type(source->type()); + } + else + result = allocate_node(source->type()); + + // Clone name and value + result->name(source->name(), source->name_size()); + result->value(source->value(), source->value_size()); + + // Clone child nodes and attributes + for (xml_node *child = source->first_node(); child; child = child->next_sibling()) + result->append_node(clone_node(child)); + for (xml_attribute *attr = source->first_attribute(); attr; attr = attr->next_attribute()) + result->append_attribute(allocate_attribute(attr->name(), attr->value(), attr->name_size(), attr->value_size())); + + return result; + } + + //! Clears the pool. + //! This causes memory occupied by nodes allocated by the pool to be freed. + //! Any nodes or strings allocated from the pool will no longer be valid. + void clear() RAPIDXML_NOEXCEPT + { + while (m_begin != m_static_memory) + { + char *previous_begin = reinterpret_cast
(align(m_begin))->previous_begin; + if (m_free_func) + m_free_func(m_begin); + else + delete[] m_begin; + m_begin = previous_begin; + } + init(); + } + + //! Sets or resets the user-defined memory allocation functions for the pool. + //! This can only be called when no memory is allocated from the pool yet, otherwise results are undefined. + //! Allocation function must not return invalid pointer on failure. It should either throw, + //! stop the program, or use longjmp() function to pass control to other place of program. + //! If it returns invalid pointer, results are undefined. + //!

+ //! User defined allocation functions must have the following forms: + //!
+ //!
void *allocate(std::size_t size); + //!
void free(void *pointer); + //!

+ //! \param af Allocation function, or 0 to restore default function + //! \param ff Free function, or 0 to restore default function + void set_allocator(alloc_func *af, free_func *ff) + { + assert(m_begin == m_static_memory && m_ptr == align(m_begin)); // Verify that no memory is allocated yet + m_alloc_func = af; + m_free_func = ff; + } + + private: + + struct header + { + char *previous_begin; + }; + + void init() RAPIDXML_NOEXCEPT + { + m_begin = m_static_memory; + m_ptr = align(m_begin); + m_end = m_static_memory + sizeof(m_static_memory); + } + + char *align(char *ptr) const RAPIDXML_NOEXCEPT + { + std::size_t alignment = ((RAPIDXML_ALIGNMENT - (std::size_t(ptr) & (RAPIDXML_ALIGNMENT - 1))) & (RAPIDXML_ALIGNMENT - 1)); + return ptr + alignment; + } + + char *allocate_raw(std::size_t size) + { + // Allocate + void *memory; + if (m_alloc_func) // Allocate memory using either user-specified allocation function or global operator new[] + { + memory = m_alloc_func(size); + assert(memory); // Allocator is not allowed to return 0, on failure it must either throw, stop the program or use longjmp + } + else + { + memory = new char[size]; +#ifdef RAPIDXML_NO_EXCEPTIONS + if (!memory) // If exceptions are disabled, verify memory allocation, because new will not be able to throw bad_alloc + RAPIDXML_PARSE_ERROR("out of memory", 0); +#endif + } + return static_cast(memory); + } + + void *allocate_aligned(std::size_t size) + { + // Calculate aligned pointer + char *result = align(m_ptr); + + // If not enough memory left in current pool, allocate a new pool + if (result + size > m_end) + { + // Calculate required pool size (may be bigger than RAPIDXML_DYNAMIC_POOL_SIZE) + std::size_t pool_size = RAPIDXML_DYNAMIC_POOL_SIZE; + if (pool_size < size) + pool_size = size; + + // Allocate + std::size_t alloc_size = sizeof(header) + (2 * RAPIDXML_ALIGNMENT - 2) + pool_size; // 2 alignments required in worst case: one for header, one for actual allocation + char *raw_memory = allocate_raw(alloc_size); + + // Setup new pool in allocated memory + char *pool = align(raw_memory); + header *new_header = reinterpret_cast
(pool); + new_header->previous_begin = m_begin; + m_begin = raw_memory; + m_ptr = pool + sizeof(header); + m_end = raw_memory + alloc_size; + + // Calculate aligned pointer again using new pool + result = align(m_ptr); + } + + // Update pool and return aligned pointer + m_ptr = result + size; + return result; + } + + char *m_begin; // Start of raw memory making up current pool + char *m_ptr; // First free byte in current pool + char *m_end; // One past last available byte in current pool + char m_static_memory[RAPIDXML_STATIC_POOL_SIZE]; // Static raw memory + alloc_func *m_alloc_func; // Allocator function, or 0 if default is to be used + free_func *m_free_func; // Free function, or 0 if default is to be used + }; + + /////////////////////////////////////////////////////////////////////////// + // XML base + + //! Base class for xml_node and xml_attribute implementing common functions: + //! name(), name_size(), value(), value_size() and parent(). + //! \param Ch Character type to use + template + class xml_base + { + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + // Construct a base with empty name, value and parent + xml_base() RAPIDXML_NOEXCEPT + : m_name(0) + , m_value(0) + , m_parent(0) + , m_offset(0) + { + } + + /////////////////////////////////////////////////////////////////////////// + // Node data access + + //! Gets name of the node. + //! Interpretation of name depends on type of node. + //! Note that name will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. + //!

+ //! Use name_size() function to determine length of the name. + //! \return Name of node, or empty string if node has no name. + Ch *name() const RAPIDXML_NOEXCEPT + { + return m_name ? m_name : nullstr(); + } + + //! Gets size of node name, not including terminator character. + //! This function works correctly irrespective of whether name is or is not zero terminated. + //! \return Size of node name, in characters. + std::size_t name_size() const RAPIDXML_NOEXCEPT + { + return m_name ? m_name_size : 0; + } + + //! Gets value of node. + //! Interpretation of value depends on type of node. + //! Note that value will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. + //!

+ //! Use value_size() function to determine length of the value. + //! \return Value of node, or empty string if node has no value. + Ch *value() const RAPIDXML_NOEXCEPT + { + return m_value ? m_value : nullstr(); + } + + //! Gets size of node value, not including terminator character. + //! This function works correctly irrespective of whether value is or is not zero terminated. + //! \return Size of node value, in characters. + std::size_t value_size() const RAPIDXML_NOEXCEPT + { + return m_value ? m_value_size : 0; + } + + //! Get the start offset of this node inside the source string. + Ch *offset() const RAPIDXML_NOEXCEPT + { + return m_offset; + } + + /////////////////////////////////////////////////////////////////////////// + // Node modification + + //! Sets name of node to a non zero-terminated string. + //! See \ref ownership_of_strings. + //!

+ //! Note that node does not own its name or value, it only stores a pointer to it. + //! It will not delete or otherwise free the pointer on destruction. + //! It is reponsibility of the user to properly manage lifetime of the string. + //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - + //! on destruction of the document the string will be automatically freed. + //!

+ //! Size of name must be specified separately, because name does not have to be zero terminated. + //! Use name(const Ch *) function to have the length automatically calculated (string must be zero terminated). + //! \param name Name of node to set. Does not have to be zero terminated. + //! \param size Size of name, in characters. This does not include zero terminator, if one is present. + void name(const Ch *name, std::size_t size) RAPIDXML_NOEXCEPT + { + m_name = const_cast(name); + m_name_size = size; + } + + //! Sets name of node to a zero-terminated string. + //! See also \ref ownership_of_strings and xml_node::name(const Ch *, std::size_t). + //! \param name Name of node to set. Must be zero terminated. + void name(const Ch *name) RAPIDXML_NOEXCEPT + { + this->name(name, internal::measure(name)); + } + + //! Sets value of node to a non zero-terminated string. + //! See \ref ownership_of_strings. + //!

+ //! Note that node does not own its name or value, it only stores a pointer to it. + //! It will not delete or otherwise free the pointer on destruction. + //! It is reponsibility of the user to properly manage lifetime of the string. + //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - + //! on destruction of the document the string will be automatically freed. + //!

+ //! Size of value must be specified separately, because it does not have to be zero terminated. + //! Use value(const Ch *) function to have the length automatically calculated (string must be zero terminated). + //!

+ //! If an element has a child node of type node_data, it will take precedence over element value when printing. + //! If you want to manipulate data of elements using values, use parser flag rapidxml::parse_no_data_nodes to prevent creation of data nodes by the parser. + //! \param value value of node to set. Does not have to be zero terminated. + //! \param size Size of value, in characters. This does not include zero terminator, if one is present. + void value(const Ch *value, std::size_t size) RAPIDXML_NOEXCEPT + { + m_value = const_cast(value); + m_value_size = size; + } + + //! Sets value of node to a zero-terminated string. + //! See also \ref ownership_of_strings and xml_node::value(const Ch *, std::size_t). + //! \param value Vame of node to set. Must be zero terminated. + void value(const Ch *value) RAPIDXML_NOEXCEPT + { + this->value(value, internal::measure(value)); + } + + //! Sets the offset inside the source string. + //! This is only intended for debugging purposes. + void offset(Ch *offset) RAPIDXML_NOEXCEPT + { + m_offset = offset; + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets node parent. + //! \return Pointer to parent node, or 0 if there is no parent. + xml_node *parent() const RAPIDXML_NOEXCEPT + { + return m_parent; + } + + protected: + + // Return empty string + static Ch *nullstr() RAPIDXML_NOEXCEPT + { + static Ch zero = Ch('\0'); + return &zero; + } + + Ch *m_name; // Name of node, or 0 if no name + Ch *m_value; // Value of node, or 0 if no value + std::size_t m_name_size; // Length of node name, or undefined of no name + std::size_t m_value_size; // Length of node value, or undefined if no value + xml_node *m_parent; // Pointer to parent node, or 0 if none + Ch *m_offset; // Start offset of this node inside the string + }; + + //! Class representing attribute node of XML document. + //! Each attribute has name and value strings, which are available through name() and value() functions (inherited from xml_base). + //! Note that after parse, both name and value of attribute will point to interior of source text used for parsing. + //! Thus, this text must persist in memory for the lifetime of attribute. + //! \param Ch Character type to use. + template + class xml_attribute: public xml_base + { + + friend class xml_node; + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + //! Constructs an empty attribute with the specified type. + //! Consider using memory_pool of appropriate xml_document if allocating attributes manually. + xml_attribute() + { + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets document of which attribute is a child. + //! \return Pointer to document that contains this attribute, or 0 if there is no parent document. + xml_document *document() const + { + if (xml_node *node = this->parent()) + { + while (node->parent()) + node = node->parent(); + return node->type() == node_document ? static_cast *>(node) : 0; + } + else + return 0; + } + + //! Gets previous attribute, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return previous attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *previous_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_prev_attribute; attribute; attribute = attribute->m_prev_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return this->m_parent ? m_prev_attribute : 0; + } + + //! Gets next attribute, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return next attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *next_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_next_attribute; attribute; attribute = attribute->m_next_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return this->m_parent ? m_next_attribute : 0; + } + + private: + + xml_attribute *m_prev_attribute; // Pointer to previous sibling of attribute, or 0 if none; only valid if parent is non-zero + xml_attribute *m_next_attribute; // Pointer to next sibling of attribute, or 0 if none; only valid if parent is non-zero + + }; + + /////////////////////////////////////////////////////////////////////////// + // XML node + + //! Class representing a node of XML document. + //! Each node may have associated name and value strings, which are available through name() and value() functions. + //! Interpretation of name and value depends on type of the node. + //! Type of node can be determined by using type() function. + //!

+ //! Note that after parse, both name and value of node, if any, will point interior of source text used for parsing. + //! Thus, this text must persist in the memory for the lifetime of node. + //! \param Ch Character type to use. + template + class xml_node: public xml_base + { + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + //! Constructs an empty node with the specified type. + //! Consider using memory_pool of appropriate document to allocate nodes manually. + //! \param type Type of node to construct. + xml_node(node_type type) + : m_type(type) + , m_first_node(0) + , m_first_attribute(0) + { + } + + /////////////////////////////////////////////////////////////////////////// + // Node data access + + //! Gets type of node. + //! \return Type of node. + node_type type() const + { + return m_type; + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets document of which node is a child. + //! \return Pointer to document that contains this node, or 0 if there is no parent document. + xml_document *document() const + { + xml_node *node = const_cast *>(this); + while (node->parent()) + node = node->parent(); + return node->type() == node_document ? static_cast *>(node) : 0; + } + + //! Gets first child node, optionally matching node name. + //! \param name Name of child to find, or 0 to return first child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found child, or 0 if not found. + xml_node *first_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *child = m_first_node; child; child = child->next_sibling()) + if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) + return child; + return 0; + } + else + return m_first_node; + } + + //! Gets last child node, optionally matching node name. + //! Behaviour is undefined if node has no children. + //! Use first_node() to test if node has children. + //! \param name Name of child to find, or 0 to return last child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found child, or 0 if not found. + xml_node *last_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(m_first_node); // Cannot query for last child if node has no children + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *child = m_last_node; child; child = child->previous_sibling()) + if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) + return child; + return 0; + } + else + return m_last_node; + } + + //! Gets previous sibling node, optionally matching node name. + //! Behaviour is undefined if node has no parent. + //! Use parent() to test if node has a parent. + //! \param name Name of sibling to find, or 0 to return previous sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found sibling, or 0 if not found. + xml_node *previous_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(this->m_parent); // Cannot query for siblings if node has no parent + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *sibling = m_prev_sibling; sibling; sibling = sibling->m_prev_sibling) + if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) + return sibling; + return 0; + } + else + return m_prev_sibling; + } + + //! Gets next sibling node, optionally matching node name. + //! Behaviour is undefined if node has no parent. + //! Use parent() to test if node has a parent. + //! \param name Name of sibling to find, or 0 to return next sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found sibling, or 0 if not found. + xml_node *next_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(this->m_parent); // Cannot query for siblings if node has no parent + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *sibling = m_next_sibling; sibling; sibling = sibling->m_next_sibling) + if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) + return sibling; + return 0; + } + else + return m_next_sibling; + } + + //! Gets first attribute of node, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return first attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *first_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_first_attribute; attribute; attribute = attribute->m_next_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return m_first_attribute; + } + + //! Gets last attribute of node, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return last attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *last_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_last_attribute; attribute; attribute = attribute->m_prev_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return m_first_attribute ? m_last_attribute : 0; + } + + /////////////////////////////////////////////////////////////////////////// + // Node modification + + //! Sets type of node. + //! \param type Type of node to set. + void type(node_type type) + { + m_type = type; + } + + /////////////////////////////////////////////////////////////////////////// + // Node manipulation + + //! Prepends a new child node. + //! The prepended child becomes the first child, and all existing children are moved one position back. + //! \param child Node to prepend. + void prepend_node(xml_node *child) + { + assert(child && !child->parent() && child->type() != node_document); + if (first_node()) + { + child->m_next_sibling = m_first_node; + m_first_node->m_prev_sibling = child; + } + else + { + child->m_next_sibling = 0; + m_last_node = child; + } + m_first_node = child; + child->m_parent = this; + child->m_prev_sibling = 0; + } + + //! Appends a new child node. + //! The appended child becomes the last child. + //! \param child Node to append. + void append_node(xml_node *child) + { + assert(child && !child->parent() && child->type() != node_document); + if (first_node()) + { + child->m_prev_sibling = m_last_node; + m_last_node->m_next_sibling = child; + } + else + { + child->m_prev_sibling = 0; + m_first_node = child; + } + m_last_node = child; + child->m_parent = this; + child->m_next_sibling = 0; + } + + //! Inserts a new child node at specified place inside the node. + //! All children after and including the specified node are moved one position back. + //! \param where Place where to insert the child, or 0 to insert at the back. + //! \param child Node to insert. + void insert_node(xml_node *where, xml_node *child) + { + assert(!where || where->parent() == this); + assert(child && !child->parent() && child->type() != node_document); + if (where == m_first_node) + prepend_node(child); + else if (where == 0) + append_node(child); + else + { + child->m_prev_sibling = where->m_prev_sibling; + child->m_next_sibling = where; + where->m_prev_sibling->m_next_sibling = child; + where->m_prev_sibling = child; + child->m_parent = this; + } + } + + //! Removes first child node. + //! If node has no children, behaviour is undefined. + //! Use first_node() to test if node has children. + void remove_first_node() + { + assert(first_node()); + xml_node *child = m_first_node; + m_first_node = child->m_next_sibling; + if (child->m_next_sibling) + child->m_next_sibling->m_prev_sibling = 0; + else + m_last_node = 0; + child->m_parent = 0; + } + + //! Removes last child of the node. + //! If node has no children, behaviour is undefined. + //! Use first_node() to test if node has children. + void remove_last_node() + { + assert(first_node()); + xml_node *child = m_last_node; + if (child->m_prev_sibling) + { + m_last_node = child->m_prev_sibling; + child->m_prev_sibling->m_next_sibling = 0; + } + else + m_first_node = 0; + child->m_parent = 0; + } + + //! Removes specified child from the node + // \param where Pointer to child to be removed. + void remove_node(xml_node *where) + { + assert(where && where->parent() == this); + assert(first_node()); + if (where == m_first_node) + remove_first_node(); + else if (where == m_last_node) + remove_last_node(); + else + { + where->m_prev_sibling->m_next_sibling = where->m_next_sibling; + where->m_next_sibling->m_prev_sibling = where->m_prev_sibling; + where->m_parent = 0; + } + } + + //! Removes all child nodes (but not attributes). + void remove_all_nodes() + { + for (xml_node *node = first_node(); node; node = node->m_next_sibling) + node->m_parent = 0; + m_first_node = 0; + } + + //! Prepends a new attribute to the node. + //! \param attribute Attribute to prepend. + void prepend_attribute(xml_attribute *attribute) + { + assert(attribute && !attribute->parent()); + if (first_attribute()) + { + attribute->m_next_attribute = m_first_attribute; + m_first_attribute->m_prev_attribute = attribute; + } + else + { + attribute->m_next_attribute = 0; + m_last_attribute = attribute; + } + m_first_attribute = attribute; + attribute->m_parent = this; + attribute->m_prev_attribute = 0; + } + + //! Appends a new attribute to the node. + //! \param attribute Attribute to append. + void append_attribute(xml_attribute *attribute) + { + assert(attribute && !attribute->parent()); + if (first_attribute()) + { + attribute->m_prev_attribute = m_last_attribute; + m_last_attribute->m_next_attribute = attribute; + } + else + { + attribute->m_prev_attribute = 0; + m_first_attribute = attribute; + } + m_last_attribute = attribute; + attribute->m_parent = this; + attribute->m_next_attribute = 0; + } + + //! Inserts a new attribute at specified place inside the node. + //! All attributes after and including the specified attribute are moved one position back. + //! \param where Place where to insert the attribute, or 0 to insert at the back. + //! \param attribute Attribute to insert. + void insert_attribute(xml_attribute *where, xml_attribute *attribute) + { + assert(!where || where->parent() == this); + assert(attribute && !attribute->parent()); + if (where == m_first_attribute) + prepend_attribute(attribute); + else if (where == 0) + append_attribute(attribute); + else + { + attribute->m_prev_attribute = where->m_prev_attribute; + attribute->m_next_attribute = where; + where->m_prev_attribute->m_next_attribute = attribute; + where->m_prev_attribute = attribute; + attribute->m_parent = this; + } + } + + //! Removes first attribute of the node. + //! If node has no attributes, behaviour is undefined. + //! Use first_attribute() to test if node has attributes. + void remove_first_attribute() + { + assert(first_attribute()); + xml_attribute *attribute = m_first_attribute; + if (attribute->m_next_attribute) + { + attribute->m_next_attribute->m_prev_attribute = 0; + } + else + m_last_attribute = 0; + attribute->m_parent = 0; + m_first_attribute = attribute->m_next_attribute; + } + + //! Removes last attribute of the node. + //! If node has no attributes, behaviour is undefined. + //! Use first_attribute() to test if node has attributes. + void remove_last_attribute() + { + assert(first_attribute()); + xml_attribute *attribute = m_last_attribute; + if (attribute->m_prev_attribute) + { + attribute->m_prev_attribute->m_next_attribute = 0; + m_last_attribute = attribute->m_prev_attribute; + } + else + m_first_attribute = 0; + attribute->m_parent = 0; + } + + //! Removes specified attribute from node. + //! \param where Pointer to attribute to be removed. + void remove_attribute(xml_attribute *where) + { + assert(first_attribute() && where->parent() == this); + if (where == m_first_attribute) + remove_first_attribute(); + else if (where == m_last_attribute) + remove_last_attribute(); + else + { + where->m_prev_attribute->m_next_attribute = where->m_next_attribute; + where->m_next_attribute->m_prev_attribute = where->m_prev_attribute; + where->m_parent = 0; + } + } + + //! Removes all attributes of node. + void remove_all_attributes() + { + for (xml_attribute *attribute = first_attribute(); attribute; attribute = attribute->m_next_attribute) + attribute->m_parent = 0; + m_first_attribute = 0; + } + + private: + + /////////////////////////////////////////////////////////////////////////// + // Restrictions + + // No copying + xml_node(const xml_node &); + void operator =(const xml_node &); + + /////////////////////////////////////////////////////////////////////////// + // Data members + + // Note that some of the pointers below have UNDEFINED values if certain other pointers are 0. + // This is required for maximum performance, as it allows the parser to omit initialization of + // unneded/redundant values. + // + // The rules are as follows: + // 1. first_node and first_attribute contain valid pointers, or 0 if node has no children/attributes respectively + // 2. last_node and last_attribute are valid only if node has at least one child/attribute respectively, otherwise they contain garbage + // 3. prev_sibling and next_sibling are valid only if node has a parent, otherwise they contain garbage + + node_type m_type; // Type of node; always valid + xml_node *m_first_node; // Pointer to first child node, or 0 if none; always valid + xml_node *m_last_node; // Pointer to last child node, or 0 if none; this value is only valid if m_first_node is non-zero + xml_attribute *m_first_attribute; // Pointer to first attribute of node, or 0 if none; always valid + xml_attribute *m_last_attribute; // Pointer to last attribute of node, or 0 if none; this value is only valid if m_first_attribute is non-zero + xml_node *m_prev_sibling; // Pointer to previous sibling of node, or 0 if none; this value is only valid if m_parent is non-zero + xml_node *m_next_sibling; // Pointer to next sibling of node, or 0 if none; this value is only valid if m_parent is non-zero + + }; + + /////////////////////////////////////////////////////////////////////////// + // XML document + + //! This class represents root of the DOM hierarchy. + //! It is also an xml_node and a memory_pool through public inheritance. + //! Use parse() function to build a DOM tree from a zero-terminated XML text string. + //! parse() function allocates memory for nodes and attributes by using functions of xml_document, + //! which are inherited from memory_pool. + //! To access root node of the document, use the document itself, as if it was an xml_node. + //! \param Ch Character type to use. + template + class xml_document: public xml_node, public memory_pool + { + + public: + + //! Constructs empty XML document + xml_document() + : xml_node(node_document) + { + } + + //! Parses zero-terminated XML string according to given flags. + //! Passed string will be modified by the parser, unless rapidxml::parse_non_destructive flag is used. + //! The string must persist for the lifetime of the document. + //! In case of error, rapidxml::parse_error exception will be thrown. + //!

+ //! If you want to parse contents of a file, you must first load the file into the memory, and pass pointer to its beginning. + //! Make sure that data is zero-terminated. + //!

+ //! Document can be parsed into multiple times. + //! Each new call to parse removes previous nodes and attributes (if any), but does not clear memory pool. + //! \param text XML data to parse; pointer is non-const to denote fact that this data may be modified by the parser. + template + void parse(Ch *text) + { + assert(text); + + // Remove current contents + this->remove_all_nodes(); + this->remove_all_attributes(); + + // Parse BOM, if any + parse_bom(text); + + // Parse children + while (1) + { + // Skip whitespace before node + skip(text); + if (*text == 0) + break; + + // Parse and append new child + if (*text == Ch('<')) + { + ++text; // Skip '<' + if (xml_node *node = parse_node(text)) + this->append_node(node); + } + else + RAPIDXML_PARSE_ERROR("expected <", text); + } + + } + + //! Clears the document by deleting all nodes and clearing the memory pool. + //! All nodes owned by document pool are destroyed. + void clear() + { + this->remove_all_nodes(); + this->remove_all_attributes(); + memory_pool::clear(); + } + + private: + + /////////////////////////////////////////////////////////////////////// + // Internal character utility functions + + // Detect whitespace character + struct whitespace_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_whitespace[static_cast(ch)]; + } + }; + + // Detect node name character + struct node_name_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_node_name[static_cast(ch)]; + } + }; + + // Detect attribute name character + struct attribute_name_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_attribute_name[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) + struct text_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) that does not require processing + struct text_pure_no_ws_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text_pure_no_ws[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) that does not require processing + struct text_pure_with_ws_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text_pure_with_ws[static_cast(ch)]; + } + }; + + // Detect attribute value character + template + struct attribute_value_pred + { + static unsigned char test(Ch ch) + { + if (Quote == Ch('\'')) + return internal::lookup_tables<0>::lookup_attribute_data_1[static_cast(ch)]; + if (Quote == Ch('\"')) + return internal::lookup_tables<0>::lookup_attribute_data_2[static_cast(ch)]; + return 0; // Should never be executed, to avoid warnings on Comeau + } + }; + + // Detect attribute value character + template + struct attribute_value_pure_pred + { + static unsigned char test(Ch ch) + { + if (Quote == Ch('\'')) + return internal::lookup_tables<0>::lookup_attribute_data_1_pure[static_cast(ch)]; + if (Quote == Ch('\"')) + return internal::lookup_tables<0>::lookup_attribute_data_2_pure[static_cast(ch)]; + return 0; // Should never be executed, to avoid warnings on Comeau + } + }; + + // Insert coded character, using UTF8 or 8-bit ASCII + template + static void insert_coded_character(Ch *&text, unsigned long code) + { + if (Flags & parse_no_utf8) + { + // Insert 8-bit ASCII character + // Todo: possibly verify that code is less than 256 and use replacement char otherwise? + text[0] = static_cast(code); + text += 1; + } + else + { + // Insert UTF8 sequence + if (code < 0x80) // 1 byte sequence + { + text[0] = static_cast(code); + text += 1; + } + else if (code < 0x800) // 2 byte sequence + { + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xC0); + text += 2; + } + else if (code < 0x10000) // 3 byte sequence + { + text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xE0); + text += 3; + } + else if (code < 0x110000) // 4 byte sequence + { + text[3] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xF0); + text += 4; + } + else // Invalid, only codes up to 0x10FFFF are allowed in Unicode + { + RAPIDXML_PARSE_ERROR("invalid numeric character entity", text); + } + } + } + + // Skip characters until predicate evaluates to true + template + static void skip(Ch *&text) + { + Ch *tmp = text; + while (StopPred::test(*tmp)) + ++tmp; + text = tmp; + } + + // Skip characters until predicate evaluates to true while doing the following: + // - replacing XML character entity references with proper characters (' & " < > &#...;) + // - condensing whitespace sequences to single space character + template + static Ch *skip_and_expand_character_refs(Ch *&text) + { + // If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip + if (Flags & parse_no_entity_translation && + !(Flags & parse_normalize_whitespace) && + !(Flags & parse_trim_whitespace)) + { + skip(text); + return text; + } + + // Use simple skip until first modification is detected + skip(text); + + // Use translation skip + Ch *src = text; + Ch *dest = src; + while (StopPred::test(*src)) + { + // If entity translation is enabled + if (!(Flags & parse_no_entity_translation)) + { + // Test if replacement is needed + if (src[0] == Ch('&')) + { + switch (src[1]) + { + + // & ' + case Ch('a'): + if (src[2] == Ch('m') && src[3] == Ch('p') && src[4] == Ch(';')) + { + *dest = Ch('&'); + ++dest; + src += 5; + continue; + } + if (src[2] == Ch('p') && src[3] == Ch('o') && src[4] == Ch('s') && src[5] == Ch(';')) + { + *dest = Ch('\''); + ++dest; + src += 6; + continue; + } + break; + + // " + case Ch('q'): + if (src[2] == Ch('u') && src[3] == Ch('o') && src[4] == Ch('t') && src[5] == Ch(';')) + { + *dest = Ch('"'); + ++dest; + src += 6; + continue; + } + break; + + // > + case Ch('g'): + if (src[2] == Ch('t') && src[3] == Ch(';')) + { + *dest = Ch('>'); + ++dest; + src += 4; + continue; + } + break; + + // < + case Ch('l'): + if (src[2] == Ch('t') && src[3] == Ch(';')) + { + *dest = Ch('<'); + ++dest; + src += 4; + continue; + } + break; + + // &#...; - assumes ASCII + case Ch('#'): + if (src[2] == Ch('x')) + { + unsigned long code = 0; + src += 3; // Skip &#x + while (1) + { + unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; + if (digit == 0xFF) + break; + code = code * 16 + digit; + ++src; + } + insert_coded_character(dest, code); // Put character in output + } + else + { + unsigned long code = 0; + src += 2; // Skip &# + while (1) + { + unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; + if (digit == 0xFF) + break; + code = code * 10 + digit; + ++src; + } + insert_coded_character(dest, code); // Put character in output + } + if (*src == Ch(';')) + ++src; + else + RAPIDXML_PARSE_ERROR("expected ;", src); + continue; + + // Something else + default: + // Ignore, just copy '&' verbatim + break; + + } + } + } + + // If whitespace condensing is enabled + if (Flags & parse_normalize_whitespace) + { + // Test if condensing is needed + if (whitespace_pred::test(*src)) + { + *dest = Ch(' '); ++dest; // Put single space in dest + ++src; // Skip first whitespace char + // Skip remaining whitespace chars + while (whitespace_pred::test(*src)) + ++src; + continue; + } + } + + // No replacement, only copy character + *dest++ = *src++; + + } + + // Return new end + text = src; + return dest; + + } + + /////////////////////////////////////////////////////////////////////// + // Internal parsing functions + + // Parse BOM, if any + template + void parse_bom(Ch *&text) + { + // UTF-8? + if (static_cast(text[0]) == 0xEF && + static_cast(text[1]) == 0xBB && + static_cast(text[2]) == 0xBF) + { + text += 3; // Skup utf-8 bom + } + } + + // Parse XML declaration ( + xml_node *parse_xml_declaration(Ch *&text) + { + // If parsing of declaration is disabled + if (!(Flags & parse_declaration_node)) + { + // Skip until end of declaration + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 2; // Skip '?>' + return 0; + } + + // Create declaration + xml_node *declaration = this->allocate_node(node_declaration); + declaration->offset(text); + + // Skip whitespace before attributes or ?> + skip(text); + + // Parse declaration attributes + parse_node_attributes(text, declaration); + + // Skip ?> + if (text[0] != Ch('?') || text[1] != Ch('>')) + RAPIDXML_PARSE_ERROR("expected ?>", text); + text += 2; + + return declaration; + } + + // Parse XML comment (' + return 0; // Do not produce comment node + } + + // Remember value start + Ch *value = text; + + // Skip until end of comment + while (text[0] != Ch('-') || text[1] != Ch('-') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Create comment node + xml_node *comment = this->allocate_node(node_comment); + comment->offset(value); + comment->value(value, text - value); + + // Place zero terminator after comment value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 3; // Skip '-->' + return comment; + } + + // Parse DOCTYPE + template + xml_node *parse_doctype(Ch *&text) + { + // Remember value start + Ch *value = text; + + // Skip to > + while (*text != Ch('>')) + { + // Determine character type + switch (*text) + { + + // If '[' encountered, scan for matching ending ']' using naive algorithm with depth + // This works for all W3C test files except for 2 most wicked + case Ch('['): + { + ++text; // Skip '[' + int depth = 1; + while (depth > 0) + { + switch (*text) + { + case Ch('['): ++depth; break; + case Ch(']'): --depth; break; + case 0: RAPIDXML_PARSE_ERROR("unexpected end of data", text); + } + ++text; + } + break; + } + + // Error on end of text + case Ch('\0'): + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + + // Other character, skip it + default: + ++text; + + } + } + + // If DOCTYPE nodes enabled + if (Flags & parse_doctype_node) + { + // Create a new doctype node + xml_node *doctype = this->allocate_node(node_doctype); + doctype->offset(value); + doctype->value(value, text - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 1; // skip '>' + return doctype; + } + else + { + text += 1; // skip '>' + return 0; + } + + } + + // Parse PI + template + xml_node *parse_pi(Ch *&text) + { + // If creation of PI nodes is enabled + if (Flags & parse_pi_nodes) + { + // Create pi node + xml_node *pi = this->allocate_node(node_pi); + pi->offset(text); + + // Extract PI target name + Ch *name = text; + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected PI target", text); + pi->name(name, text - name); + + // Skip whitespace between pi target and pi + skip(text); + + // Remember start of pi + Ch *value = text; + + // Skip to '?>' + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (*text == Ch('\0')) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Set pi value (verbatim, no entity expansion or whitespace normalization) + pi->value(value, text - value); + + // Place zero terminator after name and value + if (!(Flags & parse_no_string_terminators)) + { + pi->name()[pi->name_size()] = Ch('\0'); + pi->value()[pi->value_size()] = Ch('\0'); + } + + text += 2; // Skip '?>' + return pi; + } + else + { + // Skip to '?>' + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (*text == Ch('\0')) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 2; // Skip '?>' + return 0; + } + } + + // Parse and append data + // Return character that ends data. + // This is necessary because this character might have been overwritten by a terminating 0 + template + Ch parse_and_append_data(xml_node *node, Ch *&text, Ch *contents_start) + { + // Backup to contents start if whitespace trimming is disabled + if (!(Flags & parse_trim_whitespace)) + text = contents_start; + + // Skip until end of data + Ch *value = text, *end; + if (Flags & parse_normalize_whitespace) + end = skip_and_expand_character_refs(text); + else + end = skip_and_expand_character_refs(text); + + // Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after > + if (Flags & parse_trim_whitespace) + { + if (Flags & parse_normalize_whitespace) + { + // Whitespace is already condensed to single space characters by skipping function, so just trim 1 char off the end + if (*(end - 1) == Ch(' ')) + --end; + } + else + { + // Backup until non-whitespace character is found + while (whitespace_pred::test(*(end - 1))) + --end; + } + } + + // If characters are still left between end and value (this test is only necessary if normalization is enabled) + // Create new data node + if (!(Flags & parse_no_data_nodes)) + { + xml_node *data = this->allocate_node(node_data); + data->value(value, end - value); + node->append_node(data); + } + + // Add data to parent node if no data exists yet + if (!(Flags & parse_no_element_values)) + if (*node->value() == Ch('\0')) + node->value(value, end - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + { + Ch ch = *text; + *end = Ch('\0'); + return ch; // Return character that ends data; this is required because zero terminator overwritten it + } + + // Return character that ends data + return *text; + } + + // Parse CDATA + template + xml_node *parse_cdata(Ch *&text) + { + // If CDATA is disabled + if (Flags & parse_no_data_nodes) + { + // Skip until end of cdata + while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 3; // Skip ]]> + return 0; // Do not produce CDATA node + } + + // Skip until end of cdata + Ch *value = text; + while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Create new cdata node + xml_node *cdata = this->allocate_node(node_cdata); + cdata->offset(value); + cdata->value(value, text - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 3; // Skip ]]> + return cdata; + } + + // Parse element node + template + xml_node *parse_element(Ch *&text) + { + // Create element node + xml_node *element = this->allocate_node(node_element); + element->offset(text); + + // Extract element name + Ch *name = text; + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected element name", text); + element->name(name, text - name); + + // Skip whitespace between element name and attributes or > + skip(text); + + // Parse attributes, if any + parse_node_attributes(text, element); + + // Determine ending type + if (*text == Ch('>')) + { + ++text; + parse_node_contents(text, element); + } + else if (*text == Ch('/')) + { + ++text; + if (*text != Ch('>')) + RAPIDXML_PARSE_ERROR("expected >", text); + ++text; + } + else + RAPIDXML_PARSE_ERROR("expected >", text); + + // Place zero terminator after name + if (!(Flags & parse_no_string_terminators)) + element->name()[element->name_size()] = Ch('\0'); + + // Return parsed element + return element; + } + + // Determine node type, and parse it + template + xml_node *parse_node(Ch *&text) + { + // Parse proper node type + switch (text[0]) + { + + // <... + default: + // Parse and append element node + return parse_element(text); + + // (text); + } + else + { + // Parse PI + return parse_pi(text); + } + + // (text); + } + break; + + // (text); + } + break; + + // (text); + } + + } // switch + + // Attempt to skip other, unrecognized node types starting with ')) + { + if (*text == 0) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + ++text; // Skip '>' + return 0; // No node recognized + + } + } + + // Parse contents of the node - children, data etc. + template + void parse_node_contents(Ch *&text, xml_node *node) + { + // For all children and text + while (1) + { + // Skip whitespace between > and node contents + Ch *contents_start = text; // Store start of node contents before whitespace is skipped + skip(text); + Ch next_char = *text; + + // After data nodes, instead of continuing the loop, control jumps here. + // This is because zero termination inside parse_and_append_data() function + // would wreak havoc with the above code. + // Also, skipping whitespace after data nodes is unnecessary. + after_data_node: + + // Determine what comes next: node closing, child node, data node, or 0? + switch (next_char) + { + + // Node closing or child node + case Ch('<'): + if (text[1] == Ch('/')) + { + // Node closing + text += 2; // Skip '(text); + if (!internal::compare(node->name(), node->name_size(), closing_name, text - closing_name, true)) + RAPIDXML_PARSE_ERROR("invalid closing tag name", text); + } + else + { + // No validation, just skip name + skip(text); + } + // Skip remaining whitespace after node name + skip(text); + if (*text != Ch('>')) + RAPIDXML_PARSE_ERROR("expected >", text); + ++text; // Skip '>' + return; // Node closed, finished parsing contents + } + else + { + // Child node + ++text; // Skip '<' + if (xml_node *child = parse_node(text)) + node->append_node(child); + } + break; + + // End of data - error + case Ch('\0'): + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + + // Data node + default: + next_char = parse_and_append_data(node, text, contents_start); + goto after_data_node; // Bypass regular processing after data nodes + + } + } + } + + // Parse XML attributes of the node + template + void parse_node_attributes(Ch *&text, xml_node *node) + { + // For all attributes + while (attribute_name_pred::test(*text)) + { + // Extract attribute name + Ch *name = text; + ++text; // Skip first character of attribute name + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected attribute name", name); + + // Create new attribute + xml_attribute *attribute = this->allocate_attribute(); + attribute->name(name, text - name); + node->append_attribute(attribute); + + // Skip whitespace after attribute name + skip(text); + + // Skip = + if (*text != Ch('=')) + RAPIDXML_PARSE_ERROR("expected =", text); + ++text; + + // Add terminating zero after name + if (!(Flags & parse_no_string_terminators)) + attribute->name()[attribute->name_size()] = 0; + + // Skip whitespace after = + skip(text); + + // Skip quote and remember if it was ' or " + Ch quote = *text; + if (quote != Ch('\'') && quote != Ch('"')) + RAPIDXML_PARSE_ERROR("expected ' or \"", text); + ++text; + + // Extract attribute value and expand char refs in it + Ch *value = text, *end; + const int AttFlags = Flags & ~parse_normalize_whitespace; // No whitespace normalization in attributes + if (quote == Ch('\'')) + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + else + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + + // Set attribute value + attribute->value(value, end - value); + + // Make sure that end quote is present + if (*text != quote) + RAPIDXML_PARSE_ERROR("expected ' or \"", text); + ++text; // Skip quote + + // Add terminating zero after value + if (!(Flags & parse_no_string_terminators)) + attribute->value()[attribute->value_size()] = 0; + + // Skip whitespace after attribute value + skip(text); + } + } + + }; + + //! \cond internal + namespace internal + { + + // Whitespace (space \n \r \t) + template + const unsigned char lookup_tables::lookup_whitespace[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // F + }; + + // Node name (anything but space \n \r \t / > ? \0) + template + const unsigned char lookup_tables::lookup_node_name[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) (anything but < \0) + template + const unsigned char lookup_tables::lookup_text[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) that does not require processing when ws normalization is disabled + // (anything but < \0 &) + template + const unsigned char lookup_tables::lookup_text_pure_no_ws[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) that does not require processing when ws normalizationis is enabled + // (anything but < \0 & space \n \r \t) + template + const unsigned char lookup_tables::lookup_text_pure_with_ws[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute name (anything but space \n \r \t / < > = ? ! \0) + template + const unsigned char lookup_tables::lookup_attribute_name[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with single quote (anything but ' \0) + template + const unsigned char lookup_tables::lookup_attribute_data_1[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with single quote that does not require processing (anything but ' \0 &) + template + const unsigned char lookup_tables::lookup_attribute_data_1_pure[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with double quote (anything but " \0) + template + const unsigned char lookup_tables::lookup_attribute_data_2[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with double quote that does not require processing (anything but " \0 &) + template + const unsigned char lookup_tables::lookup_attribute_data_2_pure[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Digits (dec and hex, 255 denotes end of numeric character reference) + template + const unsigned char lookup_tables::lookup_digits[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3 + 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5 + 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 // F + }; + + // Upper case conversion + template + const unsigned char lookup_tables::lookup_upcase[256] = + { + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A B C D E F + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, // 0 + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, // 1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, // 2 + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // 3 + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 4 + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, // 5 + 96, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 6 + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 123,124,125,126,127, // 7 + 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, // 8 + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, // 9 + 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175, // A + 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, // B + 192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207, // C + 208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223, // D + 224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239, // E + 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255 // F + }; + } + //! \endcond + +} + +// Undefine internal macros +#undef RAPIDXML_PARSE_ERROR + +// On MSVC, restore warnings state +#ifdef _MSC_VER + #pragma warning(pop) +#endif + +#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp b/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp new file mode 100644 index 0000000..68cf57f --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp @@ -0,0 +1,295 @@ +#ifndef RAPIDXML_ITERATORS_HPP_INCLUDED +#define RAPIDXML_ITERATORS_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/15 23:02:39 $ +//! \file rapidxml_iterators.hpp This file contains rapidxml iterators + +#include "rapidxml.hpp" + +namespace rapidxml +{ + const unsigned int iterate_check_name = 1 << 0; + const unsigned int iterate_case_sensitive = 1 << 1; + + //! Iterator of child nodes of xml_node + template + class node_iterator + { + public: + typedef xml_node *value_type; + typedef const value_type& reference; + typedef xml_node *pointer; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + + node_iterator() + : m_cur(0) + , m_prev(0) + , m_flags(0) + { + } + + node_iterator(xml_node* node, xml_node* prev, + unsigned char flags) + : m_cur(node) + , m_prev(prev) + , m_flags(flags) + { + } + + reference operator*() const + { + assert(m_cur); + return m_cur; + } + + pointer operator->() const + { + assert(m_cur); + return m_cur; + } + + node_iterator& operator++() + { + increment(); + return *this; + } + + node_iterator operator++(int) + { + node_iterator tmp = *this; + increment(); + return tmp; + } + + node_iterator& operator--() + { + decrement(); + return *this; + } + + node_iterator operator--(int) + { + node_iterator tmp = *this; + decrement(); + return tmp; + } + + bool operator==(const node_iterator &rhs) const + { + return m_cur == rhs.m_cur; + } + + bool operator!=(const node_iterator &rhs) const + { + return m_cur != rhs.m_cur; + } + + private: + void increment() + { + assert(m_cur && "Attempted to increment end iterator"); + m_prev = m_cur; + + if (m_flags & iterate_check_name) + m_cur = m_cur->next_sibling( + m_cur->name(), m_cur->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_cur = m_cur->next_sibling(); + } + + void decrement() + { + assert(m_prev && "Attempted to decrement begin iterator"); + m_cur = m_prev; + + if (m_flags & iterate_check_name) + m_prev = m_prev->previous_sibling( + m_prev->name(), m_prev->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_prev = m_prev->previous_sibling(); + } + + xml_node *m_cur; + xml_node *m_prev; + unsigned char m_flags; + }; + + //! Iterator of child attributes of xml_node + template + class attribute_iterator + { + public: + typedef xml_attribute *value_type; + typedef const value_type& reference; + typedef xml_attribute *pointer; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + + attribute_iterator() + : m_cur(0) + , m_prev(0) + , m_flags(0) + { + } + + attribute_iterator(xml_attribute* attr, xml_attribute* prev, + unsigned char flags) + : m_cur(attr) + , m_prev(prev) + , m_flags(flags) + { + } + + reference operator*() const + { + assert(m_cur); + return m_cur; + } + + pointer operator->() const + { + assert(m_cur); + return m_cur; + } + + attribute_iterator& operator++() + { + increment(); + return *this; + } + + attribute_iterator operator++(int) + { + attribute_iterator tmp = *this; + increment(); + return tmp; + } + + attribute_iterator& operator--() + { + decrement(); + return *this; + } + + attribute_iterator operator--(int) + { + attribute_iterator tmp = *this; + decrement(); + return tmp; + } + + bool operator==(const attribute_iterator &rhs) const + { + return m_cur == rhs.m_cur; + } + + bool operator!=(const attribute_iterator &rhs) const + { + return m_cur != rhs.m_cur; + } + + private: + void increment() + { + assert(m_cur && "Attempted to increment end iterator"); + m_prev = m_cur; + + if (m_flags & iterate_check_name) + m_cur = m_cur->next_attribute( + m_cur->name(), m_cur->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_cur = m_cur->next_attribute(); + } + + void decrement() + { + assert(m_prev && "Attempted to decrement begin iterator"); + m_cur = m_prev; + + if (m_flags & iterate_check_name) + m_prev = m_prev->previous_attribute( + m_prev->name(), m_prev->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_prev = m_prev->previous_attribute(); + } + + xml_attribute* m_cur; + xml_attribute* m_prev; + unsigned char m_flags; + }; + + // Range-based for loop support + template + class iterator_range + { + public: + typedef Iterator const_iterator; + typedef Iterator iterator; + + iterator_range(Iterator first, Iterator last) + : m_first(first) + , m_last(last) + { + } + + Iterator begin() const { return m_first; } + Iterator end() const { return m_last; } + + private: + Iterator m_first; + Iterator m_last; + }; + + template + iterator_range> nodes(const xml_node* node, + const Ch* name = 0, + std::size_t name_size = 0, + bool case_sensitive = true) + { + unsigned char flags = 0; + if (name) + flags |= iterate_check_name; + if (case_sensitive) + flags |= iterate_case_sensitive; + + xml_node* first = + node->first_node(name, name_size, case_sensitive); + xml_node* last = first ? + node->last_node(name, name_size, case_sensitive) : nullptr; + + node_iterator begin(first, 0, flags); + node_iterator end(0, last, flags); + return iterator_range>(begin, end); + } + + template + iterator_range> attributes(const xml_node* node, + const Ch *name = 0, + std::size_t name_size = 0, + bool case_sensitive = true) + { + unsigned char flags = 0; + if (name) + flags |= iterate_check_name; + if (case_sensitive) + flags |= iterate_case_sensitive; + + xml_attribute* first = + node->first_attribute(name, name_size, case_sensitive); + xml_attribute* last = + node->last_attribute(name, name_size, case_sensitive); + + attribute_iterator begin(first, 0, flags); + attribute_iterator end(0, last, flags); + return iterator_range>(begin, end); + } +} + +#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_print.hpp b/crates/joko_package/vendor/rapid/rapidxml_print.hpp new file mode 100644 index 0000000..ae80e1f --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapidxml_print.hpp @@ -0,0 +1,422 @@ +#ifndef RAPIDXML_PRINT_HPP_INCLUDED +#define RAPIDXML_PRINT_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/13 01:46:17 $ +//! \file rapidxml_print.hpp This file contains rapidxml printer implementation + +#include "rapidxml.hpp" + +// Only include streams if not disabled +#ifndef RAPIDXML_NO_STREAMS + #include + #include +#endif + +namespace rapidxml +{ + + /////////////////////////////////////////////////////////////////////// + // Printing flags + + const int print_no_indenting = 0x1; //!< Printer flag instructing the printer to suppress indenting of XML. See print() function. + + /////////////////////////////////////////////////////////////////////// + // Internal + + //! \cond internal + namespace internal + { + + /////////////////////////////////////////////////////////////////////////// + // Internal character operations + + // Copy characters from given range to given output iterator + template + inline OutIt copy_chars(const Ch *begin, const Ch *end, OutIt out) + { + while (begin != end) + *out++ = *begin++; + return out; + } + + // Copy characters from given range to given output iterator and expand + // characters into references (< > ' " &) + template + inline OutIt copy_and_expand_chars(const Ch *begin, const Ch *end, Ch noexpand, OutIt out) + { + while (begin != end) + { + if (*begin == noexpand) + { + *out++ = *begin; // No expansion, copy character + } + else + { + switch (*begin) + { + case Ch('<'): + *out++ = Ch('&'); *out++ = Ch('l'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('>'): + *out++ = Ch('&'); *out++ = Ch('g'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('\''): + *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('p'); *out++ = Ch('o'); *out++ = Ch('s'); *out++ = Ch(';'); + break; + case Ch('"'): + *out++ = Ch('&'); *out++ = Ch('q'); *out++ = Ch('u'); *out++ = Ch('o'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('&'): + *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('m'); *out++ = Ch('p'); *out++ = Ch(';'); + break; + default: + *out++ = *begin; // No expansion, copy character + } + } + ++begin; // Step to next character + } + return out; + } + + // Fill given output iterator with repetitions of the same character + template + inline OutIt fill_chars(OutIt out, int n, Ch ch) + { + for (int i = 0; i < n; ++i) + *out++ = ch; + return out; + } + + // Find character + template + inline bool find_char(const Ch *begin, const Ch *end) + { + while (begin != end) + if (*begin++ == ch) + return true; + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Internal printing operations + + template + OutIt print_node(OutIt out, const xml_node *node, int flags, int indent); + + // Print children of the node + template + inline OutIt print_children(OutIt out, const xml_node *node, int flags, int indent) + { + for (xml_node *child = node->first_node(); child; child = child->next_sibling()) + out = print_node(out, child, flags, indent); + return out; + } + + // Print attributes of the node + template + inline OutIt print_attributes(OutIt out, const xml_node *node, int flags) + { + for (xml_attribute *attribute = node->first_attribute(); attribute; attribute = attribute->next_attribute()) + { + if (attribute->name() && attribute->value()) + { + // Print attribute name + *out = Ch(' '), ++out; + out = copy_chars(attribute->name(), attribute->name() + attribute->name_size(), out); + *out = Ch('='), ++out; + // Print attribute value using appropriate quote type + if (find_char(attribute->value(), attribute->value() + attribute->value_size())) + { + *out = Ch('\''), ++out; + out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('"'), out); + *out = Ch('\''), ++out; + } + else + { + *out = Ch('"'), ++out; + out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('\''), out); + *out = Ch('"'), ++out; + } + } + } + return out; + } + + // Print data node + template + inline OutIt print_data_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_data); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); + return out; + } + + // Print data node + template + inline OutIt print_cdata_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_cdata); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'); ++out; + *out = Ch('!'); ++out; + *out = Ch('['); ++out; + *out = Ch('C'); ++out; + *out = Ch('D'); ++out; + *out = Ch('A'); ++out; + *out = Ch('T'); ++out; + *out = Ch('A'); ++out; + *out = Ch('['); ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch(']'); ++out; + *out = Ch(']'); ++out; + *out = Ch('>'); ++out; + return out; + } + + // Print element node + template + inline OutIt print_element_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_element); + + // Print element name and attributes, if any + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + out = print_attributes(out, node, flags); + + // If node is childless + if (node->value_size() == 0 && !node->first_node()) + { + // Print childless node tag ending + *out = Ch('/'), ++out; + *out = Ch('>'), ++out; + } + else + { + // Print normal node tag ending + *out = Ch('>'), ++out; + + // Test if node contains a single data node only (and no other nodes) + xml_node *child = node->first_node(); + if (!child) + { + // If node has no children, only print its value without indenting + out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); + } + else if (child->next_sibling() == 0 && child->type() == node_data) + { + // If node has a sole data child, only print its value without indenting + out = copy_and_expand_chars(child->value(), child->value() + child->value_size(), Ch(0), out); + } + else + { + // Print all children with full indenting + if (!(flags & print_no_indenting)) + *out = Ch('\n'), ++out; + out = print_children(out, node, flags, indent + 1); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + } + + // Print node end + *out = Ch('<'), ++out; + *out = Ch('/'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + *out = Ch('>'), ++out; + } + return out; + } + + // Print declaration node + template + inline OutIt print_declaration_node(OutIt out, const xml_node *node, int flags, int indent) + { + // Print declaration start + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('?'), ++out; + *out = Ch('x'), ++out; + *out = Ch('m'), ++out; + *out = Ch('l'), ++out; + + // Print attributes + out = print_attributes(out, node, flags); + + // Print declaration end + *out = Ch('?'), ++out; + *out = Ch('>'), ++out; + + return out; + } + + // Print comment node + template + inline OutIt print_comment_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_comment); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('!'), ++out; + *out = Ch('-'), ++out; + *out = Ch('-'), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('-'), ++out; + *out = Ch('-'), ++out; + *out = Ch('>'), ++out; + return out; + } + + // Print doctype node + template + inline OutIt print_doctype_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_doctype); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('!'), ++out; + *out = Ch('D'), ++out; + *out = Ch('O'), ++out; + *out = Ch('C'), ++out; + *out = Ch('T'), ++out; + *out = Ch('Y'), ++out; + *out = Ch('P'), ++out; + *out = Ch('E'), ++out; + *out = Ch(' '), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('>'), ++out; + return out; + } + + // Print pi node + template + inline OutIt print_pi_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_pi); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('?'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + *out = Ch(' '), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('?'), ++out; + *out = Ch('>'), ++out; + return out; + } + + // Print node + template + inline OutIt print_node(OutIt out, const xml_node *node, int flags, int indent) + { + // Print proper node type + switch (node->type()) + { + // Document + case node_document: + out = print_children(out, node, flags, indent); + break; + + // Element + case node_element: + out = print_element_node(out, node, flags, indent); + break; + + // Data + case node_data: + out = print_data_node(out, node, flags, indent); + break; + + // CDATA + case node_cdata: + out = print_cdata_node(out, node, flags, indent); + break; + + // Declaration + case node_declaration: + out = print_declaration_node(out, node, flags, indent); + break; + + // Comment + case node_comment: + out = print_comment_node(out, node, flags, indent); + break; + + // Doctype + case node_doctype: + out = print_doctype_node(out, node, flags, indent); + break; + + // Pi + case node_pi: + out = print_pi_node(out, node, flags, indent); + break; + + // Unknown + default: + assert(0); + break; + } + + // If indenting not disabled, add line break after node + if (!(flags & print_no_indenting)) + *out = Ch('\n'), ++out; + + // Return modified iterator + return out; + } + } + //! \endcond + + /////////////////////////////////////////////////////////////////////////// + // Printing + + //! Prints XML to given output iterator. + //! \param out Output iterator to print to. + //! \param node Node to be printed. Pass xml_document to print entire document. + //! \param flags Flags controlling how XML is printed. + //! \return Output iterator pointing to position immediately after last character of printed text. + template + inline OutIt print(OutIt out, const xml_node &node, int flags = 0) + { + return internal::print_node(out, &node, flags, 0); + } + +#ifndef RAPIDXML_NO_STREAMS + + //! Prints XML to given output stream. + //! \param out Output stream to print to. + //! \param node Node to be printed. Pass xml_document to print entire document. + //! \param flags Flags controlling how XML is printed. + //! \return Output stream. + template + inline std::basic_ostream &print(std::basic_ostream &out, const xml_node &node, int flags = 0) + { + print(std::ostream_iterator(out), node, flags); + return out; + } + + //! Prints formatted XML to given output stream. Uses default printing flags. Use print() function to customize printing process. + //! \param out Output stream to print to. + //! \param node Node to be printed. + //! \return Output stream. + template + inline std::basic_ostream &operator <<(std::basic_ostream &out, const xml_node &node) + { + return print(out, node); + } + +#endif + +} + +#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_utils.hpp b/crates/joko_package/vendor/rapid/rapidxml_utils.hpp new file mode 100644 index 0000000..91cf83e --- /dev/null +++ b/crates/joko_package/vendor/rapid/rapidxml_utils.hpp @@ -0,0 +1,56 @@ +#ifndef RAPIDXML_UTILS_HPP_INCLUDED +#define RAPIDXML_UTILS_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/15 23:02:39 $ +//! \file rapidxml_utils.hpp This file contains high-level rapidxml utilities that can be useful +//! in certain simple scenarios. They should probably not be used if maximizing performance is the main objective. + +#include "rapidxml.hpp" +#include + +namespace rapidxml +{ + //! Counts children of node. Time complexity is O(n). + //! \return Number of children of node + template + inline std::size_t count_children(const xml_node *node, + const Ch* name = 0, + std::size_t name_size = 0) + { + if (name && name_size == 0) + name_size = internal::measure(name); + + xml_node *child = node->first_node(name, name_size); + std::size_t count = 0; + while (child) + { + ++count; + child = child->next_sibling(name, name_size); + } + return count; + } + + //! Counts attributes of node. Time complexity is O(n). + //! \return Number of attributes of node + template + inline std::size_t count_attributes(const xml_node *node, + const Ch* name = 0, + std::size_t name_size = 0) + { + if (name && name_size == 0) + name_size = internal::measure(name); + + xml_attribute *attr = node->first_attribute(name, name_size); + std::size_t count = 0; + while (attr) + { + ++count; + attr = attr->next_attribute(name, name_size); + } + return count; + } +} + +#endif diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml index dfaa519..e0b3e34 100644 --- a/crates/joko_render/Cargo.toml +++ b/crates/joko_render/Cargo.toml @@ -17,4 +17,5 @@ egui_window_glfw_passthrough = { version = "0.8" } jokolink = { path = "../jokolink" } -joko_marker_format = { path = "../joko_marker_format" } +joko_render_models = { path = "../joko_render_models" } + diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index b0077b6..28ec05e 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -1,11 +1,12 @@ -use std::sync::Arc; - use egui::ahash::HashMap; use egui_render_three_d::{ three_d::{context::*, Context, HasContext}, GpuTexture, }; -use joko_marker_format::message::{MarkerVertex, MarkerObject, TrailObject}; +use joko_render_models::{ + marker::{MarkerVertex, MarkerObject}, + trail::TrailObject +}; use tracing::{error, info, trace, warn}; use crate::gl_error; diff --git a/crates/joko_render/src/gl.rs b/crates/joko_render/src/gl.rs new file mode 100644 index 0000000..9724f2f --- /dev/null +++ b/crates/joko_render/src/gl.rs @@ -0,0 +1,9 @@ +#[macro_export] +macro_rules! gl_error { + ($gl:expr) => {{ + let e = $gl.get_error(); + if e != egui_render_three_d::three_d::context::NO_ERROR { + tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); + } + }}; +} \ No newline at end of file diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index b6c5ba3..0b02e64 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -1,181 +1,3 @@ +pub mod gl; pub mod billboard; -use billboard::BillBoardRenderer; -use egui_render_three_d::three_d; -use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; -use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; -use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; -use egui_render_three_d::three_d::Camera; -use egui_render_three_d::three_d::HasContext; -use egui_render_three_d::three_d::ScissorBox; -use egui_render_three_d::three_d::Viewport; -use egui_render_three_d::ThreeDBackend; -use egui_render_three_d::ThreeDConfig; -use egui_window_glfw_passthrough::GlfwBackend; -use glam::Mat4; -use jokolink::MumbleLink; -use three_d::prelude::*; - - -use joko_marker_format::message::{MarkerObject, TrailObject}; - -#[macro_export] -macro_rules! gl_error { - ($gl:expr) => {{ - let e = $gl.get_error(); - if e != egui_render_three_d::three_d::context::NO_ERROR { - tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); - } - }}; -} - -pub struct JokoRenderer { - pub view_proj: Mat4, - pub cam_pos: glam::Vec3, - pub camera: Camera, - pub viewport: Viewport, - pub has_link: bool, - pub billboard_renderer: BillBoardRenderer, - pub gl: egui_render_three_d::ThreeDBackend, -} - -impl JokoRenderer { - pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { - let glfw = glfw_backend.glfw.clone(); - let backend = ThreeDBackend::new( - ThreeDConfig { - glow_config: Default::default(), - }, - |s| glfw.get_proc_address_raw(s), - //glfw_backend.window.raw_window_handle(), - glfw_backend.framebuffer_size_physical, - ); - let viewport = Viewport { - x: 0, - y: 0, - width: glfw_backend.framebuffer_size_physical[0], - height: glfw_backend.framebuffer_size_physical[1], - }; - let gl = &backend.context; - unsafe { gl_error!(gl) }; - let billboard_renderer = BillBoardRenderer::new(gl); - unsafe { gl_error!(gl) }; - Self { - viewport, - view_proj: Default::default(), - camera: Camera::new_perspective( - viewport, - [0.0, 0.0, 0.0].into(), - [0.0, 0.0, 0.0].into(), - Vector3::unit_y(), - Deg(90.0), - 1.0, - 5000.0, - ), - has_link: false, - gl: backend, - billboard_renderer, - cam_pos: Default::default(), - } - } - pub fn get_z_near() -> f32 { - 1.0 - } - pub fn get_z_far() -> f32 { - 1000.0 - } - pub fn swap(&mut self) { - self.billboard_renderer.swap(); - } - pub fn tick(&mut self, link: Option<&MumbleLink>) { - if let Some(link) = link { - let center = link.cam_pos + link.f_camera_front; - let camera = Camera::new_perspective( - self.viewport, - link.cam_pos.to_array().into(), - center.to_array().into(), - Vector3::unit_y(), - Rad(link.fov), - Self::get_z_near(), - Self::get_z_far(), - ); - self.camera = camera; - let view = Mat4::look_at_lh(link.cam_pos, center, glam::Vec3::Y); - let proj = Mat4::perspective_lh( - link.fov, - self.viewport.aspect(), - Self::get_z_near(), - Self::get_z_far(), - ); - self.view_proj = proj * view; - self.cam_pos = link.cam_pos; - self.has_link = true; - } else { - self.has_link = false; - } - } - pub fn extend_markers(&mut self, marker_objects: Vec) { - self.billboard_renderer.markers_wip.extend(marker_objects); - } - pub fn add_billboard(&mut self, marker_object: MarkerObject) { - self.billboard_renderer.markers_wip.push(marker_object); - } - - pub fn extend_trails(&mut self, trail_objects: Vec) { - self.billboard_renderer.trails_wip.extend(trail_objects); - } - pub fn add_trail(&mut self, trail_object: TrailObject) { - self.billboard_renderer.trails_wip.push(trail_object); - } - - pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - self.gl.prepare_frame(latest_framebuffer_size_getter); - unsafe { - let gl = self.gl.context.clone(); - gl_error!(gl); - // self.gl.context.set_viewport(self.viewport); - self.gl.context.set_scissor(ScissorBox::new_at_origo( - self.viewport.width, - self.viewport.height, - )); - self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); - self.gl - .context - .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); - gl_error!(gl); - } - } - - pub fn render_egui( - &mut self, - meshes: Vec, - textures_delta: egui::TexturesDelta, - logical_screen_size: [f32; 2], - ) { - if self.has_link { - self.billboard_renderer - .prepare_render_data(&self.gl.context); - self.billboard_renderer.render( - &self.gl.context, - self.cam_pos, - &self.view_proj, - &self.gl.glow_backend.painter.managed_textures, - ); - } - self.gl - .render_egui(meshes, textures_delta, logical_screen_size); - } - - pub fn present(&mut self) {} - - pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { - tracing::info!(?latest_size, "resizing framebuffer"); - - self.viewport = Viewport { - x: 0, - y: 0, - width: latest_size[0], - height: latest_size[1], - }; - self.gl.resize_framebuffer(latest_size); - } -} +pub mod renderer; \ No newline at end of file diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs new file mode 100644 index 0000000..6621559 --- /dev/null +++ b/crates/joko_render/src/renderer.rs @@ -0,0 +1,280 @@ +use crate::gl_error; +use crate::billboard::BillBoardRenderer; +use egui_render_three_d::three_d; +use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; +use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; +use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; +use egui_render_three_d::three_d::Camera; +use egui_render_three_d::three_d::HasContext; +use egui_render_three_d::three_d::ScissorBox; +use egui_render_three_d::three_d::Viewport; +use egui_render_three_d::ThreeDBackend; +use egui_render_three_d::ThreeDConfig; +use egui_window_glfw_passthrough::GlfwBackend; +use glam::Mat4; +use jokolink::MumbleLink; +use jokolink::UIState; +use three_d::prelude::*; + +use joko_render_models::{ + marker::MarkerObject, + trail::TrailObject, +}; + +pub struct JokoRenderer { + pub view_proj: Mat4, + pub cam_pos: glam::Vec3, + pub camera: Camera, + pub viewport: Viewport, + pub has_link: bool, + pub is_map_open: bool, + pub billboard_renderer: BillBoardRenderer, + pub gl: egui_render_three_d::ThreeDBackend, +} + +impl JokoRenderer { + pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { + let glfw = glfw_backend.glfw.clone(); + let backend = ThreeDBackend::new( + ThreeDConfig { + glow_config: Default::default(), + }, + |s| glfw.get_proc_address_raw(s), + //glfw_backend.window.raw_window_handle(), + glfw_backend.framebuffer_size_physical, + ); + let viewport = Viewport { + x: 0, + y: 0, + width: glfw_backend.framebuffer_size_physical[0], + height: glfw_backend.framebuffer_size_physical[1], + }; + let gl = &backend.context; + unsafe { gl_error!(gl) }; + let billboard_renderer = BillBoardRenderer::new(gl); + unsafe { gl_error!(gl) }; + Self { + viewport, + view_proj: Default::default(), + camera: Camera::new_perspective( + viewport, + [0.0, 0.0, 0.0].into(), + [0.0, 0.0, 0.0].into(), + Vector3::unit_y(), + Deg(90.0), + 1.0, + 5000.0, + ), + has_link: false, + is_map_open: false, + gl: backend, + billboard_renderer, + cam_pos: Default::default(), + } + } + + /* + CRect GetMinimapRectangle() +{ + int w = mumbleLink.miniMap.compassWidth; + int h = mumbleLink.miniMap.compassHeight; + + CRect pos; + CRect size = App->GetRoot()->GetClientRect(); + float scale = GetWindowTooSmallScale(); + + pos.x1 = int( size.Width() - w * scale ); + pos.x2 = size.Width(); + + + if ( mumbleLink.isMinimapTopRight ) + { + pos.y1 = 1; + pos.y2 = int( h * scale + 1 ); + } + else + { + int delta = 37; + if ( mumbleLink.uiSize == 0 ) + delta = 33; + if ( mumbleLink.uiSize == 2 ) + delta = 41; + if ( mumbleLink.uiSize == 3 ) + delta = 45; + + pos.y1 = int( size.Height() - h * scale - delta * scale ); + pos.y2 = int( size.Height() - delta * scale ); + } + + return pos; +} + */ + pub fn get_z_near() -> f32 { + 1.0 + } + pub fn get_z_far() -> f32 { + 1000.0 + } + pub fn swap(&mut self) { + self.billboard_renderer.swap(); + } + /* + //https://wiki.guildwars2.com/wiki/API:1/event_details#Coordinate_recalculation + fn _scale_coords(continent_rect, map_rect, coords){ + continent_width = continent_rect[1].x - continent_rect[0].x; + continent_height = continent_rect[1].y - continent_rect[0].y; + map_width = map_rect[1].x - map_rect[0].x; + map_height = map_rect[1].y - map_rect[0].y; + position_on_map_x = coords.x - map_rect[0].x; + position_on_map_y = coords.y - map_rect[1].y; + return [ + Math.round( continent_rect[0].x + ( 1 * position_on_map_x / map_width * continent_width ) ), + Math.round( continent_rect[0].y + (-1 * position_on_map_y / map_height * continent_height ) ) + ]; + } + */ + pub fn tick(&mut self, link: Option<&MumbleLink>) { + if let Some(link) = link { + //x positive => east + //y positive => ascention + //z positive => north + self.is_map_open = if let Some(ui_state) = link.ui_state { + ui_state.contains(UIState::IsMapOpen) + } else { + false + }; + + //TODO: change perspective is map is open + let center = link.cam_pos + link.f_camera_front; + let cam_pos = link.cam_pos; + /* + let map_pos_x = (link.player_x - link.map_center_x) / 1.64; + let map_pos_y = (link.map_center_y - link.player_y) / 1.64; + let center = if self.is_map_open { + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 100.0, + z: link.player_pos.z - map_pos_y, + } + } else { + link.cam_pos + link.f_camera_front //default old one + }; + + let client_width = (link.client_size.x) as f32; + let client_height = (link.client_size.y) as f32; + + let cam_pos = if self.is_map_open { + //TODO: validate values + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 101.0, + z: link.player_pos.z - map_pos_y, + } + }else { + link.cam_pos //default old one + };*/ + let camera = Camera::new_perspective( + self.viewport, + cam_pos.to_array().into(), + center.to_array().into(), + Vector3::unit_y(), + Rad(link.fov), + Self::get_z_near(), + Self::get_z_far(), + ); + self.camera = camera; + /* + is_map_open: + target camera direction: 0 -20 1 + have trails seen from further + have trails fatter drawing + + println!("client: {} {} {} {}", client_width, client_height, client_width.div(client_height), client_height.div(client_width)); + println!("map scale: {}", link.map_scale); + println!("map position: {} {}", map_pos_x, map_pos_y); + println!("cam: {} {} {}", cam_pos.x, cam_pos.y, cam_pos.z); + println!("center: {} {} {}", center.x, center.y, center.z); + println!("H: {}", cam_pos.y - center.y); + println!("player: {} {} {}", link.player_pos.x, link.player_pos.y, link.player_pos.z); + */ + + let view = Mat4::look_at_lh(cam_pos, center, glam::Vec3::Y); + let proj = Mat4::perspective_lh( + link.fov, + self.viewport.aspect(), + Self::get_z_near(), + Self::get_z_far(), + ); + self.view_proj = proj * view; + self.cam_pos = cam_pos; + self.has_link = true; + } else { + self.has_link = false; + } + } + pub fn extend_markers(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip.extend(marker_objects); + } + pub fn add_billboard(&mut self, marker_object: MarkerObject) { + self.billboard_renderer.markers_wip.push(marker_object); + } + + pub fn extend_trails(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip.extend(trail_objects); + } + pub fn add_trail(&mut self, trail_object: TrailObject) { + self.billboard_renderer.trails_wip.push(trail_object); + } + + pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { + self.gl.prepare_frame(latest_framebuffer_size_getter); + unsafe { + let gl = self.gl.context.clone(); + gl_error!(gl); + // self.gl.context.set_viewport(self.viewport); + self.gl.context.set_scissor(ScissorBox::new_at_origo( + self.viewport.width, + self.viewport.height, + )); + self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); + self.gl + .context + .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); + gl_error!(gl); + } + } + + pub fn render_egui( + &mut self, + meshes: Vec, + textures_delta: egui::TexturesDelta, + logical_screen_size: [f32; 2], + ) { + if self.has_link && !self.is_map_open { + self.billboard_renderer + .prepare_render_data(&self.gl.context); + self.billboard_renderer.render( + &self.gl.context, + self.cam_pos, + &self.view_proj, + &self.gl.glow_backend.painter.managed_textures, + ); + } + self.gl + .render_egui(meshes, textures_delta, logical_screen_size); + } + + pub fn present(&mut self) {} + + pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { + tracing::info!(?latest_size, "resizing framebuffer"); + + self.viewport = Viewport { + x: 0, + y: 0, + width: latest_size[0], + height: latest_size[1], + }; + self.gl.resize_framebuffer(latest_size); + } +} diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml new file mode 100644 index 0000000..3f95c4f --- /dev/null +++ b/crates/joko_render_models/Cargo.toml @@ -0,0 +1,13 @@ +# Define all structures that can be sent through asynchronous messages + +[package] +name = "joko_render_models" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytemuck = { workspace = true } +glam = { workspace = true, features = ["bytemuck"] } + diff --git a/crates/joko_render_models/src/lib.rs b/crates/joko_render_models/src/lib.rs new file mode 100644 index 0000000..5fa3de1 --- /dev/null +++ b/crates/joko_render_models/src/lib.rs @@ -0,0 +1,2 @@ +pub mod marker; +pub mod trail; diff --git a/crates/joko_render_models/src/marker.rs b/crates/joko_render_models/src/marker.rs new file mode 100644 index 0000000..39aa922 --- /dev/null +++ b/crates/joko_render_models/src/marker.rs @@ -0,0 +1,23 @@ +use glam::{Vec2, Vec3}; + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct MarkerVertex { + pub position: Vec3, + pub alpha: f32, + pub texture_coordinates: Vec2, + pub fade_near_far: Vec2, + pub color: [u8; 4], +} + +#[derive(Debug)] +pub struct MarkerObject { + /// The six vertices that make up the marker quad + pub vertices: [MarkerVertex; 6], + /// The (managed) texture id from egui data + pub texture: u64, + /// The distance from camera + /// As markers have transparency, we need to render them from far -> near order + /// So, we will sort them using this distance just before rendering + pub distance: f32, +} diff --git a/crates/joko_render_models/src/trail.rs b/crates/joko_render_models/src/trail.rs new file mode 100644 index 0000000..a80da51 --- /dev/null +++ b/crates/joko_render_models/src/trail.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use crate::marker::MarkerVertex; + +#[derive(Debug, Clone)] +pub struct TrailObject { + pub vertices: Arc<[MarkerVertex]>, + pub texture: u64, +} diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index 575661d..6c74e3d 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -6,23 +6,37 @@ use crate::prelude::*; #[repr(u8)] #[derive(Debug, Clone, Copy)] pub enum Race { - ASURA = 1 << 0, - CHARR = 1 << 2, - HUMAN = 1 << 3, - NORN = 1 << 4, - SYLVARI = 1 << 5, + Unknown = 1 << 1, + Asura = 1 << 2, + Charr = 1 << 3, + Human = 1 << 4, + Norn = 1 << 5, + Sylvari = 1 << 6, +} + +impl Race { + fn from_link_id(race_id: u32) -> Race { + match race_id { + 0 => Race::Asura, + 1 => Race::Charr, + 2 => Race::Human, + 3 => Race::Norn, + 4 => Race::Sylvari, + _ => return Race::Unknown, + } + } } impl FromStr for Race { type Err = &'static str; fn from_str(s: &str) -> std::result::Result { Ok(match s { - "asura" => Self::ASURA, - "charr" => Self::CHARR, - "human" => Self::HUMAN, - "norn" => Self::NORN, - "sylvari" => Self::SYLVARI, - _ => return Err("invalid race string"), + "Asura" => Self::Asura, + "Charr" => Self::Charr, + "Human" => Self::Human, + "Norn" => Self::Norn, + "Sylvari" => Self::Sylvari, + _ => Self::Unknown, }) } } @@ -30,11 +44,12 @@ impl FromStr for Race { impl AsRef for Race { fn as_ref(&self) -> &'static str { match self { - Self::ASURA => "asura", - Self::CHARR => "charr", - Self::HUMAN => "human", - Self::NORN => "norn", - Self::SYLVARI => "sylvari", + Self::Asura => "Asura", + Self::Charr => "Charr", + Self::Human => "Human", + Self::Norn => "Norn", + Self::Sylvari => "Sylvari", + Self::Unknown => "Unknown", } } } diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index 5883649..9853872 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -13,9 +13,10 @@ path = "src/main.rs" wayland = ["egui_window_glfw_passthrough/wayland"] [dependencies] -joko_core = { path = "../joko_core" } +enumflags2 = { workspace = true } +#joko_core = { path = "../joko_core" } joko_render = { path = "../joko_render" } -jmf = { path = "../joko_marker_format", package = "joko_marker_format" } +jmf = { path = "../joko_package", package = "joko_package" } jokolink = { path = "../jokolink" } egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users @@ -23,10 +24,23 @@ egui_window_glfw_passthrough = { version = "0.8" } cap-directories = { workspace = true } cap-std = { workspace = true } tracing = { workspace = true } +tracing-appender = { workspace = true } +tracing-subscriber = { workspace = true } miette = { workspace = true } +serde_json = { workspace = true } +indexmap = { workspace = true } +ringbuffer = { workspace = true } -egui = { workspace = true, features = ["serde"] } rayon = { workspace = true } +serde = { workspace = true } +rfd = { workspace = true } +glam = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } + +egui = { workspace = true, features = ["serde"] } +egui_extras = { workspace = true } + uuid = { workspace = true } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index d72275e..373243c 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -13,18 +13,19 @@ use uuid::Uuid; use init::get_jokolay_dir; use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; //use jmf::FileManager; -use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; +use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use jmf::message::BackToUIMessage; -use joko_render::JokoRenderer; +use joko_render::renderer::JokoRenderer; use jokolink::{MumbleChanges, MumbleLink, MumbleManager, mumble_gui}; use miette::{Context, IntoDiagnostic, Result}; -use tracing::{error, info, info_span, span}; -use jmf::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +use tracing::{error, info, info_span}; +use jmf::{LoadedPackData, LoadedPackTexture, build_from_core}; use jmf::{ImportStatus, import_pack_from_zip_file_path}; #[derive(Clone)] struct JokolayUIState { link: Option, + editable_mumble: bool, window_changed: bool, list_of_textures_changed: bool,//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display nb_running_tasks_on_back: i32,// store the number of running tasks in background thread @@ -34,6 +35,8 @@ struct JokolayUIState { struct JokolayBackState { choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI + read_ui_link: bool, + copy_of_ui_link: Option, } struct JokolayApp { mumble_manager: MumbleManager, @@ -121,7 +124,8 @@ impl Jokolay { package_manager: package_data_manager, }))), state_ui: JokolayUIState { - link: None, + link: Some(MumbleLink::default()), + editable_mumble: false, window_changed: true, list_of_textures_changed: false, nb_running_tasks_on_back: 0, @@ -130,6 +134,8 @@ impl Jokolay { }, state_back: JokolayBackState { choice_of_category_changed: false, + read_ui_link: false, + copy_of_ui_link: Default::default(), } }) } @@ -212,6 +218,18 @@ impl Jokolay { } b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } + UIToBackMessage::MumbleLinkAutonomous => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); + local_state.read_ui_link = false; + } + UIToBackMessage::MumbleLinkBindedOnUI => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); + local_state.read_ui_link = true; + } + UIToBackMessage::MumbleLink(link) => { + tracing::trace!("Handling of UIToBackMessage::MumbleLink"); + local_state.copy_of_ui_link = link; + } UIToBackMessage::ReloadPack => { unimplemented!("Handling of UIToBackMessage::ReloadPack has not been implemented yet"); } @@ -265,29 +283,24 @@ impl Jokolay { mumble_manager, package_manager } = &mut app.deref_mut().as_mut(); - //very first thing to do is to read the mumble link - let link = match mumble_manager.tick() { - Ok(ml) => { - //let link_clone = ml.cloned(); - //b2u_sender.send(BackToUIMessage::MumbleLink(link_clone)); - if let Some(link) = ml { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - b2u_sender.send(BackToUIMessage::MumbleLinkChanged); - } - } - ml - }, - Err(e) => { - error!(?e, "mumble manager tick error"); - None - } - }; + while let Ok(msg) = u2b_receiver.try_recv() { Self::handle_u2b_message(package_manager, &mut local_state, &b2u_sender, msg); nb_messages += 1; } + let link = if local_state.read_ui_link { + local_state.copy_of_ui_link.as_ref() + }else { + match mumble_manager.tick() { + Ok(ml) => { + ml + }, + Err(e) => { + error!(?e, "mumble manager tick error"); + None + } + } + }; tracing::trace!("choice_of_category_changed: {}", local_state.choice_of_category_changed); package_manager.tick( &b2u_sender, @@ -373,14 +386,6 @@ impl Jokolay { tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); gui.package_manager.load_marker_texture(&gui.egui_context, pack_uuid, tex_path, marker_uuid, position, common_attributes); } - BackToUIMessage::MumbleLink(link) => { - tracing::trace!("Handling of BackToUIMessage::MumbleLink"); - local_state.link = link; - } - BackToUIMessage::MumbleLinkChanged => { - //too verbose to trace - local_state.window_changed = true; - } BackToUIMessage::NbTasksRunning(nb_tasks) => { tracing::trace!("Handling of BackToUIMessage::NbTasksRunning"); local_state.nb_running_tasks_on_back = nb_tasks; @@ -512,26 +517,36 @@ impl Jokolay { // do all the non-gui stuff first frame_stats.tick(latest_time); - let link = match mumble_manager.tick() { - Ok(ml) => { - if let Some(link) = ml { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - local_state.window_changed = true; + if local_state.editable_mumble { + local_state.window_changed = true; + local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); + //TODO: at some point update the changes + u2b_sender.send(UIToBackMessage::MumbleLink(local_state.link.clone())); + u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); + } else { + u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); + let is_mumble_alive = mumble_manager.is_alive(); + match mumble_manager.tick() { + Ok(ml) => { + if let Some(link) = ml { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + local_state.window_changed = true; + } + if is_mumble_alive { + local_state.link = Some(link.clone()); + } } - local_state.link = Some(link.clone()); + }, + Err(e) => { + error!(?e, "mumble manager tick error"); } - ml - }, - Err(e) => { - error!(?e, "mumble manager tick error"); - None } - }; + } // check if we need to change window position or size. - if let Some(link) = link { + if let Some(link) = local_state.link.as_ref() { if local_state.window_changed { glfw_backend .window @@ -539,9 +554,11 @@ impl Jokolay { // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + let client_size_x = 1024.max(link.client_size.x); + let client_size_y = 768.max(link.client_size.y); glfw_backend .window - .set_size(link.client_size.x - 1, link.client_size.y - 1); + .set_size(client_size_x - 1, client_size_y - 1); } if local_state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map) { package_manager.tick( @@ -555,8 +572,8 @@ impl Jokolay { local_state.window_changed = false; } - joko_renderer.tick(link); - menu_panel.tick(&etx, link); + joko_renderer.tick(local_state.link.as_ref()); + menu_panel.tick(&etx, local_state.link.as_ref()); // do the gui stuff now egui::Area::new("menu panel") @@ -612,7 +629,8 @@ impl Jokolay { if let Some(link) = local_state.link.as_mut() { //updates need to be sent to the background state - mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, true, link); + //TODO: editable link need to trigger a map reload + mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, &mut local_state.editable_mumble, link); }; package_manager.gui( &u2b_sender, @@ -621,7 +639,7 @@ impl Jokolay { &local_state.import_status, &mut menu_panel.show_file_manager_window, latest_time, - link + local_state.link.as_ref() ); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); @@ -649,6 +667,7 @@ impl Jokolay { glfw_backend .window .set_mouse_passthrough(!(etx.wants_keyboard_input() || etx.wants_pointer_input())); + //TODO: view when map is open joko_renderer.render_egui( etx.tessellate(shapes, etx.pixels_per_point()), textures_delta, @@ -773,8 +792,8 @@ impl MenuPanel { let min_width = 1024.0 * gw2_scale; let min_height = 768.0 * gw2_scale; - let gw2_width = link.client_size.x as f32; - let gw2_height = link.client_size.y as f32; + let gw2_width = link.client_size.x.max(1024) as f32; + let gw2_height = link.client_size.y.max(768) as f32; let min_width_ratio = min_width.min(gw2_width) / min_width; let min_height_ratio = min_height.min(gw2_height) / min_height; diff --git a/crates/jokolay/src/lib.rs b/crates/jokolay/src/lib.rs index 2894e1e..e3aded2 100644 --- a/crates/jokolay/src/lib.rs +++ b/crates/jokolay/src/lib.rs @@ -1,4 +1,5 @@ mod app; +mod manager; pub use app::start_jokolay; diff --git a/crates/joko_core/src/manager/mod.rs b/crates/jokolay/src/manager/mod.rs similarity index 100% rename from crates/joko_core/src/manager/mod.rs rename to crates/jokolay/src/manager/mod.rs diff --git a/crates/joko_core/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs similarity index 100% rename from crates/joko_core/src/manager/theme/mod.rs rename to crates/jokolay/src/manager/theme/mod.rs diff --git a/crates/joko_core/src/manager/theme/roboto.ttf b/crates/jokolay/src/manager/theme/roboto.ttf similarity index 100% rename from crates/joko_core/src/manager/theme/roboto.ttf rename to crates/jokolay/src/manager/theme/roboto.ttf diff --git a/crates/joko_core/src/manager/trace/mod.rs b/crates/jokolay/src/manager/trace/mod.rs similarity index 92% rename from crates/joko_core/src/manager/trace/mod.rs rename to crates/jokolay/src/manager/trace/mod.rs index b27a99d..4ed1fc5 100644 --- a/crates/joko_core/src/manager/trace/mod.rs +++ b/crates/jokolay/src/manager/trace/mod.rs @@ -11,24 +11,46 @@ pub struct JokolayTracingLayer; static JKL_TRACING_DATA: OnceLock> = OnceLock::new(); impl JokolayTracingLayer { - pub fn install_tracing( - jokolay_dir: &Dir, + pub fn install_tracing<'l> ( + jokolay_dir: &'l Dir, ) -> Result { use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; + + std::panic::set_hook(Box::new(|info: &std::panic::PanicInfo| { + use std::fs::File; + use std::io::Write; + let backtrace = std::backtrace::Backtrace::force_capture(); + let output = + if let Some(string) = info.payload().downcast_ref::() { + format!("{string}") + } else if let Some(str) = info.payload().downcast_ref::<&'static str>() { + format!("{str}") + } else { + format!("{info:?}") + }; + eprintln!("{output}"); + eprintln!("Backtrace: {backtrace:}"); + let mut w = File::create("jokolay.errror").unwrap(); + writeln!(&mut w, "{output}").unwrap(); + writeln!(&mut w, "Backtrace: {backtrace:}").unwrap(); + })); + + // get the log level let filter_layer = EnvFilter::try_from_env("JOKOLAY_LOG") .or_else(|_| EnvFilter::try_new("info,wgpu=warn,naga=warn")) .into_diagnostic() .wrap_err("failed to parse log filter levels from env")?; // create log file in the data dir. This will also serve as a check that the directory is "writeable" by us - let writer = std::io::BufWriter::new( + let log_writer = std::io::BufWriter::new( jokolay_dir .create("jokolay.log") .into_diagnostic() .wrap_err("failed to create jokolay.log file")?, ); - let (nb, guard) = tracing_appender::non_blocking(writer); + + let (nb, guard) = tracing_appender::non_blocking(log_writer); let fmt_layer = fmt::layer() .with_ansi(false) .with_target(false) diff --git a/crates/jokolink/Cargo.toml b/crates/jokolink/Cargo.toml index a21ac1e..304101d 100644 --- a/crates/jokolink/Cargo.toml +++ b/crates/jokolink/Cargo.toml @@ -12,9 +12,6 @@ crate-type = ["cdylib", "lib"] widestring = { version = "1", default-features = false, features = ["std"] } num-derive = { version = "0", default-features = false } num-traits = { version = "0", default-features = false } -tracing-appender = { version = "0" } -tracing-subscriber = { version = "*" } -jokoapi = { path = "../jokoapi" } enumflags2 = { workspace = true } time = { workspace = true } miette = { workspace = true } @@ -23,7 +20,6 @@ egui = { workspace = true } serde = { workspace = true } glam = { workspace = true } serde_json = { workspace = true } -notify = { version = "*", default-features = false } [target.'cfg(unix)'.dependencies] x11rb = { version = "0.12", default-features = false, features = [] } diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index 4e41b67..ff4ccef 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -12,7 +12,7 @@ mod mumble; use egui::DragValue; use enumflags2::BitFlags; use glam::IVec2; -use jokoapi::end_point::mounts::Mount; +//use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::{IntoDiagnostic, Result, WrapErr}; pub use mumble::*; use serde_json::from_str; @@ -56,6 +56,7 @@ impl MumbleManager { pub fn is_alive(&self) -> bool { self.backend.is_alive() } + pub fn tick(&mut self) -> Result> { if let Err(e) = self.backend.tick() { error!(?e, "mumble backend tick error"); @@ -70,9 +71,11 @@ impl MumbleManager { } // backend is alive and tick is successful. time to get link let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); - if cml.ui_tick == 0 && self.link.ui_tick != 0 { - self.link = Default::default(); - } + let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { + Default::default() + } else { + self.link.clone() + }; if cml.ui_tick == 0 || cml.context.client_pos_size == [0; 4] { return Ok(None); @@ -98,31 +101,15 @@ impl MumbleManager { } else { std::net::Ipv4Addr::UNSPECIFIED.into() }; - if self.link.ui_tick != cml.ui_tick { + if new_link.ui_tick != cml.ui_tick { changes.insert(MumbleChanges::UiTick); } - if self.link.name != identity.name { + if new_link.name != identity.name { changes.insert(MumbleChanges::Character); } - if self.link.map_id != cml.context.map_id { + if new_link.map_id != cml.context.map_id { changes.insert(MumbleChanges::Map); } - // let window_pos = IVec2::new( - // cml.context.window_pos_size[0], - // cml.context.window_pos_size[1], - // ); - // let window_size = IVec2::new( - // cml.context.window_pos_size[2], - // cml.context.window_pos_size[3], - // ); - // let window_pos_without_borders = IVec2::new( - // cml.context.window_pos_size_without_borders[0], - // cml.context.window_pos_size_without_borders[1], - // ); - // let window_size_without_borders = IVec2::new( - // cml.context.window_pos_size_without_borders[2], - // cml.context.window_pos_size_without_borders[3], - // ); let client_pos = IVec2::new( cml.context.client_pos_size[0], cml.context.client_pos_size[1], @@ -132,23 +119,24 @@ impl MumbleManager { cml.context.client_pos_size[3], ); - if self.link.client_pos != client_pos { + if new_link.client_pos != client_pos { changes.insert(MumbleChanges::WindowPosition); } - if self.link.client_size != client_size { + if new_link.client_size != client_size { changes.insert(MumbleChanges::WindowSize); } let cam_pos = cml.f_camera_position.into(); - if self.link.cam_pos != cam_pos { + if new_link.cam_pos != cam_pos { changes.insert(MumbleChanges::Camera); } let player_pos = cml.f_avatar_position.into(); - if self.link.player_pos != player_pos { + if new_link.player_pos != player_pos { changes.insert(MumbleChanges::Position); } + //let player_race = Self::get_race(identity.race); - self.link = MumbleLink { + new_link = MumbleLink { ui_tick: cml.ui_tick, player_pos, f_avatar_front: cml.f_avatar_front.into(), @@ -172,7 +160,7 @@ impl MumbleManager { shard_id: cml.context.shard_id, instance: cml.context.instance, build_id: cml.context.build_id, - ui_state: cml.context.ui_state, + ui_state: cml.context.get_ui_state(), compass_width: cml.context.compass_width, compass_height: cml.context.compass_height, compass_rotation: cml.context.compass_rotation, @@ -182,9 +170,11 @@ impl MumbleManager { map_center_y: cml.context.map_center_y, map_scale: cml.context.map_scale, process_id: cml.context.process_id, - mount: Mount::try_from_mumble_link(cml.context.mount_index), + mount: cml.context.mount_index, + race: identity.race, }; - //self.link = link.clone(); + self.link = new_link; + Ok(if self.link.ui_tick == 0 { None } else { @@ -193,17 +183,23 @@ impl MumbleManager { } } -pub fn mumble_gui(etx: &egui::Context, open: &mut bool, is_alive: bool, link: &mut MumbleLink) { +pub fn mumble_gui(etx: &egui::Context, open: &mut bool, editable_mumble: &mut bool, link: &mut MumbleLink) { egui::Window::new("Mumble Manager") .open(open) .show(etx, |ui| { - if !is_alive { + if *editable_mumble { + if ui.button("back to live").clicked() { + *editable_mumble = false; + } ui.label( egui::RichText::new("Mumble is not initialized, display dummy link instead.") .color(egui::Color32::RED) ); editable_mumble_ui(ui, link); } else { + if ui.button("go to edit mode").clicked() { + *editable_mumble = true; + } let link: MumbleLink = link.clone(); mumble_ui(ui, link); } @@ -246,6 +242,21 @@ fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.add(DragValue::new(&mut link.f_camera_front.z)); }); ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut link.compass_height)); + ui.add(DragValue::new(&mut link.compass_width)); + ui.add(DragValue::new(&mut link.compass_rotation)); + }); + ui.end_row(); ui.label("fov"); ui.add(DragValue::new(&mut link.fov)); @@ -256,14 +267,26 @@ fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.add(DragValue::new(&mut ratio)); ui.end_row(); ui.label("character"); - ui.label(&link.name); + ui.horizontal(|ui|{ + ui.label(&link.name); + ui.label(format!("{:?}", link.race)); + }); ui.end_row(); + ui.label("map id"); ui.add(DragValue::new(&mut link.map_id)); ui.end_row(); ui.label("map type"); ui.add(DragValue::new(&mut link.map_type)); ui.end_row(); + ui.label("world position"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut link.map_center_x)); + ui.add(DragValue::new(&mut link.map_center_y)); + ui.add(DragValue::new(&mut link.map_scale)); + }); + ui.end_row(); + ui.label("address"); ui.label(format!("{}", link.server_address)); ui.end_row(); @@ -335,6 +358,22 @@ fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { }); ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = dummy_link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut dummy_link.compass_height)); + ui.add(DragValue::new(&mut dummy_link.compass_width)); + ui.add(DragValue::new(&mut dummy_link.compass_rotation)); + }); + ui.end_row(); + ui.label("fov"); ui.add(DragValue::new(&mut dummy_link.fov)); ui.end_row(); diff --git a/crates/jokolink/src/mumble/ctypes.rs b/crates/jokolink/src/mumble/ctypes.rs index 4012cc8..21ebaeb 100644 --- a/crates/jokolink/src/mumble/ctypes.rs +++ b/crates/jokolink/src/mumble/ctypes.rs @@ -1,5 +1,4 @@ use enumflags2::BitFlags; -use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::bail; use serde::{Deserialize, Serialize}; @@ -242,22 +241,6 @@ impl CMumbleContext { ]); Ok(ip) } - - pub fn get_mount(&self) -> Option { - Some(match self.mount_index { - 1 => Mount::Jackal, - 2 => Mount::Griffon, - 3 => Mount::Springer, - 4 => Mount::Skimmer, - 5 => Mount::Raptor, - 6 => Mount::RollerBeetle, - 7 => Mount::Warclaw, - 8 => Mount::Skyscale, - 9 => Mount::Skiff, - 10 => Mount::SiegeTurtle, - _ => return None, - }) - } } #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] @@ -299,14 +282,4 @@ impl CIdentity { _ => return None, }) } - pub fn get_race(&self) -> Option { - Some(match self.race { - 0 => Race::ASURA, - 1 => Race::CHARR, - 2 => Race::HUMAN, - 3 => Race::NORN, - 4 => Race::SYLVARI, - _ => return None, - }) - } } diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs index 0bb2366..451a7a3 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/jokolink/src/mumble/mod.rs @@ -5,7 +5,6 @@ use std::net::IpAddr; use enumflags2::{bitflags, BitFlags}; use glam::{IVec2, Vec3}; -use jokoapi::end_point::mounts::Mount; use num_derive::FromPrimitive; use num_derive::ToPrimitive; use serde::Deserialize; @@ -38,7 +37,7 @@ pub struct MumbleLink { /// The rest of the data from here is what gw2 provides for the benefit of addons. /// This is the current UI state of the game. refer to [UIState] /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: u32, + pub ui_state: Option>, pub compass_width: u16, // pixels pub compass_height: u16, // pixels pub compass_rotation: f32, // radians @@ -55,7 +54,10 @@ pub struct MumbleLink { pub process_id: u32, /// refers to [Mount] /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api - pub mount: Option, + //pub mount: Option, + //pub race: Race, + pub mount: u8, + pub race: u32, /// Vertical field-of-view pub fov: f32, @@ -106,6 +108,7 @@ impl Default for MumbleLink { map_scale: Default::default(), process_id: Default::default(), mount: Default::default(), + race: u32::MAX, fov: 2.0, uisz: Default::default(), dpi: Default::default(), From 89efd896e9f0fd9a1825f9d79a71ef534ae4976a Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 10 Apr 2024 13:16:54 +0200 Subject: [PATCH 24/54] more steps to separate structures from controler and have simpler rust packages dependancies (faster compilation intended in the end) --- crates/joko_marker_format/Cargo.toml | 51 - crates/joko_marker_format/README.md | 87 - crates/joko_marker_format/build.rs | 14 - .../joko_marker_format/src/io/deserialize.rs | 1399 --------- crates/joko_marker_format/src/io/error.rs | 1 - crates/joko_marker_format/src/io/mod.rs | 188 -- crates/joko_marker_format/src/io/serialize.rs | 227 -- crates/joko_marker_format/src/io/test.xml | 12 - .../src/io/xmlfile_schema.xsd | 394 --- crates/joko_marker_format/src/lib.rs | 47 - crates/joko_marker_format/src/manager/mod.rs | 24 - .../src/manager/pack/activation.rs | 21 - .../src/manager/pack/active.rs | 290 -- .../src/manager/pack/category_selection.rs | 269 -- .../src/manager/pack/dirty.rs | 29 - .../src/manager/pack/entry.rs | 6 - .../src/manager/pack/file_selection.rs | 44 - .../src/manager/pack/import.rs | 38 - .../src/manager/pack/list.rs | 6 - .../src/manager/pack/loaded.rs | 789 ----- .../src/manager/pack/mod.rs | 8 - .../joko_marker_format/src/manager/package.rs | 679 ----- crates/joko_marker_format/src/message.rs | 78 - crates/joko_marker_format/src/pack/common.rs | 2274 -------------- crates/joko_marker_format/src/pack/marker.png | Bin 173015 -> 0 bytes crates/joko_marker_format/src/pack/marker.rs | 15 - crates/joko_marker_format/src/pack/mod.rs | 413 --- .../joko_marker_format/src/pack/question.png | Bin 4248 -> 0 bytes crates/joko_marker_format/src/pack/route.rs | 20 - crates/joko_marker_format/src/pack/trail.png | Bin 6896 -> 0 bytes crates/joko_marker_format/src/pack/trail.rs | 31 - .../src/pack/trail_black.png | Bin 2293 -> 0 bytes .../src/pack/trail_rainbow.png | Bin 16987 -> 0 bytes .../vendor/rapid/license.txt | 52 - .../joko_marker_format/vendor/rapid/rapid.cpp | 66 - .../joko_marker_format/vendor/rapid/rapid.hpp | 7 - .../vendor/rapid/rapidxml.hpp | 2645 ----------------- .../vendor/rapid/rapidxml_iterators.hpp | 295 -- .../vendor/rapid/rapidxml_print.hpp | 422 --- .../vendor/rapid/rapidxml_utils.hpp | 56 - 40 files changed, 10997 deletions(-) delete mode 100755 crates/joko_marker_format/Cargo.toml delete mode 100644 crates/joko_marker_format/README.md delete mode 100644 crates/joko_marker_format/build.rs delete mode 100644 crates/joko_marker_format/src/io/deserialize.rs delete mode 100644 crates/joko_marker_format/src/io/error.rs delete mode 100644 crates/joko_marker_format/src/io/mod.rs delete mode 100644 crates/joko_marker_format/src/io/serialize.rs delete mode 100644 crates/joko_marker_format/src/io/test.xml delete mode 100644 crates/joko_marker_format/src/io/xmlfile_schema.xsd delete mode 100644 crates/joko_marker_format/src/lib.rs delete mode 100644 crates/joko_marker_format/src/manager/mod.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/activation.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/active.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/category_selection.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/dirty.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/entry.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/file_selection.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/import.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/list.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/loaded.rs delete mode 100644 crates/joko_marker_format/src/manager/pack/mod.rs delete mode 100644 crates/joko_marker_format/src/manager/package.rs delete mode 100644 crates/joko_marker_format/src/message.rs delete mode 100644 crates/joko_marker_format/src/pack/common.rs delete mode 100755 crates/joko_marker_format/src/pack/marker.png delete mode 100644 crates/joko_marker_format/src/pack/marker.rs delete mode 100644 crates/joko_marker_format/src/pack/mod.rs delete mode 100644 crates/joko_marker_format/src/pack/question.png delete mode 100644 crates/joko_marker_format/src/pack/route.rs delete mode 100755 crates/joko_marker_format/src/pack/trail.png delete mode 100644 crates/joko_marker_format/src/pack/trail.rs delete mode 100644 crates/joko_marker_format/src/pack/trail_black.png delete mode 100644 crates/joko_marker_format/src/pack/trail_rainbow.png delete mode 100644 crates/joko_marker_format/vendor/rapid/license.txt delete mode 100644 crates/joko_marker_format/vendor/rapid/rapid.cpp delete mode 100644 crates/joko_marker_format/vendor/rapid/rapid.hpp delete mode 100644 crates/joko_marker_format/vendor/rapid/rapidxml.hpp delete mode 100644 crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp delete mode 100644 crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp delete mode 100644 crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp diff --git a/crates/joko_marker_format/Cargo.toml b/crates/joko_marker_format/Cargo.toml deleted file mode 100755 index 1eb9c48..0000000 --- a/crates/joko_marker_format/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "joko_marker_format" -version = "0.2.1" -edition = "2021" - -[dependencies] -# jmf deps -# for marker packs -base64 = "0.21.2" -bytemuck = { workspace = true } -cap-std = { workspace = true } -cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings -data-encoding = "2.4.0" -egui = { workspace = true } -enumflags2 = { workspace = true } -glam = { workspace = true } -image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. -indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip -itertools = { workspace = true } -joko_core = { path = "../joko_core" } -jokoapi = { path = "../jokoapi" } -jokolink = { path = "../jokolink" } -miette = { workspace = true } -ordered_hash_map = { workspace = true } -paste = { workspace = true } -phf = { version = "*", features = ["macros"] } -rayon = { workspace = true } -rfd = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -smol_str = { workspace = true } -time = { workspace = true , features = ["serde"]} -tracing = { workspace = true } -tribool = "0.3.0" -url = { workspace = true } -uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } -xot = { version = "0.16.0" } -zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) - - - -[dev-dependencies] -# jmf deps -rstest = { version = "0", default-features = false } -# rstest_reuse = "0.3.0" -similar-asserts = "1" - - -[build-dependencies] -# for rapidxml -cxx-build = { version = "1" } diff --git a/crates/joko_marker_format/README.md b/crates/joko_marker_format/README.md deleted file mode 100644 index cbbf8d6..0000000 --- a/crates/joko_marker_format/README.md +++ /dev/null @@ -1,87 +0,0 @@ - -## Status -still in early stages of development - - - - -### RapidXML Integration -Taco uses RapidXML, which is very very lenient in its parsing. -this led to marker packs not caring about their xml being valid xml. -Blish instead created a custom parsing library to deal with this and have workarounds for known issues. - -rapidxml does fix these issues itself when we roundtrip xml through it. so, we have a function called `rapid_filter` which takes in xml string and returns a "filtered" xml string that fixes a bunch of issues like escaping special characters like -ampersand, gt, lt etc.. with proper xml formatting i.e `&`, `>` etc.. - -Sources of rapidxml are in the vendor folder. it is a custom fork from https://github.com/timniederhausen/rapidxml which -added some fixes / enhancements. its stil a mess with compiler warnings, but whatever. - -we use cxxbridge crate. -`rapid.hpp` is our header with declaration for `rapid_filter` inside `rapid` namespace. (includes `joko_marker_format/src/lib.rs.h`) -`lib.rs` has extern declaration which has the same signature but in rust. (includes `joko_marker_format/vendor/rapid/rapid.hpp`) -`build.rs` has the compilation instructions. it uses `lib.rs` extern declaration, `rapid.cpp` as compilation unit as it - contains the definition of `rapid_filter` and finally outputs a `librapid.a` for linking. - -with this, we now filter the xml with `rapid_filter` before deserializing it in rust. if we still have errors we just -complain about it. - - - -### XML Marker Format -Marker Pack - -1. Textures - 1. identified by the relative path. case sensitive. But to accommodate case-insensitive MS windows packs, we will convert all paths to lowercase when importing. - 2. png format. - 3. need to convert to a srgba texture and upload to gpu to use it - 4. mostly tiny images. here's the composition of tekkit's pack textures - -| count | dimensions | -|-------|---------------| -| 630 | 100x100 | -| 7 | 150x150 | -| 89 | 200x200 | -| 683 | 250x250 | -| 42 | 256x256 | -| 435 | 500x500 | - -2. Tbins - 1. binary data of a series of vec3 positions. + mapid + a version (just ver 2 for now) - 2. need to generate a mesh to be usable to upload on gpu. different mesh for 2d map / minimap. trail_scale an affect width of the generated mesh - 3. anim_speed attr needs dynamic texture coords (probably based on time delta offset) - 4. color attribute requires blending. - 5. uses texture - 6. can be statically or dynamically filtered (culled). but no cooldowns. - -3. MarkerCategories - 1. create a tree structure of menu to be displayed. - 2. identified by their name (and parents in the hierarchy) as a unique path. - 3. can be enabled or disabled. need to persist this data in activation data or somewhere else. - 4. enabled / disabled categories act as dynamic filters for markers / trails. - 5. attributes get inherited by children unless overrided. and also inehrited by the markers / trails. - 6. can be enabled / disabled by a marker action (toggle_category attribute) -4. Markers - 1. render a quad. either billbaord or static rotation. - 2. needs texture + alpha attribute + color attribute for blending. - 3. alpha is also affected by fadenear and fadefar attributes. - 4. static filters like ingamevisibility or map visibility or minimap visibility. - 5. can display text via info / tip-description. - 6. dynamic filters like behavior + race + profession + specialization + mount + map type + category + festival + achievement. - 7. size is determined by texture + minSize / maxSize + scale. map quad rendering affected by scale on map and mapdisplaysize attribute - 8. triggers actions of behavior + copy-message (copy clipboard) + bounce?? + toggling category based on player proximity and pressing of a special action key (usually F) -5. Trails - 1. render the tbin mesh. - 2. same filters as marker - 3. no triggering / activation / cooldowns though. - - -3D: -1. can match blish -2. need to ignore certain attributes like minSize and maxSize. - - -2D: -1. can match taco -2. more performance because 2d? - - diff --git a/crates/joko_marker_format/build.rs b/crates/joko_marker_format/build.rs deleted file mode 100644 index 062e89b..0000000 --- a/crates/joko_marker_format/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() { - cxx_build::bridge("src/lib.rs") // our extern declaration in rust for rapid_filter - .file("vendor/rapid/rapid.cpp") // our compilation unit containing definition - .warnings(false) - .extra_warnings(false) - .compile("rapid"); // name of library = librapid.a - - println!("cargo:rerun-if-changed=src/lib.rs"); - println!("cargo:rerun-if-changed=vendor/rapid/rapid.cpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapid.hpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapidxml.hpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapidxml_print.hpp"); - // shadow_rs::new().expect("failed to run shadow"); -} diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs deleted file mode 100644 index a2ac83d..0000000 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ /dev/null @@ -1,1399 +0,0 @@ -use joko_core::RelativePath; -use miette::{bail, Context, IntoDiagnostic, Result}; - -use crate::{ - pack::{prefix_parent, Category, CommonAttributes, MapData, Marker, PackCore, RawCategory, Route, TBin, TBinStatus, Trail}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::{Dir, DirEntry}; -use glam::Vec3; -use indexmap::IndexMap; -use std::{collections::{VecDeque, HashMap}, io::Read}; -use ordered_hash_map::OrderedHashMap; -use tracing::{debug, info, info_span, instrument, trace, warn}; -use uuid::Uuid; -use xot::{Node, Xot, Element}; - -use super::XotAttributeNameIDs; - -pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { - //called from already parsed data - let mut core_pack = PackCore::new(); - // walks the directory and loads all files into the hashmap - let start = std::time::SystemTime::now(); - recursive_walk_dir_and_read_images_and_tbins( - dir, - &mut core_pack.textures, - &mut core_pack.tbins, - &RelativePath::default(), - ) - .wrap_err("failed to walk dir when loading a markerpack")?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of core package textures from disk took {} ms", elaspsed.as_millis()); - - //categories are required to register other objects - let cats_xml = dir - .read_to_string("categories.xml") - .into_diagnostic() - .wrap_err("failed to read categories.xml")?; - let categories_file = String::from("categories.xml"); - let parse_categories_file_start = std::time::SystemTime::now(); - parse_categories_file(&categories_file, &cats_xml, &mut core_pack) - .wrap_err("failed to parse category file")?; - info!("parse_categories_file took {} ms", parse_categories_file_start.elapsed().unwrap_or_default().as_millis()); - - // parse map data of the pack - for entry in dir - .entries() - .into_diagnostic() - .wrap_err("failed to read entries of pack dir")? - { - let dir_entry = entry - .into_diagnostic() - .wrap_err("entry error whiel reading xml files")?; - - let name = dir_entry - .file_name() - .into_diagnostic() - .wrap_err("map data entry name not utf-8")? - .to_string(); - - if name.ends_with(".xml") { - if let Some(name_as_str) = name.strip_suffix(".xml") { - match name_as_str { - "categories" => { - //already done - } - map_id => { - // parse map file - let span_guard = info_span!("load map", map_id).entered(); - if let Ok(map_id) = map_id.parse::() { - //let mut partial_pack = PackCore::partial(&core_pack.all_categories); - load_map_file(map_id, &dir_entry, &mut core_pack)?; - //core_pack.merge_partial(partial_pack); - } else { - info!("unrecognized xml file {map_id}") - } - std::mem::drop(span_guard); - } - } - } - } else { - trace!("file ignored: {name}") - } - } - info!("Entities registered (category + markers): {}", core_pack.entities_parents.len()); - info!("Categories registered: {}", core_pack.all_categories.len()); - info!("Markers registered: {}", core_pack.entities_parents.len() - core_pack.all_categories.len()); - info!("Maps registered: {}", core_pack.maps.len()); - info!("Textures registered: {}", core_pack.textures.len()); - info!("Trail binaries registered: {}", core_pack.tbins.len()); - Ok(core_pack) -} - - -fn recursive_walk_dir_and_read_images_and_tbins( - dir: &Dir, - images: &mut HashMap>, - tbins: &mut HashMap, - parent_path: &RelativePath, -) -> Result<()> { - for entry in dir - .entries() - .into_diagnostic() - .wrap_err("failed to get directory entries")? - { - let entry = entry - .into_diagnostic() - .wrap_err("dir entry error when iterating dir entries")?; - let name = entry.file_name().into_diagnostic()?; - let path = parent_path.join_str(&name); - - if entry - .file_type() - .into_diagnostic() - .wrap_err("failed to get file type")? - .is_file() - { - if path.ends_with(".png") || path.ends_with(".trl") { - let mut bytes = vec![]; - entry - .open() - .into_diagnostic() - .wrap_err("failed to open file")? - .read_to_end(&mut bytes) - .into_diagnostic() - .wrap_err("failed to read file contents")?; - if name.ends_with(".png") { - images.insert(path.clone(), bytes); - } else if name.ends_with(".trl") { - if let Some(tbs) = parse_tbin_from_slice(&bytes) { - let is_closed: bool = tbs.closed; - if is_closed { - if tbs.iso_x {} - if tbs.iso_y {} - if tbs.iso_z {} - } - tbins.insert(path, tbs.tbin); - } else { - info!("invalid tbin: {path}"); - } - } - } - } else { - recursive_walk_dir_and_read_images_and_tbins( - &entry.open_dir().into_diagnostic()?, - images, - tbins, - &path, - )?; - } - } - Ok(()) -} -fn parse_tbin_from_slice(bytes: &[u8]) -> Option { - let content_length = bytes.len(); - // content_length must be atleast 8 to contain version + map_id - if content_length < 8 { - info!("failed to parse tbin because the len is less than 8"); - return None; - } - - let mut version_bytes = [0_u8; 4]; - version_bytes.copy_from_slice(&bytes[4..8]); - let version = u32::from_ne_bytes(version_bytes); - let mut map_id_bytes = [0_u8; 4]; - map_id_bytes.copy_from_slice(&bytes[4..8]); - let map_id = u32::from_ne_bytes(map_id_bytes); - - let zero = Vec3{x:0.0, y:0.0, z:0.0}; - - // this will either be empty vec or series of vec3s. - let nodes: VecDeque = bytes[8..] - .chunks_exact(12) - .map(|float_bytes| { - // make [f32 ;3] out of those 12 bytes - let arr = [ - f32::from_le_bytes([ - // first float - float_bytes[0], - float_bytes[1], - float_bytes[2], - float_bytes[3], - ]), - f32::from_le_bytes([ - // second float - float_bytes[4], - float_bytes[5], - float_bytes[6], - float_bytes[7], - ]), - f32::from_le_bytes([ - // third float - float_bytes[8], - float_bytes[9], - float_bytes[10], - float_bytes[11], - ]), - ]; - - Vec3::from_array(arr) - }) - .collect(); - - //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. - let mut iso_x = false; - let mut iso_y = false; - let mut iso_z = false; - let mut closed = false; - let mut resulting_nodes : Vec = Vec::new(); - if nodes.len() > 0 { - let ref_node = nodes[0]; - let mut c_iso_x = true; - let mut c_iso_y = true; - let mut c_iso_z = true; - // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts - resulting_nodes.push(ref_node); - for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { - //ignore zeroes since they would be separators - if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { - let distance_to_next_point = a.distance_squared(*b); - let mut current_cursor = distance_to_next_point; - while current_cursor > 400.0 { - let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); - resulting_nodes.push(c); - current_cursor -= 400.0; - } - } - resulting_nodes.push(*b); - } - for node in &nodes { - if resulting_nodes.len() > 1 { - //TODO: load epsilon from a configuration somewhere, with a default value - if (node.x - ref_node.x).abs() < 0.1 { - c_iso_x = false; - } - if (node.y - ref_node.y).abs() < 0.1 { - c_iso_y = false; - } - if (node.z - ref_node.z).abs() < 0.1 { - c_iso_z = false; - } - } - } - iso_x = c_iso_x; - iso_y = c_iso_y; - iso_z = c_iso_z; - if nodes.len() > 1 {// TODO: get this threshold from configuration - closed = nodes.front().unwrap().distance(*nodes.back().unwrap()).abs() < 0.1 - } - } - Some(TBinStatus{ - tbin: TBin { - map_id, - version, - nodes: resulting_nodes, - }, - iso_x, - iso_y, - iso_z, - closed - }) -} - -fn parse_categories( - tree: &Xot, - tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, - names: &XotAttributeNameIDs, -) { - //called once per file - parse_categories_recursive(tree, tags, first_pass_categories, names, None); - -} - - -// a recursive function to parse the marker category tree. -fn parse_categories_recursive( - tree: &Xot, - tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, - names: &XotAttributeNameIDs, - parent_name: Option, -) { - for tag in tags { - let ele = match tree.element(tag) { - Some(ele) => ele, - None => continue, - }; - if ele.name() != names.marker_category { - continue; - } - - let name = ele - .get_attribute(names.name) - .or(ele.get_attribute(names.CapitalName)) - .unwrap_or_default() - .to_lowercase(); - if name.is_empty() { - continue; - } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - - let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); - - let separator = ele - .get_attribute(names.separator) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or_default(); - - let default_enabled = ele - .get_attribute(names.default_enabled) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or(true); - let guid = parse_guid(names, ele); - let full_category_name: String = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, name) - } else { - name.to_string() - }; - trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); - if !first_pass_categories.contains_key(&full_category_name) { - first_pass_categories.insert(full_category_name.clone(), RawCategory { - guid, - parent_name: parent_name.clone(), - display_name: display_name.to_string(), - relative_category_name: name.to_string(), - full_category_name: full_category_name.clone(), - separator, - default_enabled, - props: ca, - }); - } - parse_categories_recursive( - tree, - tree.children(tag), - first_pass_categories, - names, - Some(full_category_name), - ); - } -} - -fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { - let mut tree = xot::Xot::new(); - let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); - let root_node = tree - .parse(cats_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; - - let overlay_data_node = tree - .document_element(root_node) - .into_diagnostic() - .wrap_err("no doc element")?; - - if let Some(od) = tree.element(overlay_data_node) { - let mut categories: IndexMap = Default::default(); - if od.name() == xot_names.overlay_data { - parse_category_categories_xml_recursive( - &file_name, - &tree, - tree.children(overlay_data_node), - pack, - &mut categories, - &xot_names, - None, - None, - ); - trace!("loaded categories: {:?}", categories); - pack.categories = categories; - pack.register_categories(); - } else { - bail!("root tag is not OverlayData") - } - } else { - bail!("doc element is not element???"); - } - Ok(()) -} - - -fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Result<()> { - let mut xml_str = String::new(); - dir_entry - .open() - .into_diagnostic() - .wrap_err("failed to open xml file")? - .read_to_string(&mut xml_str) - .into_diagnostic() - .wrap_err("faield to read xml string")?; - //TODO: launch an async load of the file + make a priority queue to have current map first - parse_map_xml_string(map_id, &xml_str, target).wrap_err_with(|| { - miette::miette!("error parsing map file: {map_id}") - }) -} - -fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) -> Result<()> { - /* - fields read: - all_categories - - fields modified: - maps - all_categories - late_discovery_categories - source_files - tbins - entities_parents - */ - let mut tree = Xot::new(); - let root_node = tree - .parse(map_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let overlay_data_node = tree - .document_element(root_node) - .into_diagnostic() - .wrap_err("missing doc element")?; - - let overlay_data_element = tree - .element(overlay_data_node) - .ok_or_else(|| miette::miette!("no doc ele"))?; - - if overlay_data_element.name() != names.overlay_data { - bail!("root tag is not OverlayData"); - } - let pois = tree - .children(overlay_data_node) - .find(|node| match tree.element(*node) { - Some(ele) => ele.name() == names.pois, - None => false, - }) - .ok_or_else(|| miette::miette!("missing pois node"))?; - - for poi_node in tree.children(pois) { - if let Some(child_element) = tree.element(poi_node) { - let full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - - let span_guard = info_span!("category", full_category_name).entered(); - - let source_file_name = child_element.get_attribute(names._source_file_name).unwrap_or_default().to_string(); - target.source_files.insert(source_file_name.clone(), true); - - if child_element.name() == names.route { - debug!("Found a route in core pack {:?}", child_element); - let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_name.clone()); - if let Some(mut route) = route { - //TODO: make sure there is no "very late" discovery - //let category_uuid = target.get_or_create_category_uuid(&route.category); - //route.parent = category_uuid; - target.register_route(route)?; - } else { - info!("Could not parse route {:?}", child_element); - } - } else { - if full_category_name.is_empty() { - panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); - } - let raw_uid = child_element.get_attribute(names.guid); - if raw_uid.is_none() { - info!("This POI is either invalid or inside a Route {:?}", child_element); - span_guard.exit(); - continue; - } - //FIXME: this needs to be changed for partial load - let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed - let guid = raw_uid.and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - }) - .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; - - if child_element.name() == names.poi { - debug!("Found a POI in core pack {:?}", child_element); - if child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } - let xpos = child_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let ypos = child_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let zpos = child_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - target.register_uuid(&full_category_name, &guid); - let marker = Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category: full_category_name, - parent: category_uuid.clone(), - attrs: ca, - guid, - source_file_name - }; - - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); - } - target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); - } else if child_element.name() == names.trail { - debug!("Found a trail in core pack {:?}", child_element); - if child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - target.register_uuid(&full_category_name, &guid); - let trail = Trail { - category: full_category_name, - parent: category_uuid.clone(), - map_id, - props: ca, - guid, - dynamic: false, - source_file_name - }; - - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); - } - target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); - } - } - span_guard.exit(); - } - } - Ok(()) -} - -// a temporary recursive function to parse the marker category tree. -fn parse_category_categories_xml_recursive( - file_name: &String, - tree: &Xot, - tags: impl Iterator, - pack: &mut PackCore, - cats: &mut IndexMap, - names: &XotAttributeNameIDs, - parent_uuid: Option, - parent_name: Option, -) { - for tag in tags { - if let Some(ele) = tree.element(tag) { - if ele.name() != names.marker_category { - continue; - } - - let relative_category_name = ele.get_attribute(names.name) - .or(ele.get_attribute(names.display_name) - .or(ele.get_attribute(names.CapitalName) - ) - ).unwrap_or_default().to_lowercase(); - if relative_category_name.is_empty() { - info!("category doesn't have a name attribute: {ele:#?}"); - continue; - } - let span_guard = info_span!("category", relative_category_name).entered(); - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); - - let separator = match ele.get_attribute(names.separator).unwrap_or("0") { - "0" => false, - "1" => true, - ors => { - info!("separator attribute has invalid value: {ors}"); - false - } - }; - - let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { - "0" => false, - "1" => true, - ors => { - info!("default_enabled attribute has invalid value: {ors}"); - true - } - }; - let full_category_name: String = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, relative_category_name) - } else { - relative_category_name.to_string() - }; - let guid = parse_guid(names, ele); - trace!("recursive_marker_category_parser_categories_xml {} {} {:?}", full_category_name, guid, parent_uuid); - if display_name.is_empty() { - assert!(parent_name.is_none()); - parse_category_categories_xml_recursive( - file_name, - tree, - tree.children(tag), - pack, - cats, - names, - Some(guid), - Some(full_category_name), - ); - } else { - let current_category = cats - .entry(guid) - .or_insert_with(|| Category { - guid, - parent: parent_uuid.clone(), - display_name: display_name.to_string(), - relative_category_name: relative_category_name.to_string(), - full_category_name: full_category_name.clone(), - separator, - default_enabled, - props: ca, - children: Default::default(), - }); - parse_category_categories_xml_recursive( - file_name, - tree, - tree.children(tag), - pack, - &mut current_category.children, - names, - Some(guid), - Some(full_category_name), - ); - }; - - std::mem::drop(span_guard); - } else { - //it may be a comment, a space, anything - //info!("In file {}, ignore node {:?}", file_name, tag); - } - } -} - -/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. -/// will return error if there's an issue with zipfile. -/// -/// but any other errors like invalid attributes or missing markers etc.. will just be logged. -/// the intention is "best effort" parsing and not "validating" xml marker packs. -/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. -#[instrument(skip_all)] -pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { - //called to import a new pack - // all the contents of ZPack - let mut pack = PackCore::new(); - // parse zip file - let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) - .into_diagnostic() - .wrap_err("failed to read zip archive")?; - - // file paths of different file types - let mut images = vec![]; - let mut tbins = vec![]; - let mut xmls = vec![]; - // we collect the names first, because reading a file from zip is a mutating operation. - // So, we can't iterate AND read the file at the same time - for name in zip_archive.file_names() { - let name_as_string = name.to_string(); - if name_as_string.ends_with(".png") { - images.push(name_as_string); - } else if name_as_string.ends_with(".trl") { - tbins.push(name_as_string); - } else if name_as_string.ends_with(".xml") { - xmls.push(name_as_string); - } else if name_as_string.replace("\\", "/").ends_with('/') { - // directory. so, we can silently ignore this. - } else { - info!("ignoring file: {name}"); - } - } - xmls.sort();//build back the intended order in folder, since zip_archive may not give the files in order. - let start = std::time::SystemTime::now(); - for name in images { - let span = info_span!("load image", name).entered(); - let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); - if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { - match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { - Ok(_) => assert!( - pack.textures.insert(file_path.clone(), bytes).is_none(), - "duplicate image file {name}" - ), - Err(e) => { - info!(?e, "failed to parse image file"); - } - } - } - std::mem::drop(span); - } - - for name in tbins { - let span = info_span!("load tbin {name}").entered(); - - let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); - if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { - if let Some(tbs) = parse_tbin_from_slice(&bytes) { - let is_closed: bool = tbs.closed; - if is_closed { - if tbs.iso_x {} - if tbs.iso_y {} - if tbs.iso_z {} - } - assert!( - pack.tbins.insert(file_path, tbs.tbin).is_none(), - "duplicate tbin file {name}" - ); - } else { - info!("failed to parse tbin from slice: {file_path}"); - } - } else { - info!(name, "failed to read tbin from zipfile"); - } - std::mem::drop(span); - } - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of taco package textures from disk took {} ms", elaspsed.as_millis()); - - let span_guard_categories = info_span!("deserialize xml: categories").entered(); - - //first pass: categories only - let span_guard_first_pass = info_span!("deserialize xml first pass: load MarkerCategory").entered(); - let mut first_pass_categories: OrderedHashMap = Default::default(); - for source_file_name in xmls.iter() { - let mut xml_str = String::new(); - let span_guard = info_span!("deserialize xml first pass: load file", source_file_name).entered(); - if zip_archive - .by_name(&source_file_name) - .ok() - .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) - .is_none() - { - info!("failed to read file from zip"); - continue; - }; - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - - parse_categories(&tree, tree.children(od), &mut first_pass_categories, &names); - drop(span_guard); - } - span_guard_first_pass.exit(); - - //second pass: orphan categories - let span_guard_second_pass = info_span!("deserialize xml second pass: orphan categories").entered(); - for source_file_name in xmls.iter() { - let mut xml_str = String::new(); - let span_guard = info_span!("deserialize xml second pass: load file", source_file_name).entered(); - if zip_archive - .by_name(&source_file_name) - .ok() - .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) - .is_none() - { - info!("failed to read file from zip"); - continue; - }; - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - let pois = match tree.children(od).find(|node| { - tree.element(*node) - .map(|ele: &xot::Element| ele.name() == names.pois) - .unwrap_or_default() - }) { - Some(pois) => pois, - None => { - info!("missing pois tag"); - continue; - } - }; - - for child_node in tree.children(pois) { - let child_element = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - let mut full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - if full_category_name.is_empty() { - if child_element.name() == names.route { - // If route, take the first element inside - if let Some(category) = parse_route_category(&names, &tree, &child_node, child_element) { - if category.is_empty() { - continue; - } - full_category_name = category; - } else { - continue; - } - } else { - continue; - } - } - if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { - let category_uuid = Uuid::new_v4(); - first_pass_categories.insert(full_category_name.clone(), RawCategory{ - default_enabled: true, - guid: category_uuid, - parent_name: prefix_parent(&full_category_name, '.'), - display_name: full_category_name.clone(), - full_category_name: full_category_name.clone(), - relative_category_name: full_category_name.clone(), - props: Default::default(), - separator: false - }); - info!("There is an orphan missing category '{}' which was created", full_category_name); - } - } - drop(span_guard); - } - span_guard_second_pass.exit(); - - pack.categories = Category::reassemble(&first_pass_categories, &mut pack.late_discovery_categories); - pack.register_categories(); - - //third and last pass: elements - let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); - for source_file_name in xmls.iter() { - let mut xml_str = String::new(); - let span_guard = info_span!("deserialize xml third pass load file ", source_file_name).entered(); - if zip_archive - .by_name(&source_file_name) - .ok() - .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) - .is_none() - { - info!("failed to read file from zip"); - continue; - }; - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - - let pois = match tree.children(od).find(|node| { - tree.element(*node) - .map(|ele: &xot::Element| ele.name() == names.pois) - .unwrap_or_default() - }) { - Some(pois) => pois, - None => { - info!("missing pois tag"); - continue; - } - }; - - for child_node in tree.children(pois) { - let child_element = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - let full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - - debug!("import element: {:?}", child_element); - if child_element.name() == names.route { - let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, source_file_name.clone()); - if let Some(mut route) = route { - //one must not create category anymore - route.parent = pack.get_category_uuid(&route.category).unwrap().clone(); - pack.register_route(route)?; - } else { - info!("Could not parse route {:?}", child_element); - } - } else { - if full_category_name.is_empty() { - info!("full_category_name is empty {:?}", child_element); - continue; - } - if ! pack.category_exists(&full_category_name) { - panic!("Missing category {}, previous pass should have taken care of this", full_category_name); - } - let category_uuid = pack.get_or_create_category_uuid(&full_category_name); - if child_element.name() == names.poi { - if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { - pack.register_marker(full_category_name, marker)?; - } else { - debug!("Could not parse POI"); - } - } else if child_element.name() == names.trail { - if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { - pack.register_trail(full_category_name, trail)?; - } else { - debug!("Could not parse Trail"); - } - } else { - info!("unknown element: {:?}", child_element); - } - } - } - - drop(span_guard); - } - span_guard_third_pass.exit(); - span_guard_categories.exit(); - Ok(pack) -} - - -fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { - child - .get_attribute(names.guid) - .and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - .or_else(|| { - info!(guid, "failed to deserialize guid"); - None - }) - }) -} -fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ - parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) -} - -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { - if let Some(map_id) = poi_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - { - let xpos = poi_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let ypos = poi_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let zpos = poi_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(poi_element, &names); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - info!(%icon_file, "failed to find this texture in this pack"); - } - } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { - info!(icf, "marker's icon file attribute failed to parse"); - } - Some(Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category: category_name.clone(), - parent: category_uuid.clone(), - attrs: common_attributes, - guid: parse_guid(names, poi_element), - source_file_name - }) - } else { - info!("missing map id"); - None - } -} - -fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { - let x = poi_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let y = poi_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let z = poi_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - Vec3{x, y, z} -} - - -fn parse_route_category( - names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, -) -> Option { - for child_node in tree.children(*route_node) { - let child = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - if child.name() == names.poi { - if let Some(cat) = child.get_attribute(names.category) { - return Some(cat.to_string()); - } - } - } - info!("Could not find a category for route element: {route_element:?}"); - None -} - -fn parse_route( - names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, - category_name: &String, - source_file_name: String -) -> Option { - - let mut path: Vec = Vec::new(); - let resetposx = route_element - .get_attribute(names.resetposx) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let resetposy = route_element - .get_attribute(names.resetposy) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let resetposz = route_element - .get_attribute(names.resetposz) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let reset_position = Vec3::new(resetposx, resetposy, resetposz); - let reset_range = route_element.get_attribute(names.reset_range).and_then(|map_id| map_id.parse::().ok()); - let name = route_element.get_attribute(names.name).or(route_element.get_attribute(names.CapitalName)); - - if name.is_none() { - info!("route element is missing name: {route_element:?}"); - return None; - } - let mut category: String = category_name.clone(); - let mut category_uuid: Option = parse_optional_guid(names, route_element); - let mut map_id: Option = route_element.get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()); - for child_node in tree.children(*route_node) { - let child = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - if child.name() == names.poi { - let marker = parse_position(&names, child); - path.push(marker); - if category.is_empty() { - if let Some(cat) = child.get_attribute(names.category) { - category = cat.to_string(); - } - } - if category_uuid.is_none() { - category_uuid = parse_optional_guid(names, &child) - } - if map_id.is_none() { - if let Some(node_map_id) = child - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - { - map_id = Some(node_map_id); - } - } - } - } - if category.is_empty() { - info!("Could not find a category for route element: {route_element:?}"); - return None; - } - if map_id.is_none() { - info!("Could not find a map_id for route element: {route_element:?}"); - return None; - } - if category_uuid.is_none() { - info!("Could not find a uuid for route element: {route_element:?}"); - return None; - } - debug!("found route with {:?} elements {route_element:?}", path.len()); - - Some(Route { - category, - parent: category_uuid.unwrap(), - path, - reset_position, - reset_range: reset_range.unwrap_or(0.0), - map_id: map_id.unwrap(), - name: name.unwrap().into(), - guid: parse_guid(names, &route_element), - source_file_name, - }) -} - - -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { - //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html - if let Some(map_id) = trail_element - .get_attribute(names.trail_data) - .and_then(|trail_data| { - let path: RelativePath = trail_data.parse().unwrap(); - pack.tbins.get(&path).map(|tb| tb.map_id) - }) - { - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(trail_element, &names); - - if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) { - info!(%tex, "failed to find this texture in this pack"); - } - } - - Some(Trail { - category: category_name.clone(), - parent: category_uuid.clone(), - map_id, - props: common_attributes, - guid: parse_guid(names, trail_element), - dynamic: false, - source_file_name, - }) - } else { - let td = trail_element.get_attribute(names.trail_data); - let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); - info!("missing map_id: {td:?} {rp} {tbin:?}"); - None - } - -} - -#[instrument(skip(zip_archive))] -fn read_file_bytes_from_zip_by_name( - name: &str, - zip_archive: &mut zip::ZipArchive, -) -> Option> { - let mut bytes = vec![]; - match zip_archive.by_name(name) { - Ok(mut file) => match file.read_to_end(&mut bytes) { - Ok(size) => { - if size == 0 { - info!("empty file {name}"); - } else { - return Some(bytes); - } - } - Err(e) => { - info!(?e, "failed to read file"); - } - }, - Err(e) => { - info!(?e, "failed to get file from zip"); - } - } - None -} - - -// #[cfg(test)] -// mod test { - -// use indexmap::IndexMap; -// use rstest::*; - -// use semver::Version; -// use similar_asserts::assert_eq; -// use std::io::Write; -// use std::sync::Arc; - -// use zip::write::FileOptions; -// use zip::ZipWriter; - -// use crate::{ -// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, -// INCHES_PER_METER, -// }; - -// const TEST_XML: &str = include_str!("test.xml"); -// const TEST_MARKER_PNG_NAME: &str = "marker.png"; -// const TEST_TRL_NAME: &str = "basic.trl"; - -// #[fixture] -// #[once] -// fn test_zip() -> Vec { -// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); -// // category.xml -// writer -// .start_file("category.xml", FileOptions::default()) -// .expect("failed to create category.xml"); -// writer -// .write_all(TEST_XML.as_bytes()) -// .expect("failed to write category.xml"); -// // marker.png -// writer -// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) -// .expect("failed to create marker.png"); -// writer -// .write_all(MARKER_PNG) -// .expect("failed to write marker.png"); -// // basic.trl -// writer -// .start_file(TEST_TRL_NAME, FileOptions::default()) -// .expect("failed to create basic trail"); -// writer -// .write_all(&0u32.to_ne_bytes()) -// .expect("failed to write version"); -// writer -// .write_all(&15u32.to_ne_bytes()) -// .expect("failed to write mapid "); -// writer -// .write_all(bytemuck::cast_slice(&[0f32; 3])) -// .expect("failed to write first node"); -// // done -// writer -// .finish() -// .expect("failed to finalize zip") -// .into_inner() -// } - -// #[fixture] -// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { -// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); -// assert_eq!(file_entries.len(), 3); -// let test_xml = std::str::from_utf8( -// file_entries -// .get(String::new("category.xml")) -// .expect("failed to get category.xml"), -// ) -// .expect("failed to get str from category.xml contents"); -// assert_eq!(test_xml, TEST_XML); -// let test_marker_png = file_entries -// .get(String::new("marker.png")) -// .expect("failed to get marker.png"); -// assert_eq!(test_marker_png, MARKER_PNG); -// file_entries -// } -// #[fixture] -// #[once] -// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { -// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); -// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); -// assert_eq!(pack.tbins.len(), 1); -// assert_eq!(pack.textures.len(), 1); -// assert_eq!( -// pack.textures -// .get(String::new(TEST_MARKER_PNG_NAME)) -// .expect("failed to get marker.png from textures"), -// MARKER_PNG -// ); - -// let tbin = pack -// .tbins -// .get(String::new(TEST_TRL_NAME)) -// .expect("failed to get basic trail") -// .clone(); - -// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); -// pack -// } - -// // #[rstest] -// // fn test_tag(test_pack: &Pack) { -// // let mut test_category_menu = CategoryMenu::default(); -// // let parent_path = String::new("parent"); -// // let child1_path = String::new("parent/child1"); -// // let subchild_path = String::new("parent/child1/subchild"); -// // let child2_path = String::new("parent/child2"); -// // test_category_menu.create_category(subchild_path); -// // test_category_menu.create_category(child2_path); -// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); -// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); -// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); -// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); - -// // assert_eq!(test_category_menu, test_pack.category_menu) -// // } - -// #[rstest] -// fn test_markers(test_pack: &Pack) { -// let marker = test_pack -// .markers -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// marker.props.texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); -// } -// #[rstest] -// fn test_trails(test_pack: &Pack) { -// let trail = test_pack -// .trails -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// trail.props.tbin.as_ref().unwrap(), -// String::new(TEST_TRL_NAME) -// ); -// assert_eq!( -// trail.props.trail_texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// } -// } diff --git a/crates/joko_marker_format/src/io/error.rs b/crates/joko_marker_format/src/io/error.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/joko_marker_format/src/io/error.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs deleted file mode 100644 index 2640b97..0000000 --- a/crates/joko_marker_format/src/io/mod.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! This modules primarily deals with serializing and deserializing xml data from marker packs -//! - -use xot::{NameId, Xot}; - -mod deserialize; -mod error; -mod serialize; - -pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; -pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; -pub(crate) struct XotAttributeNameIDs { - // xml tags - pub overlay_data: NameId, - pub marker_category: NameId, - pub pois: NameId, - pub poi: NameId, - pub trail: NameId, - pub route: NameId, - // marker specific attributes - pub category: NameId, - pub guid: NameId, - pub map_id: NameId, - pub xpos: NameId, - pub ypos: NameId, - pub zpos: NameId, - // marker category specific attributes - pub default_enabled: NameId, - pub display_name: NameId, - pub name: NameId, - pub CapitalName: NameId,//same than "name" but with a starting capital letter - pub separator: NameId, - // inheritable attributes - pub achievement_id: NameId, - pub achievement_bit: NameId, - pub alpha: NameId, - pub anim_speed: NameId, - pub auto_trigger: NameId, - pub behavior: NameId, - pub bounce: NameId, - pub bounce_delay: NameId, - pub bounce_duration: NameId, - pub bounce_height: NameId, - pub can_fade: NameId, - pub color: NameId, - pub copy: NameId, - pub copy_message: NameId, - pub cull: NameId, - pub fade_far: NameId, - pub fade_near: NameId, - pub festival: NameId, - pub has_countdown: NameId, - pub height_offset: NameId, - pub hide: NameId, - pub icon_file: NameId, - pub icon_size: NameId, - pub in_game_visibility: NameId, - pub info: NameId, - pub info_range: NameId, - pub invert_behavior: NameId, - pub is_wall: NameId, - pub keep_on_map_edge: NameId, - pub map_display_size: NameId, - pub map_fade_out_scale_level: NameId, - pub map_type: NameId, - pub map_visibility: NameId, - pub max_size: NameId, - pub min_size: NameId, - pub mini_map_visibility: NameId, - pub mount: NameId, - pub profession: NameId, - pub race: NameId, - pub reset_length: NameId, - pub reset_offset: NameId, - pub rotate: NameId, - pub rotate_x: NameId, - pub rotate_y: NameId, - pub rotate_z: NameId, - pub scale_on_map_with_zoom: NameId, - pub show: NameId, - pub specialization: NameId, - pub text: NameId, - pub texture: NameId, - pub tip_name: NameId, - pub tip_description: NameId, - pub title: NameId, - pub title_color: NameId, - pub toggle_category: NameId, - pub trail_data: NameId, - pub trail_scale: NameId, - pub trigger_range: NameId, - pub reset_range: NameId, - pub resetposx: NameId, - pub resetposy: NameId, - pub resetposz: NameId, - pub _source_file_name: NameId, -} -impl XotAttributeNameIDs { - pub fn register_with_xot(tree: &mut Xot) -> Self { - Self { - // tags - overlay_data: tree.add_name("OverlayData"), - marker_category: tree.add_name("MarkerCategory"), - pois: tree.add_name("POIs"), - poi: tree.add_name("POI"), - trail: tree.add_name("Trail"), - route: tree.add_name("Route"), - // non inheritable attributes - category: tree.add_name("type"), - xpos: tree.add_name("xpos"), - ypos: tree.add_name("ypos"), - zpos: tree.add_name("zpos"), - map_id: tree.add_name("MapID"), - guid: tree.add_name("GUID"), - - // marker category specific attrs - separator: tree.add_name("IsSeparator"), - default_enabled: tree.add_name("defaulttoggle"), - display_name: tree.add_name("DisplayName"), - name: tree.add_name("name"), - CapitalName: tree.add_name("Name"), - // inheritable attributes - achievement_id: tree.add_name("achievementId"), - achievement_bit: tree.add_name("achievementBit"), - alpha: tree.add_name("alpha"), - anim_speed: tree.add_name("animSpeed"), - auto_trigger: tree.add_name("autotrigger"), - behavior: tree.add_name("behavior"), - color: tree.add_name("color"), - copy: tree.add_name("copy"), - copy_message: tree.add_name("copy-message"), - fade_near: tree.add_name("fadeNear"), - fade_far: tree.add_name("fadeFar"), - festival: tree.add_name("festival"), - has_countdown: tree.add_name("hasCountdown"), - height_offset: tree.add_name("heightOffset"), - icon_file: tree.add_name("iconFile"), - icon_size: tree.add_name("iconSize"), - in_game_visibility: tree.add_name("inGameVisibility"), - info: tree.add_name("info"), - info_range: tree.add_name("infoRange"), - map_display_size: tree.add_name("mapDisplaySize"), - map_visibility: tree.add_name("mapVisibility"), - max_size: tree.add_name("maxSize"), - min_size: tree.add_name("minSize"), - mini_map_visibility: tree.add_name("miniMapVisibility"), - mount: tree.add_name("mount"), - profession: tree.add_name("profession"), - race: tree.add_name("race"), - reset_length: tree.add_name("resetLength"), - reset_offset: tree.add_name("resetOffset"), - scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), - tip_name: tree.add_name("tip-name"), - tip_description: tree.add_name("tip-description"), - toggle_category: tree.add_name("togglecateogry"), - texture: tree.add_name("texture"), - trail_data: tree.add_name("trailData"), - trail_scale: tree.add_name("trailScale"), - trigger_range: tree.add_name("triggerRange"), - bounce_delay: tree.add_name("bounce-delay"), - bounce_duration: tree.add_name("bounce-duration"), - bounce_height: tree.add_name("bounce-height"), - can_fade: tree.add_name("canfade"), - cull: tree.add_name("cull"), - hide: tree.add_name("hide"), - is_wall: tree.add_name("iswall"), - invert_behavior: tree.add_name("invertbehavior"), - map_type: tree.add_name("maptype"), - rotate: tree.add_name("rotate"), - rotate_x: tree.add_name("rotate-x"), - rotate_y: tree.add_name("rotate-y"), - rotate_z: tree.add_name("rotate-z"), - show: tree.add_name("show"), - specialization: tree.add_name("specialization"), - title: tree.add_name("title"), - title_color: tree.add_name("title-color"), - text: tree.add_name("text"), - bounce: tree.add_name("bounce"), - keep_on_map_edge: tree.add_name("keepOnMapEdge"), - map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), - reset_range: tree.add_name("resetrange"), - resetposx: tree.add_name("resetposx"), - resetposy: tree.add_name("resetposy"), - resetposz: tree.add_name("resetposz"), - _source_file_name: tree.add_name("_source_file_name"), - } - } -} diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs deleted file mode 100644 index 2155f20..0000000 --- a/crates/joko_marker_format/src/io/serialize.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::{ - pack::{Category, Marker, Trail, Route}, - manager::{LoadedPackData, LoadedPackTexture}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::Dir; -use indexmap::IndexMap; -use miette::{Context, IntoDiagnostic, Result}; -use std::io::Write; -use tracing::info; -use uuid::Uuid; -use xot::{Element, Node, SerializeOptions, Xot}; - -use super::XotAttributeNameIDs; -/// Save the pack core as xml pack using the given directory as pack root path. -pub(crate) fn save_pack_data_to_dir( - pack_data: &LoadedPackData, - writing_directory: &Dir, -) -> Result<()> { - // save categories - info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) - .wrap_err("failed to serialize cats")?; - let cats = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to convert cats xot to string")?; - writing_directory.create("categories.xml") - .into_diagnostic() - .wrap_err("failed to create categories.xml")? - .write_all(cats.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write to categories.xml")?; - // save maps - for (map_id, map_data) in pack_data.maps.iter() { - if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node: Node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create root wiht overlay data for pois")?; - let pois = tree.new_element(names.pois); - tree.append(od, pois) - .into_diagnostic() - .wrap_err("faild to append pois to od node")?; - for marker in map_data.markers.values() { - let poi = tree.new_element(names.poi); - tree.append(pois, poi) - .into_diagnostic() - .wrap_err("failed to append poi (marker) to pois")?; - let ele = tree.element_mut(poi).unwrap(); - serialize_marker_to_element(marker, ele, &names); - } - for route_path in map_data.routes.values() { - serialize_route_to_element(&mut tree, route_path, &pois, &names)?; - } - for trail in map_data.trails.values() { - if trail.dynamic { - continue; - } - let trail_node = tree.new_element(names.trail); - tree.append(pois, trail_node) - .into_diagnostic() - .wrap_err("failed to append a trail node to pois")?; - let ele = tree.element_mut(trail_node).unwrap(); - serialize_trail_to_element(trail, ele, &names); - } - let map_xml = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to serialize map data to string")?; - writing_directory.create(format!("{map_id}.xml")) - .into_diagnostic() - .wrap_err("failed to create map xml file")? - .write_all(map_xml.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write map data to file")?; - } - Ok(()) -} -pub(crate) fn save_pack_texture_to_dir( - pack_texture: &LoadedPackTexture, - writing_directory: &Dir, -) -> Result<()> { - - info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); - // save images - for (img_path, img) in pack_texture.textures.iter() { - if let Some(parent) = img_path.parent() { - writing_directory.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir for an image: {img_path}") - })?; - } - writing_directory.create(img_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(img) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to write image bytes to file: {img_path}") - })?; - } - // save tbins - for (tbin_path, tbin) in pack_texture.tbins.iter() { - if let Some(parent) = tbin_path.parent() { - writing_directory.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir of tbin: {tbin_path}") - })?; - } - let mut bytes: Vec = vec![]; - bytes.reserve(8 + tbin.nodes.len() * 12); - bytes.extend_from_slice(&tbin.version.to_ne_bytes()); - bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); - for node in &tbin.nodes { - bytes.extend_from_slice(&node[0].to_ne_bytes()); - bytes.extend_from_slice(&node[1].to_ne_bytes()); - bytes.extend_from_slice(&node[2].to_ne_bytes()); - } - writing_directory.create(tbin_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? - .write_all(&bytes) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; - } - Ok(()) -} - -fn recursive_cat_serializer( - tree: &mut Xot, - names: &XotAttributeNameIDs, - cats: &IndexMap, - parent: Node, -) -> Result<()> { - for (_, cat) in cats { - let cat_node = tree.new_element(names.marker_category); - tree.append(parent, cat_node).into_diagnostic()?; - { - let ele = tree.element_mut(cat_node).unwrap(); - ele.set_attribute(names.display_name, &cat.display_name); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); - // let cat_name = tree.add_name(cat_name); - ele.set_attribute(names.name, &cat.relative_category_name); - // no point in serializing default values - if !cat.default_enabled { - ele.set_attribute(names.default_enabled, "0"); - } - if cat.separator { - ele.set_attribute(names.separator, "1"); - } - cat.props.serialize_to_element(ele, names); - } - recursive_cat_serializer(tree, names, &cat.children, cat_node)?; - } - Ok(()) -} -fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); - ele.set_attribute(names.category, &trail.category); - ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute(names._source_file_name, &trail.source_file_name); - trail.props.serialize_to_element(ele, names); -} - -fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.xpos, format!("{}", marker.position[0])); - ele.set_attribute(names.ypos, format!("{}", marker.position[1])); - ele.set_attribute(names.zpos, format!("{}", marker.position[2])); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); - ele.set_attribute(names.map_id, format!("{}", marker.map_id)); - ele.set_attribute(names.category, &marker.category); - ele.set_attribute(names._source_file_name, &marker.source_file_name); - marker.attrs.serialize_to_element(ele, names); -} - -fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { - let route_node = tree.new_element(names.route); - tree.append(*parent, route_node) - .into_diagnostic() - .wrap_err("failed to append route to pois")?; - let ele = tree.element_mut(route_node).unwrap(); - - ele.set_attribute(names.category, route.category.clone()); - ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); - ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); - ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); - ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); - ele.set_attribute(names.name, route.name.clone()); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); - ele.set_attribute(names.map_id, format!("{}", route.map_id)); - ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute(names._source_file_name, &route.source_file_name); - for pos in &route.path { - let child = tree.new_element(names.poi); - tree.append(route_node, child); - let child_elt = tree.element_mut(child).unwrap(); - child_elt.set_attribute(names.xpos, format!("{}", pos.x)); - child_elt.set_attribute(names.ypos, format!("{}", pos.y)); - child_elt.set_attribute(names.zpos, format!("{}", pos.z)); - //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); - } - Ok(()) -} - diff --git a/crates/joko_marker_format/src/io/test.xml b/crates/joko_marker_format/src/io/test.xml deleted file mode 100644 index 3b50657..0000000 --- a/crates/joko_marker_format/src/io/test.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/crates/joko_marker_format/src/io/xmlfile_schema.xsd b/crates/joko_marker_format/src/io/xmlfile_schema.xsd deleted file mode 100644 index 895a0ac..0000000 --- a/crates/joko_marker_format/src/io/xmlfile_schema.xsd +++ /dev/null @@ -1,394 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_marker_format/src/lib.rs deleted file mode 100644 index de5b736..0000000 --- a/crates/joko_marker_format/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! ReadOnly XML marker packs support for Jokolay -//! -//! - -pub(crate) mod io; -pub(crate) mod manager; -pub(crate) mod pack; -pub mod message; - -pub use manager::{ - PackageDataManager, - PackageUIManager, - LoadedPackData, - LoadedPackTexture, - load_all_from_dir, - build_from_core, - ImportStatus, - import_pack_from_zip_file_path -}; - -// for compile time build info like pkg version or build timestamp or git hash etc.. -// shadow_rs::shadow!(build); - -// to filter the xml with rapidxml first -#[cxx::bridge(namespace = "rapid")] -mod ffi { - unsafe extern "C++" { - include!("joko_marker_format/vendor/rapid/rapid.hpp"); - pub fn rapid_filter(src_xml: String) -> String; - - } -} - -pub fn rapid_filter_rust(src_xml: String) -> String { - ffi::rapid_filter(src_xml) -} - -pub const INCHES_PER_METER: f32 = 39.37; - -pub fn is_default(t: &T) -> bool { - t == &T::default() -} - -pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( - &base64::alphabet::STANDARD, - base64::engine::GeneralPurposeConfig::new(), -); diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs deleted file mode 100644 index c6064dd..0000000 --- a/crates/joko_marker_format/src/manager/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! How should the pack be stored by jokolay? -//! 1. Inside a directory called packs, we will have a separate directory for each pack. -//! 2. the name of the directory will serve as an ID for each pack. -//! 3. Inside the directory, we will have -//! 1. categories.xml -> The xml file which contains the whole category tree -//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. -//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. - -/* -expensive: -categories being a tree with order among siblings (better to use a tree crate?) -markers/trails referring to a category via full path. -editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. - -We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. - -*/ - -mod package; -mod pack; - -pub use package::{PackageDataManager, PackageUIManager}; -pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; -pub use pack::import::{ImportStatus, import_pack_from_zip_file_path}; \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/activation.rs b/crates/joko_marker_format/src/manager/pack/activation.rs deleted file mode 100644 index a0a2a83..0000000 --- a/crates/joko_marker_format/src/manager/pack/activation.rs +++ /dev/null @@ -1,21 +0,0 @@ -use indexmap::IndexMap; -use uuid::Uuid; - - -/// This is the activation data per pack -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -pub struct ActivationData { - /// this is for markers which are global and only activate once regardless of account - pub global: IndexMap, - /// this is the activation data per character - /// for markers which trigger once per character - pub character: IndexMap>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ActivationType { - /// clean these up when the map is changed - ReappearOnMapChange, - /// clean these up when the timestamp is reached - TimeStamp(time::OffsetDateTime), - Instance(std::net::IpAddr), -} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/active.rs b/crates/joko_marker_format/src/manager/pack/active.rs deleted file mode 100644 index a274352..0000000 --- a/crates/joko_marker_format/src/manager/pack/active.rs +++ /dev/null @@ -1,290 +0,0 @@ -use std::collections::HashSet; - -use ordered_hash_map::OrderedHashMap; - -use egui::TextureHandle; -use glam::{vec2, Vec2, Vec3}; -use indexmap::IndexMap; -use crate::message::{MarkerObject, MarkerVertex, TrailObject}; -use uuid::Uuid; - -use joko_core::RelativePath; -use crate::{ - pack::CommonAttributes, - INCHES_PER_METER, -}; -use jokolink::MumbleLink; - -/* -- activation data with uuids and track the latest timestamp that will be activated -- category activation data -> track and changes to propagate to markers of this map -- current active markers, which will keep track of their original marker, so as to propagate any changes easily -*/ -#[derive(Clone)] -pub struct ActiveTrail { - pub trail_object: TrailObject, - pub texture_handle: TextureHandle, -} -/// This is an active marker. -/// It stores all the info that we need to scan every frame -#[derive(Clone)] -pub(crate) struct ActiveMarker { - /// texture id from managed textures - pub texture_id: u64, - /// owned texture handle to keep it alive - pub _texture: TextureHandle, - /// position - pub pos: Vec3, - /// billboard must not be bigger than this size in pixels - pub max_pixel_size: f32, - /// billboard must not be smaller than this size in pixels - pub min_pixel_size: f32, - pub common_attributes: CommonAttributes, -} - -pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; - -impl ActiveMarker { - pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { - let Self { - texture_id, - pos, - common_attributes: attrs, - _texture, - max_pixel_size, - min_pixel_size, - .. - } = self; - // let width = *width; - // let height = *height; - let texture_id = *texture_id; - let pos = *pos; - // filters - if let Some(mounts) = attrs.get_mount() { - if let Some(current) = link.mount { - if !mounts.contains(current) { - return None; - } - } else { - return None; - } - } - let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); - let player_distance = pos.distance(link.player_pos); - let camera_distance = pos.distance(link.cam_pos); - let fade_near_far = Vec2::new(fade_near, fade_far); - - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let color = attrs.get_color().copied().unwrap_or_default(); - /* - 1. we need to filter the markers - 1. statically - mapid, character, map_type, race, profession - 2. dynamically - achievement, behavior, mount, fade_far, cull - 3. force hide/show by user discretion - 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior - 3. store the state for these markers activation data, and temporary data like bounce - */ - /* - skip if: - alpha is 0.0 - achievement id/bit is done (maybe this should be at map filter level?) - behavior (activation) - cull - distance > fade_far - visibility (ingame/map/minimap) - mount - specialization - */ - if fade_far > 0.0 && player_distance > fade_far { - return None; - } - // markers are 1 meter in width/height by default - let mut pos = pos; - pos.y += height_offset; - let direction_to_marker = link.cam_pos - pos; - let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); - - let far_offset = { - let dpi = if link.dpi_scaling <= 0 { - 96.0 - } else { - link.dpi as f32 - } / 96.0; - let gw2_width = link.client_size.as_vec2().x / dpi; - - // offset (half width i.e. distance from center of the marker to the side of the marker) - const SIDE_OFFSET_FAR: f32 = 1.0; - // the size of the projected on to the near plane - let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); - // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width - let near_offset_in_pixels = near_offset * gw2_width; - - // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width - let near_offset_in_pixels = near_offset_in_pixels - .clamp(*min_pixel_size, *max_pixel_size) - .min(gw2_width / 2.0); - - let near_offset_of_marker = near_offset_in_pixels / gw2_width; - near_offset_of_marker * camera_distance / z_near - }; - // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; - // we want to map 100 pixels to one meter in game - // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard - // But, i will ignore that as that makes markers too small - let x_offset = far_offset; - let y_offset = x_offset; // seems all markers are squares - let bottom_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 1.0), - alpha, - color, - fade_near_far, - }; - - let top_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 0.0), - alpha, - color, - fade_near_far, - }; - let top_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 0.0), - alpha, - color, - fade_near_far, - }; - let bottom_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 1.0), - alpha, - color, - fade_near_far, - }; - let vertices = [ - top_left, - bottom_left, - bottom_right, - bottom_right, - top_right, - top_left, - ]; - Some(MarkerObject { - vertices, - texture: texture_id, - distance: player_distance, - }) - } -} - -impl ActiveTrail { - pub fn get_vertices_and_texture( - attrs: &CommonAttributes, - positions: &[Vec3], - texture: TextureHandle, - ) -> Option { - // can't have a trail without atleast two nodes - if positions.len() < 2 { - return None; - } - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_near_far = Vec2::new(fade_near, fade_far); - let color = attrs.get_color().copied().unwrap_or([0u8; 4]); - // default taco width - let horizontal_offset = 20.0 / INCHES_PER_METER; - // scale it trail scale - let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); - let height = horizontal_offset * 2.0; - - let mut vertices = vec![]; - // trail mesh is split by separating different parts with a [0, 0, 0] - // we will call each separate trail mesh as a "strip" of trail. - // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. - for strip in positions.split(|&v| v == Vec3::ZERO) { - let mut y_offset = 1.0; - for two_positions in strip.windows(2) { - let first = two_positions[0]; - let second = two_positions[1]; - // right side of the vector from first to second - let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); - - let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; - let first_left = MarkerVertex { - position: first - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, y_offset), - alpha, - color, - fade_near_far, - }; - let first_right = MarkerVertex { - position: first + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, y_offset), - alpha, - color, - fade_near_far, - }; - let second_left = MarkerVertex { - position: second - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, new_offset), - alpha, - color, - fade_near_far, - }; - let second_right = MarkerVertex { - position: second + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, new_offset), - alpha, - color, - fade_near_far, - }; - y_offset = if new_offset.is_sign_positive() { - new_offset - } else { - 1.0 - new_offset.fract().abs() - }; - vertices.extend([ - second_left, - first_left, - first_right, - first_right, - second_right, - second_left, - ]); - } - } - - Some(ActiveTrail { - trail_object: TrailObject { - vertices: vertices.into(), - texture: match texture.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }, - }, - texture_handle: texture, - }) - } -} - -#[derive(Default, Clone)] -pub(crate) struct CurrentMapData { - /// the map to which the current map data belongs to - pub map_id: u32, - //pub active_elements: HashSet, - /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: OrderedHashMap, - /// The key is the index of the marker in the map markers - /// Their position in the map markers serves as their "id" as uuids can be duplicates. - pub active_markers: IndexMap, - pub wip_markers: IndexMap, - /// The key is the position/index of this trail in the map trails. same as markers - pub active_trails: IndexMap, - pub wip_trails: IndexMap, -} - diff --git a/crates/joko_marker_format/src/manager/pack/category_selection.rs b/crates/joko_marker_format/src/manager/pack/category_selection.rs deleted file mode 100644 index 367b79d..0000000 --- a/crates/joko_marker_format/src/manager/pack/category_selection.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::collections::{HashSet, HashMap}; -use ordered_hash_map::OrderedHashMap; - -use indexmap::IndexMap; -use uuid::Uuid; - -use crate::{ - message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, PackCore} -}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct CategorySelection { - //#[serde(skip)] - pub uuid: Uuid,//FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information - #[serde(skip)] - pub parent: Option, - pub is_selected: bool,//has it been selected in configuration to be displayed - pub is_active: bool,//currently being displayed (i.e.: active) - pub separator: bool, - pub display_name: String, - pub children: OrderedHashMap, -} - -pub struct SelectedCategoryManager { - data: OrderedHashMap, - -} -impl<'a> SelectedCategoryManager { - pub fn new( - selected_categories: &OrderedHashMap, - categories: &IndexMap - ) -> Self { - let mut list_of_enabled_categories = Default::default(); - CategorySelection::get_list_of_enabled_categories( - &selected_categories, - &categories, - &mut list_of_enabled_categories, - &Default::default(), - ); - - Self { data: list_of_enabled_categories } - } - pub fn cloned_data(&self) -> OrderedHashMap { - self.data.clone() - } - pub fn is_selected(&self, category: &Uuid) -> bool { - self.data.contains_key(category) - } - pub fn get(&self, key: &Uuid) -> &CommonAttributes { - self.data.get(key).unwrap() - } - pub fn len(&self) -> usize { - self.data.len() - } - pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { - self.data.keys() - } -} - - - -impl CategorySelection { - pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { - let mut selectable_categories = OrderedHashMap::new(); - Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); - selectable_categories - } - fn get_list_of_enabled_categories( - selection: &OrderedHashMap, - categories: &IndexMap, - list_of_enabled_categories: &mut OrderedHashMap, - parent_common_attributes: &CommonAttributes, - ) { - for (_, cat) in categories { - if let Some(selectable_category) = selection.get(&cat.relative_category_name) { - if !selectable_category.is_selected { - continue; - } - let mut common_attributes = cat.props.clone(); - common_attributes.inherit_if_attr_none(parent_common_attributes); - Self::get_list_of_enabled_categories( - &selectable_category.children, - &cat.children, - list_of_enabled_categories, - &common_attributes, - ); - list_of_enabled_categories.insert(cat.guid, common_attributes); - } - } - } - pub fn get(selection: &mut OrderedHashMap, uuid: Uuid) -> Option<&mut CategorySelection> { - if selection.is_empty() { - return None; - } else { - for cat in selection.values_mut() { - if cat.uuid == uuid { - return Some(cat); - } - if let Some(res) = Self::get(&mut cat.children, uuid) { - return Some(res); - } - } - return None; - } - } - pub fn recursive_populate_guids( - selection: &mut OrderedHashMap, - entities_parents: &mut HashMap, - parent_uuid: Option, - ) { - for (cat_name, cat) in selection.iter_mut() { - if cat.uuid.is_nil() { - cat.uuid = Uuid::new_v4(); - } - cat.parent = parent_uuid.clone(); - Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); - if parent_uuid.is_some() { - entities_parents.insert(cat.uuid, parent_uuid.unwrap().clone()); - } - //assert!(cat.guid.len() > 0); - } - } - fn recursive_create_selectable_categories( - selectable_categories: &mut OrderedHashMap, - cats: &IndexMap, - ) { - for (_, cat) in cats.iter() { - if !selectable_categories.contains_key(&cat.relative_category_name) { - let to_insert = CategorySelection { - uuid: cat.guid, - parent: cat.parent, - is_selected: cat.default_enabled, - is_active: !cat.separator,//by default separators are not considered active since they contain nothing - separator: cat.separator, - display_name: cat.display_name.clone(), - children: Default::default(), - }; - //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); - selectable_categories.insert(cat.relative_category_name.clone(), to_insert); - } - let s = selectable_categories.get_mut(&cat.relative_category_name).unwrap(); - Self::recursive_create_selectable_categories(&mut s.children, &cat.children); - } - } - - pub fn recursive_set(selection: &mut OrderedHashMap, uuid: Uuid, status: bool) -> bool { - if selection.is_empty() { - return false; - } else { - for cat in selection.values_mut() { - if cat.separator { - continue; - } - if cat.uuid == uuid { - cat.is_selected = status; - return true; - } - if Self::recursive_set(&mut cat.children, uuid, status) { - return true; - } - } - return false; - } - } - pub fn recursive_set_all(selection: &mut OrderedHashMap, status: bool) { - if selection.is_empty() { - return; - } - for cat in selection.values_mut() { - if cat.separator { - continue; - } - cat.is_selected = status; - Self::recursive_set_all(&mut cat.children, status); - } - } - - pub fn recursive_update_active_categories(selection: &mut OrderedHashMap, active_elements: &HashSet) -> bool { - let mut is_active = false; - if selection.is_empty() { - //println!("recursive_update_active_categories is_empty"); - return is_active; - } - for cat in selection.values_mut() { - cat.is_active = active_elements.contains(&cat.uuid) || Self::recursive_update_active_categories(&mut cat.children, active_elements); - if cat.is_active { - is_active = true; - } - } - return is_active; - } - - fn context_menu( - u2b_sender: &std::sync::mpsc::Sender, - cs: &mut CategorySelection, - ui: &mut egui::Ui - ) { - if ui.button("Activate branch").clicked() { - cs.is_selected = true; - CategorySelection::recursive_set_all(&mut cs.children, true); - u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); - ui.close_menu(); - } - if ui.button("Deactivate branch").clicked() { - CategorySelection::recursive_set_all(&mut cs.children, false); - cs.is_selected = false; - u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); - ui.close_menu(); - } - } - - pub fn recursive_selection_ui( - u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, - selection: &mut OrderedHashMap, - ui: &mut egui::Ui, - is_dirty: &mut bool, - show_only_active: bool, - late_discovery_categories: &HashSet, - ) { - if selection.is_empty() { - return; - } - egui::ScrollArea::vertical().show(ui, |ui| { - for (name, cat) in selection.iter_mut() { - if !cat.is_active && show_only_active && !cat.separator { - continue; - } - ui.horizontal(|ui| { - if cat.separator { - ui.add_space(3.0); - } else { - let cb = ui.checkbox(&mut cat.is_selected, ""); - if cb.changed() { - u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); - *is_dirty = true; - } - } - //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); - let color = if late_discovery_categories.contains(&cat.uuid) { - egui::Color32::LIGHT_RED - } else if cat.is_active { - egui::Color32::LIGHT_GREEN - } else { - egui::Color32::GRAY - }; - let label = egui::RichText::new(&cat.display_name).color(color); - if cat.children.is_empty() { - ui.label(label); - } else { - ui.menu_button(label, |ui: &mut egui::Ui| { - Self::recursive_selection_ui( - u2b_sender, - u2u_sender, - &mut cat.children, - ui, - is_dirty, - show_only_active, - late_discovery_categories - ); - }).response.context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); - } - }); - } - }); - } -} - diff --git a/crates/joko_marker_format/src/manager/pack/dirty.rs b/crates/joko_marker_format/src/manager/pack/dirty.rs deleted file mode 100644 index 3dd900c..0000000 --- a/crates/joko_marker_format/src/manager/pack/dirty.rs +++ /dev/null @@ -1,29 +0,0 @@ - -use ordered_hash_map::OrderedHashSet; - -use joko_core::RelativePath; - -#[derive(Debug, Default, Clone)] -pub(crate) struct DirtyMarker { - pub all: bool, - /// whether categories need to be saved - pub categories: bool, - /// whether selected categories needs to be saved - pub selected_categories: bool, - /// Whether any mapdata needs saving - pub map: OrderedHashSet, - /// whether any texture needs saving - pub texture: OrderedHashSet, - /// whether any tbin needs saving - pub tbin: OrderedHashSet, -} - -impl DirtyMarker { - pub fn is_dirty(&self) -> bool { - self.categories - || self.selected_categories - || !self.map.is_empty() - || !self.texture.is_empty() - || !self.tbin.is_empty() - } -} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/entry.rs b/crates/joko_marker_format/src/manager/pack/entry.rs deleted file mode 100644 index ad78681..0000000 --- a/crates/joko_marker_format/src/manager/pack/entry.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Debug)] -pub struct PackEntry { - pub url: url::Url, - pub description: String, -} - diff --git a/crates/joko_marker_format/src/manager/pack/file_selection.rs b/crates/joko_marker_format/src/manager/pack/file_selection.rs deleted file mode 100644 index 73d27cd..0000000 --- a/crates/joko_marker_format/src/manager/pack/file_selection.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::{ - collections::BTreeMap, -}; - -pub struct SelectedFileManager { - data: BTreeMap, - -} -impl<'a> SelectedFileManager { - pub fn new( - selected_files: &BTreeMap, - pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, - ) -> Self { - let mut list_of_enabled_files: BTreeMap = Default::default(); - SelectedFileManager::recursive_get_full_names( - &selected_files, - &pack_source_files, - ¤tly_used_files, - &mut list_of_enabled_files, - ); - Self { data: list_of_enabled_files } - } - fn recursive_get_full_names( - _selected_files: &BTreeMap, - _pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, - list_of_enabled_files: &mut BTreeMap - ){ - for (key, v) in currently_used_files.iter() { - list_of_enabled_files.insert(key.clone(), *v); - } - } - pub fn cloned_data(&self) -> BTreeMap { - self.data.clone() - } - pub fn is_selected(&self, source_file_name: &String) -> bool { - let default = false; - self.data.is_empty() || *self.data.get(source_file_name).unwrap_or(&default) - } - pub fn len(&self) -> usize { - self.data.len() - } -} diff --git a/crates/joko_marker_format/src/manager/pack/import.rs b/crates/joko_marker_format/src/manager/pack/import.rs deleted file mode 100644 index 4100841..0000000 --- a/crates/joko_marker_format/src/manager/pack/import.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::{ - io::Read, -}; -use tracing::{info}; - -use miette::{IntoDiagnostic, Result}; -use crate::pack::PackCore; - - -#[derive(Debug, Default)] -pub enum ImportStatus { - #[default] - UnInitialized, - WaitingForFileChooser, - LoadingPack(std::path::PathBuf), - WaitingLoading(std::path::PathBuf), - PackDone(String, PackCore, bool), - PackError(miette::Report), -} - -pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { - let mut taco_zip = vec![]; - std::fs::File::open(&file_path) - .into_diagnostic()? - .read_to_end(&mut taco_zip) - .into_diagnostic()?; - - info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { - ( - file_path - .file_name() - .map(|ostr| ostr.to_string_lossy().to_string()) - .unwrap_or_default(), - pack, - ) - }) -} \ No newline at end of file diff --git a/crates/joko_marker_format/src/manager/pack/list.rs b/crates/joko_marker_format/src/manager/pack/list.rs deleted file mode 100644 index 499fe2f..0000000 --- a/crates/joko_marker_format/src/manager/pack/list.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Debug, Default)] -pub struct PackList { - pub packs: BTreeMap, -} - - diff --git a/crates/joko_marker_format/src/manager/pack/loaded.rs b/crates/joko_marker_format/src/manager/pack/loaded.rs deleted file mode 100644 index 8427dc2..0000000 --- a/crates/joko_marker_format/src/manager/pack/loaded.rs +++ /dev/null @@ -1,789 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, sync::Arc -}; - -use indexmap::IndexMap; -use ordered_hash_map::OrderedHashMap; - -use cap_std::fs_utf8::Dir; -use egui::{ColorImage, TextureHandle}; -use image::EncodableLayout; -use tracing::{debug, error, info, info_span}; -use uuid::Uuid; - -use crate::{ - io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} -}; -use jokolink::MumbleLink; -use joko_core::{ - task::{AsyncTask, AsyncTaskGuard}, - RelativePath -}; -use crate::message::{ - BackToUIMessage, TrailObject -}; -use miette::{bail, Context, IntoDiagnostic, Result}; - -use super::activation::{ActivationData, ActivationType}; -use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; -use crate::manager::pack::category_selection::CategorySelection; -use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; - - -//TODO: separate in front and back tasks -pub (crate) struct PackTasks { - //an object that can handle such tasks should be passed as argument of any function that may required an async action - save_texture_task: AsyncTask>, - save_data_task: AsyncTask>, - load_all_packs_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> -} - -#[derive(Clone)] -pub struct LoadedPackData { - pub name: String, - pub uuid: Uuid, - pub dir: Arc
, - /// The actual xml pack. - //pub core: PackCore, - pub categories: IndexMap, - pub all_categories: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: HashMap, - selected_files: BTreeMap, - _is_dirty: bool,//there was an edition in the package itself - - // loca copy in the data side of what is exposed in UI - selectable_categories: OrderedHashMap, - pub entities_parents: HashMap, - activation_data: ActivationData, - active_elements: HashSet,//keep track of which elements are active -} - -#[derive(Clone)] -pub struct LoadedPackTexture { - pub name: String, - pub uuid: Uuid, - /// The directory inside which the pack data is stored - /// There should be a subdirectory called `core` which stores the pack core - /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. - /// eg: Active categories, activation data etc.. - pub dir: Arc, - pub tbins: HashMap, - pub textures: HashMap>, - - /// The selection of categories which are "enabled" and markers belonging to these may be rendered - selectable_categories: OrderedHashMap, - current_map_data: CurrentMapData, - activation_data: ActivationData, - active_elements: HashSet,//which are the active elements (loaded) - pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. - _is_dirty: bool, -} - -impl PackTasks { - pub fn new() -> Self { - Self { - save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), - save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), - load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), - } - } - pub fn is_running(&self) -> bool { - self.save_texture_task.lock().unwrap().is_running() || - self.save_data_task.lock().unwrap().is_running() - } - pub fn count(&self) -> i32 { - 0 - + self.save_texture_task.lock().unwrap().count() - + self.save_data_task.lock().unwrap().count() - + self.load_all_packs_task.lock().unwrap().count() - } - - pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { - if status { - std::mem::take(&mut texture_pack._is_dirty); - self.save_texture_task.lock().unwrap().send( - texture_pack.clone() - ); - } - } - - pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { - if status { - std::mem::take(&mut data_pack._is_dirty); - self.save_data_task.lock().unwrap().send( - data_pack.clone() - ); - } - } - pub fn load_all_packs(&self, jokolay_dir: Arc) { - self.load_all_packs_task.lock().unwrap().send( - jokolay_dir - ); - } - pub fn wait_for_load_all_packs(&self) -> Result<(BTreeMap, BTreeMap)> { - self.load_all_packs_task.lock().unwrap().recv().unwrap() - } - - fn change_map( - &self, - pack: &mut LoadedPackData, - b2u_sender: &std::sync::mpsc::Sender, - link: &MumbleLink, - currently_used_files: &BTreeMap - ) { - //TODO - //self.load_map_task.lock().unwrap().send(pack); - } - - fn async_save_texture( - pack_texture: LoadedPackTexture - ) -> Result<()> { - info!("Save texture package {:?}", pack_texture.dir); - match serde_json::to_string_pretty(&pack_texture.selectable_categories) { - Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - match serde_json::to_string_pretty(&pack_texture.activation_data) { - Ok(ad_json) => match pack_texture.dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { - Ok(_) => { - debug!("wrote activation to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write activation data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize activation"); - } - } - let writing_directory = pack_texture.dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - save_pack_texture_to_dir(&pack_texture, &writing_directory)?; - Ok(()) - } - - fn async_save_data( - pack_data: LoadedPackData - ) -> Result<()> { - info!("Save data package {:?}", pack_data.dir); - pack_data.dir - .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to create xmlpack directory")?; - let writing_directory = pack_data.dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - save_pack_data_to_dir( - &pack_data, - &writing_directory, - )?; - Ok(()) - } - -} - - -impl LoadedPackData { - const CORE_PACK_DIR_NAME: &'static str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; - - fn load_selectable_categories(pack_dir: &Arc, pack: &PackCore) -> OrderedHashMap { - //FIXME: we need to patch those categories from the one in the files - (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { - match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize category data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&pack); - match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - cs - }) - } - pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { - if !pack_dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? - { - bail!("pack core doesn't exist in this pack"); - } - let core_dir = pack_dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - let start = std::time::SystemTime::now(); - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); - - //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. - //let selectable_categories = CategorySelection::default_from_pack_core(&core); - let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); - - Ok(LoadedPackData { - name, - uuid: core.uuid, - dir: pack_dir, - selected_files: Default::default(), - all_categories: core.all_categories, - categories: core.categories, - maps: core.maps, - source_files: core.source_files, - _is_dirty: false, - active_elements: Default::default(), - activation_data: Default::default(), - selectable_categories, - entities_parents: core.entities_parents, - }) - } - - pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { - if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { - self._is_dirty = true; - true - } else { - false - } - } - pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { - if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { - cs.is_selected = status; - self._is_dirty = true; - if CategorySelection::recursive_set(&mut cs.children, uuid, status) { - return true; - } - } - false - } - pub fn category_set_all(&mut self, status: bool) { - CategorySelection::recursive_set_all(&mut self.selectable_categories, status); - self._is_dirty = true; - } - - pub fn is_dirty(&self) -> bool { - self._is_dirty - } - - pub fn tick( - &mut self, - b2u_sender: &std::sync::mpsc::Sender, - loop_index: u128, - link: &MumbleLink, - currently_used_files: &BTreeMap, - list_of_active_or_selected_elements_changed: bool, - map_changed: bool, - tasks: &PackTasks, - next_loaded: &mut HashSet, - ) { - //since the loading of texture is lazy, there is no problem when calling this regularly - if map_changed || list_of_active_or_selected_elements_changed { - tasks.change_map(self, b2u_sender, link, currently_used_files); - let mut active_elements: HashSet = Default::default(); - self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); - self.active_elements = active_elements.clone(); - next_loaded.extend(active_elements); - } - } - - fn on_map_changed( - &mut self, - b2u_sender: &std::sync::mpsc::Sender, - link: &MumbleLink, - currently_used_files: &BTreeMap, - active_elements: &mut HashSet, - ){ - info!(link.map_id, "current map data is updated. {}", self.name); - if link.map_id == 0 { - info!("No map do not do anything"); - return; - } - debug!("Start building SelectedCategoryManager {}", self.selectable_categories.len()); - let selected_categories_manager = SelectedCategoryManager::new(&self.selectable_categories, &self.categories); - - debug!("Start building SelectedFileManager"); - let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.source_files, ¤tly_used_files); - - debug!("Start loading markers"); - let mut nb_markers_attempt = 0; - let mut nb_markers_loaded = 0; - for (_index, marker) in self - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .markers - .values() - .enumerate() - { - nb_markers_attempt += 1; - if selected_files_manager.is_selected(&marker.source_file_name) { - active_elements.insert(marker.guid); - active_elements.insert(marker.parent); - if selected_categories_manager.is_selected(&marker.parent) { - let category_attributes = selected_categories_manager.get(&marker.parent); - let mut common_attributes = marker.attrs.clone();// why a clone ? - common_attributes.inherit_if_attr_none(category_attributes); - let key = &marker.guid; - if let Some(behavior) = common_attributes.get_behavior() { - use crate::pack::Behavior; - if match behavior { - Behavior::AlwaysVisible => false, - Behavior::ReappearOnMapChange - | Behavior::ReappearOnDailyReset - | Behavior::OnlyVisibleBeforeActivation - | Behavior::ReappearAfterTimer - | Behavior::ReappearOnMapReset - | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), - Behavior::OncePerInstance => self - .activation_data - .global - .get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default(), - Behavior::DailyPerChar => - self.activation_data - .character - .get(&link.name) - .map(|a| a.contains_key(key)) - .unwrap_or_default(), - Behavior::OncePerInstancePerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| { - a.get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default() - }) - .unwrap_or_default(), - Behavior::WvWObjective => { - false // ??? - } - } { - continue; - } - } - if let Some(tex_path) = common_attributes.get_icon_file() { - b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); - } else { - debug!("no texture attribute on this marker"); - } - - nb_markers_loaded += 1; - } else { - debug!("category {} = {} is not enabled", marker.category, marker.parent); - } - } - } - - debug!("Start loading trails"); - let mut nb_trails_attempt = 0; - let mut nb_trails_loaded = 0; - for (_index, trail) in self - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .trails - .values() - .enumerate() - { - nb_trails_attempt += 1; - if selected_files_manager.is_selected(&trail.source_file_name) { - active_elements.insert(trail.guid); - active_elements.insert(trail.parent); - if selected_categories_manager.is_selected(&trail.parent) { - let category_attributes = selected_categories_manager.get(&trail.parent); - let mut common_attributes = trail.props.clone(); - common_attributes.inherit_if_attr_none(category_attributes); - if let Some(tex_path) = common_attributes.get_texture() { - b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); - } else { - debug!("no texture attribute on this trail"); - } - nb_trails_loaded += 1; - } else { - debug!("category {} = {} is not enabled", trail.category, trail.parent); - } - } - } - info!("Load notifications for {} on map {}: {}/{} markers and {}/{} trails", self.name, link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); - debug!("active categories: {:?}", selected_categories_manager.keys()); - } -} - - - -impl LoadedPackTexture { - const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; - - pub fn category_set_all(&mut self, status: bool) { - CategorySelection::recursive_set_all(&mut self.selectable_categories, status); - self._is_dirty = true; - } - - pub fn update_active_categories(&mut self, active_elements: &HashSet) { - CategorySelection::recursive_update_active_categories(&mut self.selectable_categories, active_elements); - } - pub fn category_sub_menu( - &mut self, - u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, - ui: &mut egui::Ui, - show_only_active: bool, - ) { - //it is important to generate a new id each time to avoid collision - ui.push_id(ui.next_auto_id(), |ui| { - CategorySelection::recursive_selection_ui( - u2b_sender, - u2u_sender, - &mut self.selectable_categories, - ui, - &mut self._is_dirty, - show_only_active, - &self.late_discovery_categories - ); - }); - if self._is_dirty { - u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); - } - } - - pub fn is_dirty(&self) -> bool { - self._is_dirty - } - pub fn tick( - &mut self, - u2u_sender: &std::sync::mpsc::Sender, - _timestamp: f64, - link: &MumbleLink, - //next_on_screen: &mut HashSet, - z_near: f32, - tasks: &PackTasks, - ) { - tracing::trace!("LoadedPackTexture.tick: {} {}-{} {}-{}", - self.name, - self.current_map_data.active_markers.len(), - self.current_map_data.wip_markers.len(), - self.current_map_data.active_trails.len(), - self.current_map_data.wip_trails.len(), - ); - let mut marker_objects = Vec::new(); - for (uuid, marker) in self.current_map_data.active_markers.iter() { - if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { - marker_objects.push(mo); - } - } - tracing::trace!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); - u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); - let mut trail_objects = Vec::new(); - for (uuid, trail) in self.current_map_data.active_trails.iter() { - trail_objects.push(TrailObject { - vertices: trail.trail_object.vertices.clone(), - texture: trail.trail_object.texture, - }); - //next_on_screen.insert(*uuid); - } - tracing::trace!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); - u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); - } - - pub fn swap(&mut self) { - info!("swap {} to display {} textures, {} markers, {} trails", - self.name, - self.current_map_data.active_textures.len(), - self.current_map_data.wip_markers.len(), - self.current_map_data.wip_trails.len() - ); - self.current_map_data.active_markers = std::mem::take(&mut self.current_map_data.wip_markers); - self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); - } - - pub fn load_marker_texture( - &mut self, - egui_context: &egui::Context, - default_tex_id: &TextureHandle, - tex_path: &RelativePath, - marker_uuid: Uuid, - position: glam::Vec3, - common_attributes: CommonAttributes, - ) { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - - self.current_map_data.active_textures.insert( - tex_path.clone(), - egui_context.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this icon texture"); - } - } - let th = self.current_map_data.active_textures.get(tex_path) - .unwrap_or(default_tex_id); - let texture_id = match th.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }; - - let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size - let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size - let am = ActiveMarker { - texture_id, - _texture: th.clone(), - common_attributes, - pos: position, - max_pixel_size, - min_pixel_size, - }; - self.current_map_data - .wip_markers - .insert(marker_uuid, am); - } - - pub fn load_trail_texture( - &mut self, - egui_context: &egui::Context, - default_tex_id: &TextureHandle, - tex_path: &RelativePath, - trail_uuid: Uuid, - common_attributes: CommonAttributes, - ) { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - egui_context.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, "failed to find this trail texture"); - } - } else { - debug!("Trail texture alreadu loaded {:?}", tex_path); - } - let texture_path = common_attributes.get_texture(); - let th = texture_path - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - - let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { - debug!(?texture_path, "tbin path"); - tbin - } else { - info!(?trail_uuid, "missing tbin path"); - return; - }; - let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { - tbin - } else { - info!(%tbin_path, "failed to find tbin"); - return; - }; - if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( - &common_attributes, - &tbin.nodes, - th.clone(), - ) { - self.current_map_data - .wip_trails - .insert(trail_uuid, active_trail); - } else { - info!("Cannot display {texture_path:?}") - } - - } - -} - -pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { - jokolay_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!("failed to create marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; - let marker_manager_dir = jokolay_dir - .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!("failed to open marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; - marker_manager_dir - .create_dir_all(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!("failed to create marker packs directory {}", PACKAGES_DIRECTORY_NAME))?; - let marker_packs_dir = marker_manager_dir - .open_dir(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!("failed to open marker packs dir {}", PACKAGES_DIRECTORY_NAME))?; - Ok(marker_packs_dir) -} - -pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap, BTreeMap)>{ - let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; - let mut data_packs: BTreeMap = Default::default(); - let mut texture_packs: BTreeMap = Default::default(); - - - for entry in marker_packs_dir - .entries() - .into_diagnostic() - .wrap_err("failed to get entries of marker packs dir")? - { - let entry = entry.into_diagnostic()?; - if entry.metadata().into_diagnostic()?.is_file() { - continue; - } - if let Ok(name) = entry.file_name() { - let pack_dir = entry - .open_dir() - .into_diagnostic() - .wrap_err(format!("failed to open pack entry as directory: {}", name))?; - { - let span_guard = info_span!("loading pack from dir", name).entered(); - - match build_from_dir(name.clone(), pack_dir.into()) { - Ok(lp) => { - let (data, tex) = lp; - data_packs.insert(data.uuid, data); - texture_packs.insert(tex.uuid, tex); - } - Err(e) => { - error!(?e, "failed to load pack from directory: {}", name); - } - } - drop(span_guard); - } - } - } - Ok((data_packs, texture_packs)) -} - -fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, LoadedPackTexture)> { - if !pack_dir - .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? - { - bail!("pack core doesn't exist in this pack"); - } - let core_dir = pack_dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - let start = std::time::SystemTime::now(); - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); - let res = build_from_core(name.clone(), pack_dir, core); - Ok(res) -} - - -pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { - let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); - let data = LoadedPackData { - name: name.clone(), - uuid: core.uuid, - dir: Arc::clone(&pack_dir), - selected_files: Default::default(), - all_categories: core.all_categories, - categories: core.categories, - maps: core.maps, - source_files: core.source_files, - _is_dirty: false, - activation_data: Default::default(), - active_elements: Default::default(), - selectable_categories: selectable_categories.clone(), - entities_parents: core.entities_parents, - }; - let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); - let tex = LoadedPackTexture { - uuid: core.uuid, - selectable_categories, - textures: core.textures, - current_map_data: Default::default(), - _is_dirty: false, - activation_data, - dir: Arc::clone(&pack_dir), - late_discovery_categories: core.late_discovery_categories, - name: name, - tbins: core.tbins, - active_elements: Default::default(), - }; - (data, tex) -} - diff --git a/crates/joko_marker_format/src/manager/pack/mod.rs b/crates/joko_marker_format/src/manager/pack/mod.rs deleted file mode 100644 index 2833ae2..0000000 --- a/crates/joko_marker_format/src/manager/pack/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod category_selection; -pub mod file_selection; -pub mod activation; -pub mod active; -pub mod loaded; -pub mod dirty; -pub mod import; - diff --git a/crates/joko_marker_format/src/manager/package.rs b/crates/joko_marker_format/src/manager/package.rs deleted file mode 100644 index 05f5dea..0000000 --- a/crates/joko_marker_format/src/manager/package.rs +++ /dev/null @@ -1,679 +0,0 @@ -use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex} -}; - -use glam::Vec3; -use tribool::Tribool; -use cap_std::fs_utf8::Dir; -use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; -use image::EncodableLayout; - -use tracing::{error, info, info_span, trace}; - -use joko_core::RelativePath; -use jokolink::MumbleLink; -use miette::{Context, IntoDiagnostic, Result}; -use uuid::Uuid; -use crate::{load_all_from_dir, message::{UIToBackMessage, UIToUIMessage}}; - -use crate::{message::BackToUIMessage, pack::CommonAttributes}; -use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; -use crate::manager::pack::import::{ImportStatus, import_pack_from_zip_file_path}; - -use super::pack::loaded::jokolay_to_marker_dir; - -pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose -pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose -// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; - -/// It manage everything that has to do with marker packs. -/// 1. imports, loads, saves and exports marker packs. -/// 2. maintains the categories selection data for every pack -/// 3. contains activation data globally and per character -/// 4. When we load into a map, it filters the markers and runs the logic every frame -/// 1. If a marker needs to be activated (based on player position or whatever) -/// 2. marker needs to be drawn -/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture -/// 4. render that marker use joko_render -#[must_use] -pub struct PackageDataManager { - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - //_marker_manager_dir: Arc, - /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. - /// The name of the child directory is the name of the pack - pub marker_packs_dir: Arc, - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - pub packs: BTreeMap, - tasks: PackTasks, - current_map_id: u32, - show_only_active: bool, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, - - pub currently_used_files: BTreeMap, - parents: HashMap, - loaded_elements: HashSet, - on_screen: BTreeSet, -} -#[must_use] -pub struct PackageUIManager { - default_marker_texture: Option, - default_trail_texture: Option, - packs: BTreeMap, - tasks: PackTasks, - - currently_used_files: BTreeMap, - all_files_tribool: Tribool, - all_files_toggle: bool, - show_only_active: bool, -} - -impl PackageDataManager { - /// Creates a new instance of [MarkerManager]. - /// 1. It opens the marker manager directory - /// 2. loads its configuration - /// 3. opens the packs directory - /// 4. loads all the packs - /// 5. loads all the activation data - /// 6. returns self - pub fn new(packs: BTreeMap, jokolay_dir: Arc) -> Result { - let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; - Ok(Self { - packs, - tasks: PackTasks::new(), - marker_packs_dir: Arc::new(marker_packs_dir), - //_marker_manager_dir: marker_manager_dir.into(), - current_map_id: 0, - save_interval: 0.0, - show_only_active: true, - currently_used_files: Default::default(), - parents: Default::default(), - loaded_elements: Default::default(), - on_screen: Default::default(), - }) - } - - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { - self.currently_used_files = currently_used_files; - } - - pub fn category_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_set(uuid, status) { - break; - } - } - } - - pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_branch_set(uuid, status) { - break; - } - } - } - - pub fn category_set_all(&mut self, status: bool) { - for pack in self.packs.values_mut() { - pack.category_set_all(status); - } - } - - pub fn register(&mut self, element: Uuid, parent: Uuid) { - self.parents.insert(element, parent); - } - pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { - self.parents.get(element) - } - pub fn get_parents<'a, I>(&self, input: I) -> HashSet - where I: Iterator - { - let iter = input.into_iter(); - let mut result: HashSet = HashSet::new(); - let mut current_generation: Vec = Vec::new(); - for elt in iter { - current_generation.push(*elt) - } - //info!("starts with {}", current_generation.len()); - loop { - if current_generation.is_empty() { - //info!("ends with {}", result.len()); - return result; - } - let mut next_gen: Vec = Vec::new(); - for elt in current_generation.iter() { - if let Some(p) = self.get_parent(elt) { - if result.contains(p) { - //avoid duplicate, redundancy or loop - continue; - } - next_gen.push(p.clone()); - } - } - let to_insert = std::mem::replace(&mut current_generation, next_gen); - result.extend(to_insert); - } - unreachable!("The loop should always return"); - } - - pub fn get_active_elements_parents(&mut self, categories_and_elements_to_be_loaded: HashSet) { - trace!("There are {} active elements", categories_and_elements_to_be_loaded.len()); - - //first merge the parents to iterate overit - let mut parents: HashMap = Default::default(); - for pack in self.packs.values_mut() { - parents.extend(pack.entities_parents.clone()); - } - self.parents = parents; - //then climb up the tree of parent's categories - self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); - } - - pub fn tick( - &mut self, - b2u_sender: &std::sync::mpsc::Sender, - loop_index: u128, - link: Option<&MumbleLink>, - choice_of_category_changed: bool, - ) { - let mut currently_used_files: BTreeMap = Default::default(); - let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - - match link { - Some(link) => { - //TODO: how to save/load the active files ? - //TODO: find an efficient way to propagate the file deactivation - let mut have_used_files_list_changed = false; - let map_changed = self.current_map_id != link.map_id; - self.current_map_id = link.map_id; - for pack in self.packs.values_mut() { - if let Some(current_map) = pack.maps.get(&link.map_id) { - for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_name) { - currently_used_files.insert( - marker.source_file_name.clone(), - *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) - ); - } - } - for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_name) { - currently_used_files.insert( - trail.source_file_name.clone(), - *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) - ); - } - } - } - } - let mut tasks = &self.tasks; - for (uuid, pack) in self.packs.iter_mut() { - let span_guard = info_span!("Updating package status").entered(); - b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); - tasks.save_data(pack, pack.is_dirty()); - pack.tick( - &b2u_sender, - loop_index, - link, - ¤tly_used_files, - have_used_files_list_changed || choice_of_category_changed, - map_changed, - &tasks, - &mut categories_and_elements_to_be_loaded, - ); - std::mem::drop(span_guard); - } - if map_changed { - self.get_active_elements_parents(categories_and_elements_to_be_loaded); - b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); - } - if map_changed || have_used_files_list_changed || choice_of_category_changed { - //there is no point in sending a new list if nothing changed - b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); - self.currently_used_files = currently_used_files; - b2u_sender.send(BackToUIMessage::TextureSwapChain); - } - }, - None => {}, - }; - } - - fn delete_packs(&mut self, to_delete: Vec) { - for uuid in to_delete { - self.packs.remove(&uuid); - } - } - pub fn save(&mut self, mut data_pack: LoadedPackData) -> Uuid { - let mut to_delete: Vec = Vec::new(); - for (uuid, pack) in self.packs.iter() { - if pack.name == data_pack.name { - to_delete.push(*uuid); - } - } - self.delete_packs(to_delete); - self.tasks.save_data(&mut data_pack, true); - let mut uuid_to_insert = data_pack.uuid.clone(); - while self.packs.contains_key(&uuid_to_insert) {//collision avoidance - trace!("Uuid collision detected for {} for package {}", uuid_to_insert, data_pack.name); - uuid_to_insert = Uuid::new_v4(); - } - data_pack.uuid = uuid_to_insert; - self.packs.insert(uuid_to_insert, data_pack); - uuid_to_insert - } - - pub fn load_all( - &mut self, - jokolay_dir: Arc, - b2u_sender: &std::sync::mpsc::Sender, - ) { - // Called only once at application start. - b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); - self.tasks.load_all_packs(jokolay_dir); - if let Ok((data_packages, texture_packages)) = self.tasks.wait_for_load_all_packs() { - for (uuid, data_pack) in data_packages { - self.packs.insert(uuid, data_pack); - } - for (uuid, texture_pack) in texture_packages { - b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); - } - b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); - } - - } - -} - - -impl PackageUIManager { - pub fn new(packs: BTreeMap) -> Self { - Self { - packs, - tasks: PackTasks::new(), - default_marker_texture: None, - default_trail_texture: None, - - all_files_tribool: Tribool::True, - all_files_toggle: false, - show_only_active: true, - currently_used_files: Default::default()// UI copy to (de-)activate files - } - } - - pub fn late_init( - &mut self, - etx: &egui::Context, - ) { - if self.default_marker_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.default_marker_texture = Some(etx.load_texture( - "default marker", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - if self.default_trail_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/trail_rainbow.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.default_trail_texture = Some(etx.load_texture( - "default trail", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - } - - pub fn delete_packs(&mut self, to_delete: Vec) { - for uuid in to_delete { - self.packs.remove(&uuid); - } - } - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { - self.currently_used_files = currently_used_files; - } - - pub fn update_active_categories(&mut self, active_elements: &HashSet) { - trace!("There are {} active elements", active_elements.len()); - for pack in self.packs.values_mut() { - pack.update_active_categories(active_elements); - } - } - - pub fn update_pack_active_categories(&mut self, pack_uuid: Uuid, active_elements: &HashSet) { - trace!("There are {} active elements", active_elements.len()); - for (uuid, pack) in self.packs.iter_mut() { - if uuid == &pack_uuid { - pack.update_active_categories(active_elements); - break; - } - } - } - pub fn swap(&mut self) { - for pack in self.packs.values_mut() { - pack.swap(); - } - } - - pub fn load_marker_texture( - &mut self, - egui_context: &egui::Context, - pack_uuid: Uuid, - tex_path: RelativePath, - marker_uuid: Uuid, - position: Vec3, - common_attributes: CommonAttributes, - ) { - self.packs - .get_mut(&pack_uuid) - .map( |pack| { - pack.load_marker_texture( - egui_context, - self.default_marker_texture.as_ref().unwrap(), - &tex_path, - marker_uuid, - position, - common_attributes, - ); - }); - } - pub fn load_trail_texture( - &mut self, - egui_context: &egui::Context, - pack_uuid: Uuid, - tex_path: RelativePath, - trail_uuid: Uuid, - common_attributes: CommonAttributes, - ) { - self.packs - .get_mut(&pack_uuid) - .map( |pack| { - pack.load_trail_texture( - egui_context, - &self.default_trail_texture.as_ref().unwrap(), - &tex_path, - trail_uuid, - common_attributes, - ); - }); - } - - fn pack_importer( - import_status: Arc>, - ) { - //called when a new pack is imported - rayon::spawn( move || { - *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; - - if let Some(file_path) = rfd::FileDialog::new() - .add_filter("taco", &["zip", "taco"]) - .pick_file() - { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); - } else { - *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); - } - }); - } - - fn category_set_all(&mut self, status: bool) { - for pack in self.packs.values_mut() { - pack.category_set_all(status); - } - } - - pub fn tick( - &mut self, - u2u_sender: &std::sync::mpsc::Sender, - timestamp: f64, - link: &MumbleLink, - z_near: f32, - ) { - let mut tasks = &self.tasks; - for (uuid, pack) in self.packs.iter_mut() { - let span_guard = info_span!("Updating package status").entered(); - tasks.save_texture(pack, pack.is_dirty()); - pack.tick( - &u2u_sender, - timestamp, - link, - z_near, - &tasks - ); - std::mem::drop(span_guard); - } - u2u_sender.send(UIToUIMessage::RenderSwapChain); - //u2u_sender.send(UIToUIMessage::Present); - } - - pub fn menu_ui( - &mut self, - u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, - ui: &mut egui::Ui, - nb_running_tasks_on_back: i32, - nb_running_tasks_on_network: i32, - ) { - ui.menu_button("Markers", |ui| { - if self.show_only_active { - if ui.button("Show everything").clicked() { - self.show_only_active = false; - } - } else { - if ui.button("Show only active").clicked() { - self.show_only_active = true; - } - } - if ui.button("Activate all elements").clicked() { - self.category_set_all(true); - u2b_sender.send(UIToBackMessage::CategorySetAll(true)); - } - if ui.button("Deactivate all elements").clicked() { - self.category_set_all(false); - u2b_sender.send(UIToBackMessage::CategorySetAll(false)); - } - - for pack in self.packs.values_mut() { - //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; - //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active); - } - - }); - if self.tasks.is_running() || nb_running_tasks_on_back > 0 || nb_running_tasks_on_network > 0{ - let sp = egui::Spinner::new().color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); - ui.add(sp); - } - } - pub fn status_as_color(&self, nb_running_tasks_on_back: i32, nb_running_tasks_on_network: i32) -> egui::Color32 { - //we can choose whatever color code we want to focus on load, save, network queries, anything. - let nb_running_tasks_on_ui = self.tasks.count(); - //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 - let color_ui = if nb_running_tasks_on_ui > 0 { - let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; - let res = nb_ui_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - let color_back = if nb_running_tasks_on_back > 0 { - let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; - let res = nb_bask_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - let color_network = if nb_running_tasks_on_network > 0 { - let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; - let res = nb_network_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - egui::Color32::from_rgb(color_ui, color_back, color_network) - } - - fn gui_file_manager( - &mut self, - event_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - open: &mut bool, - link: Option<&MumbleLink> - ) { - let mut files_changed = false; - Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { - egui::ScrollArea::vertical().show(ui, |ui| { - egui::Grid::new("link grid") - .num_columns(4) - .striped(true) - .show(ui, |ui| { - if self.all_files_tribool.is_indeterminate(){ - ui.add(egui::Checkbox::new(&mut self.all_files_toggle, "File").indeterminate(true)); - } else { - ui.checkbox(&mut self.all_files_toggle, "File"); - } - ui.label("Trails"); - ui.label("Markers"); - ui.end_row(); - - for file in self.currently_used_files.iter_mut() { - let cb = ui.checkbox(file.1, file.0.clone()); - if cb.changed() { - files_changed = true; - } - if ui.button("Edit").clicked() { - println!("click {}", file.0.clone()); - } - ui.end_row(); - } - ui.end_row(); - }) - }); - Ok(()) - }); - if files_changed { - event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); - } - } - fn gui_package_loader( - &mut self, - u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - import_status: &Arc>, - open: &mut bool - ) { - Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { - CollapsingHeader::new("Loaded Packs").show(ui, |ui| { - egui::Grid::new("packs").striped(true).show(ui, |ui| { - let mut to_delete = vec![]; - for pack in self.packs.values() { - ui.label(pack.name.clone()); - if ui.button("delete").clicked() { - to_delete.push(pack.uuid); - } - if ui.button("Details").clicked() { - //TODO - } - ui.end_row(); - } - if !to_delete.is_empty() { - u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); - } - }); - }); - - if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? - //let import_status = import_status.lock().unwrap(); - Self::pack_importer(Arc::clone(import_status)); - } - ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { - ui.label(format!("pack is being imported from {p:?}")); - } - ImportStatus::PackDone(name, pack, saved) => { - if *saved { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); - } else { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - if ui.button("save").clicked() { - u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); - } - } - } - ImportStatus::PackError(e) => { - let error_msg = format!("failed to import pack due to error: {e:#?}"); - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - *status = ImportStatus::UnInitialized; - } - ui.colored_label( - egui::Color32::RED, - error_msg, - ); - } - } - } - - Ok(()) - }); - } - pub fn gui( - &mut self, - u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - is_marker_open: &mut bool, - import_status: &Arc>, - is_file_open: &mut bool, - timestamp: f64, - link: Option<&MumbleLink> - ) { - self.gui_package_loader(u2b_sender, etx, import_status, is_marker_open); - self.gui_file_manager(u2b_sender, etx, is_file_open, link); - } - - pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { - /* - We save in a file with the name of the package, while we keep track of it from a uuid point of view. - It means we can have duplicates unless package with same name is deleted. - */ - let mut to_delete: Vec = Vec::new(); - for (uuid, pack) in self.packs.iter() { - if pack.name == texture_pack.name { - to_delete.push(*uuid); - } - } - self.delete_packs(to_delete); - self.tasks.save_texture(&mut texture_pack, true); - self.packs.insert(texture_pack.uuid, texture_pack); - } -} - - diff --git a/crates/joko_marker_format/src/message.rs b/crates/joko_marker_format/src/message.rs deleted file mode 100644 index 189d543..0000000 --- a/crates/joko_marker_format/src/message.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::sync::Arc; -use std::collections::{BTreeMap, HashSet}; - -use uuid::Uuid; - -use glam::{Vec2, Vec3}; - -use jokolink::MumbleLink; -use joko_core::RelativePath; - -use crate::{pack::{CommonAttributes, PackCore}, LoadedPackTexture}; - - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct MarkerVertex { - pub position: Vec3, - pub alpha: f32, - pub texture_coordinates: Vec2, - pub fade_near_far: Vec2, - pub color: [u8; 4], -} - -#[derive(Debug)] -pub struct MarkerObject { - /// The six vertices that make up the marker quad - pub vertices: [MarkerVertex; 6], - /// The (managed) texture id from egui data - pub texture: u64, - /// The distance from camera - /// As markers have transparency, we need to render them from far -> near order - /// So, we will sort them using this distance just before rendering - pub distance: f32, -} - -#[derive(Debug, Clone)] -pub struct TrailObject { - pub vertices: Arc<[MarkerVertex]>, - pub texture: u64, -} - -pub enum BackToUIMessage { - ActiveElements(HashSet),//list of all elements that are loaded for current map - CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display - LoadedPack(LoadedPackTexture),//push a loaded pack to UI - DeletedPacks(Vec),//push a deleted set of packs to UI - ImportedPack(String, PackCore), - ImportFailure(miette::Report), - MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), - MumbleLink(Option), - MumbleLinkChanged,//tell there is a need to resize - NbTasksRunning(i32),//tell the number of taks running in background - PackageActiveElements(Uuid, HashSet),// first is the package reference, second is the list of active elements in the package. - TextureSwapChain,// The list of texture to load was changed, will be soon followed by a RenderSwapChain - TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), -} - -pub enum UIToBackMessage { - ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. - CategoryActivationElementStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. - CategoryActivationBranchStatusChange(Uuid, bool),//same, for a whole branch - CategoryActivationStatusChanged,//something happened that needs to reload the whole set - CategorySetAll(bool),//signal all categories should be now at this status - DeletePacks(Vec),//uuid of the pack to delete - ImportPack(std::path::PathBuf), - ReloadPack, - SavePack(String, PackCore), -} - -pub enum UIToUIMessage { - BulkMarkerObject(Vec), - BulkTrailObject(Vec), - //Present,// a render loop is finished and we can present it - MarkerObject(MarkerObject), - RenderSwapChain,// The list of elements to display was changed - TrailObject(TrailObject), -} - diff --git a/crates/joko_marker_format/src/pack/common.rs b/crates/joko_marker_format/src/pack/common.rs deleted file mode 100644 index aea6dff..0000000 --- a/crates/joko_marker_format/src/pack/common.rs +++ /dev/null @@ -1,2274 +0,0 @@ -use std::str::FromStr; - -use enumflags2::{bitflags, BitFlags}; -use glam::Vec3; -use itertools::Itertools; -use tracing::info; -use xot::Element; - -use crate::io::XotAttributeNameIDs; - -use super::RelativePath; -use jokoapi::end_point::mounts::Mount; -use jokoapi::end_point::races::Race; -use smol_str::SmolStr; -/// This is a onetime macro to reduce code duplication -/// It basically takes the CommmonAttributes struct, adds the active_attributes and bool_attributes fields to it. -/// Then, it creates a method call `inherit_if_attr_none`, which will clone fields from other struct, if its own fields are not active (set) -/// Finally, it derives a getter and setter for all of the fields. -/// -/// Once we are close to releasing a 1.0 version of this crate, we should just expand all these macros to raw code as its never going to change again. -macro_rules! common_attributes_struct_macro { - ( - $( #[$attr:meta] )* - $vis:vis struct $name:ident { - $( $( #[$field_attr:meta] )* $field_vis:vis $field:ident : $ty:ty ),* $(,)? - } - ) => { - $( #[$attr] )* - $vis struct $name { - active_attributes: BitFlags, - bool_attributes: BitFlags, - $( $( #[$field_attr] )* $field : $ty ),* - } - impl $name { - $vis fn inherit_if_attr_none(&mut self, other: &$name) { - $(if !self.active_attributes.contains(ActiveAttributes::$field) - && other.active_attributes.contains(ActiveAttributes::$field) { - self.active_attributes.insert(ActiveAttributes::$field); - self.$field = other.$field.clone(); - })+ - } - $( - paste::paste!( - /// This gets the value IF the attribute is set. Otherwise returns None. - #[allow(unused)] - $vis fn [](&self) -> Option<&$ty> { - self.active_attributes.contains(ActiveAttributes::$field).then_some(&self.$field) - } - /// This directly sets the field to value IF the value is Some. Otherwise deactivates the attribute. - /// - /// Warning: This simply overwrites the value of the existing field. - /// So, if you wanted to combine them (an array or bitflags), then do get -> combine it smh -> set. - #[allow(unused)] - $vis fn [](&mut self, value: Option<$ty>) { - if let Some(value) = value { - self.active_attributes.insert(ActiveAttributes::$field); - self.$field = value; - } else { - self.active_attributes.remove(ActiveAttributes::$field); - } - } - ); - )+ - } - } -} -/// uses the [ToString] impl of attributes to serialize them (only if the relevant active attribute flag is set) -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust -/// set_attribute_to_ele!(ca, ele, names, [field1, field2, field3]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if ca.active_attributes.contains(ActiveAttributes::field1) { -/// ele.set_attribute(names.field1, ca.field1.to_string()); -/// } -/// ``` -macro_rules! set_attribute_to_ele { - ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if $ca.active_attributes.contains(ActiveAttributes::$field) { - $ele.set_attribute($names.$field, $ca.$field.to_string()); - })+ - }; -} -/// true -> 1 and 0 -> false. (only if the relevant active attribute flag is set) -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust -/// set_attribute_bool_to_ele!(ca, ele, names, [field1, field2, field3]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if ca.active_attributes.contains(ActiveAttributes::field1) { -/// ele.set_attribute(names.field1, -/// ca -/// .bool_attributes -/// .contains(BoolAttributes::field1) -/// .then_some(1) -/// .unwrap_or(0u8) -/// .to_string() -/// ); -/// } -/// ``` -macro_rules! set_attribute_bool_to_ele { - ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if $ca.active_attributes.contains(ActiveAttributes::$field) { - $ele.set_attribute( - $names.can_fade, - $ca.bool_attributes - .contains(BoolAttributes::$field) - .then_some(1) - .unwrap_or(0u8) - .to_string(), - ); - })+ - }; -} -/// iterates over a bitflags field and joins the enabled flags (as str) with comma. (only if the relevant active attribute flag is set) -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust -/// set_attribute_bitflags_as_array_to_ele!(ca, ele, names, [field1, field2, field3]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if ca.active_attributes.contains(ActiveAttributes::field1) { -/// ele.set_attribute( -/// names.field1, -/// ca.field1.iter().map(|s| s.as_ref()).join(","), -/// ); -/// } -/// ``` -macro_rules! set_attribute_bitflags_as_array_to_ele { - ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if $ca.active_attributes.contains(ActiveAttributes::$field) { - $ele.set_attribute( - $names.$field, - $ca.$field.iter().map(|s| s.to_string()).join(","), - ); - })+ - }; -} -/// uses the [FromStr] impl of attributes to deserialize them (and set the relevant active attribute flag if successful) -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust -/// update_attribute_from_ele!(ca, ele, names, [field1, field2, field3]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if let Some(value) = ele.get_attribute(names.field1) { -/// match value.trim().parse() { -/// Ok(value) => { -/// ca -/// .active_attributes -/// .insert(ActiveAttributes::fiel1); -/// ca.field1 = value; -/// } -/// Err(e) => { -/// tracing::info!(?e, value, "failed to parse {}", "field1"); -/// } -/// } -/// } -/// ``` -macro_rules! update_attribute_from_ele { - ($ca: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if let Some(value) = $ele.get_attribute($names.$field) { - match value.trim().parse() { - Ok(value) => { - $ca - .active_attributes - .insert(ActiveAttributes::$field); - $ca.$field = value; - } - Err(e) => { - tracing::info!(?e, value, "failed to parse {}", stringify!($field)); - } - } - })+ - }; -} - -/// deserializes an [i8] and matches that as 1 -> true and 0 -> false. -/// On success, set the relevant active attribute flag. -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust -/// update_attribute_bool_from_ele!(ca, ele, names, [field1, field2, field3]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if let Some(value) = ele.get_attribute(names.field1) { -/// match value.trim().parse::() { -/// Ok(value) => { -/// match value { -/// 0 | 1 => { -/// ca -/// .active_attributes -/// .insert(ActiveAttributes::field1); -/// ca.bool_attributes.set( -/// BoolAttributes::field1, -/// if value == 0 { false } else { true }, -/// ); -/// } -/// _ => { -/// info!(value, "failed to parse {}", "field1"); -/// } -/// } -/// } -/// Err(e) => { -/// tracing::info!(?e, value, "failed to parse {}", "field1"); -/// } -/// } -/// } -/// ``` - -fn parse_boolean(raw_value: &str) -> Option { - let trimmed = raw_value.trim().to_lowercase(); - match trimmed.as_ref() { - "true" => {Some(true)}, - "false" => {Some(false)}, - _ => { - match trimmed.parse::() {//might entirely get rid of parsing - Ok(parsed_value) => { - match parsed_value { - 0 | 1 => { - Some(parsed_value == 1) - } - _ => None - } - } - Err(_e) => { - None - } - } - }, - } -} -macro_rules! update_attribute_bool_from_ele { - ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { - $(if let Some(value) = $ele.get_attribute($names.$field) { - if let Some(found) = parse_boolean(value) { - $common_attributes - .active_attributes - .insert(ActiveAttributes::$field); - $common_attributes.bool_attributes.set( - BoolAttributes::$field, - found, - ); - } else { - tracing::info!(value, "failed to parse {}", stringify!($field)); - } - })+ - }; -} -/// deserializes an [i8] and matches that as 1 -> true and 0 -> false. -/// On success, set the relevant active attribute flag. -/// -/// #### Args: -/// - ca: &[CommonAttributes] (ref to the struct that we are serializing) -/// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) -/// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) -/// - [f1,t1; f2,t2;...]: an array of field identifiers which will be serialized and their enum type. -/// ```rust -/// update_attribute_bitflags_array_from_ele!(ca, ele, names, [f1, t1; f2, t2]); -/// ``` -/// -/// The expansion for each field is like this -/// ```rust -/// if let Some(field1_str) = ele.get_attribute(names.field1) { -/// for value in field1_str.split(',') { -/// match value.trim().parse::() { -/// Ok(flag) => { -/// ca -/// .active_attributes -/// .insert(ActiveAttributes::field1); -/// ca.field1.set(flag); -/// } -/// Err(e) => { -/// tracing::info!(value, e); -/// } -/// } -/// } -/// } -/// ``` -macro_rules! update_attribute_bitflags_array_from_ele { - ($ca: ident, $ele: ident,$xot_names: ident, [$($field: ident, $ty: ty);+]) => { - $(if let Some(value) = $ele.get_attribute($xot_names.$field) { - for item in value.trim().split(',') { - match item.trim().parse::<$ty>() { - Ok(flag) => { - $ca.active_attributes.insert(ActiveAttributes::$field); - $ca.$field.insert(flag); - } - Err(e) => { - info!(item, e); - } - } - } - })+ - }; -} -/// generates getters for bool attributes -/// ```rust -/// getters_for_bool_attributes!([field1, field2, field3]); -/// ``` -/// -/// This generates a `fn get_field1(&self) -> Option` -/// if attribute is not active, we return None. Otherwise, the value of the boolean attribute -macro_rules! getters_for_bool_attributes { - ([$($field: ident),+]) => { - paste::paste!{ - $( - /// If the attribute is not set, then we return None. - /// Otherwise, we return the boolean value of the attribute. - #[allow(unused)] - fn [](&self) -> Option { - self.active_attributes.contains(ActiveAttributes::$field).then_some( - self.bool_attributes.contains(BoolAttributes::$field) - ) - } - )+ - } - }; -} -/// generates setters for bool attributes -/// ```rust -/// setters_for_bool_attributes!([field1, field2, field3]); -/// ``` -/// -/// This generates a `fn set_field1(&mut self, value: Option)` -/// if attribute is not active, we return None. Otherwise, the value of the boolean attribute -macro_rules! setters_for_bool_attributes { - ([$($field: ident),+]) => { - paste::paste!{ - $( - /// If the attribute is not set, then we return None. - /// Otherwise, we return the boolean value of the attribute. - #[allow(unused)] - fn [](&mut self, value: Option) { - if let Some(value) = value { - self.active_attributes.insert(ActiveAttributes::$field); - self.bool_attributes.set(BoolAttributes::$field, value); - } else { - self.active_attributes.remove(ActiveAttributes::$field); - } - } - )+ - } - }; -} -common_attributes_struct_macro!( - /// the struct we use for inheritance from category/other markers. - #[derive(Debug, Clone, Default)] - pub struct CommonAttributes { - /// An ID for an achievement from the GW2 API. Markers with the corresponding achievement ID will be hidden if the ID is marked as "done" for the API key that's entered in TacO. - achievement_id: u32, - /// This is similar to achievementId, but works for partially completed achievements as well, if the achievement has "bits", they can be individually referenced with this. - achievement_bit: u32, - /// How opaque the displayed icon should be. The default is 1.0 - alpha: f32, - anim_speed: f32, - /// it describes the way the marker will behave when a player presses 'F' over it. - behavior: Behavior, - bounce: SmolStr, - bounce_delay: f32, - bounce_duration: f32, - bounce_height: f32, - /// hex value. The color tint of the marker. sRGBA8 - color: [u8; 4], - copy: SmolStr, - copy_message: SmolStr, - cull: Cull, - /// Determines how far the marker will completely disappear. If below 0, the marker won't disappear at any distance. Default is -1. FadeFar needs to be higher than fadeNear for sane results. This value is in game units (inches). - // #[serde(rename = "fadeFar")] - fade_far: f32, - /// Determines how far the marker will start to fade out. If below 0, the marker won't disappear at any distance. Default is -1. This value is in game units (inches). - // #[serde(rename = "fadeNear")] - fade_near: f32, - festival: BitFlags, - /// Specifies how high above the ground the marker is displayed. Default value is 1.5. in meters - height_offset: f32, - hide: SmolStr, - /// The icon to be displayed for the marker. If not given, this defaults to the image shown at the start of this article. This should point to a .png file. The overlay looks for the image files both starting from the root directory and the POIs directory for convenience. Make sure you don't use too high resolution (above 128x128) images because the texture atlas used for these is limited in size and it's a needless waste of resources to fill it quickly.Default value: 20 - icon_file: RelativePath, - /// The size of the icon in the game world. Default is 1.0 if this is not defined. Note that the "screen edges herd icons" option will limit the size of the displayed images for technical reasons. - icon_size: f32, - /// his can be a multiline string, it will show up on screen as a text when the player is inside of infoRange of the marker - info: SmolStr, - /// This determines how far away from the marker the info string will be visible. in meters. - info_range: f32, - /// The size of the marker at normal UI scale, at zoom level 1 on the miniMap, in Pixels. For trails this value can be used to tweak the width - // #[serde(rename = "mapDisplaySize")] - map_display_size: f32, - map_fade_out_scale_level: f32, - map_type: BitFlags, - /// Determines the maximum size of a marker on the screen, in pixels. - // #[serde(rename = "maxSize")] - max_size: f32, - /// Determines the minimum size of a marker on the screen, in pixels. - // #[serde(rename = "minSize")] - min_size: f32, - mount: BitFlags, - profession: BitFlags, - race: BitFlags, - /// For behavior 4 this tells how long the marker should be invisible after pressing 'F'. For behavior 5 this will tell how long a map cycle is. in seconds. - // #[serde(rename = "resetLength")] - reset_length: f32, - /// this will supply data for behavior 5. The data will be given in seconds. - // #[serde(rename = "resetOffset")] - reset_offset: f32, - rotate: Vec3, - rotate_x: f32, - rotate_y: f32, - rotate_z: f32, - show: SmolStr, - specialization: Vec, - text: SmolStr, - texture: RelativePath, - tip_name: SmolStr, - tip_description: SmolStr, - title: SmolStr, - title_color: [u8; 4], - /// will toggle the specified category on or off when triggered with the action key. or with auto_trigger/trigger_range - // #[serde(rename = "toggleCategory")] - toggle_category: SmolStr, - trail_data: RelativePath, - trail_scale: f32, - /// Determines the range from where the marker is triggered. in meters. - trigger_range: f32, - } -); - -impl CommonAttributes { - getters_for_bool_attributes!([ - auto_trigger, - can_fade, - has_countdown, - in_game_visibility, - invert_behavior, - is_wall, - keep_on_map_edge, - map_visibility, - mini_map_visibility, - scale_on_map_with_zoom - ]); - setters_for_bool_attributes!([ - auto_trigger, - can_fade, - has_countdown, - in_game_visibility, - invert_behavior, - is_wall, - keep_on_map_edge, - map_visibility, - mini_map_visibility, - scale_on_map_with_zoom - ]); - pub(crate) fn update_common_attributes_from_element( - &mut self, - ele: &Element, - names: &XotAttributeNameIDs, - ) { - if let Some(input_str) = ele.get_attribute(names.color) { - use data_encoding::HEXLOWER_PERMISSIVE; - let mut output = [0u8; 4]; - match HEXLOWER_PERMISSIVE.decode_len(input_str.len()) { - Ok(len) => { - match HEXLOWER_PERMISSIVE.decode_mut(input_str.as_bytes(), &mut output[0..len]) - { - Ok(_) => { - self.active_attributes.insert(ActiveAttributes::color); - self.color = output; - } - Err(e) => { - info!(?e, input_str, "failed to decode hex bytes of the attribute"); - } - } - } - Err(e) => { - info!(?e, input_str, "failed to get decode len for hex attribute"); - } - } - } - if let Some(input_str) = ele.get_attribute(names.title_color) { - use data_encoding::HEXLOWER_PERMISSIVE; - let mut output = [0u8; 4]; - match HEXLOWER_PERMISSIVE.decode_len(input_str.len()) { - Ok(len) => { - match HEXLOWER_PERMISSIVE.decode_mut(input_str.as_bytes(), &mut output[0..len]) - { - Ok(_) => { - self.active_attributes.insert(ActiveAttributes::title_color); - self.title_color = output; - } - Err(e) => { - info!(?e, input_str, "failed to decode hex bytes of the attribute"); - } - } - } - Err(e) => { - info!(?e, input_str, "failed to get decode len for hex attribute"); - } - } - } - if let Some(rotate_str) = ele.get_attribute(names.rotate) { - let mut array = [0f32; 3]; - for (index, value) in rotate_str.trim().split(',').enumerate() { - match value.parse::() { - Ok(f) => { - if let Some(x) = array.get_mut(index) { - *x = f; - self.rotate = array.into(); - self.active_attributes.insert(ActiveAttributes::rotate); - } - } - Err(e) => { - info!(?e, rotate_str, value, "failed to parse rotate attribute"); - } - } - } - } - if let Some(specs) = ele.get_attribute(names.specialization) { - for spec in specs.trim().split(',') { - match spec.parse() { - Ok(s) => { - self.active_attributes - .insert(ActiveAttributes::specialization); - self.specialization.push(s); - } - Err(e) => { - info!(specs, spec, e); - } - } - } - } - // bitflags with multiple elements - update_attribute_bitflags_array_from_ele!(self, ele, names, [ - festival, Festival; - map_type, MapType; - mount, Mount; - profession, Profession; - race, Race - ]); - - // bools - update_attribute_bool_from_ele!( - self, - ele, - names, - [ - auto_trigger, - can_fade, - has_countdown, - in_game_visibility, - invert_behavior, - is_wall, - keep_on_map_edge, - map_visibility, - mini_map_visibility, - scale_on_map_with_zoom - ] - ); - update_attribute_from_ele!( - self, - ele, - names, - [ - icon_file, - texture, - trail_data, - achievement_id, - achievement_bit, - bounce, - copy, - hide, - info, - copy_message, - show, - text, - tip_name, - tip_description, - title, - toggle_category, - alpha, - anim_speed, - bounce_delay, - bounce_duration, - bounce_height, - fade_near, - fade_far, - height_offset, - icon_size, - info_range, - map_display_size, - map_fade_out_scale_level, - max_size, - min_size, - reset_length, - reset_offset, - rotate_x, - rotate_y, - rotate_z, - trail_scale, - trigger_range, - cull, - behavior - ] - ); - } - - pub(crate) fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { - // color arrays - if self.active_attributes.contains(ActiveAttributes::color) { - ele.set_attribute(names.color, data_encoding::HEXLOWER.encode(&self.color)); - } - if self - .active_attributes - .contains(ActiveAttributes::title_color) - { - ele.set_attribute( - names.title_color, - data_encoding::HEXLOWER.encode(&self.title_color), - ); - } - // rotate array - if self.active_attributes.contains(ActiveAttributes::rotate) { - ele.set_attribute( - names.rotate, - format!("{},{},{}", self.rotate.x, self.rotate.y, self.rotate.z), - ); - } - // spec vector - if self - .active_attributes - .contains(ActiveAttributes::specialization) - { - ele.set_attribute( - names.specialization, - self.specialization - .iter() - .copied() - .map(|s| s as u8) - .join(","), - ); - } - // bitflags arrays - set_attribute_bitflags_as_array_to_ele!( - self, - ele, - names, - [festival, map_type, mount, profession, race] - ); - // bools - set_attribute_bool_to_ele!( - self, - ele, - names, - [ - auto_trigger, - can_fade, - has_countdown, - in_game_visibility, - invert_behavior, - is_wall, - keep_on_map_edge, - map_visibility, - mini_map_visibility, - scale_on_map_with_zoom - ] - ); - // tostrings - set_attribute_to_ele!( - self, - ele, - names, - [ - icon_file, - texture, - trail_data, - achievement_id, - achievement_bit, - bounce, - copy, - hide, - info, - copy_message, - show, - text, - tip_name, - tip_description, - title, - toggle_category, - alpha, - anim_speed, - bounce_delay, - bounce_duration, - bounce_height, - fade_near, - fade_far, - height_offset, - icon_size, - info_range, - map_display_size, - map_fade_out_scale_level, - max_size, - min_size, - reset_length, - reset_offset, - rotate_x, - rotate_y, - rotate_z, - trail_scale, - trigger_range - ] - ); - } - /* - - TF32 height = 1.5f; - TF32 triggerRange = 2.0f; - TF32 animSpeed = 1; - TS32 miniMapSize = 20; - TF32 miniMapFadeOutLevel = 100.0f; - TF32 infoRange = 2.0f; - CColor color = CColor( 0xffffffff ); - - TS16 resetLength = 0; - TS16 minSize = 5; - TS16 maxSize = 2048; - - */ -} - -#[allow(non_camel_case_types)] -#[bitflags] -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum BoolAttributes { - /// should the trigger activate when within trigger range - auto_trigger = 1, - can_fade = 1 << 1, - /// should we show the countdown timers for markers that are sleeping - has_countdown = 1 << 2, - /// whether the marker is drawn ingame - in_game_visibility = 1 << 3, - invert_behavior = 1 << 4, - is_wall = 1 << 5, - keep_on_map_edge = 1 << 6, - /// whether draw on map - map_visibility = 1 << 7, - /// draw on minimap - mini_map_visibility = 1 << 8, - /// scaling of marker on 2d map (or minimap) - scale_on_map_with_zoom = 1 << 9, -} -#[allow(non_camel_case_types)] -#[bitflags] -#[repr(u64)] -#[derive(Debug, Clone, Copy)] -pub enum ActiveAttributes { - achievement_id = 1, - achievement_bit = 1 << 1, - alpha = 1 << 2, - anim_speed = 1 << 3, - auto_trigger = 1 << 4, - behavior = 1 << 5, - bounce = 1 << 6, - bounce_delay = 1 << 7, - bounce_duration = 1 << 8, - bounce_height = 1 << 9, - can_fade = 1 << 10, - color = 1 << 11, - copy = 1 << 12, - copy_message = 1 << 13, - cull = 1 << 14, - fade_far = 1 << 15, - fade_near = 1 << 16, - festival = 1 << 17, - has_countdown = 1 << 18, - height_offset = 1 << 19, - hide = 1 << 20, - icon_file = 1 << 21, - icon_size = 1 << 22, - in_game_visibility = 1 << 23, - info = 1 << 24, - info_range = 1 << 25, - invert_behavior = 1 << 26, - is_wall = 1 << 27, - keep_on_map_edge = 1 << 28, - map_display_size = 1 << 29, - map_fade_out_scale_level = 1 << 30, - map_type = 1 << 31, - map_visibility = 1 << 32, - max_size = 1 << 33, - min_size = 1 << 34, - mini_map_visibility = 1 << 35, - mount = 1 << 36, - profession = 1 << 37, - race = 1 << 38, - reset_length = 1 << 39, - reset_offset = 1 << 40, - rotate = 1 << 41, - rotate_x = 1 << 42, - rotate_y = 1 << 43, - rotate_z = 1 << 44, - scale_on_map_with_zoom = 1 << 45, - show = 1 << 46, - specialization = 1 << 47, - text = 1 << 48, - texture = 1 << 49, - tip_name = 1 << 50, - tip_description = 1 << 51, - title = 1 << 52, - title_color = 1 << 53, - toggle_category = 1 << 54, - trail_data = 1 << 55, - trail_scale = 1 << 56, - trigger_range = 1 << 57, -} -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub enum Behavior { - #[default] - AlwaysVisible, - /// live. marker_id - ReappearOnMapChange, - /// store. marker_id + next reset timestamp - ReappearOnDailyReset, - /// store. marker_id - OnlyVisibleBeforeActivation, - /// store. marker_id + timestamp of when to wakeup - ReappearAfterTimer, - /// store. marker_id + timestamp of next reset of map - ReappearOnMapReset, - /// live. marker_id + instance ip / shard id - OncePerInstance, - /// store. marker_id + next reset. character data - DailyPerChar, - /// live. marker_id + instance_id + character_name - OncePerInstancePerChar, - /// I have no idea. - WvWObjective, - WeeklyReset = 101, -} -impl FromStr for Behavior { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "0" => Self::AlwaysVisible, - "1" => Self::ReappearOnMapChange, - "2" => Self::ReappearOnDailyReset, - "3" => Self::OnlyVisibleBeforeActivation, - "4" => Self::ReappearAfterTimer, - "5" => Self::ReappearOnMapReset, - "6" => Self::OncePerInstance, - "7" => Self::DailyPerChar, - "8" => Self::OncePerInstancePerChar, - "9" => Self::WvWObjective, - "101" => Self::WeeklyReset, - _ => return Err("invalid behavior value"), - }) - } -} -/// Filter which professions the marker should be active for. if its null, its available for all professions -#[bitflags] -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum Profession { - Elementalist = 1 << 0, - Engineer = 1 << 1, - Guardian = 1 << 2, - Mesmer = 1 << 3, - Necromancer = 1 << 4, - Ranger = 1 << 5, - Revenant = 1 << 6, - Thief = 1 << 7, - Warrior = 1 << 8, -} -impl FromStr for Profession { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "guardian" => Profession::Guardian, - "warrior" => Profession::Warrior, - "engineer" => Profession::Engineer, - "ranger" => Profession::Ranger, - "thief" => Profession::Thief, - "elementalist" => Profession::Elementalist, - "mesmer" => Profession::Mesmer, - "necromancer" => Profession::Necromancer, - "revenant" => Profession::Revenant, - _ => return Err("invalid profession"), - }) - } -} -impl AsRef for Profession { - fn as_ref(&self) -> &str { - match self { - Profession::Guardian => "guardian", - Profession::Warrior => "warrior", - Profession::Engineer => "engineer", - Profession::Ranger => "ranger", - Profession::Thief => "thief", - Profession::Elementalist => "elementalist", - Profession::Mesmer => "mesmer", - Profession::Necromancer => "necromancer", - Profession::Revenant => "revenant", - } - } -} -impl ToString for Profession { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -#[derive(Debug, Clone, Copy, Default)] -pub enum Cull { - #[default] - None, - ClockWise, - CounterClockWise, -} -impl FromStr for Cull { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "None" => Cull::None, - "Clockwise" => Cull::ClockWise, - "CounterClockwise" => Cull::CounterClockWise, - _ => { - return Err("invalid value for cull attribute"); - } - }) - } -} -impl AsRef for Cull { - fn as_ref(&self) -> &'static str { - match self { - Cull::None => "None", - Cull::ClockWise => "Clockwise", - Cull::CounterClockWise => "CounterClockwise", - } - } -} -impl ToString for Cull { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -/// Filter for which festivals will the marker be active for -#[bitflags] -#[repr(u8)] -#[derive(Debug, Clone, Copy)] -pub enum Festival { - DragonBash = 1 << 0, - #[allow(clippy::enum_variant_names)] - FestivalOfTheFourWinds = 1 << 1, - Halloween = 1 << 2, - LunarNewYear = 1 << 3, - SuperAdventureBox = 1 << 4, - Wintersday = 1 << 5, -} -impl FromStr for Festival { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "halloween" => Festival::Halloween, - "wintersday" => Festival::Wintersday, - "superadventurefestival" => Festival::SuperAdventureBox, - "lunarnewyear" => Festival::LunarNewYear, - "festivalofthefourwinds" => Festival::FestivalOfTheFourWinds, - "dragonbash" => Festival::DragonBash, - _ => return Err("unrecognized festival"), - }) - } -} -impl AsRef for Festival { - fn as_ref(&self) -> &'static str { - match self { - Festival::Halloween => "halloween", - Festival::Wintersday => "wintersday", - Festival::SuperAdventureBox => "superadventurefestival", - Festival::LunarNewYear => "lunarnewyear", - Festival::FestivalOfTheFourWinds => "festivalofthefourwinds", - Festival::DragonBash => "dragonbash", - } - } -} -impl ToString for Festival { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -/// Filter for which specializations (the third traitline) will the marker be active for -#[derive(Debug, Clone, Copy)] -#[repr(u8)] -pub enum Specialization { - Dueling = 0, - DeathMagic = 1, - Invocation = 2, - Strength = 3, - Druid = 4, - Explosives = 5, - Daredevil = 6, - Marksmanship = 7, - Retribution = 8, - Domination = 9, - Tactics = 10, - Salvation = 11, - Valor = 12, - Corruption = 13, - Devastation = 14, - Radiance = 15, - Water = 16, - Berserker = 17, - BloodMagic = 18, - ShadowArts = 19, - Tools = 20, - Defense = 21, - Inspiration = 22, - Illusions = 23, - NatureMagic = 24, - Earth = 25, - Dragonhunter = 26, - DeadlyArts = 27, - Alchemy = 28, - Skirmishing = 29, - Fire = 30, - BeastMastery = 31, - WildernessSurvival = 32, - Reaper = 33, - CriticalStrikes = 34, - Arms = 35, - Arcane = 36, - Firearms = 37, - Curses = 38, - Chronomancer = 39, - Air = 40, - Zeal = 41, - Scrapper = 42, - Trickery = 43, - Chaos = 44, - Virtues = 45, - Inventions = 46, - Tempest = 47, - Honor = 48, - SoulReaping = 49, - Discipline = 50, - Herald = 51, - Spite = 52, - Acrobatics = 53, - Soulbeast = 54, - Weaver = 55, - Holosmith = 56, - Deadeye = 57, - Mirage = 58, - Scourge = 59, - Spellbreaker = 60, - Firebrand = 61, - Renegade = 62, - Harbinger = 63, - Willbender = 64, - Virtuoso = 65, - Catalyst = 66, - Bladesworn = 67, - Vindicator = 68, - Mechanist = 69, - Specter = 70, - Untamed = 71, -} - -impl FromStr for Specialization { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "dueling" => Self::Dueling, - "deathmagic" => Self::DeathMagic, - "invocation" => Self::Invocation, - "strength" => Self::Strength, - "druid" => Self::Druid, - "explosives" => Self::Explosives, - "daredevil" => Self::Daredevil, - "marksmanship" => Self::Marksmanship, - "retribution" => Self::Retribution, - "domination" => Self::Domination, - "tactics" => Self::Tactics, - "salvation" => Self::Salvation, - "valor" => Self::Valor, - "corruption" => Self::Corruption, - "devastation" => Self::Devastation, - "radiance" => Self::Radiance, - "water" => Self::Water, - "berserker" => Self::Berserker, - "bloodmagic" => Self::BloodMagic, - "shadowarts" => Self::ShadowArts, - "tools" => Self::Tools, - "defense" => Self::Defense, - "inspiration" => Self::Inspiration, - "illusions" => Self::Illusions, - "naturemagic" => Self::NatureMagic, - "earth" => Self::Earth, - "dragonhunter" => Self::Dragonhunter, - "deadlyarts" => Self::DeadlyArts, - "alchemy" => Self::Alchemy, - "skirmishing" => Self::Skirmishing, - "fire" => Self::Fire, - "beastmastery" => Self::BeastMastery, - "wildernesssurvival" => Self::WildernessSurvival, - "reaper" => Self::Reaper, - "criticalstrikes" => Self::CriticalStrikes, - "arms" => Self::Arms, - "arcane" => Self::Arcane, - "firearms" => Self::Firearms, - "curses" => Self::Curses, - "chronomancer" => Self::Chronomancer, - "air" => Self::Air, - "zeal" => Self::Zeal, - "scrapper" => Self::Scrapper, - "trickery" => Self::Trickery, - "chaos" => Self::Chaos, - "virtues" => Self::Virtues, - "inventions" => Self::Inventions, - "tempest" => Self::Tempest, - "honor" => Self::Honor, - "soulreaping" => Self::SoulReaping, - "discipline" => Self::Discipline, - "herald" => Self::Herald, - "spite" => Self::Spite, - "acrobatics" => Self::Acrobatics, - "soulbeast" => Self::Soulbeast, - "weaver" => Self::Weaver, - "holosmith" => Self::Holosmith, - "deadeye" => Self::Deadeye, - "mirage" => Self::Mirage, - "scourge" => Self::Scourge, - "spellbreaker" => Self::Spellbreaker, - "firebrand" => Self::Firebrand, - "renegade" => Self::Renegade, - "harbinger" => Self::Harbinger, - "willbender" => Self::Willbender, - "virtuoso" => Self::Virtuoso, - "catalyst" => Self::Catalyst, - "bladesworn" => Self::Bladesworn, - "vindicator" => Self::Vindicator, - "mechanist" => Self::Mechanist, - "specter" => Self::Specter, - "untamed" => Self::Untamed, - _ => return Err("invalid specialization"), - }) - } -} -impl AsRef for Specialization { - fn as_ref(&self) -> &str { - match self { - Self::Dueling => "dueling", - Self::DeathMagic => "deathmagic", - Self::Invocation => "invocation", - Self::Strength => "strength", - Self::Druid => "druid", - Self::Explosives => "explosives", - Self::Daredevil => "daredevil", - Self::Marksmanship => "marksmanship", - Self::Retribution => "retribution", - Self::Domination => "domination", - Self::Tactics => "tactics", - Self::Salvation => "salvation", - Self::Valor => "valor", - Self::Corruption => "corruption", - Self::Devastation => "devastation", - Self::Radiance => "radiance", - Self::Water => "water", - Self::Berserker => "berserker", - Self::BloodMagic => "bloodmagic", - Self::ShadowArts => "shadowarts", - Self::Tools => "tools", - Self::Defense => "defense", - Self::Inspiration => "inspiration", - Self::Illusions => "illusions", - Self::NatureMagic => "naturemagic", - Self::Earth => "earth", - Self::Dragonhunter => "dragonhunter", - Self::DeadlyArts => "deadlyarts", - Self::Alchemy => "alchemy", - Self::Skirmishing => "skirmishing", - Self::Fire => "fire", - Self::BeastMastery => "beastmastery", - Self::WildernessSurvival => "wildernesssurvival", - Self::Reaper => "reaper", - Self::CriticalStrikes => "criticalstrikes", - Self::Arms => "arms", - Self::Arcane => "arcane", - Self::Firearms => "firearms", - Self::Curses => "curses", - Self::Chronomancer => "chronomancer", - Self::Air => "air", - Self::Zeal => "zeal", - Self::Scrapper => "scrapper", - Self::Trickery => "trickery", - Self::Chaos => "chaos", - Self::Virtues => "virtues", - Self::Inventions => "inventions", - Self::Tempest => "tempest", - Self::Honor => "honor", - Self::SoulReaping => "soulreaping", - Self::Discipline => "discipline", - Self::Herald => "herald", - Self::Spite => "spite", - Self::Acrobatics => "acrobatics", - Self::Soulbeast => "soulbeast", - Self::Weaver => "weaver", - Self::Holosmith => "holosmith", - Self::Deadeye => "deadeye", - Self::Mirage => "mirage", - Self::Scourge => "scourge", - Self::Spellbreaker => "spellbreaker", - Self::Firebrand => "firebrand", - Self::Renegade => "renegade", - Self::Harbinger => "harbinger", - Self::Willbender => "willbender", - Self::Virtuoso => "virtuoso", - Self::Catalyst => "catalyst", - Self::Bladesworn => "bladesworn", - Self::Vindicator => "vindicator", - Self::Mechanist => "mechanist", - Self::Specter => "specter", - Self::Untamed => "untamed", - } - } -} - -impl ToString for Specialization { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -/// Most of this data is stolen from BlishHUD. -#[bitflags] -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum MapType { - Unknown = 1 << 0, - /// Redirect map type, e.g. when logging in while in a PvP match. - Redirect = 1 << 1, - /// Character create map type. - CharacterCreate = 1 << 2, - /// PvP map type. - PvP = 1 << 3, - /// GvG map type. Unused. - /// Quote from lye: "lol unused ;_;". - GvG = 1 << 4, - /// Instance map type, e.g. dungeons and story content. - Instance = 1 << 5, - /// Public map type, e.g. open world. - Public = 1 << 6, - /// Tournament map type. Probably unused. - Tournament = 1 << 7, - /// Tutorial map type. - Tutorial = 1 << 8, - /// User tournament map type. Probably unused. - UserTournament = 1 << 9, - /// Eternal Battlegrounds (WvW) map type. - EternalBattlegrounds = 1 << 10, - /// Blue Borderlands (WvW) map type. - BlueBorderlands = 1 << 11, - /// Green Borderlands (WvW) map type. - GreenBorderlands = 1 << 12, - /// Red Borderlands (WvW) map type. - RedBorderlands = 1 << 13, - /// Fortune's Vale. Unused. - FortunesVale = 1 << 14, - /// Obsidian Sanctum (WvW) map type. - ObsidianSanctum = 1 << 15, - /// Edge of the Mists (WvW) map type. - EdgeOfTheMists = 1 << 16, - /// Mini public map type, e.g. Dry Top, the Silverwastes and Mistlock Sanctuary. - PublicMini = 1 << 17, - /// WvW lounge map type, e.g. Armistice Bastion. - WvwLounge = 1 << 18, -} -impl FromStr for MapType { - type Err = &'static str; - fn from_str(_s: &str) -> Result { - unimplemented!("needs research to verify the map type values") - } -} -impl AsRef for MapType { - fn as_ref(&self) -> &str { - unimplemented!("needs research to verify the maptype values") - } -} -impl ToString for MapType { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -/// made it using multi cursor (ctrl + shift + L) by copy-pasting json from api -#[allow(unused)] -pub static MAP_ID_TO_NAME: phf::OrderedMap = phf::phf_ordered_map! { - 15u16 => "Queensdale", - 17u16 => "Harathi Hinterlands", - 18u16 => "Divinity's Reach", - 19u16 => "Plains of Ashford", - 20u16 => "Blazeridge Steppes", - 21u16 => "Fields of Ruin", - 22u16 => "Fireheart Rise", - 23u16 => "Kessex Hills", - 24u16 => "Gendarran Fields", - 25u16 => "Iron Marches", - 26u16 => "Dredgehaunt Cliffs", - 27u16 => "Lornar's Pass", - 28u16 => "Wayfarer Foothills", - 29u16 => "Timberline Falls", - 30u16 => "Frostgorge Sound", - 31u16 => "Snowden Drifts", - 32u16 => "Diessa Plateau", - 33u16 => "Ascalonian Catacombs", - 34u16 => "Caledon Forest", - 35u16 => "Metrica Province", - 36u16 => "Ascalonian Catacombs", - 37u16 => "Arson at the Orphanage", - 38u16 => "Eternal Battlegrounds", - 39u16 => "Mount Maelstrom", - 50u16 => "Lion's Arch", - 51u16 => "Straits of Devastation", - 53u16 => "Sparkfly Fen", - 54u16 => "Brisban Wildlands", - 55u16 => "The Hospital in Jeopardy", - 61u16 => "Infiltration", - 62u16 => "Cursed Shore", - 63u16 => "Sorrow's Embrace", - 64u16 => "Sorrow's Embrace", - 65u16 => "Malchor's Leap", - 66u16 => "Citadel of Flame", - 67u16 => "Twilight Arbor", - 68u16 => "Twilight Arbor", - 69u16 => "Citadel of Flame", - 70u16 => "Honor of the Waves", - 71u16 => "Honor of the Waves", - 73u16 => "Bloodtide Coast", - 75u16 => "Caudecus's Manor", - 76u16 => "Caudecus's Manor", - 77u16 => "Search the Premises", - 79u16 => "The Informant", - 80u16 => "A Society Function", - 81u16 => "Crucible of Eternity", - 82u16 => "Crucible of Eternity", - 89u16 => "Chasing the Culprits", - 91u16 => "The Grove", - 92u16 => "The Trial of Julius Zamon", - 95u16 => " Alpine Borderlands", - 96u16 => " Alpine Borderlands", - 97u16 => "Infiltration", - 110u16 => "The Perils of Friendship", - 111u16 => "Victory or Death", - 112u16 => "The Ruined City of Arah", - 113u16 => "Desperate Medicine", - 120u16 => "The Commander", - 138u16 => "Defense of Shaemoor", - 139u16 => "Rata Sum", - 140u16 => "The Apothecary", - 142u16 => "Going Undercover", - 143u16 => "Going Undercover", - 144u16 => "The Greater Good", - 145u16 => "The Rescue", - 147u16 => "Breaking the Blade", - 148u16 => "The Fall of Falcon Company", - 149u16 => "The Fall of Falcon Company", - 152u16 => "Confronting Captain Tervelan", - 153u16 => "Seek Logan's Aid", - 154u16 => "Seek Logan's Aid", - 157u16 => "Accusation", - 159u16 => "Accusation", - 161u16 => "Liberation", - 162u16 => "Voices From the Past", - 163u16 => "Voices From the Past", - 171u16 => "Rending the Mantle", - 172u16 => "Rending the Mantle", - 178u16 => "The Floating Grizwhirl", - 179u16 => "The Floating Grizwhirl", - 180u16 => "The Floating Grizwhirl", - 182u16 => "Clown College", - 184u16 => "The Artist's Workshop", - 185u16 => "Into the Woods", - 186u16 => "The Ringmaster", - 190u16 => "The Orders of Tyria", - 191u16 => "The Orders of Tyria", - 192u16 => "Brute Force", - 193u16 => "Mortus Virge", - 195u16 => "Triskell Quay", - 196u16 => "Track the Seraph", - 198u16 => "Speaker of the Dead", - 199u16 => "The Sad Tale of the \"Ravenous\"", - 201u16 => "Kellach's Attack", - 202u16 => "The Queen's Justice", - 203u16 => "The Trap", - 211u16 => "Best Laid Plans", - 212u16 => "Welcome Home", - 215u16 => "The Tribune's Call", - 216u16 => "The Tribune's Call", - 217u16 => "The Tribune's Call", - 218u16 => "Black Citadel", - 222u16 => "A Spy for a Spy", - 224u16 => "Scrapyard Dogs", - 225u16 => "A Spy for a Spy", - 226u16 => "On the Mend", - 232u16 => "Spilled Blood", - 234u16 => "Ghostbore Musket", - 237u16 => "Iron Grip of the Legion", - 238u16 => "The Flame Advances", - 239u16 => "The Flame Advances", - 242u16 => "Test Your Metal", - 244u16 => "Quick and Quiet", - 248u16 => "Salma District (Home)", - 249u16 => "An Unusual Inheritance", - 250u16 => "Windrock Maze", - 251u16 => "Mired Deep", - 252u16 => "Mired Deep", - 254u16 => "Deadly Force", - 255u16 => "Ghostbore Artillery", - 256u16 => "No Negotiations", - 257u16 => "Salvaging Scrap", - 258u16 => "Salvaging Scrap", - 259u16 => "In the Ruins", - 260u16 => "In the Ruins", - 262u16 => "Chain of Command", - 263u16 => "Chain of Command", - 264u16 => "Time for a Promotion", - 267u16 => "The End of the Line", - 269u16 => "Magic Users", - 271u16 => "Rage Suppression", - 272u16 => "Rage Suppression", - 274u16 => "Operation: Bulwark", - 275u16 => "AWOL", - 276u16 => "Human's Lament", - 282u16 => "Misplaced Faith", - 283u16 => "Thicker Than Water", - 284u16 => "Dishonorable Discharge", - 287u16 => "Searching for the Truth", - 288u16 => "Lighting the Beacons", - 290u16 => "Stoking the Flame", - 294u16 => "A Fork in the Road", - 295u16 => "Sins of the Father", - 297u16 => "Graveyard Ornaments", - 326u16 => "Hoelbrak", - 327u16 => "Desperate Medicine", - 330u16 => "Seraph Headquarters", - 334u16 => "Keg Brawl", - 335u16 => "Claw Island", - 336u16 => "Chantry of Secrets", - 350u16 => "Heart of the Mists", - 363u16 => "The Sting", - 364u16 => "Drawing Out the Cult", - 365u16 => "Ashes of the Past", - 371u16 => "Hero's Canton (Home)", - 372u16 => "Blood Tribune Quarters", - 373u16 => "The Command Core", - 374u16 => "Knut Whitebear's Loft", - 375u16 => "Hunter's Hearth (Home)", - 376u16 => "Stonewright's Steading", - 378u16 => "Queen's Throne Room", - 379u16 => "The Great Hunt", - 380u16 => "A Weapon of Legend", - 381u16 => "The Last of the Giant-Kings", - 382u16 => "Disciples of the Dragon", - 385u16 => "A Weapon of Legend", - 386u16 => "Echoes of Ages Past", - 387u16 => "Wild Spirits", - 388u16 => "Out of the Skies", - 389u16 => "Echoes of Ages Past", - 390u16 => "Twilight of the Wolf", - 391u16 => "Rage of the Minotaurs", - 392u16 => "A Pup's Illness", - 393u16 => "Through the Veil", - 394u16 => "A Trap Foiled", - 396u16 => "Raven's Revered", - 397u16 => "One Good Drink Deserves Another", - 399u16 => "Shape of the Spirit", - 400u16 => "Into the Mists", - 401u16 => "Through the Veil", - 405u16 => "Blessed of Bear", - 407u16 => "The Wolf Havroun", - 410u16 => "Minotaur Rampant", - 411u16 => "Minotaur Rampant", - 412u16 => "Unexpected Visitors", - 413u16 => "Rumors of Trouble", - 414u16 => "A New Challenger", - 415u16 => "Unexpected Visitors", - 416u16 => "Roadblock", - 417u16 => "Assault on Moledavia", - 418u16 => "Don't Leave Your Toys Out", - 419u16 => "A New Challenger", - 420u16 => "First Attack", - 421u16 => "The Finishing Blow", - 422u16 => "The Semifinals", - 423u16 => "The Championship Fight", - 424u16 => "The Championship Fight", - 425u16 => "The Machine in Action", - 427u16 => "Among the Kodan", - 428u16 => "Rumors of Trouble", - 429u16 => "Rage of the Minotaurs", - 430u16 => "Darkness at Drakentelt", - 432u16 => "Fighting the Nightmare", - 434u16 => "Preserving the Balance", - 435u16 => "Means to an End", - 436u16 => "Dredge Technology", - 439u16 => "Underground Scholar", - 440u16 => "Dredge Assault", - 441u16 => "The Dredge Hideout", - 444u16 => "Sabotage", - 447u16 => "Codebreaker", - 449u16 => "Armaments", - 453u16 => "Assault the Hill", - 454u16 => "Silent Warfare", - 455u16 => "Sever the Head", - 458u16 => "Fury of the Dead", - 459u16 => "A Fork in the Road", - 460u16 => "Citadel Stockade", - 464u16 => "Tribunes in Effigy", - 465u16 => "Sins of the Father", - 466u16 => "Misplaced Faith", - 470u16 => "Graveyard Ornaments", - 471u16 => "Undead Infestation", - 474u16 => "Whispers in the Dark", - 476u16 => "Dangerous Research", - 477u16 => "Digging Up Answers", - 480u16 => "Defending the Keep", - 481u16 => "Undead Detection", - 483u16 => "Ever Vigilant", - 485u16 => "Research and Destroy", - 487u16 => "Whispers of Vengeance", - 488u16 => "Killer Instinct", - 489u16 => "Meeting my Mentor", - 490u16 => "A Fragile Peace", - 492u16 => "Don't Shoot the Messenger", - 496u16 => "Meeting my Mentor", - 497u16 => "Dredging Up the Past", - 498u16 => "Dredging Up the Past", - 499u16 => "Scrapyard Dogs", - 502u16 => "Quaestor's Siege", - 503u16 => "Minister's Defense", - 504u16 => "Called to Service", - 505u16 => "Called to Service", - 507u16 => "Mockery of Death", - 509u16 => "Discovering Darkness", - 511u16 => "Hounds and the Hunted", - 512u16 => "Hounds and the Hunted", - 513u16 => "Loved and Lost", - 514u16 => "Saving the Stag", - 515u16 => "Hidden in Darkness", - 516u16 => "Good Work Spoiled", - 517u16 => "Black Night, White Stag", - 518u16 => "The Omphalos Chamber", - 519u16 => "Weakness of the Heart", - 520u16 => "Awakening", - 521u16 => "Holding Back the Darkness", - 522u16 => "A Sly Trick", - 523u16 => "Deep Tangled Roots", - 524u16 => "The Heart of Nightmare", - 525u16 => "Beneath a Cold Moon", - 527u16 => "The Knight's Duel", - 528u16 => "Hammer and Steel", - 529u16 => "Where Life Goes", - 532u16 => "After the Storm", - 533u16 => "After the Storm", - 534u16 => "Beneath the Waves", - 535u16 => "Mirror, Mirror", - 536u16 => "A Vision of Darkness", - 537u16 => "Shattered Light", - 538u16 => "An Unknown Soul", - 539u16 => "An Unknown Soul", - 540u16 => "Where Life Goes", - 542u16 => "Source of the Issue", - 543u16 => "Wild Growth", - 544u16 => "Wild Growth", - 545u16 => "Seeking the Zalisco", - 546u16 => "The Direct Approach", - 547u16 => "Trading Trickery", - 548u16 => "Eye of the Sun", - 549u16 => "Battle of Kyhlo", - 552u16 => "Seeking the Zalisco", - 554u16 => "Forest of Niflhel", - 556u16 => "A Different Dream", - 557u16 => "A Splinter in the Flesh", - 558u16 => "Shadow of the Tree", - 559u16 => "Eye of the Sun", - 560u16 => "Sharpened Thorns", - 561u16 => "Bramble Walls", - 563u16 => "Secrets in the Earth", - 564u16 => "The Blossom of Youth", - 566u16 => "The Bad Apple", - 567u16 => "Trouble at the Roots", - 569u16 => "Flower of Death", - 570u16 => "Dead of Winter", - 571u16 => "A Tangle of Weeds", - 573u16 => "Explosive Intellect", - 574u16 => "In Snaff's Footsteps", - 575u16 => "Golem Positioning System", - 576u16 => "Monkey Wrench", - 577u16 => "Defusing the Problem", - 578u16 => "The Things We Do For Love", - 579u16 => "The Snaff Prize", - 581u16 => "A Sparkling Rescue", - 582u16 => "High Maintenance", - 583u16 => "Snaff Would Be Proud", - 584u16 => "Taking Credit Back", - 586u16 => "Political Homicide", - 587u16 => "Here, There, Everywhere", - 588u16 => "Piece Negotiations", - 589u16 => "Readings On the Rise", - 590u16 => "Snaff Would Be Proud", - 591u16 => "Readings On the Rise", - 592u16 => "Unscheduled Delay", - 594u16 => "Stand By Your Krewe", - 595u16 => "Unwelcome Visitors", - 596u16 => "Where Credit Is Due", - 597u16 => "Where Credit Is Due", - 598u16 => "Short Fuse", - 599u16 => "Short Fuse", - 606u16 => "Salt in the Wound", - 607u16 => "Free Rein", - 608u16 => "Serving Up Trouble", - 609u16 => "Serving Up Trouble", - 610u16 => "Flash Flood", - 611u16 => "I Smell a Rat", - 613u16 => "Magnum Opus", - 614u16 => "Magnum Opus", - 617u16 => "Bad Business", - 618u16 => "Beta Test", - 619u16 => "Beta Test", - 620u16 => "Any Sufficiently Advanced Science", - 621u16 => "Any Sufficiently Advanced Science", - 622u16 => "Bad Forecast", - 623u16 => "Industrial Espionage", - 624u16 => "Split Second", - 625u16 => "Carry a Big Stick", - 627u16 => "Meeting my Mentor", - 628u16 => "Stealing Secrets", - 629u16 => "A Bold New Theory", - 630u16 => "Forging Permission", - 631u16 => "Forging Permission", - 633u16 => "Setting the Stage", - 634u16 => "Containment", - 635u16 => "Containment", - 636u16 => "Hazardous Environment", - 638u16 => "Down the Hatch", - 639u16 => "Down the Hatch", - 642u16 => "The Stone Sheath", - 643u16 => "Bad Blood", - 644u16 => "Test Subject", - 645u16 => "Field Test", - 646u16 => "The House of Caithe", - 647u16 => "Dreamer's Terrace (Home)", - 648u16 => "The Omphalos Chamber", - 649u16 => "Snaff Memorial Lab", - 650u16 => "Applied Development Lab (Home)", - 651u16 => "Council Level", - 652u16 => "A Meeting of the Minds", - 653u16 => "Mightier than the Sword", - 654u16 => "They Went Thataway", - 655u16 => "Lines of Communication", - 656u16 => "Untamed Wilds", - 657u16 => "An Apple a Day", - 658u16 => "Base of Operations", - 659u16 => "The Lost Chieftain's Return", - 660u16 => "Thrown Off Guard", - 662u16 => "Pets and Walls Make Stronger Kraals", - 663u16 => "Doubt", - 664u16 => "The False God's Lair", - 666u16 => "Bad Ice", - 667u16 => "Bad Ice", - 668u16 => "Pets and Walls Make Stronger Kraals", - 669u16 => "Attempted Deicide", - 670u16 => "Doubt", - 672u16 => "Rat-Tastrophe", - 673u16 => "Salvation Through Heresy", - 674u16 => "Enraged and Unashamed", - 675u16 => "Pastkeeper", - 676u16 => "Protest Too Much", - 677u16 => "Prying the Eye Open", - 678u16 => "The Hatchery", - 680u16 => "Convincing the Faithful", - 681u16 => "Evacuation", - 682u16 => "Untamed Wilds", - 683u16 => "Champion's Sacrifice", - 684u16 => "Thieving from Thieves", - 685u16 => "Crusader's Return", - 686u16 => "Unholy Grounds", - 687u16 => "Chosen of the Sun", - 691u16 => "Set to Blow", - 692u16 => "Gadd's Last Gizmo", - 693u16 => "Library Science", - 694u16 => "Rakt and Ruin", - 695u16 => "Suspicious Activity", - 696u16 => "Reconnaissance", - 697u16 => "Critical Blowback", - 698u16 => "The Battle of Claw Island", - 699u16 => "Suspicious Activity", - 700u16 => "Priory Library", - 701u16 => "On Red Alert", - 702u16 => "Forearmed Is Forewarned", - 703u16 => "The Oratory", - 704u16 => "Killing Fields", - 705u16 => "The Ghost Rite", - 706u16 => "The Good Fight", - 707u16 => "Defense Contract", - 708u16 => "Shards of Orr", - 709u16 => "The Sound of Psi-Lance", - 710u16 => "Early Parole", - 711u16 => "Magic Sucks", - 712u16 => "A Light in the Darkness", - 713u16 => "The Priory Assailed", - 714u16 => "Under Siege", - 715u16 => "Retribution", - 716u16 => "Retribution", - 719u16 => "The Sound of Psi-Lance", - 726u16 => "Wet Work", - 727u16 => "Shell Shock", - 728u16 => "Volcanic Extraction", - 729u16 => "Munition Acquisition", - 730u16 => "To the Core", - 731u16 => "The Battle of Fort Trinity", - 732u16 => "Tower Down", - 733u16 => "Forging the Pact", - 735u16 => "Willing Captives", - 736u16 => "Marshaling the Truth", - 737u16 => "Breaking the Bone Ship", - 738u16 => "Liberating Apatia", - 739u16 => "Liberating Apatia", - 743u16 => "Fixing the Blame", - 744u16 => "A Sad Duty", - 745u16 => "Striking off the Chains", - 746u16 => "Delivering Justice", - 747u16 => "Intercepting the Orb", - 750u16 => "Close the Eye", - 751u16 => "Through the Looking Glass", - 758u16 => "The Cathedral of Silence", - 760u16 => "Starving the Beast", - 761u16 => "Stealing Light", - 762u16 => "Hunters and Prey", - 763u16 => "Romke's Final Voyage", - 764u16 => "Marching Orders", - 766u16 => "Air Drop", - 767u16 => "Estate of Decay", - 768u16 => "What the Eye Beholds", - 769u16 => "Conscript the Dead Ships", - 772u16 => "Ossuary of Unquiet Dead", - 775u16 => "Temple of the Forgotten God", - 776u16 => "Temple of the Forgotten God", - 777u16 => "Temple of the Forgotten God", - 778u16 => "Through the Looking Glass", - 779u16 => "Starving the Beast", - 780u16 => "Against the Corruption", - 781u16 => "The Source of Orr", - 782u16 => "Armor Guard", - 783u16 => "Blast from the Past", - 784u16 => "The Steel Tide", - 785u16 => "Further Into Orr", - 786u16 => "Ships of the Line", - 787u16 => "Source of Orr", - 788u16 => "Victory or Death", - 789u16 => "A Grisly Shipment", - 790u16 => "Blast from the Past", - 792u16 => "A Pup's Illness", - 793u16 => "Hunters and Prey", - 795u16 => "Legacy of the Foefire", - 796u16 => "The Informant", - 797u16 => "A Traitor's Testimony", - 799u16 => "Follow the Trail", - 806u16 => "Awakening", - 807u16 => "Eye of the North", - 820u16 => "The Omphalos Chamber", - 821u16 => "The Omphalos Chamber", - 825u16 => "Codebreaker", - 827u16 => "Caer Aval", - 828u16 => "The Durmand Priory", - 830u16 => "Vigil Headquarters", - 833u16 => "Ash Tribune Quarters", - 845u16 => "Shattered Light", - 862u16 => "Reaper's Rumble", - 863u16 => "Ascent to Madness", - 864u16 => "Lunatic Inquisition", - 865u16 => "Mad King's Clock Tower", - 866u16 => "Mad King's Labyrinth", - 872u16 => "Fractals of the Mists", - 873u16 => "Southsun Cove", - 875u16 => "Temple of the Silent Storm", - 877u16 => "Snowball Mayhem", - 878u16 => "Tixx's Infinirarium", - 880u16 => "Toypocalypse", - 881u16 => "Bell Choir Ensemble", - 882u16 => "Winter Wonderland", - 894u16 => "Spirit Watch", - 895u16 => "Super Adventure Box", - 896u16 => "North Nolan Hatchery", - 897u16 => "Cragstead", - 899u16 => "Obsidian Sanctum", - 900u16 => "Skyhammer", - 901u16 => "Molten Furnace", - 905u16 => "Crab Toss", - 911u16 => "Dragon Ball Arena", - 912u16 => "Ceremony and Acrimony—Memorials on the Pyre", - 913u16 => "Hard Boiled—The Scene of the Crime", - 914u16 => "The Dead End", - 915u16 => "Aetherblade Retreat", - 917u16 => "No More Secrets—The Scene of the Crime", - 918u16 => "Aspect Arena", - 919u16 => "Sanctum Sprint", - 920u16 => "Southsun Survival", - 922u16 => "Labyrinthine Cliffs", - 924u16 => "Grandmaster of Om", - 929u16 => "The Crown Pavilion", - 930u16 => "Opening Ceremony", - 931u16 => "Scarlet's Playhouse", - 932u16 => "Closing Ceremony", - 934u16 => "Super Adventure Box", - 935u16 => "Super Adventure Box", - 937u16 => "Scarlet's End", - 943u16 => "The Tower of Nightmares (Public)", - 945u16 => "The Nightmare Ends", - 947u16 => "Fractals of the Mists", - 948u16 => "Fractals of the Mists", - 949u16 => "Fractals of the Mists", - 950u16 => "Fractals of the Mists", - 951u16 => "Fractals of the Mists", - 952u16 => "Fractals of the Mists", - 953u16 => "Fractals of the Mists", - 954u16 => "Fractals of the Mists", - 955u16 => "Fractals of the Mists", - 956u16 => "Fractals of the Mists", - 957u16 => "Fractals of the Mists", - 958u16 => "Fractals of the Mists", - 959u16 => "Fractals of the Mists", - 960u16 => "Fractals of the Mists", - 964u16 => "Scarlet's Secret Lair", - 965u16 => "The Origins of Madness: A Moment's Peace", - 968u16 => "Edge of the Mists", - 971u16 => "The Dead End: A Study in Scarlet", - 973u16 => "The Evacuation of Lion's Arch", - 980u16 => "The Dead End: Celebration", - 984u16 => "Courtyard", - 987u16 => "Lion's Arch: Honored Guests", - 988u16 => "Dry Top", - 989u16 => "Prosperity's Mystery", - 990u16 => "Cornered", - 991u16 => "Disturbance in Brisban Wildlands", - 992u16 => "Fallen Hopes", - 993u16 => "Scarlet's Secret Room", - 994u16 => "The Concordia Incident", - 997u16 => "Discovering Scarlet's Breakthrough", - 998u16 => "The Machine", - 999u16 => "Trouble at Fort Salma", - 1000u16 => "The Waypoint Conundrum", - 1001u16 => "Summit Invitations", - 1002u16 => "Mission Accomplished", - 1003u16 => "Rallying Call", - 1004u16 => "Plan of Attack", - 1005u16 => "Party Politics", - 1006u16 => "Foefire Cleansing", - 1007u16 => "Recalibrating the Waypoints", - 1008u16 => "The Ghosts of Fort Salma", - 1009u16 => "Taimi's Device", - 1010u16 => "The World Summit", - 1011u16 => "Battle of Champion's Dusk", - 1015u16 => "The Silverwastes", - 1016u16 => "Hidden Arcana", - 1017u16 => "Reunion with the Pact", - 1018u16 => "Caithe's Reconnaissance Squad", - 1019u16 => "Fort Trinity", - 1021u16 => "Into the Labyrinth", - 1022u16 => "Return to Camp Resolve", - 1023u16 => "Tracking the Aspect Masters", - 1024u16 => "No Refuge", - 1025u16 => "The Newly Awakened", - 1026u16 => "Meeting the Asura", - 1027u16 => "Pact Assaulted", - 1028u16 => "The Mystery Cave", - 1029u16 => "Arcana Obscura", - 1032u16 => "Prized Possessions", - 1033u16 => "Buried Insight", - 1037u16 => "The Jungle Provides", - 1040u16 => "Hearts and Minds", - 1041u16 => "Dragon's Stand", - 1042u16 => "Verdant Brink", - 1043u16 => "Auric Basin", - 1045u16 => "Tangled Depths", - 1046u16 => "Roots of Terror", - 1048u16 => "City of Hope", - 1050u16 => "Torn from the Sky", - 1051u16 => "Prisoners of the Dragon", - 1052u16 => "Verdant Brink", - 1054u16 => "Bitter Harvest", - 1057u16 => "Strange Observations", - 1058u16 => "Prologue: Rally to Maguuma", - 1062u16 => "Spirit Vale", - 1063u16 => "Southsun Crab Toss", - 1064u16 => "Claiming the Lost Precipice", - 1065u16 => "Angvar's Trove", - 1066u16 => "Claiming the Gilded Hollow", - 1067u16 => "Angvar's Trove", - 1068u16 => "Gilded Hollow", - 1069u16 => "Lost Precipice", - 1070u16 => "Claiming the Lost Precipice", - 1071u16 => "Lost Precipice", - 1072u16 => "Southsun Crab Toss", - 1073u16 => "Guild Initiative Office", - 1074u16 => "Blightwater Shatterstrike", - 1075u16 => "Proxemics Lab", - 1076u16 => "Lost Precipice", - 1078u16 => "Claiming the Gilded Hollow", - 1079u16 => "Deep Trouble", - 1080u16 => "Branded for Termination", - 1081u16 => "Langmar Estate", - 1082u16 => "Langmar Estate", - 1083u16 => "Deep Trouble", - 1084u16 => "Southsun Crab Toss", - 1086u16 => "Save Our Supplies", - 1087u16 => "Proxemics Lab", - 1088u16 => "Claiming the Gilded Hollow", - 1089u16 => "Angvar's Trove", - 1090u16 => "Langmar Estate", - 1091u16 => "Save Our Supplies", - 1092u16 => "Scratch Sentry Defense", - 1093u16 => "Angvar's Trove", - 1094u16 => "Save Our Supplies", - 1095u16 => "Dragon's Stand (Heart of Thorns)", - 1097u16 => "Proxemics Lab", - 1098u16 => "Claiming the Gilded Hollow", - 1099u16 => " Desert Borderlands", - 1100u16 => "Scratch Sentry Defense", - 1101u16 => "Gilded Hollow", - 1104u16 => "Lost Precipice", - 1105u16 => "Langmar Estate", - 1106u16 => "Deep Trouble", - 1107u16 => "Gilded Hollow", - 1108u16 => "Gilded Hollow", - 1109u16 => "Angvar's Trove", - 1110u16 => "Scrap Rifle Field Test", - 1111u16 => "Scratch Sentry Defense", - 1112u16 => "Branded for Termination", - 1113u16 => "Scratch Sentry Defense", - 1115u16 => "Haywire Punch-o-Matic Battle", - 1116u16 => "Deep Trouble", - 1117u16 => "Claiming the Lost Precipice", - 1118u16 => "Save Our Supplies", - 1121u16 => "Gilded Hollow", - 1122u16 => "Claiming the Gilded Hollow", - 1123u16 => "Blightwater Shatterstrike", - 1124u16 => "Lost Precipice", - 1126u16 => "Southsun Crab Toss", - 1128u16 => "Scratch Sentry Defense", - 1129u16 => "Langmar Estate", - 1130u16 => "Deep Trouble", - 1131u16 => "Blightwater Shatterstrike", - 1132u16 => "Claiming the Lost Precipice", - 1133u16 => "Branded for Termination", - 1134u16 => "Blightwater Shatterstrike", - 1135u16 => "Branded for Termination", - 1136u16 => "Proxemics Lab", - 1137u16 => "Proxemics Lab", - 1138u16 => "Save Our Supplies", - 1139u16 => "Southsun Crab Toss", - 1140u16 => "Claiming the Lost Precipice", - 1142u16 => "Blightwater Shatterstrike", - 1146u16 => "Branded for Termination", - 1147u16 => "Spirit Vale", - 1149u16 => "Salvation Pass", - 1153u16 => "Tiger Den", - 1154u16 => "Special Forces Training Area", - 1155u16 => "Lion's Arch Aerodrome", - 1156u16 => "Stronghold of the Faithful", - 1158u16 => "Noble's Folly", - 1159u16 => "Research in Rata Novus", - 1161u16 => "Eir's Homestead", - 1163u16 => "Revenge of the Capricorn", - 1164u16 => "Fractals of the Mists", - 1165u16 => "Bloodstone Fen", - 1166u16 => "Confessor's Stronghold", - 1167u16 => "A Shadow's Deeds", - 1169u16 => "Rata Novus", - 1170u16 => "Taimi's Game", - 1171u16 => "Eternal Coliseum", - 1172u16 => "Dragon Vigil", - 1173u16 => "Taimi's Game", - 1175u16 => "Ember Bay", - 1176u16 => "Taimi's Game", - 1177u16 => "Fractals of the Mists", - 1178u16 => "Bitterfrost Frontier", - 1180u16 => "The Bitter Cold", - 1181u16 => "Frozen Out", - 1182u16 => "Precocious Aurene", - 1185u16 => "Lake Doric", - 1188u16 => "Bastion of the Penitent", - 1189u16 => "Regrouping with the Queen", - 1190u16 => "A Meeting of Ministers", - 1191u16 => "Confessor's End", - 1192u16 => "The Second Vision", - 1193u16 => "The First Vision", - 1194u16 => "The Sword Regrown", - 1195u16 => "Draconis Mons", - 1196u16 => "Heart of the Volcano", - 1198u16 => "Taimi's Pet Project", - 1200u16 => "Hall of the Mists", - 1201u16 => "Asura Arena", - 1202u16 => "White Mantle Hideout", - 1203u16 => "Siren's Landing", - 1204u16 => "Palace Temple", - 1205u16 => "Fractals of the Mists", - 1206u16 => "Mistlock Sanctuary", - 1207u16 => "The Last Chance", - 1208u16 => "Shining Blade Headquarters", - 1209u16 => "The Sacrifice", - 1210u16 => "Crystal Oasis", - 1211u16 => "Desert Highlands", - 1212u16 => "Office of the Chief Councilor", - 1214u16 => "Windswept Haven", - 1215u16 => "Windswept Haven", - 1217u16 => "Sparking the Flame", - 1219u16 => "Enemy of My Enemy: The Beastmarshal", - 1220u16 => "Sparking the Flame (Prologue)", - 1221u16 => "The Way Forward", - 1222u16 => "Claiming Windswept Haven", - 1223u16 => "Small Victory (Epilogue)", - 1224u16 => "Windswept Haven", - 1226u16 => "The Desolation", - 1227u16 => "Hallowed Ground: Tomb of Primeval Kings", - 1228u16 => "Elon Riverlands", - 1230u16 => "Facing the Truth: The Sanctum", - 1231u16 => "Claiming Windswept Haven", - 1232u16 => "Windswept Haven", - 1234u16 => "To Kill a God", - 1236u16 => "Claiming Windswept Haven", - 1240u16 => "Blazing a Trail", - 1241u16 => "Night of Fires", - 1242u16 => "Zalambur's Office", - 1243u16 => "Windswept Haven", - 1244u16 => "Claiming Windswept Haven", - 1245u16 => "The Departing", - 1246u16 => "Captain Kiel's Office", - 1247u16 => "Enemy of My Enemy", - 1248u16 => "Domain of Vabbi", - 1250u16 => "Windswept Haven", - 1252u16 => "Crystalline Memories", - 1253u16 => "Beast of War", - 1255u16 => "Enemy of My Enemy: The Troopmarshal", - 1256u16 => "The Dark Library", - 1257u16 => "Spearmarshal's Lament", - 1260u16 => "Eye of the Brandstorm", - 1263u16 => "Domain of Istan", - 1264u16 => "Hall of Chains", - 1265u16 => "The Hero of Istan", - 1266u16 => "Cave of the Sunspear Champion", - 1267u16 => "Fractals of the Mists", - 1268u16 => "Fahranur, the First City", - 1270u16 => "Toypocalypse", - 1271u16 => "Sandswept Isles", - 1274u16 => "The Charge", - 1275u16 => "Courtyard", - 1276u16 => "The Test Subject", - 1277u16 => "The Charge", - 1278u16 => "???", - 1279u16 => "ERROR: SIGNAL LOST", - 1281u16 => "A Kindness Repaid", - 1282u16 => "Tracking the Scientist", - 1283u16 => "???", - 1285u16 => "???", - 1288u16 => "Domain of Kourna", - 1289u16 => "Seized", - 1290u16 => "Fractals of the Mists", - 1291u16 => "Forearmed Is Forewarned", - 1292u16 => "Be My Guest", - 1294u16 => "Sun's Refuge", - 1295u16 => "Legacy", - 1296u16 => "Storm Tracking", - 1297u16 => "A Shattered Nation", - 1299u16 => "Storm Tracking", - 1300u16 => "From the Ashes—The Deadeye", - 1301u16 => "Jahai Bluffs", - 1302u16 => "Storm Tracking", - 1303u16 => "Mythwright Gambit", - 1304u16 => "Mad King's Raceway", - 1305u16 => "Djinn's Dominion", - 1306u16 => "Secret Lair of the Snowmen (Squad)", - 1308u16 => "Scion & Champion", - 1309u16 => "Fractals of the Mists", - 1310u16 => "Thunderhead Peaks", - 1313u16 => "The Crystal Dragon", - 1314u16 => "The Crystal Blooms", - 1315u16 => "Armistice Bastion", - 1316u16 => "Mists Rift", - 1317u16 => "Dragonfall", - 1318u16 => "Dragonfall", - 1319u16 => "Descent", - 1320u16 => "The End", - 1321u16 => "Dragonflight", - 1322u16 => "Epilogue", - 1323u16 => "The Key of Ahdashim", - 1326u16 => "Dragon Bash Arena", - 1327u16 => "Dragon Arena Survival", - 1328u16 => "Auric Span", - 1329u16 => "Coming Home", - 1330u16 => "Grothmar Valley", - 1331u16 => "Strike Mission: Shiverpeaks Pass (Public)", - 1332u16 => "Strike Mission: Shiverpeaks Pass (Squad)", - 1334u16 => "Deeper and Deeper", - 1336u16 => "A Race to Arms", - 1338u16 => "Bad Blood", - 1339u16 => "Weekly Strike Mission: Boneskinner (Squad)", - 1340u16 => "Weekly Strike Mission: Voice of the Fallen and Claw of the Fallen (Public)", - 1341u16 => "Weekly Strike Mission: Fraenir of Jormag (Squad)", - 1342u16 => "The Invitation", - 1343u16 => "Bjora Marches", - 1344u16 => "Weekly Strike Mission: Fraenir of Jormag (Public)", - 1345u16 => "What's Left Behind", - 1346u16 => "Weekly Strike Mission: Voice of the Fallen and Claw of the Fallen (Squad)", - 1349u16 => "Silence", - 1351u16 => "Weekly Strike Mission: Boneskinner (Public)", - 1352u16 => "Secret Lair of the Snowmen (Public)", - 1353u16 => "Celestial Challenge", - 1355u16 => "Voice in the Deep", - 1356u16 => "Chasing Ghosts", - 1357u16 => "Strike Mission: Whisper of Jormag (Public)", - 1358u16 => "Eye of the North", - 1359u16 => "Strike Mission: Whisper of Jormag (Squad)", - 1361u16 => "The Nightmare Incarnate", - 1362u16 => "Forging Steel (Public)", - 1363u16 => "New Friends, New Enemies—North Nolan Hatchery", - 1364u16 => "The Battle for Cragstead", - 1366u16 => "Darkrime Delves", - 1368u16 => "Forging Steel (Squad)", - 1369u16 => "Canach's Lair", - 1370u16 => "Eye of the North", - 1371u16 => "Drizzlewood Coast", - 1372u16 => "Turnabout", - 1373u16 => "Pointed Parley", - 1374u16 => "Strike Mission: Cold War (Squad)", - 1375u16 => "Snapping Steel", - 1376u16 => "Strike Mission: Cold War (Public)", - 1378u16 => "Behind Enemy Lines", - 1379u16 => "One Charr, One Dragon, One Champion", - 1380u16 => "Epilogue", - 1382u16 => "Arena of the Wolverine", - 1383u16 => "A Simple Negotiation", - 1384u16 => "Fractals of the Mists", - 1385u16 => "Caledon Forest (Private)", - 1386u16 => "Thunderhead Peaks (Private)", - 1387u16 => "Bloodtide Coast (Public)", - 1388u16 => "Snowden Drifts (Private)", - 1389u16 => "Snowden Drifts (Public)", - 1390u16 => "Fireheart Rise (Public)", - 1391u16 => "Brisban Wildlands (Private)", - 1392u16 => "Primordus Rising", - 1393u16 => "Lake Doric (Public)", - 1394u16 => "Bloodtide Coast (Private)", - 1395u16 => "Thunderhead Peaks (Public)", - 1396u16 => "Gendarran Fields (Public)", - 1397u16 => "Metrica Province (Public)", - 1398u16 => "Fields of Ruin (Public)", - 1399u16 => "Brisban Wildlands (Public)", - 1400u16 => "Fields of Ruin (Private)", - 1401u16 => "Metrica Province (Private)", - 1402u16 => "Lake Doric (Private)", - 1403u16 => "Caledon Forest (Public)", - 1404u16 => "Fireheart Rise (Private)", - 1405u16 => "Gendarran Fields (Private)", - 1407u16 => "Council Level", - 1408u16 => "Wildfire", - 1409u16 => "Dragonstorm (Private Squad)", - 1410u16 => "Champion's End", - 1411u16 => "Dragonstorm (Public)", - 1412u16 => "Dragonstorm", - 1413u16 => "The Twisted Marionette (Public)", - 1414u16 => "The Twisted Marionette (Private Squad)", - 1415u16 => "The Future in Jade: Power Plant", - 1416u16 => "Deepest Secrets: Yong Reactor", - 1419u16 => "Isle of Reflection", - 1420u16 => "Fallout: Nika's Blade", - 1421u16 => "???", - 1422u16 => "Dragon's End", - 1426u16 => "Isle of Reflection", - 1427u16 => "Weight of the World: Lady Joon's Estate", - 1428u16 => "Arborstone", - 1429u16 => "The Cycle, Reborn: Arborstone", - 1430u16 => "Claiming the Isle of Reflection", - 1432u16 => "Strike Mission: Aetherblade Hideout", - 1433u16 => "Old Friends", - 1434u16 => "Empty", - 1435u16 => "Isle of Reflection", - 1436u16 => "Extraction Point: Command Quarters", - 1437u16 => "Strike Mission: Harvest Temple", - 1438u16 => "New Kaineng City", - 1439u16 => "The Only One", - 1440u16 => "Laying to Rest", - 1442u16 => "Seitung Province", - 1444u16 => "Isle of Reflection", - 1445u16 => "The Future in Jade: Nahpui Lab", - 1446u16 => "Aetherblade Armada", - 1448u16 => "The Cycle, Reborn: The Dead End Bar", - 1449u16 => "Aurene's Sanctuary", - 1450u16 => "Strike Mission: Xunlai Jade Junkyard", - 1451u16 => "Strike Mission: Kaineng Overlook", - 1452u16 => "The Echovald Wilds", - 1453u16 => "Ministry of Security: Main Office", - 1454u16 => "The Scenic Route: Kaineng Docks", - 1456u16 => "Claiming the Isle of Reflection", - 1457u16 => "Detention Facility", - 1458u16 => "Aurene's Sanctuary", - 1459u16 => "Claiming the Isle of Reflection", - 1460u16 => "Empress Ihn's Court", - 1461u16 => "Zen Daijun Hideaway", - 1462u16 => "Isle of Reflection", - 1463u16 => "Claiming the Isle of Reflection", - 1464u16 => "Fallout: Arborstone", - 1465u16 => "Thousand Seas Pavilion", - 1466u16 => "A Quiet Celebration—Knut Whitebear's Loft", - 1467u16 => "New Friends, New Enemies—The Command Core", - 1468u16 => "The Battle for Cragstead—Knut Whitebear's Loft", - 1469u16 => "New Friends, New Enemies—Blood Tribune Quarters", - 1470u16 => "A Quiet Celebration—Citadel Stockade", - 1471u16 => "Case Closed—The Dead End", - 1472u16 => "Hard Boiled—The Dead End", - 1474u16 => "Picking Up the Pieces", - 1477u16 => "The Tower of Nightmares (Private Squad)", - 1478u16 => "The Battle for Lion's Arch (Private Squad)", - 1480u16 => "The Twisted Marionette", - 1481u16 => "Battle on the Breachmaker", - 1482u16 => "The Battle For Lion's Arch (Public)", - 1483u16 => "Memory of Old Lion's Arch", - 1484u16 => "North Evacuation Camp", - 1485u16 => "Strike Mission: Old Lion's Court", - 1487u16 => "The Aether Escape", - 1488u16 => "On the Case: Excavation Yard", - 1489u16 => "A Raw Deal: Red Duck Tea House", - 1490u16 => "Gyala Delve", - 1491u16 => "Deep Trouble: Excavation Yard", - 1492u16 => "Deep Trouble: The Deep", - 1494u16 => "Entrapment: The Deep", - 1495u16 => "A Plan Emerges: Power Plant", - 1496u16 => "Emotional Release: Jade Pools", - 1497u16 => "Emotional Release: Command Quarters", - 1498u16 => "Full Circle: Red Duck Tea House", - 1499u16 => "Forward", - 1500u16 => "Fractals of the Mists", -}; diff --git a/crates/joko_marker_format/src/pack/marker.png b/crates/joko_marker_format/src/pack/marker.png deleted file mode 100755 index 294a322e8475b221dbee3297868b719baa40f656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173015 zcmd3MRZ|>H(r6+1RAkYRiIAb7pwQ&yq|~9Hp#Oo;P>AsVI(?T4%l|M}by*3hnn{wQe+ZnF zxUx7D)UO1TR};WL9LY)Uiz^fqdf$HyddRWD913a;RbEP5)64MGAEAI$`tBk1TT8~m z%b4OtT4?^X;$bJfax@T2N>fVgJ(wObvgnpo?tDkpx^mLrnn9UnS(Tudl9{8_uYVL5_sBbkAk@AFs8=&JorjlDM6&YF1sXM@(R9 zHLd?gL=5C?4A3j7Y`r%M+lh_={%PxMeNOAqS~X~Mby9M6$J{Xa-(0BV0bxwrtJ}wc zXlW~R%4WJ_Eq;j?%nr|^oBTUysgx18ItW*!g9R{;=_Y5t?b#w1RqCBL6%_>tjqBh-is^Ny5=LW-dXOZ_XdBmKCn7Gx71DHmWCL_e8AfOK{h^y)_ULmzVR6``jIn?Pc|c#840(=pE9)SbCX}H z0#HXZq*5a(x~(;i|8TyB#-MXx;sXy+yq0pZt3LTzX!v5fEmoPi27UX3y4ePh=-%Ld zSnp^>cv!U%8-~a@{9@w#=b*?`Lb`3UpYh+u41S!#Onwno&)dM|eOrM*UxjB4B8DN|F0_Vb!lkBh;{x^ez-*kz!&3S5m#+KpLx%@a=iDtx5PrSZGB9K==ZC$UHG z?>Aum21QL$6RmytY0lyv*E0~gbHcmarSXEI^*kPyc)T0P8_2>PoeT?BtkZOL2yp)M zq4PH(p+DGtFzX8b+sEN_m8Zr8Lv5>F4&?Jss86P7%it01gR_4J0$-^2;T9JCcE>JL z6+bvX(dcljAKJP~w*pXLY`O{N=`tE{?IO~`ZD`%Zt%)(yq@aZ}4DsKNV>e;5nDz2p ziwKYc25da@;vAGNPtbg)d&SPW*XKpNd6tVhUnZNx#H}vXl3|^(SsIW8O|%|{CKswv zFQqG^Z7{Iwqg;pBmcy;v8<{%LlU-|p6(d3~{t|YV)zWMjt=@kCl)21yikhDH^dmJ2 zG{4*Vi@L5f)4pQ2K6Xr)4v$Y?@KfpVOexdEtfFd5y&FR88Gl1uMQ&bY== zbb+m@#F6|(mj*8`LtktUitLRBDi-N|j=SF#|U~y8WhV43yweF@i`p<+TyV z*i9o$W`v?Ur9|J>_0Ypqq8nG&`VJ=oR8qbdsFX z8c}0!nCjKuO2{*A&t7D4{ODEu`FA@5H$|nN^FoH{c#>ITQ*DzQ&B_nY@N+zmxq{(c zfq||35Lskmw7B^v3^Z+`KI%OmkvSVrT-6uUGL^h4OXuTX`l3%>#%BAD|1=nv`wW*J zW9aV_sN%C4Au={=I4BLwC;+Hl)#**cKEFX`&YhadA3$9PS{%$?8layIlsyxLa z)48Y3E)S~cSrZeQSBG&C@$uOgjq?ms5IAuqFO0`x>GGIr@BSwz1;i^t9L$ZTwU+7z^2St z4zyMnYTl{9Z_o{0N9DH~2oDKR0+~!3>@n1hnt>weX?0nLeT9 zWdM0UWI6m~{NRNymU<;+DOsNZ+d82?mIc$P_cV9=i!@Q9JJac|>ADdSUiO7WV?^u6lmOD?{$`7&sg@YS$^5Ob`5E zk~t&XSfAMO#O?m0R?*&9rQLyd$!d+;n*V8x^r$8+)G+kW@e z$-eL+Ufl#!P+4BEprH3va?;4aLXC6|ar-+C0apXF9I9{NiWWu-=eFD=sC<9N6)@!Z z7-G&sFp*@LuA&wB2!++ykY*uBXd*<;c;?zWlbtj=h@G&otFiu~ZJ4*wQtx;(y;jeW zBp+IMvk5Dr_*z0*O+|hgMfr~WEyQ8JBiZKeTTW784>s65<$F#DwQ%BBXhZT>d0;4H zKrGke>BoxXpt|TILFt?{a5MT8OlBm~!poHK-Q8nvtCID$!VK9wbeJ-R z?AWi$sW1?>_@3b>O!%#2)g@P*BqNFVEJ?9wG+(#F8+DZCRh{@N%0DmP2;sy3QD_xa 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%0D>~L^060CsHx=NV^<)QR_ycX_mn5bPDsp8-9 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�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, - pub routes: IndexMap, - pub trails: IndexMap, -} - -#[derive(Debug, Clone)] -pub(crate) struct RawCategory { - pub guid: Uuid, - pub parent_name: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, -} - -#[derive(Debug, Clone)] -pub(crate) struct Category { - pub guid: Uuid, - pub parent: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, - pub children: IndexMap, -} - -#[derive(Debug, Clone)] -pub struct PackCore { - /* - PackCore is a temporary holder of data - It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. - */ - pub uuid: Uuid, - pub textures: HashMap>, - pub(crate) tbins: HashMap, - pub(crate) categories: IndexMap, - pub all_categories: HashMap, - pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. - pub entities_parents: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: HashMap, -} - - -fn route_to_tbin(route: &Route) -> TBin { - assert!( route.path.len() > 1); - TBin { - map_id: route.map_id, - version: 0, - nodes: route.path.clone(), - } -} - -fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { - let mut props = CommonAttributes::default(); - props.set_texture(None); - props.set_trail_data(Some(file_path.clone())); - Trail { - map_id: route.map_id, - category: route.category.clone(), - parent: route.parent.clone(), - guid: route.guid, - props: props, - dynamic: true, - source_file_name: route.source_file_name.clone(), - } -} - -impl PackCore { - - pub fn new() -> Self { - let mut res = Self { - all_categories: Default::default(), - categories: Default::default(), - entities_parents: Default::default(), - late_discovery_categories: Default::default(), - maps: Default::default(), - source_files: Default::default(), - tbins: Default::default(), - textures: Default::default(), - uuid: Default::default(), - }; - res.uuid = Uuid::new_v4(); - res - } - pub fn partial(all_categories: &HashMap) -> Self { - // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. - let mut res: Self = Self::new(); - res.all_categories = all_categories.clone(); - res - } - - pub fn merge_partial(&mut self, partial_pack: PackCore) { - self.maps.extend(partial_pack.maps); - self.all_categories = partial_pack.all_categories; - self.late_discovery_categories.extend(partial_pack.late_discovery_categories); - self.source_files.extend(partial_pack.source_files); - self.tbins.extend(partial_pack.tbins); - self.entities_parents.extend(partial_pack.entities_parents); - } - pub fn category_exists(&self, full_category_name: &String) -> bool { - self.all_categories.contains_key(full_category_name) - } - - pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { - self.all_categories.get(full_category_name) - } - - pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { - if let Some(category_uuid) = self.all_categories.get(full_category_name) { - category_uuid.clone() - } else { - //TODO: if import is "dirty", create missing category - //TODO: default import mode is "strict" (get inspiration from HTML modes) - debug!("There is no defined category for {}", full_category_name); - - let mut n = 0; - let mut last_uuid: Option = None; - while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { - n += 1; - if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { - //FIXME: might want to make the difference between impacted parents and actual missing category - self.late_discovery_categories.insert(*parent_uuid); - last_uuid = Some(*parent_uuid); - } else { - let new_uuid = Uuid::new_v4(); - debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); - self.all_categories.insert(parent_full_category_name.clone(), new_uuid); - self.late_discovery_categories.insert(new_uuid); - last_uuid = Some(new_uuid); - } - } - trace!("{} uuid: {:?}", full_category_name, last_uuid); - assert!(last_uuid.is_some()); - last_uuid.unwrap() - } - } - - pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ - if let Some(parent_uuid) = self.all_categories.get(full_category_name) { - let mut uuid_to_insert = uuid.clone(); - while self.entities_parents.contains_key(&uuid_to_insert) { - trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); - uuid_to_insert = Uuid::new_v4(); - } - self.entities_parents.insert(uuid_to_insert, *parent_uuid); - Ok(uuid_to_insert) - } else { - //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) - } - } - - pub(crate) fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { - let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; - marker.guid = uuid_to_insert; - if !self.maps.contains_key(&marker.map_id) { - self.maps.insert(marker.map_id, MapData::default()); - } - self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); - Ok(()) - } - - pub(crate) fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { - let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; - trail.guid = uuid_to_insert; - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); - } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); - Ok(()) - } - - pub(crate) fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { - let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); - let tbin_path: RelativePath = file_name.parse().unwrap(); - let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; - route.guid = uuid_to_insert; - let trail = route_to_trail(&route, &tbin_path); - let tbin = route_to_tbin(&route); - - self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); - } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); - self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); - Ok(()) - } - - pub fn register_categories(&mut self) { - let mut entities_parents: HashMap = Default::default(); - let mut all_categories: HashMap = Default::default(); - Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); - self.entities_parents.extend(entities_parents); - self.all_categories = all_categories; - } - fn recursive_register_categories( - entities_parents: &mut HashMap, - categories: &IndexMap, - all_categories: &mut HashMap, - ) { - for (_, cat) in categories.iter() { - debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); - all_categories.insert(cat.full_category_name.clone(), cat.guid); - if let Some(parent) = cat.parent { - entities_parents.insert(cat.guid, parent); - } - Self::recursive_register_categories(entities_parents, &cat.children, all_categories); - } - } -} - - -pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { - let res = s.match_indices(pat) - .nth(n) - .map(|(index, _)| s.split_at(index)) - .map(|(left, _)| left.to_string()); - debug!("prefix_until_nth_char {} {} {:?}", s, n, res); - res -} - -pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { - let nb_matches = s.matches(pat).count(); - assert!(nb_matches + 1 > n); - let res = s.split(pat) - .nth(n) - ; - debug!("nth_chunk {} {} {:?}", s, n, res); - res.unwrap().to_string() -} - -pub fn prefix_parent(s: &str, pat: char) -> Option { - let n = s.matches(pat).count(); - assert!(n > 0); - let res = s.match_indices(pat) - .nth(n - 1) - .map(|(index, _)| s.split_at(index)) - .map(|(left, _)| left.to_string()); - debug!("prefix_parent {} {} {:?}", s, n, res); - res -} - -impl Category { - // Required method - pub fn from(value: &RawCategory, parent: Option) -> Self { - Self { - guid: value.guid.clone(), - props: value.props.clone(), - separator: value.separator, - default_enabled: value.default_enabled, - display_name: value.display_name.clone(), - relative_category_name: value.relative_category_name.clone(), - full_category_name: value.full_category_name.clone(), - parent: parent, - children: Default::default() - } - } - pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { - for (_, cat) in categories { - if &cat.guid == uuid { - return Some(cat); - } - let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); - if sub_res.is_some() { - return sub_res; - } - } - return None; - } - pub fn reassemble( - input_first_pass_categories: &OrderedHashMap, - late_discovered_categories: &mut HashSet, - ) -> IndexMap { - let mut first_pass_categories = input_first_pass_categories.clone(); - let mut second_pass_categories: OrderedHashMap = Default::default(); - let mut need_a_pass: bool = true; - - let mut third_pass_categories: IndexMap = Default::default(); - let mut third_pass_categories_ref: Vec = Default::default(); - let mut root: IndexMap = Default::default(); - while need_a_pass { - need_a_pass = false; - for (key, value) in first_pass_categories.iter() { - debug!("reassemble_categories {:?}", value); - let mut to_insert = value.clone(); - if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { - let mut n = 0; - let mut last_name: Option = None; - // This is an almost duplication of code of pack/mod.rs - while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { - debug!("{} {}", parent_name, n); - if let Some(parent_category) = first_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); - last_name = Some(parent_name.clone()); - } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); - last_name = Some(parent_name.clone()); - }else{ - let new_uuid = Uuid::new_v4(); - let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); - debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); - let to_insert = RawCategory { - default_enabled: value.default_enabled, - guid: new_uuid, - relative_category_name: relative_category_name.clone(), - display_name: relative_category_name.clone(), - parent_name: prefix_until_nth_char(&parent_name, '.', n-1), - props: value.props.clone(), - separator: false, - full_category_name: parent_name.clone() - }; - last_name = Some(to_insert.full_category_name.clone()); - second_pass_categories.insert(parent_name.clone(), to_insert); - late_discovered_categories.insert(new_uuid); - need_a_pass = true; - } - n += 1; - } - late_discovered_categories.insert(value.guid); - to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); - to_insert.display_name = to_insert.relative_category_name.clone(); - debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); - assert!(last_name.is_some()); - to_insert.parent_name = last_name; - } else { - to_insert.parent_name = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.full_category_name.clone()) - } else { - None - } - }else { - None - }; - debug!("insert as is {:?}", to_insert); - } - second_pass_categories.insert(key.clone(), to_insert); - } - if need_a_pass { - std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); - second_pass_categories.clear(); - } - } - for (key, value) in second_pass_categories { - let parent = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.guid.clone()) - } else { - None - } - } else { - None - }; - - debug!("{} parent is {:?}", key , parent); - let cat = Category::from(&value, parent); - let ref_uuid = cat.guid.clone(); - if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { - third_pass_categories_ref.push(ref_uuid); - } - } - - for full_category_name in third_pass_categories_ref { - if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { - if let Some(parent) = cat.parent { - if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { - parent_category.children.insert(cat.guid.clone(), cat); - } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { - parent_category.children.insert(cat.guid.clone(), cat); - } else { - panic!("Could not find parent {} for {:?}", parent, cat); - } - } else { - root.insert(cat.guid.clone(), cat); - } - } else { - panic!("Some bad logic at works"); - } - } - debug!("reassemble_categories {:?}", root); - root - } - - -} - diff --git a/crates/joko_marker_format/src/pack/question.png b/crates/joko_marker_format/src/pack/question.png deleted file mode 100644 index 02851711a0a83c17291f68672e13a7d4cb26f586..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4248 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, - pub reset_position: Vec3, - pub reset_range: f64, - pub map_id: u32, - pub guid: Uuid, - pub name: String, - pub source_file_name: String, -} diff --git a/crates/joko_marker_format/src/pack/trail.png b/crates/joko_marker_format/src/pack/trail.png deleted file mode 100755 index 7529ba0fccf9f596a6515371d695d21ad15ef1ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6896 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 E19PVjH2?qr diff --git a/crates/joko_marker_format/src/pack/trail.rs b/crates/joko_marker_format/src/pack/trail.rs deleted file mode 100644 index 71908f1..0000000 --- a/crates/joko_marker_format/src/pack/trail.rs +++ /dev/null @@ -1,31 +0,0 @@ -use uuid::Uuid; - -use super::CommonAttributes; - -#[derive(Debug, Clone)] -pub(crate) struct Trail { - pub guid: Uuid, - pub parent: Uuid, - pub map_id: u32, - pub category: String, - pub props: CommonAttributes, - pub dynamic: bool, - pub source_file_name: String, -} - -#[derive(Debug, Clone)] -pub(crate) struct TBin { - pub map_id: u32, - pub version: u32, - pub nodes: Vec, -} -#[derive(Debug, Clone)] -pub(crate) struct TBinStatus { - pub tbin: TBin, - pub iso_x: bool, - pub iso_y: bool, - pub iso_z: bool, - pub closed: bool, -} - -impl TBin {} diff --git a/crates/joko_marker_format/src/pack/trail_black.png b/crates/joko_marker_format/src/pack/trail_black.png deleted file mode 100644 index d4326e68a6bdc79ef11a27f2deee592e3dbc4382..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2293 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#QHWK~#8N?VU%6 zWknQ*$DA?eoE_=%sJa1|knwU2G`c6$D4`bAp<*yx2@UBEAsK zlRt`uVrQ|kuqqLQ|A6>Sj2iS|ZWDV6s{%3Y^W-!5Wci~8e3)MZdBTZeDPa{V627O! zOw;}-z7o%iqXjua^{OceWS@V4xKNPrZ6IbVasa#nzLyWg&*BeZWiGaH3^@Dz$y>d- zaEsVQkW5;cYj5$8U>i4TFvkI__+D{@urk)U;tBDC7&VY{;d}9%z${{ksZOQm0H=ww z#J+;HyKAi|FpD-7?~89lRc2AC0l=ESrMOHSC~)&aiQT~Ff*rz#g2eDIVI>p;e!pPv zK5FU?#jRp@VI|ZF0+WaBSzD8*D=?NG75pEnP-#oR1>ziWm{?83cI-$5*=}wpJ{KQ} ze?+k=B>-FL41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@ z;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LY ziI(MnCR2O~3P|_`#rOm{1^{dRd~vB@@I#tBU5UhSBf(6CS%g2~_OdZVw$FgG=JREa znsRf8D&lT}-^H4x$nqZ+JUzLt`7^)Kw`L-z$~JyoMldsIsmBZ;p$y2uy<2Bd|iuD@($uN z@v$JMu~{^vtp!y**3D7tX$_Fe+$k1_?38i_pqYbu{;7h!bgiw}{9d~X@&bAZ-WCK# z7D^QWv~gkd(9A($rPjf3gtvj2#f(Psx0B~sI27$lN)6i$J^s!6$U62zbQ>*~s zxE3dNjuA9c^j+A*6pt{A=rrbddXkwSMgUB8KELnE;(mgV0YK)vvA9CeFfqNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNw zI$slb-`3p3Hjz?tP`PcX=W+n_Tn>Pq%K?y|u!K2k7R4HN0OTj^;8zBp!4)C^e6?CZ z3K0NLiPtCoXpJErAP&ax8C)TZAZpyWT7V5q)yr<+%=$#UDxMYGdPZF!cpeaHV7Rvr zH;_Xf)VxF09HtW;Nc85@f6(Ulq!@$qmTLKWS#TvCByAGuLVV%C62su9U<<)i+V-~a z+Xa1!qyDZK@LhSP>`2`B%~kQUxmSSGJ6r&DoaoI!5P7xf6hYtNp@PCn+rgwcgQHwr z3du+Bs5p*iPI>oz+D=L8uDlZOUMwt6^d!_#-yFz7c6`t@)( zt;AyRN%&&k7EW%_TmWSy6w^LWK7-#?!q*n+^jrW{%{zxn%K=FESo66!nsqt0W5@JQ z>o&)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<` zi>iE~R4&>{knnYB@^l4H{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m z0f4hd!pGA^fhM=XZ`520$sLEytL2KRBIXzXtfw3;zg$q#9jB@nTUgNS47eJWAfSrq zOCiS;88&Nvskeo52^M)v5z1`_I9yyKnv0}zo%W~GeuAwc)x>xBLC(r408STN9(j}~ z`SM7*p~+b|-Ah*ySHG}($x^ul0Ok(2hw@2AUAmY7a4NYc$!5_sA^=RAPd2+>^0odY^5p#!N&1-%eac~x+i%TGqnnke!0F#G(KIiUydE`XB zgQgaiL-GOY2T9DL7y*F2i^InZz#3>xhyoIhC%FN$0OjQ9W>LrhAj)yW@bizC~KvM?`e{ P00000NkvXXu0mjf7Ck~> diff --git a/crates/joko_marker_format/src/pack/trail_rainbow.png b/crates/joko_marker_format/src/pack/trail_rainbow.png deleted file mode 100644 index ea3ff6d305a60bb6a05c6418b6417c832ac14c16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16987 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 diff --git a/crates/joko_marker_format/vendor/rapid/license.txt b/crates/joko_marker_format/vendor/rapid/license.txt deleted file mode 100644 index e5ecd23..0000000 --- a/crates/joko_marker_format/vendor/rapid/license.txt +++ /dev/null @@ -1,52 +0,0 @@ -Use of this software is granted under one of the following two licenses, -to be chosen freely by the user. - -1. Boost Software License - Version 1.0 - August 17th, 2003 -=============================================================================== - -Copyright (c) 2006, 2007 Marcin Kalicinski - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -2. The MIT License -=============================================================================== - -Copyright (c) 2006, 2007 Marcin Kalicinski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/crates/joko_marker_format/vendor/rapid/rapid.cpp b/crates/joko_marker_format/vendor/rapid/rapid.cpp deleted file mode 100644 index 98ea7db..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapid.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "joko_marker_format/vendor/rapid/rapid.hpp" -#include "joko_marker_format/vendor/rapid/rapidxml.hpp" -#include "joko_marker_format/vendor/rapid/rapidxml_print.hpp" -#include "joko_marker_format/src/lib.rs.h" -#include -#include -#include -#include -void remove_duplicate_nodes(rapidxml::xml_node *node) -{ - - std::set duplicates; - rapidxml::xml_attribute *attr = node->first_attribute(); - while (attr) - { - std::string name(attr->name(), attr->name_size()); - if (duplicates.count(name) == 1) - { - rapidxml::xml_attribute *prev = attr; - attr = attr->next_attribute(); - node->remove_attribute(prev); - } - else - { - duplicates.insert(name); - attr = attr->next_attribute(); - } - } - for (rapidxml::xml_node *child = node->first_node(); child; child = child->next_sibling()) - { - remove_duplicate_nodes(child); - } -} - -namespace rapid -{ - - rust::String rapid_filter(rust::String src_xml) - { - // return std::string(src_xml); - std::string src = static_cast(src_xml); - std::string dst; - using namespace rapidxml; - // create document - xml_document doc; - // rapid xml throws exception if there's a parsing error - try - { - // parse the xml text. if there's exceptions we go to catch block from here - doc.parse<0>((char *)src.c_str()); - // delete all the duplicate attributes, so that there's no obvious errors for rust deserializers - for (rapidxml::xml_node *child = doc.first_node(); child; child = child->next_sibling()) - { - remove_duplicate_nodes(child); - } - std::ostringstream oss; - oss << doc; - dst = oss.str(); - } - catch (const parse_error &e) - { - return ""; - } - return dst; - } -} diff --git a/crates/joko_marker_format/vendor/rapid/rapid.hpp b/crates/joko_marker_format/vendor/rapid/rapid.hpp deleted file mode 100644 index 248935a..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapid.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include "joko_marker_format/src/lib.rs.h" -#include "rust/cxx.h" - -namespace rapid { - rust::String rapid_filter(rust::String src_xml); -} \ No newline at end of file diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml.hpp b/crates/joko_marker_format/vendor/rapid/rapidxml.hpp deleted file mode 100644 index d025eef..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapidxml.hpp +++ /dev/null @@ -1,2645 +0,0 @@ -#ifndef RAPIDXML_HPP_INCLUDED -#define RAPIDXML_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/13 01:46:17 $ -//! \file rapidxml.hpp This file contains rapidxml parser and DOM implementation - -// If standard library is disabled, user must provide implementations of required functions and typedefs -#if !defined(RAPIDXML_NO_STDLIB) - #include // For std::size_t - #include // (Optional.) For std::strlen, ... - #include // (Optional.) For std::wcslen, ... - #include // For assert - #include // For placement new -#endif - -// RAPIDXML_NOEXCEPT: Expands to 'noexcept' on supported compilers. -#if !defined(RAPIDXML_NOEXCEPT) -# if !defined(RAPIDXML_DISABLE_NOEXCEPT) -# if defined(__clang__) -# if __has_feature(__cxx_noexcept__) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# elif defined(__GNUC__) -# if ((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4) -# if defined(__GXX_EXPERIMENTAL_CXX0X__) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# endif -# elif defined(_MSC_VER) && (_MSC_VER >= 1900) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# endif -# if !defined(RAPIDXML_NOEXCEPT) -# define RAPIDXML_NOEXCEPT -# endif -#endif - -// On MSVC, disable "conditional expression is constant" warning (level 4). -// This warning is almost impossible to avoid with certain types of templated code -#ifdef _MSC_VER - #pragma warning(push) - #pragma warning(disable:4127) // Conditional expression is constant -#endif - -/////////////////////////////////////////////////////////////////////////// -// RAPIDXML_PARSE_ERROR - -#if defined(RAPIDXML_NO_EXCEPTIONS) - -#define RAPIDXML_PARSE_ERROR(what, where) { parse_error_handler(what, where); assert(0); } - -namespace rapidxml -{ - //! When exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, - //! this function is called to notify user about the error. - //! It must be defined by the user. - //!

- //! This function cannot return. If it does, the results are undefined. - //!

- //! A very simple definition might look like that: - //!

-    //! void %rapidxml::%parse_error_handler(const char *what, void *where)
-    //! {
-    //!     std::cout << "Parse error: " << what << "\n";
-    //!     std::abort();
-    //! }
-    //! 
- //! \param what Human readable description of the error. - //! \param where Pointer to character data where error was detected. - void parse_error_handler(const char *what, void *where); -} - -#else - -#include // For std::exception - -#define RAPIDXML_PARSE_ERROR(what, where) throw parse_error(what, where) - -namespace rapidxml -{ - - //! Parse error exception. - //! This exception is thrown by the parser when an error occurs. - //! Use what() function to get human-readable error message. - //! Use where() function to get a pointer to position within source text where error was detected. - //!

- //! If throwing exceptions by the parser is undesirable, - //! it can be disabled by defining RAPIDXML_NO_EXCEPTIONS macro before rapidxml.hpp is included. - //! This will cause the parser to call rapidxml::parse_error_handler() function instead of throwing an exception. - //! This function must be defined by the user. - //!

- //! This class derives from std::exception class. - class parse_error: public std::exception - { - public: - //! Constructs parse error - parse_error(const char *what, void *where) - : m_what(what) - , m_where(where) - { - } - - //! Gets human readable description of error. - //! \return Pointer to null terminated description of the error. - virtual const char *what() const throw() - { - return m_what; - } - - //! Gets pointer to character data where error happened. - //! Ch should be the same as char type of xml_document that produced the error. - //! \return Pointer to location within the parsed string where error occured. - template - Ch *where() const - { - return reinterpret_cast(m_where); - } - - private: - const char *m_what; - void *m_where; - }; -} - -#endif - -/////////////////////////////////////////////////////////////////////////// -// Pool sizes - -#ifndef RAPIDXML_STATIC_POOL_SIZE - // Size of static memory block of memory_pool. - // Define RAPIDXML_STATIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. - // No dynamic memory allocations are performed by memory_pool until static memory is exhausted. - #define RAPIDXML_STATIC_POOL_SIZE (64 * 1024) -#endif - -#ifndef RAPIDXML_DYNAMIC_POOL_SIZE - // Size of dynamic memory block of memory_pool. - // Define RAPIDXML_DYNAMIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. - // After the static block is exhausted, dynamic blocks with approximately this size are allocated by memory_pool. - #define RAPIDXML_DYNAMIC_POOL_SIZE (64 * 1024) -#endif - -#ifndef RAPIDXML_ALIGNMENT - // Memory allocation alignment. - // Define RAPIDXML_ALIGNMENT before including rapidxml.hpp if you want to override the default value, which is the size of pointer. - // All memory allocations for nodes, attributes and strings will be aligned to this value. - // This must be a power of 2 and at least 1, otherwise memory_pool will not work. - #define RAPIDXML_ALIGNMENT sizeof(void *) -#endif - -namespace rapidxml -{ - // Forward declarations - template class xml_node; - template class xml_attribute; - template class xml_document; - - //! Enumeration listing all node types produced by the parser. - //! Use xml_node::type() function to query node type. - enum node_type - { - node_document, //!< A document node. Name and value are empty. - node_element, //!< An element node. Name contains element name. Value contains text of first data node. - node_data, //!< A data node. Name is empty. Value contains data text. - node_cdata, //!< A CDATA node. Name is empty. Value contains data text. - node_comment, //!< A comment node. Name is empty. Value contains comment text. - node_declaration, //!< A declaration node. Name and value are empty. Declaration parameters (version, encoding and standalone) are in node attributes. - node_doctype, //!< A DOCTYPE node. Name is empty. Value contains DOCTYPE text. - node_pi //!< A PI node. Name contains target. Value contains instructions. - }; - - /////////////////////////////////////////////////////////////////////// - // Parsing flags - - //! Parse flag instructing the parser to not create data nodes. - //! Text of first data node will still be placed in value of parent element, unless rapidxml::parse_no_element_values flag is also specified. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_data_nodes = 0x1; - - //! Parse flag instructing the parser to not use text of first data node as a value of parent element. - //! Can be combined with other flags by use of | operator. - //! Note that child data nodes of element node take precendence over its value when printing. - //! That is, if element has one or more child data nodes and a value, the value will be ignored. - //! Use rapidxml::parse_no_data_nodes flag to prevent creation of data nodes if you want to manipulate data using values of elements. - //!

- //! See xml_document::parse() function. - const int parse_no_element_values = 0x2; - - //! Parse flag instructing the parser to not place zero terminators after strings in the source text. - //! By default zero terminators are placed, modifying source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_string_terminators = 0x4; - - //! Parse flag instructing the parser to not translate entities in the source text. - //! By default entities are translated, modifying source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_entity_translation = 0x8; - - //! Parse flag instructing the parser to disable UTF-8 handling and assume plain 8 bit characters. - //! By default, UTF-8 handling is enabled. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_utf8 = 0x10; - - //! Parse flag instructing the parser to create XML declaration node. - //! By default, declaration node is not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_declaration_node = 0x20; - - //! Parse flag instructing the parser to create comments nodes. - //! By default, comment nodes are not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_comment_nodes = 0x40; - - //! Parse flag instructing the parser to create DOCTYPE node. - //! By default, doctype node is not created. - //! Although W3C specification allows at most one DOCTYPE node, RapidXml will silently accept documents with more than one. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_doctype_node = 0x80; - - //! Parse flag instructing the parser to create PI nodes. - //! By default, PI nodes are not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_pi_nodes = 0x100; - - //! Parse flag instructing the parser to validate closing tag names. - //! If not set, name inside closing tag is irrelevant to the parser. - //! By default, closing tags are not validated. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_validate_closing_tags = 0x200; - - //! Parse flag instructing the parser to trim all leading and trailing whitespace of data nodes. - //! By default, whitespace is not trimmed. - //! This flag does not cause the parser to modify source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_trim_whitespace = 0x400; - - //! Parse flag instructing the parser to condense all whitespace runs of data nodes to a single space character. - //! Trimming of leading and trailing whitespace of data is controlled by rapidxml::parse_trim_whitespace flag. - //! By default, whitespace is not normalized. - //! If this flag is specified, source text will be modified. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_normalize_whitespace = 0x800; - - // Compound flags - - //! Parse flags which represent default behaviour of the parser. - //! This is always equal to 0, so that all other flags can be simply ored together. - //! Normally there is no need to inconveniently disable flags by anding with their negated (~) values. - //! This also means that meaning of each flag is a negation of the default setting. - //! For example, if flag name is rapidxml::parse_no_utf8, it means that utf-8 is enabled by default, - //! and using the flag will disable it. - //!

- //! See xml_document::parse() function. - const int parse_default = 0; - - //! A combination of parse flags that forbids any modifications of the source text. - //! This also results in faster parsing. However, note that the following will occur: - //!
    - //!
  • names and values of nodes will not be zero terminated, you have to use xml_base::name_size() and xml_base::value_size() functions to determine where name and value ends
  • - //!
  • entities will not be translated
  • - //!
  • whitespace will not be normalized
  • - //!
- //! See xml_document::parse() function. - const int parse_non_destructive = parse_no_string_terminators | parse_no_entity_translation; - - //! A combination of parse flags resulting in fastest possible parsing, without sacrificing important data. - //!

- //! See xml_document::parse() function. - const int parse_fastest = parse_non_destructive | parse_no_data_nodes; - - //! A combination of parse flags resulting in largest amount of data being extracted. - //! This usually results in slowest parsing. - //!

- //! See xml_document::parse() function. - const int parse_full = parse_declaration_node | parse_comment_nodes | parse_doctype_node | parse_pi_nodes | parse_validate_closing_tags; - - /////////////////////////////////////////////////////////////////////// - // Internals - - //! \cond internal - namespace internal - { - - // Struct that contains lookup tables for the parser - // It must be a template to allow correct linking (because it has static data members, which are defined in a header file). - template - struct lookup_tables - { - static const unsigned char lookup_whitespace[256]; // Whitespace table - static const unsigned char lookup_node_name[256]; // Node name table - static const unsigned char lookup_text[256]; // Text table - static const unsigned char lookup_text_pure_no_ws[256]; // Text table - static const unsigned char lookup_text_pure_with_ws[256]; // Text table - static const unsigned char lookup_attribute_name[256]; // Attribute name table - static const unsigned char lookup_attribute_data_1[256]; // Attribute data table with single quote - static const unsigned char lookup_attribute_data_1_pure[256]; // Attribute data table with single quote - static const unsigned char lookup_attribute_data_2[256]; // Attribute data table with double quotes - static const unsigned char lookup_attribute_data_2_pure[256]; // Attribute data table with double quotes - static const unsigned char lookup_digits[256]; // Digits - static const unsigned char lookup_upcase[256]; // To uppercase conversion table for ASCII characters - }; - - // Find length of the string - template - inline std::size_t measure(const Ch *p) RAPIDXML_NOEXCEPT - { - const Ch *tmp = p; - while (*tmp) - ++tmp; - return tmp - p; - } - -#if !defined(RAPIDXML_NO_STDLIB) - inline std::size_t measure(const char* p) RAPIDXML_NOEXCEPT - { return std::strlen(p); } - - inline std::size_t measure(const wchar_t* p) RAPIDXML_NOEXCEPT - { return std::wcslen(p); } -#endif - - // Compare strings for equality - template - inline bool compare(const Ch *p1, std::size_t size1, const Ch *p2, - std::size_t size2, bool case_sensitive) RAPIDXML_NOEXCEPT - { - if (size1 != size2) - return false; - if (case_sensitive) - { - for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) - if (*p1 != *p2) - return false; - } - else - { - for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) - if (lookup_tables<0>::lookup_upcase[static_cast(*p1)] != lookup_tables<0>::lookup_upcase[static_cast(*p2)]) - return false; - } - return true; - } - } - //! \endcond - - /////////////////////////////////////////////////////////////////////// - // Memory pool - - //! This class is used by the parser to create new nodes and attributes, without overheads of dynamic memory allocation. - //! In most cases, you will not need to use this class directly. - //! However, if you need to create nodes manually or modify names/values of nodes, - //! you are encouraged to use memory_pool of relevant xml_document to allocate the memory. - //! Not only is this faster than allocating them by using new operator, - //! but also their lifetime will be tied to the lifetime of document, - //! possibly simplyfing memory management. - //!

- //! Call allocate_node() or allocate_attribute() functions to obtain new nodes or attributes from the pool. - //! You can also call allocate_string() function to allocate strings. - //! Such strings can then be used as names or values of nodes without worrying about their lifetime. - //! Note that there is no free() function -- all allocations are freed at once when clear() function is called, - //! or when the pool is destroyed. - //!

- //! It is also possible to create a standalone memory_pool, and use it - //! to allocate nodes, whose lifetime will not be tied to any document. - //!

- //! Pool maintains RAPIDXML_STATIC_POOL_SIZE bytes of statically allocated memory. - //! Until static memory is exhausted, no dynamic memory allocations are done. - //! When static memory is exhausted, pool allocates additional blocks of memory of size RAPIDXML_DYNAMIC_POOL_SIZE each, - //! by using global new[] and delete[] operators. - //! This behaviour can be changed by setting custom allocation routines. - //! Use set_allocator() function to set them. - //!

- //! Allocations for nodes, attributes and strings are aligned at RAPIDXML_ALIGNMENT bytes. - //! This value defaults to the size of pointer on target architecture. - //!

- //! To obtain absolutely top performance from the parser, - //! it is important that all nodes are allocated from a single, contiguous block of memory. - //! Otherwise, cache misses when jumping between two (or more) disjoint blocks of memory can slow down parsing quite considerably. - //! If required, you can tweak RAPIDXML_STATIC_POOL_SIZE, RAPIDXML_DYNAMIC_POOL_SIZE and RAPIDXML_ALIGNMENT - //! to obtain best wasted memory to performance compromise. - //! To do it, define their values before rapidxml.hpp file is included. - //! \param Ch Character type of created nodes. - template - class memory_pool - { - - public: - - //! \cond internal - typedef void *(alloc_func)(std::size_t); // Type of user-defined function used to allocate memory - typedef void (free_func)(void *); // Type of user-defined function used to free memory - //! \endcond - - //! Constructs empty pool with default allocator functions. - memory_pool() RAPIDXML_NOEXCEPT - : m_alloc_func(0) - , m_free_func(0) - { - init(); - } - - //! Destroys pool and frees all the memory. - //! This causes memory occupied by nodes allocated by the pool to be freed. - //! Nodes allocated from the pool are no longer valid. - ~memory_pool() - { - clear(); - } - - //! Allocates a new node from the pool, and optionally assigns name and value to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param type Type of node to create. - //! \param name Name to assign to the node, or 0 to assign no name. - //! \param value Value to assign to the node, or 0 to assign no value. - //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. - //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. - //! \return Pointer to allocated node. This pointer will never be NULL. - xml_node *allocate_node(node_type type, - const Ch *name = 0, const Ch *value = 0, - std::size_t name_size = 0, std::size_t value_size = 0) - { - void *memory = allocate_aligned(sizeof(xml_node)); - xml_node *node = new(memory) xml_node(type); - if (name) - { - if (name_size > 0) - node->name(name, name_size); - else - node->name(name); - } - if (value) - { - if (value_size > 0) - node->value(value, value_size); - else - node->value(value); - } - return node; - } - - //! Allocates a new attribute from the pool, and optionally assigns name and value to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param name Name to assign to the attribute, or 0 to assign no name. - //! \param value Value to assign to the attribute, or 0 to assign no value. - //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. - //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. - //! \return Pointer to allocated attribute. This pointer will never be NULL. - xml_attribute *allocate_attribute(const Ch *name = 0, const Ch *value = 0, - std::size_t name_size = 0, std::size_t value_size = 0) - { - void *memory = allocate_aligned(sizeof(xml_attribute)); - xml_attribute *attribute = new(memory) xml_attribute; - if (name) - { - if (name_size > 0) - attribute->name(name, name_size); - else - attribute->name(name); - } - if (value) - { - if (value_size > 0) - attribute->value(value, value_size); - else - attribute->value(value); - } - return attribute; - } - - //! Allocates a char array of given size from the pool, and optionally copies a given string to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param source String to initialize the allocated memory with, or 0 to not initialize it. - //! \param size Number of characters to allocate, or zero to calculate it automatically from source string length; if size is 0, source string must be specified and null terminated. - //! \return Pointer to allocated char array. This pointer will never be NULL. - Ch *allocate_string(const Ch *source = 0, std::size_t size = 0) - { - assert(source || size); // Either source or size (or both) must be specified - if (size == 0) - size = internal::measure(source) + 1; - Ch *result = static_cast(allocate_aligned(size * sizeof(Ch))); - if (source) - for (std::size_t i = 0; i < size; ++i) - result[i] = source[i]; - return result; - } - - //! Clones an xml_node and its hierarchy of child nodes and attributes. - //! Nodes and attributes are allocated from this memory pool. - //! Names and values are not cloned, they are shared between the clone and the source. - //! Result node can be optionally specified as a second parameter, - //! in which case its contents will be replaced with cloned source node. - //! This is useful when you want to clone entire document. - //! \param source Node to clone. - //! \param result Node to put results in, or 0 to automatically allocate result node - //! \return Pointer to cloned node. This pointer will never be NULL. - xml_node *clone_node(const xml_node *source, xml_node *result = 0) - { - // Prepare result node - if (result) - { - result->remove_all_attributes(); - result->remove_all_nodes(); - result->type(source->type()); - } - else - result = allocate_node(source->type()); - - // Clone name and value - result->name(source->name(), source->name_size()); - result->value(source->value(), source->value_size()); - - // Clone child nodes and attributes - for (xml_node *child = source->first_node(); child; child = child->next_sibling()) - result->append_node(clone_node(child)); - for (xml_attribute *attr = source->first_attribute(); attr; attr = attr->next_attribute()) - result->append_attribute(allocate_attribute(attr->name(), attr->value(), attr->name_size(), attr->value_size())); - - return result; - } - - //! Clears the pool. - //! This causes memory occupied by nodes allocated by the pool to be freed. - //! Any nodes or strings allocated from the pool will no longer be valid. - void clear() RAPIDXML_NOEXCEPT - { - while (m_begin != m_static_memory) - { - char *previous_begin = reinterpret_cast
(align(m_begin))->previous_begin; - if (m_free_func) - m_free_func(m_begin); - else - delete[] m_begin; - m_begin = previous_begin; - } - init(); - } - - //! Sets or resets the user-defined memory allocation functions for the pool. - //! This can only be called when no memory is allocated from the pool yet, otherwise results are undefined. - //! Allocation function must not return invalid pointer on failure. It should either throw, - //! stop the program, or use longjmp() function to pass control to other place of program. - //! If it returns invalid pointer, results are undefined. - //!

- //! User defined allocation functions must have the following forms: - //!
- //!
void *allocate(std::size_t size); - //!
void free(void *pointer); - //!

- //! \param af Allocation function, or 0 to restore default function - //! \param ff Free function, or 0 to restore default function - void set_allocator(alloc_func *af, free_func *ff) - { - assert(m_begin == m_static_memory && m_ptr == align(m_begin)); // Verify that no memory is allocated yet - m_alloc_func = af; - m_free_func = ff; - } - - private: - - struct header - { - char *previous_begin; - }; - - void init() RAPIDXML_NOEXCEPT - { - m_begin = m_static_memory; - m_ptr = align(m_begin); - m_end = m_static_memory + sizeof(m_static_memory); - } - - char *align(char *ptr) const RAPIDXML_NOEXCEPT - { - std::size_t alignment = ((RAPIDXML_ALIGNMENT - (std::size_t(ptr) & (RAPIDXML_ALIGNMENT - 1))) & (RAPIDXML_ALIGNMENT - 1)); - return ptr + alignment; - } - - char *allocate_raw(std::size_t size) - { - // Allocate - void *memory; - if (m_alloc_func) // Allocate memory using either user-specified allocation function or global operator new[] - { - memory = m_alloc_func(size); - assert(memory); // Allocator is not allowed to return 0, on failure it must either throw, stop the program or use longjmp - } - else - { - memory = new char[size]; -#ifdef RAPIDXML_NO_EXCEPTIONS - if (!memory) // If exceptions are disabled, verify memory allocation, because new will not be able to throw bad_alloc - RAPIDXML_PARSE_ERROR("out of memory", 0); -#endif - } - return static_cast(memory); - } - - void *allocate_aligned(std::size_t size) - { - // Calculate aligned pointer - char *result = align(m_ptr); - - // If not enough memory left in current pool, allocate a new pool - if (result + size > m_end) - { - // Calculate required pool size (may be bigger than RAPIDXML_DYNAMIC_POOL_SIZE) - std::size_t pool_size = RAPIDXML_DYNAMIC_POOL_SIZE; - if (pool_size < size) - pool_size = size; - - // Allocate - std::size_t alloc_size = sizeof(header) + (2 * RAPIDXML_ALIGNMENT - 2) + pool_size; // 2 alignments required in worst case: one for header, one for actual allocation - char *raw_memory = allocate_raw(alloc_size); - - // Setup new pool in allocated memory - char *pool = align(raw_memory); - header *new_header = reinterpret_cast
(pool); - new_header->previous_begin = m_begin; - m_begin = raw_memory; - m_ptr = pool + sizeof(header); - m_end = raw_memory + alloc_size; - - // Calculate aligned pointer again using new pool - result = align(m_ptr); - } - - // Update pool and return aligned pointer - m_ptr = result + size; - return result; - } - - char *m_begin; // Start of raw memory making up current pool - char *m_ptr; // First free byte in current pool - char *m_end; // One past last available byte in current pool - char m_static_memory[RAPIDXML_STATIC_POOL_SIZE]; // Static raw memory - alloc_func *m_alloc_func; // Allocator function, or 0 if default is to be used - free_func *m_free_func; // Free function, or 0 if default is to be used - }; - - /////////////////////////////////////////////////////////////////////////// - // XML base - - //! Base class for xml_node and xml_attribute implementing common functions: - //! name(), name_size(), value(), value_size() and parent(). - //! \param Ch Character type to use - template - class xml_base - { - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - // Construct a base with empty name, value and parent - xml_base() RAPIDXML_NOEXCEPT - : m_name(0) - , m_value(0) - , m_parent(0) - , m_offset(0) - { - } - - /////////////////////////////////////////////////////////////////////////// - // Node data access - - //! Gets name of the node. - //! Interpretation of name depends on type of node. - //! Note that name will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. - //!

- //! Use name_size() function to determine length of the name. - //! \return Name of node, or empty string if node has no name. - Ch *name() const RAPIDXML_NOEXCEPT - { - return m_name ? m_name : nullstr(); - } - - //! Gets size of node name, not including terminator character. - //! This function works correctly irrespective of whether name is or is not zero terminated. - //! \return Size of node name, in characters. - std::size_t name_size() const RAPIDXML_NOEXCEPT - { - return m_name ? m_name_size : 0; - } - - //! Gets value of node. - //! Interpretation of value depends on type of node. - //! Note that value will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. - //!

- //! Use value_size() function to determine length of the value. - //! \return Value of node, or empty string if node has no value. - Ch *value() const RAPIDXML_NOEXCEPT - { - return m_value ? m_value : nullstr(); - } - - //! Gets size of node value, not including terminator character. - //! This function works correctly irrespective of whether value is or is not zero terminated. - //! \return Size of node value, in characters. - std::size_t value_size() const RAPIDXML_NOEXCEPT - { - return m_value ? m_value_size : 0; - } - - //! Get the start offset of this node inside the source string. - Ch *offset() const RAPIDXML_NOEXCEPT - { - return m_offset; - } - - /////////////////////////////////////////////////////////////////////////// - // Node modification - - //! Sets name of node to a non zero-terminated string. - //! See \ref ownership_of_strings. - //!

- //! Note that node does not own its name or value, it only stores a pointer to it. - //! It will not delete or otherwise free the pointer on destruction. - //! It is reponsibility of the user to properly manage lifetime of the string. - //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - - //! on destruction of the document the string will be automatically freed. - //!

- //! Size of name must be specified separately, because name does not have to be zero terminated. - //! Use name(const Ch *) function to have the length automatically calculated (string must be zero terminated). - //! \param name Name of node to set. Does not have to be zero terminated. - //! \param size Size of name, in characters. This does not include zero terminator, if one is present. - void name(const Ch *name, std::size_t size) RAPIDXML_NOEXCEPT - { - m_name = const_cast(name); - m_name_size = size; - } - - //! Sets name of node to a zero-terminated string. - //! See also \ref ownership_of_strings and xml_node::name(const Ch *, std::size_t). - //! \param name Name of node to set. Must be zero terminated. - void name(const Ch *name) RAPIDXML_NOEXCEPT - { - this->name(name, internal::measure(name)); - } - - //! Sets value of node to a non zero-terminated string. - //! See \ref ownership_of_strings. - //!

- //! Note that node does not own its name or value, it only stores a pointer to it. - //! It will not delete or otherwise free the pointer on destruction. - //! It is reponsibility of the user to properly manage lifetime of the string. - //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - - //! on destruction of the document the string will be automatically freed. - //!

- //! Size of value must be specified separately, because it does not have to be zero terminated. - //! Use value(const Ch *) function to have the length automatically calculated (string must be zero terminated). - //!

- //! If an element has a child node of type node_data, it will take precedence over element value when printing. - //! If you want to manipulate data of elements using values, use parser flag rapidxml::parse_no_data_nodes to prevent creation of data nodes by the parser. - //! \param value value of node to set. Does not have to be zero terminated. - //! \param size Size of value, in characters. This does not include zero terminator, if one is present. - void value(const Ch *value, std::size_t size) RAPIDXML_NOEXCEPT - { - m_value = const_cast(value); - m_value_size = size; - } - - //! Sets value of node to a zero-terminated string. - //! See also \ref ownership_of_strings and xml_node::value(const Ch *, std::size_t). - //! \param value Vame of node to set. Must be zero terminated. - void value(const Ch *value) RAPIDXML_NOEXCEPT - { - this->value(value, internal::measure(value)); - } - - //! Sets the offset inside the source string. - //! This is only intended for debugging purposes. - void offset(Ch *offset) RAPIDXML_NOEXCEPT - { - m_offset = offset; - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets node parent. - //! \return Pointer to parent node, or 0 if there is no parent. - xml_node *parent() const RAPIDXML_NOEXCEPT - { - return m_parent; - } - - protected: - - // Return empty string - static Ch *nullstr() RAPIDXML_NOEXCEPT - { - static Ch zero = Ch('\0'); - return &zero; - } - - Ch *m_name; // Name of node, or 0 if no name - Ch *m_value; // Value of node, or 0 if no value - std::size_t m_name_size; // Length of node name, or undefined of no name - std::size_t m_value_size; // Length of node value, or undefined if no value - xml_node *m_parent; // Pointer to parent node, or 0 if none - Ch *m_offset; // Start offset of this node inside the string - }; - - //! Class representing attribute node of XML document. - //! Each attribute has name and value strings, which are available through name() and value() functions (inherited from xml_base). - //! Note that after parse, both name and value of attribute will point to interior of source text used for parsing. - //! Thus, this text must persist in memory for the lifetime of attribute. - //! \param Ch Character type to use. - template - class xml_attribute: public xml_base - { - - friend class xml_node; - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - //! Constructs an empty attribute with the specified type. - //! Consider using memory_pool of appropriate xml_document if allocating attributes manually. - xml_attribute() - { - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets document of which attribute is a child. - //! \return Pointer to document that contains this attribute, or 0 if there is no parent document. - xml_document *document() const - { - if (xml_node *node = this->parent()) - { - while (node->parent()) - node = node->parent(); - return node->type() == node_document ? static_cast *>(node) : 0; - } - else - return 0; - } - - //! Gets previous attribute, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return previous attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *previous_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_prev_attribute; attribute; attribute = attribute->m_prev_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return this->m_parent ? m_prev_attribute : 0; - } - - //! Gets next attribute, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return next attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *next_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_next_attribute; attribute; attribute = attribute->m_next_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return this->m_parent ? m_next_attribute : 0; - } - - private: - - xml_attribute *m_prev_attribute; // Pointer to previous sibling of attribute, or 0 if none; only valid if parent is non-zero - xml_attribute *m_next_attribute; // Pointer to next sibling of attribute, or 0 if none; only valid if parent is non-zero - - }; - - /////////////////////////////////////////////////////////////////////////// - // XML node - - //! Class representing a node of XML document. - //! Each node may have associated name and value strings, which are available through name() and value() functions. - //! Interpretation of name and value depends on type of the node. - //! Type of node can be determined by using type() function. - //!

- //! Note that after parse, both name and value of node, if any, will point interior of source text used for parsing. - //! Thus, this text must persist in the memory for the lifetime of node. - //! \param Ch Character type to use. - template - class xml_node: public xml_base - { - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - //! Constructs an empty node with the specified type. - //! Consider using memory_pool of appropriate document to allocate nodes manually. - //! \param type Type of node to construct. - xml_node(node_type type) - : m_type(type) - , m_first_node(0) - , m_first_attribute(0) - { - } - - /////////////////////////////////////////////////////////////////////////// - // Node data access - - //! Gets type of node. - //! \return Type of node. - node_type type() const - { - return m_type; - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets document of which node is a child. - //! \return Pointer to document that contains this node, or 0 if there is no parent document. - xml_document *document() const - { - xml_node *node = const_cast *>(this); - while (node->parent()) - node = node->parent(); - return node->type() == node_document ? static_cast *>(node) : 0; - } - - //! Gets first child node, optionally matching node name. - //! \param name Name of child to find, or 0 to return first child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found child, or 0 if not found. - xml_node *first_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *child = m_first_node; child; child = child->next_sibling()) - if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) - return child; - return 0; - } - else - return m_first_node; - } - - //! Gets last child node, optionally matching node name. - //! Behaviour is undefined if node has no children. - //! Use first_node() to test if node has children. - //! \param name Name of child to find, or 0 to return last child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found child, or 0 if not found. - xml_node *last_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(m_first_node); // Cannot query for last child if node has no children - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *child = m_last_node; child; child = child->previous_sibling()) - if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) - return child; - return 0; - } - else - return m_last_node; - } - - //! Gets previous sibling node, optionally matching node name. - //! Behaviour is undefined if node has no parent. - //! Use parent() to test if node has a parent. - //! \param name Name of sibling to find, or 0 to return previous sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found sibling, or 0 if not found. - xml_node *previous_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(this->m_parent); // Cannot query for siblings if node has no parent - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *sibling = m_prev_sibling; sibling; sibling = sibling->m_prev_sibling) - if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) - return sibling; - return 0; - } - else - return m_prev_sibling; - } - - //! Gets next sibling node, optionally matching node name. - //! Behaviour is undefined if node has no parent. - //! Use parent() to test if node has a parent. - //! \param name Name of sibling to find, or 0 to return next sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found sibling, or 0 if not found. - xml_node *next_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(this->m_parent); // Cannot query for siblings if node has no parent - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *sibling = m_next_sibling; sibling; sibling = sibling->m_next_sibling) - if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) - return sibling; - return 0; - } - else - return m_next_sibling; - } - - //! Gets first attribute of node, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return first attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *first_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_first_attribute; attribute; attribute = attribute->m_next_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return m_first_attribute; - } - - //! Gets last attribute of node, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return last attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *last_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_last_attribute; attribute; attribute = attribute->m_prev_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return m_first_attribute ? m_last_attribute : 0; - } - - /////////////////////////////////////////////////////////////////////////// - // Node modification - - //! Sets type of node. - //! \param type Type of node to set. - void type(node_type type) - { - m_type = type; - } - - /////////////////////////////////////////////////////////////////////////// - // Node manipulation - - //! Prepends a new child node. - //! The prepended child becomes the first child, and all existing children are moved one position back. - //! \param child Node to prepend. - void prepend_node(xml_node *child) - { - assert(child && !child->parent() && child->type() != node_document); - if (first_node()) - { - child->m_next_sibling = m_first_node; - m_first_node->m_prev_sibling = child; - } - else - { - child->m_next_sibling = 0; - m_last_node = child; - } - m_first_node = child; - child->m_parent = this; - child->m_prev_sibling = 0; - } - - //! Appends a new child node. - //! The appended child becomes the last child. - //! \param child Node to append. - void append_node(xml_node *child) - { - assert(child && !child->parent() && child->type() != node_document); - if (first_node()) - { - child->m_prev_sibling = m_last_node; - m_last_node->m_next_sibling = child; - } - else - { - child->m_prev_sibling = 0; - m_first_node = child; - } - m_last_node = child; - child->m_parent = this; - child->m_next_sibling = 0; - } - - //! Inserts a new child node at specified place inside the node. - //! All children after and including the specified node are moved one position back. - //! \param where Place where to insert the child, or 0 to insert at the back. - //! \param child Node to insert. - void insert_node(xml_node *where, xml_node *child) - { - assert(!where || where->parent() == this); - assert(child && !child->parent() && child->type() != node_document); - if (where == m_first_node) - prepend_node(child); - else if (where == 0) - append_node(child); - else - { - child->m_prev_sibling = where->m_prev_sibling; - child->m_next_sibling = where; - where->m_prev_sibling->m_next_sibling = child; - where->m_prev_sibling = child; - child->m_parent = this; - } - } - - //! Removes first child node. - //! If node has no children, behaviour is undefined. - //! Use first_node() to test if node has children. - void remove_first_node() - { - assert(first_node()); - xml_node *child = m_first_node; - m_first_node = child->m_next_sibling; - if (child->m_next_sibling) - child->m_next_sibling->m_prev_sibling = 0; - else - m_last_node = 0; - child->m_parent = 0; - } - - //! Removes last child of the node. - //! If node has no children, behaviour is undefined. - //! Use first_node() to test if node has children. - void remove_last_node() - { - assert(first_node()); - xml_node *child = m_last_node; - if (child->m_prev_sibling) - { - m_last_node = child->m_prev_sibling; - child->m_prev_sibling->m_next_sibling = 0; - } - else - m_first_node = 0; - child->m_parent = 0; - } - - //! Removes specified child from the node - // \param where Pointer to child to be removed. - void remove_node(xml_node *where) - { - assert(where && where->parent() == this); - assert(first_node()); - if (where == m_first_node) - remove_first_node(); - else if (where == m_last_node) - remove_last_node(); - else - { - where->m_prev_sibling->m_next_sibling = where->m_next_sibling; - where->m_next_sibling->m_prev_sibling = where->m_prev_sibling; - where->m_parent = 0; - } - } - - //! Removes all child nodes (but not attributes). - void remove_all_nodes() - { - for (xml_node *node = first_node(); node; node = node->m_next_sibling) - node->m_parent = 0; - m_first_node = 0; - } - - //! Prepends a new attribute to the node. - //! \param attribute Attribute to prepend. - void prepend_attribute(xml_attribute *attribute) - { - assert(attribute && !attribute->parent()); - if (first_attribute()) - { - attribute->m_next_attribute = m_first_attribute; - m_first_attribute->m_prev_attribute = attribute; - } - else - { - attribute->m_next_attribute = 0; - m_last_attribute = attribute; - } - m_first_attribute = attribute; - attribute->m_parent = this; - attribute->m_prev_attribute = 0; - } - - //! Appends a new attribute to the node. - //! \param attribute Attribute to append. - void append_attribute(xml_attribute *attribute) - { - assert(attribute && !attribute->parent()); - if (first_attribute()) - { - attribute->m_prev_attribute = m_last_attribute; - m_last_attribute->m_next_attribute = attribute; - } - else - { - attribute->m_prev_attribute = 0; - m_first_attribute = attribute; - } - m_last_attribute = attribute; - attribute->m_parent = this; - attribute->m_next_attribute = 0; - } - - //! Inserts a new attribute at specified place inside the node. - //! All attributes after and including the specified attribute are moved one position back. - //! \param where Place where to insert the attribute, or 0 to insert at the back. - //! \param attribute Attribute to insert. - void insert_attribute(xml_attribute *where, xml_attribute *attribute) - { - assert(!where || where->parent() == this); - assert(attribute && !attribute->parent()); - if (where == m_first_attribute) - prepend_attribute(attribute); - else if (where == 0) - append_attribute(attribute); - else - { - attribute->m_prev_attribute = where->m_prev_attribute; - attribute->m_next_attribute = where; - where->m_prev_attribute->m_next_attribute = attribute; - where->m_prev_attribute = attribute; - attribute->m_parent = this; - } - } - - //! Removes first attribute of the node. - //! If node has no attributes, behaviour is undefined. - //! Use first_attribute() to test if node has attributes. - void remove_first_attribute() - { - assert(first_attribute()); - xml_attribute *attribute = m_first_attribute; - if (attribute->m_next_attribute) - { - attribute->m_next_attribute->m_prev_attribute = 0; - } - else - m_last_attribute = 0; - attribute->m_parent = 0; - m_first_attribute = attribute->m_next_attribute; - } - - //! Removes last attribute of the node. - //! If node has no attributes, behaviour is undefined. - //! Use first_attribute() to test if node has attributes. - void remove_last_attribute() - { - assert(first_attribute()); - xml_attribute *attribute = m_last_attribute; - if (attribute->m_prev_attribute) - { - attribute->m_prev_attribute->m_next_attribute = 0; - m_last_attribute = attribute->m_prev_attribute; - } - else - m_first_attribute = 0; - attribute->m_parent = 0; - } - - //! Removes specified attribute from node. - //! \param where Pointer to attribute to be removed. - void remove_attribute(xml_attribute *where) - { - assert(first_attribute() && where->parent() == this); - if (where == m_first_attribute) - remove_first_attribute(); - else if (where == m_last_attribute) - remove_last_attribute(); - else - { - where->m_prev_attribute->m_next_attribute = where->m_next_attribute; - where->m_next_attribute->m_prev_attribute = where->m_prev_attribute; - where->m_parent = 0; - } - } - - //! Removes all attributes of node. - void remove_all_attributes() - { - for (xml_attribute *attribute = first_attribute(); attribute; attribute = attribute->m_next_attribute) - attribute->m_parent = 0; - m_first_attribute = 0; - } - - private: - - /////////////////////////////////////////////////////////////////////////// - // Restrictions - - // No copying - xml_node(const xml_node &); - void operator =(const xml_node &); - - /////////////////////////////////////////////////////////////////////////// - // Data members - - // Note that some of the pointers below have UNDEFINED values if certain other pointers are 0. - // This is required for maximum performance, as it allows the parser to omit initialization of - // unneded/redundant values. - // - // The rules are as follows: - // 1. first_node and first_attribute contain valid pointers, or 0 if node has no children/attributes respectively - // 2. last_node and last_attribute are valid only if node has at least one child/attribute respectively, otherwise they contain garbage - // 3. prev_sibling and next_sibling are valid only if node has a parent, otherwise they contain garbage - - node_type m_type; // Type of node; always valid - xml_node *m_first_node; // Pointer to first child node, or 0 if none; always valid - xml_node *m_last_node; // Pointer to last child node, or 0 if none; this value is only valid if m_first_node is non-zero - xml_attribute *m_first_attribute; // Pointer to first attribute of node, or 0 if none; always valid - xml_attribute *m_last_attribute; // Pointer to last attribute of node, or 0 if none; this value is only valid if m_first_attribute is non-zero - xml_node *m_prev_sibling; // Pointer to previous sibling of node, or 0 if none; this value is only valid if m_parent is non-zero - xml_node *m_next_sibling; // Pointer to next sibling of node, or 0 if none; this value is only valid if m_parent is non-zero - - }; - - /////////////////////////////////////////////////////////////////////////// - // XML document - - //! This class represents root of the DOM hierarchy. - //! It is also an xml_node and a memory_pool through public inheritance. - //! Use parse() function to build a DOM tree from a zero-terminated XML text string. - //! parse() function allocates memory for nodes and attributes by using functions of xml_document, - //! which are inherited from memory_pool. - //! To access root node of the document, use the document itself, as if it was an xml_node. - //! \param Ch Character type to use. - template - class xml_document: public xml_node, public memory_pool - { - - public: - - //! Constructs empty XML document - xml_document() - : xml_node(node_document) - { - } - - //! Parses zero-terminated XML string according to given flags. - //! Passed string will be modified by the parser, unless rapidxml::parse_non_destructive flag is used. - //! The string must persist for the lifetime of the document. - //! In case of error, rapidxml::parse_error exception will be thrown. - //!

- //! If you want to parse contents of a file, you must first load the file into the memory, and pass pointer to its beginning. - //! Make sure that data is zero-terminated. - //!

- //! Document can be parsed into multiple times. - //! Each new call to parse removes previous nodes and attributes (if any), but does not clear memory pool. - //! \param text XML data to parse; pointer is non-const to denote fact that this data may be modified by the parser. - template - void parse(Ch *text) - { - assert(text); - - // Remove current contents - this->remove_all_nodes(); - this->remove_all_attributes(); - - // Parse BOM, if any - parse_bom(text); - - // Parse children - while (1) - { - // Skip whitespace before node - skip(text); - if (*text == 0) - break; - - // Parse and append new child - if (*text == Ch('<')) - { - ++text; // Skip '<' - if (xml_node *node = parse_node(text)) - this->append_node(node); - } - else - RAPIDXML_PARSE_ERROR("expected <", text); - } - - } - - //! Clears the document by deleting all nodes and clearing the memory pool. - //! All nodes owned by document pool are destroyed. - void clear() - { - this->remove_all_nodes(); - this->remove_all_attributes(); - memory_pool::clear(); - } - - private: - - /////////////////////////////////////////////////////////////////////// - // Internal character utility functions - - // Detect whitespace character - struct whitespace_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_whitespace[static_cast(ch)]; - } - }; - - // Detect node name character - struct node_name_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_node_name[static_cast(ch)]; - } - }; - - // Detect attribute name character - struct attribute_name_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_attribute_name[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) - struct text_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) that does not require processing - struct text_pure_no_ws_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text_pure_no_ws[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) that does not require processing - struct text_pure_with_ws_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text_pure_with_ws[static_cast(ch)]; - } - }; - - // Detect attribute value character - template - struct attribute_value_pred - { - static unsigned char test(Ch ch) - { - if (Quote == Ch('\'')) - return internal::lookup_tables<0>::lookup_attribute_data_1[static_cast(ch)]; - if (Quote == Ch('\"')) - return internal::lookup_tables<0>::lookup_attribute_data_2[static_cast(ch)]; - return 0; // Should never be executed, to avoid warnings on Comeau - } - }; - - // Detect attribute value character - template - struct attribute_value_pure_pred - { - static unsigned char test(Ch ch) - { - if (Quote == Ch('\'')) - return internal::lookup_tables<0>::lookup_attribute_data_1_pure[static_cast(ch)]; - if (Quote == Ch('\"')) - return internal::lookup_tables<0>::lookup_attribute_data_2_pure[static_cast(ch)]; - return 0; // Should never be executed, to avoid warnings on Comeau - } - }; - - // Insert coded character, using UTF8 or 8-bit ASCII - template - static void insert_coded_character(Ch *&text, unsigned long code) - { - if (Flags & parse_no_utf8) - { - // Insert 8-bit ASCII character - // Todo: possibly verify that code is less than 256 and use replacement char otherwise? - text[0] = static_cast(code); - text += 1; - } - else - { - // Insert UTF8 sequence - if (code < 0x80) // 1 byte sequence - { - text[0] = static_cast(code); - text += 1; - } - else if (code < 0x800) // 2 byte sequence - { - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xC0); - text += 2; - } - else if (code < 0x10000) // 3 byte sequence - { - text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xE0); - text += 3; - } - else if (code < 0x110000) // 4 byte sequence - { - text[3] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xF0); - text += 4; - } - else // Invalid, only codes up to 0x10FFFF are allowed in Unicode - { - RAPIDXML_PARSE_ERROR("invalid numeric character entity", text); - } - } - } - - // Skip characters until predicate evaluates to true - template - static void skip(Ch *&text) - { - Ch *tmp = text; - while (StopPred::test(*tmp)) - ++tmp; - text = tmp; - } - - // Skip characters until predicate evaluates to true while doing the following: - // - replacing XML character entity references with proper characters (' & " < > &#...;) - // - condensing whitespace sequences to single space character - template - static Ch *skip_and_expand_character_refs(Ch *&text) - { - // If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip - if (Flags & parse_no_entity_translation && - !(Flags & parse_normalize_whitespace) && - !(Flags & parse_trim_whitespace)) - { - skip(text); - return text; - } - - // Use simple skip until first modification is detected - skip(text); - - // Use translation skip - Ch *src = text; - Ch *dest = src; - while (StopPred::test(*src)) - { - // If entity translation is enabled - if (!(Flags & parse_no_entity_translation)) - { - // Test if replacement is needed - if (src[0] == Ch('&')) - { - switch (src[1]) - { - - // & ' - case Ch('a'): - if (src[2] == Ch('m') && src[3] == Ch('p') && src[4] == Ch(';')) - { - *dest = Ch('&'); - ++dest; - src += 5; - continue; - } - if (src[2] == Ch('p') && src[3] == Ch('o') && src[4] == Ch('s') && src[5] == Ch(';')) - { - *dest = Ch('\''); - ++dest; - src += 6; - continue; - } - break; - - // " - case Ch('q'): - if (src[2] == Ch('u') && src[3] == Ch('o') && src[4] == Ch('t') && src[5] == Ch(';')) - { - *dest = Ch('"'); - ++dest; - src += 6; - continue; - } - break; - - // > - case Ch('g'): - if (src[2] == Ch('t') && src[3] == Ch(';')) - { - *dest = Ch('>'); - ++dest; - src += 4; - continue; - } - break; - - // < - case Ch('l'): - if (src[2] == Ch('t') && src[3] == Ch(';')) - { - *dest = Ch('<'); - ++dest; - src += 4; - continue; - } - break; - - // &#...; - assumes ASCII - case Ch('#'): - if (src[2] == Ch('x')) - { - unsigned long code = 0; - src += 3; // Skip &#x - while (1) - { - unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; - if (digit == 0xFF) - break; - code = code * 16 + digit; - ++src; - } - insert_coded_character(dest, code); // Put character in output - } - else - { - unsigned long code = 0; - src += 2; // Skip &# - while (1) - { - unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; - if (digit == 0xFF) - break; - code = code * 10 + digit; - ++src; - } - insert_coded_character(dest, code); // Put character in output - } - if (*src == Ch(';')) - ++src; - else - RAPIDXML_PARSE_ERROR("expected ;", src); - continue; - - // Something else - default: - // Ignore, just copy '&' verbatim - break; - - } - } - } - - // If whitespace condensing is enabled - if (Flags & parse_normalize_whitespace) - { - // Test if condensing is needed - if (whitespace_pred::test(*src)) - { - *dest = Ch(' '); ++dest; // Put single space in dest - ++src; // Skip first whitespace char - // Skip remaining whitespace chars - while (whitespace_pred::test(*src)) - ++src; - continue; - } - } - - // No replacement, only copy character - *dest++ = *src++; - - } - - // Return new end - text = src; - return dest; - - } - - /////////////////////////////////////////////////////////////////////// - // Internal parsing functions - - // Parse BOM, if any - template - void parse_bom(Ch *&text) - { - // UTF-8? - if (static_cast(text[0]) == 0xEF && - static_cast(text[1]) == 0xBB && - static_cast(text[2]) == 0xBF) - { - text += 3; // Skup utf-8 bom - } - } - - // Parse XML declaration ( - xml_node *parse_xml_declaration(Ch *&text) - { - // If parsing of declaration is disabled - if (!(Flags & parse_declaration_node)) - { - // Skip until end of declaration - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 2; // Skip '?>' - return 0; - } - - // Create declaration - xml_node *declaration = this->allocate_node(node_declaration); - declaration->offset(text); - - // Skip whitespace before attributes or ?> - skip(text); - - // Parse declaration attributes - parse_node_attributes(text, declaration); - - // Skip ?> - if (text[0] != Ch('?') || text[1] != Ch('>')) - RAPIDXML_PARSE_ERROR("expected ?>", text); - text += 2; - - return declaration; - } - - // Parse XML comment (' - return 0; // Do not produce comment node - } - - // Remember value start - Ch *value = text; - - // Skip until end of comment - while (text[0] != Ch('-') || text[1] != Ch('-') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Create comment node - xml_node *comment = this->allocate_node(node_comment); - comment->offset(value); - comment->value(value, text - value); - - // Place zero terminator after comment value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 3; // Skip '-->' - return comment; - } - - // Parse DOCTYPE - template - xml_node *parse_doctype(Ch *&text) - { - // Remember value start - Ch *value = text; - - // Skip to > - while (*text != Ch('>')) - { - // Determine character type - switch (*text) - { - - // If '[' encountered, scan for matching ending ']' using naive algorithm with depth - // This works for all W3C test files except for 2 most wicked - case Ch('['): - { - ++text; // Skip '[' - int depth = 1; - while (depth > 0) - { - switch (*text) - { - case Ch('['): ++depth; break; - case Ch(']'): --depth; break; - case 0: RAPIDXML_PARSE_ERROR("unexpected end of data", text); - } - ++text; - } - break; - } - - // Error on end of text - case Ch('\0'): - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - - // Other character, skip it - default: - ++text; - - } - } - - // If DOCTYPE nodes enabled - if (Flags & parse_doctype_node) - { - // Create a new doctype node - xml_node *doctype = this->allocate_node(node_doctype); - doctype->offset(value); - doctype->value(value, text - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 1; // skip '>' - return doctype; - } - else - { - text += 1; // skip '>' - return 0; - } - - } - - // Parse PI - template - xml_node *parse_pi(Ch *&text) - { - // If creation of PI nodes is enabled - if (Flags & parse_pi_nodes) - { - // Create pi node - xml_node *pi = this->allocate_node(node_pi); - pi->offset(text); - - // Extract PI target name - Ch *name = text; - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected PI target", text); - pi->name(name, text - name); - - // Skip whitespace between pi target and pi - skip(text); - - // Remember start of pi - Ch *value = text; - - // Skip to '?>' - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (*text == Ch('\0')) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Set pi value (verbatim, no entity expansion or whitespace normalization) - pi->value(value, text - value); - - // Place zero terminator after name and value - if (!(Flags & parse_no_string_terminators)) - { - pi->name()[pi->name_size()] = Ch('\0'); - pi->value()[pi->value_size()] = Ch('\0'); - } - - text += 2; // Skip '?>' - return pi; - } - else - { - // Skip to '?>' - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (*text == Ch('\0')) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 2; // Skip '?>' - return 0; - } - } - - // Parse and append data - // Return character that ends data. - // This is necessary because this character might have been overwritten by a terminating 0 - template - Ch parse_and_append_data(xml_node *node, Ch *&text, Ch *contents_start) - { - // Backup to contents start if whitespace trimming is disabled - if (!(Flags & parse_trim_whitespace)) - text = contents_start; - - // Skip until end of data - Ch *value = text, *end; - if (Flags & parse_normalize_whitespace) - end = skip_and_expand_character_refs(text); - else - end = skip_and_expand_character_refs(text); - - // Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after > - if (Flags & parse_trim_whitespace) - { - if (Flags & parse_normalize_whitespace) - { - // Whitespace is already condensed to single space characters by skipping function, so just trim 1 char off the end - if (*(end - 1) == Ch(' ')) - --end; - } - else - { - // Backup until non-whitespace character is found - while (whitespace_pred::test(*(end - 1))) - --end; - } - } - - // If characters are still left between end and value (this test is only necessary if normalization is enabled) - // Create new data node - if (!(Flags & parse_no_data_nodes)) - { - xml_node *data = this->allocate_node(node_data); - data->value(value, end - value); - node->append_node(data); - } - - // Add data to parent node if no data exists yet - if (!(Flags & parse_no_element_values)) - if (*node->value() == Ch('\0')) - node->value(value, end - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - { - Ch ch = *text; - *end = Ch('\0'); - return ch; // Return character that ends data; this is required because zero terminator overwritten it - } - - // Return character that ends data - return *text; - } - - // Parse CDATA - template - xml_node *parse_cdata(Ch *&text) - { - // If CDATA is disabled - if (Flags & parse_no_data_nodes) - { - // Skip until end of cdata - while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 3; // Skip ]]> - return 0; // Do not produce CDATA node - } - - // Skip until end of cdata - Ch *value = text; - while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Create new cdata node - xml_node *cdata = this->allocate_node(node_cdata); - cdata->offset(value); - cdata->value(value, text - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 3; // Skip ]]> - return cdata; - } - - // Parse element node - template - xml_node *parse_element(Ch *&text) - { - // Create element node - xml_node *element = this->allocate_node(node_element); - element->offset(text); - - // Extract element name - Ch *name = text; - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected element name", text); - element->name(name, text - name); - - // Skip whitespace between element name and attributes or > - skip(text); - - // Parse attributes, if any - parse_node_attributes(text, element); - - // Determine ending type - if (*text == Ch('>')) - { - ++text; - parse_node_contents(text, element); - } - else if (*text == Ch('/')) - { - ++text; - if (*text != Ch('>')) - RAPIDXML_PARSE_ERROR("expected >", text); - ++text; - } - else - RAPIDXML_PARSE_ERROR("expected >", text); - - // Place zero terminator after name - if (!(Flags & parse_no_string_terminators)) - element->name()[element->name_size()] = Ch('\0'); - - // Return parsed element - return element; - } - - // Determine node type, and parse it - template - xml_node *parse_node(Ch *&text) - { - // Parse proper node type - switch (text[0]) - { - - // <... - default: - // Parse and append element node - return parse_element(text); - - // (text); - } - else - { - // Parse PI - return parse_pi(text); - } - - // (text); - } - break; - - // (text); - } - break; - - // (text); - } - - } // switch - - // Attempt to skip other, unrecognized node types starting with ')) - { - if (*text == 0) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - ++text; // Skip '>' - return 0; // No node recognized - - } - } - - // Parse contents of the node - children, data etc. - template - void parse_node_contents(Ch *&text, xml_node *node) - { - // For all children and text - while (1) - { - // Skip whitespace between > and node contents - Ch *contents_start = text; // Store start of node contents before whitespace is skipped - skip(text); - Ch next_char = *text; - - // After data nodes, instead of continuing the loop, control jumps here. - // This is because zero termination inside parse_and_append_data() function - // would wreak havoc with the above code. - // Also, skipping whitespace after data nodes is unnecessary. - after_data_node: - - // Determine what comes next: node closing, child node, data node, or 0? - switch (next_char) - { - - // Node closing or child node - case Ch('<'): - if (text[1] == Ch('/')) - { - // Node closing - text += 2; // Skip '(text); - if (!internal::compare(node->name(), node->name_size(), closing_name, text - closing_name, true)) - RAPIDXML_PARSE_ERROR("invalid closing tag name", text); - } - else - { - // No validation, just skip name - skip(text); - } - // Skip remaining whitespace after node name - skip(text); - if (*text != Ch('>')) - RAPIDXML_PARSE_ERROR("expected >", text); - ++text; // Skip '>' - return; // Node closed, finished parsing contents - } - else - { - // Child node - ++text; // Skip '<' - if (xml_node *child = parse_node(text)) - node->append_node(child); - } - break; - - // End of data - error - case Ch('\0'): - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - - // Data node - default: - next_char = parse_and_append_data(node, text, contents_start); - goto after_data_node; // Bypass regular processing after data nodes - - } - } - } - - // Parse XML attributes of the node - template - void parse_node_attributes(Ch *&text, xml_node *node) - { - // For all attributes - while (attribute_name_pred::test(*text)) - { - // Extract attribute name - Ch *name = text; - ++text; // Skip first character of attribute name - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected attribute name", name); - - // Create new attribute - xml_attribute *attribute = this->allocate_attribute(); - attribute->name(name, text - name); - node->append_attribute(attribute); - - // Skip whitespace after attribute name - skip(text); - - // Skip = - if (*text != Ch('=')) - RAPIDXML_PARSE_ERROR("expected =", text); - ++text; - - // Add terminating zero after name - if (!(Flags & parse_no_string_terminators)) - attribute->name()[attribute->name_size()] = 0; - - // Skip whitespace after = - skip(text); - - // Skip quote and remember if it was ' or " - Ch quote = *text; - if (quote != Ch('\'') && quote != Ch('"')) - RAPIDXML_PARSE_ERROR("expected ' or \"", text); - ++text; - - // Extract attribute value and expand char refs in it - Ch *value = text, *end; - const int AttFlags = Flags & ~parse_normalize_whitespace; // No whitespace normalization in attributes - if (quote == Ch('\'')) - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); - else - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); - - // Set attribute value - attribute->value(value, end - value); - - // Make sure that end quote is present - if (*text != quote) - RAPIDXML_PARSE_ERROR("expected ' or \"", text); - ++text; // Skip quote - - // Add terminating zero after value - if (!(Flags & parse_no_string_terminators)) - attribute->value()[attribute->value_size()] = 0; - - // Skip whitespace after attribute value - skip(text); - } - } - - }; - - //! \cond internal - namespace internal - { - - // Whitespace (space \n \r \t) - template - const unsigned char lookup_tables::lookup_whitespace[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, // 0 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // F - }; - - // Node name (anything but space \n \r \t / > ? \0) - template - const unsigned char lookup_tables::lookup_node_name[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) (anything but < \0) - template - const unsigned char lookup_tables::lookup_text[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) that does not require processing when ws normalization is disabled - // (anything but < \0 &) - template - const unsigned char lookup_tables::lookup_text_pure_no_ws[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) that does not require processing when ws normalizationis is enabled - // (anything but < \0 & space \n \r \t) - template - const unsigned char lookup_tables::lookup_text_pure_with_ws[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute name (anything but space \n \r \t / < > = ? ! \0) - template - const unsigned char lookup_tables::lookup_attribute_name[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with single quote (anything but ' \0) - template - const unsigned char lookup_tables::lookup_attribute_data_1[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with single quote that does not require processing (anything but ' \0 &) - template - const unsigned char lookup_tables::lookup_attribute_data_1_pure[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with double quote (anything but " \0) - template - const unsigned char lookup_tables::lookup_attribute_data_2[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with double quote that does not require processing (anything but " \0 &) - template - const unsigned char lookup_tables::lookup_attribute_data_2_pure[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Digits (dec and hex, 255 denotes end of numeric character reference) - template - const unsigned char lookup_tables::lookup_digits[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3 - 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5 - 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 // F - }; - - // Upper case conversion - template - const unsigned char lookup_tables::lookup_upcase[256] = - { - // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A B C D E F - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, // 0 - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, // 1 - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, // 2 - 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // 3 - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 4 - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, // 5 - 96, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 6 - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 123,124,125,126,127, // 7 - 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, // 8 - 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, // 9 - 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175, // A - 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, // B - 192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207, // C - 208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223, // D - 224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239, // E - 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255 // F - }; - } - //! \endcond - -} - -// Undefine internal macros -#undef RAPIDXML_PARSE_ERROR - -// On MSVC, restore warnings state -#ifdef _MSC_VER - #pragma warning(pop) -#endif - -#endif diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp b/crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp deleted file mode 100644 index 68cf57f..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp +++ /dev/null @@ -1,295 +0,0 @@ -#ifndef RAPIDXML_ITERATORS_HPP_INCLUDED -#define RAPIDXML_ITERATORS_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/15 23:02:39 $ -//! \file rapidxml_iterators.hpp This file contains rapidxml iterators - -#include "rapidxml.hpp" - -namespace rapidxml -{ - const unsigned int iterate_check_name = 1 << 0; - const unsigned int iterate_case_sensitive = 1 << 1; - - //! Iterator of child nodes of xml_node - template - class node_iterator - { - public: - typedef xml_node *value_type; - typedef const value_type& reference; - typedef xml_node *pointer; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - - node_iterator() - : m_cur(0) - , m_prev(0) - , m_flags(0) - { - } - - node_iterator(xml_node* node, xml_node* prev, - unsigned char flags) - : m_cur(node) - , m_prev(prev) - , m_flags(flags) - { - } - - reference operator*() const - { - assert(m_cur); - return m_cur; - } - - pointer operator->() const - { - assert(m_cur); - return m_cur; - } - - node_iterator& operator++() - { - increment(); - return *this; - } - - node_iterator operator++(int) - { - node_iterator tmp = *this; - increment(); - return tmp; - } - - node_iterator& operator--() - { - decrement(); - return *this; - } - - node_iterator operator--(int) - { - node_iterator tmp = *this; - decrement(); - return tmp; - } - - bool operator==(const node_iterator &rhs) const - { - return m_cur == rhs.m_cur; - } - - bool operator!=(const node_iterator &rhs) const - { - return m_cur != rhs.m_cur; - } - - private: - void increment() - { - assert(m_cur && "Attempted to increment end iterator"); - m_prev = m_cur; - - if (m_flags & iterate_check_name) - m_cur = m_cur->next_sibling( - m_cur->name(), m_cur->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_cur = m_cur->next_sibling(); - } - - void decrement() - { - assert(m_prev && "Attempted to decrement begin iterator"); - m_cur = m_prev; - - if (m_flags & iterate_check_name) - m_prev = m_prev->previous_sibling( - m_prev->name(), m_prev->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_prev = m_prev->previous_sibling(); - } - - xml_node *m_cur; - xml_node *m_prev; - unsigned char m_flags; - }; - - //! Iterator of child attributes of xml_node - template - class attribute_iterator - { - public: - typedef xml_attribute *value_type; - typedef const value_type& reference; - typedef xml_attribute *pointer; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - - attribute_iterator() - : m_cur(0) - , m_prev(0) - , m_flags(0) - { - } - - attribute_iterator(xml_attribute* attr, xml_attribute* prev, - unsigned char flags) - : m_cur(attr) - , m_prev(prev) - , m_flags(flags) - { - } - - reference operator*() const - { - assert(m_cur); - return m_cur; - } - - pointer operator->() const - { - assert(m_cur); - return m_cur; - } - - attribute_iterator& operator++() - { - increment(); - return *this; - } - - attribute_iterator operator++(int) - { - attribute_iterator tmp = *this; - increment(); - return tmp; - } - - attribute_iterator& operator--() - { - decrement(); - return *this; - } - - attribute_iterator operator--(int) - { - attribute_iterator tmp = *this; - decrement(); - return tmp; - } - - bool operator==(const attribute_iterator &rhs) const - { - return m_cur == rhs.m_cur; - } - - bool operator!=(const attribute_iterator &rhs) const - { - return m_cur != rhs.m_cur; - } - - private: - void increment() - { - assert(m_cur && "Attempted to increment end iterator"); - m_prev = m_cur; - - if (m_flags & iterate_check_name) - m_cur = m_cur->next_attribute( - m_cur->name(), m_cur->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_cur = m_cur->next_attribute(); - } - - void decrement() - { - assert(m_prev && "Attempted to decrement begin iterator"); - m_cur = m_prev; - - if (m_flags & iterate_check_name) - m_prev = m_prev->previous_attribute( - m_prev->name(), m_prev->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_prev = m_prev->previous_attribute(); - } - - xml_attribute* m_cur; - xml_attribute* m_prev; - unsigned char m_flags; - }; - - // Range-based for loop support - template - class iterator_range - { - public: - typedef Iterator const_iterator; - typedef Iterator iterator; - - iterator_range(Iterator first, Iterator last) - : m_first(first) - , m_last(last) - { - } - - Iterator begin() const { return m_first; } - Iterator end() const { return m_last; } - - private: - Iterator m_first; - Iterator m_last; - }; - - template - iterator_range> nodes(const xml_node* node, - const Ch* name = 0, - std::size_t name_size = 0, - bool case_sensitive = true) - { - unsigned char flags = 0; - if (name) - flags |= iterate_check_name; - if (case_sensitive) - flags |= iterate_case_sensitive; - - xml_node* first = - node->first_node(name, name_size, case_sensitive); - xml_node* last = first ? - node->last_node(name, name_size, case_sensitive) : nullptr; - - node_iterator begin(first, 0, flags); - node_iterator end(0, last, flags); - return iterator_range>(begin, end); - } - - template - iterator_range> attributes(const xml_node* node, - const Ch *name = 0, - std::size_t name_size = 0, - bool case_sensitive = true) - { - unsigned char flags = 0; - if (name) - flags |= iterate_check_name; - if (case_sensitive) - flags |= iterate_case_sensitive; - - xml_attribute* first = - node->first_attribute(name, name_size, case_sensitive); - xml_attribute* last = - node->last_attribute(name, name_size, case_sensitive); - - attribute_iterator begin(first, 0, flags); - attribute_iterator end(0, last, flags); - return iterator_range>(begin, end); - } -} - -#endif diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp b/crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp deleted file mode 100644 index ae80e1f..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp +++ /dev/null @@ -1,422 +0,0 @@ -#ifndef RAPIDXML_PRINT_HPP_INCLUDED -#define RAPIDXML_PRINT_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/13 01:46:17 $ -//! \file rapidxml_print.hpp This file contains rapidxml printer implementation - -#include "rapidxml.hpp" - -// Only include streams if not disabled -#ifndef RAPIDXML_NO_STREAMS - #include - #include -#endif - -namespace rapidxml -{ - - /////////////////////////////////////////////////////////////////////// - // Printing flags - - const int print_no_indenting = 0x1; //!< Printer flag instructing the printer to suppress indenting of XML. See print() function. - - /////////////////////////////////////////////////////////////////////// - // Internal - - //! \cond internal - namespace internal - { - - /////////////////////////////////////////////////////////////////////////// - // Internal character operations - - // Copy characters from given range to given output iterator - template - inline OutIt copy_chars(const Ch *begin, const Ch *end, OutIt out) - { - while (begin != end) - *out++ = *begin++; - return out; - } - - // Copy characters from given range to given output iterator and expand - // characters into references (< > ' " &) - template - inline OutIt copy_and_expand_chars(const Ch *begin, const Ch *end, Ch noexpand, OutIt out) - { - while (begin != end) - { - if (*begin == noexpand) - { - *out++ = *begin; // No expansion, copy character - } - else - { - switch (*begin) - { - case Ch('<'): - *out++ = Ch('&'); *out++ = Ch('l'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('>'): - *out++ = Ch('&'); *out++ = Ch('g'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('\''): - *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('p'); *out++ = Ch('o'); *out++ = Ch('s'); *out++ = Ch(';'); - break; - case Ch('"'): - *out++ = Ch('&'); *out++ = Ch('q'); *out++ = Ch('u'); *out++ = Ch('o'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('&'): - *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('m'); *out++ = Ch('p'); *out++ = Ch(';'); - break; - default: - *out++ = *begin; // No expansion, copy character - } - } - ++begin; // Step to next character - } - return out; - } - - // Fill given output iterator with repetitions of the same character - template - inline OutIt fill_chars(OutIt out, int n, Ch ch) - { - for (int i = 0; i < n; ++i) - *out++ = ch; - return out; - } - - // Find character - template - inline bool find_char(const Ch *begin, const Ch *end) - { - while (begin != end) - if (*begin++ == ch) - return true; - return false; - } - - /////////////////////////////////////////////////////////////////////////// - // Internal printing operations - - template - OutIt print_node(OutIt out, const xml_node *node, int flags, int indent); - - // Print children of the node - template - inline OutIt print_children(OutIt out, const xml_node *node, int flags, int indent) - { - for (xml_node *child = node->first_node(); child; child = child->next_sibling()) - out = print_node(out, child, flags, indent); - return out; - } - - // Print attributes of the node - template - inline OutIt print_attributes(OutIt out, const xml_node *node, int flags) - { - for (xml_attribute *attribute = node->first_attribute(); attribute; attribute = attribute->next_attribute()) - { - if (attribute->name() && attribute->value()) - { - // Print attribute name - *out = Ch(' '), ++out; - out = copy_chars(attribute->name(), attribute->name() + attribute->name_size(), out); - *out = Ch('='), ++out; - // Print attribute value using appropriate quote type - if (find_char(attribute->value(), attribute->value() + attribute->value_size())) - { - *out = Ch('\''), ++out; - out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('"'), out); - *out = Ch('\''), ++out; - } - else - { - *out = Ch('"'), ++out; - out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('\''), out); - *out = Ch('"'), ++out; - } - } - } - return out; - } - - // Print data node - template - inline OutIt print_data_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_data); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); - return out; - } - - // Print data node - template - inline OutIt print_cdata_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_cdata); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'); ++out; - *out = Ch('!'); ++out; - *out = Ch('['); ++out; - *out = Ch('C'); ++out; - *out = Ch('D'); ++out; - *out = Ch('A'); ++out; - *out = Ch('T'); ++out; - *out = Ch('A'); ++out; - *out = Ch('['); ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch(']'); ++out; - *out = Ch(']'); ++out; - *out = Ch('>'); ++out; - return out; - } - - // Print element node - template - inline OutIt print_element_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_element); - - // Print element name and attributes, if any - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - out = print_attributes(out, node, flags); - - // If node is childless - if (node->value_size() == 0 && !node->first_node()) - { - // Print childless node tag ending - *out = Ch('/'), ++out; - *out = Ch('>'), ++out; - } - else - { - // Print normal node tag ending - *out = Ch('>'), ++out; - - // Test if node contains a single data node only (and no other nodes) - xml_node *child = node->first_node(); - if (!child) - { - // If node has no children, only print its value without indenting - out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); - } - else if (child->next_sibling() == 0 && child->type() == node_data) - { - // If node has a sole data child, only print its value without indenting - out = copy_and_expand_chars(child->value(), child->value() + child->value_size(), Ch(0), out); - } - else - { - // Print all children with full indenting - if (!(flags & print_no_indenting)) - *out = Ch('\n'), ++out; - out = print_children(out, node, flags, indent + 1); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - } - - // Print node end - *out = Ch('<'), ++out; - *out = Ch('/'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - *out = Ch('>'), ++out; - } - return out; - } - - // Print declaration node - template - inline OutIt print_declaration_node(OutIt out, const xml_node *node, int flags, int indent) - { - // Print declaration start - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('?'), ++out; - *out = Ch('x'), ++out; - *out = Ch('m'), ++out; - *out = Ch('l'), ++out; - - // Print attributes - out = print_attributes(out, node, flags); - - // Print declaration end - *out = Ch('?'), ++out; - *out = Ch('>'), ++out; - - return out; - } - - // Print comment node - template - inline OutIt print_comment_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_comment); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('!'), ++out; - *out = Ch('-'), ++out; - *out = Ch('-'), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('-'), ++out; - *out = Ch('-'), ++out; - *out = Ch('>'), ++out; - return out; - } - - // Print doctype node - template - inline OutIt print_doctype_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_doctype); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('!'), ++out; - *out = Ch('D'), ++out; - *out = Ch('O'), ++out; - *out = Ch('C'), ++out; - *out = Ch('T'), ++out; - *out = Ch('Y'), ++out; - *out = Ch('P'), ++out; - *out = Ch('E'), ++out; - *out = Ch(' '), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('>'), ++out; - return out; - } - - // Print pi node - template - inline OutIt print_pi_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_pi); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('?'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - *out = Ch(' '), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('?'), ++out; - *out = Ch('>'), ++out; - return out; - } - - // Print node - template - inline OutIt print_node(OutIt out, const xml_node *node, int flags, int indent) - { - // Print proper node type - switch (node->type()) - { - // Document - case node_document: - out = print_children(out, node, flags, indent); - break; - - // Element - case node_element: - out = print_element_node(out, node, flags, indent); - break; - - // Data - case node_data: - out = print_data_node(out, node, flags, indent); - break; - - // CDATA - case node_cdata: - out = print_cdata_node(out, node, flags, indent); - break; - - // Declaration - case node_declaration: - out = print_declaration_node(out, node, flags, indent); - break; - - // Comment - case node_comment: - out = print_comment_node(out, node, flags, indent); - break; - - // Doctype - case node_doctype: - out = print_doctype_node(out, node, flags, indent); - break; - - // Pi - case node_pi: - out = print_pi_node(out, node, flags, indent); - break; - - // Unknown - default: - assert(0); - break; - } - - // If indenting not disabled, add line break after node - if (!(flags & print_no_indenting)) - *out = Ch('\n'), ++out; - - // Return modified iterator - return out; - } - } - //! \endcond - - /////////////////////////////////////////////////////////////////////////// - // Printing - - //! Prints XML to given output iterator. - //! \param out Output iterator to print to. - //! \param node Node to be printed. Pass xml_document to print entire document. - //! \param flags Flags controlling how XML is printed. - //! \return Output iterator pointing to position immediately after last character of printed text. - template - inline OutIt print(OutIt out, const xml_node &node, int flags = 0) - { - return internal::print_node(out, &node, flags, 0); - } - -#ifndef RAPIDXML_NO_STREAMS - - //! Prints XML to given output stream. - //! \param out Output stream to print to. - //! \param node Node to be printed. Pass xml_document to print entire document. - //! \param flags Flags controlling how XML is printed. - //! \return Output stream. - template - inline std::basic_ostream &print(std::basic_ostream &out, const xml_node &node, int flags = 0) - { - print(std::ostream_iterator(out), node, flags); - return out; - } - - //! Prints formatted XML to given output stream. Uses default printing flags. Use print() function to customize printing process. - //! \param out Output stream to print to. - //! \param node Node to be printed. - //! \return Output stream. - template - inline std::basic_ostream &operator <<(std::basic_ostream &out, const xml_node &node) - { - return print(out, node); - } - -#endif - -} - -#endif diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp b/crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp deleted file mode 100644 index 91cf83e..0000000 --- a/crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef RAPIDXML_UTILS_HPP_INCLUDED -#define RAPIDXML_UTILS_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/15 23:02:39 $ -//! \file rapidxml_utils.hpp This file contains high-level rapidxml utilities that can be useful -//! in certain simple scenarios. They should probably not be used if maximizing performance is the main objective. - -#include "rapidxml.hpp" -#include - -namespace rapidxml -{ - //! Counts children of node. Time complexity is O(n). - //! \return Number of children of node - template - inline std::size_t count_children(const xml_node *node, - const Ch* name = 0, - std::size_t name_size = 0) - { - if (name && name_size == 0) - name_size = internal::measure(name); - - xml_node *child = node->first_node(name, name_size); - std::size_t count = 0; - while (child) - { - ++count; - child = child->next_sibling(name, name_size); - } - return count; - } - - //! Counts attributes of node. Time complexity is O(n). - //! \return Number of attributes of node - template - inline std::size_t count_attributes(const xml_node *node, - const Ch* name = 0, - std::size_t name_size = 0) - { - if (name && name_size == 0) - name_size = internal::measure(name); - - xml_attribute *attr = node->first_attribute(name, name_size); - std::size_t count = 0; - while (attr) - { - ++count; - attr = attr->next_attribute(name, name_size); - } - return count; - } -} - -#endif From 1b33592d67ed923351e0ba5f5566022fddbf61db Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 11 Apr 2024 01:05:47 +0200 Subject: [PATCH 25/54] separate models from package parsing and loading --- Cargo.lock | 31 +- Cargo.toml | 1 + crates/joko_package/Cargo.toml | 1 + .../{src/pack => images}/marker.png | Bin .../{src/pack => images}/question.png | Bin .../{src/pack => images}/trail.png | Bin .../{src/pack => images}/trail_black.png | Bin .../{src/pack => images}/trail_rainbow.png | Bin crates/joko_package/src/io/deserialize.rs | 6 +- crates/joko_package/src/io/mod.rs | 177 -------- crates/joko_package/src/io/serialize.rs | 3 +- crates/joko_package/src/lib.rs | 1 - .../joko_package/src/manager/pack/active.rs | 8 +- .../src/manager/pack/category_selection.rs | 3 +- .../joko_package/src/manager/pack/import.rs | 2 +- .../joko_package/src/manager/pack/loaded.rs | 4 +- crates/joko_package/src/manager/package.rs | 7 +- crates/joko_package/src/message.rs | 3 +- crates/joko_package/src/pack/mod.rs | 413 ------------------ crates/joko_package/src/pack/route.rs | 20 - crates/joko_package_models/Cargo.toml | 41 ++ .../src/attributes.rs} | 189 +++++++- crates/joko_package_models/src/category.rs | 209 +++++++++ crates/joko_package_models/src/lib.rs | 9 + crates/joko_package_models/src/map.rs | 13 + .../src}/marker.rs | 4 +- crates/joko_package_models/src/package.rs | 175 ++++++++ crates/joko_package_models/src/route.rs | 48 ++ .../pack => joko_package_models/src}/trail.rs | 8 +- crates/joko_render/src/billboard.rs | 3 + crates/joko_render/src/renderer.rs | 2 +- crates/jokolay/src/app/mod.rs | 21 +- crates/jokolay/src/app/mumble.rs | 282 ++++++++++++ crates/jokolink/Cargo.toml | 1 - crates/jokolink/src/lib.rs | 266 ----------- 35 files changed, 1031 insertions(+), 920 deletions(-) rename crates/joko_package/{src/pack => images}/marker.png (100%) rename crates/joko_package/{src/pack => images}/question.png (100%) rename crates/joko_package/{src/pack => images}/trail.png (100%) rename crates/joko_package/{src/pack => images}/trail_black.png (100%) rename crates/joko_package/{src/pack => images}/trail_rainbow.png (100%) delete mode 100644 crates/joko_package/src/pack/mod.rs delete mode 100644 crates/joko_package/src/pack/route.rs create mode 100644 crates/joko_package_models/Cargo.toml rename crates/{joko_package/src/pack/common.rs => joko_package_models/src/attributes.rs} (92%) create mode 100644 crates/joko_package_models/src/category.rs create mode 100644 crates/joko_package_models/src/lib.rs create mode 100644 crates/joko_package_models/src/map.rs rename crates/{joko_package/src/pack => joko_package_models/src}/marker.rs (79%) create mode 100644 crates/joko_package_models/src/package.rs create mode 100644 crates/joko_package_models/src/route.rs rename crates/{joko_package/src/pack => joko_package_models/src}/trail.rs (80%) create mode 100644 crates/jokolay/src/app/mumble.rs diff --git a/Cargo.lock b/Cargo.lock index e0b958c..a2738b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,6 +1404,7 @@ dependencies = [ "indexmap", "itertools", "joko_core", + "joko_package_models", "joko_render_models", "jokoapi", "jokolink", @@ -1428,6 +1429,35 @@ dependencies = [ "zip", ] +[[package]] +name = "joko_package_models" +version = "0.2.1" +dependencies = [ + "base64", + "bytemuck", + "cxx-build", + "data-encoding", + "enumflags2", + "glam", + "indexmap", + "itertools", + "joko_core", + "jokoapi", + "miette", + "ordered_hash_map", + "paste", + "phf", + "rstest", + "serde", + "serde_json", + "similar-asserts", + "smol_str", + "tracing", + "url", + "uuid", + "xot", +] + [[package]] name = "joko_render" version = "0.2.1" @@ -1495,7 +1525,6 @@ name = "jokolink" version = "0.2.1" dependencies = [ "arcdps", - "egui", "enumflags2", "glam", "miette", diff --git a/Cargo.toml b/Cargo.toml index b7b8e73..c3c3e79 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/joko_render", "crates/joko_render_models", "crates/joko_package", + "crates/joko_package_models", "crates/jokolink", "crates/jokoapi", "crates/jokolay", diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml index 060f953..f795f14 100644 --- a/crates/joko_package/Cargo.toml +++ b/crates/joko_package/Cargo.toml @@ -19,6 +19,7 @@ indexmap = { workspace = true, features = ["serde"]} # to keep the order of file itertools = { workspace = true } joko_core = { path = "../joko_core" } joko_render_models = { path = "../joko_render_models" } +joko_package_models = { path = "../joko_package_models" } jokoapi = { path = "../jokoapi" } jokolink = { path = "../jokolink" } miette = { workspace = true } diff --git a/crates/joko_package/src/pack/marker.png b/crates/joko_package/images/marker.png similarity index 100% rename from crates/joko_package/src/pack/marker.png rename to crates/joko_package/images/marker.png diff --git a/crates/joko_package/src/pack/question.png b/crates/joko_package/images/question.png similarity index 100% rename from crates/joko_package/src/pack/question.png rename to crates/joko_package/images/question.png diff --git a/crates/joko_package/src/pack/trail.png b/crates/joko_package/images/trail.png similarity index 100% rename from crates/joko_package/src/pack/trail.png rename to crates/joko_package/images/trail.png diff --git a/crates/joko_package/src/pack/trail_black.png b/crates/joko_package/images/trail_black.png similarity index 100% rename from crates/joko_package/src/pack/trail_black.png rename to crates/joko_package/images/trail_black.png diff --git a/crates/joko_package/src/pack/trail_rainbow.png b/crates/joko_package/images/trail_rainbow.png similarity index 100% rename from crates/joko_package/src/pack/trail_rainbow.png rename to crates/joko_package/images/trail_rainbow.png diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 7219c5e..c54156b 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -1,8 +1,8 @@ use joko_core::RelativePath; +use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, map::MapData, marker::Marker, package::PackCore, route::Route, trail::{TBin, TBinStatus, Trail}}; use miette::{bail, Context, IntoDiagnostic, Result}; use crate::{ - pack::{prefix_parent, Category, CommonAttributes, MapData, Marker, PackCore, RawCategory, Route, TBin, TBinStatus, Trail}, BASE64_ENGINE, }; use base64::Engine; @@ -15,7 +15,6 @@ use tracing::{debug, info, info_span, instrument, trace, warn}; use uuid::Uuid; use xot::{Node, Xot, Element}; -use super::XotAttributeNameIDs; pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { //called from already parsed data @@ -455,9 +454,6 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - debug!("Found a route in core pack {:?}", child_element); let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_name.clone()); if let Some(route) = route { - //TODO: make sure there is no "very late" discovery - //let category_uuid = target.get_or_create_category_uuid(&route.category); - //route.parent = category_uuid; target.register_route(route)?; } else { info!("Could not parse route {:?}", child_element); diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs index de2bd5c..6db6a1f 100644 --- a/crates/joko_package/src/io/mod.rs +++ b/crates/joko_package/src/io/mod.rs @@ -9,180 +9,3 @@ mod serialize; pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; -pub(crate) struct XotAttributeNameIDs { - // xml tags - pub overlay_data: NameId, - pub marker_category: NameId, - pub pois: NameId, - pub poi: NameId, - pub trail: NameId, - pub route: NameId, - // marker specific attributes - pub category: NameId, - pub guid: NameId, - pub map_id: NameId, - pub xpos: NameId, - pub ypos: NameId, - pub zpos: NameId, - // marker category specific attributes - pub default_enabled: NameId, - pub display_name: NameId, - pub name: NameId, - pub capital_name: NameId,//same than "name" but with a starting capital letter - pub separator: NameId, - // inheritable attributes - pub achievement_id: NameId, - pub achievement_bit: NameId, - pub alpha: NameId, - pub anim_speed: NameId, - pub auto_trigger: NameId, - pub behavior: NameId, - pub bounce: NameId, - pub bounce_delay: NameId, - pub bounce_duration: NameId, - pub bounce_height: NameId, - pub can_fade: NameId, - pub color: NameId, - pub copy: NameId, - pub copy_message: NameId, - pub cull: NameId, - pub fade_far: NameId, - pub fade_near: NameId, - pub festival: NameId, - pub has_countdown: NameId, - pub height_offset: NameId, - pub hide: NameId, - pub icon_file: NameId, - pub icon_size: NameId, - pub in_game_visibility: NameId, - pub info: NameId, - pub info_range: NameId, - pub invert_behavior: NameId, - pub is_wall: NameId, - pub keep_on_map_edge: NameId, - pub map_display_size: NameId, - pub map_fade_out_scale_level: NameId, - pub map_type: NameId, - pub map_visibility: NameId, - pub max_size: NameId, - pub min_size: NameId, - pub mini_map_visibility: NameId, - pub mount: NameId, - pub profession: NameId, - pub race: NameId, - pub reset_length: NameId, - pub reset_offset: NameId, - pub rotate: NameId, - pub rotate_x: NameId, - pub rotate_y: NameId, - pub rotate_z: NameId, - pub scale_on_map_with_zoom: NameId, - pub show: NameId, - pub specialization: NameId, - pub text: NameId, - pub texture: NameId, - pub tip_name: NameId, - pub tip_description: NameId, - pub title: NameId, - pub title_color: NameId, - pub toggle_category: NameId, - pub trail_data: NameId, - pub trail_scale: NameId, - pub trigger_range: NameId, - pub reset_range: NameId, - pub resetposx: NameId, - pub resetposy: NameId, - pub resetposz: NameId, - pub _source_file_name: NameId, -} -impl XotAttributeNameIDs { - pub fn register_with_xot(tree: &mut Xot) -> Self { - Self { - // tags - overlay_data: tree.add_name("OverlayData"), - marker_category: tree.add_name("MarkerCategory"), - pois: tree.add_name("POIs"), - poi: tree.add_name("POI"), - trail: tree.add_name("Trail"), - route: tree.add_name("Route"), - // non inheritable attributes - category: tree.add_name("type"), - xpos: tree.add_name("xpos"), - ypos: tree.add_name("ypos"), - zpos: tree.add_name("zpos"), - map_id: tree.add_name("MapID"), - guid: tree.add_name("GUID"), - - // marker category specific attrs - separator: tree.add_name("IsSeparator"), - default_enabled: tree.add_name("defaulttoggle"), - display_name: tree.add_name("DisplayName"), - name: tree.add_name("name"), - capital_name: tree.add_name("Name"), - // inheritable attributes - achievement_id: tree.add_name("achievementId"), - achievement_bit: tree.add_name("achievementBit"), - alpha: tree.add_name("alpha"), - anim_speed: tree.add_name("animSpeed"), - auto_trigger: tree.add_name("autotrigger"), - behavior: tree.add_name("behavior"), - color: tree.add_name("color"), - copy: tree.add_name("copy"), - copy_message: tree.add_name("copy-message"), - fade_near: tree.add_name("fadeNear"), - fade_far: tree.add_name("fadeFar"), - festival: tree.add_name("festival"), - has_countdown: tree.add_name("hasCountdown"), - height_offset: tree.add_name("heightOffset"), - icon_file: tree.add_name("iconFile"), - icon_size: tree.add_name("iconSize"), - in_game_visibility: tree.add_name("inGameVisibility"), - info: tree.add_name("info"), - info_range: tree.add_name("infoRange"), - map_display_size: tree.add_name("mapDisplaySize"), - map_visibility: tree.add_name("mapVisibility"), - max_size: tree.add_name("maxSize"), - min_size: tree.add_name("minSize"), - mini_map_visibility: tree.add_name("miniMapVisibility"), - mount: tree.add_name("mount"), - profession: tree.add_name("profession"), - race: tree.add_name("race"), - reset_length: tree.add_name("resetLength"), - reset_offset: tree.add_name("resetOffset"), - scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), - tip_name: tree.add_name("tip-name"), - tip_description: tree.add_name("tip-description"), - toggle_category: tree.add_name("togglecateogry"), - texture: tree.add_name("texture"), - trail_data: tree.add_name("trailData"), - trail_scale: tree.add_name("trailScale"), - trigger_range: tree.add_name("triggerRange"), - bounce_delay: tree.add_name("bounce-delay"), - bounce_duration: tree.add_name("bounce-duration"), - bounce_height: tree.add_name("bounce-height"), - can_fade: tree.add_name("canfade"), - cull: tree.add_name("cull"), - hide: tree.add_name("hide"), - is_wall: tree.add_name("iswall"), - invert_behavior: tree.add_name("invertbehavior"), - map_type: tree.add_name("maptype"), - rotate: tree.add_name("rotate"), - rotate_x: tree.add_name("rotate-x"), - rotate_y: tree.add_name("rotate-y"), - rotate_z: tree.add_name("rotate-z"), - show: tree.add_name("show"), - specialization: tree.add_name("specialization"), - title: tree.add_name("title"), - title_color: tree.add_name("title-color"), - text: tree.add_name("text"), - bounce: tree.add_name("bounce"), - keep_on_map_edge: tree.add_name("keepOnMapEdge"), - map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), - reset_range: tree.add_name("resetrange"), - resetposx: tree.add_name("resetposx"), - resetposy: tree.add_name("resetposy"), - resetposz: tree.add_name("resetposz"), - _source_file_name: tree.add_name("_source_file_name"), - } - } -} diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index 2155f20..e3f920a 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -1,18 +1,17 @@ use crate::{ - pack::{Category, Marker, Trail, Route}, manager::{LoadedPackData, LoadedPackTexture}, BASE64_ENGINE, }; use base64::Engine; use cap_std::fs_utf8::Dir; use indexmap::IndexMap; +use joko_package_models::{attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail}; use miette::{Context, IntoDiagnostic, Result}; use std::io::Write; use tracing::info; use uuid::Uuid; use xot::{Element, Node, SerializeOptions, Xot}; -use super::XotAttributeNameIDs; /// Save the pack core as xml pack using the given directory as pack root path. pub(crate) fn save_pack_data_to_dir( pack_data: &LoadedPackData, diff --git a/crates/joko_package/src/lib.rs b/crates/joko_package/src/lib.rs index dc4c68a..607ecf4 100644 --- a/crates/joko_package/src/lib.rs +++ b/crates/joko_package/src/lib.rs @@ -4,7 +4,6 @@ pub(crate) mod io; pub(crate) mod manager; -pub(crate) mod pack; pub mod message; pub use manager::{ diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs index 4745e33..4c1ad22 100644 --- a/crates/joko_package/src/manager/pack/active.rs +++ b/crates/joko_package/src/manager/pack/active.rs @@ -1,3 +1,4 @@ +use joko_package_models::attributes::CommonAttributes; use jokoapi::end_point::mounts::Mount; use ordered_hash_map::OrderedHashMap; @@ -8,7 +9,6 @@ use uuid::Uuid; use joko_core::RelativePath; use crate::{ - pack::CommonAttributes, INCHES_PER_METER, }; use jokolink::MumbleLink; @@ -44,7 +44,7 @@ pub(crate) struct ActiveMarker { pub common_attributes: CommonAttributes, } -pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; +pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0;// in game metric, for GW2, inches impl ActiveMarker { pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { @@ -73,7 +73,7 @@ impl ActiveMarker { } let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); let player_distance = pos.distance(link.player_pos); let camera_distance = pos.distance(link.cam_pos); @@ -195,7 +195,7 @@ impl ActiveTrail { } let alpha = attrs.get_alpha().copied().unwrap_or(1.0); let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs.get_fade_far().copied().unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; let fade_near_far = Vec2::new(fade_near, fade_far); let color = attrs.get_color().copied().unwrap_or([0u8; 4]); // default taco width diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs index 367b79d..e01a2a3 100644 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -1,11 +1,12 @@ use std::collections::{HashSet, HashMap}; +use joko_package_models::{attributes::CommonAttributes, category::Category, package::PackCore}; use ordered_hash_map::OrderedHashMap; use indexmap::IndexMap; use uuid::Uuid; use crate::{ - message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, PackCore} + message::{UIToBackMessage, UIToUIMessage} }; use serde::{Deserialize, Serialize}; diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs index 58f50a1..8b65377 100644 --- a/crates/joko_package/src/manager/pack/import.rs +++ b/crates/joko_package/src/manager/pack/import.rs @@ -1,8 +1,8 @@ use std::io::Read; +use joko_package_models::package::PackCore; use tracing::info; use miette::{IntoDiagnostic, Result}; -use crate::pack::PackCore; #[derive(Debug, Default)] diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 58c0f7b..fdf6727 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -3,6 +3,7 @@ use std::{ }; use indexmap::IndexMap; +use joko_package_models::{attributes::{Behavior, CommonAttributes}, category::Category, map::MapData, package::PackCore, trail::TBin}; use ordered_hash_map::OrderedHashMap; use cap_std::fs_utf8::Dir; @@ -12,7 +13,7 @@ use tracing::{debug, error, info, info_span}; use uuid::Uuid; use crate::{ - io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage}, pack::{Category, CommonAttributes, MapData, PackCore, TBin} + io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage} }; use jokolink::MumbleLink; use joko_core::{ @@ -363,7 +364,6 @@ impl LoadedPackData { common_attributes.inherit_if_attr_none(category_attributes); let key = &marker.guid; if let Some(behavior) = common_attributes.get_behavior() { - use crate::pack::Behavior; if match behavior { Behavior::AlwaysVisible => false, Behavior::ReappearOnMapChange diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index 059a3f5..fff0fd3 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -3,6 +3,7 @@ use std::{ }; use glam::Vec3; +use joko_package_models::attributes::CommonAttributes; use tribool::Tribool; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; @@ -16,7 +17,7 @@ use miette::Result; use uuid::Uuid; use crate::message::{UIToBackMessage, UIToUIMessage}; -use crate::{message::BackToUIMessage, pack::CommonAttributes}; +use crate::{message::BackToUIMessage}; use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; use crate::manager::pack::import::ImportStatus; @@ -309,7 +310,7 @@ impl PackageUIManager { etx: &egui::Context, ) { if self.default_marker_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); + let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); let size = [img.width() as _, img.height() as _]; self.default_marker_texture = Some(etx.load_texture( "default marker", @@ -322,7 +323,7 @@ impl PackageUIManager { )); } if self.default_trail_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/trail_rainbow.png")).unwrap(); + let img = image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); let size = [img.width() as _, img.height() as _]; self.default_trail_texture = Some(etx.load_texture( "default trail", diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index 9279f3c..efb353b 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, HashSet}; +use joko_package_models::{attributes::CommonAttributes, package::PackCore}; use uuid::Uuid; use glam::Vec3; @@ -11,7 +12,7 @@ use joko_render_models::{ trail::TrailObject }; -use crate::{pack::{CommonAttributes, PackCore}, LoadedPackTexture}; +use crate::LoadedPackTexture; pub enum BackToUIMessage { ActiveElements(HashSet),//list of all elements that are loaded for current map diff --git a/crates/joko_package/src/pack/mod.rs b/crates/joko_package/src/pack/mod.rs deleted file mode 100644 index d7a5df6..0000000 --- a/crates/joko_package/src/pack/mod.rs +++ /dev/null @@ -1,413 +0,0 @@ -mod common; -mod marker; -mod trail; -mod route; - -use std::collections::{BTreeMap, HashMap, HashSet}; - -use indexmap::IndexMap; -use ordered_hash_map::OrderedHashMap; - -use tracing::{debug, info, trace}; - -use joko_core::RelativePath; -pub use common::*; -pub(crate) use marker::*; -pub(crate) use trail::*; -pub(crate) use route::*; -use uuid::Uuid; - -#[derive(Default, Debug, Clone)] -pub(crate) struct MapData { - pub markers: IndexMap, - pub routes: IndexMap, - pub trails: IndexMap, -} - -#[derive(Debug, Clone)] -pub(crate) struct RawCategory { - pub guid: Uuid, - pub parent_name: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, -} - -#[derive(Debug, Clone)] -pub(crate) struct Category { - pub guid: Uuid, - pub parent: Option, - pub display_name: String, - pub relative_category_name: String, - pub full_category_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, - pub children: IndexMap, -} - -#[derive(Debug, Clone)] -pub struct PackCore { - /* - PackCore is a temporary holder of data - It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. - */ - pub uuid: Uuid, - pub textures: HashMap>, - pub(crate) tbins: HashMap, - pub(crate) categories: IndexMap, - pub all_categories: HashMap, - pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. - pub entities_parents: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package - pub maps: HashMap, -} - - -fn route_to_tbin(route: &Route) -> TBin { - assert!( route.path.len() > 1); - TBin { - map_id: route.map_id, - version: 0, - nodes: route.path.clone(), - } -} - -fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { - let mut props = CommonAttributes::default(); - props.set_texture(None); - props.set_trail_data(Some(file_path.clone())); - Trail { - map_id: route.map_id, - category: route.category.clone(), - parent: route.parent.clone(), - guid: route.guid, - props: props, - dynamic: true, - source_file_name: route.source_file_name.clone(), - } -} - -impl PackCore { - - pub fn new() -> Self { - let mut res = Self { - all_categories: Default::default(), - categories: Default::default(), - entities_parents: Default::default(), - late_discovery_categories: Default::default(), - maps: Default::default(), - source_files: Default::default(), - tbins: Default::default(), - textures: Default::default(), - uuid: Default::default(), - }; - res.uuid = Uuid::new_v4(); - res - } - pub fn partial(all_categories: &HashMap) -> Self { - // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. - let mut res: Self = Self::new(); - res.all_categories = all_categories.clone(); - res - } - - pub fn merge_partial(&mut self, partial_pack: PackCore) { - self.maps.extend(partial_pack.maps); - self.all_categories = partial_pack.all_categories; - self.late_discovery_categories.extend(partial_pack.late_discovery_categories); - self.source_files.extend(partial_pack.source_files); - self.tbins.extend(partial_pack.tbins); - self.entities_parents.extend(partial_pack.entities_parents); - } - pub fn category_exists(&self, full_category_name: &String) -> bool { - self.all_categories.contains_key(full_category_name) - } - - pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { - self.all_categories.get(full_category_name) - } - - pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { - if let Some(category_uuid) = self.all_categories.get(full_category_name) { - category_uuid.clone() - } else { - //TODO: if import is "dirty", create missing category - //TODO: default import mode is "strict" (get inspiration from HTML modes) - debug!("There is no defined category for {}", full_category_name); - - let mut n = 0; - let mut last_uuid: Option = None; - while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { - n += 1; - if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { - //FIXME: might want to make the difference between impacted parents and actual missing category - self.late_discovery_categories.insert(*parent_uuid); - last_uuid = Some(*parent_uuid); - } else { - let new_uuid = Uuid::new_v4(); - debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); - self.all_categories.insert(parent_full_category_name.clone(), new_uuid); - self.late_discovery_categories.insert(new_uuid); - last_uuid = Some(new_uuid); - } - } - trace!("{} uuid: {:?}", full_category_name, last_uuid); - assert!(last_uuid.is_some()); - last_uuid.unwrap() - } - } - - pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ - if let Some(parent_uuid) = self.all_categories.get(full_category_name) { - let mut uuid_to_insert = uuid.clone(); - while self.entities_parents.contains_key(&uuid_to_insert) { - trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); - uuid_to_insert = Uuid::new_v4(); - } - self.entities_parents.insert(uuid_to_insert, *parent_uuid); - Ok(uuid_to_insert) - } else { - //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) - } - } - - pub(crate) fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { - let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; - marker.guid = uuid_to_insert; - if !self.maps.contains_key(&marker.map_id) { - self.maps.insert(marker.map_id, MapData::default()); - } - self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); - Ok(()) - } - - pub(crate) fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { - let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; - trail.guid = uuid_to_insert; - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); - } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); - Ok(()) - } - - pub(crate) fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { - let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); - let tbin_path: RelativePath = file_name.parse().unwrap(); - let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; - route.guid = uuid_to_insert; - let trail = route_to_trail(&route, &tbin_path); - let tbin = route_to_tbin(&route); - - self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); - } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); - self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); - Ok(()) - } - - pub fn register_categories(&mut self) { - let mut entities_parents: HashMap = Default::default(); - let mut all_categories: HashMap = Default::default(); - Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); - self.entities_parents.extend(entities_parents); - self.all_categories = all_categories; - } - fn recursive_register_categories( - entities_parents: &mut HashMap, - categories: &IndexMap, - all_categories: &mut HashMap, - ) { - for (_, cat) in categories.iter() { - debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); - all_categories.insert(cat.full_category_name.clone(), cat.guid); - if let Some(parent) = cat.parent { - entities_parents.insert(cat.guid, parent); - } - Self::recursive_register_categories(entities_parents, &cat.children, all_categories); - } - } -} - - -pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { - let res = s.match_indices(pat) - .nth(n) - .map(|(index, _)| s.split_at(index)) - .map(|(left, _)| left.to_string()); - debug!("prefix_until_nth_char {} {} {:?}", s, n, res); - res -} - -pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { - let nb_matches = s.matches(pat).count(); - assert!(nb_matches + 1 > n); - let res = s.split(pat) - .nth(n) - ; - debug!("nth_chunk {} {} {:?}", s, n, res); - res.unwrap().to_string() -} - -pub fn prefix_parent(s: &str, pat: char) -> Option { - let n = s.matches(pat).count(); - assert!(n > 0); - let res = s.match_indices(pat) - .nth(n - 1) - .map(|(index, _)| s.split_at(index)) - .map(|(left, _)| left.to_string()); - debug!("prefix_parent {} {} {:?}", s, n, res); - res -} - -impl Category { - // Required method - pub fn from(value: &RawCategory, parent: Option) -> Self { - Self { - guid: value.guid.clone(), - props: value.props.clone(), - separator: value.separator, - default_enabled: value.default_enabled, - display_name: value.display_name.clone(), - relative_category_name: value.relative_category_name.clone(), - full_category_name: value.full_category_name.clone(), - parent: parent, - children: Default::default() - } - } - pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { - for (_, cat) in categories { - if &cat.guid == uuid { - return Some(cat); - } - let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); - if sub_res.is_some() { - return sub_res; - } - } - return None; - } - pub fn reassemble( - input_first_pass_categories: &OrderedHashMap, - late_discovered_categories: &mut HashSet, - ) -> IndexMap { - let mut first_pass_categories = input_first_pass_categories.clone(); - let mut second_pass_categories: OrderedHashMap = Default::default(); - let mut need_a_pass: bool = true; - - let mut third_pass_categories: IndexMap = Default::default(); - let mut third_pass_categories_ref: Vec = Default::default(); - let mut root: IndexMap = Default::default(); - while need_a_pass { - need_a_pass = false; - for (key, value) in first_pass_categories.iter() { - debug!("reassemble_categories {:?}", value); - let mut to_insert = value.clone(); - if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { - let mut n = 0; - let mut last_name: Option = None; - // This is an almost duplication of code of pack/mod.rs - while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { - debug!("{} {}", parent_name, n); - if let Some(parent_category) = first_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); - last_name = Some(parent_name.clone()); - } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); - last_name = Some(parent_name.clone()); - }else{ - let new_uuid = Uuid::new_v4(); - let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); - debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); - let to_insert = RawCategory { - default_enabled: value.default_enabled, - guid: new_uuid, - relative_category_name: relative_category_name.clone(), - display_name: relative_category_name.clone(), - parent_name: prefix_until_nth_char(&parent_name, '.', n-1), - props: value.props.clone(), - separator: false, - full_category_name: parent_name.clone() - }; - last_name = Some(to_insert.full_category_name.clone()); - second_pass_categories.insert(parent_name.clone(), to_insert); - late_discovered_categories.insert(new_uuid); - need_a_pass = true; - } - n += 1; - } - late_discovered_categories.insert(value.guid); - to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); - to_insert.display_name = to_insert.relative_category_name.clone(); - debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); - assert!(last_name.is_some()); - to_insert.parent_name = last_name; - } else { - to_insert.parent_name = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.full_category_name.clone()) - } else { - None - } - }else { - None - }; - debug!("insert as is {:?}", to_insert); - } - second_pass_categories.insert(key.clone(), to_insert); - } - if need_a_pass { - std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); - second_pass_categories.clear(); - } - } - for (key, value) in second_pass_categories { - let parent = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.guid.clone()) - } else { - None - } - } else { - None - }; - - debug!("{} parent is {:?}", key , parent); - let cat = Category::from(&value, parent); - let ref_uuid = cat.guid.clone(); - if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { - third_pass_categories_ref.push(ref_uuid); - } - } - - for full_category_name in third_pass_categories_ref { - if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { - if let Some(parent) = cat.parent { - if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { - parent_category.children.insert(cat.guid.clone(), cat); - } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { - parent_category.children.insert(cat.guid.clone(), cat); - } else { - panic!("Could not find parent {} for {:?}", parent, cat); - } - } else { - root.insert(cat.guid.clone(), cat); - } - } else { - panic!("Some bad logic at works"); - } - } - debug!("reassemble_categories {:?}", root); - root - } - - -} - diff --git a/crates/joko_package/src/pack/route.rs b/crates/joko_package/src/pack/route.rs deleted file mode 100644 index 2bef53b..0000000 --- a/crates/joko_package/src/pack/route.rs +++ /dev/null @@ -1,20 +0,0 @@ -use joko_core::RelativePath; -use uuid::Uuid; -use glam::Vec3; - -use crate::pack::CommonAttributes; - -use super::{TBin, Trail}; - -#[derive(Debug, Clone)] -pub(crate) struct Route { - pub category: String, - pub parent: Uuid, - pub path: Vec, - pub reset_position: Vec3, - pub reset_range: f64, - pub map_id: u32, - pub guid: Uuid, - pub name: String, - pub source_file_name: String, -} diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml new file mode 100644 index 0000000..a6af7b8 --- /dev/null +++ b/crates/joko_package_models/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "joko_package_models" +version = "0.2.1" +edition = "2021" + +[dependencies] +# jmf deps +# for marker packs +base64 = "0.21.2" +bytemuck = { workspace = true } +data-encoding = "2.4.0" +enumflags2 = { workspace = true } +glam = { workspace = true } +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +jokoapi = { path = "../jokoapi" } +miette = { workspace = true } +ordered_hash_map = { workspace = true } +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } +serde = { workspace = true } +serde_json = { workspace = true } +smol_str = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } + + + +[dev-dependencies] +# jmf deps +rstest = { version = "0", default-features = false } +# rstest_reuse = "0.3.0" +similar-asserts = "1" + + +[build-dependencies] +# for rapidxml +cxx-build = { version = "1" } diff --git a/crates/joko_package/src/pack/common.rs b/crates/joko_package_models/src/attributes.rs similarity index 92% rename from crates/joko_package/src/pack/common.rs rename to crates/joko_package_models/src/attributes.rs index aea6dff..a7c00bc 100644 --- a/crates/joko_package/src/pack/common.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -4,14 +4,193 @@ use enumflags2::{bitflags, BitFlags}; use glam::Vec3; use itertools::Itertools; use tracing::info; -use xot::Element; +use xot::{Element, NameId, Xot}; -use crate::io::XotAttributeNameIDs; -use super::RelativePath; +use joko_core::RelativePath; use jokoapi::end_point::mounts::Mount; use jokoapi::end_point::races::Race; use smol_str::SmolStr; + + +pub struct XotAttributeNameIDs { + // xml tags + pub overlay_data: NameId, + pub marker_category: NameId, + pub pois: NameId, + pub poi: NameId, + pub trail: NameId, + pub route: NameId, + // marker specific attributes + pub category: NameId, + pub guid: NameId, + pub map_id: NameId, + pub xpos: NameId, + pub ypos: NameId, + pub zpos: NameId, + // marker category specific attributes + pub default_enabled: NameId, + pub display_name: NameId, + pub name: NameId, + pub capital_name: NameId,//same than "name" but with a starting capital letter + pub separator: NameId, + // inheritable attributes + pub achievement_id: NameId, + pub achievement_bit: NameId, + pub alpha: NameId, + pub anim_speed: NameId, + pub auto_trigger: NameId, + pub behavior: NameId, + pub bounce: NameId, + pub bounce_delay: NameId, + pub bounce_duration: NameId, + pub bounce_height: NameId, + pub can_fade: NameId, + pub color: NameId, + pub copy: NameId, + pub copy_message: NameId, + pub cull: NameId, + pub fade_far: NameId, + pub fade_near: NameId, + pub festival: NameId, + pub has_countdown: NameId, + pub height_offset: NameId, + pub hide: NameId, + pub icon_file: NameId, + pub icon_size: NameId, + pub in_game_visibility: NameId, + pub info: NameId, + pub info_range: NameId, + pub invert_behavior: NameId, + pub is_wall: NameId, + pub keep_on_map_edge: NameId, + pub map_display_size: NameId, + pub map_fade_out_scale_level: NameId, + pub map_type: NameId, + pub map_visibility: NameId, + pub max_size: NameId, + pub min_size: NameId, + pub mini_map_visibility: NameId, + pub mount: NameId, + pub profession: NameId, + pub race: NameId, + pub reset_length: NameId, + pub reset_offset: NameId, + pub rotate: NameId, + pub rotate_x: NameId, + pub rotate_y: NameId, + pub rotate_z: NameId, + pub scale_on_map_with_zoom: NameId, + pub show: NameId, + pub specialization: NameId, + pub text: NameId, + pub texture: NameId, + pub tip_name: NameId, + pub tip_description: NameId, + pub title: NameId, + pub title_color: NameId, + pub toggle_category: NameId, + pub trail_data: NameId, + pub trail_scale: NameId, + pub trigger_range: NameId, + pub reset_range: NameId, + pub resetposx: NameId, + pub resetposy: NameId, + pub resetposz: NameId, + pub _source_file_name: NameId, +} +impl XotAttributeNameIDs { + pub fn register_with_xot(tree: &mut Xot) -> Self { + Self { + // tags + overlay_data: tree.add_name("OverlayData"), + marker_category: tree.add_name("MarkerCategory"), + pois: tree.add_name("POIs"), + poi: tree.add_name("POI"), + trail: tree.add_name("Trail"), + route: tree.add_name("Route"), + // non inheritable attributes + category: tree.add_name("type"), + xpos: tree.add_name("xpos"), + ypos: tree.add_name("ypos"), + zpos: tree.add_name("zpos"), + map_id: tree.add_name("MapID"), + guid: tree.add_name("GUID"), + + // marker category specific attrs + separator: tree.add_name("IsSeparator"), + default_enabled: tree.add_name("defaulttoggle"), + display_name: tree.add_name("DisplayName"), + name: tree.add_name("name"), + capital_name: tree.add_name("Name"), + // inheritable attributes + achievement_id: tree.add_name("achievementId"), + achievement_bit: tree.add_name("achievementBit"), + alpha: tree.add_name("alpha"), + anim_speed: tree.add_name("animSpeed"), + auto_trigger: tree.add_name("autotrigger"), + behavior: tree.add_name("behavior"), + color: tree.add_name("color"), + copy: tree.add_name("copy"), + copy_message: tree.add_name("copy-message"), + fade_near: tree.add_name("fadeNear"), + fade_far: tree.add_name("fadeFar"), + festival: tree.add_name("festival"), + has_countdown: tree.add_name("hasCountdown"), + height_offset: tree.add_name("heightOffset"), + icon_file: tree.add_name("iconFile"), + icon_size: tree.add_name("iconSize"), + in_game_visibility: tree.add_name("inGameVisibility"), + info: tree.add_name("info"), + info_range: tree.add_name("infoRange"), + map_display_size: tree.add_name("mapDisplaySize"), + map_visibility: tree.add_name("mapVisibility"), + max_size: tree.add_name("maxSize"), + min_size: tree.add_name("minSize"), + mini_map_visibility: tree.add_name("miniMapVisibility"), + mount: tree.add_name("mount"), + profession: tree.add_name("profession"), + race: tree.add_name("race"), + reset_length: tree.add_name("resetLength"), + reset_offset: tree.add_name("resetOffset"), + scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), + tip_name: tree.add_name("tip-name"), + tip_description: tree.add_name("tip-description"), + toggle_category: tree.add_name("togglecateogry"), + texture: tree.add_name("texture"), + trail_data: tree.add_name("trailData"), + trail_scale: tree.add_name("trailScale"), + trigger_range: tree.add_name("triggerRange"), + bounce_delay: tree.add_name("bounce-delay"), + bounce_duration: tree.add_name("bounce-duration"), + bounce_height: tree.add_name("bounce-height"), + can_fade: tree.add_name("canfade"), + cull: tree.add_name("cull"), + hide: tree.add_name("hide"), + is_wall: tree.add_name("iswall"), + invert_behavior: tree.add_name("invertbehavior"), + map_type: tree.add_name("maptype"), + rotate: tree.add_name("rotate"), + rotate_x: tree.add_name("rotate-x"), + rotate_y: tree.add_name("rotate-y"), + rotate_z: tree.add_name("rotate-z"), + show: tree.add_name("show"), + specialization: tree.add_name("specialization"), + title: tree.add_name("title"), + title_color: tree.add_name("title-color"), + text: tree.add_name("text"), + bounce: tree.add_name("bounce"), + keep_on_map_edge: tree.add_name("keepOnMapEdge"), + map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), + reset_range: tree.add_name("resetrange"), + resetposx: tree.add_name("resetposx"), + resetposy: tree.add_name("resetposy"), + resetposz: tree.add_name("resetposz"), + _source_file_name: tree.add_name("_source_file_name"), + } + } +} + /// This is a onetime macro to reduce code duplication /// It basically takes the CommmonAttributes struct, adds the active_attributes and bool_attributes fields to it. /// Then, it creates a method call `inherit_if_attr_none`, which will clone fields from other struct, if its own fields are not active (set) @@ -482,7 +661,7 @@ impl CommonAttributes { mini_map_visibility, scale_on_map_with_zoom ]); - pub(crate) fn update_common_attributes_from_element( + pub fn update_common_attributes_from_element( &mut self, ele: &Element, names: &XotAttributeNameIDs, @@ -635,7 +814,7 @@ impl CommonAttributes { ); } - pub(crate) fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { + pub fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { // color arrays if self.active_attributes.contains(ActiveAttributes::color) { ele.set_attribute(names.color, data_encoding::HEXLOWER.encode(&self.color)); diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs new file mode 100644 index 0000000..28a5dbc --- /dev/null +++ b/crates/joko_package_models/src/category.rs @@ -0,0 +1,209 @@ +use std::collections::HashSet; + +use ordered_hash_map::OrderedHashMap; +use tracing::debug; +use uuid::Uuid; +use indexmap::IndexMap; +use crate::attributes::CommonAttributes; + +#[derive(Debug, Clone)] +pub struct RawCategory { + pub guid: Uuid, + pub parent_name: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, +} + +#[derive(Debug, Clone)] +pub struct Category { + pub guid: Uuid, + pub parent: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, + pub children: IndexMap, +} + +pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { + let nb_matches = s.matches(pat).count(); + assert!(nb_matches + 1 > n); + let res = s.split(pat) + .nth(n) + ; + debug!("nth_chunk {} {} {:?}", s, n, res); + res.unwrap().to_string() +} + +pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { + let res = s.match_indices(pat) + .nth(n) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_until_nth_char {} {} {:?}", s, n, res); + res +} + +pub fn prefix_parent(s: &str, pat: char) -> Option { + let n = s.matches(pat).count(); + assert!(n > 0); + let res = s.match_indices(pat) + .nth(n - 1) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_parent {} {} {:?}", s, n, res); + res +} + + + +impl Category { + // Required method + pub fn from(value: &RawCategory, parent: Option) -> Self { + Self { + guid: value.guid.clone(), + props: value.props.clone(), + separator: value.separator, + default_enabled: value.default_enabled, + display_name: value.display_name.clone(), + relative_category_name: value.relative_category_name.clone(), + full_category_name: value.full_category_name.clone(), + parent: parent, + children: Default::default() + } + } + pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + for (_, cat) in categories { + if &cat.guid == uuid { + return Some(cat); + } + let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); + if sub_res.is_some() { + return sub_res; + } + } + return None; + } + pub fn reassemble( + input_first_pass_categories: &OrderedHashMap, + late_discovered_categories: &mut HashSet, + ) -> IndexMap { + let mut first_pass_categories = input_first_pass_categories.clone(); + let mut second_pass_categories: OrderedHashMap = Default::default(); + let mut need_a_pass: bool = true; + + let mut third_pass_categories: IndexMap = Default::default(); + let mut third_pass_categories_ref: Vec = Default::default(); + let mut root: IndexMap = Default::default(); + while need_a_pass { + need_a_pass = false; + for (key, value) in first_pass_categories.iter() { + debug!("reassemble_categories {:?}", value); + let mut to_insert = value.clone(); + if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { + let mut n = 0; + let mut last_name: Option = None; + // This is an almost duplication of code of pack/mod.rs + while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { + debug!("{} {}", parent_name, n); + if let Some(parent_category) = first_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { + late_discovered_categories.insert(parent_category.guid); + last_name = Some(parent_name.clone()); + }else{ + let new_uuid = Uuid::new_v4(); + let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); + let to_insert = RawCategory { + default_enabled: value.default_enabled, + guid: new_uuid, + relative_category_name: relative_category_name.clone(), + display_name: relative_category_name.clone(), + parent_name: prefix_until_nth_char(&parent_name, '.', n-1), + props: value.props.clone(), + separator: false, + full_category_name: parent_name.clone() + }; + last_name = Some(to_insert.full_category_name.clone()); + second_pass_categories.insert(parent_name.clone(), to_insert); + late_discovered_categories.insert(new_uuid); + need_a_pass = true; + } + n += 1; + } + late_discovered_categories.insert(value.guid); + to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + to_insert.display_name = to_insert.relative_category_name.clone(); + debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); + assert!(last_name.is_some()); + to_insert.parent_name = last_name; + } else { + to_insert.parent_name = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.full_category_name.clone()) + } else { + None + } + }else { + None + }; + debug!("insert as is {:?}", to_insert); + } + second_pass_categories.insert(key.clone(), to_insert); + } + if need_a_pass { + std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); + second_pass_categories.clear(); + } + } + for (key, value) in second_pass_categories { + let parent = if let Some(parent_name) = &value.parent_name { + if let Some(parent_category) = first_pass_categories.get(parent_name) { + Some(parent_category.guid.clone()) + } else { + None + } + } else { + None + }; + + debug!("{} parent is {:?}", key , parent); + let cat = Category::from(&value, parent); + let ref_uuid = cat.guid.clone(); + if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { + third_pass_categories_ref.push(ref_uuid); + } + } + + for full_category_name in third_pass_categories_ref { + if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { + if let Some(parent) = cat.parent { + if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { + parent_category.children.insert(cat.guid.clone(), cat); + } else { + panic!("Could not find parent {} for {:?}", parent, cat); + } + } else { + root.insert(cat.guid.clone(), cat); + } + } else { + panic!("Some bad logic at works"); + } + } + debug!("reassemble_categories {:?}", root); + root + } + + +} + diff --git a/crates/joko_package_models/src/lib.rs b/crates/joko_package_models/src/lib.rs new file mode 100644 index 0000000..df91d6c --- /dev/null +++ b/crates/joko_package_models/src/lib.rs @@ -0,0 +1,9 @@ + +pub mod attributes; +pub mod category; +pub mod map; +pub mod marker; +pub mod package; +pub mod route; +pub mod trail; + diff --git a/crates/joko_package_models/src/map.rs b/crates/joko_package_models/src/map.rs new file mode 100644 index 0000000..2902c42 --- /dev/null +++ b/crates/joko_package_models/src/map.rs @@ -0,0 +1,13 @@ +use uuid::Uuid; +use indexmap::IndexMap; +use crate::marker::Marker; +use crate::route::Route; +use crate::trail::Trail; + +#[derive(Default, Debug, Clone)] +pub struct MapData { + pub markers: IndexMap, + pub routes: IndexMap, + pub trails: IndexMap, +} + diff --git a/crates/joko_package/src/pack/marker.rs b/crates/joko_package_models/src/marker.rs similarity index 79% rename from crates/joko_package/src/pack/marker.rs rename to crates/joko_package_models/src/marker.rs index 79c6898..5f00ff6 100644 --- a/crates/joko_package/src/pack/marker.rs +++ b/crates/joko_package_models/src/marker.rs @@ -1,10 +1,10 @@ -use super::CommonAttributes; +use crate::attributes::CommonAttributes; use glam::Vec3; use uuid::Uuid; #[derive(Debug, Clone)] -pub(crate) struct Marker { +pub struct Marker { pub guid: Uuid, pub parent: Uuid, pub position: Vec3, diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs new file mode 100644 index 0000000..a8637e5 --- /dev/null +++ b/crates/joko_package_models/src/package.rs @@ -0,0 +1,175 @@ +use joko_core::RelativePath; +use tracing::{debug, trace}; +use uuid::Uuid; +use std::collections::{HashMap, HashSet, BTreeMap}; +use indexmap::IndexMap; +use crate::marker::Marker; +use crate::route::{route_to_tbin, route_to_trail, Route}; +use crate::trail::{TBin, Trail}; +use crate::category::{prefix_until_nth_char, Category}; +use crate::map::MapData; + + +#[derive(Debug, Clone)] +pub struct PackCore { + /* + PackCore is a temporary holder of data + It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. + */ + pub uuid: Uuid, + pub textures: HashMap>, + pub tbins: HashMap, + pub categories: IndexMap, + pub all_categories: HashMap, + pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + pub entities_parents: HashMap, + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, +} + + + +impl PackCore { + + pub fn new() -> Self { + let mut res = Self { + all_categories: Default::default(), + categories: Default::default(), + entities_parents: Default::default(), + late_discovery_categories: Default::default(), + maps: Default::default(), + source_files: Default::default(), + tbins: Default::default(), + textures: Default::default(), + uuid: Default::default(), + }; + res.uuid = Uuid::new_v4(); + res + } + pub fn partial(all_categories: &HashMap) -> Self { + // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. + let mut res: Self = Self::new(); + res.all_categories = all_categories.clone(); + res + } + + pub fn merge_partial(&mut self, partial_pack: PackCore) { + self.maps.extend(partial_pack.maps); + self.all_categories = partial_pack.all_categories; + self.late_discovery_categories.extend(partial_pack.late_discovery_categories); + self.source_files.extend(partial_pack.source_files); + self.tbins.extend(partial_pack.tbins); + self.entities_parents.extend(partial_pack.entities_parents); + } + pub fn category_exists(&self, full_category_name: &String) -> bool { + self.all_categories.contains_key(full_category_name) + } + + pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { + self.all_categories.get(full_category_name) + } + + pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { + if let Some(category_uuid) = self.all_categories.get(full_category_name) { + category_uuid.clone() + } else { + //TODO: if import is "dirty", create missing category + //TODO: default import mode is "strict" (get inspiration from HTML modes) + debug!("There is no defined category for {}", full_category_name); + + let mut n = 0; + let mut last_uuid: Option = None; + while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { + n += 1; + if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { + //FIXME: might want to make the difference between impacted parents and actual missing category + self.late_discovery_categories.insert(*parent_uuid); + last_uuid = Some(*parent_uuid); + } else { + let new_uuid = Uuid::new_v4(); + debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); + self.all_categories.insert(parent_full_category_name.clone(), new_uuid); + self.late_discovery_categories.insert(new_uuid); + last_uuid = Some(new_uuid); + } + } + trace!("{} uuid: {:?}", full_category_name, last_uuid); + assert!(last_uuid.is_some()); + last_uuid.unwrap() + } + } + + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ + if let Some(parent_uuid) = self.all_categories.get(full_category_name) { + let mut uuid_to_insert = uuid.clone(); + while self.entities_parents.contains_key(&uuid_to_insert) { + trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); + uuid_to_insert = Uuid::new_v4(); + } + self.entities_parents.insert(uuid_to_insert, *parent_uuid); + Ok(uuid_to_insert) + } else { + //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. + Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) + } + } + + pub fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; + marker.guid = uuid_to_insert; + if !self.maps.contains_key(&marker.map_id) { + self.maps.insert(marker.map_id, MapData::default()); + } + self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); + Ok(()) + } + + pub fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { + let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; + trail.guid = uuid_to_insert; + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + Ok(()) + } + + pub fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { + let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); + let tbin_path: RelativePath = file_name.parse().unwrap(); + let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; + route.guid = uuid_to_insert; + let trail = route_to_trail(&route, &tbin_path); + let tbin = route_to_tbin(&route); + + self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time + if !self.maps.contains_key(&trail.map_id) { + self.maps.insert(trail.map_id, MapData::default()); + } + self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); + Ok(()) + } + + pub fn register_categories(&mut self) { + let mut entities_parents: HashMap = Default::default(); + let mut all_categories: HashMap = Default::default(); + Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); + self.entities_parents.extend(entities_parents); + self.all_categories = all_categories; + } + fn recursive_register_categories( + entities_parents: &mut HashMap, + categories: &IndexMap, + all_categories: &mut HashMap, + ) { + for (_, cat) in categories.iter() { + debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); + all_categories.insert(cat.full_category_name.clone(), cat.guid); + if let Some(parent) = cat.parent { + entities_parents.insert(cat.guid, parent); + } + Self::recursive_register_categories(entities_parents, &cat.children, all_categories); + } + } +} \ No newline at end of file diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs new file mode 100644 index 0000000..81689a4 --- /dev/null +++ b/crates/joko_package_models/src/route.rs @@ -0,0 +1,48 @@ +use joko_core::RelativePath; +use uuid::Uuid; +use glam::Vec3; + +use crate::{attributes::CommonAttributes, trail::{TBin, Trail}}; + +#[derive(Debug, Clone)] +pub struct Route { + pub category: String, + pub parent: Uuid, + pub path: Vec, + pub reset_position: Vec3, + pub reset_range: f64, + pub map_id: u32, + pub guid: Uuid, + pub name: String, + pub source_file_name: String, +} + + + +pub(crate) fn route_to_tbin(route: &Route) -> TBin { + assert!( route.path.len() > 1); + TBin { + map_id: route.map_id, + version: 0, + nodes: route.path.clone(), + } +} + +pub(crate) fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { + let mut props = CommonAttributes::default(); + props.set_texture(None); + props.set_trail_data(Some(file_path.clone())); + Trail { + map_id: route.map_id, + category: route.category.clone(), + parent: route.parent.clone(), + guid: route.guid, + props: props, + dynamic: true, + source_file_name: route.source_file_name.clone(), + } +} + + + + diff --git a/crates/joko_package/src/pack/trail.rs b/crates/joko_package_models/src/trail.rs similarity index 80% rename from crates/joko_package/src/pack/trail.rs rename to crates/joko_package_models/src/trail.rs index 71908f1..ebca7ba 100644 --- a/crates/joko_package/src/pack/trail.rs +++ b/crates/joko_package_models/src/trail.rs @@ -1,9 +1,9 @@ use uuid::Uuid; -use super::CommonAttributes; +use crate::attributes::CommonAttributes; #[derive(Debug, Clone)] -pub(crate) struct Trail { +pub struct Trail { pub guid: Uuid, pub parent: Uuid, pub map_id: u32, @@ -14,13 +14,13 @@ pub(crate) struct Trail { } #[derive(Debug, Clone)] -pub(crate) struct TBin { +pub struct TBin { pub map_id: u32, pub version: u32, pub nodes: Vec, } #[derive(Debug, Clone)] -pub(crate) struct TBinStatus { +pub struct TBinStatus { pub tbin: TBin, pub iso_x: bool, pub iso_y: bool, diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 28ec05e..d25fc5c 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -84,6 +84,9 @@ impl BillBoardRenderer { } pub fn prepare_render_data(&mut self, gl: &Context) { + //TODO: trim down the trails too far + // fatten them ? + // what about view from above (map view) unsafe { gl_error!(gl); } diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs index 6621559..8eb11d0 100644 --- a/crates/joko_render/src/renderer.rs +++ b/crates/joko_render/src/renderer.rs @@ -113,7 +113,7 @@ impl JokoRenderer { 1.0 } pub fn get_z_far() -> f32 { - 1000.0 + 1000.0 //TODO: should match the distance for marker exclusion } pub fn swap(&mut self) { self.billboard_renderer.swap(); diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 373243c..1e7ae3a 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -9,14 +9,17 @@ use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; mod wm; +mod mumble; use uuid::Uuid; use init::get_jokolay_dir; use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; //use jmf::FileManager; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; +use crate::app::mumble::mumble_gui; + use jmf::message::BackToUIMessage; use joko_render::renderer::JokoRenderer; -use jokolink::{MumbleChanges, MumbleLink, MumbleManager, mumble_gui}; +use jokolink::{MumbleChanges, MumbleLink, MumbleManager}; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span}; use jmf::{LoadedPackData, LoadedPackTexture, build_from_core}; @@ -64,9 +67,11 @@ pub struct Jokolay { impl Jokolay { pub fn new(jokolay_dir: Arc
) -> Result { - //We have two mumble_managers, one for UI, one for backend, each keeping its own copy - //this avoid transmition between threads to read same data from system - //TODO: if we want to be able to edit the link, one need to put a "form submission" logic. + /* + We have two mumble_managers, one for UI, one for backend, each keeping its own copy + this avoid transmition between threads to read same data from system + It happens anyway when the UI start the edit mode of the mumble link. + */ let mumble_data_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; let mumble_ui_manager = @@ -172,6 +177,7 @@ impl Jokolay { UIToBackMessage::ActiveFiles(currently_used_files) => { tracing::trace!("Handling of UIToBackMessage::ActiveFiles"); package_manager.set_currently_used_files(currently_used_files); + local_state.choice_of_category_changed = true; } UIToBackMessage::CategoryActivationElementStatusChange(category_uuid, status) => { tracing::trace!("Handling of UIToBackMessage::CategoryActivationElementStatusChange"); @@ -520,11 +526,8 @@ impl Jokolay { if local_state.editable_mumble { local_state.window_changed = true; local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); - //TODO: at some point update the changes u2b_sender.send(UIToBackMessage::MumbleLink(local_state.link.clone())); - u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); } else { - u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); let is_mumble_alive = mumble_manager.is_alive(); match mumble_manager.tick() { Ok(ml) => { @@ -628,9 +631,7 @@ impl Jokolay { ); if let Some(link) = local_state.link.as_mut() { - //updates need to be sent to the background state - //TODO: editable link need to trigger a map reload - mumble_gui(&etx, &mut menu_panel.show_mumble_manager_window, &mut local_state.editable_mumble, link); + mumble_gui(&u2b_sender, &etx, &mut menu_panel.show_mumble_manager_window, &mut local_state.editable_mumble, link); }; package_manager.gui( &u2b_sender, diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs new file mode 100644 index 0000000..1450a62 --- /dev/null +++ b/crates/jokolay/src/app/mumble.rs @@ -0,0 +1,282 @@ + +use egui; +use egui::DragValue; +use jmf::message::UIToBackMessage; +use jokolink::MumbleLink; + + +pub fn mumble_gui( + u2b_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + open: &mut bool, + editable_mumble: &mut bool, + link: &mut MumbleLink +) { + egui::Window::new("Mumble Manager") + .open(open) + .show(etx, |ui| { + ui.horizontal(|ui| { + if ui.selectable_label(!*editable_mumble, "live").clicked() { + *editable_mumble = false; + u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); + } + if ui.selectable_label(*editable_mumble, "editable").clicked() { + *editable_mumble = true; + u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); + } + }); + if *editable_mumble { + ui.label( + egui::RichText::new("Mumble is not live, values need to be manually updated.") + .color(egui::Color32::RED) + ); + editable_mumble_ui(ui, link); + } else { + let link: MumbleLink = link.clone(); + live_mumble_ui(ui, link); + } + }); +} + +fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.player_pos.x)); + ui.add(DragValue::new(&mut link.player_pos.y)); + ui.add(DragValue::new(&mut link.player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.f_avatar_front.x)); + ui.add(DragValue::new(&mut link.f_avatar_front.y)); + ui.add(DragValue::new(&mut link.f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.cam_pos.x)); + ui.add(DragValue::new(&mut link.cam_pos.y)); + ui.add(DragValue::new(&mut link.cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.f_camera_front.x)); + ui.add(DragValue::new(&mut link.f_camera_front.y)); + ui.add(DragValue::new(&mut link.f_camera_front.z)); + }); + ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut link.compass_height)); + ui.add(DragValue::new(&mut link.compass_width)); + ui.add(DragValue::new(&mut link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = link.client_size.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.horizontal(|ui|{ + ui.label(&link.name); + ui.label(format!("{:?}", link.race)); + }); + ui.end_row(); + + ui.label("map id"); + ui.add(DragValue::new(&mut link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut link.map_type)); + ui.end_row(); + ui.label("world position"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut link.map_center_x)); + ui.add(DragValue::new(&mut link.map_center_y)); + ui.add(DragValue::new(&mut link.map_scale)); + }); + ui.end_row(); + + ui.label("address"); + ui.label(format!("{}", link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.client_pos.x)); + ui.add(DragValue::new(&mut link.client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.client_size.x)); + ui.add(DragValue::new(&mut link.client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut link.dpi)); + ui.end_row(); + }); +} + + +fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut dummy_link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.player_pos.x)); + ui.add(DragValue::new(&mut dummy_link.player_pos.y)); + ui.add(DragValue::new(&mut dummy_link.player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.x)); + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.y)); + ui.add(DragValue::new(&mut dummy_link.f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.cam_pos.x)); + ui.add(DragValue::new(&mut dummy_link.cam_pos.y)); + ui.add(DragValue::new(&mut dummy_link.cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.f_camera_front.x)); + ui.add(DragValue::new(&mut dummy_link.f_camera_front.y)); + ui.add(DragValue::new(&mut dummy_link.f_camera_front.z)); + }); + ui.end_row(); + + ui.label("ui state"); + if let Some(ui_state) = dummy_link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui|{ + ui.add(DragValue::new(&mut dummy_link.compass_height)); + ui.add(DragValue::new(&mut dummy_link.compass_width)); + ui.add(DragValue::new(&mut dummy_link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut dummy_link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = dummy_link.client_size.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.label(&dummy_link.name); + ui.end_row(); + ui.label("map id"); + ui.add(DragValue::new(&mut dummy_link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut dummy_link.map_type)); + ui.end_row(); + ui.label("address"); + ui.label(format!("{}", dummy_link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut dummy_link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut dummy_link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", dummy_link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.client_pos.x)); + ui.add(DragValue::new(&mut dummy_link.client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.client_size.x)); + ui.add(DragValue::new(&mut dummy_link.client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut dummy_link.dpi)); + ui.end_row(); + + // ui.label("position"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos.x)); + // ui.add(DragValue::new(&mut link.window_pos.y)); + // }); + // ui.end_row(); + // ui.label("size"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size.x)); + // ui.add(DragValue::new(&mut link.window_size.y)); + // }); + // ui.end_row(); + // ui.label("position_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); + // }); + // ui.end_row(); + // ui.label("size_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); + // }); + // ui.end_row(); + }); +} diff --git a/crates/jokolink/Cargo.toml b/crates/jokolink/Cargo.toml index 304101d..99e9e95 100644 --- a/crates/jokolink/Cargo.toml +++ b/crates/jokolink/Cargo.toml @@ -16,7 +16,6 @@ enumflags2 = { workspace = true } time = { workspace = true } miette = { workspace = true } tracing = { workspace = true } -egui = { workspace = true } serde = { workspace = true } glam = { workspace = true } serde_json = { workspace = true } diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index ff4ccef..c3b9dad 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -9,7 +9,6 @@ //! mod mumble; -use egui::DragValue; use enumflags2::BitFlags; use glam::IVec2; //use jokoapi::end_point::{mounts::Mount, races::Race}; @@ -183,268 +182,3 @@ impl MumbleManager { } } -pub fn mumble_gui(etx: &egui::Context, open: &mut bool, editable_mumble: &mut bool, link: &mut MumbleLink) { - egui::Window::new("Mumble Manager") - .open(open) - .show(etx, |ui| { - if *editable_mumble { - if ui.button("back to live").clicked() { - *editable_mumble = false; - } - ui.label( - egui::RichText::new("Mumble is not initialized, display dummy link instead.") - .color(egui::Color32::RED) - ); - editable_mumble_ui(ui, link); - } else { - if ui.button("go to edit mode").clicked() { - *editable_mumble = true; - } - let link: MumbleLink = link.clone(); - mumble_ui(ui, link); - } - }); -} - -fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { - egui::Grid::new("link grid") - .num_columns(2) - .striped(true) - .show(ui, |ui| { - ui.label("ui tick"); - ui.add(DragValue::new(&mut link.ui_tick)); - ui.end_row(); - ui.label("player position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.player_pos.x)); - ui.add(DragValue::new(&mut link.player_pos.y)); - ui.add(DragValue::new(&mut link.player_pos.z)); - }); - ui.end_row(); - ui.label("player direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_avatar_front.x)); - ui.add(DragValue::new(&mut link.f_avatar_front.y)); - ui.add(DragValue::new(&mut link.f_avatar_front.z)); - }); - ui.end_row(); - ui.label("camera position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.cam_pos.x)); - ui.add(DragValue::new(&mut link.cam_pos.y)); - ui.add(DragValue::new(&mut link.cam_pos.z)); - }); - ui.end_row(); - ui.label("camera direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_camera_front.x)); - ui.add(DragValue::new(&mut link.f_camera_front.y)); - ui.add(DragValue::new(&mut link.f_camera_front.z)); - }); - ui.end_row(); - ui.label("ui state"); - if let Some(ui_state) = link.ui_state { - ui.label(ui_state.to_string()); - } else { - ui.label("None"); - } - - ui.end_row(); - ui.label("compass"); - ui.horizontal(|ui|{ - ui.add(DragValue::new(&mut link.compass_height)); - ui.add(DragValue::new(&mut link.compass_width)); - ui.add(DragValue::new(&mut link.compass_rotation)); - }); - ui.end_row(); - - ui.label("fov"); - ui.add(DragValue::new(&mut link.fov)); - ui.end_row(); - ui.label("w/h ratio"); - let ratio = link.client_size.as_vec2(); - let mut ratio = ratio.x / ratio.y; - ui.add(DragValue::new(&mut ratio)); - ui.end_row(); - ui.label("character"); - ui.horizontal(|ui|{ - ui.label(&link.name); - ui.label(format!("{:?}", link.race)); - }); - ui.end_row(); - - ui.label("map id"); - ui.add(DragValue::new(&mut link.map_id)); - ui.end_row(); - ui.label("map type"); - ui.add(DragValue::new(&mut link.map_type)); - ui.end_row(); - ui.label("world position"); - ui.horizontal(|ui|{ - ui.add(DragValue::new(&mut link.map_center_x)); - ui.add(DragValue::new(&mut link.map_center_y)); - ui.add(DragValue::new(&mut link.map_scale)); - }); - ui.end_row(); - - ui.label("address"); - ui.label(format!("{}", link.server_address)); - ui.end_row(); - ui.label("instance"); - ui.add(DragValue::new(&mut link.instance)); - ui.end_row(); - ui.label("shard id"); - ui.add(DragValue::new(&mut link.shard_id)); - ui.end_row(); - ui.label("mount"); - ui.label(format!("{:?}", link.mount)); - ui.end_row(); - ui.label("client pos"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_pos.x)); - ui.add(DragValue::new(&mut link.client_pos.y)); - }); - ui.end_row(); - ui.label("client size"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_size.x)); - ui.add(DragValue::new(&mut link.client_size.y)); - }); - ui.end_row(); - ui.label("dpi scaling"); - ui.add(DragValue::new(&mut link.dpi_scaling)); - ui.end_row(); - ui.label("dpi"); - ui.add(DragValue::new(&mut link.dpi)); - ui.end_row(); - }); -} - - -fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { - egui::Grid::new("link grid") - .num_columns(2) - .striped(true) - .show(ui, |ui| { - ui.label("ui tick"); - ui.add(DragValue::new(&mut dummy_link.ui_tick)); - ui.end_row(); - ui.label("player position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.player_pos.x)); - ui.add(DragValue::new(&mut dummy_link.player_pos.y)); - ui.add(DragValue::new(&mut dummy_link.player_pos.z)); - }); - ui.end_row(); - ui.label("player direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.x)); - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.y)); - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.z)); - }); - ui.end_row(); - ui.label("camera position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.cam_pos.x)); - ui.add(DragValue::new(&mut dummy_link.cam_pos.y)); - ui.add(DragValue::new(&mut dummy_link.cam_pos.z)); - }); - ui.end_row(); - ui.label("camera direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.f_camera_front.x)); - ui.add(DragValue::new(&mut dummy_link.f_camera_front.y)); - ui.add(DragValue::new(&mut dummy_link.f_camera_front.z)); - }); - ui.end_row(); - - ui.label("ui state"); - if let Some(ui_state) = dummy_link.ui_state { - ui.label(ui_state.to_string()); - } else { - ui.label("None"); - } - - ui.end_row(); - ui.label("compass"); - ui.horizontal(|ui|{ - ui.add(DragValue::new(&mut dummy_link.compass_height)); - ui.add(DragValue::new(&mut dummy_link.compass_width)); - ui.add(DragValue::new(&mut dummy_link.compass_rotation)); - }); - ui.end_row(); - - ui.label("fov"); - ui.add(DragValue::new(&mut dummy_link.fov)); - ui.end_row(); - ui.label("w/h ratio"); - let ratio = dummy_link.client_size.as_vec2(); - let mut ratio = ratio.x / ratio.y; - ui.add(DragValue::new(&mut ratio)); - ui.end_row(); - ui.label("character"); - ui.label(&dummy_link.name); - ui.end_row(); - ui.label("map id"); - ui.add(DragValue::new(&mut dummy_link.map_id)); - ui.end_row(); - ui.label("map type"); - ui.add(DragValue::new(&mut dummy_link.map_type)); - ui.end_row(); - ui.label("address"); - ui.label(format!("{}", dummy_link.server_address)); - ui.end_row(); - ui.label("instance"); - ui.add(DragValue::new(&mut dummy_link.instance)); - ui.end_row(); - ui.label("shard id"); - ui.add(DragValue::new(&mut dummy_link.shard_id)); - ui.end_row(); - ui.label("mount"); - ui.label(format!("{:?}", dummy_link.mount)); - ui.end_row(); - ui.label("client pos"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.client_pos.x)); - ui.add(DragValue::new(&mut dummy_link.client_pos.y)); - }); - ui.end_row(); - ui.label("client size"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.client_size.x)); - ui.add(DragValue::new(&mut dummy_link.client_size.y)); - }); - ui.end_row(); - ui.label("dpi scaling"); - ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); - ui.end_row(); - ui.label("dpi"); - ui.add(DragValue::new(&mut dummy_link.dpi)); - ui.end_row(); - - // ui.label("position"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_pos.x)); - // ui.add(DragValue::new(&mut link.window_pos.y)); - // }); - // ui.end_row(); - // ui.label("size"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_size.x)); - // ui.add(DragValue::new(&mut link.window_size.y)); - // }); - // ui.end_row(); - // ui.label("position_nb"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); - // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); - // }); - // ui.end_row(); - // ui.label("size_nb"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); - // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); - // }); - // ui.end_row(); - }); -} From 8351c00bfe98d48d1660baa0cf27a3d3bb777466 Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 13 Apr 2024 23:29:31 +0200 Subject: [PATCH 26/54] add package import report (quality, statistics, telemetry) --- crates/joko_core/src/lib.rs | 20 +- crates/joko_package/src/io/deserialize.rs | 206 +++++++++------ crates/joko_package/src/io/export.rs | 235 ++++++++++++++++++ crates/joko_package/src/io/mod.rs | 3 +- crates/joko_package/src/io/serialize.rs | 4 +- .../joko_package/src/manager/pack/active.rs | 4 +- .../src/manager/pack/category_selection.rs | 27 +- .../joko_package/src/manager/pack/loaded.rs | 104 +++++--- crates/joko_package/src/manager/package.rs | 70 +++--- crates/joko_package/src/message.rs | 5 +- crates/joko_package_models/src/category.rs | 60 +++-- crates/joko_package_models/src/package.rs | 199 ++++++++++++++- crates/jokolay/src/app/mod.rs | 112 ++++++--- crates/jokolay/src/app/mumble.rs | 6 +- crates/jokolink/src/lib.rs | 4 +- 15 files changed, 826 insertions(+), 233 deletions(-) create mode 100644 crates/joko_package/src/io/export.rs diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 60e0737..e5bf55d 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -25,22 +25,28 @@ pub mod task; pub struct RelativePath(SmolStr); #[allow(unused)] impl RelativePath { + pub fn normalize(path: &str) -> String { + let normalized_slash = path.replace("\\", "/"); + let trimmed_path = normalized_slash.trim_start_matches('/'); + let lower_case = trimmed_path.to_lowercase(); + lower_case + } + pub fn join_str(&self, path: &str) -> Self { - let path = path.trim_start_matches('/'); - if path.is_empty() { + let normalized_path = RelativePath::normalize(path); + if normalized_path.is_empty() { return Self(self.0.clone()); } - let lower_case = path.to_lowercase(); if self.0.is_empty() { // no need to push `/` if we are empty, as that would make it an absolute path - return Self(lower_case.into()); + return Self(normalized_path.into()); } let mut new = self.0.to_string(); if !self.0.ends_with('/') { new.push('/'); } - new.push_str(&lower_case); + new.push_str(&normalized_path); Self(new.into()) } @@ -86,11 +92,11 @@ impl FromStr for RelativePath { type Err = &'static str; fn from_str(s: &str) -> Result { - let path = s.trim_start_matches('/'); + let path = RelativePath::normalize(s); if path.is_empty() { return Ok(Self::default()); } - Ok(Self(path.to_lowercase().into())) + Ok(Self(path.into())) } } diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index c54156b..9f8b766 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -2,13 +2,10 @@ use joko_core::RelativePath; use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, map::MapData, marker::Marker, package::PackCore, route::Route, trail::{TBin, TBinStatus, Trail}}; use miette::{bail, Context, IntoDiagnostic, Result}; -use crate::{ - BASE64_ENGINE, -}; +use crate::BASE64_ENGINE; use base64::Engine; use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; -use indexmap::IndexMap; use std::{collections::{VecDeque, HashMap}, io::Read}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, info, info_span, instrument, trace, warn}; @@ -40,7 +37,8 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { let parse_categories_file_start = std::time::SystemTime::now(); parse_categories_file(&categories_file, &cats_xml, &mut core_pack) .wrap_err("failed to parse category file")?; - info!("parse_categories_file took {} ms", parse_categories_file_start.elapsed().unwrap_or_default().as_millis()); + let elapsed = parse_categories_file_start.elapsed().unwrap_or_default(); + info!("parse_categories_file took {} ms", elapsed.as_millis()); // parse map data of the pack for entry in dir @@ -219,10 +217,10 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { let distance_to_next_point = a.distance_squared(*b); let mut current_cursor = distance_to_next_point; - while current_cursor > 400.0 { + while current_cursor > 1600.0 { let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); resulting_nodes.push(c); - current_cursor -= 400.0; + current_cursor -= 1600.0; } } resulting_nodes.push(*b); @@ -262,24 +260,27 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { } fn parse_categories( + pack: &mut PackCore, tree: &Xot, tags: impl Iterator, first_pass_categories: &mut OrderedHashMap, names: &XotAttributeNameIDs, + source_file_name: &String, ) { //called once per file - parse_categories_recursive(tree, tags, first_pass_categories, names, None); - + parse_categories_recursive(pack, tree, tags, first_pass_categories, names, None, source_file_name) } // a recursive function to parse the marker category tree. fn parse_categories_recursive( + pack: &mut PackCore, tree: &Xot, tags: impl Iterator, first_pass_categories: &mut OrderedHashMap, names: &XotAttributeNameIDs, parent_name: Option, + source_file_name: &String, ) { for tag in tags { let ele = match tree.element(tag) { @@ -298,9 +299,8 @@ fn parse_categories_recursive( if name.is_empty() { continue; } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(ele, names); let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); let separator = ele @@ -316,14 +316,23 @@ fn parse_categories_recursive( .parse() .map(|u: u8| u != 0) .unwrap_or(true); - let guid = parse_guid(names, ele); let full_category_name: String = if let Some(parent_name) = &parent_name { format!("{}.{}", parent_name, name) } else { name.to_string() }; + let guid = parse_guid(names, ele); trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); if !first_pass_categories.contains_key(&full_category_name) { + let mut sources: OrderedHashMap = OrderedHashMap::new(); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_inherited_texture(icon_file.as_str().to_string(), full_category_name.clone(), source_file_name); + } + } + + sources.insert(guid.clone(), source_file_name.clone()); first_pass_categories.insert(full_category_name.clone(), RawCategory { guid, parent_name: parent_name.clone(), @@ -332,15 +341,18 @@ fn parse_categories_recursive( full_category_name: full_category_name.clone(), separator, default_enabled, - props: ca, + props: common_attributes, + sources, }); } parse_categories_recursive( + pack, tree, tree.children(tag), first_pass_categories, names, Some(full_category_name), + source_file_name ); } } @@ -359,7 +371,7 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack .wrap_err("no doc element")?; if let Some(od) = tree.element(overlay_data_node) { - let mut categories: IndexMap = Default::default(); + let mut categories: OrderedHashMap = Default::default(); if od.name() == xot_names.overlay_data { parse_category_categories_xml_recursive( &file_name, @@ -507,7 +519,7 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); - target.register_uuid(&full_category_name, &guid); + target.register_uuid(&full_category_name, &guid)?; let marker = Marker { position: [xpos, ypos, zpos].into(), map_id, @@ -535,7 +547,7 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); - target.register_uuid(&full_category_name, &guid); + target.register_uuid(&full_category_name, &guid)?; let trail = Trail { category: full_category_name, parent: category_uuid.clone(), @@ -564,7 +576,7 @@ fn parse_category_categories_xml_recursive( tree: &Xot, tags: impl Iterator, pack: &mut PackCore, - cats: &mut IndexMap, + cats: &mut OrderedHashMap, names: &XotAttributeNameIDs, parent_uuid: Option, parent_name: Option, @@ -627,9 +639,11 @@ fn parse_category_categories_xml_recursive( Some(full_category_name), ); } else { - let current_category = cats - .entry(guid) - .or_insert_with(|| Category { + + let current_category = if let Some(c) = cats.get_mut(&guid) { + c + } else { + let c = Category { guid, parent: parent_uuid.clone(), display_name: display_name.to_string(), @@ -639,7 +653,10 @@ fn parse_category_categories_xml_recursive( default_enabled, props: ca, children: Default::default(), - }); + }; + cats.insert(guid, c); + cats.back_mut().unwrap() + }; parse_category_categories_xml_recursive( file_name, tree, @@ -697,16 +714,15 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } } xmls.sort();//build back the intended order in folder, since zip_archive may not give the files in order. - let start = std::time::SystemTime::now(); + let start_texture_loading = std::time::SystemTime::now(); for name in images { let span = info_span!("load image", name).entered(); - let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); + let file_path: RelativePath = name.parse().unwrap(); if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { - Ok(_) => assert!( - pack.textures.insert(file_path.clone(), bytes).is_none(), - "duplicate image file {name}" - ), + Ok(_) => { + pack.register_texture(name, &file_path, bytes); + }, Err(e) => { info!(?e, "failed to parse image file"); } @@ -718,7 +734,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { for name in tbins { let span = info_span!("load tbin {name}").entered(); - let file_path: RelativePath = name.replace("\\", "/").parse().unwrap(); + let file_path: RelativePath = name.parse().unwrap(); if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { if let Some(tbs) = parse_tbin_from_slice(&bytes) { let is_closed: bool = tbs.closed; @@ -739,11 +755,12 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } std::mem::drop(span); } - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of taco package textures from disk took {} ms", elaspsed.as_millis()); + let elapsed_texture_loading = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.texture_loading = elapsed_texture_loading.as_millis(); + tracing::info!("Loading of taco package textures from disk took {} ms", elapsed_texture_loading.as_millis()); let span_guard_categories = info_span!("deserialize xml: categories").entered(); - + let start_categories_loading = std::time::SystemTime::now(); //first pass: categories only let span_guard_first_pass = info_span!("deserialize xml first pass: load MarkerCategory").entered(); let mut first_pass_categories: OrderedHashMap = Default::default(); @@ -782,13 +799,16 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } }; - parse_categories(&tree, tree.children(od), &mut first_pass_categories, &names); + parse_categories(&mut pack, &tree, tree.children(od), &mut first_pass_categories, &names, &source_file_name); drop(span_guard); } span_guard_first_pass.exit(); + let elaspsed_first_pass = start_categories_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_first_pass = elaspsed_first_pass.as_millis(); //second pass: orphan categories let span_guard_second_pass = info_span!("deserialize xml second pass: orphan categories").entered(); + let start_categories_loading_second_pass = std::time::SystemTime::now(); for source_file_name in xmls.iter() { let mut xml_str = String::new(); let span_guard = info_span!("deserialize xml second pass: load file", source_file_name).entered(); @@ -819,7 +839,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { { Some(od) => od, None => { - info!("missing overlay data tag"); + debug!("missing overlay data tag"); continue; } }; @@ -830,7 +850,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }) { Some(pois) => pois, None => { - info!("missing pois tag"); + debug!("missing pois tag"); continue; } }; @@ -859,8 +879,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { continue; } } + let guid = parse_guid(&names, child_element); if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { let category_uuid = Uuid::new_v4(); + let mut sources: OrderedHashMap = OrderedHashMap::new(); + sources.insert(guid.clone(), source_file_name.clone()); first_pass_categories.insert(full_category_name.clone(), RawCategory{ default_enabled: true, guid: category_uuid, @@ -869,20 +892,43 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { full_category_name: full_category_name.clone(), relative_category_name: full_category_name.clone(), props: Default::default(), - separator: false + separator: false, + sources }); - info!("There is an orphan missing category '{}' which was created", full_category_name); + debug!("There is an orphan missing category '{}' which was created", full_category_name); + } else { + let cat = first_pass_categories.get_mut(&full_category_name); + cat.unwrap().sources.insert(guid.clone(), source_file_name.clone()); } } drop(span_guard); } span_guard_second_pass.exit(); - pack.categories = Category::reassemble(&first_pass_categories, &mut pack.late_discovery_categories); + let elaspsed_second_pass = start_categories_loading_second_pass.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_second_pass = elaspsed_second_pass.as_millis(); + + let start_categories_reassemble = std::time::SystemTime::now(); + pack.categories = Category::reassemble(&first_pass_categories, &mut pack.report); + let elaspsed_reassemble = start_categories_reassemble.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_reassemble.total = elaspsed_reassemble.as_millis(); + + let start_categories_registering = std::time::SystemTime::now(); pack.register_categories(); + let elaspsed_categories_registering = start_categories_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_registering = elaspsed_categories_registering.as_millis(); + + let elaspsed = start_categories_loading.elapsed().unwrap_or_default(); + tracing::info!("Loading of taco package categories from disk took {} ms, {} + {} + {}", + elaspsed.as_millis(), + elaspsed_first_pass.as_millis(), + elaspsed_second_pass.as_millis(), + elaspsed_reassemble.as_millis(), + ); //third and last pass: elements let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); + let start_elements_registering = std::time::SystemTime::now(); for source_file_name in xmls.iter() { let mut xml_str = String::new(); let span_guard = info_span!("deserialize xml third pass load file ", source_file_name).entered(); @@ -925,7 +971,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { }) { Some(pois) => pois, None => { - info!("missing pois tag"); + debug!("missing POIs tag"); continue; } }; @@ -958,15 +1004,16 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { if ! pack.category_exists(&full_category_name) { panic!("Missing category {}, previous pass should have taken care of this", full_category_name); } - let category_uuid = pack.get_or_create_category_uuid(&full_category_name); + let guid = parse_guid(&names, child_element); + let category_uuid = pack.get_or_create_category_uuid(&full_category_name, guid, source_file_name); if child_element.name() == names.poi { - if let Some(marker) = parse_marker(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + if let Some(marker) = parse_marker(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_name.clone()) { pack.register_marker(full_category_name, marker)?; } else { debug!("Could not parse POI"); } } else if child_element.name() == names.trail { - if let Some(trail) = parse_trail(&mut pack, &names, child_element, &full_category_name, &category_uuid, source_file_name.clone()) { + if let Some(trail) = parse_trail(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_name.clone()) { pack.register_trail(full_category_name, trail)?; } else { debug!("Could not parse Trail"); @@ -981,6 +1028,11 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } span_guard_third_pass.exit(); span_guard_categories.exit(); + let elaspsed_elements_registering = start_elements_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.elements_registering = elaspsed_elements_registering.as_millis(); + + let elapsed_import = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.total = elapsed_import.as_millis(); Ok(pack) } @@ -1004,7 +1056,19 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(poi_element, &names); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_element_texture(icon_file.as_str().to_string(), guid, &source_file_name); + } + } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { + debug!(icf, "marker's icon file attribute failed to parse"); + pack.found_missing_element_texture(icf.to_string(), guid, &source_file_name); + } + if let Some(map_id) = poi_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) @@ -1024,26 +1088,17 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & .unwrap_or_default() .parse::() .unwrap_or_default(); - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(poi_element, &names); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - info!(%icon_file, "failed to find this texture in this pack"); - } - } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { - info!(icf, "marker's icon file attribute failed to parse"); - } Some(Marker { position: [xpos, ypos, zpos].into(), map_id, category: category_name.clone(), parent: category_uuid.clone(), attrs: common_attributes, - guid: parse_guid(names, poi_element), + guid, source_file_name }) } else { - info!("missing map id"); + debug!("missing map id"); None } } @@ -1180,38 +1235,49 @@ fn parse_route( } -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html + + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(trail_element, &names); + + if let Some(tex) = common_attributes.get_texture() { + if !pack.textures.contains_key(tex) { + info!(%tex, "failed to find this texture in this pack"); + pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_name); + } + } + if let Some(map_id) = trail_element .get_attribute(names.trail_data) .and_then(|trail_data| { - let path: RelativePath = trail_data.parse().unwrap(); - pack.tbins.get(&path).map(|tb| tb.map_id) + //fix the path which may be a mix of windows and linux path + let file_path: RelativePath = trail_data.parse().unwrap(); + if let Some(tb) = pack.tbins.get(&file_path) { + Some(tb.map_id) + }else { + pack.found_missing_trail(&file_path, guid, &source_file_name); + None + } }) { - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(trail_element, &names); - - if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) { - info!(%tex, "failed to find this texture in this pack"); - } - } Some(Trail { category: category_name.clone(), parent: category_uuid.clone(), map_id, props: common_attributes, - guid: parse_guid(names, trail_element), + guid, dynamic: false, source_file_name, }) } else { - let td = trail_element.get_attribute(names.trail_data); - let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); - info!("missing map_id: {td:?} {rp} {tbin:?}"); + /*let td = trail_element.get_attribute(names.trail_data); + let file_path: RelativePath = td.unwrap_or_default().parse().unwrap(); + //pack.report.found_orphan_trail(&file_path, guid, &source_file_name); + let tbin = pack.tbins.get(&file_path).map(|tbin| (tbin.map_id, tbin.version)); + info!("missing map_id: {td:?} {file_path} {tbin:?}"); + */ None } diff --git a/crates/joko_package/src/io/export.rs b/crates/joko_package/src/io/export.rs new file mode 100644 index 0000000..922189b --- /dev/null +++ b/crates/joko_package/src/io/export.rs @@ -0,0 +1,235 @@ +use crate::{ + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::Dir; +use joko_package_models::{attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, route::Route, trail::Trail}; +use miette::{Context, IntoDiagnostic, Result}; +use ordered_hash_map::OrderedHashMap; +use std::io::Write; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +pub(crate) fn export_package_v2( + pack: &PackCore, + writing_directory: &Dir, + name: String, +) -> Result<()> { + Ok(()) +} + +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn export_package_v1( + pack_data: &LoadedPackData, + pack_textures: &LoadedPackData, + writing_directory: &Dir, +) -> Result<()> { + // save categories + info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create new root with overlay data node")?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) + .wrap_err("failed to serialize cats")?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to convert cats xot to string")?; + writing_directory.create("categories.xml") + .into_diagnostic() + .wrap_err("failed to create categories.xml")? + .write_all(cats.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write to categories.xml")?; + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create root wiht overlay data for pois")?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .into_diagnostic() + .wrap_err("faild to append pois to od node")?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .into_diagnostic() + .wrap_err("failed to append poi (marker) to pois")?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .into_diagnostic() + .wrap_err("failed to append a trail node to pois")?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to serialize map data to string")?; + writing_directory.create(format!("{map_id}.xml")) + .into_diagnostic() + .wrap_err("failed to create map xml file")? + .write_all(map_xml.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write map data to file")?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<()> { + + info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + writing_directory.create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir for an image: {img_path}") + })?; + } + writing_directory.create(img_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .write(img) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to write image bytes to file: {img_path}") + })?; + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + writing_directory.create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir of tbin: {tbin_path}") + })?; + } + let mut bytes: Vec = vec![]; + bytes.reserve(8 + tbin.nodes.len() * 12); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + writing_directory.create(tbin_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .write_all(&bytes) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &OrderedHashMap, + parent: Node, +) -> Result<()> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node).into_diagnostic()?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute(names._source_file_name, &trail.source_file_name); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.xpos, format!("{}", marker.position[0])); + ele.set_attribute(names.ypos, format!("{}", marker.position[1])); + ele.set_attribute(names.zpos, format!("{}", marker.position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute(names._source_file_name, &marker.source_file_name); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .into_diagnostic() + .wrap_err("failed to append route to pois")?; + let ele = tree.element_mut(route_node).unwrap(); + + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute(names._source_file_name, &route.source_file_name); + for pos in &route.path { + let child = tree.new_element(names.poi); + tree.append(route_node, child); + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} + diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs index 6db6a1f..4b8fca7 100644 --- a/crates/joko_package/src/io/mod.rs +++ b/crates/joko_package/src/io/mod.rs @@ -1,11 +1,10 @@ //! This modules primarily deals with serializing and deserializing xml data from marker packs //! -use xot::{NameId, Xot}; - mod deserialize; mod error; mod serialize; +mod export; pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index e3f920a..08cf09e 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -4,9 +4,9 @@ use crate::{ }; use base64::Engine; use cap_std::fs_utf8::Dir; -use indexmap::IndexMap; use joko_package_models::{attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail}; use miette::{Context, IntoDiagnostic, Result}; +use ordered_hash_map::OrderedHashMap; use std::io::Write; use tracing::info; use uuid::Uuid; @@ -151,7 +151,7 @@ pub(crate) fn save_pack_texture_to_dir( fn recursive_cat_serializer( tree: &mut Xot, names: &XotAttributeNameIDs, - cats: &IndexMap, + cats: &OrderedHashMap, parent: Node, ) -> Result<()> { for (_, cat) in cats { diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs index 4c1ad22..a1b8353 100644 --- a/crates/joko_package/src/manager/pack/active.rs +++ b/crates/joko_package/src/manager/pack/active.rs @@ -8,9 +8,7 @@ use indexmap::IndexMap; use uuid::Uuid; use joko_core::RelativePath; -use crate::{ - INCHES_PER_METER, -}; +use crate::INCHES_PER_METER; use jokolink::MumbleLink; use joko_render_models::{ marker::{MarkerObject, MarkerVertex}, diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs index e01a2a3..2be0436 100644 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -1,14 +1,11 @@ use std::collections::{HashSet, HashMap}; -use joko_package_models::{attributes::CommonAttributes, category::Category, package::PackCore}; +use joko_package_models::{attributes::CommonAttributes, category::Category, package::{PackageImportReport, PackCore}}; use ordered_hash_map::OrderedHashMap; -use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{ - message::{UIToBackMessage, UIToUIMessage} -}; -use serde::{Deserialize, Serialize}; +use crate::message::{UIToBackMessage, UIToUIMessage}; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { @@ -30,7 +27,7 @@ pub struct SelectedCategoryManager { impl<'a> SelectedCategoryManager { pub fn new( selected_categories: &OrderedHashMap, - categories: &IndexMap + categories: &OrderedHashMap ) -> Self { let mut list_of_enabled_categories = Default::default(); CategorySelection::get_list_of_enabled_categories( @@ -69,7 +66,7 @@ impl CategorySelection { } fn get_list_of_enabled_categories( selection: &OrderedHashMap, - categories: &IndexMap, + categories: &OrderedHashMap, list_of_enabled_categories: &mut OrderedHashMap, parent_common_attributes: &CommonAttributes, ) { @@ -124,7 +121,7 @@ impl CategorySelection { } fn recursive_create_selectable_categories( selectable_categories: &mut OrderedHashMap, - cats: &IndexMap, + cats: &OrderedHashMap, ) { for (_, cat) in cats.iter() { if !selectable_categories.contains_key(&cat.relative_category_name) { @@ -200,13 +197,13 @@ impl CategorySelection { if ui.button("Activate branch").clicked() { cs.is_selected = true; CategorySelection::recursive_set_all(&mut cs.children, true); - u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); ui.close_menu(); } if ui.button("Deactivate branch").clicked() { CategorySelection::recursive_set_all(&mut cs.children, false); cs.is_selected = false; - u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); ui.close_menu(); } } @@ -218,7 +215,7 @@ impl CategorySelection { ui: &mut egui::Ui, is_dirty: &mut bool, show_only_active: bool, - late_discovery_categories: &HashSet, + import_quality_report: &PackageImportReport, ) { if selection.is_empty() { return; @@ -234,12 +231,12 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { - u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); *is_dirty = true; } } //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); - let color = if late_discovery_categories.contains(&cat.uuid) { + let color = if import_quality_report.is_category_discovered_late(cat.uuid) { egui::Color32::LIGHT_RED } else if cat.is_active { egui::Color32::LIGHT_GREEN @@ -258,7 +255,7 @@ impl CategorySelection { ui, is_dirty, show_only_active, - late_discovery_categories + import_quality_report ); }).response.context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); } diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index fdf6727..5f4c76f 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -2,14 +2,13 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, sync::Arc }; -use indexmap::IndexMap; -use joko_package_models::{attributes::{Behavior, CommonAttributes}, category::Category, map::MapData, package::PackCore, trail::TBin}; +use joko_package_models::{attributes::{Behavior, CommonAttributes}, category::Category, map::MapData, package::{PackageImportReport, PackCore}, trail::TBin}; use ordered_hash_map::OrderedHashMap; use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; use image::EncodableLayout; -use tracing::{debug, error, info, info_span}; +use tracing::{debug, error, info, info_span, trace}; use uuid::Uuid; use crate::{ @@ -30,14 +29,19 @@ use crate::manager::pack::category_selection::CategorySelection; use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; +type ImportAllTriplet = (BTreeMap, BTreeMap, BTreeMap); +type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); + //TODO: separate in front and back tasks pub (crate) struct PackTasks { //an object that can handle such tasks should be passed as argument of any function that may required an async action save_texture_task: AsyncTask>, save_data_task: AsyncTask>, - load_all_packs_task: AsyncTask, Result<(BTreeMap, BTreeMap)>> + save_report_task: AsyncTask<(Arc, PackageImportReport), Result<()>>, + load_all_packs_task: AsyncTask, Result> } +//TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? #[derive(Clone)] pub struct LoadedPackData { pub name: String, @@ -45,7 +49,7 @@ pub struct LoadedPackData { pub dir: Arc, /// The actual xml pack. //pub core: PackCore, - pub categories: IndexMap, + pub categories: OrderedHashMap, pub all_categories: HashMap, pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, @@ -76,7 +80,7 @@ pub struct LoadedPackTexture { current_map_data: CurrentMapData, activation_data: ActivationData, active_elements: HashSet,//which are the active elements (loaded) - pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + //pub report: ImportQualityReport,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. _is_dirty: bool, } @@ -85,6 +89,7 @@ impl PackTasks { Self { save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + save_report_task: AsyncTaskGuard::new(PackTasks::async_save_report), load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), } } @@ -102,7 +107,7 @@ impl PackTasks { pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { if status { std::mem::take(&mut texture_pack._is_dirty); - self.save_texture_task.lock().unwrap().send( + let _ = self.save_texture_task.lock().unwrap().send( texture_pack.clone() ); } @@ -111,17 +116,24 @@ impl PackTasks { pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { if status { std::mem::take(&mut data_pack._is_dirty); - self.save_data_task.lock().unwrap().send( + let _ = self.save_data_task.lock().unwrap().send( data_pack.clone() ); } } + pub fn save_report(&self, target_dir: Arc, report: PackageImportReport, status: bool) { + if status { + let _ = self.save_report_task.lock().unwrap().send( + (target_dir, report) + ); + } + } pub fn load_all_packs(&self, jokolay_dir: Arc) { - self.load_all_packs_task.lock().unwrap().send( + let _ = self.load_all_packs_task.lock().unwrap().send( jokolay_dir ); } - pub fn wait_for_load_all_packs(&self) -> Result<(BTreeMap, BTreeMap)> { + pub fn wait_for_load_all_packs(&self) -> Result { self.load_all_packs_task.lock().unwrap().recv().unwrap() } @@ -139,7 +151,7 @@ impl PackTasks { fn async_save_texture( pack_texture: LoadedPackTexture ) -> Result<()> { - info!("Save texture package {:?}", pack_texture.dir); + trace!("Save texture package {:?}", pack_texture.dir); match serde_json::to_string_pretty(&pack_texture.selectable_categories) { Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -177,7 +189,7 @@ impl PackTasks { fn async_save_data( pack_data: LoadedPackData ) -> Result<()> { - info!("Save data package {:?}", pack_data.dir); + trace!("Save data package {:?}", pack_data.dir); pack_data.dir .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() @@ -193,6 +205,27 @@ impl PackTasks { Ok(()) } + fn async_save_report( + input: (Arc, PackageImportReport) + ) -> Result<()> { + let (writing_directory, report,) = input; + trace!("Save report package {:?}", writing_directory); + match serde_json::to_string_pretty(&report) { + Ok(cs_json) => match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote import quality report to disk"); + } + Err(e) => { + debug!(?e, "failed to write import quality report to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize import quality report"); + } + } + Ok(()) + } + } @@ -303,7 +336,7 @@ impl LoadedPackData { self._is_dirty } - pub fn tick( + pub(crate) fn tick( &mut self, b2u_sender: &std::sync::mpsc::Sender, loop_index: u128, @@ -319,7 +352,7 @@ impl LoadedPackData { tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); + let _ = b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); self.active_elements = active_elements.clone(); next_loaded.extend(active_elements); } @@ -408,7 +441,7 @@ impl LoadedPackData { } } if let Some(tex_path) = common_attributes.get_icon_file() { - b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); + let _ = b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); } else { debug!("no texture attribute on this marker"); } @@ -440,7 +473,7 @@ impl LoadedPackData { let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); + let _ = b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); } else { debug!("no texture attribute on this trail"); } @@ -474,6 +507,7 @@ impl LoadedPackTexture { u2u_sender: &std::sync::mpsc::Sender, ui: &mut egui::Ui, show_only_active: bool, + import_quality_report: &PackageImportReport, ) { //it is important to generate a new id each time to avoid collision ui.push_id(ui.next_auto_id(), |ui| { @@ -484,18 +518,18 @@ impl LoadedPackTexture { ui, &mut self._is_dirty, show_only_active, - &self.late_discovery_categories + &import_quality_report ); }); if self._is_dirty { - u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); } } pub fn is_dirty(&self) -> bool { self._is_dirty } - pub fn tick( + pub(crate) fn tick( &mut self, u2u_sender: &std::sync::mpsc::Sender, _timestamp: f64, @@ -512,15 +546,15 @@ impl LoadedPackTexture { self.current_map_data.wip_trails.len(), ); let mut marker_objects = Vec::new(); - for (uuid, marker) in self.current_map_data.active_markers.iter() { + for marker in self.current_map_data.active_markers.values() { if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { marker_objects.push(mo); } } tracing::trace!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); - u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); + let _ = u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); let mut trail_objects = Vec::new(); - for (uuid, trail) in self.current_map_data.active_trails.iter() { + for trail in self.current_map_data.active_trails.values() { trail_objects.push(TrailObject { vertices: trail.trail_object.vertices.clone(), texture: trail.trail_object.texture, @@ -528,7 +562,7 @@ impl LoadedPackTexture { //next_on_screen.insert(*uuid); } tracing::trace!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); - u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); + let _ = u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); } pub fn swap(&mut self) { @@ -567,7 +601,7 @@ impl LoadedPackTexture { ), ); } else { - info!(%tex_path, "failed to find this icon texture"); + error!(%tex_path, "failed to find this icon texture"); } } let th = self.current_map_data.active_textures.get(tex_path) @@ -615,10 +649,10 @@ impl LoadedPackTexture { ), ); } else { - info!(%tex_path, "failed to find this trail texture"); + error!(%tex_path, "failed to find this trail texture"); } } else { - debug!("Trail texture alreadu loaded {:?}", tex_path); + trace!("Trail texture already loaded {:?}", tex_path); } let texture_path = common_attributes.get_texture(); let th = texture_path @@ -673,10 +707,13 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { Ok(marker_packs_dir) } -pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap, BTreeMap)>{ +pub fn load_all_from_dir(jokolay_dir: Arc) + -> Result + { let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); + let mut report_packs: BTreeMap = Default::default(); for entry in marker_packs_dir @@ -698,9 +735,10 @@ pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap { - let (data, tex) = lp; + let (data, tex, report) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); } Err(e) => { error!(?e, "failed to load pack from directory: {}", name); @@ -710,10 +748,10 @@ pub fn load_all_from_dir(jokolay_dir: Arc) -> Result<(BTreeMap) -> Result<(LoadedPackData, LoadedPackTexture)> { +fn build_from_dir(name: String, pack_dir: Arc) -> Result { if !pack_dir .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() @@ -734,7 +772,7 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result<(LoadedPackData, L } -pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> (LoadedPackData, LoadedPackTexture) { +pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> ImportTriplet { let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); let data = LoadedPackData { name: name.clone(), @@ -778,11 +816,11 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> (Loa _is_dirty: false, activation_data, dir: Arc::clone(&pack_dir), - late_discovery_categories: core.late_discovery_categories, name: name, tbins: core.tbins, active_elements: Default::default(), }; - (data, tex) + let report = core.report; + (data, tex, report) } diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index fff0fd3..cdbe22c 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -3,7 +3,7 @@ use std::{ }; use glam::Vec3; -use joko_package_models::attributes::CommonAttributes; +use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use tribool::Tribool; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; @@ -64,6 +64,7 @@ pub struct PackageUIManager { default_marker_texture: Option, default_trail_texture: Option, packs: BTreeMap, + reports: BTreeMap, tasks: PackTasks, currently_used_files: BTreeMap, @@ -210,10 +211,10 @@ impl PackageDataManager { } } } - let mut tasks = &self.tasks; - for (uuid, pack) in self.packs.iter_mut() { + let tasks = &self.tasks; + for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); - b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); tasks.save_data(pack, pack.is_dirty()); pack.tick( &b2u_sender, @@ -229,13 +230,13 @@ impl PackageDataManager { } if map_changed { self.get_active_elements_parents(categories_and_elements_to_be_loaded); - b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); + let _ = b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); } if map_changed || have_used_files_list_changed || choice_of_category_changed { //there is no point in sending a new list if nothing changed - b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); + let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); self.currently_used_files = currently_used_files; - b2u_sender.send(BackToUIMessage::TextureSwapChain); + let _ = b2u_sender.send(BackToUIMessage::TextureSwapChain); } }, None => {}, @@ -247,7 +248,7 @@ impl PackageDataManager { self.packs.remove(&uuid); } } - pub fn save(&mut self, mut data_pack: LoadedPackData) -> Uuid { + pub fn save(&mut self, mut data_pack: LoadedPackData, mut report: PackageImportReport) -> Uuid { let mut to_delete: Vec = Vec::new(); for (uuid, pack) in self.packs.iter() { if pack.name == data_pack.name { @@ -255,6 +256,7 @@ impl PackageDataManager { } } self.delete_packs(to_delete); + self.tasks.save_report(Arc::clone(&data_pack.dir), report, true); self.tasks.save_data(&mut data_pack, true); let mut uuid_to_insert = data_pack.uuid.clone(); while self.packs.contains_key(&uuid_to_insert) {//collision avoidance @@ -273,17 +275,18 @@ impl PackageDataManager { ) { once::assert_has_not_been_called!("Early load must happen only once"); // Called only once at application start. - b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); self.tasks.load_all_packs(jokolay_dir); - if let Ok((data_packages, texture_packages)) = self.tasks.wait_for_load_all_packs() { + if let Ok((data_packages, texture_packages, report_packages)) = self.tasks.wait_for_load_all_packs() { for (uuid, data_pack) in data_packages { self.packs.insert(uuid, data_pack); } - for (uuid, texture_pack) in texture_packages { - b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + for ((_, texture_pack), (_, report)) in std::iter::zip(texture_packages, report_packages) { + let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); } - b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } + let _ = b2u_sender.send(BackToUIMessage::FirstLoadDone); } @@ -295,6 +298,7 @@ impl PackageUIManager { Self { packs, tasks: PackTasks::new(), + reports: Default::default(), default_marker_texture: None, default_trail_texture: None, @@ -340,6 +344,7 @@ impl PackageUIManager { pub fn delete_packs(&mut self, to_delete: Vec) { for uuid in to_delete { self.packs.remove(&uuid); + self.reports.remove(&uuid); } } pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { @@ -443,8 +448,8 @@ impl PackageUIManager { link: &MumbleLink, z_near: f32, ) { - let mut tasks = &self.tasks; - for (uuid, pack) in self.packs.iter_mut() { + let tasks = &self.tasks; + for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); tasks.save_texture(pack, pack.is_dirty()); pack.tick( @@ -456,7 +461,7 @@ impl PackageUIManager { ); std::mem::drop(span_guard); } - u2u_sender.send(UIToUIMessage::RenderSwapChain); + let _ = u2u_sender.send(UIToUIMessage::RenderSwapChain); //u2u_sender.send(UIToUIMessage::Present); } @@ -480,17 +485,17 @@ impl PackageUIManager { } if ui.button("Activate all elements").clicked() { self.category_set_all(true); - u2b_sender.send(UIToBackMessage::CategorySetAll(true)); + let _ = u2b_sender.send(UIToBackMessage::CategorySetAll(true)); } if ui.button("Deactivate all elements").clicked() { self.category_set_all(false); - u2b_sender.send(UIToBackMessage::CategorySetAll(false)); + let _ = u2b_sender.send(UIToBackMessage::CategorySetAll(false)); } - for pack in self.packs.values_mut() { + for (pack, import_quality_report) in std::iter::zip(self.packs.values_mut(), self.reports.values()) { //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active); + pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active, &import_quality_report); } }); @@ -569,19 +574,23 @@ impl PackageUIManager { Ok(()) }); if files_changed { - event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); + let _ = event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); } } - fn gui_package_loader( + fn gui_package_list( &mut self, u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, import_status: &Arc>, - open: &mut bool + open: &mut bool, + first_load_done: bool, ) { Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { + if !first_load_done { + ui.label("Loading in progress..."); + } let mut to_delete = vec![]; for pack in self.packs.values() { ui.label(pack.name.clone()); @@ -591,10 +600,13 @@ impl PackageUIManager { if ui.button("Details").clicked() { //TODO } + if ui.button("Export").clicked() { + //TODO + } ui.end_row(); } if !to_delete.is_empty() { - u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); + let _ = u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); } }); }); @@ -607,7 +619,7 @@ impl PackageUIManager { //let import_status = import_status.lock().unwrap(); Self::pack_importer(Arc::clone(import_status)); } - ui.label("import not started yet"); + //ui.label("import not started yet"); } ImportStatus::WaitingForFileChooser => { ui.label( @@ -626,7 +638,7 @@ impl PackageUIManager { ui.text_edit_singleline(name); }); if ui.button("save").clicked() { - u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); + let _ = u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); } } } @@ -654,14 +666,15 @@ impl PackageUIManager { is_marker_open: &mut bool, import_status: &Arc>, is_file_open: &mut bool, + first_load_done: bool, timestamp: f64, link: Option<&MumbleLink> ) { - self.gui_package_loader(u2b_sender, etx, import_status, is_marker_open); + self.gui_package_list(u2b_sender, etx, import_status, is_marker_open, first_load_done); self.gui_file_manager(u2b_sender, etx, is_file_open, link); } - pub fn save(&mut self, mut texture_pack: LoadedPackTexture) { + pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { /* We save in a file with the name of the package, while we keep track of it from a uuid point of view. It means we can have duplicates unless package with same name is deleted. @@ -675,6 +688,7 @@ impl PackageUIManager { self.delete_packs(to_delete); self.tasks.save_texture(&mut texture_pack, true); self.packs.insert(texture_pack.uuid, texture_pack); + self.reports.insert(report.uuid, report); } } diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index efb353b..0aaf667 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashSet}; -use joko_package_models::{attributes::CommonAttributes, package::PackCore}; +use joko_package_models::{attributes::CommonAttributes, package::{PackCore, PackageImportReport}}; use uuid::Uuid; use glam::Vec3; @@ -17,8 +17,9 @@ use crate::LoadedPackTexture; pub enum BackToUIMessage { ActiveElements(HashSet),//list of all elements that are loaded for current map CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display - LoadedPack(LoadedPackTexture),//push a loaded pack to UI + LoadedPack(LoadedPackTexture, PackageImportReport),//push a loaded pack to UI DeletedPacks(Vec),//push a deleted set of packs to UI + FirstLoadDone, ImportedPack(String, PackCore), ImportFailure(miette::Report), MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 28a5dbc..3b08fdc 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -1,10 +1,7 @@ -use std::collections::HashSet; - use ordered_hash_map::OrderedHashMap; use tracing::debug; use uuid::Uuid; -use indexmap::IndexMap; -use crate::attributes::CommonAttributes; +use crate::{attributes::CommonAttributes, package::PackageImportReport}; #[derive(Debug, Clone)] pub struct RawCategory { @@ -16,6 +13,7 @@ pub struct RawCategory { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, + pub sources: OrderedHashMap, } #[derive(Debug, Clone)] @@ -28,7 +26,7 @@ pub struct Category { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub children: IndexMap, + pub children: OrderedHashMap,//TODO: make a branch to test if having an Vec associated with global list of categories is faster. } pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { @@ -78,7 +76,7 @@ impl Category { children: Default::default() } } - pub fn per_uuid<'a>(categories: &'a mut IndexMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + pub fn per_uuid<'a>(categories: &'a mut OrderedHashMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { for (_, cat) in categories { if &cat.guid == uuid { return Some(cat); @@ -92,19 +90,27 @@ impl Category { } pub fn reassemble( input_first_pass_categories: &OrderedHashMap, - late_discovered_categories: &mut HashSet, - ) -> IndexMap { + report: &mut PackageImportReport, + ) -> OrderedHashMap { + let start_initialize = std::time::SystemTime::now(); let mut first_pass_categories = input_first_pass_categories.clone(); let mut second_pass_categories: OrderedHashMap = Default::default(); let mut need_a_pass: bool = true; - let mut third_pass_categories: IndexMap = Default::default(); + let mut third_pass_categories: OrderedHashMap = Default::default(); let mut third_pass_categories_ref: Vec = Default::default(); - let mut root: IndexMap = Default::default(); + let mut root: OrderedHashMap = Default::default(); + + let elaspsed_initialize = start_initialize.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.initialize = elaspsed_initialize.as_millis(); + + let start_multi_pass_missing_categories_creation = std::time::SystemTime::now(); + let mut nb_pass_done = 0; while need_a_pass { need_a_pass = false; + nb_pass_done += 1; for (key, value) in first_pass_categories.iter() { - debug!("reassemble_categories {:?}", value); + debug!("reassemble_categories pass #{} {:?}", nb_pass_done, value); let mut to_insert = value.clone(); if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { let mut n = 0; @@ -113,15 +119,16 @@ impl Category { while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { debug!("{} {}", parent_name, n); if let Some(parent_category) = first_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); + report.found_category_late(&parent_name, parent_category.guid); last_name = Some(parent_name.clone()); } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { - late_discovered_categories.insert(parent_category.guid); + report.found_category_late(&parent_name, parent_category.guid); last_name = Some(parent_name.clone()); }else{ let new_uuid = Uuid::new_v4(); let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); + let sources: OrderedHashMap = OrderedHashMap::new(); let to_insert = RawCategory { default_enabled: value.default_enabled, guid: new_uuid, @@ -130,16 +137,20 @@ impl Category { parent_name: prefix_until_nth_char(&parent_name, '.', n-1), props: value.props.clone(), separator: false, - full_category_name: parent_name.clone() + full_category_name: parent_name.clone(), + sources, }; last_name = Some(to_insert.full_category_name.clone()); + report.found_category_late(&to_insert.full_category_name, new_uuid); second_pass_categories.insert(parent_name.clone(), to_insert); - late_discovered_categories.insert(new_uuid); need_a_pass = true; } n += 1; } - late_discovered_categories.insert(value.guid); + for (requester_uuid, source_file_name) in value.sources.iter() { + report.found_category_late_with_details(&value.full_category_name, value.guid, requester_uuid, source_file_name); + } + report.found_category_late(&value.full_category_name, value.guid); to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); to_insert.display_name = to_insert.relative_category_name.clone(); debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); @@ -164,6 +175,11 @@ impl Category { second_pass_categories.clear(); } } + let elaspsed_multi_pass_missing_categories_creation = start_multi_pass_missing_categories_creation.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.missing_categories_creation = elaspsed_multi_pass_missing_categories_creation.as_millis(); + + debug!("nb_pass_done {}", nb_pass_done); + let start_parent_child_relationship = std::time::SystemTime::now(); for (key, value) in second_pass_categories { let parent = if let Some(parent_name) = &value.parent_name { if let Some(parent_category) = first_pass_categories.get(parent_name) { @@ -182,9 +198,13 @@ impl Category { third_pass_categories_ref.push(ref_uuid); } } + let elaspsed_parent_child_relationship = start_parent_child_relationship.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.parent_child_relationship = elaspsed_parent_child_relationship.as_millis(); - for full_category_name in third_pass_categories_ref { - if let Some(cat) = third_pass_categories.shift_remove(&full_category_name) { + debug!("third_pass_categories_ref"); + let start_tree_insertion = std::time::SystemTime::now(); + for full_category_uuid in third_pass_categories_ref { + if let Some(cat) = third_pass_categories.remove(&full_category_uuid) { if let Some(parent) = cat.parent { if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { parent_category.children.insert(cat.guid.clone(), cat); @@ -200,7 +220,9 @@ impl Category { panic!("Some bad logic at works"); } } - debug!("reassemble_categories {:?}", root); + let elaspsed_tree_insertion = start_tree_insertion.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.tree_insertion = elaspsed_tree_insertion.as_millis(); + debug!("reassemble_categories end {:?}", root); root } diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index a8637e5..840c1af 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -1,8 +1,10 @@ +use base64::Engine; use joko_core::RelativePath; +use serde::{Serialize, Serializer}; use tracing::{debug, trace}; use uuid::Uuid; -use std::collections::{HashMap, HashSet, BTreeMap}; -use indexmap::IndexMap; +use std::collections::{BTreeMap, HashMap, HashSet}; +use ordered_hash_map::OrderedHashMap; use crate::marker::Marker; use crate::route::{route_to_tbin, route_to_trail, Route}; use crate::trail::{TBin, Trail}; @@ -10,6 +12,102 @@ use crate::category::{prefix_until_nth_char, Category}; use crate::map::MapData; +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); + +fn serialize_reference(reference: &ElementReference, serializer: S) -> Result +where S: Serializer +{ + match reference { + ElementReference::Uuid(uuid) => { + let to_do = BASE64_ENGINE.encode(uuid); + serializer.serialize_str(to_do.as_str()) + } + ElementReference::Category(full_category_name) => { + serializer.serialize_str(&full_category_name.as_str()) + } + } +} + +fn serialize_uuid_in_base64(uuid: &Uuid, serializer: S) -> Result +where S: Serializer +{ + let to_do = BASE64_ENGINE.encode(uuid); + serializer.serialize_str(to_do.as_str()) +} + + +#[derive(Debug, Clone, Serialize)] +struct PackageCategorySource { + full_category_name: String, + #[serde(serialize_with= "serialize_uuid_in_base64")] + requester_uuid: Uuid, + source_file_name: String, +} +#[derive(Debug, Clone, Serialize)] +enum ElementReference { + Uuid(Uuid), + Category(String), +} +#[derive(Debug, Clone, Serialize)] +struct PackageElementSource { + file_path: String, + #[serde(serialize_with= "serialize_reference")] + requester_reference: ElementReference, + source_file_name: String, +} + +#[derive(Default, Debug, Clone, Serialize)] +pub struct PackageImportStatistics { + categories: usize, // total number of found categories + missing_categories: usize, // categories that should be defined in a node + textures: usize, //total number of texture used (or should) + missing_textures: usize, // how many of the textures are missing + entities: usize, // total number of tracked elements: categories, trails, markers, ... + markers: usize, // total number of markers + trails: usize, // total number of trails + routes: usize, // total number of routes defined, they shall not count as trails even if imported as such + maps: usize, // total number of maps covered +} + + +#[derive(Default, Debug, Clone, Serialize)] +pub struct PackageImportReassembleTelemetry { + pub total: u128, + pub initialize: u128, + pub missing_categories_creation: u128, + pub parent_child_relationship: u128, + pub tree_insertion: u128, +} +#[derive(Default, Debug, Clone, Serialize)] +pub struct PackageImportTelemetry { + pub total: u128, + pub texture_loading: u128, + pub categories_loading: u128, + pub categories_first_pass: u128, + pub categories_second_pass: u128, + pub categories_registering: u128, + pub elements_registering: u128, + pub categories_reassemble: PackageImportReassembleTelemetry, +} +#[derive(Default, Debug, Clone, Serialize)] +pub struct PackageImportReport { + #[serde(skip)] + pub uuid: Uuid, + number_of: PackageImportStatistics, // count everything we can think of + pub telemetry: PackageImportTelemetry, // all the time spent in which step + late_discovered_categories: OrderedHashMap,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + missing_categories: Vec,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + #[serde(skip)] + _missing_categories_tracker: HashSet, // for tracking purpose to avoid duplicate + #[serde(skip)] + _missing_textures_tracker: HashSet, // for tracking purpose to avoid duplicate + missing_textures: Vec,//missing texture for display + missing_trails: Vec,//missing file for trail +} + #[derive(Debug, Clone)] pub struct PackCore { /* @@ -19,15 +117,52 @@ pub struct PackCore { pub uuid: Uuid, pub textures: HashMap>, pub tbins: HashMap, - pub categories: IndexMap, + pub categories: OrderedHashMap, pub all_categories: HashMap, - pub late_discovery_categories: HashSet,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. pub entities_parents: HashMap, pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, + pub report: PackageImportReport, } +impl PackageImportReport { + pub const REPORT_FILE_NAME: &'static str = "import_report.json"; + fn merge_partial(&mut self, partial_report: PackageImportReport) { + self.late_discovered_categories.extend(partial_report.late_discovered_categories); + } + + pub fn is_category_discovered_late(&self, uuid: Uuid) -> bool { + self.late_discovered_categories.contains_key(&uuid) + } + + pub fn found_category_late(&mut self, full_category_name: &String, category_uuid: Uuid) { + self.late_discovered_categories.insert(category_uuid, full_category_name.clone()); + } + pub fn found_category_late_with_details(&mut self, full_category_name: &String, category_uuid: Uuid, requester_uuid: &Uuid, source_file_name: &String) { + self.found_category_late(full_category_name, category_uuid); + + //for this to work we need to keep track of where each category was called and thus defined since late + self.missing_categories.push(PackageCategorySource{ + full_category_name: full_category_name.clone(), + requester_uuid: requester_uuid.clone(), + source_file_name: source_file_name.clone() + }); + if !self._missing_categories_tracker.contains(full_category_name) { + self.number_of.missing_categories += 1; + self._missing_categories_tracker.insert(full_category_name.clone()); + } + } + fn found_missing_texture(&mut self, file_path: &String) { + if !self._missing_textures_tracker.contains(file_path) { + self.number_of.missing_textures += 1; + self._missing_textures_tracker.insert(file_path.clone()); + } + + } + +} + impl PackCore { @@ -36,7 +171,7 @@ impl PackCore { all_categories: Default::default(), categories: Default::default(), entities_parents: Default::default(), - late_discovery_categories: Default::default(), + report: Default::default(), maps: Default::default(), source_files: Default::default(), tbins: Default::default(), @@ -44,6 +179,7 @@ impl PackCore { uuid: Default::default(), }; res.uuid = Uuid::new_v4(); + res.report.uuid = res.uuid.clone(); res } pub fn partial(all_categories: &HashMap) -> Self { @@ -56,7 +192,7 @@ impl PackCore { pub fn merge_partial(&mut self, partial_pack: PackCore) { self.maps.extend(partial_pack.maps); self.all_categories = partial_pack.all_categories; - self.late_discovery_categories.extend(partial_pack.late_discovery_categories); + self.report.merge_partial(partial_pack.report); self.source_files.extend(partial_pack.source_files); self.tbins.extend(partial_pack.tbins); self.entities_parents.extend(partial_pack.entities_parents); @@ -69,7 +205,7 @@ impl PackCore { self.all_categories.get(full_category_name) } - pub fn get_or_create_category_uuid(&mut self, full_category_name: &String) -> Uuid { + pub fn get_or_create_category_uuid(&mut self, full_category_name: &String, requester_uuid: Uuid, source_file_name: &String) -> Uuid { if let Some(category_uuid) = self.all_categories.get(full_category_name) { category_uuid.clone() } else { @@ -83,13 +219,13 @@ impl PackCore { n += 1; if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { //FIXME: might want to make the difference between impacted parents and actual missing category - self.late_discovery_categories.insert(*parent_uuid); + self.report.found_category_late(&full_category_name, *parent_uuid); last_uuid = Some(*parent_uuid); } else { let new_uuid = Uuid::new_v4(); debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); self.all_categories.insert(parent_full_category_name.clone(), new_uuid); - self.late_discovery_categories.insert(new_uuid); + self.report.found_category_late_with_details(&full_category_name, new_uuid, &requester_uuid, source_file_name); last_uuid = Some(new_uuid); } } @@ -99,6 +235,15 @@ impl PackCore { } } + + pub fn register_texture(&mut self, name: String, file_path: &RelativePath, bytes: Vec) { + assert!( + self.textures.insert(file_path.clone(), bytes).is_none(), + "duplicate image file {name}" + ); + self.report.number_of.textures += 1; + } + pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ if let Some(parent_uuid) = self.all_categories.get(full_category_name) { let mut uuid_to_insert = uuid.clone(); @@ -107,6 +252,7 @@ impl PackCore { uuid_to_insert = Uuid::new_v4(); } self.entities_parents.insert(uuid_to_insert, *parent_uuid); + self.report.number_of.entities += 1; Ok(uuid_to_insert) } else { //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. @@ -119,8 +265,10 @@ impl PackCore { marker.guid = uuid_to_insert; if !self.maps.contains_key(&marker.map_id) { self.maps.insert(marker.map_id, MapData::default()); + self.report.number_of.maps += 1; } self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); + self.report.number_of.markers += 1; Ok(()) } @@ -129,8 +277,10 @@ impl PackCore { trail.guid = uuid_to_insert; if !self.maps.contains_key(&trail.map_id) { self.maps.insert(trail.map_id, MapData::default()); + self.report.number_of.maps += 1; } self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + self.report.number_of.trails += 1; Ok(()) } @@ -145,9 +295,11 @@ impl PackCore { self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time if !self.maps.contains_key(&trail.map_id) { self.maps.insert(trail.map_id, MapData::default()); + self.report.number_of.maps += 1; } self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); + self.report.number_of.routes += 1; Ok(()) } @@ -156,11 +308,12 @@ impl PackCore { let mut all_categories: HashMap = Default::default(); Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); self.entities_parents.extend(entities_parents); + self.report.number_of.categories = all_categories.len(); self.all_categories = all_categories; } fn recursive_register_categories( entities_parents: &mut HashMap, - categories: &IndexMap, + categories: &OrderedHashMap, all_categories: &mut HashMap, ) { for (_, cat) in categories.iter() { @@ -172,4 +325,30 @@ impl PackCore { Self::recursive_register_categories(entities_parents, &cat.children, all_categories); } } + + pub fn found_missing_element_texture(&mut self, file_path: String, requester_uuid: Uuid, source_file_name: &String) { + self.report.found_missing_texture(&file_path); + self.report.missing_textures.push(PackageElementSource{ + file_path, + requester_reference: ElementReference::Uuid(requester_uuid), + source_file_name: source_file_name.clone() + }); + } + pub fn found_missing_inherited_texture(&mut self, file_path: String, full_category_name: String, source_file_name: &String) { + self.report.found_missing_texture(&file_path); + self.report.missing_textures.push(PackageElementSource{ + file_path, + requester_reference: ElementReference::Category(full_category_name), + source_file_name: source_file_name.clone() + }); + + } + pub fn found_missing_trail(&mut self, file_path: &RelativePath, requester_uuid: Uuid, source_file_name: &String) { + self.report.missing_trails.push(PackageElementSource{ + file_path: file_path.as_str().to_string(), + requester_reference: ElementReference::Uuid(requester_uuid), + source_file_name: source_file_name.clone() + }); + + } } \ No newline at end of file diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 1e7ae3a..2d32614 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -25,15 +25,24 @@ use tracing::{error, info, info_span}; use jmf::{LoadedPackData, LoadedPackTexture, build_from_core}; use jmf::{ImportStatus, import_pack_from_zip_file_path}; + +const MINIMAL_WINDOW_WIDTH: i32 = 640; +const MINIMAL_WINDOW_HEIGHT: i32 = 480; +const MINIMAL_WINDOW_POSITION_X: i32 = 0; +const MINIMAL_WINDOW_POSITION_Y: i32 = 0; + #[derive(Clone)] struct JokolayUIState { link: Option, editable_mumble: bool, window_changed: bool, list_of_textures_changed: bool,//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + first_load_done: bool, nb_running_tasks_on_back: i32,// store the number of running tasks in background thread nb_running_tasks_on_network: i32,// store the number of running tasks (requests) in progress import_status: Arc>, + maximal_window_width: i32, + maximal_window_height: i32, } struct JokolayBackState { @@ -79,7 +88,7 @@ impl Jokolay { let data_packages: BTreeMap = Default::default(); let texture_packages: BTreeMap = Default::default(); - let mut package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; + let package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; @@ -102,14 +111,20 @@ impl Jokolay { window_title: "Jokolay".to_string(), ..Default::default() }); + let screen_physical_size = glfw_backend.glfw.with_primary_monitor(|_, m| { + if let Some(m) = m { + Some(m.get_physical_size()) + } else { + None + } + }); + info!("Monitor physical size: {:?}", screen_physical_size); glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); let frame_stats = wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _); - let mut menu_panel = MenuPanel::default(); - //menu_panel.show_theme_window = true; - //menu_panel.show_package_manager_window = true; + let menu_panel = MenuPanel::default(); package_ui_manager.late_init(&egui_context); Ok(Self { @@ -133,9 +148,12 @@ impl Jokolay { editable_mumble: false, window_changed: true, list_of_textures_changed: false, + first_load_done: false, nb_running_tasks_on_back: 0, nb_running_tasks_on_network: 0, import_status: Default::default(), + maximal_window_width: screen_physical_size.unwrap().0, + maximal_window_height: screen_physical_size.unwrap().1, }, state_back: JokolayBackState { choice_of_category_changed: false, @@ -200,29 +218,33 @@ impl Jokolay { tracing::trace!("Handling of UIToBackMessage::DeletePacks"); let mut deleted = Vec::new(); for pack_uuid in to_delete { - let pack = package_manager.packs.remove(&pack_uuid).unwrap(); - if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) { - error!(?e, pack.name,"failed to remove pack"); - } else { - info!("deleted marker pack: {}", pack.name); - deleted.push(pack_uuid); + if let Some(pack) = package_manager.packs.remove(&pack_uuid) { + if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) { + error!(?e, pack.name,"failed to remove pack"); + } else { + info!("deleted marker pack: {}", pack.name); + deleted.push(pack_uuid); + } } } - b2u_sender.send(BackToUIMessage::DeletedPacks(deleted)); + let _ = b2u_sender.send(BackToUIMessage::DeletedPacks(deleted)); } UIToBackMessage::ImportPack(file_path) => { tracing::trace!("Handling of UIToBackMessage::ImportPack"); - b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); + let start = std::time::SystemTime::now(); let result = import_pack_from_zip_file_path(file_path); + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!("Loading of taco package from disk took {} ms", elaspsed.as_millis()); match result { Ok((file_name, pack)) => { - b2u_sender.send(BackToUIMessage::ImportedPack(file_name, pack)); + let _ = b2u_sender.send(BackToUIMessage::ImportedPack(file_name, pack)); } Err(e) => { - b2u_sender.send(BackToUIMessage::ImportFailure(e)); + let _ = b2u_sender.send(BackToUIMessage::ImportFailure(e)); } } - b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } UIToBackMessage::MumbleLinkAutonomous => { tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); @@ -257,10 +279,12 @@ impl Jokolay { } match package_manager.marker_packs_dir.open_dir(name) { Ok(dir) => { - let (mut data_pack, mut texture_pack) = build_from_core(name.to_string(), dir.into(), pack); + let (data_pack, mut texture_pack, mut report) = build_from_core(name.to_string(), dir.into(), pack); tracing::trace!("Package loaded into data and texture"); - texture_pack.uuid = package_manager.save(data_pack); - b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack)); + let uuid_of_insertion = package_manager.save(data_pack, report.clone()); + report.uuid = uuid_of_insertion; + texture_pack.uuid = uuid_of_insertion; + let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); }, Err(e) => { error!(?e, "failed to open marker pack directory to save pack {:?} {}", package_manager.marker_packs_dir, name); @@ -273,7 +297,7 @@ impl Jokolay { } } fn background_loop( - mut app: Arc>>, + app: Arc>>, mut local_state: JokolayBackState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, @@ -319,12 +343,12 @@ impl Jokolay { thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; } + unreachable!("Program broke out a never ending loop !"); drop(span_guard); } fn handle_u2u_message( gui: &mut JokolayGui, - state: &mut JokolayUIState, msg: UIToUIMessage ) { match msg { @@ -373,6 +397,9 @@ impl Jokolay { tracing::trace!("Handling of BackToUIMessage::DeletedPacks"); gui.package_manager.delete_packs(to_delete); } + BackToUIMessage::FirstLoadDone => { + local_state.first_load_done = true; + } BackToUIMessage::ImportedPack(file_name, pack) => { tracing::trace!("Handling of BackToUIMessage::ImportedPack"); *local_state.import_status.lock().unwrap() = ImportStatus::PackDone(file_name, pack, false); @@ -382,11 +409,11 @@ impl Jokolay { *local_state.import_status.lock().unwrap() = ImportStatus::PackError(error); } - BackToUIMessage::LoadedPack(pack_texture) => { + BackToUIMessage::LoadedPack(pack_texture, report) => { tracing::trace!("Handling of BackToUIMessage::LoadedPack"); - gui.package_manager.save(pack_texture); + gui.package_manager.save(pack_texture, report); local_state.import_status = Default::default(); - u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); } BackToUIMessage::MarkerTexture(pack_uuid, tex_path, marker_uuid, position, common_attributes) => { tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); @@ -415,7 +442,7 @@ impl Jokolay { } } - pub fn enter_event_loop(mut self) { + pub fn enter_event_loop(self) { let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); @@ -436,7 +463,7 @@ impl Jokolay { if let Ok(mut import_status) = local_state.import_status.lock() { match &mut *import_status { ImportStatus::LoadingPack(file_path) => { - u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); + let _ = u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); *import_status = ImportStatus::WaitingLoading(file_path.clone()); } _ => {} @@ -446,7 +473,7 @@ impl Jokolay { let mut gui = self.gui.lock().unwrap(); while let Ok(msg) = u2u_receiver.try_recv() { nb_messages += 1; - Self::handle_u2u_message(gui.deref_mut(), &mut local_state, msg); + Self::handle_u2u_message(gui.deref_mut(), msg); nb_message_on_curent_loop += 1; if nb_message_on_curent_loop == max_nb_messages_per_loop { break; @@ -526,7 +553,7 @@ impl Jokolay { if local_state.editable_mumble { local_state.window_changed = true; local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); - u2b_sender.send(UIToBackMessage::MumbleLink(local_state.link.clone())); + let _ = u2b_sender.send(UIToBackMessage::MumbleLink(local_state.link.clone())); } else { let is_mumble_alive = mumble_manager.is_alive(); match mumble_manager.tick() { @@ -553,12 +580,12 @@ impl Jokolay { if local_state.window_changed { glfw_backend .window - .set_pos(link.client_pos.x, link.client_pos.y); + .set_pos(link.client_pos.x.max(MINIMAL_WINDOW_POSITION_X), link.client_pos.y.max(MINIMAL_WINDOW_POSITION_Y)); // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - let client_size_x = 1024.max(link.client_size.x); - let client_size_y = 768.max(link.client_size.y); + let client_size_x = MINIMAL_WINDOW_WIDTH.max(link.client_size.x); + let client_size_y = MINIMAL_WINDOW_HEIGHT.max(link.client_size.y); glfw_backend .window .set_size(client_size_x - 1, client_size_y - 1); @@ -639,6 +666,7 @@ impl Jokolay { &mut menu_panel.show_package_manager_window, &local_state.import_status, &mut menu_panel.show_file_manager_window, + local_state.first_load_done, latest_time, local_state.link.as_ref() ); @@ -668,7 +696,18 @@ impl Jokolay { glfw_backend .window .set_mouse_passthrough(!(etx.wants_keyboard_input() || etx.wants_pointer_input())); - //TODO: view when map is open + //TODO: view from above when map is open + /* + TODO: have a clean view when game is not focused. + let mut do_draw = local_state.editable_mumble; + if !do_draw { + if let Some(link) = local_state.link.as_ref() { + if let Some(ui_state) = link.ui_state { + do_draw = ui_state.contains(UIState::GameHasFocus) + } + }; + }*/ + joko_renderer.render_egui( etx.tessellate(shapes, etx.pixels_per_point()), textures_delta, @@ -676,6 +715,7 @@ impl Jokolay { ); joko_renderer.present(); glfw_backend.window.swap_buffers(); + nb_frames += 1; } drop(span_guard); @@ -720,6 +760,7 @@ pub fn start_jokolay() { }; std::mem::drop(log_file_flush_guard); } + /// Guild Wars 2 has an array of menu icons on top left corner of the game. /// Its size is affected by four different factors /// 1. UISZ: @@ -733,7 +774,6 @@ pub fn start_jokolay() { /// 3. Dimensions of the gw2 window /// This is something we get from mumble link and win32 api. We store this as client pos/size in mumble link /// It is not just the width or height, but their ratio to the 1024x768 resolution - /// /// 1. By default, with dpi 96 (scale 1.0), at resolution 1024x768 these are the sizes of menu at different uisz settings /// UISZ -> WIDTH HEIGHT @@ -791,10 +831,10 @@ impl MenuPanel { let uisz_scale = convert_uisz_to_scale(link.uisz); ui_scaling_factor *= uisz_scale; - let min_width = 1024.0 * gw2_scale; - let min_height = 768.0 * gw2_scale; - let gw2_width = link.client_size.x.max(1024) as f32; - let gw2_height = link.client_size.y.max(768) as f32; + let min_width = MINIMAL_WINDOW_WIDTH as f32 * gw2_scale; + let min_height = MINIMAL_WINDOW_HEIGHT as f32 * gw2_scale; + let gw2_width = link.client_size.x.max(MINIMAL_WINDOW_WIDTH) as f32; + let gw2_height = link.client_size.y.max(MINIMAL_WINDOW_HEIGHT) as f32; let min_width_ratio = min_width.min(gw2_width) / min_width; let min_height_ratio = min_height.min(gw2_height) / min_height; diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs index 1450a62..07f1297 100644 --- a/crates/jokolay/src/app/mumble.rs +++ b/crates/jokolay/src/app/mumble.rs @@ -1,5 +1,3 @@ - -use egui; use egui::DragValue; use jmf::message::UIToBackMessage; use jokolink::MumbleLink; @@ -18,11 +16,11 @@ pub fn mumble_gui( ui.horizontal(|ui| { if ui.selectable_label(!*editable_mumble, "live").clicked() { *editable_mumble = false; - u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); + let _ = u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); } if ui.selectable_label(*editable_mumble, "editable").clicked() { *editable_mumble = true; - u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); + let _ = u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); } }); if *editable_mumble { diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index c3b9dad..ebb46c8 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -63,8 +63,8 @@ impl MumbleManager { } if !self.backend.is_alive() { - self.link.client_size.x = self.link.client_size.x.max(1024); - self.link.client_size.y = self.link.client_size.y.max(768); + self.link.client_size.x = -1; + self.link.client_size.y = -1; self.link.changes = BitFlags::all(); return Ok(Some(&self.link)); } From 04395ae804e07dbeeff3ef488f614004ea56d13f Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 14 Apr 2024 00:39:32 +0200 Subject: [PATCH 27/54] fix perf issues in reassemble tree_insertion --- crates/joko_package/src/io/deserialize.rs | 9 ++++-- crates/joko_package_models/src/category.rs | 34 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 9f8b766..9827146 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -8,7 +8,7 @@ use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; use std::{collections::{VecDeque, HashMap}, io::Read}; use ordered_hash_map::OrderedHashMap; -use tracing::{debug, info, info_span, instrument, trace, warn}; +use tracing::{debug, error, info, info_span, instrument, trace, warn}; use uuid::Uuid; use xot::{Node, Xot, Element}; @@ -481,7 +481,12 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - continue; } //FIXME: this needs to be changed for partial load - let category_uuid = target.get_category_uuid(&full_category_name).unwrap().clone();//categories MUST exist, they have already been parsed + let opt_cat_uuid = target.get_category_uuid(&full_category_name); + if opt_cat_uuid.is_none() { + error!("Mandatory category missing, packge is corrupted {:?} {:?}", map_id, child_element); + return Err(miette::Report::msg(format!("Mandatory category missing, packge is corrupted {:?} {:?}", map_xml_str, child_element))); + } + let category_uuid = opt_cat_uuid.unwrap().clone();//categories MUST exist, they have already been parsed let guid = raw_uid.and_then(|guid| { let mut buffer = [0u8; 20]; BASE64_ENGINE diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 3b08fdc..2288022 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -76,7 +76,31 @@ impl Category { children: Default::default() } } + pub fn per_route<'a>(categories: &'a mut OrderedHashMap, route: &Vec<&str>, depth: usize) -> Option<&'a mut Category> { + let mut route = route.clone(); + route.reverse(); + Category::_per_route(categories, &mut route, depth) + } + fn _per_route<'a>(categories: &'a mut OrderedHashMap, route: &mut Vec<&str>, depth: usize) -> Option<&'a mut Category> { + if let Some(relative_category_name) = route.pop() { + for (_, cat) in categories { + if cat.relative_category_name == relative_category_name { + if route.is_empty() { + return Some(cat); + } else { + return Category::_per_route(&mut cat.children, route, depth + 1); + } + } + } + } + return None; + } pub fn per_uuid<'a>(categories: &'a mut OrderedHashMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + /* + Do a look up in the tree based on uuid. Whole tree is scanned until a match is found. + + WARNING: very inefficient in the general case. + */ for (_, cat) in categories { if &cat.guid == uuid { return Some(cat); @@ -193,9 +217,9 @@ impl Category { debug!("{} parent is {:?}", key , parent); let cat = Category::from(&value, parent); - let ref_uuid = cat.guid.clone(); + let cat_ref = cat.guid.clone(); if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { - third_pass_categories_ref.push(ref_uuid); + third_pass_categories_ref.push(cat_ref); } } let elaspsed_parent_child_relationship = start_parent_child_relationship.elapsed().unwrap_or_default(); @@ -205,10 +229,12 @@ impl Category { let start_tree_insertion = std::time::SystemTime::now(); for full_category_uuid in third_pass_categories_ref { if let Some(cat) = third_pass_categories.remove(&full_category_uuid) { + let mut route = Vec::from_iter(cat.full_category_name.split('.')); + route.pop();//it is now the parent route if let Some(parent) = cat.parent { - if let Some(parent_category) = Category::per_uuid(&mut third_pass_categories, &parent, 0) { + if let Some(parent_category) = Category::per_route(&mut third_pass_categories, &route, 0) { parent_category.children.insert(cat.guid.clone(), cat); - } else if let Some(parent_category) = Category::per_uuid(&mut root, &parent, 0) { + } else if let Some(parent_category) = Category::per_route(&mut root, &route, 0) { parent_category.children.insert(cat.guid.clone(), cat); } else { panic!("Could not find parent {} for {:?}", parent, cat); From 829c7608d3552a55660a0af19ee0e95ddd8a0bf3 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 14 Apr 2024 13:12:27 +0200 Subject: [PATCH 28/54] import package import reports to now display all information required when relaunching the tool --- Cargo.lock | 118 +++++++++++++++--- crates/joko_package/src/io/deserialize.rs | 89 +++++++------ crates/joko_package/src/io/export.rs | 6 +- crates/joko_package/src/io/serialize.rs | 6 +- .../src/manager/pack/file_selection.rs | 26 ++-- .../joko_package/src/manager/pack/loaded.rs | 49 ++++++-- crates/joko_package/src/manager/package.rs | 55 ++++---- crates/joko_package/src/message.rs | 4 +- crates/joko_package_models/Cargo.toml | 2 + crates/joko_package_models/src/category.rs | 12 +- crates/joko_package_models/src/marker.rs | 2 +- crates/joko_package_models/src/package.rs | 103 +++++++++++---- crates/joko_package_models/src/route.rs | 4 +- crates/joko_package_models/src/trail.rs | 2 +- 14 files changed, 343 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2738b2..11bbd69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.12", "once_cell", "serde", "version_check", @@ -130,7 +130,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.8.5", "serde", "serde_repr", "url", @@ -343,6 +343,15 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -585,6 +594,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "contracts" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9424f2ca1e42776615720e5746eed6efa19866fdbaac2923ab51c294ac4d1f2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -811,7 +831,7 @@ checksum = "21691a0388394a02b9352fb31edc7a008645ef1af13bc6eace5da06c2f599e60" dependencies = [ "bytemuck", "egui", - "getrandom", + "getrandom 0.2.12", "glow", "js-sys", "raw-window-handle 0.6.0", @@ -1152,6 +1172,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1161,7 +1192,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1434,6 +1465,7 @@ name = "joko_package_models" version = "0.2.1" dependencies = [ "base64", + "bimap", "bytemuck", "cxx-build", "data-encoding", @@ -1455,6 +1487,7 @@ dependencies = [ "tracing", "url", "uuid", + "vector-map", "xot", ] @@ -1951,7 +1984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -2072,6 +2105,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2079,8 +2125,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2090,7 +2146,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2099,7 +2164,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2149,7 +2223,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom", + "getrandom 0.2.12", "libredox", "thiserror", ] @@ -2235,7 +2309,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.12", "libc", "spin", "untrusted", @@ -2935,8 +3009,8 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom", - "rand", + "getrandom 0.2.12", + "rand 0.8.5", "serde", "uuid-macro-internal", ] @@ -2958,12 +3032,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vector-map" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550f72ae94a45c0e2139188709e6c4179f0b5ff9bdaa435239ad19048b0cd68c" +dependencies = [ + "contracts", + "rand 0.7.3", +] + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3363,7 +3453,7 @@ dependencies = [ "hex", "nix 0.28.0", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 9827146..f0dbbfd 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -1,25 +1,30 @@ use joko_core::RelativePath; -use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, map::MapData, marker::Marker, package::PackCore, route::Route, trail::{TBin, TBinStatus, Trail}}; +use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, map::MapData, marker::Marker, package::{PackCore, PackageImportReport}, route::Route, trail::{TBin, TBinStatus, Trail}}; use miette::{bail, Context, IntoDiagnostic, Result}; use crate::BASE64_ENGINE; use base64::Engine; use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; -use std::{collections::{VecDeque, HashMap}, io::Read}; +use std::{collections::{HashMap, VecDeque}, io::Read, str::FromStr}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, error, info, info_span, instrument, trace, warn}; use uuid::Uuid; use xot::{Node, Xot, Element}; -pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { +pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option ) -> Result { //called from already parsed data let mut core_pack = PackCore::new(); + if let Some(mut import_report) = import_report { + import_report.reset_counters(); + import_report.uuid = core_pack.uuid; + core_pack.report = import_report; + } // walks the directory and loads all files into the hashmap let start = std::time::SystemTime::now(); recursive_walk_dir_and_read_images_and_tbins( - dir, + core_dir, &mut core_pack.textures, &mut core_pack.tbins, &RelativePath::default(), @@ -29,7 +34,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { tracing::info!("Loading of core package textures from disk took {} ms", elaspsed.as_millis()); //categories are required to register other objects - let cats_xml = dir + let cats_xml = core_dir .read_to_string("categories.xml") .into_diagnostic() .wrap_err("failed to read categories.xml")?; @@ -41,7 +46,7 @@ pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { info!("parse_categories_file took {} ms", elapsed.as_millis()); // parse map data of the pack - for entry in dir + for entry in core_dir .entries() .into_diagnostic() .wrap_err("failed to read entries of pack dir")? @@ -265,10 +270,10 @@ fn parse_categories( tags: impl Iterator, first_pass_categories: &mut OrderedHashMap, names: &XotAttributeNameIDs, - source_file_name: &String, + source_file_uuid: &Uuid, ) { //called once per file - parse_categories_recursive(pack, tree, tags, first_pass_categories, names, None, source_file_name) + parse_categories_recursive(pack, tree, tags, first_pass_categories, names, None, source_file_uuid) } @@ -280,7 +285,7 @@ fn parse_categories_recursive( first_pass_categories: &mut OrderedHashMap, names: &XotAttributeNameIDs, parent_name: Option, - source_file_name: &String, + source_file_uuid: &Uuid, ) { for tag in tags { let ele = match tree.element(tag) { @@ -324,15 +329,15 @@ fn parse_categories_recursive( let guid = parse_guid(names, ele); trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); if !first_pass_categories.contains_key(&full_category_name) { - let mut sources: OrderedHashMap = OrderedHashMap::new(); + let mut sources: OrderedHashMap = OrderedHashMap::new(); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_inherited_texture(icon_file.as_str().to_string(), full_category_name.clone(), source_file_name); + pack.found_missing_inherited_texture(icon_file.as_str().to_string(), full_category_name.clone(), source_file_uuid); } } - sources.insert(guid.clone(), source_file_name.clone()); + sources.insert(guid.clone(), source_file_uuid.clone()); first_pass_categories.insert(full_category_name.clone(), RawCategory { guid, parent_name: parent_name.clone(), @@ -352,7 +357,7 @@ fn parse_categories_recursive( first_pass_categories, names, Some(full_category_name), - source_file_name + source_file_uuid ); } } @@ -459,12 +464,21 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - let span_guard = info_span!("category", full_category_name).entered(); - let source_file_name = child_element.get_attribute(names._source_file_name).unwrap_or_default().to_string(); - target.source_files.insert(source_file_name.clone(), true); + let opt_source_file_uuid = Uuid::from_str(child_element.get_attribute(names._source_file_name).unwrap_or_default()); + let source_file_uuid = if opt_source_file_uuid.is_err() { + error!("Package corrupted, invalid source file uuid"); + //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); + Uuid::new_v4() + } else { + opt_source_file_uuid.unwrap() + }; + + //There is no file name, only an uuid to register + target.active_source_files.insert(source_file_uuid.clone(), true); if child_element.name() == names.route { debug!("Found a route in core pack {:?}", child_element); - let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_name.clone()); + let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_uuid.clone()); if let Some(route) = route { target.register_route(route)?; } else { @@ -532,7 +546,7 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - parent: category_uuid.clone(), attrs: ca, guid, - source_file_name + source_file_uuid }; if !target.maps.contains_key(&map_id) { @@ -560,7 +574,7 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - props: ca, guid, dynamic: false, - source_file_name + source_file_uuid }; if !target.maps.contains_key(&map_id) { @@ -781,6 +795,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { info!("failed to read file from zip"); continue; }; + let source_file_uuid = pack.register_source_file(source_file_name); let filtered_xml_str = crate::rapid_filter_rust(xml_str); let mut tree = Xot::new(); @@ -804,7 +819,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { } }; - parse_categories(&mut pack, &tree, tree.children(od), &mut first_pass_categories, &names, &source_file_name); + parse_categories(&mut pack, &tree, tree.children(od), &mut first_pass_categories, &names, &source_file_uuid); drop(span_guard); } span_guard_first_pass.exit(); @@ -826,6 +841,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { info!("failed to read file from zip"); continue; }; + let source_file_uuid = pack.register_source_file(source_file_name); let filtered_xml_str = crate::rapid_filter_rust(xml_str); let mut tree = Xot::new(); @@ -887,8 +903,8 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { let guid = parse_guid(&names, child_element); if !pack.category_exists(&full_category_name) && ! first_pass_categories.contains_key(&full_category_name) { let category_uuid = Uuid::new_v4(); - let mut sources: OrderedHashMap = OrderedHashMap::new(); - sources.insert(guid.clone(), source_file_name.clone()); + let mut sources: OrderedHashMap = OrderedHashMap::new(); + sources.insert(guid.clone(), source_file_uuid.clone()); first_pass_categories.insert(full_category_name.clone(), RawCategory{ default_enabled: true, guid: category_uuid, @@ -903,7 +919,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { debug!("There is an orphan missing category '{}' which was created", full_category_name); } else { let cat = first_pass_categories.get_mut(&full_category_name); - cat.unwrap().sources.insert(guid.clone(), source_file_name.clone()); + cat.unwrap().sources.insert(guid.clone(), source_file_uuid.clone()); } } drop(span_guard); @@ -946,6 +962,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { info!("failed to read file from zip"); continue; }; + let source_file_uuid = pack.register_source_file(source_file_name); let filtered_xml_str = crate::rapid_filter_rust(xml_str); let mut tree = Xot::new(); @@ -993,7 +1010,7 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { debug!("import element: {:?}", child_element); if child_element.name() == names.route { - let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, source_file_name.clone()); + let route = parse_route(&names, &tree, &child_node, child_element, &full_category_name, source_file_uuid.clone()); if let Some(mut route) = route { //one must not create category anymore route.parent = pack.get_category_uuid(&route.category).unwrap().clone(); @@ -1010,15 +1027,15 @@ pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { panic!("Missing category {}, previous pass should have taken care of this", full_category_name); } let guid = parse_guid(&names, child_element); - let category_uuid = pack.get_or_create_category_uuid(&full_category_name, guid, source_file_name); + let category_uuid = pack.get_or_create_category_uuid(&full_category_name, guid, &source_file_uuid); if child_element.name() == names.poi { - if let Some(marker) = parse_marker(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_name.clone()) { + if let Some(marker) = parse_marker(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_uuid.clone()) { pack.register_marker(full_category_name, marker)?; } else { debug!("Could not parse POI"); } } else if child_element.name() == names.trail { - if let Some(trail) = parse_trail(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_name.clone()) { + if let Some(trail) = parse_trail(&mut pack, &names, child_element, guid, &full_category_name, &category_uuid, source_file_uuid.clone()) { pack.register_trail(full_category_name, trail)?; } else { debug!("Could not parse Trail"); @@ -1061,17 +1078,17 @@ fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid{ parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { +fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_uuid: Uuid) -> Option { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(poi_element, &names); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_element_texture(icon_file.as_str().to_string(), guid, &source_file_name); + pack.found_missing_element_texture(icon_file.as_str().to_string(), guid, &source_file_uuid); } } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { debug!(icf, "marker's icon file attribute failed to parse"); - pack.found_missing_element_texture(icf.to_string(), guid, &source_file_name); + pack.found_missing_element_texture(icf.to_string(), guid, &source_file_uuid); } if let Some(map_id) = poi_element @@ -1100,7 +1117,7 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & parent: category_uuid.clone(), attrs: common_attributes, guid, - source_file_name + source_file_uuid }) } else { debug!("missing map id"); @@ -1155,7 +1172,7 @@ fn parse_route( route_node: &Node, route_element: &Element, category_name: &String, - source_file_name: String + source_file_uuid: Uuid ) -> Option { let mut path: Vec = Vec::new(); @@ -1235,12 +1252,12 @@ fn parse_route( map_id: map_id.unwrap(), name: name.unwrap().into(), guid: parse_guid(names, &route_element), - source_file_name, + source_file_uuid, }) } -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_name: String) -> Option { +fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_uuid: Uuid) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html let mut common_attributes = CommonAttributes::default(); @@ -1249,7 +1266,7 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: if let Some(tex) = common_attributes.get_texture() { if !pack.textures.contains_key(tex) { info!(%tex, "failed to find this texture in this pack"); - pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_name); + pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_uuid); } } @@ -1261,7 +1278,7 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: if let Some(tb) = pack.tbins.get(&file_path) { Some(tb.map_id) }else { - pack.found_missing_trail(&file_path, guid, &source_file_name); + pack.found_missing_trail(&file_path, guid, &source_file_uuid); None } }) @@ -1274,7 +1291,7 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: props: common_attributes, guid, dynamic: false, - source_file_name, + source_file_uuid, }) } else { /*let td = trail_element.get_attribute(names.trail_data); diff --git a/crates/joko_package/src/io/export.rs b/crates/joko_package/src/io/export.rs index 922189b..7108ab3 100644 --- a/crates/joko_package/src/io/export.rs +++ b/crates/joko_package/src/io/export.rs @@ -189,7 +189,7 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); ele.set_attribute(names.category, &trail.category); ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute(names._source_file_name, &trail.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", trail.source_file_uuid)); trail.props.serialize_to_element(ele, names); } @@ -200,7 +200,7 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); - ele.set_attribute(names._source_file_name, &marker.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", marker.source_file_uuid)); marker.attrs.serialize_to_element(ele, names); } @@ -220,7 +220,7 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); ele.set_attribute(names.map_id, format!("{}", route.map_id)); ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute(names._source_file_name, &route.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", route.source_file_uuid)); for pos in &route.path { let child = tree.new_element(names.poi); tree.append(route_node, child); diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index 08cf09e..a44b7b2 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -180,7 +180,7 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); ele.set_attribute(names.category, &trail.category); ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute(names._source_file_name, &trail.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", trail.source_file_uuid)); trail.props.serialize_to_element(ele, names); } @@ -191,7 +191,7 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); - ele.set_attribute(names._source_file_name, &marker.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", marker.source_file_uuid)); marker.attrs.serialize_to_element(ele, names); } @@ -211,7 +211,7 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); ele.set_attribute(names.map_id, format!("{}", route.map_id)); ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute(names._source_file_name, &route.source_file_name); + ele.set_attribute(names._source_file_name, format!("{}", route.source_file_uuid)); for pos in &route.path { let child = tree.new_element(names.poi); tree.append(route_node, child); diff --git a/crates/joko_package/src/manager/pack/file_selection.rs b/crates/joko_package/src/manager/pack/file_selection.rs index 0d4a4fa..8210caa 100644 --- a/crates/joko_package/src/manager/pack/file_selection.rs +++ b/crates/joko_package/src/manager/pack/file_selection.rs @@ -1,16 +1,18 @@ use std::collections::BTreeMap; +use uuid::Uuid; + pub struct SelectedFileManager { - data: BTreeMap, + data: BTreeMap, } impl<'a> SelectedFileManager { pub fn new( - selected_files: &BTreeMap, - pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, + selected_files: &BTreeMap, + pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, ) -> Self { - let mut list_of_enabled_files: BTreeMap = Default::default(); + let mut list_of_enabled_files: BTreeMap = Default::default(); SelectedFileManager::recursive_get_full_names( &selected_files, &pack_source_files, @@ -20,21 +22,21 @@ impl<'a> SelectedFileManager { Self { data: list_of_enabled_files } } fn recursive_get_full_names( - _selected_files: &BTreeMap, - _pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, - list_of_enabled_files: &mut BTreeMap + _selected_files: &BTreeMap, + _pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + list_of_enabled_files: &mut BTreeMap ){ for (key, v) in currently_used_files.iter() { list_of_enabled_files.insert(key.clone(), *v); } } - pub fn cloned_data(&self) -> BTreeMap { + pub fn cloned_data(&self) -> BTreeMap { self.data.clone() } - pub fn is_selected(&self, source_file_name: &String) -> bool { + pub fn is_selected(&self, source_file_uuid: &Uuid) -> bool { let default = false; - self.data.is_empty() || *self.data.get(source_file_name).unwrap_or(&default) + self.data.is_empty() || *self.data.get(source_file_uuid).unwrap_or(&default) } pub fn len(&self) -> usize { self.data.len() diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 5f4c76f..8a5cbe3 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -51,9 +51,9 @@ pub struct LoadedPackData { //pub core: PackCore, pub categories: OrderedHashMap, pub all_categories: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, - selected_files: BTreeMap, + selected_files: BTreeMap, _is_dirty: bool,//there was an edition in the package itself // loca copy in the data side of what is exposed in UI @@ -72,6 +72,7 @@ pub struct LoadedPackTexture { /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. /// eg: Active categories, activation data etc.. pub dir: Arc, + pub source_files: BTreeMap, pub tbins: HashMap, pub textures: HashMap>, @@ -80,7 +81,6 @@ pub struct LoadedPackTexture { current_map_data: CurrentMapData, activation_data: ActivationData, active_elements: HashSet,//which are the active elements (loaded) - //pub report: ImportQualityReport,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. _is_dirty: bool, } @@ -142,7 +142,7 @@ impl PackTasks { pack: &mut LoadedPackData, b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, - currently_used_files: &BTreeMap + currently_used_files: &BTreeMap ) { //TODO //self.load_map_task.lock().unwrap().send(pack); @@ -271,6 +271,28 @@ impl LoadedPackData { cs }) } + + fn load_import_report(pack_dir: &Arc) -> Option { + //FIXME: we need to patch those categories from the one in the files + (if pack_dir.is_file(PackageImportReport::REPORT_FILE_NAME) { + match pack_dir.read_to_string(PackageImportReport::REPORT_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize import report"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of import report"); + None + } + } + } else { + None + }) + .flatten() + } pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { if !pack_dir .try_exists(Self::CORE_PACK_DIR_NAME) @@ -284,7 +306,8 @@ impl LoadedPackData { .into_diagnostic() .wrap_err("failed to open core pack directory")?; let start = std::time::SystemTime::now(); - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + let import_report = LoadedPackData::load_import_report(&pack_dir); + let core = load_pack_core_from_dir(&core_dir, import_report).wrap_err("failed to load pack from dir")?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); @@ -300,7 +323,7 @@ impl LoadedPackData { all_categories: core.all_categories, categories: core.categories, maps: core.maps, - source_files: core.source_files, + source_files: core.active_source_files, _is_dirty: false, active_elements: Default::default(), activation_data: Default::default(), @@ -341,7 +364,7 @@ impl LoadedPackData { b2u_sender: &std::sync::mpsc::Sender, loop_index: u128, link: &MumbleLink, - currently_used_files: &BTreeMap, + currently_used_files: &BTreeMap, list_of_active_or_selected_elements_changed: bool, map_changed: bool, tasks: &PackTasks, @@ -362,7 +385,7 @@ impl LoadedPackData { &mut self, b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, - currently_used_files: &BTreeMap, + currently_used_files: &BTreeMap, active_elements: &mut HashSet, ){ info!(link.map_id, "current map data is updated. {}", self.name); @@ -388,7 +411,7 @@ impl LoadedPackData { .enumerate() { nb_markers_attempt += 1; - if selected_files_manager.is_selected(&marker.source_file_name) { + if selected_files_manager.is_selected(&marker.source_file_uuid) { active_elements.insert(marker.guid); active_elements.insert(marker.parent); if selected_categories_manager.is_selected(&marker.parent) { @@ -465,7 +488,7 @@ impl LoadedPackData { .enumerate() { nb_trails_attempt += 1; - if selected_files_manager.is_selected(&trail.source_file_name) { + if selected_files_manager.is_selected(&trail.source_file_uuid) { active_elements.insert(trail.guid); active_elements.insert(trail.parent); if selected_categories_manager.is_selected(&trail.parent) { @@ -764,7 +787,8 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result { .into_diagnostic() .wrap_err("failed to open core pack directory")?; let start = std::time::SystemTime::now(); - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; + let import_report = LoadedPackData::load_import_report(&pack_dir); + let core = load_pack_core_from_dir(&core_dir, import_report).wrap_err("failed to load pack from dir")?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); let res = build_from_core(name.clone(), pack_dir, core); @@ -782,7 +806,7 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo all_categories: core.all_categories, categories: core.categories, maps: core.maps, - source_files: core.source_files, + source_files: core.active_source_files.clone(), _is_dirty: false, activation_data: Default::default(), active_elements: Default::default(), @@ -819,6 +843,7 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo name: name, tbins: core.tbins, active_elements: Default::default(), + source_files: core.active_source_files }; let report = core.report; (data, tex, report) diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index cdbe22c..08949a9 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -54,7 +54,7 @@ pub struct PackageDataManager { /// This allows us to avoid saving the pack too often. pub save_interval: f64, - pub currently_used_files: BTreeMap, + pub currently_used_files: BTreeMap, parents: HashMap, loaded_elements: HashSet, on_screen: BTreeSet, @@ -67,7 +67,7 @@ pub struct PackageUIManager { reports: BTreeMap, tasks: PackTasks, - currently_used_files: BTreeMap, + currently_used_files: BTreeMap, all_files_tribool: Tribool, all_files_toggle: bool, show_only_active: bool, @@ -98,7 +98,7 @@ impl PackageDataManager { }) } - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { self.currently_used_files = currently_used_files; } @@ -181,7 +181,7 @@ impl PackageDataManager { link: Option<&MumbleLink>, choice_of_category_changed: bool, ) { - let mut currently_used_files: BTreeMap = Default::default(); + let mut currently_used_files: BTreeMap = Default::default(); let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); match link { @@ -194,18 +194,18 @@ impl PackageDataManager { for pack in self.packs.values_mut() { if let Some(current_map) = pack.maps.get(&link.map_id) { for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_name) { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { currently_used_files.insert( - marker.source_file_name.clone(), - *self.currently_used_files.get(&marker.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + marker.source_file_uuid.clone(), + *self.currently_used_files.get(&marker.source_file_uuid).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) ); } } for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_name) { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { currently_used_files.insert( - trail.source_file_name.clone(), - *self.currently_used_files.get(&trail.source_file_name).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + trail.source_file_uuid.clone(), + *self.currently_used_files.get(&trail.source_file_uuid).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) ); } } @@ -248,7 +248,7 @@ impl PackageDataManager { self.packs.remove(&uuid); } } - pub fn save(&mut self, mut data_pack: LoadedPackData, mut report: PackageImportReport) -> Uuid { + pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { let mut to_delete: Vec = Vec::new(); for (uuid, pack) in self.packs.iter() { if pack.name == data_pack.name { @@ -347,7 +347,7 @@ impl PackageUIManager { self.reports.remove(&uuid); } } - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { self.currently_used_files = currently_used_files; } @@ -554,19 +554,30 @@ impl PackageUIManager { } else { ui.checkbox(&mut self.all_files_toggle, "File"); } - ui.label("Trails"); - ui.label("Markers"); + //ui.label("Trails"); + //ui.label("Markers"); ui.end_row(); - for file in self.currently_used_files.iter_mut() { - let cb = ui.checkbox(file.1, file.0.clone()); - if cb.changed() { - files_changed = true; + for pack in self.packs.values_mut() { + let report = self.reports.get(&pack.uuid).unwrap(); + for (source_file_uuid, is_selected) in pack.source_files.iter_mut() { + if self.currently_used_files.contains_key(source_file_uuid) { + //reports may be corrupted or not loaded, files are there + if let Some(source_file_name) = report.source_file_uuid_to_name(source_file_uuid) { + //FIXME: format the file from reports and packages + ensure there is the package name as a prefix + let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_name)); + if cb.changed() { + files_changed = true; + } + } else { + let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_uuid)); + if cb.changed() { + files_changed = true; + } + } + ui.end_row(); + } } - if ui.button("Edit").clicked() { - println!("click {}", file.0.clone()); - } - ui.end_row(); } ui.end_row(); }) diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index 0aaf667..65e4ec3 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -16,7 +16,7 @@ use crate::LoadedPackTexture; pub enum BackToUIMessage { ActiveElements(HashSet),//list of all elements that are loaded for current map - CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display + CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display LoadedPack(LoadedPackTexture, PackageImportReport),//push a loaded pack to UI DeletedPacks(Vec),//push a deleted set of packs to UI FirstLoadDone, @@ -32,7 +32,7 @@ pub enum BackToUIMessage { } pub enum UIToBackMessage { - ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. + ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. CategoryActivationElementStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. CategoryActivationBranchStatusChange(Uuid, bool),//same, for a whole branch CategoryActivationStatusChanged,//something happened that needs to reload the whole set diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml index a6af7b8..74780b8 100644 --- a/crates/joko_package_models/Cargo.toml +++ b/crates/joko_package_models/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # jmf deps # for marker packs base64 = "0.21.2" +bimap = { version = "0.6.3", features = ["serde"] } bytemuck = { workspace = true } data-encoding = "2.4.0" enumflags2 = { workspace = true } @@ -25,6 +26,7 @@ smol_str = { workspace = true } tracing = { workspace = true } url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +vector-map = "1.0.1" xot = { version = "0.16.0" } diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 2288022..a7daf42 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -13,7 +13,7 @@ pub struct RawCategory { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub sources: OrderedHashMap, + pub sources: OrderedHashMap, } #[derive(Debug, Clone)] @@ -76,7 +76,7 @@ impl Category { children: Default::default() } } - pub fn per_route<'a>(categories: &'a mut OrderedHashMap, route: &Vec<&str>, depth: usize) -> Option<&'a mut Category> { + fn per_route<'a>(categories: &'a mut OrderedHashMap, route: &Vec<&str>, depth: usize) -> Option<&'a mut Category> { let mut route = route.clone(); route.reverse(); Category::_per_route(categories, &mut route, depth) @@ -95,7 +95,7 @@ impl Category { } return None; } - pub fn per_uuid<'a>(categories: &'a mut OrderedHashMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + fn per_uuid<'a>(categories: &'a mut OrderedHashMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { /* Do a look up in the tree based on uuid. Whole tree is scanned until a match is found. @@ -152,7 +152,7 @@ impl Category { let new_uuid = Uuid::new_v4(); let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); - let sources: OrderedHashMap = OrderedHashMap::new(); + let sources: OrderedHashMap = OrderedHashMap::new(); let to_insert = RawCategory { default_enabled: value.default_enabled, guid: new_uuid, @@ -171,8 +171,8 @@ impl Category { } n += 1; } - for (requester_uuid, source_file_name) in value.sources.iter() { - report.found_category_late_with_details(&value.full_category_name, value.guid, requester_uuid, source_file_name); + for (requester_uuid, source_file_uuid) in value.sources.iter() { + report.found_category_late_with_details(&value.full_category_name, value.guid, requester_uuid, source_file_uuid); } report.found_category_late(&value.full_category_name, value.guid); to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); diff --git a/crates/joko_package_models/src/marker.rs b/crates/joko_package_models/src/marker.rs index 5f00ff6..d4b446b 100644 --- a/crates/joko_package_models/src/marker.rs +++ b/crates/joko_package_models/src/marker.rs @@ -10,6 +10,6 @@ pub struct Marker { pub position: Vec3, pub map_id: u32, pub category: String, - pub source_file_name: String, + pub source_file_uuid: Uuid, pub attrs: CommonAttributes, } diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 840c1af..f0d41c3 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -1,9 +1,9 @@ use base64::Engine; use joko_core::RelativePath; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer, Deserializer}; use tracing::{debug, trace}; use uuid::Uuid; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use ordered_hash_map::OrderedHashMap; use crate::marker::Marker; use crate::route::{route_to_tbin, route_to_trail, Route}; @@ -31,6 +31,21 @@ where S: Serializer } } +fn deserialize_reference<'de, D>(deserializer: D) -> Result +where D: Deserializer<'de> +{ + let encoded_uuid_or_full_category_name = String::deserialize(deserializer)?; + if let Ok(bytes) = BASE64_ENGINE.decode(encoded_uuid_or_full_category_name.as_bytes()) { + let mut uuid_bytes: [u8; 16] = Default::default(); + uuid_bytes.copy_from_slice(bytes.as_slice()); + let res = Uuid::from_bytes(uuid_bytes); + Ok(ElementReference::Uuid(res)) + } else { + Ok(ElementReference::Category(encoded_uuid_or_full_category_name)) + } +} + + fn serialize_uuid_in_base64(uuid: &Uuid, serializer: S) -> Result where S: Serializer { @@ -38,28 +53,43 @@ where S: Serializer serializer.serialize_str(to_do.as_str()) } +fn deserialize_uuid_in_base64<'de, D>(deserializer: D) -> Result +where D: Deserializer<'de> +{ + let encoded = String::deserialize(deserializer)?; + if let Ok(bytes) = BASE64_ENGINE.decode(encoded.as_bytes()) { + let mut uuid_bytes: [u8; 16] = Default::default(); + uuid_bytes.copy_from_slice(bytes.as_slice()); + let res = Uuid::from_bytes(uuid_bytes); + Ok(res) + } else { + Err(serde::de::Error::custom("Could not parse base64 encoded uuid")) + } + +} + -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct PackageCategorySource { full_category_name: String, - #[serde(serialize_with= "serialize_uuid_in_base64")] + #[serde(serialize_with= "serialize_uuid_in_base64", deserialize_with= "deserialize_uuid_in_base64")] requester_uuid: Uuid, source_file_name: String, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] enum ElementReference { Uuid(Uuid), Category(String), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct PackageElementSource { file_path: String, - #[serde(serialize_with= "serialize_reference")] + #[serde(serialize_with= "serialize_reference", deserialize_with = "deserialize_reference")] requester_reference: ElementReference, source_file_name: String, } -#[derive(Default, Debug, Clone, Serialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportStatistics { categories: usize, // total number of found categories missing_categories: usize, // categories that should be defined in a node @@ -70,10 +100,11 @@ pub struct PackageImportStatistics { trails: usize, // total number of trails routes: usize, // total number of routes defined, they shall not count as trails even if imported as such maps: usize, // total number of maps covered + source_files: usize, // total number of XML files } -#[derive(Default, Debug, Clone, Serialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportReassembleTelemetry { pub total: u128, pub initialize: u128, @@ -81,7 +112,7 @@ pub struct PackageImportReassembleTelemetry { pub parent_child_relationship: u128, pub tree_insertion: u128, } -#[derive(Default, Debug, Clone, Serialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportTelemetry { pub total: u128, pub texture_loading: u128, @@ -92,7 +123,7 @@ pub struct PackageImportTelemetry { pub elements_registering: u128, pub categories_reassemble: PackageImportReassembleTelemetry, } -#[derive(Default, Debug, Clone, Serialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportReport { #[serde(skip)] pub uuid: Uuid, @@ -106,6 +137,7 @@ pub struct PackageImportReport { _missing_textures_tracker: HashSet, // for tracking purpose to avoid duplicate missing_textures: Vec,//missing texture for display missing_trails: Vec,//missing file for trail + source_files: bimap::BiMap, //map of all files to uuid. When exporting this shall have to be reversed. } #[derive(Debug, Clone)] @@ -120,7 +152,7 @@ pub struct PackCore { pub categories: OrderedHashMap, pub all_categories: HashMap, pub entities_parents: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub active_source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, pub report: PackageImportReport, } @@ -128,6 +160,10 @@ pub struct PackCore { impl PackageImportReport { pub const REPORT_FILE_NAME: &'static str = "import_report.json"; + + pub fn reset_counters(&mut self) { + self.number_of = Default::default(); + } fn merge_partial(&mut self, partial_report: PackageImportReport) { self.late_discovered_categories.extend(partial_report.late_discovered_categories); } @@ -136,11 +172,19 @@ impl PackageImportReport { self.late_discovered_categories.contains_key(&uuid) } + pub fn source_file_uuid_to_name(&self, source_file_uuid: &Uuid) -> Option<&String> { + self.source_files.get_by_right(source_file_uuid) + } + pub fn source_file_name_to_uuid(&self, source_file_name: &String) -> Option<&Uuid> { + self.source_files.get_by_left(source_file_name) + } + pub fn found_category_late(&mut self, full_category_name: &String, category_uuid: Uuid) { self.late_discovered_categories.insert(category_uuid, full_category_name.clone()); } - pub fn found_category_late_with_details(&mut self, full_category_name: &String, category_uuid: Uuid, requester_uuid: &Uuid, source_file_name: &String) { + pub fn found_category_late_with_details(&mut self, full_category_name: &String, category_uuid: Uuid, requester_uuid: &Uuid, source_file_uuid: &Uuid) { self.found_category_late(full_category_name, category_uuid); + let source_file_name = self.source_files.get_by_right(source_file_uuid).unwrap(); //for this to work we need to keep track of where each category was called and thus defined since late self.missing_categories.push(PackageCategorySource{ @@ -160,7 +204,6 @@ impl PackageImportReport { } } - } @@ -173,7 +216,7 @@ impl PackCore { entities_parents: Default::default(), report: Default::default(), maps: Default::default(), - source_files: Default::default(), + active_source_files: Default::default(), tbins: Default::default(), textures: Default::default(), uuid: Default::default(), @@ -193,7 +236,7 @@ impl PackCore { self.maps.extend(partial_pack.maps); self.all_categories = partial_pack.all_categories; self.report.merge_partial(partial_pack.report); - self.source_files.extend(partial_pack.source_files); + self.active_source_files.extend(partial_pack.active_source_files); self.tbins.extend(partial_pack.tbins); self.entities_parents.extend(partial_pack.entities_parents); } @@ -205,7 +248,7 @@ impl PackCore { self.all_categories.get(full_category_name) } - pub fn get_or_create_category_uuid(&mut self, full_category_name: &String, requester_uuid: Uuid, source_file_name: &String) -> Uuid { + pub fn get_or_create_category_uuid(&mut self, full_category_name: &String, requester_uuid: Uuid, source_file_uuid: &Uuid) -> Uuid { if let Some(category_uuid) = self.all_categories.get(full_category_name) { category_uuid.clone() } else { @@ -225,7 +268,7 @@ impl PackCore { let new_uuid = Uuid::new_v4(); debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); self.all_categories.insert(parent_full_category_name.clone(), new_uuid); - self.report.found_category_late_with_details(&full_category_name, new_uuid, &requester_uuid, source_file_name); + self.report.found_category_late_with_details(&full_category_name, new_uuid, &requester_uuid, source_file_uuid); last_uuid = Some(new_uuid); } } @@ -235,7 +278,22 @@ impl PackCore { } } + pub fn get_source_file_uuid(&mut self, source_file_name: &String) -> Uuid { + // Must always exist when called since we registered the file already. + *self.report.source_files.get_by_left(source_file_name).unwrap() + } + pub fn register_source_file(&mut self, source_file_name: &String) -> Uuid { + if !self.report.source_files.contains_left(source_file_name) { + let uuid_to_insert = Uuid::new_v4();//TODO: have a uuid built from current package name and source file name + self.report.source_files.insert(source_file_name.clone(), uuid_to_insert); + self.report.number_of.source_files += 1; + self.active_source_files.insert(uuid_to_insert, true); + uuid_to_insert + } else { + self.get_source_file_uuid(source_file_name) + } + } pub fn register_texture(&mut self, name: String, file_path: &RelativePath, bytes: Vec) { assert!( self.textures.insert(file_path.clone(), bytes).is_none(), @@ -326,16 +384,18 @@ impl PackCore { } } - pub fn found_missing_element_texture(&mut self, file_path: String, requester_uuid: Uuid, source_file_name: &String) { + pub fn found_missing_element_texture(&mut self, file_path: String, requester_uuid: Uuid, source_file_uuid: &Uuid) { self.report.found_missing_texture(&file_path); + let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); self.report.missing_textures.push(PackageElementSource{ file_path, requester_reference: ElementReference::Uuid(requester_uuid), source_file_name: source_file_name.clone() }); } - pub fn found_missing_inherited_texture(&mut self, file_path: String, full_category_name: String, source_file_name: &String) { + pub fn found_missing_inherited_texture(&mut self, file_path: String, full_category_name: String, source_file_uuid: &Uuid) { self.report.found_missing_texture(&file_path); + let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); self.report.missing_textures.push(PackageElementSource{ file_path, requester_reference: ElementReference::Category(full_category_name), @@ -343,7 +403,8 @@ impl PackCore { }); } - pub fn found_missing_trail(&mut self, file_path: &RelativePath, requester_uuid: Uuid, source_file_name: &String) { + pub fn found_missing_trail(&mut self, file_path: &RelativePath, requester_uuid: Uuid, source_file_uuid: &Uuid) { + let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); self.report.missing_trails.push(PackageElementSource{ file_path: file_path.as_str().to_string(), requester_reference: ElementReference::Uuid(requester_uuid), diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs index 81689a4..ef547ce 100644 --- a/crates/joko_package_models/src/route.rs +++ b/crates/joko_package_models/src/route.rs @@ -14,7 +14,7 @@ pub struct Route { pub map_id: u32, pub guid: Uuid, pub name: String, - pub source_file_name: String, + pub source_file_uuid: Uuid, } @@ -39,7 +39,7 @@ pub(crate) fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { guid: route.guid, props: props, dynamic: true, - source_file_name: route.source_file_name.clone(), + source_file_uuid: route.source_file_uuid.clone(), } } diff --git a/crates/joko_package_models/src/trail.rs b/crates/joko_package_models/src/trail.rs index ebca7ba..6669463 100644 --- a/crates/joko_package_models/src/trail.rs +++ b/crates/joko_package_models/src/trail.rs @@ -10,7 +10,7 @@ pub struct Trail { pub category: String, pub props: CommonAttributes, pub dynamic: bool, - pub source_file_name: String, + pub source_file_uuid: Uuid, } #[derive(Debug, Clone)] From 193df5f1b8331271c331fe77224aaac9725e13a3 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 14 Apr 2024 13:37:38 +0200 Subject: [PATCH 29/54] trim useless dependancy --- Cargo.lock | 108 +++------------------- crates/joko_package_models/Cargo.toml | 1 - crates/joko_package_models/src/package.rs | 4 +- 3 files changed, 16 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11bbd69..2bafb91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.12", + "getrandom", "once_cell", "serde", "version_check", @@ -130,7 +130,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.8.5", + "rand", "serde", "serde_repr", "url", @@ -594,17 +594,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "contracts" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9424f2ca1e42776615720e5746eed6efa19866fdbaac2923ab51c294ac4d1f2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -831,7 +820,7 @@ checksum = "21691a0388394a02b9352fb31edc7a008645ef1af13bc6eace5da06c2f599e60" dependencies = [ "bytemuck", "egui", - "getrandom 0.2.12", + "getrandom", "glow", "js-sys", "raw-window-handle 0.6.0", @@ -1172,17 +1161,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.12" @@ -1192,7 +1170,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1487,7 +1465,6 @@ dependencies = [ "tracing", "url", "uuid", - "vector-map", "xot", ] @@ -1984,7 +1961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand", ] [[package]] @@ -2105,19 +2082,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -2125,18 +2089,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -2146,16 +2100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -2164,16 +2109,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", + "getrandom", ] [[package]] @@ -2223,7 +2159,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.12", + "getrandom", "libredox", "thiserror", ] @@ -2309,7 +2245,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.12", + "getrandom", "libc", "spin", "untrusted", @@ -3009,8 +2945,8 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.12", - "rand 0.8.5", + "getrandom", + "rand", "serde", "uuid-macro-internal", ] @@ -3032,28 +2968,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vector-map" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "550f72ae94a45c0e2139188709e6c4179f0b5ff9bdaa435239ad19048b0cd68c" -dependencies = [ - "contracts", - "rand 0.7.3", -] - [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3453,7 +3373,7 @@ dependencies = [ "hex", "nix 0.28.0", "ordered-stream", - "rand 0.8.5", + "rand", "serde", "serde_repr", "sha1", diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml index 74780b8..af29fc6 100644 --- a/crates/joko_package_models/Cargo.toml +++ b/crates/joko_package_models/Cargo.toml @@ -26,7 +26,6 @@ smol_str = { workspace = true } tracing = { workspace = true } url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } -vector-map = "1.0.1" xot = { version = "0.16.0" } diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index f0d41c3..82a4a56 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -3,7 +3,7 @@ use joko_core::RelativePath; use serde::{Deserialize, Serialize, Serializer, Deserializer}; use tracing::{debug, trace}; use uuid::Uuid; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use ordered_hash_map::OrderedHashMap; use crate::marker::Marker; use crate::route::{route_to_tbin, route_to_trail, Route}; @@ -120,8 +120,8 @@ pub struct PackageImportTelemetry { pub categories_first_pass: u128, pub categories_second_pass: u128, pub categories_registering: u128, - pub elements_registering: u128, pub categories_reassemble: PackageImportReassembleTelemetry, + pub elements_registering: u128, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportReport { From a2ac560c7d51f444eded2cea70f8593d3acf6fc1 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 14 Apr 2024 14:51:33 +0200 Subject: [PATCH 30/54] toggle buttons on file manager, global and per pack + trim useless dependancy --- Cargo.lock | 7 --- crates/joko_package/Cargo.toml | 1 - crates/joko_package/src/manager/package.rs | 59 +++++++++++++++------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bafb91..a843c1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1431,7 +1431,6 @@ dependencies = [ "smol_str", "time", "tracing", - "tribool", "url", "uuid", "xot", @@ -2807,12 +2806,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tribool" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8660361502033a51e119386b47fbb811e5706722f2e91ccf867aa6b2b09f90" - [[package]] name = "ttf-parser" version = "0.20.0" diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml index f795f14..10da30d 100644 --- a/crates/joko_package/Cargo.toml +++ b/crates/joko_package/Cargo.toml @@ -34,7 +34,6 @@ serde_json = { workspace = true } smol_str = { workspace = true } time = { workspace = true , features = ["serde"]} tracing = { workspace = true } -tribool = "0.3.0" url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } xot = { version = "0.16.0" } diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index 08949a9..5a22e62 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -1,10 +1,9 @@ use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex} + collections::{BTreeMap, HashMap, HashSet}, sync::{Arc, Mutex} }; use glam::Vec3; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; -use tribool::Tribool; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; use image::EncodableLayout; @@ -17,7 +16,7 @@ use miette::Result; use uuid::Uuid; use crate::message::{UIToBackMessage, UIToUIMessage}; -use crate::{message::BackToUIMessage}; +use crate::message::BackToUIMessage; use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; use crate::manager::pack::import::ImportStatus; @@ -49,7 +48,6 @@ pub struct PackageDataManager { pub packs: BTreeMap, tasks: PackTasks, current_map_id: u32, - show_only_active: bool, /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. /// This allows us to avoid saving the pack too often. pub save_interval: f64, @@ -57,7 +55,6 @@ pub struct PackageDataManager { pub currently_used_files: BTreeMap, parents: HashMap, loaded_elements: HashSet, - on_screen: BTreeSet, } #[must_use] pub struct PackageUIManager { @@ -68,8 +65,7 @@ pub struct PackageUIManager { tasks: PackTasks, currently_used_files: BTreeMap, - all_files_tribool: Tribool, - all_files_toggle: bool, + all_files_activation_status: bool,// this consume a change of display event show_only_active: bool, } @@ -90,11 +86,9 @@ impl PackageDataManager { //_marker_manager_dir: marker_manager_dir.into(), current_map_id: 0, save_interval: 0.0, - show_only_active: true, currently_used_files: Default::default(), parents: Default::default(), loaded_elements: Default::default(), - on_screen: Default::default(), }) } @@ -302,8 +296,7 @@ impl PackageUIManager { default_marker_texture: None, default_trail_texture: None, - all_files_tribool: Tribool::True, - all_files_toggle: false, + all_files_activation_status: false, show_only_active: true, currently_used_files: Default::default()// UI copy to (de-)activate files } @@ -549,27 +542,55 @@ impl PackageUIManager { .num_columns(4) .striped(true) .show(ui, |ui| { - if self.all_files_tribool.is_indeterminate(){ - ui.add(egui::Checkbox::new(&mut self.all_files_toggle, "File").indeterminate(true)); - } else { - ui.checkbox(&mut self.all_files_toggle, "File"); - } + let mut all_files_toggle = false; + ui.horizontal(|ui|{ + if ui.button("activate all").clicked() { + self.all_files_activation_status = true; + all_files_toggle = true; + } + if ui.button("deactivate all").clicked() { + self.all_files_activation_status = false; + all_files_toggle = true; + } + }); //ui.label("Trails"); //ui.label("Markers"); ui.end_row(); for pack in self.packs.values_mut() { let report = self.reports.get(&pack.uuid).unwrap(); - for (source_file_uuid, is_selected) in pack.source_files.iter_mut() { - if self.currently_used_files.contains_key(source_file_uuid) { + let mut pack_files_toggle = false; + let mut pack_files_activation_status = true; + ui.horizontal(|ui|{ + ui.label(&pack.name); + if ui.button("activate all").clicked() { + pack_files_activation_status = true; + pack_files_toggle = true; + } + if ui.button("deactivate all").clicked() { + pack_files_activation_status = false; + pack_files_toggle = true; + } + }); + ui.end_row(); + for source_file_uuid in pack.source_files.keys() { + if let Some(is_selected) = self.currently_used_files.get_mut(source_file_uuid) { + if all_files_toggle { + *is_selected = self.all_files_activation_status; + } + if pack_files_toggle { + *is_selected = pack_files_activation_status; + } + ui.add_space(3.0); //reports may be corrupted or not loaded, files are there if let Some(source_file_name) = report.source_file_uuid_to_name(source_file_uuid) { - //FIXME: format the file from reports and packages + ensure there is the package name as a prefix + //format the file from reports and packages + prefix with the package name let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_name)); if cb.changed() { files_changed = true; } } else { + // Import report is corrupted, only print reference let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_uuid)); if cb.changed() { files_changed = true; From 7a9016ca830bdbe9f39fa67f6ffdc317896f7d8d Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 17 Apr 2024 01:34:26 +0200 Subject: [PATCH 31/54] some cleanup in import to have same functions used for both zip and dir + set maximum on window size to avoid crash in mumble editable mode --- Cargo.toml | 3 +- README.md | 12 ++-- crates/joko_package/src/io/deserialize.rs | 68 +++++++++------------- crates/joko_package/src/manager/package.rs | 54 +++++++++++++---- crates/joko_package_models/src/package.rs | 22 +++---- crates/joko_render/src/billboard.rs | 8 ++- crates/joko_render/src/renderer.rs | 2 +- crates/jokolay/src/app/mod.rs | 49 +++++++++++----- crates/jokolink/src/lib.rs | 18 +++--- crates/jokolink/src/mumble/ctypes.rs | 6 +- crates/jokolink/src/mumble/mod.rs | 5 +- 11 files changed, 146 insertions(+), 101 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3c3e79..7089308 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,8 @@ opt-level = 3 [profile.release] -strip = "symbols" +#https://doc.rust-lang.org/cargo/reference/profiles.html#strip +#strip = "symbols" #lto make the build very slow #lto = true diff --git a/README.md b/README.md index 5beccae..ea2482f 100755 --- a/README.md +++ b/README.md @@ -3,10 +3,14 @@ An Overlay for Guild Wars 2 in Rust Well, technically, this contains a family of crates related to jokolay. -1. `jokolink`: This is what you will run from the wine prefix of gw2 . it reads the *official* [shared memory](https://wiki.guildwars2.com/wiki/API:MumbleLink) of gw2 to get live player data and copy into a shared memory file under /dev/shm for linux native apps (like Jokolay) to use. -2. `jokoapi`: API bindings for gw2 api in rust. if anyone wants to contribute, this is the best place. its just copy pasting api endpoints and filling out all the required fields of structs, writing tests to verify. -3. `jokolay`: this is the actual overlay. -4. `joko_marker_format`: deals with marker packs. +1. `joko_core`: Contains very basic and common structures. +2. `jokolink`: This is what you will run from the wine prefix of gw2 . it reads the *official* [shared memory](https://wiki.guildwars2.com/wiki/API:MumbleLink) of gw2 to get live player data and copy into a shared memory file under /dev/shm for linux native apps (like Jokolay) to use. +3. `jokoapi`: API bindings for gw2 api in rust. if anyone wants to contribute, this is the best place. its just copy pasting api endpoints and filling out all the required fields of structs, writing tests to verify. +4. `jokolay`: this is the actual overlay. +5. `joko_package`: deals with TacO marker packs. +6. `joko_package_models`: structures that need to be shared with other modules. +7. `joko_render`: in charge of displaying on screen. +8. `joko_render_models`: structures that need to be shared with other modules. ## Minimum Requirements 1. Requires Vulkan. most GPUs after gtx 750 should be okay. diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index f0dbbfd..2d21f20 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -13,6 +13,8 @@ use uuid::Uuid; use xot::{Node, Xot, Element}; +const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; + pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option ) -> Result { //called from already parsed data let mut core_pack = PackCore::new(); @@ -25,8 +27,7 @@ pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option>, - tbins: &mut HashMap, + pack: &mut PackCore, parent_path: &RelativePath, ) -> Result<()> { for entry in dir @@ -128,7 +128,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( .into_diagnostic() .wrap_err("failed to read file contents")?; if name.ends_with(".png") { - images.insert(path.clone(), bytes); + pack.register_texture(name, &path, bytes); } else if name.ends_with(".trl") { if let Some(tbs) = parse_tbin_from_slice(&bytes) { let is_closed: bool = tbs.closed; @@ -137,7 +137,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( if tbs.iso_y {} if tbs.iso_z {} } - tbins.insert(path, tbs.tbin); + pack.tbins.insert(path, tbs.tbin); } else { info!("invalid tbin: {path}"); } @@ -146,8 +146,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( } else { recursive_walk_dir_and_read_images_and_tbins( &entry.open_dir().into_diagnostic()?, - images, - tbins, + pack, &path, )?; } @@ -222,10 +221,10 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { let distance_to_next_point = a.distance_squared(*b); let mut current_cursor = distance_to_next_point; - while current_cursor > 1600.0 { + while current_cursor > MAX_TRAIL_CHUNK_LENGTH { let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); resulting_nodes.push(c); - current_cursor -= 1600.0; + current_cursor -= MAX_TRAIL_CHUNK_LENGTH; } } resulting_nodes.push(*b); @@ -387,7 +386,7 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack &xot_names, None, None, - ); + )?; trace!("loaded categories: {:?}", categories); pack.categories = categories; pack.register_categories(); @@ -417,18 +416,6 @@ fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Re } fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) -> Result<()> { - /* - fields read: - all_categories - - fields modified: - maps - all_categories - late_discovery_categories - source_files - tbins - entities_parents - */ let mut tree = Xot::new(); let root_node = tree .parse(map_xml_str) @@ -473,6 +460,13 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - opt_source_file_uuid.unwrap() }; + if let Some(source_file_name) = target.report.source_file_uuid_to_name(&source_file_uuid) { + let source_file_name = source_file_name.clone();// this is to bypass borrow checker which has no idea this cannot be changed + target.register_source_file(&source_file_name); + } else { + println!("{:?}", source_file_uuid); + } + //There is no file name, only an uuid to register target.active_source_files.insert(source_file_uuid.clone(), true); @@ -538,21 +532,16 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); - target.register_uuid(&full_category_name, &guid)?; let marker = Marker { position: [xpos, ypos, zpos].into(), map_id, - category: full_category_name, + category: full_category_name.clone(), parent: category_uuid.clone(), attrs: ca, guid, source_file_uuid }; - - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); - } - target.maps.get_mut(&map_id).unwrap().markers.insert(marker.guid, marker); + target.register_marker(full_category_name, marker); } else if child_element.name() == names.trail { debug!("Found a trail in core pack {:?}", child_element); if child_element @@ -566,9 +555,8 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); - target.register_uuid(&full_category_name, &guid)?; let trail = Trail { - category: full_category_name, + category: full_category_name.clone(), parent: category_uuid.clone(), map_id, props: ca, @@ -576,11 +564,8 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - dynamic: false, source_file_uuid }; + target.register_trail(full_category_name, trail)?; - if !target.maps.contains_key(&map_id) { - target.maps.insert(map_id, MapData::default()); - } - target.maps.get_mut(&map_id).unwrap().trails.insert(trail.guid, trail); } } span_guard.exit(); @@ -599,7 +584,7 @@ fn parse_category_categories_xml_recursive( names: &XotAttributeNameIDs, parent_uuid: Option, parent_name: Option, -) { +) -> Result<()> { for tag in tags { if let Some(ele) = tree.element(tag) { if ele.name() != names.marker_category { @@ -646,7 +631,9 @@ fn parse_category_categories_xml_recursive( let guid = parse_guid(names, ele); trace!("recursive_marker_category_parser_categories_xml {} {} {:?}", full_category_name, guid, parent_uuid); if display_name.is_empty() { - assert!(parent_name.is_none()); + if parent_name.is_some() { + return Err(miette::Error::msg("Package is corrupted, please import it again with current version")); + } parse_category_categories_xml_recursive( file_name, tree, @@ -656,7 +643,7 @@ fn parse_category_categories_xml_recursive( names, Some(guid), Some(full_category_name), - ); + )?; } else { let current_category = if let Some(c) = cats.get_mut(&guid) { @@ -685,7 +672,7 @@ fn parse_category_categories_xml_recursive( names, Some(guid), Some(full_category_name), - ); + )?; }; std::mem::drop(span_guard); @@ -694,6 +681,7 @@ fn parse_category_categories_xml_recursive( //info!("In file {}, ignore node {:?}", file_name, tag); } } + Ok(()) } /// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index 5a22e62..c0a64a4 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -5,7 +5,7 @@ use std::{ use glam::Vec3; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use cap_std::fs_utf8::Dir; -use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; +use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; use image::EncodableLayout; use tracing::{info_span, trace}; @@ -67,6 +67,7 @@ pub struct PackageUIManager { currently_used_files: BTreeMap, all_files_activation_status: bool,// this consume a change of display event show_only_active: bool, + pack_details: Option, // if filled, display the details of the package } impl PackageDataManager { @@ -181,7 +182,6 @@ impl PackageDataManager { match link { Some(link) => { //TODO: how to save/load the active files ? - //TODO: find an efficient way to propagate the file deactivation let mut have_used_files_list_changed = false; let map_changed = self.current_map_id != link.map_id; self.current_map_id = link.map_id; @@ -298,7 +298,8 @@ impl PackageUIManager { all_files_activation_status: false, show_only_active: true, - currently_used_files: Default::default()// UI copy to (de-)activate files + currently_used_files: Default::default(),// UI copy to (de-)activate files + pack_details: None, } } @@ -533,7 +534,6 @@ impl PackageUIManager { event_sender: &std::sync::mpsc::Sender, etx: &egui::Context, open: &mut bool, - link: Option<&MumbleLink> ) { let mut files_changed = false; Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { @@ -547,10 +547,12 @@ impl PackageUIManager { if ui.button("activate all").clicked() { self.all_files_activation_status = true; all_files_toggle = true; + files_changed = true; } if ui.button("deactivate all").clicked() { self.all_files_activation_status = false; all_files_toggle = true; + files_changed = true; } }); //ui.label("Trails"); @@ -558,6 +560,7 @@ impl PackageUIManager { ui.end_row(); for pack in self.packs.values_mut() { + //TODO: first loop to list what is active per pack, to not display all packs let report = self.reports.get(&pack.uuid).unwrap(); let mut pack_files_toggle = false; let mut pack_files_activation_status = true; @@ -566,10 +569,12 @@ impl PackageUIManager { if ui.button("activate all").clicked() { pack_files_activation_status = true; pack_files_toggle = true; + files_changed = true; } if ui.button("deactivate all").clicked() { pack_files_activation_status = false; pack_files_toggle = true; + files_changed = true; } }); ui.end_row(); @@ -609,6 +614,34 @@ impl PackageUIManager { let _ = event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); } } + + fn gui_package_details(&mut self, ui: &mut Ui, uuid: Uuid) { + let pack = self.packs.get(&uuid).unwrap(); + let report = self.reports.get(&uuid).unwrap(); + + let collapsing = CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + let header_response = collapsing + .open(Some(true)) + .show(ui, |ui| { + egui::Grid::new("packs details").striped(true).show(ui, |ui| { + let number_of = &report.number_of; + ui.label("categories"); ui.label(format!("{}", number_of.categories)); ui.end_row(); + ui.label("missing_categories");ui.label(format!("{}", number_of.missing_categories));ui.end_row(); + ui.label("textures"); ui.label(format!("{}", number_of.textures)); ui.end_row(); + ui.label("missing_textures"); ui.label(format!("{}", number_of.missing_textures)); ui.end_row(); + ui.label("entities"); ui.label(format!("{}", number_of.entities)); ui.end_row(); + ui.label("markers"); ui.label(format!("{}", number_of.markers)); ui.end_row(); + ui.label("trails"); ui.label(format!("{}", number_of.trails)); ui.end_row(); + ui.label("routes"); ui.label(format!("{}", number_of.routes)); ui.end_row(); + ui.label("maps"); ui.label(format!("{}", number_of.maps)); ui.end_row(); + ui.label("source_files"); ui.label(format!("{}", number_of.source_files)); ui.end_row(); + }) + }) + .header_response; + if header_response.clicked() { + self.pack_details = None; + } + } fn gui_package_list( &mut self, u2b_sender: &std::sync::mpsc::Sender, @@ -630,7 +663,7 @@ impl PackageUIManager { to_delete.push(pack.uuid); } if ui.button("Details").clicked() { - //TODO + self.pack_details = Some(pack.uuid); } if ui.button("Export").clicked() { //TODO @@ -642,13 +675,12 @@ impl PackageUIManager { } }); }); - - if let Ok(mut status) = import_status.lock() { + if let Some(uuid) = self.pack_details { + self.gui_package_details(ui, uuid); + } else if let Ok(mut status) = import_status.lock() { match &mut *status { ImportStatus::UnInitialized => { if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - //TODO: send message to background thread, UIToBackMessage::ImportPack instead of a rayon thread ? - //let import_status = import_status.lock().unwrap(); Self::pack_importer(Arc::clone(import_status)); } //ui.label("import not started yet"); @@ -699,11 +731,9 @@ impl PackageUIManager { import_status: &Arc>, is_file_open: &mut bool, first_load_done: bool, - timestamp: f64, - link: Option<&MumbleLink> ) { self.gui_package_list(u2b_sender, etx, import_status, is_marker_open, first_load_done); - self.gui_file_manager(u2b_sender, etx, is_file_open, link); + self.gui_file_manager(u2b_sender, etx, is_file_open); } pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 82a4a56..8fb2e71 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -91,16 +91,16 @@ struct PackageElementSource { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportStatistics { - categories: usize, // total number of found categories - missing_categories: usize, // categories that should be defined in a node - textures: usize, //total number of texture used (or should) - missing_textures: usize, // how many of the textures are missing - entities: usize, // total number of tracked elements: categories, trails, markers, ... - markers: usize, // total number of markers - trails: usize, // total number of trails - routes: usize, // total number of routes defined, they shall not count as trails even if imported as such - maps: usize, // total number of maps covered - source_files: usize, // total number of XML files + pub categories: usize, // total number of found categories + pub missing_categories: usize, // categories that should be defined in a node + pub textures: usize, //total number of texture used (or should) + pub missing_textures: usize, // how many of the textures are missing + pub entities: usize, // total number of tracked elements: categories, trails, markers, ... + pub markers: usize, // total number of markers + pub trails: usize, // total number of trails + pub routes: usize, // total number of routes defined, they shall not count as trails even if imported as such + pub maps: usize, // total number of maps covered + pub source_files: usize, // total number of XML files } @@ -127,7 +127,7 @@ pub struct PackageImportTelemetry { pub struct PackageImportReport { #[serde(skip)] pub uuid: Uuid, - number_of: PackageImportStatistics, // count everything we can think of + pub number_of: PackageImportStatistics, // count everything we can think of pub telemetry: PackageImportTelemetry, // all the time spent in which step late_discovered_categories: OrderedHashMap,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. missing_categories: Vec,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index d25fc5c..0c2a4e1 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -84,9 +84,11 @@ impl BillBoardRenderer { } pub fn prepare_render_data(&mut self, gl: &Context) { - //TODO: trim down the trails too far - // fatten them ? - // what about view from above (map view) + /* + TODO: map view (view from above) + trim down the trails too far ? + fatten them ? + */ unsafe { gl_error!(gl); } diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs index 8eb11d0..6621559 100644 --- a/crates/joko_render/src/renderer.rs +++ b/crates/joko_render/src/renderer.rs @@ -113,7 +113,7 @@ impl JokoRenderer { 1.0 } pub fn get_z_far() -> f32 { - 1000.0 //TODO: should match the distance for marker exclusion + 1000.0 } pub fn swap(&mut self) { self.billboard_renderer.swap(); diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 2d32614..2398239 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -6,7 +6,7 @@ use std::{ }; use cap_std::fs_utf8::Dir; -use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; +use egui_window_glfw_passthrough::{glfw::{ffi::glfwGetVideoMode, Context as _}, GlfwBackend, GlfwConfig}; mod init; mod wm; mod mumble; @@ -26,12 +26,11 @@ use jmf::{LoadedPackData, LoadedPackTexture, build_from_core}; use jmf::{ImportStatus, import_pack_from_zip_file_path}; -const MINIMAL_WINDOW_WIDTH: i32 = 640; -const MINIMAL_WINDOW_HEIGHT: i32 = 480; +const MINIMAL_WINDOW_WIDTH: u32 = 640; +const MINIMAL_WINDOW_HEIGHT: u32 = 480; const MINIMAL_WINDOW_POSITION_X: i32 = 0; const MINIMAL_WINDOW_POSITION_Y: i32 = 0; -#[derive(Clone)] struct JokolayUIState { link: Option, editable_mumble: bool, @@ -41,8 +40,8 @@ struct JokolayUIState { nb_running_tasks_on_back: i32,// store the number of running tasks in background thread nb_running_tasks_on_network: i32,// store the number of running tasks (requests) in progress import_status: Arc>, - maximal_window_width: i32, - maximal_window_height: i32, + maximal_window_width: u32, + maximal_window_height: u32, } struct JokolayBackState { @@ -111,14 +110,16 @@ impl Jokolay { window_title: "Jokolay".to_string(), ..Default::default() }); - let screen_physical_size = glfw_backend.glfw.with_primary_monitor(|_, m| { + + //retrieve current screen resolution + let video_mode = glfw_backend.glfw.with_primary_monitor(|_, m| { if let Some(m) = m { - Some(m.get_physical_size()) + m.get_video_mode() } else { None } }); - info!("Monitor physical size: {:?}", screen_physical_size); + glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); @@ -152,8 +153,8 @@ impl Jokolay { nb_running_tasks_on_back: 0, nb_running_tasks_on_network: 0, import_status: Default::default(), - maximal_window_width: screen_physical_size.unwrap().0, - maximal_window_height: screen_physical_size.unwrap().1, + maximal_window_width: video_mode.unwrap().width, + maximal_window_height: video_mode.unwrap().height, }, state_back: JokolayBackState { choice_of_category_changed: false, @@ -507,6 +508,21 @@ impl Jokolay { let etx = egui_context.clone(); + /* + if etx.input(|i| { + TODO: + handle shortcuts + a module publish a list of shortcuts + At import, user need to accept those. + We can't have a module that is a keyboard listener. + + modifiers are not forwarded. + println!("{:?} {:?}", i.keys_down, i.modifiers); + false + }) { + } + */ + // gather events glfw_backend.glfw.poll_events(); glfw_backend.tick(); @@ -584,11 +600,14 @@ impl Jokolay { // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - let client_size_x = MINIMAL_WINDOW_WIDTH.max(link.client_size.x); - let client_size_y = MINIMAL_WINDOW_HEIGHT.max(link.client_size.y); + let client_size_x = MINIMAL_WINDOW_WIDTH.max(link.client_size.x).min(local_state.maximal_window_width); + let client_size_y = MINIMAL_WINDOW_HEIGHT.max(link.client_size.y).min(local_state.maximal_window_height); glfw_backend .window - .set_size(client_size_x - 1, client_size_y - 1); + .set_size( + (client_size_x - 1) as i32, + (client_size_y - 1) as i32 + ); } if local_state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map) { package_manager.tick( @@ -667,8 +686,6 @@ impl Jokolay { &local_state.import_status, &mut menu_panel.show_file_manager_window, local_state.first_load_done, - latest_time, - local_state.link.as_ref() ); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index ebb46c8..efb280f 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -10,7 +10,7 @@ mod mumble; use enumflags2::BitFlags; -use glam::IVec2; +use glam::{IVec2, UVec2}; //use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::{IntoDiagnostic, Result, WrapErr}; pub use mumble::*; @@ -63,8 +63,8 @@ impl MumbleManager { } if !self.backend.is_alive() { - self.link.client_size.x = -1; - self.link.client_size.y = -1; + self.link.client_size.x = 0; + self.link.client_size.y = 0; self.link.changes = BitFlags::all(); return Ok(Some(&self.link)); } @@ -76,7 +76,7 @@ impl MumbleManager { self.link.clone() }; - if cml.ui_tick == 0 || cml.context.client_pos_size == [0; 4] { + if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { return Ok(None); } let mut changes: BitFlags = Default::default(); @@ -110,12 +110,12 @@ impl MumbleManager { changes.insert(MumbleChanges::Map); } let client_pos = IVec2::new( - cml.context.client_pos_size[0], - cml.context.client_pos_size[1], + cml.context.client_pos[0], + cml.context.client_pos[1], ); - let client_size = IVec2::new( - cml.context.client_pos_size[2], - cml.context.client_pos_size[3], + let client_size = UVec2::new( + cml.context.client_size[0], + cml.context.client_size[1], ); if new_link.client_pos != client_pos { diff --git a/crates/jokolink/src/mumble/ctypes.rs b/crates/jokolink/src/mumble/ctypes.rs index 21ebaeb..db9d26f 100644 --- a/crates/jokolink/src/mumble/ctypes.rs +++ b/crates/jokolink/src/mumble/ctypes.rs @@ -185,7 +185,8 @@ pub struct CMumbleContext { /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. pub dpi: i32, /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. - pub client_pos_size: [i32; 4], + pub client_pos: [i32; 2], + pub client_size: [u32; 4], /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. pub padding: [u8; 96], } @@ -217,7 +218,8 @@ impl Default for CMumbleContext { // window_pos_size_without_borders: Default::default(), dpi_scaling: Default::default(), dpi: Default::default(), - client_pos_size: Default::default(), + client_pos: Default::default(), + client_size: Default::default(), } } } diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs index 451a7a3..71fb27c 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/jokolink/src/mumble/mod.rs @@ -4,6 +4,7 @@ pub mod ctypes; use std::net::IpAddr; use enumflags2::{bitflags, BitFlags}; +use glam::UVec2; use glam::{IVec2, Vec3}; use num_derive::FromPrimitive; use num_derive::ToPrimitive; @@ -78,7 +79,7 @@ pub struct MumbleLink { /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* pub client_pos: IVec2, /// This is the size of gw2's viewport (width/height) in screen coordinates - pub client_size: IVec2, + pub client_size: UVec2, /// changes since last mumble link update pub changes: BitFlags, } @@ -114,7 +115,7 @@ impl Default for MumbleLink { dpi: Default::default(), dpi_scaling: 96, client_pos: Default::default(), - client_size: IVec2{x: 1024, y: 768}, + client_size: UVec2{x: 1024, y: 768}, changes: Default::default(), } } From 3005035fac8d9ef6b75072a50d6667826a4ef745 Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 20 Apr 2024 16:40:03 +0200 Subject: [PATCH 32/54] fix crash while deleting package if its details are being displayed + animate trail --- Cargo.lock.backup_20240318 | 3087 ----------------- Cargo.toml | 2 +- crates/joko_package/src/io/deserialize.rs | 27 +- crates/joko_package/src/lib.rs | 1 + crates/joko_package/src/manager/mod.rs | 2 +- .../joko_package/src/manager/pack/import.rs | 13 +- .../joko_package/src/manager/pack/loaded.rs | 52 +- crates/joko_package/src/manager/package.rs | 55 +- crates/joko_render/shaders/trail.fs | 22 + crates/joko_render/shaders/trail.vs | 37 + crates/joko_render/src/billboard.rs | 185 +- crates/joko_render/src/renderer.rs | 2 + crates/jokolay/src/app/init.rs | 7 + crates/jokolay/src/app/mod.rs | 26 +- crates/jokolay/src/manager/trace/mod.rs | 2 +- 15 files changed, 315 insertions(+), 3205 deletions(-) delete mode 100644 Cargo.lock.backup_20240318 create mode 100644 crates/joko_render/shaders/trail.fs create mode 100644 crates/joko_render/shaders/trail.vs diff --git a/Cargo.lock.backup_20240318 b/Cargo.lock.backup_20240318 deleted file mode 100644 index 3951136..0000000 --- a/Cargo.lock.backup_20240318 +++ /dev/null @@ -1,3087 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - -[[package]] -name = "accesskit" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eb1adf08c5bcaa8490b9851fd53cca27fa9880076f178ea9d29f05196728a8" -dependencies = [ - "enumn", - "serde", -] - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "serde", - "version_check", - "zerocopy 0.7.25", -] - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arcdps" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e8e3e68ba99ea4d9fc0af6c26f7277c6a30f9fbd7a1884efd8d016dcdfdc39" -dependencies = [ - "arcdps_codegen", - "chrono", - "once_cell", -] - -[[package]] -name = "arcdps_codegen" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b73c6f84c5845e9eba3a232593d20ef3db434281848f5072a367edbcc1f3fee" -dependencies = [ - "paste", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "base64" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata 0.1.10", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "bytemuck" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" - -[[package]] -name = "cap-directories" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182588d07579a8ca97dbfbea2787d450341d068b16062c8caa2205158ddb269d" -dependencies = [ - "cap-std", - "directories-next", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "cap-primitives" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf30c373a3bee22c292b1b6a7a26736a38376840f1af3d2d806455edf8c3899" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix", - "windows-sys 0.48.0", - "winx", -] - -[[package]] -name = "cap-std" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bade423fa6403efeebeafe568fdb230e8c590a275fba2ba978dd112efcf6e9" -dependencies = [ - "camino", - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-expr" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx", - "num-traits", -] - -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.48.5", -] - -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "console" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "const_format" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "cxx" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.39", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "data-encoding" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" - -[[package]] -name = "deranged" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "ecolor" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "egui" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" -dependencies = [ - "accesskit", - "ahash", - "epaint", - "nohash-hasher", - "serde", -] - -[[package]] -name = "egui_extras" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ffe3fe5c00295f91c2a61a74ee271c32f74049c94ba0b1cea8f26eb478bc07" -dependencies = [ - "egui", - "enum-map", - "log", - "mime_guess", - "serde", -] - -[[package]] -name = "egui_render_glow" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df0cb60080432a2c025f00942fbd1a0f8b719338ab6a28adab5a1ca15013771" -dependencies = [ - "bytemuck", - "egui", - "getrandom", - "glow", - "js-sys", - "raw-window-handle", - "tracing", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "egui_render_three_d" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4038e7bac93f9356eb88ffabd20d9486070b79910584d662af1ac3bf64f01e2a" -dependencies = [ - "egui", - "egui_render_glow", - "raw-window-handle", - "three-d", -] - -[[package]] -name = "egui_window_glfw_passthrough" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecec3abb56e2be5104a35a4c1848f976add5167a8655f67ae7c84d45d35c8905" -dependencies = [ - "egui", - "glfw-passthrough", - "tracing", -] - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "emath" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "encoding_rs" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-map" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed40247825a1a0393b91b51d475ea1063a6cbbf0847592e7f13fb427aca6a716" -dependencies = [ - "enum-map-derive", - "serde", -] - -[[package]] -name = "enum-map-derive" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7933cd46e720348d29ed1493f89df9792563f272f96d8f13d18afe03b32f8cb8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "enumflags2" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "enumn" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "epaint" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" -dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "nohash-hasher", - "parking_lot", - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "fdeflate" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "filetime" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", -] - -[[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs-set-times" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" -dependencies = [ - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gethostname" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "getrandom" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glam" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "glfw-passthrough" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89ad199bb99922313a6e97b609dab23a88e3b68a6b0233d1fafdb5044a7728f" -dependencies = [ - "bitflags 1.3.2", - "glfw-sys-passthrough", - "objc", - "raw-window-handle", - "winapi", -] - -[[package]] -name = "glfw-sys-passthrough" -version = "4.0.3+3.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b2db4d361b9ebe743c3a542ddef5d605269bd1f93e1090440fff075e666ddf" -dependencies = [ - "cmake", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "glow" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "half" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy 0.6.5", -] - -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "iana-time-zone" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "image" -version = "0.24.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-rational", - "num-traits", - "png", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown 0.14.2", - "serde", -] - -[[package]] -name = "indextree" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-extras" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3c230ee517ee76b1cc593b52939ff68deda3fae9e41eca426c6b4993df51c4" -dependencies = [ - "io-lifetimes", - "windows-sys 0.48.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "joko_core" -version = "0.2.1" -dependencies = [ - "cap-directories", - "cap-std", - "egui", - "egui_extras", - "glam", - "indexmap", - "miette", - "ordered_hash_map", - "rayon", - "rfd", - "ringbuffer", - "serde", - "serde_json", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] -name = "joko_ext" -version = "0.1.0" - -[[package]] -name = "joko_marker_format" -version = "0.2.1" -dependencies = [ - "base64", - "cap-std", - "cxx", - "cxx-build", - "data-encoding", - "egui", - "enumflags2", - "glam", - "image", - "indexmap", - "itertools", - "joko_render", - "jokoapi", - "jokolink", - "miette", - "ordered_hash_map", - "paste", - "phf", - "rayon", - "rfd", - "rstest", - "serde", - "serde_json", - "similar-asserts", - "smol_str", - "time", - "tracing", - "url", - "uuid", - "xot", - "zip", -] - -[[package]] -name = "joko_render" -version = "0.2.1" -dependencies = [ - "bytemuck", - "egui", - "egui_render_three_d", - "egui_window_glfw_passthrough", - "glam", - "jokolink", - "raw-window-handle", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "jokoapi" -version = "0.2.1" -dependencies = [ - "const_format", - "enumflags2", - "miette", - "serde", - "ureq", -] - -[[package]] -name = "jokolay" -version = "0.2.1" -dependencies = [ - "cap-directories", - "cap-std", - "egui", - "egui_extras", - "egui_window_glfw_passthrough", - "glam", - "indexmap", - "joko_core", - "joko_marker_format", - "joko_render", - "jokolink", - "miette", - "rayon", - "rfd", - "ringbuffer", - "serde", - "serde_json", - "tracing", - "tracing-appender", - "tracing-subscriber", - "url", -] - -[[package]] -name = "jokolink" -version = "0.2.1" -dependencies = [ - "arcdps", - "egui", - "enumflags2", - "glam", - "jokoapi", - "miette", - "notify", - "num-derive", - "num-traits", - "serde", - "serde_json", - "time", - "tracing", - "tracing-appender", - "tracing-subscriber", - "widestring", - "windows", - "x11rb", -] - -[[package]] -name = "js-sys" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kqueue" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libredox" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" -dependencies = [ - "bitflags 2.4.1", - "libc", - "redox_syscall 0.4.1", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - -[[package]] -name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miette" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" -dependencies = [ - "backtrace", - "backtrace-ext", - "is-terminal", - "miette-derive", - "once_cell", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "thiserror", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "next-gen" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1962f0b64c859f27f9551c74afbdbec7090fa83518daf6c5eb5b31d153455beb" -dependencies = [ - "next-gen-proc_macros", - "unwind_safe", -] - -[[package]] -name = "next-gen-proc_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a59395d2ffdd03894479cdd1ce4b7e0700d379d517f2d396cee2a4828707c5a0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.4.1", - "filetime", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "ordered_hash_map" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" -dependencies = [ - "hashbrown 0.13.2", - "serde", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "owned_ttf_parser" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "png" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - -[[package]] -name = "rayon" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "relative-path" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" - -[[package]] -name = "rfd" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" -dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "ring" -version = "0.17.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" -dependencies = [ - "cc", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.48.0", -] - -[[package]] -name = "ringbuffer" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" - -[[package]] -name = "rstest" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" -dependencies = [ - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" -dependencies = [ - "cfg-if", - "glob", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.39", - "unicode-ident", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" -dependencies = [ - "bitflags 2.4.1", - "errno", - "itoa", - "libc", - "linux-raw-sys", - "once_cell", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustls" -version = "0.21.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scratch" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "semver" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" - -[[package]] -name = "serde" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.192" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "serde_json" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" -dependencies = [ - "serde", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "similar" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" -dependencies = [ - "bstr", - "unicode-segmentation", -] - -[[package]] -name = "similar-asserts" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" -dependencies = [ - "console", - "similar", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "smol_str" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" -dependencies = [ - "serde", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "supports-unicode" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - -[[package]] -name = "termcolor" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "three-d" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2db9010227411ab0aa5948e770304e807e5c9b6d5d0719c3de248bae7be7096" -dependencies = [ - "cgmath", - "glow", - "instant", - "thiserror", - "three-d-asset", -] - -[[package]] -name = "three-d-asset" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9959d4427b63958661828008f7470d6a8d2c0945b3df0dc7377d6aca38fb694" -dependencies = [ - "cgmath", - "half", - "thiserror", - "web-sys", -] - -[[package]] -name = "time" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" -dependencies = [ - "deranged", - "itoa", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" -dependencies = [ - "crossbeam-channel", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "time", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "ttf-parser" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "unwind_safe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" - -[[package]] -name = "ureq" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-webpki", - "serde", - "serde_json", - "url", - "webpki-roots", -] - -[[package]] -name = "url" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "uuid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" -dependencies = [ - "getrandom", - "rand", - "serde", - "uuid-macro-internal", -] - -[[package]] -name = "uuid-macro-internal" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" - -[[package]] -name = "web-sys" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" -dependencies = [ - "windows-core", - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "winnow" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" -dependencies = [ - "memchr", -] - -[[package]] -name = "winx" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5" -dependencies = [ - "bitflags 2.4.1", - "windows-sys 0.48.0", -] - -[[package]] -name = "x11rb" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" -dependencies = [ - "gethostname", - "nix", - "winapi", - "winapi-wsapoll", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" -dependencies = [ - "nix", -] - -[[package]] -name = "xhtmlchardet" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc471704e8954f426350a7300e92a4da6932b762068ae8e6aa5dcacf141e133" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "xot" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55dc1c3603e452c78983b59f466cd8251695db1729b230f473d004d70b3d94d8" -dependencies = [ - "ahash", - "encoding_rs", - "indextree", - "next-gen", - "xhtmlchardet", - "xmlparser", -] - -[[package]] -name = "zerocopy" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96f8f25c15a0edc9b07eb66e7e6e97d124c0505435c382fde1ab7ceb188aa956" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.5", -] - -[[package]] -name = "zerocopy" -version = "0.7.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" -dependencies = [ - "zerocopy-derive 0.7.25", -] - -[[package]] -name = "zerocopy-derive" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "855e0f6af9cd72b87d8a6c586f3cb583f5cdcc62c2c80869d8cd7e96fdf7ee20" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", - "flate2", -] diff --git a/Cargo.toml b/Cargo.toml index 7089308..01442e5 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ opt-level = 3 [profile.release] #https://doc.rust-lang.org/cargo/reference/profiles.html#strip -#strip = "symbols" +strip = "none" #lto make the build very slow #lto = true diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 2d21f20..c4289d7 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -1,12 +1,12 @@ use joko_core::RelativePath; -use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, map::MapData, marker::Marker, package::{PackCore, PackageImportReport}, route::Route, trail::{TBin, TBinStatus, Trail}}; +use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, marker::Marker, package::{PackCore, PackageImportReport}, route::Route, trail::{TBin, TBinStatus, Trail}}; use miette::{bail, Context, IntoDiagnostic, Result}; use crate::BASE64_ENGINE; use base64::Engine; use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; -use std::{collections::{HashMap, VecDeque}, io::Read, str::FromStr}; +use std::{collections::VecDeque, io::Read, str::FromStr}; use ordered_hash_map::OrderedHashMap; use tracing::{debug, error, info, info_span, instrument, trace, warn}; use uuid::Uuid; @@ -684,6 +684,22 @@ fn parse_category_categories_xml_recursive( Ok(()) } + +pub(crate) fn get_pack_from_taco_zip(input_path: std::path::PathBuf, working_path: &std::path::PathBuf) -> Result { + let mut taco_zip = vec![]; + std::fs::File::open(&input_path) + .into_diagnostic()? + .read_to_end(&mut taco_zip) + .into_diagnostic()?; + + let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) + .into_diagnostic() + .wrap_err("failed to read zip archive")?; + zip_archive.extract(working_path).into_diagnostic()?; + + _get_pack_from_taco_zip(zip_archive) +} + /// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. /// will return error if there's an issue with zipfile. /// @@ -691,14 +707,11 @@ fn parse_category_categories_xml_recursive( /// the intention is "best effort" parsing and not "validating" xml marker packs. /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] -pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { +fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive>>) -> Result { + //TODO: simply extract the file into a working folder ? //called to import a new pack // all the contents of ZPack let mut pack = PackCore::new(); - // parse zip file - let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) - .into_diagnostic() - .wrap_err("failed to read zip archive")?; // file paths of different file types let mut images = vec![]; diff --git a/crates/joko_package/src/lib.rs b/crates/joko_package/src/lib.rs index 607ecf4..1d0e49f 100644 --- a/crates/joko_package/src/lib.rs +++ b/crates/joko_package/src/lib.rs @@ -13,6 +13,7 @@ pub use manager::{ LoadedPackTexture, load_all_from_dir, build_from_core, + jokolay_to_working_path, ImportStatus, import_pack_from_zip_file_path }; diff --git a/crates/joko_package/src/manager/mod.rs b/crates/joko_package/src/manager/mod.rs index c6064dd..b15e3ea 100644 --- a/crates/joko_package/src/manager/mod.rs +++ b/crates/joko_package/src/manager/mod.rs @@ -20,5 +20,5 @@ mod package; mod pack; pub use package::{PackageDataManager, PackageUIManager}; -pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core}; +pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core, jokolay_to_working_path}; pub use pack::import::{ImportStatus, import_pack_from_zip_file_path}; \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs index 8b65377..d43b28a 100644 --- a/crates/joko_package/src/manager/pack/import.rs +++ b/crates/joko_package/src/manager/pack/import.rs @@ -1,8 +1,7 @@ -use std::io::Read; use joko_package_models::package::PackCore; use tracing::info; -use miette::{IntoDiagnostic, Result}; +use miette::Result; #[derive(Debug, Default)] @@ -16,15 +15,9 @@ pub enum ImportStatus { PackError(miette::Report), } -pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { - let mut taco_zip = vec![]; - std::fs::File::open(&file_path) - .into_diagnostic()? - .read_to_end(&mut taco_zip) - .into_diagnostic()?; - +pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf, working_path: &std::path::PathBuf) -> Result<(String, PackCore)> { info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { + crate::io::get_pack_from_taco_zip(file_path.clone(), working_path).map(|pack| { ( file_path .file_name() diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 8a5cbe3..07c233e 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -26,7 +26,7 @@ use miette::{bail, Context, IntoDiagnostic, Result}; use super::activation::{ActivationData, ActivationType}; use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; use crate::manager::pack::category_selection::CategorySelection; -use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME}; +use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME, EDITABLE_PACKAGE_NAME, WORKING_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME}; type ImportAllTriplet = (BTreeMap, BTreeMap, BTreeMap); @@ -711,6 +711,15 @@ impl LoadedPackTexture { } +pub fn jokolay_to_working_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { + let marker_manager_path = jokolay_to_marker_path(jokolay_path); + marker_manager_path.join(WORKING_PACKAGE_NAME) +} + +pub fn jokolay_to_marker_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf{ + jokolay_path.join(PACKAGE_MANAGER_DIRECTORY_NAME).join(PACKAGES_DIRECTORY_NAME) +} + pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { jokolay_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() @@ -719,6 +728,7 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() .wrap_err(format!("failed to open marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + marker_manager_dir .create_dir_all(PACKAGES_DIRECTORY_NAME) .into_diagnostic() @@ -727,6 +737,18 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { .open_dir(PACKAGES_DIRECTORY_NAME) .into_diagnostic() .wrap_err(format!("failed to open marker packs dir {}", PACKAGES_DIRECTORY_NAME))?; + + marker_packs_dir.create_dir_all(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + let editable_package = marker_packs_dir.open_dir(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + + editable_package.create_dir_all("data") + .into_diagnostic() + .wrap_err("failed to create data folder for editable package")?; + Ok(marker_packs_dir) } @@ -738,7 +760,6 @@ pub fn load_all_from_dir(jokolay_dir: Arc) let mut texture_packs: BTreeMap = Default::default(); let mut report_packs: BTreeMap = Default::default(); - for entry in marker_packs_dir .entries() .into_diagnostic() @@ -754,20 +775,33 @@ pub fn load_all_from_dir(jokolay_dir: Arc) .into_diagnostic() .wrap_err(format!("failed to open pack entry as directory: {}", name))?; { - let span_guard = info_span!("loading pack from dir", name).entered(); - - match build_from_dir(name.clone(), pack_dir.into()) { - Ok(lp) => { + if name == EDITABLE_PACKAGE_NAME { + //TODO: have a version of loading that does not involve already ingested packages + if let Ok(pack_core) = load_pack_core_from_dir(&pack_dir, None) { + let lp = build_from_core(name.clone(), pack_dir.into(), pack_core); let (data, tex, report) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); report_packs.insert(report.uuid, report); } - Err(e) => { - error!(?e, "failed to load pack from directory: {}", name); + } else if name == LOCAL_EXPANDED_PACKAGE_NAME { + //ignore this package, it'll be overwriten + } else { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name.clone(), pack_dir.into()) { + Ok(lp) => { + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + Err(e) => { + error!(?e, "failed to load pack from directory: {}", name); + } } + drop(span_guard); } - drop(span_guard); } } } diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index c0a64a4..b44e389 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -24,6 +24,9 @@ use super::pack::loaded::jokolay_to_marker_dir; pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose +pub const EDITABLE_PACKAGE_NAME: &str = "_work";//package automatically created and always imported as an overwrite +pub const WORKING_PACKAGE_NAME: &str = "editable";//working dir where a package is extracted before reading +pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded";//result of import of the editable package // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; /// It manage everything that has to do with marker packs. @@ -616,29 +619,35 @@ impl PackageUIManager { } fn gui_package_details(&mut self, ui: &mut Ui, uuid: Uuid) { - let pack = self.packs.get(&uuid).unwrap(); - let report = self.reports.get(&uuid).unwrap(); - - let collapsing = CollapsingHeader::new(format!("Last load details of package {}", pack.name)); - let header_response = collapsing - .open(Some(true)) - .show(ui, |ui| { - egui::Grid::new("packs details").striped(true).show(ui, |ui| { - let number_of = &report.number_of; - ui.label("categories"); ui.label(format!("{}", number_of.categories)); ui.end_row(); - ui.label("missing_categories");ui.label(format!("{}", number_of.missing_categories));ui.end_row(); - ui.label("textures"); ui.label(format!("{}", number_of.textures)); ui.end_row(); - ui.label("missing_textures"); ui.label(format!("{}", number_of.missing_textures)); ui.end_row(); - ui.label("entities"); ui.label(format!("{}", number_of.entities)); ui.end_row(); - ui.label("markers"); ui.label(format!("{}", number_of.markers)); ui.end_row(); - ui.label("trails"); ui.label(format!("{}", number_of.trails)); ui.end_row(); - ui.label("routes"); ui.label(format!("{}", number_of.routes)); ui.end_row(); - ui.label("maps"); ui.label(format!("{}", number_of.maps)); ui.end_row(); - ui.label("source_files"); ui.label(format!("{}", number_of.source_files)); ui.end_row(); - }) - }) - .header_response; - if header_response.clicked() { + // protection against deletion while displaying details + if let Some(pack) = self.packs.get(&uuid) { + if let Some(report) = self.reports.get(&uuid) { + let collapsing = CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + let header_response = collapsing + .open(Some(true)) + .show(ui, |ui| { + egui::Grid::new("packs details").striped(true).show(ui, |ui| { + let number_of = &report.number_of; + ui.label("categories"); ui.label(format!("{}", number_of.categories)); ui.end_row(); + ui.label("missing_categories");ui.label(format!("{}", number_of.missing_categories));ui.end_row(); + ui.label("textures"); ui.label(format!("{}", number_of.textures)); ui.end_row(); + ui.label("missing_textures"); ui.label(format!("{}", number_of.missing_textures)); ui.end_row(); + ui.label("entities"); ui.label(format!("{}", number_of.entities)); ui.end_row(); + ui.label("markers"); ui.label(format!("{}", number_of.markers)); ui.end_row(); + ui.label("trails"); ui.label(format!("{}", number_of.trails)); ui.end_row(); + ui.label("routes"); ui.label(format!("{}", number_of.routes)); ui.end_row(); + ui.label("maps"); ui.label(format!("{}", number_of.maps)); ui.end_row(); + ui.label("source_files"); ui.label(format!("{}", number_of.source_files)); ui.end_row(); + }) + }) + .header_response; + if header_response.clicked() { + self.pack_details = None; + } + } else { + self.pack_details = None; + } + } else { self.pack_details = None; } } diff --git a/crates/joko_render/shaders/trail.fs b/crates/joko_render/shaders/trail.fs new file mode 100644 index 0000000..e8ca1d4 --- /dev/null +++ b/crates/joko_render/shaders/trail.fs @@ -0,0 +1,22 @@ +#version 450 + +layout(location = 0) in vec2 vtex_coord; +layout(location = 1) in float valpha; +layout(location = 2) in vec4 vcolor; + +layout(location = 0) out vec4 ocolor; + +layout(location = 1) uniform sampler2D sam; // wrap_s = "REPEAT" wrap_t = "REPEAT" +layout(location = 3) uniform vec2 scroll_texture; + +void main() { + //vec4 color = texture(sam, vec2 (vtex_coord.x + scroll_texture.x, vtex_coord.y + scroll_texture.y), -2.0); + vec4 color = texture(sam, vec2 (vtex_coord.x + 0.0, vtex_coord.y + scroll_texture.y), -2.0); + //vec4 color = texture(sam, vtex_coord + scroll_texture); + //vec4 color = texture(sam, vtex_coord, -2.0);//original working + color.a = color.a * valpha; + if (color.a < 0.01) { + discard; + } + ocolor = color; +} diff --git a/crates/joko_render/shaders/trail.vs b/crates/joko_render/shaders/trail.vs new file mode 100644 index 0000000..c8f04b6 --- /dev/null +++ b/crates/joko_render/shaders/trail.vs @@ -0,0 +1,37 @@ +#version 450 + +layout(location = 0) in vec4 position; +layout(location = 1) in float alpha; +layout(location = 2) in vec2 tex_coord; +layout(location = 3) in vec2 fade_near_far; +layout(location = 4) in vec4 color; + + +layout(location = 0) out vec2 vtex_coord; +layout(location = 1) out float valpha; +layout(location = 2) out vec4 vcolor; + +layout(location = 0) uniform vec3 camera_pos; +// location 1 is for sampler in frag shader +layout(location = 2) uniform mat4 transform; +// location 3 is for scroll_texture + +void main( +) { + valpha = alpha; + vtex_coord = tex_coord; + gl_Position = transform * position; + vcolor = color; + + float dist = distance(camera_pos, position.xyz); + if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { + // if distance is exactly fade_near, we will multiply with 1.0 + // if its more, then we will multiply with how far we are in between fade_near and fade_far + float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); + // The actual alpha + valpha *= ratio; + } + if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { + valpha = 0.0; + } +} diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 0c2a4e1..3e9c36f 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -3,6 +3,7 @@ use egui_render_three_d::{ three_d::{context::*, Context, HasContext}, GpuTexture, }; +use glam::Vec2; use joko_render_models::{ marker::{MarkerVertex, MarkerObject}, trail::TrailObject @@ -18,58 +19,46 @@ pub struct BillBoardRenderer { pub markers_wip: Vec,//work in progress: this is where the markers are inserted pub trails_wip: Vec,//work in progress: this is where the markers are inserted marker_program: NativeProgram, - vao: NativeVertexArray, - vb: NativeBuffer, - trail_buffers: Vec, + marker_vertex_buffer: NativeBuffer, + marker_vertex_array: NativeVertexArray, + + trail_program: NativeProgram, + trail_vertex_buffers: Vec, + trail_vertex_arrays: Vec, } + -const MARKER_VS: &str = include_str!("../shaders/marker.vs"); +const MARKER_VERTEX_SHADER: &str = include_str!("../shaders/marker.vs"); +const MARKER_FRAGMENT_SHADER: &str = include_str!("../shaders/marker.fs"); +const TRAIL_VERTEX_SHADER: &str = include_str!("../shaders/trail.vs"); +const TRAIL_FRAGMENT_SHADER: &str = include_str!("../shaders/trail.fs"); -const MARKER_FS: &str = include_str!("../shaders/marker.fs"); impl BillBoardRenderer { pub fn new(gl: &Context) -> Self { unsafe { - let marker_program = new_program(gl, MARKER_VS, MARKER_FS, None); - let vb = create_marker_buffer(gl); - let vao = gl.create_vertex_array().expect("failed to create egui vao"); - gl.bind_vertex_array(Some(vao)); - gl.bind_vertex_buffer(0, Some(vb), 0, MARKER_VERTEX_STRIDE); + let marker_program = new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); gl_error!(gl); - gl.enable_vertex_array_attrib(vao, 0); - gl.vertex_array_attrib_format_f32(vao, 0, 3, FLOAT, false, 0); - gl.vertex_array_attrib_binding_f32(vao, 0, 0); + let trail_shift_program = new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 1); - gl.vertex_array_attrib_format_f32(vao, 1, 1, FLOAT, false, 12); - gl.vertex_array_attrib_binding_f32(vao, 1, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 2); - gl.vertex_array_attrib_format_f32(vao, 2, 2, FLOAT, false, 16); - gl.vertex_array_attrib_binding_f32(vao, 2, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 3); - gl.vertex_array_attrib_format_f32(vao, 3, 2, FLOAT, false, 24); - gl.vertex_array_attrib_binding_f32(vao, 3, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 4); - gl.vertex_array_attrib_format_f32(vao, 4, 4, UNSIGNED_BYTE, true, 32); - gl.vertex_array_attrib_binding_f32(vao, 4, 0); - gl_error!(gl); - + + let marker_vertex_buffer = create_buffer(gl); + let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); + Self { markers: Vec::new(), - marker_program, markers_wip: Vec::new(), - vb, + + marker_program, + marker_vertex_buffer, + marker_vertex_array, + trails: Vec::new(), trails_wip: Vec::new(), - trail_buffers: Default::default(), - vao, + + trail_program: trail_shift_program, + trail_vertex_buffers: Default::default(), + trail_vertex_arrays: Default::default(), } } } @@ -111,17 +100,22 @@ impl BillBoardRenderer { } unsafe { gl_error!(gl); - gl.bind_buffer(ARRAY_BUFFER, Some(self.vb)); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); gl.buffer_data_u8_slice(ARRAY_BUFFER, bytemuck::cast_slice(&vb), DYNAMIC_DRAW); gl_error!(gl); } - if self.trails.len() > self.trail_buffers.len() { - let needs = self.trails.len() - self.trail_buffers.len(); + if self.trails.len() > self.trail_vertex_buffers.len() { + let needs = self.trails.len() - self.trail_vertex_buffers.len(); for _ in 0..needs { - self.trail_buffers.push(unsafe { create_marker_buffer(gl) }); + let vb = unsafe { create_buffer(gl) }; + self.trail_vertex_buffers.push(vb); + let trail_vertex_array = unsafe { + create_trail_array(gl, vb, 1) + }; + self.trail_vertex_arrays.push(trail_vertex_array); } } - for (trail, trail_buffer) in self.trails.iter().zip(self.trail_buffers.iter()) { + for (trail, trail_buffer) in self.trails.iter().zip(self.trail_vertex_buffers.iter()) { unsafe { gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); gl.buffer_data_u8_slice( @@ -141,33 +135,64 @@ impl BillBoardRenderer { cam_pos: glam::Vec3, view_proj: &glam::Mat4, textures: &HashMap, + latest_time: f64, ) { unsafe { gl_error!(gl); gl.disable(SCISSOR_TEST); - gl.use_program(Some(self.marker_program)); - gl.bind_vertex_array(Some(self.vao)); + gl.use_program(Some(self.trail_program)); + gl_error!(gl); gl.active_texture(TEXTURE0); + gl_error!(gl); + let scroll_texture: Vec2 = Vec2 { x: 0.0, y: (latest_time as f32 % 2.0) - 1.0};//TODO: manage speed in some configurations. per trail ? - gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); - gl.uniform_matrix_4_f32_slice( - Some(&NativeUniformLocation(2)), - false, - view_proj.to_cols_array().as_ref(), - ); - for (trail, trail_buffer) in self.trails.iter().zip(self.trail_buffers.iter()) { + gl.uniform_2_f32_slice(Some(&NativeUniformLocation(3)), scroll_texture.as_ref()); + //https://stackoverflow.com/questions/27771902/opengl-changing-texture-coordinates-on-the-fly + //https://www.khronos.org/opengl/wiki/Uniform_(GLSL) + for ( (trail, trail_buffer), trail_array) + in self.trails.iter().zip(self.trail_vertex_buffers.iter()).zip(self.trail_vertex_arrays.iter() + ) { if let Some(texture) = textures.get(&trail.texture) { + gl.bind_vertex_array(Some(*trail_array)); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + + gl.bind_vertex_buffer(0, Some(*trail_buffer), 0, MARKER_VERTEX_STRIDE); gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); gl.bind_texture(TEXTURE_2D, Some(texture.handle)); gl.bind_sampler(0, Some(texture.sampler)); + gl_error!(gl); + gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl_error!(gl); + + /* + gl.polygon_mode(FRONT_AND_BACK, LINE); gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl.polygon_mode(FRONT_AND_BACK, FILL); + gl_error!(gl); + */ } } - gl.bind_vertex_buffer(0, Some(self.vb), 0, MARKER_VERTEX_STRIDE); - - gl.bind_buffer(ARRAY_BUFFER, Some(self.vb)); + gl.use_program(Some(self.marker_program)); + gl_error!(gl); + gl.bind_vertex_array(Some(self.marker_vertex_array)); + gl_error!(gl); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + gl.bind_vertex_buffer(0, Some(self.marker_vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); for (index, mo) in self.markers.iter().enumerate() { let index: u32 = index.try_into().unwrap(); if let Some(texture) = textures.get(&mo.texture) { @@ -192,6 +217,7 @@ pub fn new_program( fragment_shader_source: &str, _geometry_shader_source: Option<&str>, ) -> NativeProgram { + //https://www.khronos.org/opengl/wiki/Shader_Compilation#Program_setup unsafe { gl_error!(gl); @@ -262,7 +288,7 @@ pub fn new_program( program } } -unsafe fn create_marker_buffer(gl: &Context) -> NativeBuffer { +unsafe fn create_buffer(gl: &Context) -> NativeBuffer { gl_error!(gl); let vb = gl.create_buffer().expect("failed to create vb for markers"); gl_error!(gl); @@ -275,3 +301,50 @@ unsafe fn create_marker_buffer(gl: &Context) -> NativeBuffer { gl_error!(gl); vb } + +unsafe fn create_marker_array(gl: &Context, vertex_buffer: NativeBuffer) -> NativeVertexArray { + create_array(gl, vertex_buffer, 1) +} + +unsafe fn create_array(gl: &Context, vertex_buffer: NativeBuffer, binding_index: u32) -> NativeVertexArray { + let marker_vertex_array = gl.create_vertex_array().expect("failed to create egui vao"); + gl.bind_vertex_array(Some(marker_vertex_array)); + gl.bind_vertex_buffer(binding_index, Some(vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 0); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 0, 3, FLOAT, false, 0); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 0, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 1); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 1, 1, FLOAT, false, 12); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 1, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 2); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 2, 2, FLOAT, false, 16); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 2, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 3); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 3, 2, FLOAT, false, 24); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 3, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 4); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 4, 4, UNSIGNED_BYTE, true, 32); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 4, 0); + gl_error!(gl); + marker_vertex_array +} + +unsafe fn create_trail_array(gl: &Context, vertex_buffer: NativeBuffer, binding_index: u32) -> NativeVertexArray { + let trail_vertex_array = create_array(gl, vertex_buffer, binding_index); + gl.enable_vertex_array_attrib(trail_vertex_array, 5); + gl.vertex_array_attrib_format_f32(trail_vertex_array, 5, 2, FLOAT, false, 36); + gl.vertex_array_attrib_binding_f32(trail_vertex_array, 5, 0); + gl_error!(gl); + + trail_vertex_array +} diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs index 6621559..544b49e 100644 --- a/crates/joko_render/src/renderer.rs +++ b/crates/joko_render/src/renderer.rs @@ -249,6 +249,7 @@ impl JokoRenderer { meshes: Vec, textures_delta: egui::TexturesDelta, logical_screen_size: [f32; 2], + latest_time: f64, ) { if self.has_link && !self.is_map_open { self.billboard_renderer @@ -258,6 +259,7 @@ impl JokoRenderer { self.cam_pos, &self.view_proj, &self.gl.glow_backend.painter.managed_textures, + latest_time ); } self.gl diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index 63dfeea..f6d8b35 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -1,8 +1,15 @@ use cap_std::{ambient_authority, fs_utf8::camino::Utf8PathBuf, fs_utf8::Dir}; use miette::{Context, IntoDiagnostic, Result}; + /// Jokolay Configuration /// We will read a path from env `JOKOLAY_DATA_DIR` or create a folder at data_local_dir/jokolay, where data_local_dir is platform specific /// Inside this directory, we will store all of jokolay's data like configuration files, themes, logs etc.. + +pub fn get_jokolay_path() -> Result { + let dir = get_jokolay_dir().unwrap(); + dir.canonicalize(".") +} + pub fn get_jokolay_dir() -> Result { let authoratah = ambient_authority(); let jdir = if let Ok(env_dir) = std::env::var("JOKOLAY_DATA_DIR") { diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 2398239..2a06705 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -6,12 +6,12 @@ use std::{ }; use cap_std::fs_utf8::Dir; -use egui_window_glfw_passthrough::{glfw::{ffi::glfwGetVideoMode, Context as _}, GlfwBackend, GlfwConfig}; +use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; mod wm; mod mumble; use uuid::Uuid; -use init::get_jokolay_dir; +use init::{get_jokolay_dir, get_jokolay_path}; use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; //use jmf::FileManager; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; @@ -22,7 +22,7 @@ use joko_render::renderer::JokoRenderer; use jokolink::{MumbleChanges, MumbleLink, MumbleManager}; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span}; -use jmf::{LoadedPackData, LoadedPackTexture, build_from_core}; +use jmf::{LoadedPackData, LoadedPackTexture, build_from_core, jokolay_to_working_path}; use jmf::{ImportStatus, import_pack_from_zip_file_path}; @@ -48,6 +48,7 @@ struct JokolayBackState { choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI read_ui_link: bool, copy_of_ui_link: Option, + working_path: std::path::PathBuf, } struct JokolayApp { mumble_manager: MumbleManager, @@ -74,7 +75,7 @@ pub struct Jokolay { impl Jokolay { - pub fn new(jokolay_dir: Arc) -> Result { + pub fn new(jokolay_dir: Arc, working_path: std::path::PathBuf) -> Result { /* We have two mumble_managers, one for UI, one for backend, each keeping its own copy this avoid transmition between threads to read same data from system @@ -90,7 +91,7 @@ impl Jokolay { let package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; - + let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); let mut glfw_backend = GlfwBackend::new(GlfwConfig { @@ -153,13 +154,14 @@ impl Jokolay { nb_running_tasks_on_back: 0, nb_running_tasks_on_network: 0, import_status: Default::default(), - maximal_window_width: video_mode.unwrap().width, + maximal_window_width: video_mode.unwrap().width, //TODO: what happens if change of screen ? maximal_window_height: video_mode.unwrap().height, }, state_back: JokolayBackState { choice_of_category_changed: false, read_ui_link: false, copy_of_ui_link: Default::default(), + working_path, } }) } @@ -234,7 +236,7 @@ impl Jokolay { tracing::trace!("Handling of UIToBackMessage::ImportPack"); let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); let start = std::time::SystemTime::now(); - let result = import_pack_from_zip_file_path(file_path); + let result = import_pack_from_zip_file_path(file_path, &local_state.working_path); let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!("Loading of taco package from disk took {} ms", elaspsed.as_millis()); match result { @@ -729,6 +731,7 @@ impl Jokolay { etx.tessellate(shapes, etx.pixels_per_point()), textures_delta, glfw_backend.window_size_logical, + latest_time ); joko_renderer.present(); glfw_backend.window.swap_buffers(); @@ -740,14 +743,17 @@ impl Jokolay { } pub fn start_jokolay() { - let jdir = match get_jokolay_dir() { + let jokolay_dir = match get_jokolay_dir() { Ok(jdir) => jdir, Err(e) => { eprintln!("failed to create jokolay dir: {e:#?}"); panic!("failed to create jokolay_dir: {e:#?}"); } }; - let log_file_flush_guard = match JokolayTracingLayer::install_tracing(&jdir) { + let jokolay_path = get_jokolay_path().unwrap().as_std_path().to_path_buf(); + let working_path = jokolay_to_working_path(&jokolay_path); + + let log_file_flush_guard = match JokolayTracingLayer::install_tracing(&jokolay_dir) { Ok(g) => g, Err(e) => { eprintln!("failed to install tracing: {e:#?}"); @@ -767,7 +773,7 @@ pub fn start_jokolay() { ); } - match Jokolay::new(jdir.into()) { + match Jokolay::new(jokolay_dir.into(), working_path) { Ok(jokolay) => { jokolay.enter_event_loop(); } diff --git a/crates/jokolay/src/manager/trace/mod.rs b/crates/jokolay/src/manager/trace/mod.rs index 4ed1fc5..d3e0f4f 100644 --- a/crates/jokolay/src/manager/trace/mod.rs +++ b/crates/jokolay/src/manager/trace/mod.rs @@ -31,7 +31,7 @@ impl JokolayTracingLayer { }; eprintln!("{output}"); eprintln!("Backtrace: {backtrace:}"); - let mut w = File::create("jokolay.errror").unwrap(); + let mut w = File::create("jokolay.error").unwrap(); writeln!(&mut w, "{output}").unwrap(); writeln!(&mut w, "Backtrace: {backtrace:}").unwrap(); })); From 310a1b982a2fe2e7053447b87b252e7c815df39d Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 21 Apr 2024 16:50:29 +0200 Subject: [PATCH 33/54] enforce cargo fmt on all files + steps to load a TaCo folder and not zip only --- crates/joko_core/src/lib.rs | 3 - crates/joko_core/src/task/mod.rs | 18 +- crates/joko_package/Cargo.toml | 1 + crates/joko_package/src/io/deserialize.rs | 594 +++++++++++------- crates/joko_package/src/io/export.rs | 67 +- crates/joko_package/src/io/mod.rs | 4 +- crates/joko_package/src/io/serialize.rs | 66 +- crates/joko_package/src/lib.rs | 12 +- crates/joko_package/src/manager/mod.rs | 9 +- .../src/manager/pack/activation.rs | 3 +- .../joko_package/src/manager/pack/active.rs | 21 +- .../src/manager/pack/category_selection.rs | 95 ++- crates/joko_package/src/manager/pack/dirty.rs | 3 +- .../src/manager/pack/file_selection.rs | 13 +- .../joko_package/src/manager/pack/import.rs | 10 +- .../joko_package/src/manager/pack/loaded.rs | 422 ++++++++----- crates/joko_package/src/manager/pack/mod.rs | 7 +- crates/joko_package/src/manager/package.rs | 441 +++++++------ crates/joko_package/src/message.rs | 44 +- crates/joko_package_models/src/attributes.rs | 29 +- crates/joko_package_models/src/category.rs | 119 ++-- crates/joko_package_models/src/lib.rs | 2 - crates/joko_package_models/src/map.rs | 5 +- crates/joko_package_models/src/marker.rs | 1 - crates/joko_package_models/src/package.rs | 281 ++++++--- crates/joko_package_models/src/route.rs | 17 +- crates/joko_render/src/billboard.rs | 63 +- crates/joko_render/src/gl.rs | 2 +- crates/joko_render/src/lib.rs | 4 +- crates/joko_render/src/renderer.rs | 71 +-- crates/jokoapi/src/end_point/races/mod.rs | 2 +- crates/jokolay/Cargo.toml | 2 + crates/jokolay/src/app/init.rs | 15 +- crates/jokolay/src/app/mod.rs | 401 ++++++++---- crates/jokolay/src/app/mumble.rs | 22 +- crates/jokolay/src/app/ui_parameters.rs | 136 ++++ crates/jokolay/src/app/wm.rs | 75 --- crates/jokolay/src/lib.rs | 1 - crates/jokolay/src/manager/theme/mod.rs | 31 +- crates/jokolay/src/manager/trace/mod.rs | 6 +- crates/jokolink/src/lib.rs | 12 +- crates/jokolink/src/mumble/mod.rs | 2 +- 42 files changed, 1946 insertions(+), 1186 deletions(-) create mode 100644 crates/jokolay/src/app/ui_parameters.rs delete mode 100644 crates/jokolay/src/app/wm.rs diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index e5bf55d..5a24a03 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -13,7 +13,6 @@ each manager must have pub mod task; - /// This newtype is used to represents relative paths in marker packs /// 1. It won't start with `/` or `C:` like roots, because its a relative path /// 2. It can be empty to represent current directory @@ -99,5 +98,3 @@ impl FromStr for RelativePath { Ok(Self(path.into())) } } - - diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs index 89afa4d..a955f3a 100644 --- a/crates/joko_core/src/task/mod.rs +++ b/crates/joko_core/src/task/mod.rs @@ -1,10 +1,14 @@ use std::{ - result::Result, sync::{mpsc::{RecvError, SendError}, Arc, Mutex}, thread::JoinHandle + result::Result, + sync::{ + mpsc::{RecvError, SendError}, + Arc, Mutex, + }, + thread::JoinHandle, }; //TODO: could this be a wrapper only and a move/copy would not impact content ? -pub struct AsyncTaskGuard -{ +pub struct AsyncTaskGuard { task_sender: std::sync::mpsc::Sender, result_receiver: std::sync::mpsc::Receiver, thread_task: Option>, @@ -33,12 +37,12 @@ where result_receiver, thread_task: None, thread_nb: None, - nb: Arc::clone(&nb) + nb: Arc::clone(&nb), })); let thread_task = std::thread::spawn(move || { while let Ok(elt) = th_task_receiver.recv() { let _guard = scopeguard::guard(0, |_| { - nb_sender.send(-1); + nb_sender.send(-1); }); nb_sender.send(1); th_result_sender.send(f(elt)); @@ -51,7 +55,7 @@ where } } }); - + { let mut t = res.lock().unwrap(); t.thread_task = Some(thread_task); @@ -73,4 +77,4 @@ where let nb = self.nb.load(std::sync::atomic::Ordering::Relaxed); nb != 0 } -} \ No newline at end of file +} diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml index 10da30d..f83282d 100644 --- a/crates/joko_package/Cargo.toml +++ b/crates/joko_package/Cargo.toml @@ -38,6 +38,7 @@ url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } xot = { version = "0.16.0" } zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) +walkdir = "2.5.0" diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index c4289d7..235a703 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -1,21 +1,30 @@ use joko_core::RelativePath; -use joko_package_models::{attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, marker::Marker, package::{PackCore, PackageImportReport}, route::Route, trail::{TBin, TBinStatus, Trail}}; +use joko_package_models::{ + attributes::{CommonAttributes, XotAttributeNameIDs}, + category::{prefix_parent, Category, RawCategory}, + marker::Marker, + package::{PackCore, PackageImportReport}, + route::Route, + trail::{TBin, TBinStatus, Trail}, +}; use miette::{bail, Context, IntoDiagnostic, Result}; use crate::BASE64_ENGINE; use base64::Engine; use cap_std::fs_utf8::{Dir, DirEntry}; use glam::Vec3; -use std::{collections::VecDeque, io::Read, str::FromStr}; use ordered_hash_map::OrderedHashMap; +use std::{collections::VecDeque, io::Read, str::FromStr}; use tracing::{debug, error, info, info_span, instrument, trace, warn}; use uuid::Uuid; -use xot::{Node, Xot, Element}; - +use xot::{Element, Node, Xot}; const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; -pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option ) -> Result { +pub(crate) fn load_pack_core_from_normalized_folder( + core_dir: &Dir, + import_report: Option, +) -> Result { //called from already parsed data let mut core_pack = PackCore::new(); if let Some(mut import_report) = import_report { @@ -32,7 +41,10 @@ pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option { //already done } - map_id => { + file_name => { // parse map file - let span_guard = info_span!("load map", map_id).entered(); - if let Ok(map_id) = map_id.parse::() { - //let mut partial_pack = PackCore::partial(&core_pack.all_categories); - load_map_file(map_id, &dir_entry, &mut core_pack)?; - //core_pack.merge_partial(partial_pack); - } else { - info!("unrecognized xml file {map_id}") - } + let span_guard = info_span!("load file", file_name).entered(); + //let mut partial_pack = PackCore::partial(&core_pack.all_categories); + load_xml_from_normalized_file(file_name, &dir_entry, &mut core_pack)?; + //core_pack.merge_partial(partial_pack); std::mem::drop(span_guard); } } @@ -86,16 +94,21 @@ pub(crate) fn load_pack_core_from_dir(core_dir: &Dir, import_report: Option Option { map_id_bytes.copy_from_slice(&bytes[4..8]); let map_id = u32::from_ne_bytes(map_id_bytes); - let zero = Vec3{x:0.0, y:0.0, z:0.0}; + let zero = Vec3 { + x: 0.0, + y: 0.0, + z: 0.0, + }; // this will either be empty vec or series of vec3s. let nodes: VecDeque = bytes[8..] @@ -208,7 +225,7 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let mut iso_y = false; let mut iso_z = false; let mut closed = false; - let mut resulting_nodes : Vec = Vec::new(); + let mut resulting_nodes: Vec = Vec::new(); if nodes.len() > 0 { let ref_node = nodes[0]; let mut c_iso_x = true; @@ -246,11 +263,17 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { iso_x = c_iso_x; iso_y = c_iso_y; iso_z = c_iso_z; - if nodes.len() > 1 {// TODO: get this threshold from configuration - closed = nodes.front().unwrap().distance(*nodes.back().unwrap()).abs() < 0.1 + if nodes.len() > 1 { + // TODO: get this threshold from configuration + closed = nodes + .front() + .unwrap() + .distance(*nodes.back().unwrap()) + .abs() + < 0.1 } } - Some(TBinStatus{ + Some(TBinStatus { tbin: TBin { map_id, version, @@ -259,7 +282,7 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { iso_x, iso_y, iso_z, - closed + closed, }) } @@ -272,10 +295,17 @@ fn parse_categories( source_file_uuid: &Uuid, ) { //called once per file - parse_categories_recursive(pack, tree, tags, first_pass_categories, names, None, source_file_uuid) + parse_categories_recursive( + pack, + tree, + tags, + first_pass_categories, + names, + None, + source_file_uuid, + ) } - // a recursive function to parse the marker category tree. fn parse_categories_recursive( pack: &mut PackCore, @@ -326,28 +356,40 @@ fn parse_categories_recursive( name.to_string() }; let guid = parse_guid(names, ele); - trace!("recursive_marker_category_parser {} {} {:?}", name, guid, parent_name); + trace!( + "recursive_marker_category_parser {} {} {:?}", + name, + guid, + parent_name + ); if !first_pass_categories.contains_key(&full_category_name) { let mut sources: OrderedHashMap = OrderedHashMap::new(); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_inherited_texture(icon_file.as_str().to_string(), full_category_name.clone(), source_file_uuid); + pack.found_missing_inherited_texture( + icon_file.as_str().to_string(), + full_category_name.clone(), + source_file_uuid, + ); } } sources.insert(guid.clone(), source_file_uuid.clone()); - first_pass_categories.insert(full_category_name.clone(), RawCategory { - guid, - parent_name: parent_name.clone(), - display_name: display_name.to_string(), - relative_category_name: name.to_string(), - full_category_name: full_category_name.clone(), - separator, - default_enabled, - props: common_attributes, - sources, - }); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + guid, + parent_name: parent_name.clone(), + display_name: display_name.to_string(), + relative_category_name: name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: common_attributes, + sources, + }, + ); } parse_categories_recursive( pack, @@ -356,12 +398,16 @@ fn parse_categories_recursive( first_pass_categories, names, Some(full_category_name), - source_file_uuid + source_file_uuid, ); } } -fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { +fn parse_categories_from_normalized_file( + file_name: &String, + cats_xml_str: &str, + pack: &mut PackCore, +) -> Result<()> { let mut tree = xot::Xot::new(); let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); let root_node = tree @@ -399,8 +445,11 @@ fn parse_categories_file(file_name: &String, cats_xml_str: &str, pack: &mut Pack Ok(()) } - -fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Result<()> { +fn load_xml_from_normalized_file( + file_name: &str, + dir_entry: &DirEntry, + target: &mut PackCore, +) -> Result<()> { let mut xml_str = String::new(); dir_entry .open() @@ -410,12 +459,11 @@ fn load_map_file(map_id: u32, dir_entry: &DirEntry, target: &mut PackCore) -> Re .into_diagnostic() .wrap_err("faield to read xml string")?; //TODO: launch an async load of the file + make a priority queue to have current map first - parse_map_xml_string(map_id, &xml_str, target).wrap_err_with(|| { - miette::miette!("error parsing map file: {map_id}") - }) + parse_map_xml_string(file_name, &xml_str, target) + .wrap_err_with(|| miette::miette!("error parsing file: {file_name}")) } -fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) -> Result<()> { +fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCore) -> Result<()> { let mut tree = Xot::new(); let root_node = tree .parse(map_xml_str) @@ -448,10 +496,14 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - .get_attribute(names.category) .unwrap_or_default() .to_lowercase(); - + let span_guard = info_span!("category", full_category_name).entered(); - - let opt_source_file_uuid = Uuid::from_str(child_element.get_attribute(names._source_file_name).unwrap_or_default()); + + let opt_source_file_uuid = Uuid::from_str( + child_element + .get_attribute(names._source_file_name) + .unwrap_or_default(), + ); let source_file_uuid = if opt_source_file_uuid.is_err() { error!("Package corrupted, invalid source file uuid"); //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); @@ -460,19 +512,30 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - opt_source_file_uuid.unwrap() }; - if let Some(source_file_name) = target.report.source_file_uuid_to_name(&source_file_uuid) { - let source_file_name = source_file_name.clone();// this is to bypass borrow checker which has no idea this cannot be changed + if let Some(source_file_name) = + target.report.source_file_uuid_to_name(&source_file_uuid) + { + let source_file_name = source_file_name.clone(); // this is to bypass borrow checker which has no idea this cannot be changed target.register_source_file(&source_file_name); } else { println!("{:?}", source_file_uuid); } //There is no file name, only an uuid to register - target.active_source_files.insert(source_file_uuid.clone(), true); + target + .active_source_files + .insert(source_file_uuid.clone(), true); if child_element.name() == names.route { debug!("Found a route in core pack {:?}", child_element); - let route = parse_route(&names, &tree, &poi_node, child_element, &full_category_name, source_file_uuid.clone()); + let route = parse_route( + &names, + &tree, + &poi_node, + child_element, + &full_category_name, + source_file_uuid.clone(), + ); if let Some(route) = route { target.register_route(route)?; } else { @@ -480,22 +543,35 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - } } else { if full_category_name.is_empty() { - panic!("full_category_name is empty {:?} {:?}", map_xml_str, child_element); + panic!( + "full_category_name is empty {:?} {:?}", + map_xml_str, child_element + ); } let raw_uid = child_element.get_attribute(names.guid); if raw_uid.is_none() { - info!("This POI is either invalid or inside a Route {:?}", child_element); + info!( + "This POI is either invalid or inside a Route {:?}", + child_element + ); span_guard.exit(); continue; } //FIXME: this needs to be changed for partial load let opt_cat_uuid = target.get_category_uuid(&full_category_name); if opt_cat_uuid.is_none() { - error!("Mandatory category missing, packge is corrupted {:?} {:?}", map_id, child_element); - return Err(miette::Report::msg(format!("Mandatory category missing, packge is corrupted {:?} {:?}", map_xml_str, child_element))); + error!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + file_name, child_element + ); + return Err(miette::Report::msg(format!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + map_xml_str, child_element + ))); } - let category_uuid = opt_cat_uuid.unwrap().clone();//categories MUST exist, they have already been parsed - let guid = raw_uid.and_then(|guid| { + let category_uuid = opt_cat_uuid.unwrap().clone(); //categories MUST exist, they have already been parsed + let guid = raw_uid + .and_then(|guid| { let mut buffer = [0u8; 20]; BASE64_ENGINE .decode_slice(guid, &mut buffer) @@ -503,17 +579,14 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) }) .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; - + if child_element.name() == names.poi { debug!("Found a POI in core pack {:?}", child_element); - if child_element + let map_id = child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } + .ok_or_else(|| miette::miette!("invalid mapid"))?; + let xpos = child_element .get_attribute(names.xpos) .unwrap_or_default() @@ -539,19 +612,15 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - parent: category_uuid.clone(), attrs: ca, guid, - source_file_uuid + source_file_uuid, }; target.register_marker(full_category_name, marker); } else if child_element.name() == names.trail { debug!("Found a trail in core pack {:?}", child_element); - if child_element + let map_id = child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } + .ok_or_else(|| miette::miette!("invalid mapid"))?; let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); @@ -562,10 +631,9 @@ fn parse_map_xml_string(map_id: u32, map_xml_str: &str, target: &mut PackCore) - props: ca, guid, dynamic: false, - source_file_uuid + source_file_uuid, }; target.register_trail(full_category_name, trail)?; - } } span_guard.exit(); @@ -591,11 +659,13 @@ fn parse_category_categories_xml_recursive( continue; } - let relative_category_name = ele.get_attribute(names.name) - .or(ele.get_attribute(names.display_name) - .or(ele.get_attribute(names.capital_name) - ) - ).unwrap_or_default().to_lowercase(); + let relative_category_name = ele + .get_attribute(names.name) + .or(ele + .get_attribute(names.display_name) + .or(ele.get_attribute(names.capital_name))) + .unwrap_or_default() + .to_lowercase(); if relative_category_name.is_empty() { info!("category doesn't have a name attribute: {ele:#?}"); continue; @@ -629,10 +699,17 @@ fn parse_category_categories_xml_recursive( relative_category_name.to_string() }; let guid = parse_guid(names, ele); - trace!("recursive_marker_category_parser_categories_xml {} {} {:?}", full_category_name, guid, parent_uuid); + trace!( + "recursive_marker_category_parser_categories_xml {} {} {:?}", + full_category_name, + guid, + parent_uuid + ); if display_name.is_empty() { if parent_name.is_some() { - return Err(miette::Error::msg("Package is corrupted, please import it again with current version")); + return Err(miette::Error::msg( + "Package is corrupted, please import it again with current version", + )); } parse_category_categories_xml_recursive( file_name, @@ -645,7 +722,6 @@ fn parse_category_categories_xml_recursive( Some(full_category_name), )?; } else { - let current_category = if let Some(c) = cats.get_mut(&guid) { c } else { @@ -664,17 +740,17 @@ fn parse_category_categories_xml_recursive( cats.back_mut().unwrap() }; parse_category_categories_xml_recursive( - file_name, - tree, - tree.children(tag), - pack, - &mut current_category.children, - names, - Some(guid), - Some(full_category_name), - )?; + file_name, + tree, + tree.children(tag), + pack, + &mut current_category.children, + names, + Some(guid), + Some(full_category_name), + )?; }; - + std::mem::drop(span_guard); } else { //it may be a comment, a space, anything @@ -684,8 +760,10 @@ fn parse_category_categories_xml_recursive( Ok(()) } - -pub(crate) fn get_pack_from_taco_zip(input_path: std::path::PathBuf, working_path: &std::path::PathBuf) -> Result { +pub(crate) fn get_pack_from_taco_zip( + input_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result { let mut taco_zip = vec![]; std::fs::File::open(&input_path) .into_diagnostic()? @@ -695,9 +773,14 @@ pub(crate) fn get_pack_from_taco_zip(input_path: std::path::PathBuf, working_pat let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) .into_diagnostic() .wrap_err("failed to read zip archive")?; - zip_archive.extract(working_path).into_diagnostic()?; + if extract_temporary_path.exists() { + std::fs::remove_dir_all(extract_temporary_path).into_diagnostic()?; + } + zip_archive + .extract(extract_temporary_path) + .into_diagnostic()?; - _get_pack_from_taco_zip(zip_archive) + _get_pack_from_taco_folder(extract_temporary_path) } /// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. @@ -706,11 +789,9 @@ pub(crate) fn get_pack_from_taco_zip(input_path: std::path::PathBuf, working_pat /// but any other errors like invalid attributes or missing markers etc.. will just be logged. /// the intention is "best effort" parsing and not "validating" xml marker packs. /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. + #[instrument(skip_all)] -fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive>>) -> Result { - //TODO: simply extract the file into a working folder ? - //called to import a new pack - // all the contents of ZPack +fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { let mut pack = PackCore::new(); // file paths of different file types @@ -719,30 +800,37 @@ fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive { - pack.register_texture(name, &file_path, bytes); - }, + pack.register_texture(file_path, &relative_file_path, bytes); + } Err(e) => { info!(?e, "failed to parse image file"); } @@ -751,11 +839,10 @@ fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive = Default::default(); for source_file_name in xmls.iter() { - let mut xml_str = String::new(); - let span_guard = info_span!("deserialize xml first pass: load file", source_file_name).entered(); - if zip_archive - .by_name(&source_file_name) - .ok() - .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) - .is_none() - { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml first pass: load file", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { info!("failed to read file from zip"); continue; }; - let source_file_uuid = pack.register_source_file(source_file_name); + let source_file_uuid = pack.register_source_file(&source_file_name); let filtered_xml_str = crate::rapid_filter_rust(xml_str); let mut tree = Xot::new(); @@ -820,7 +910,14 @@ fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive = OrderedHashMap::new(); sources.insert(guid.clone(), source_file_uuid.clone()); - first_pass_categories.insert(full_category_name.clone(), RawCategory{ - default_enabled: true, - guid: category_uuid, - parent_name: prefix_parent(&full_category_name, '.'), - display_name: full_category_name.clone(), - full_category_name: full_category_name.clone(), - relative_category_name: full_category_name.clone(), - props: Default::default(), - separator: false, - sources - }); - debug!("There is an orphan missing category '{}' which was created", full_category_name); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + default_enabled: true, + guid: category_uuid, + parent_name: prefix_parent(&full_category_name, '.'), + display_name: full_category_name.clone(), + full_category_name: full_category_name.clone(), + relative_category_name: full_category_name.clone(), + props: Default::default(), + separator: false, + sources, + }, + ); + debug!( + "There is an orphan missing category '{}' which was created", + full_category_name + ); } else { let cat = first_pass_categories.get_mut(&full_category_name); - cat.unwrap().sources.insert(guid.clone(), source_file_uuid.clone()); + cat.unwrap() + .sources + .insert(guid.clone(), source_file_uuid.clone()); } } drop(span_guard); } span_guard_second_pass.exit(); - - let elaspsed_second_pass = start_categories_loading_second_pass.elapsed().unwrap_or_default(); + + let elaspsed_second_pass = start_categories_loading_second_pass + .elapsed() + .unwrap_or_default(); pack.report.telemetry.categories_second_pass = elaspsed_second_pass.as_millis(); let start_categories_reassemble = std::time::SystemTime::now(); @@ -937,13 +1048,15 @@ fn _get_pack_from_taco_zip(mut zip_archive: zip::ZipArchive Option { - child - .get_attribute(names.guid) - .and_then(|guid| { + child.get_attribute(names.guid).and_then(|guid| { let mut buffer = [0u8; 20]; BASE64_ENGINE .decode_slice(guid, &mut buffer) @@ -1075,17 +1211,29 @@ fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option Uuid{ +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid { parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) } -fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_uuid: Uuid) -> Option { +fn parse_marker( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + poi_element: &Element, + guid: Uuid, + category_name: &String, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { let mut common_attributes = CommonAttributes::default(); common_attributes.update_common_attributes_from_element(poi_element, &names); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_element_texture(icon_file.as_str().to_string(), guid, &source_file_uuid); + pack.found_missing_element_texture( + icon_file.as_str().to_string(), + guid, + &source_file_uuid, + ); } } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { debug!(icf, "marker's icon file attribute failed to parse"); @@ -1118,7 +1266,7 @@ fn parse_marker(pack: &mut PackCore, names: &XotAttributeNameIDs, poi_element: & parent: category_uuid.clone(), attrs: common_attributes, guid, - source_file_uuid + source_file_uuid, }) } else { debug!("missing map id"); @@ -1142,15 +1290,14 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { .unwrap_or_default() .parse::() .unwrap_or_default(); - Vec3{x, y, z} + Vec3 { x, y, z } } - fn parse_route_category( names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, + tree: &Xot, + route_node: &Node, + route_element: &Element, ) -> Option { for child_node in tree.children(*route_node) { let child = match tree.element(child_node) { @@ -1169,13 +1316,12 @@ fn parse_route_category( fn parse_route( names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, - category_name: &String, - source_file_uuid: Uuid + tree: &Xot, + route_node: &Node, + route_element: &Element, + category_name: &String, + source_file_uuid: Uuid, ) -> Option { - let mut path: Vec = Vec::new(); let resetposx = route_element .get_attribute(names.resetposx) @@ -1193,8 +1339,12 @@ fn parse_route( .parse::() .unwrap_or_default(); let reset_position = Vec3::new(resetposx, resetposy, resetposz); - let reset_range = route_element.get_attribute(names.reset_range).and_then(|map_id| map_id.parse::().ok()); - let name = route_element.get_attribute(names.name).or(route_element.get_attribute(names.capital_name)); + let reset_range = route_element + .get_attribute(names.reset_range) + .and_then(|map_id| map_id.parse::().ok()); + let name = route_element + .get_attribute(names.name) + .or(route_element.get_attribute(names.capital_name)); if name.is_none() { info!("route element is missing name: {route_element:?}"); @@ -1202,7 +1352,8 @@ fn parse_route( } let mut category: String = category_name.clone(); let mut category_uuid: Option = parse_optional_guid(names, route_element); - let mut map_id: Option = route_element.get_attribute(names.map_id) + let mut map_id: Option = route_element + .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()); for child_node in tree.children(*route_node) { let child = match tree.element(child_node) { @@ -1242,11 +1393,14 @@ fn parse_route( info!("Could not find a uuid for route element: {route_element:?}"); return None; } - debug!("found route with {:?} elements {route_element:?}", path.len()); + debug!( + "found route with {:?} elements {route_element:?}", + path.len() + ); Some(Route { category, - parent: category_uuid.unwrap(), + parent: category_uuid.unwrap(), path, reset_position, reset_range: reset_range.unwrap_or(0.0), @@ -1257,8 +1411,15 @@ fn parse_route( }) } - -fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: &Element, guid: Uuid, category_name: &String, category_uuid: &Uuid, source_file_uuid: Uuid) -> Option { +fn parse_trail( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + trail_element: &Element, + guid: Uuid, + category_name: &String, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html let mut common_attributes = CommonAttributes::default(); @@ -1272,19 +1433,18 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: } if let Some(map_id) = trail_element - .get_attribute(names.trail_data) + .get_attribute(names.trail_data) .and_then(|trail_data| { //fix the path which may be a mix of windows and linux path let file_path: RelativePath = trail_data.parse().unwrap(); if let Some(tb) = pack.tbins.get(&file_path) { Some(tb.map_id) - }else { + } else { pack.found_missing_trail(&file_path, guid, &source_file_uuid); None } }) { - Some(Trail { category: category_name.clone(), parent: category_uuid.clone(), @@ -1303,7 +1463,6 @@ fn parse_trail(pack: &mut PackCore, names: &XotAttributeNameIDs, trail_element: */ None } - } #[instrument(skip(zip_archive))] @@ -1332,7 +1491,6 @@ fn read_file_bytes_from_zip_by_name( None } - // #[cfg(test)] // mod test { diff --git a/crates/joko_package/src/io/export.rs b/crates/joko_package/src/io/export.rs index 7108ab3..73fc2f2 100644 --- a/crates/joko_package/src/io/export.rs +++ b/crates/joko_package/src/io/export.rs @@ -4,7 +4,10 @@ use crate::{ }; use base64::Engine; use cap_std::fs_utf8::Dir; -use joko_package_models::{attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, route::Route, trail::Trail}; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, + route::Route, trail::Trail, +}; use miette::{Context, IntoDiagnostic, Result}; use ordered_hash_map::OrderedHashMap; use std::io::Write; @@ -27,7 +30,12 @@ pub(crate) fn export_package_v1( writing_directory: &Dir, ) -> Result<()> { // save categories - info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); + info!( + "Saving data pack {}, {} categories, {} maps", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len() + ); let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); let od = tree.new_element(names.overlay_data); @@ -42,7 +50,8 @@ pub(crate) fn export_package_v1( .to_string(root_node) .into_diagnostic() .wrap_err("failed to convert cats xot to string")?; - writing_directory.create("categories.xml") + writing_directory + .create("categories.xml") .into_diagnostic() .wrap_err("failed to create categories.xml")? .write_all(cats.as_bytes()) @@ -96,7 +105,8 @@ pub(crate) fn export_package_v1( .to_string(root_node) .into_diagnostic() .wrap_err("failed to serialize map data to string")?; - writing_directory.create(format!("{map_id}.xml")) + writing_directory + .create(format!("{map_id}.xml")) .into_diagnostic() .wrap_err("failed to create map xml file")? .write_all(map_xml.as_bytes()) @@ -109,30 +119,35 @@ pub(crate) fn save_pack_texture_to_dir( pack_texture: &LoadedPackTexture, writing_directory: &Dir, ) -> Result<()> { - - info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); // save images for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { - writing_directory.create_dir_all(parent) + writing_directory + .create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir for an image: {img_path}") })?; } - writing_directory.create(img_path.as_str()) + writing_directory + .create(img_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? .write(img) .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to write image bytes to file: {img_path}") - })?; + .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; } // save tbins for (tbin_path, tbin) in pack_texture.tbins.iter() { if let Some(parent) = tbin_path.parent() { - writing_directory.create_dir_all(parent) + writing_directory + .create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir of tbin: {tbin_path}") @@ -147,7 +162,8 @@ pub(crate) fn save_pack_texture_to_dir( bytes.extend_from_slice(&node[1].to_ne_bytes()); bytes.extend_from_slice(&node[2].to_ne_bytes()); } - writing_directory.create(tbin_path.as_str()) + writing_directory + .create(tbin_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? .write_all(&bytes) @@ -189,7 +205,10 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); ele.set_attribute(names.category, &trail.category); ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute(names._source_file_name, format!("{}", trail.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); trail.props.serialize_to_element(ele, names); } @@ -200,17 +219,25 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); - ele.set_attribute(names._source_file_name, format!("{}", marker.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); marker.attrs.serialize_to_element(ele, names); } -fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<()> { let route_node = tree.new_element(names.route); tree.append(*parent, route_node) .into_diagnostic() .wrap_err("failed to append route to pois")?; let ele = tree.element_mut(route_node).unwrap(); - + ele.set_attribute(names.category, route.category.clone()); ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); @@ -220,7 +247,10 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); ele.set_attribute(names.map_id, format!("{}", route.map_id)); ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute(names._source_file_name, format!("{}", route.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); for pos in &route.path { let child = tree.new_element(names.poi); tree.append(route_node, child); @@ -232,4 +262,3 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name } Ok(()) } - diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs index 4b8fca7..7c6d495 100644 --- a/crates/joko_package/src/io/mod.rs +++ b/crates/joko_package/src/io/mod.rs @@ -3,8 +3,8 @@ mod deserialize; mod error; -mod serialize; mod export; +mod serialize; -pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; +pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_normalized_folder}; pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index a44b7b2..902788e 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -4,7 +4,9 @@ use crate::{ }; use base64::Engine; use cap_std::fs_utf8::Dir; -use joko_package_models::{attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail}; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, +}; use miette::{Context, IntoDiagnostic, Result}; use ordered_hash_map::OrderedHashMap; use std::io::Write; @@ -18,7 +20,12 @@ pub(crate) fn save_pack_data_to_dir( writing_directory: &Dir, ) -> Result<()> { // save categories - info!("Saving data pack {}, {} categories, {} maps", pack_data.name, pack_data.categories.len(), pack_data.maps.len()); + info!( + "Saving data pack {}, {} categories, {} maps", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len() + ); let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); let od = tree.new_element(names.overlay_data); @@ -33,7 +40,8 @@ pub(crate) fn save_pack_data_to_dir( .to_string(root_node) .into_diagnostic() .wrap_err("failed to convert cats xot to string")?; - writing_directory.create("categories.xml") + writing_directory + .create("categories.xml") .into_diagnostic() .wrap_err("failed to create categories.xml")? .write_all(cats.as_bytes()) @@ -87,7 +95,8 @@ pub(crate) fn save_pack_data_to_dir( .to_string(root_node) .into_diagnostic() .wrap_err("failed to serialize map data to string")?; - writing_directory.create(format!("{map_id}.xml")) + writing_directory + .create(format!("{map_id}.xml")) .into_diagnostic() .wrap_err("failed to create map xml file")? .write_all(map_xml.as_bytes()) @@ -100,30 +109,35 @@ pub(crate) fn save_pack_texture_to_dir( pack_texture: &LoadedPackTexture, writing_directory: &Dir, ) -> Result<()> { - - info!("Saving texture pack {}, {} textures, {} tbins", pack_texture.name, pack_texture.textures.len(), pack_texture.tbins.len()); + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); // save images for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { - writing_directory.create_dir_all(parent) + writing_directory + .create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir for an image: {img_path}") })?; } - writing_directory.create(img_path.as_str()) + writing_directory + .create(img_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? .write(img) .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to write image bytes to file: {img_path}") - })?; + .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; } // save tbins for (tbin_path, tbin) in pack_texture.tbins.iter() { if let Some(parent) = tbin_path.parent() { - writing_directory.create_dir_all(parent) + writing_directory + .create_dir_all(parent) .into_diagnostic() .wrap_err_with(|| { miette::miette!("failed to create parent dir of tbin: {tbin_path}") @@ -138,7 +152,8 @@ pub(crate) fn save_pack_texture_to_dir( bytes.extend_from_slice(&node[1].to_ne_bytes()); bytes.extend_from_slice(&node[2].to_ne_bytes()); } - writing_directory.create(tbin_path.as_str()) + writing_directory + .create(tbin_path.as_str()) .into_diagnostic() .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? .write_all(&bytes) @@ -180,7 +195,10 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); ele.set_attribute(names.category, &trail.category); ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute(names._source_file_name, format!("{}", trail.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); trail.props.serialize_to_element(ele, names); } @@ -191,17 +209,25 @@ fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAt ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); - ele.set_attribute(names._source_file_name, format!("{}", marker.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); marker.attrs.serialize_to_element(ele, names); } -fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, names: &XotAttributeNameIDs) -> Result<()> { +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<()> { let route_node = tree.new_element(names.route); tree.append(*parent, route_node) .into_diagnostic() .wrap_err("failed to append route to pois")?; let ele = tree.element_mut(route_node).unwrap(); - + ele.set_attribute(names.category, route.category.clone()); ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); @@ -211,7 +237,10 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); ele.set_attribute(names.map_id, format!("{}", route.map_id)); ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute(names._source_file_name, format!("{}", route.source_file_uuid)); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); for pos in &route.path { let child = tree.new_element(names.poi); tree.append(route_node, child); @@ -223,4 +252,3 @@ fn serialize_route_to_element(tree: &mut Xot, route: &Route, parent: &Node, name } Ok(()) } - diff --git a/crates/joko_package/src/lib.rs b/crates/joko_package/src/lib.rs index 1d0e49f..d10d8a8 100644 --- a/crates/joko_package/src/lib.rs +++ b/crates/joko_package/src/lib.rs @@ -7,15 +7,9 @@ pub(crate) mod manager; pub mod message; pub use manager::{ - PackageDataManager, - PackageUIManager, - LoadedPackData, - LoadedPackTexture, - load_all_from_dir, - build_from_core, - jokolay_to_working_path, - ImportStatus, - import_pack_from_zip_file_path + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, load_all_from_dir, ImportStatus, LoadedPackData, LoadedPackTexture, + PackageDataManager, PackageUIManager, }; // for compile time build info like pkg version or build timestamp or git hash etc.. diff --git a/crates/joko_package/src/manager/mod.rs b/crates/joko_package/src/manager/mod.rs index b15e3ea..b49fe0f 100644 --- a/crates/joko_package/src/manager/mod.rs +++ b/crates/joko_package/src/manager/mod.rs @@ -16,9 +16,12 @@ We will make not having a valid category/texture/tbin path as allowed. So, users */ -mod package; mod pack; +mod package; +pub use pack::import::{import_pack_from_zip_file_path, ImportStatus}; +pub use pack::loaded::{ + build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, load_all_from_dir, + LoadedPackData, LoadedPackTexture, +}; pub use package::{PackageDataManager, PackageUIManager}; -pub use pack::loaded::{LoadedPackData, LoadedPackTexture, load_all_from_dir, build_from_core, jokolay_to_working_path}; -pub use pack::import::{ImportStatus, import_pack_from_zip_file_path}; \ No newline at end of file diff --git a/crates/joko_package/src/manager/pack/activation.rs b/crates/joko_package/src/manager/pack/activation.rs index a0a2a83..71f76ff 100644 --- a/crates/joko_package/src/manager/pack/activation.rs +++ b/crates/joko_package/src/manager/pack/activation.rs @@ -1,7 +1,6 @@ use indexmap::IndexMap; use uuid::Uuid; - /// This is the activation data per pack #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct ActivationData { @@ -18,4 +17,4 @@ pub enum ActivationType { /// clean these up when the timestamp is reached TimeStamp(time::OffsetDateTime), Instance(std::net::IpAddr), -} \ No newline at end of file +} diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs index a1b8353..4b61c28 100644 --- a/crates/joko_package/src/manager/pack/active.rs +++ b/crates/joko_package/src/manager/pack/active.rs @@ -7,13 +7,13 @@ use glam::{vec2, Vec2, Vec3}; use indexmap::IndexMap; use uuid::Uuid; -use joko_core::RelativePath; use crate::INCHES_PER_METER; -use jokolink::MumbleLink; +use joko_core::RelativePath; use joko_render_models::{ marker::{MarkerObject, MarkerVertex}, - trail::TrailObject + trail::TrailObject, }; +use jokolink::MumbleLink; /* - activation data with uuids and track the latest timestamp that will be activated @@ -42,7 +42,7 @@ pub(crate) struct ActiveMarker { pub common_attributes: CommonAttributes, } -pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0;// in game metric, for GW2, inches +pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0; // in game metric, for GW2, inches impl ActiveMarker { pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { @@ -71,7 +71,11 @@ impl ActiveMarker { } let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); let player_distance = pos.distance(link.player_pos); let camera_distance = pos.distance(link.cam_pos); @@ -193,7 +197,11 @@ impl ActiveTrail { } let alpha = attrs.get_alpha().copied().unwrap_or(1.0); let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; let fade_near_far = Vec2::new(fade_near, fade_far); let color = attrs.get_color().copied().unwrap_or([0u8; 4]); // default taco width @@ -287,4 +295,3 @@ pub(crate) struct CurrentMapData { pub active_trails: IndexMap, pub wip_trails: IndexMap, } - diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs index 2be0436..6febf41 100644 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -1,6 +1,10 @@ -use std::collections::{HashSet, HashMap}; -use joko_package_models::{attributes::CommonAttributes, category::Category, package::{PackageImportReport, PackCore}}; +use joko_package_models::{ + attributes::CommonAttributes, + category::Category, + package::{PackCore, PackageImportReport}, +}; use ordered_hash_map::OrderedHashMap; +use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -10,11 +14,11 @@ use crate::message::{UIToBackMessage, UIToUIMessage}; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { //#[serde(skip)] - pub uuid: Uuid,//FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information + pub uuid: Uuid, //FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information #[serde(skip)] pub parent: Option, - pub is_selected: bool,//has it been selected in configuration to be displayed - pub is_active: bool,//currently being displayed (i.e.: active) + pub is_selected: bool, //has it been selected in configuration to be displayed + pub is_active: bool, //currently being displayed (i.e.: active) pub separator: bool, pub display_name: String, pub children: OrderedHashMap, @@ -22,12 +26,11 @@ pub struct CategorySelection { pub struct SelectedCategoryManager { data: OrderedHashMap, - } impl<'a> SelectedCategoryManager { pub fn new( selected_categories: &OrderedHashMap, - categories: &OrderedHashMap + categories: &OrderedHashMap, ) -> Self { let mut list_of_enabled_categories = Default::default(); CategorySelection::get_list_of_enabled_categories( @@ -36,8 +39,10 @@ impl<'a> SelectedCategoryManager { &mut list_of_enabled_categories, &Default::default(), ); - - Self { data: list_of_enabled_categories } + + Self { + data: list_of_enabled_categories, + } } pub fn cloned_data(&self) -> OrderedHashMap { self.data.clone() @@ -51,13 +56,11 @@ impl<'a> SelectedCategoryManager { pub fn len(&self) -> usize { self.data.len() } - pub fn keys(&'a self ) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { + pub fn keys(&'a self) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { self.data.keys() } } - - impl CategorySelection { pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { let mut selectable_categories = OrderedHashMap::new(); @@ -87,7 +90,10 @@ impl CategorySelection { } } } - pub fn get(selection: &mut OrderedHashMap, uuid: Uuid) -> Option<&mut CategorySelection> { + pub fn get( + selection: &mut OrderedHashMap, + uuid: Uuid, + ) -> Option<&mut CategorySelection> { if selection.is_empty() { return None; } else { @@ -98,7 +104,7 @@ impl CategorySelection { if let Some(res) = Self::get(&mut cat.children, uuid) { return Some(res); } - } + } return None; } } @@ -129,7 +135,7 @@ impl CategorySelection { uuid: cat.guid, parent: cat.parent, is_selected: cat.default_enabled, - is_active: !cat.separator,//by default separators are not considered active since they contain nothing + is_active: !cat.separator, //by default separators are not considered active since they contain nothing separator: cat.separator, display_name: cat.display_name.clone(), children: Default::default(), @@ -137,12 +143,18 @@ impl CategorySelection { //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); selectable_categories.insert(cat.relative_category_name.clone(), to_insert); } - let s = selectable_categories.get_mut(&cat.relative_category_name).unwrap(); + let s = selectable_categories + .get_mut(&cat.relative_category_name) + .unwrap(); Self::recursive_create_selectable_categories(&mut s.children, &cat.children); } } - pub fn recursive_set(selection: &mut OrderedHashMap, uuid: Uuid, status: bool) -> bool { + pub fn recursive_set( + selection: &mut OrderedHashMap, + uuid: Uuid, + status: bool, + ) -> bool { if selection.is_empty() { return false; } else { @@ -157,11 +169,14 @@ impl CategorySelection { if Self::recursive_set(&mut cat.children, uuid, status) { return true; } - } + } return false; } } - pub fn recursive_set_all(selection: &mut OrderedHashMap, status: bool) { + pub fn recursive_set_all( + selection: &mut OrderedHashMap, + status: bool, + ) { if selection.is_empty() { return; } @@ -174,14 +189,18 @@ impl CategorySelection { } } - pub fn recursive_update_active_categories(selection: &mut OrderedHashMap, active_elements: &HashSet) -> bool { + pub fn recursive_update_active_categories( + selection: &mut OrderedHashMap, + active_elements: &HashSet, + ) -> bool { let mut is_active = false; if selection.is_empty() { //println!("recursive_update_active_categories is_empty"); return is_active; } for cat in selection.values_mut() { - cat.is_active = active_elements.contains(&cat.uuid) || Self::recursive_update_active_categories(&mut cat.children, active_elements); + cat.is_active = active_elements.contains(&cat.uuid) + || Self::recursive_update_active_categories(&mut cat.children, active_elements); if cat.is_active { is_active = true; } @@ -191,19 +210,23 @@ impl CategorySelection { fn context_menu( u2b_sender: &std::sync::mpsc::Sender, - cs: &mut CategorySelection, - ui: &mut egui::Ui + cs: &mut CategorySelection, + ui: &mut egui::Ui, ) { if ui.button("Activate branch").clicked() { cs.is_selected = true; CategorySelection::recursive_set_all(&mut cs.children, true); - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, true)); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange( + cs.uuid, true, + )); ui.close_menu(); } if ui.button("Deactivate branch").clicked() { CategorySelection::recursive_set_all(&mut cs.children, false); cs.is_selected = false; - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange(cs.uuid, false)); + let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange( + cs.uuid, false, + )); ui.close_menu(); } } @@ -231,7 +254,12 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationElementStatusChange(cat.uuid, cat.is_selected)); + let _ = u2b_sender.send( + UIToBackMessage::CategoryActivationElementStatusChange( + cat.uuid, + cat.is_selected, + ), + ); *is_dirty = true; } } @@ -251,17 +279,18 @@ impl CategorySelection { Self::recursive_selection_ui( u2b_sender, u2u_sender, - &mut cat.children, - ui, - is_dirty, - show_only_active, - import_quality_report + &mut cat.children, + ui, + is_dirty, + show_only_active, + import_quality_report, ); - }).response.context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); + }) + .response + .context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); } }); } }); } } - diff --git a/crates/joko_package/src/manager/pack/dirty.rs b/crates/joko_package/src/manager/pack/dirty.rs index 3dd900c..f7fd509 100644 --- a/crates/joko_package/src/manager/pack/dirty.rs +++ b/crates/joko_package/src/manager/pack/dirty.rs @@ -1,4 +1,3 @@ - use ordered_hash_map::OrderedHashSet; use joko_core::RelativePath; @@ -26,4 +25,4 @@ impl DirtyMarker { || !self.texture.is_empty() || !self.tbin.is_empty() } -} \ No newline at end of file +} diff --git a/crates/joko_package/src/manager/pack/file_selection.rs b/crates/joko_package/src/manager/pack/file_selection.rs index 8210caa..52d6a7c 100644 --- a/crates/joko_package/src/manager/pack/file_selection.rs +++ b/crates/joko_package/src/manager/pack/file_selection.rs @@ -4,11 +4,10 @@ use uuid::Uuid; pub struct SelectedFileManager { data: BTreeMap, - } impl<'a> SelectedFileManager { pub fn new( - selected_files: &BTreeMap, + selected_files: &BTreeMap, pack_source_files: &BTreeMap, currently_used_files: &BTreeMap, ) -> Self { @@ -19,14 +18,16 @@ impl<'a> SelectedFileManager { ¤tly_used_files, &mut list_of_enabled_files, ); - Self { data: list_of_enabled_files } + Self { + data: list_of_enabled_files, + } } fn recursive_get_full_names( - _selected_files: &BTreeMap, + _selected_files: &BTreeMap, _pack_source_files: &BTreeMap, currently_used_files: &BTreeMap, - list_of_enabled_files: &mut BTreeMap - ){ + list_of_enabled_files: &mut BTreeMap, + ) { for (key, v) in currently_used_files.iter() { list_of_enabled_files.insert(key.clone(), *v); } diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs index d43b28a..65135f8 100644 --- a/crates/joko_package/src/manager/pack/import.rs +++ b/crates/joko_package/src/manager/pack/import.rs @@ -3,7 +3,6 @@ use tracing::info; use miette::Result; - #[derive(Debug, Default)] pub enum ImportStatus { #[default] @@ -15,9 +14,12 @@ pub enum ImportStatus { PackError(miette::Report), } -pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf, working_path: &std::path::PathBuf) -> Result<(String, PackCore)> { +pub fn import_pack_from_zip_file_path( + file_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result<(String, PackCore)> { info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(file_path.clone(), working_path).map(|pack| { + crate::io::get_pack_from_taco_zip(file_path.clone(), extract_temporary_path).map(|pack| { ( file_path .file_name() @@ -26,4 +28,4 @@ pub fn import_pack_from_zip_file_path(file_path: std::path::PathBuf, working_pat pack, ) }) -} \ No newline at end of file +} diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 07c233e..d70352b 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -1,8 +1,15 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, sync::Arc + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, }; -use joko_package_models::{attributes::{Behavior, CommonAttributes}, category::Category, map::MapData, package::{PackageImportReport, PackCore}, trail::TBin}; +use joko_package_models::{ + attributes::{Behavior, CommonAttributes}, + category::Category, + map::MapData, + package::{PackCore, PackageImportReport}, + trail::TBin, +}; use ordered_hash_map::OrderedHashMap; use cap_std::fs_utf8::Dir; @@ -11,34 +18,45 @@ use image::EncodableLayout; use tracing::{debug, error, info, info_span, trace}; use uuid::Uuid; +use crate::message::BackToUIMessage; use crate::{ - io::{load_pack_core_from_dir, save_pack_data_to_dir, save_pack_texture_to_dir,}, manager::pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, message::{UIToBackMessage, UIToUIMessage} + io::{load_pack_core_from_normalized_folder, save_pack_data_to_dir, save_pack_texture_to_dir}, + manager::{ + pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, + package::EXTRACT_DIRECTORY_NAME, + }, + message::{UIToBackMessage, UIToUIMessage}, }; -use jokolink::MumbleLink; use joko_core::{ task::{AsyncTask, AsyncTaskGuard}, - RelativePath + RelativePath, }; use joko_render_models::trail::TrailObject; -use crate::message::BackToUIMessage; +use jokolink::MumbleLink; use miette::{bail, Context, IntoDiagnostic, Result}; use super::activation::{ActivationData, ActivationType}; -use super::active::{CurrentMapData, ActiveMarker, ActiveTrail}; +use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; use crate::manager::pack::category_selection::CategorySelection; -use crate::manager::package::{PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME, EDITABLE_PACKAGE_NAME, WORKING_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME}; - +use crate::manager::package::{ + EDITABLE_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME, PACKAGES_DIRECTORY_NAME, + PACKAGE_MANAGER_DIRECTORY_NAME, +}; -type ImportAllTriplet = (BTreeMap, BTreeMap, BTreeMap); +type ImportAllTriplet = ( + BTreeMap, + BTreeMap, + BTreeMap, +); type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); //TODO: separate in front and back tasks -pub (crate) struct PackTasks { +pub(crate) struct PackTasks { //an object that can handle such tasks should be passed as argument of any function that may required an async action save_texture_task: AsyncTask>, save_data_task: AsyncTask>, save_report_task: AsyncTask<(Arc, PackageImportReport), Result<()>>, - load_all_packs_task: AsyncTask, Result> + load_all_packs_task: AsyncTask, Result>, } //TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? @@ -51,16 +69,16 @@ pub struct LoadedPackData { //pub core: PackCore, pub categories: OrderedHashMap, pub all_categories: HashMap, - pub source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, selected_files: BTreeMap, - _is_dirty: bool,//there was an edition in the package itself + _is_dirty: bool, //there was an edition in the package itself // loca copy in the data side of what is exposed in UI selectable_categories: OrderedHashMap, pub entities_parents: HashMap, activation_data: ActivationData, - active_elements: HashSet,//keep track of which elements are active + active_elements: HashSet, //keep track of which elements are active } #[derive(Clone)] @@ -80,7 +98,7 @@ pub struct LoadedPackTexture { selectable_categories: OrderedHashMap, current_map_data: CurrentMapData, activation_data: ActivationData, - active_elements: HashSet,//which are the active elements (loaded) + active_elements: HashSet, //which are the active elements (loaded) _is_dirty: bool, } @@ -94,66 +112,66 @@ impl PackTasks { } } pub fn is_running(&self) -> bool { - self.save_texture_task.lock().unwrap().is_running() || - self.save_data_task.lock().unwrap().is_running() + self.save_texture_task.lock().unwrap().is_running() + || self.save_data_task.lock().unwrap().is_running() } pub fn count(&self) -> i32 { - 0 - + self.save_texture_task.lock().unwrap().count() - + self.save_data_task.lock().unwrap().count() - + self.load_all_packs_task.lock().unwrap().count() + 0 + self.save_texture_task.lock().unwrap().count() + + self.save_data_task.lock().unwrap().count() + + self.load_all_packs_task.lock().unwrap().count() } - + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { if status { std::mem::take(&mut texture_pack._is_dirty); - let _ = self.save_texture_task.lock().unwrap().send( - texture_pack.clone() - ); + let _ = self + .save_texture_task + .lock() + .unwrap() + .send(texture_pack.clone()); } } pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { if status { std::mem::take(&mut data_pack._is_dirty); - let _ = self.save_data_task.lock().unwrap().send( - data_pack.clone() - ); + let _ = self.save_data_task.lock().unwrap().send(data_pack.clone()); } } pub fn save_report(&self, target_dir: Arc, report: PackageImportReport, status: bool) { if status { - let _ = self.save_report_task.lock().unwrap().send( - (target_dir, report) - ); + let _ = self + .save_report_task + .lock() + .unwrap() + .send((target_dir, report)); } } pub fn load_all_packs(&self, jokolay_dir: Arc) { - let _ = self.load_all_packs_task.lock().unwrap().send( - jokolay_dir - ); + let _ = self.load_all_packs_task.lock().unwrap().send(jokolay_dir); } pub fn wait_for_load_all_packs(&self) -> Result { self.load_all_packs_task.lock().unwrap().recv().unwrap() } fn change_map( - &self, + &self, pack: &mut LoadedPackData, b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, - currently_used_files: &BTreeMap + currently_used_files: &BTreeMap, ) { //TODO //self.load_map_task.lock().unwrap().send(pack); } - fn async_save_texture( - pack_texture: LoadedPackTexture - ) -> Result<()> { + fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<()> { trace!("Save texture package {:?}", pack_texture.dir); match serde_json::to_string_pretty(&pack_texture.selectable_categories) { - Ok(cs_json) => match pack_texture.dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(cs_json) => match pack_texture + .dir + .write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) + { Ok(_) => { debug!("wrote cat selections to disk after creating a default from pack"); } @@ -166,7 +184,10 @@ impl PackTasks { } } match serde_json::to_string_pretty(&pack_texture.activation_data) { - Ok(ad_json) => match pack_texture.dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(ad_json) => match pack_texture + .dir + .write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) + { Ok(_) => { debug!("wrote activation to disk after creating a default from pack"); } @@ -178,7 +199,8 @@ impl PackTasks { error!(?e, "failed to serialize activation"); } } - let writing_directory = pack_texture.dir + let writing_directory = pack_texture + .dir .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to open core pack directory")?; @@ -186,54 +208,52 @@ impl PackTasks { Ok(()) } - fn async_save_data( - pack_data: LoadedPackData - ) -> Result<()> { + fn async_save_data(pack_data: LoadedPackData) -> Result<()> { trace!("Save data package {:?}", pack_data.dir); - pack_data.dir + pack_data + .dir .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to create xmlpack directory")?; - let writing_directory = pack_data.dir + let writing_directory = pack_data + .dir .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) .into_diagnostic() .wrap_err("failed to open core pack directory")?; - save_pack_data_to_dir( - &pack_data, - &writing_directory, - )?; + save_pack_data_to_dir(&pack_data, &writing_directory)?; Ok(()) } - fn async_save_report( - input: (Arc, PackageImportReport) - ) -> Result<()> { - let (writing_directory, report,) = input; + fn async_save_report(input: (Arc, PackageImportReport)) -> Result<()> { + let (writing_directory, report) = input; trace!("Save report package {:?}", writing_directory); match serde_json::to_string_pretty(&report) { - Ok(cs_json) => match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote import quality report to disk"); - } - Err(e) => { - debug!(?e, "failed to write import quality report to disk"); + Ok(cs_json) => { + match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote import quality report to disk"); + } + Err(e) => { + debug!(?e, "failed to write import quality report to disk"); + } } - }, + } Err(e) => { error!(?e, "failed to serialize import quality report"); } } Ok(()) } - } - impl LoadedPackData { const CORE_PACK_DIR_NAME: &'static str = "core"; const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; - fn load_selectable_categories(pack_dir: &Arc, pack: &PackCore) -> OrderedHashMap { + fn load_selectable_categories( + pack_dir: &Arc, + pack: &PackCore, + ) -> OrderedHashMap { //FIXME: we need to patch those categories from the one in the files (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { @@ -307,14 +327,19 @@ impl LoadedPackData { .wrap_err("failed to open core pack directory")?; let start = std::time::SystemTime::now(); let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_dir(&core_dir, import_report).wrap_err("failed to load pack from dir")?; + let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + .wrap_err("failed to load pack from dir")?; let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); - + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. //let selectable_categories = CategorySelection::default_from_pack_core(&core); let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); - + Ok(LoadedPackData { name, uuid: core.uuid, @@ -375,30 +400,41 @@ impl LoadedPackData { tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - let _ = b2u_sender.send(BackToUIMessage::PackageActiveElements(self.uuid, active_elements.clone())); + let _ = b2u_sender.send(BackToUIMessage::PackageActiveElements( + self.uuid, + active_elements.clone(), + )); self.active_elements = active_elements.clone(); next_loaded.extend(active_elements); } } - + fn on_map_changed( &mut self, b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, active_elements: &mut HashSet, - ){ + ) { info!(link.map_id, "current map data is updated. {}", self.name); if link.map_id == 0 { info!("No map do not do anything"); return; } - debug!("Start building SelectedCategoryManager {}", self.selectable_categories.len()); - let selected_categories_manager = SelectedCategoryManager::new(&self.selectable_categories, &self.categories); + debug!( + "Start building SelectedCategoryManager {}", + self.selectable_categories.len() + ); + let selected_categories_manager = + SelectedCategoryManager::new(&self.selectable_categories, &self.categories); debug!("Start building SelectedFileManager"); - let selected_files_manager = SelectedFileManager::new(&self.selected_files, &self.source_files, ¤tly_used_files); - + let selected_files_manager = SelectedFileManager::new( + &self.selected_files, + &self.source_files, + ¤tly_used_files, + ); + debug!("Start loading markers"); let mut nb_markers_attempt = 0; let mut nb_markers_loaded = 0; @@ -416,7 +452,7 @@ impl LoadedPackData { active_elements.insert(marker.parent); if selected_categories_manager.is_selected(&marker.parent) { let category_attributes = selected_categories_manager.get(&marker.parent); - let mut common_attributes = marker.attrs.clone();// why a clone ? + let mut common_attributes = marker.attrs.clone(); // why a clone ? common_attributes.inherit_if_attr_none(category_attributes); let key = &marker.guid; if let Some(behavior) = common_attributes.get_behavior() { @@ -427,7 +463,9 @@ impl LoadedPackData { | Behavior::OnlyVisibleBeforeActivation | Behavior::ReappearAfterTimer | Behavior::ReappearOnMapReset - | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), + | Behavior::WeeklyReset => { + self.activation_data.global.contains_key(key) + } Behavior::OncePerInstance => self .activation_data .global @@ -437,8 +475,8 @@ impl LoadedPackData { _ => false, }) .unwrap_or_default(), - Behavior::DailyPerChar => - self.activation_data + Behavior::DailyPerChar => self + .activation_data .character .get(&link.name) .map(|a| a.contains_key(key)) @@ -450,7 +488,9 @@ impl LoadedPackData { .map(|a| { a.get(key) .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, + ActivationType::Instance(a) => { + a == &link.server_address + } _ => false, }) .unwrap_or_default() @@ -464,14 +504,23 @@ impl LoadedPackData { } } if let Some(tex_path) = common_attributes.get_icon_file() { - let _ = b2u_sender.send(BackToUIMessage::MarkerTexture(self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes)); + let _ = b2u_sender.send(BackToUIMessage::MarkerTexture( + self.uuid, + tex_path.clone(), + marker.guid, + marker.position, + common_attributes, + )); } else { debug!("no texture attribute on this marker"); } - + nb_markers_loaded += 1; } else { - debug!("category {} = {} is not enabled", marker.category, marker.parent); + debug!( + "category {} = {} is not enabled", + marker.category, marker.parent + ); } } } @@ -492,44 +541,64 @@ impl LoadedPackData { active_elements.insert(trail.guid); active_elements.insert(trail.parent); if selected_categories_manager.is_selected(&trail.parent) { - let category_attributes = selected_categories_manager.get(&trail.parent); + let category_attributes = selected_categories_manager.get(&trail.parent); let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - let _ = b2u_sender.send(BackToUIMessage::TrailTexture(self.uuid, tex_path.clone(), trail.guid, common_attributes)); + let _ = b2u_sender.send(BackToUIMessage::TrailTexture( + self.uuid, + tex_path.clone(), + trail.guid, + common_attributes, + )); } else { debug!("no texture attribute on this trail"); } nb_trails_loaded += 1; } else { - debug!("category {} = {} is not enabled", trail.category, trail.parent); + debug!( + "category {} = {} is not enabled", + trail.category, trail.parent + ); } } } - info!("Load notifications for {} on map {}: {}/{} markers and {}/{} trails", self.name, link.map_id, nb_markers_loaded, nb_markers_attempt, nb_trails_loaded, nb_trails_attempt); - debug!("active categories: {:?}", selected_categories_manager.keys()); + info!( + "Load notifications for {} on map {}: {}/{} markers and {}/{} trails", + self.name, + link.map_id, + nb_markers_loaded, + nb_markers_attempt, + nb_trails_loaded, + nb_trails_attempt + ); + debug!( + "active categories: {:?}", + selected_categories_manager.keys() + ); } } - - impl LoadedPackTexture { const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; - + pub fn category_set_all(&mut self, status: bool) { CategorySelection::recursive_set_all(&mut self.selectable_categories, status); self._is_dirty = true; } - + pub fn update_active_categories(&mut self, active_elements: &HashSet) { - CategorySelection::recursive_update_active_categories(&mut self.selectable_categories, active_elements); + CategorySelection::recursive_update_active_categories( + &mut self.selectable_categories, + active_elements, + ); } pub fn category_sub_menu( - &mut self, + &mut self, u2b_sender: &std::sync::mpsc::Sender, u2u_sender: &std::sync::mpsc::Sender, - ui: &mut egui::Ui, - show_only_active: bool, + ui: &mut egui::Ui, + show_only_active: bool, import_quality_report: &PackageImportReport, ) { //it is important to generate a new id each time to avoid collision @@ -541,7 +610,7 @@ impl LoadedPackTexture { ui, &mut self._is_dirty, show_only_active, - &import_quality_report + &import_quality_report, ); }); if self._is_dirty { @@ -561,11 +630,12 @@ impl LoadedPackTexture { z_near: f32, tasks: &PackTasks, ) { - tracing::trace!("LoadedPackTexture.tick: {} {}-{} {}-{}", + tracing::trace!( + "LoadedPackTexture.tick: {} {}-{} {}-{}", self.name, - self.current_map_data.active_markers.len(), - self.current_map_data.wip_markers.len(), - self.current_map_data.active_trails.len(), + self.current_map_data.active_markers.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.active_trails.len(), self.current_map_data.wip_trails.len(), ); let mut marker_objects = Vec::new(); @@ -574,7 +644,11 @@ impl LoadedPackTexture { marker_objects.push(mo); } } - tracing::trace!("LoadedPackTexture.tick: {}, markers {}", self.name, marker_objects.len()); + tracing::trace!( + "LoadedPackTexture.tick: {}, markers {}", + self.name, + marker_objects.len() + ); let _ = u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); let mut trail_objects = Vec::new(); for trail in self.current_map_data.active_trails.values() { @@ -584,24 +658,30 @@ impl LoadedPackTexture { }); //next_on_screen.insert(*uuid); } - tracing::trace!("LoadedPackTexture.tick: {}, trails {}", self.name, trail_objects.len()); + tracing::trace!( + "LoadedPackTexture.tick: {}, trails {}", + self.name, + trail_objects.len() + ); let _ = u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); } pub fn swap(&mut self) { - info!("swap {} to display {} textures, {} markers, {} trails", - self.name, + info!( + "swap {} to display {} textures, {} markers, {} trails", + self.name, self.current_map_data.active_textures.len(), - self.current_map_data.wip_markers.len(), + self.current_map_data.wip_markers.len(), self.current_map_data.wip_trails.len() ); - self.current_map_data.active_markers = std::mem::take(&mut self.current_map_data.wip_markers); + self.current_map_data.active_markers = + std::mem::take(&mut self.current_map_data.wip_markers); self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); } pub fn load_marker_texture( - &mut self, - egui_context: &egui::Context, + &mut self, + egui_context: &egui::Context, default_tex_id: &TextureHandle, tex_path: &RelativePath, marker_uuid: Uuid, @@ -611,7 +691,7 @@ impl LoadedPackTexture { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.textures.get(tex_path) { let img = image::load_from_memory(tex).unwrap(); - + self.current_map_data.active_textures.insert( tex_path.clone(), egui_context.load_texture( @@ -627,7 +707,10 @@ impl LoadedPackTexture { error!(%tex_path, "failed to find this icon texture"); } } - let th = self.current_map_data.active_textures.get(tex_path) + let th = self + .current_map_data + .active_textures + .get(tex_path) .unwrap_or(default_tex_id); let texture_id = match th.id() { egui::TextureId::Managed(i) => i, @@ -644,14 +727,12 @@ impl LoadedPackTexture { max_pixel_size, min_pixel_size, }; - self.current_map_data - .wip_markers - .insert(marker_uuid, am); + self.current_map_data.wip_markers.insert(marker_uuid, am); } pub fn load_trail_texture( - &mut self, - egui_context: &egui::Context, + &mut self, + egui_context: &egui::Context, default_tex_id: &TextureHandle, tex_path: &RelativePath, trail_uuid: Uuid, @@ -695,66 +776,82 @@ impl LoadedPackTexture { info!(%tbin_path, "failed to find tbin"); return; }; - if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( - &common_attributes, - &tbin.nodes, - th.clone(), - ) { + if let Some(active_trail) = + ActiveTrail::get_vertices_and_texture(&common_attributes, &tbin.nodes, th.clone()) + { self.current_map_data .wip_trails .insert(trail_uuid, active_trail); } else { info!("Cannot display {texture_path:?}") } - } - } -pub fn jokolay_to_working_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { +pub fn jokolay_to_editable_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { let marker_manager_path = jokolay_to_marker_path(jokolay_path); - marker_manager_path.join(WORKING_PACKAGE_NAME) + marker_manager_path.join(EDITABLE_PACKAGE_NAME) } -pub fn jokolay_to_marker_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf{ - jokolay_path.join(PACKAGE_MANAGER_DIRECTORY_NAME).join(PACKAGES_DIRECTORY_NAME) +pub fn jokolay_to_extract_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { + jokolay_path.join(EXTRACT_DIRECTORY_NAME) +} + +pub fn jokolay_to_marker_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(PACKAGES_DIRECTORY_NAME) } pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { - jokolay_dir.create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) + jokolay_dir + .create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() - .wrap_err(format!("failed to create marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + .wrap_err(format!( + "failed to create marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; let marker_manager_dir = jokolay_dir .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) .into_diagnostic() - .wrap_err(format!("failed to open marker manager directory {}", PACKAGE_MANAGER_DIRECTORY_NAME))?; + .wrap_err(format!( + "failed to open marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; marker_manager_dir .create_dir_all(PACKAGES_DIRECTORY_NAME) .into_diagnostic() - .wrap_err(format!("failed to create marker packs directory {}", PACKAGES_DIRECTORY_NAME))?; + .wrap_err(format!( + "failed to create marker packs directory {}", + PACKAGES_DIRECTORY_NAME + ))?; let marker_packs_dir = marker_manager_dir .open_dir(PACKAGES_DIRECTORY_NAME) .into_diagnostic() - .wrap_err(format!("failed to open marker packs dir {}", PACKAGES_DIRECTORY_NAME))?; + .wrap_err(format!( + "failed to open marker packs dir {}", + PACKAGES_DIRECTORY_NAME + ))?; - marker_packs_dir.create_dir_all(EDITABLE_PACKAGE_NAME) - .into_diagnostic() - .wrap_err("failed to create editable package directory")?; - let editable_package = marker_packs_dir.open_dir(EDITABLE_PACKAGE_NAME) + marker_packs_dir + .create_dir_all(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + let editable_package = marker_packs_dir + .open_dir(EDITABLE_PACKAGE_NAME) .into_diagnostic() .wrap_err("failed to create editable package directory")?; - editable_package.create_dir_all("data") + editable_package + .create_dir_all("data") .into_diagnostic() .wrap_err("failed to create data folder for editable package")?; Ok(marker_packs_dir) } -pub fn load_all_from_dir(jokolay_dir: Arc) - -> Result - { +pub fn load_all_from_dir(jokolay_dir: Arc) -> Result { let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); @@ -777,7 +874,7 @@ pub fn load_all_from_dir(jokolay_dir: Arc) { if name == EDITABLE_PACKAGE_NAME { //TODO: have a version of loading that does not involve already ingested packages - if let Ok(pack_core) = load_pack_core_from_dir(&pack_dir, None) { + if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { let lp = build_from_core(name.clone(), pack_dir.into(), pack_core); let (data, tex, report) = lp; data_packs.insert(data.uuid, data); @@ -822,14 +919,18 @@ fn build_from_dir(name: String, pack_dir: Arc) -> Result { .wrap_err("failed to open core pack directory")?; let start = std::time::SystemTime::now(); let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_dir(&core_dir, import_report).wrap_err("failed to load pack from dir")?; + let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + .wrap_err("failed to load pack from dir")?; let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of package from disk {} took {} ms", name, elaspsed.as_millis()); + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); let res = build_from_core(name.clone(), pack_dir, core); Ok(res) } - pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> ImportTriplet { let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); let data = LoadedPackData { @@ -849,23 +950,23 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo }; let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, + Ok(contents) => match serde_json::from_str(&contents) { + Ok(cd) => Some(cd), Err(e) => { - error!(?e, "failed to read string of category data"); + error!(?e, "failed to deserialize activation data"); None } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None } - } else { - None - }) - .flatten() - .unwrap_or_default(); + } + } else { + None + }) + .flatten() + .unwrap_or_default(); let tex = LoadedPackTexture { uuid: core.uuid, selectable_categories, @@ -877,9 +978,8 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo name: name, tbins: core.tbins, active_elements: Default::default(), - source_files: core.active_source_files + source_files: core.active_source_files, }; let report = core.report; (data, tex, report) } - diff --git a/crates/joko_package/src/manager/pack/mod.rs b/crates/joko_package/src/manager/pack/mod.rs index 2833ae2..cd44dd6 100644 --- a/crates/joko_package/src/manager/pack/mod.rs +++ b/crates/joko_package/src/manager/pack/mod.rs @@ -1,8 +1,7 @@ -pub mod category_selection; -pub mod file_selection; pub mod activation; pub mod active; -pub mod loaded; +pub mod category_selection; pub mod dirty; +pub mod file_selection; pub mod import; - +pub mod loaded; diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index b44e389..b559ca5 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -1,33 +1,34 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, sync::{Arc, Mutex} + collections::{BTreeMap, HashMap, HashSet}, + sync::{Arc, Mutex}, }; -use glam::Vec3; -use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; +use glam::Vec3; use image::EncodableLayout; +use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use tracing::{info_span, trace}; +use crate::message::{UIToBackMessage, UIToUIMessage}; use joko_core::RelativePath; use jokolink::MumbleLink; use miette::Result; use uuid::Uuid; -use crate::message::{UIToBackMessage, UIToUIMessage}; -use crate::message::BackToUIMessage; -use crate::manager::pack::loaded::{LoadedPackData, PackTasks, LoadedPackTexture}; use crate::manager::pack::import::ImportStatus; +use crate::manager::pack::loaded::{LoadedPackData, LoadedPackTexture, PackTasks}; +use crate::message::BackToUIMessage; use super::pack::loaded::jokolay_to_marker_dir; -pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager";//name kept for compatibility purpose -pub const PACKAGES_DIRECTORY_NAME: &str = "packs";//name kept for compatibility purpose -pub const EDITABLE_PACKAGE_NAME: &str = "_work";//package automatically created and always imported as an overwrite -pub const WORKING_PACKAGE_NAME: &str = "editable";//working dir where a package is extracted before reading -pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded";//result of import of the editable package -// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose +pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading +pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite +pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package + // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; /// It manage everything that has to do with marker packs. /// 1. imports, loads, saves and exports marker packs. @@ -68,7 +69,7 @@ pub struct PackageUIManager { tasks: PackTasks, currently_used_files: BTreeMap, - all_files_activation_status: bool,// this consume a change of display event + all_files_activation_status: bool, // this consume a change of display event show_only_active: bool, pack_details: Option, // if filled, display the details of the package } @@ -129,7 +130,8 @@ impl PackageDataManager { self.parents.get(element) } pub fn get_parents<'a, I>(&self, input: I) -> HashSet - where I: Iterator + where + I: Iterator, { let iter = input.into_iter(); let mut result: HashSet = HashSet::new(); @@ -159,8 +161,14 @@ impl PackageDataManager { unreachable!("The loop should always return"); } - pub fn get_active_elements_parents(&mut self, categories_and_elements_to_be_loaded: HashSet) { - trace!("There are {} active elements", categories_and_elements_to_be_loaded.len()); + pub fn get_active_elements_parents( + &mut self, + categories_and_elements_to_be_loaded: HashSet, + ) { + trace!( + "There are {} active elements", + categories_and_elements_to_be_loaded.len() + ); //first merge the parents to iterate overit let mut parents: HashMap = Default::default(); @@ -181,7 +189,7 @@ impl PackageDataManager { ) { let mut currently_used_files: BTreeMap = Default::default(); let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - + match link { Some(link) => { //TODO: how to save/load the active files ? @@ -191,18 +199,32 @@ impl PackageDataManager { for pack in self.packs.values_mut() { if let Some(current_map) = pack.maps.get(&link.map_id) { for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) + { currently_used_files.insert( - marker.source_file_uuid.clone(), - *self.currently_used_files.get(&marker.source_file_uuid).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + marker.source_file_uuid.clone(), + *self + .currently_used_files + .get(&marker.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), ); } } for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) + { currently_used_files.insert( - trail.source_file_uuid.clone(), - *self.currently_used_files.get(&trail.source_file_uuid).unwrap_or_else(|| {have_used_files_list_changed = true; is_active}) + trail.source_file_uuid.clone(), + *self + .currently_used_files + .get(&trail.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), ); } } @@ -220,23 +242,27 @@ impl PackageDataManager { ¤tly_used_files, have_used_files_list_changed || choice_of_category_changed, map_changed, - &tasks, + &tasks, &mut categories_and_elements_to_be_loaded, ); std::mem::drop(span_guard); } if map_changed { self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let _ = b2u_sender.send(BackToUIMessage::ActiveElements(self.loaded_elements.clone())); + let _ = b2u_sender.send(BackToUIMessage::ActiveElements( + self.loaded_elements.clone(), + )); } if map_changed || have_used_files_list_changed || choice_of_category_changed { //there is no point in sending a new list if nothing changed - let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles(currently_used_files.clone())); + let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles( + currently_used_files.clone(), + )); self.currently_used_files = currently_used_files; let _ = b2u_sender.send(BackToUIMessage::TextureSwapChain); } - }, - None => {}, + } + None => {} }; } @@ -253,11 +279,17 @@ impl PackageDataManager { } } self.delete_packs(to_delete); - self.tasks.save_report(Arc::clone(&data_pack.dir), report, true); + self.tasks + .save_report(Arc::clone(&data_pack.dir), report, true); self.tasks.save_data(&mut data_pack, true); let mut uuid_to_insert = data_pack.uuid.clone(); - while self.packs.contains_key(&uuid_to_insert) {//collision avoidance - trace!("Uuid collision detected for {} for package {}", uuid_to_insert, data_pack.name); + while self.packs.contains_key(&uuid_to_insert) { + //collision avoidance + trace!( + "Uuid collision detected for {} for package {}", + uuid_to_insert, + data_pack.name + ); uuid_to_insert = Uuid::new_v4(); } data_pack.uuid = uuid_to_insert; @@ -274,22 +306,23 @@ impl PackageDataManager { // Called only once at application start. let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); self.tasks.load_all_packs(jokolay_dir); - if let Ok((data_packages, texture_packages, report_packages)) = self.tasks.wait_for_load_all_packs() { + if let Ok((data_packages, texture_packages, report_packages)) = + self.tasks.wait_for_load_all_packs() + { for (uuid, data_pack) in data_packages { self.packs.insert(uuid, data_pack); } - for ((_, texture_pack), (_, report)) in std::iter::zip(texture_packages, report_packages) { + for ((_, texture_pack), (_, report)) in + std::iter::zip(texture_packages, report_packages) + { let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); } let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } let _ = b2u_sender.send(BackToUIMessage::FirstLoadDone); - } - } - impl PackageUIManager { pub fn new(packs: BTreeMap) -> Self { Self { @@ -301,15 +334,12 @@ impl PackageUIManager { all_files_activation_status: false, show_only_active: true, - currently_used_files: Default::default(),// UI copy to (de-)activate files + currently_used_files: Default::default(), // UI copy to (de-)activate files pack_details: None, } } - pub fn late_init( - &mut self, - etx: &egui::Context, - ) { + pub fn late_init(&mut self, etx: &egui::Context) { if self.default_marker_texture.is_none() { let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); let size = [img.width() as _, img.height() as _]; @@ -324,7 +354,8 @@ impl PackageUIManager { )); } if self.default_trail_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); + let img = + image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); let size = [img.width() as _, img.height() as _]; self.default_trail_texture = Some(etx.load_texture( "default trail", @@ -355,7 +386,11 @@ impl PackageUIManager { } } - pub fn update_pack_active_categories(&mut self, pack_uuid: Uuid, active_elements: &HashSet) { + pub fn update_pack_active_categories( + &mut self, + pack_uuid: Uuid, + active_elements: &HashSet, + ) { trace!("There are {} active elements", active_elements.len()); for (uuid, pack) in self.packs.iter_mut() { if uuid == &pack_uuid { @@ -371,53 +406,47 @@ impl PackageUIManager { } pub fn load_marker_texture( - &mut self, - egui_context: &egui::Context, - pack_uuid: Uuid, - tex_path: RelativePath, - marker_uuid: Uuid, + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + marker_uuid: Uuid, position: Vec3, common_attributes: CommonAttributes, ) { - self.packs - .get_mut(&pack_uuid) - .map( |pack| { - pack.load_marker_texture( - egui_context, - self.default_marker_texture.as_ref().unwrap(), - &tex_path, - marker_uuid, - position, - common_attributes, - ); - }); + self.packs.get_mut(&pack_uuid).map(|pack| { + pack.load_marker_texture( + egui_context, + self.default_marker_texture.as_ref().unwrap(), + &tex_path, + marker_uuid, + position, + common_attributes, + ); + }); } pub fn load_trail_texture( - &mut self, - egui_context: &egui::Context, - pack_uuid: Uuid, - tex_path: RelativePath, - trail_uuid: Uuid, + &mut self, + egui_context: &egui::Context, + pack_uuid: Uuid, + tex_path: RelativePath, + trail_uuid: Uuid, common_attributes: CommonAttributes, ) { - self.packs - .get_mut(&pack_uuid) - .map( |pack| { - pack.load_trail_texture( - egui_context, - &self.default_trail_texture.as_ref().unwrap(), - &tex_path, - trail_uuid, - common_attributes, - ); - }); + self.packs.get_mut(&pack_uuid).map(|pack| { + pack.load_trail_texture( + egui_context, + &self.default_trail_texture.as_ref().unwrap(), + &tex_path, + trail_uuid, + common_attributes, + ); + }); } - fn pack_importer( - import_status: Arc>, - ) { + fn pack_importer(import_status: Arc>) { //called when a new pack is imported - rayon::spawn( move || { + rayon::spawn(move || { *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; if let Some(file_path) = rfd::FileDialog::new() @@ -449,13 +478,7 @@ impl PackageUIManager { for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); tasks.save_texture(pack, pack.is_dirty()); - pack.tick( - &u2u_sender, - timestamp, - link, - z_near, - &tasks - ); + pack.tick(&u2u_sender, timestamp, link, z_near, &tasks); std::mem::drop(span_guard); } let _ = u2u_sender.send(UIToUIMessage::RenderSwapChain); @@ -463,7 +486,7 @@ impl PackageUIManager { } pub fn menu_ui( - &mut self, + &mut self, u2b_sender: &std::sync::mpsc::Sender, u2u_sender: &std::sync::mpsc::Sender, ui: &mut egui::Ui, @@ -489,19 +512,34 @@ impl PackageUIManager { let _ = u2b_sender.send(UIToBackMessage::CategorySetAll(false)); } - for (pack, import_quality_report) in std::iter::zip(self.packs.values_mut(), self.reports.values()) { + for (pack, import_quality_report) in + std::iter::zip(self.packs.values_mut(), self.reports.values()) + { //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - pack.category_sub_menu(u2b_sender, u2u_sender, ui, self.show_only_active, &import_quality_report); + pack.category_sub_menu( + u2b_sender, + u2u_sender, + ui, + self.show_only_active, + &import_quality_report, + ); } - }); - if self.tasks.is_running() || nb_running_tasks_on_back > 0 || nb_running_tasks_on_network > 0{ - let sp = egui::Spinner::new().color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); + if self.tasks.is_running() + || nb_running_tasks_on_back > 0 + || nb_running_tasks_on_network > 0 + { + let sp = egui::Spinner::new() + .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); ui.add(sp); } } - pub fn status_as_color(&self, nb_running_tasks_on_back: i32, nb_running_tasks_on_network: i32) -> egui::Color32 { + pub fn status_as_color( + &self, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, + ) -> egui::Color32 { //we can choose whatever color code we want to focus on load, save, network queries, anything. let nb_running_tasks_on_ui = self.tasks.count(); //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 @@ -512,7 +550,7 @@ impl PackageUIManager { } else { 0 }; - + let color_back = if nb_running_tasks_on_back > 0 { let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; let res = nb_bask_tasks * 0x80; @@ -520,7 +558,7 @@ impl PackageUIManager { } else { 0 }; - + let color_network = if nb_running_tasks_on_network > 0 { let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; let res = nb_network_tasks * 0x80; @@ -533,88 +571,102 @@ impl PackageUIManager { } fn gui_file_manager( - &mut self, + &mut self, event_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - open: &mut bool, + etx: &egui::Context, + open: &mut bool, ) { let mut files_changed = false; - Window::new("File Manager").open(open).show(etx, |ui| -> Result<()> { - egui::ScrollArea::vertical().show(ui, |ui| { - egui::Grid::new("link grid") - .num_columns(4) - .striped(true) - .show(ui, |ui| { - let mut all_files_toggle = false; - ui.horizontal(|ui|{ - if ui.button("activate all").clicked() { - self.all_files_activation_status = true; - all_files_toggle = true; - files_changed = true; - } - if ui.button("deactivate all").clicked() { - self.all_files_activation_status = false; - all_files_toggle = true; - files_changed = true; - } - }); - //ui.label("Trails"); - //ui.label("Markers"); - ui.end_row(); - - for pack in self.packs.values_mut() { - //TODO: first loop to list what is active per pack, to not display all packs - let report = self.reports.get(&pack.uuid).unwrap(); - let mut pack_files_toggle = false; - let mut pack_files_activation_status = true; - ui.horizontal(|ui|{ - ui.label(&pack.name); + Window::new("File Manager") + .open(open) + .show(etx, |ui| -> Result<()> { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Grid::new("link grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + let mut all_files_toggle = false; + ui.horizontal(|ui| { if ui.button("activate all").clicked() { - pack_files_activation_status = true; - pack_files_toggle = true; + self.all_files_activation_status = true; + all_files_toggle = true; files_changed = true; } if ui.button("deactivate all").clicked() { - pack_files_activation_status = false; - pack_files_toggle = true; + self.all_files_activation_status = false; + all_files_toggle = true; files_changed = true; } }); + //ui.label("Trails"); + //ui.label("Markers"); ui.end_row(); - for source_file_uuid in pack.source_files.keys() { - if let Some(is_selected) = self.currently_used_files.get_mut(source_file_uuid) { - if all_files_toggle { - *is_selected = self.all_files_activation_status; + + for pack in self.packs.values_mut() { + //TODO: first loop to list what is active per pack, to not display all packs + let report = self.reports.get(&pack.uuid).unwrap(); + let mut pack_files_toggle = false; + let mut pack_files_activation_status = true; + ui.horizontal(|ui| { + ui.label(&pack.name); + if ui.button("activate all").clicked() { + pack_files_activation_status = true; + pack_files_toggle = true; + files_changed = true; } - if pack_files_toggle { - *is_selected = pack_files_activation_status; + if ui.button("deactivate all").clicked() { + pack_files_activation_status = false; + pack_files_toggle = true; + files_changed = true; } - ui.add_space(3.0); - //reports may be corrupted or not loaded, files are there - if let Some(source_file_name) = report.source_file_uuid_to_name(source_file_uuid) { - //format the file from reports and packages + prefix with the package name - let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_name)); - if cb.changed() { - files_changed = true; + }); + ui.end_row(); + for source_file_uuid in pack.source_files.keys() { + if let Some(is_selected) = + self.currently_used_files.get_mut(source_file_uuid) + { + if all_files_toggle { + *is_selected = self.all_files_activation_status; + } + if pack_files_toggle { + *is_selected = pack_files_activation_status; } - } else { - // Import report is corrupted, only print reference - let cb = ui.checkbox(is_selected, format!("{}: {}", pack.name, source_file_uuid)); - if cb.changed() { - files_changed = true; + ui.add_space(3.0); + //reports may be corrupted or not loaded, files are there + if let Some(source_file_name) = + report.source_file_uuid_to_name(source_file_uuid) + { + //format the file from reports and packages + prefix with the package name + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_name), + ); + if cb.changed() { + files_changed = true; + } + } else { + // Import report is corrupted, only print reference + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_uuid), + ); + if cb.changed() { + files_changed = true; + } } + ui.end_row(); } - ui.end_row(); } } - } - ui.end_row(); - }) + ui.end_row(); + }) + }); + Ok(()) }); - Ok(()) - }); if files_changed { - let _ = event_sender.send(UIToBackMessage::ActiveFiles(self.currently_used_files.clone())); + let _ = event_sender.send(UIToBackMessage::ActiveFiles( + self.currently_used_files.clone(), + )); } } @@ -622,23 +674,46 @@ impl PackageUIManager { // protection against deletion while displaying details if let Some(pack) = self.packs.get(&uuid) { if let Some(report) = self.reports.get(&uuid) { - let collapsing = CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + let collapsing = + CollapsingHeader::new(format!("Last load details of package {}", pack.name)); let header_response = collapsing .open(Some(true)) .show(ui, |ui| { - egui::Grid::new("packs details").striped(true).show(ui, |ui| { - let number_of = &report.number_of; - ui.label("categories"); ui.label(format!("{}", number_of.categories)); ui.end_row(); - ui.label("missing_categories");ui.label(format!("{}", number_of.missing_categories));ui.end_row(); - ui.label("textures"); ui.label(format!("{}", number_of.textures)); ui.end_row(); - ui.label("missing_textures"); ui.label(format!("{}", number_of.missing_textures)); ui.end_row(); - ui.label("entities"); ui.label(format!("{}", number_of.entities)); ui.end_row(); - ui.label("markers"); ui.label(format!("{}", number_of.markers)); ui.end_row(); - ui.label("trails"); ui.label(format!("{}", number_of.trails)); ui.end_row(); - ui.label("routes"); ui.label(format!("{}", number_of.routes)); ui.end_row(); - ui.label("maps"); ui.label(format!("{}", number_of.maps)); ui.end_row(); - ui.label("source_files"); ui.label(format!("{}", number_of.source_files)); ui.end_row(); - }) + egui::Grid::new("packs details") + .striped(true) + .show(ui, |ui| { + let number_of = &report.number_of; + ui.label("categories"); + ui.label(format!("{}", number_of.categories)); + ui.end_row(); + ui.label("missing_categories"); + ui.label(format!("{}", number_of.missing_categories)); + ui.end_row(); + ui.label("textures"); + ui.label(format!("{}", number_of.textures)); + ui.end_row(); + ui.label("missing_textures"); + ui.label(format!("{}", number_of.missing_textures)); + ui.end_row(); + ui.label("entities"); + ui.label(format!("{}", number_of.entities)); + ui.end_row(); + ui.label("markers"); + ui.label(format!("{}", number_of.markers)); + ui.end_row(); + ui.label("trails"); + ui.label(format!("{}", number_of.trails)); + ui.end_row(); + ui.label("routes"); + ui.label(format!("{}", number_of.routes)); + ui.end_row(); + ui.label("maps"); + ui.label(format!("{}", number_of.maps)); + ui.end_row(); + ui.label("source_files"); + ui.label(format!("{}", number_of.source_files)); + ui.end_row(); + }) }) .header_response; if header_response.clicked() { @@ -652,9 +727,9 @@ impl PackageUIManager { } } fn gui_package_list( - &mut self, + &mut self, u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, + etx: &egui::Context, import_status: &Arc>, open: &mut bool, first_load_done: bool, @@ -733,15 +808,21 @@ impl PackageUIManager { }); } pub fn gui( - &mut self, + &mut self, u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - is_marker_open: &mut bool, + etx: &egui::Context, + is_marker_open: &mut bool, import_status: &Arc>, - is_file_open: &mut bool, + is_file_open: &mut bool, first_load_done: bool, ) { - self.gui_package_list(u2b_sender, etx, import_status, is_marker_open, first_load_done); + self.gui_package_list( + u2b_sender, + etx, + import_status, + is_marker_open, + first_load_done, + ); self.gui_file_manager(u2b_sender, etx, is_file_open); } @@ -762,5 +843,3 @@ impl PackageUIManager { self.reports.insert(report.uuid, report); } } - - diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index 65e4ec3..034f6f4 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -1,49 +1,50 @@ use std::collections::{BTreeMap, HashSet}; -use joko_package_models::{attributes::CommonAttributes, package::{PackCore, PackageImportReport}}; +use joko_package_models::{ + attributes::CommonAttributes, + package::{PackCore, PackageImportReport}, +}; use uuid::Uuid; use glam::Vec3; -use jokolink::MumbleLink; use joko_core::RelativePath; -use joko_render_models::{ - marker::MarkerObject, - trail::TrailObject -}; +use joko_render_models::{marker::MarkerObject, trail::TrailObject}; +use jokolink::MumbleLink; use crate::LoadedPackTexture; pub enum BackToUIMessage { - ActiveElements(HashSet),//list of all elements that are loaded for current map - CurrentlyUsedFiles(BTreeMap),//when there is a change in map or anything else, the list of files is sent to ui for display - LoadedPack(LoadedPackTexture, PackageImportReport),//push a loaded pack to UI - DeletedPacks(Vec),//push a deleted set of packs to UI + ActiveElements(HashSet), //list of all elements that are loaded for current map + CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display + LoadedPack(LoadedPackTexture, PackageImportReport), //push a loaded pack to UI + DeletedPacks(Vec), //push a deleted set of packs to UI FirstLoadDone, ImportedPack(String, PackCore), ImportFailure(miette::Report), MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), //MumbleLink(Option), //MumbleLinkChanged,//tell there is a need to resize - NbTasksRunning(i32),//tell the number of taks running in background - PackageActiveElements(Uuid, HashSet),// first is the package reference, second is the list of active elements in the package. - TextureSwapChain,// The list of texture to load was changed, will be soon followed by a RenderSwapChain + NbTasksRunning(i32), //tell the number of taks running in background + PackageActiveElements(Uuid, HashSet), // first is the package reference, second is the list of active elements in the package. + TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), } pub enum UIToBackMessage { - ActiveFiles(BTreeMap),//when there is a change of files activated, send whole list to data for save. - CategoryActivationElementStatusChange(Uuid, bool),//sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. - CategoryActivationBranchStatusChange(Uuid, bool),//same, for a whole branch - CategoryActivationStatusChanged,//something happened that needs to reload the whole set - CategorySetAll(bool),//signal all categories should be now at this status - DeletePacks(Vec),//uuid of the pack to delete + ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. + CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationBranchStatusChange(Uuid, bool), //same, for a whole branch + CategoryActivationStatusChanged, //something happened that needs to reload the whole set + CategorySetAll(bool), //signal all categories should be now at this status + DeletePacks(Vec), //uuid of the pack to delete ImportPack(std::path::PathBuf), MumbleLinkBindedOnUI, MumbleLinkAutonomous, - MumbleLink(Option),//pushed from a value imposed by UI. Either a form or a traveling for demo. + MumbleLink(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. ReloadPack, SavePack(String, PackCore), + SaveUIConfiguration(String), } pub enum UIToUIMessage { @@ -51,7 +52,6 @@ pub enum UIToUIMessage { BulkTrailObject(Vec), //Present,// a render loop is finished and we can present it MarkerObject(MarkerObject), - RenderSwapChain,// The list of elements to display was changed + RenderSwapChain, // The list of elements to display was changed TrailObject(TrailObject), } - diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index a7c00bc..627733f 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -6,13 +6,11 @@ use itertools::Itertools; use tracing::info; use xot::{Element, NameId, Xot}; - use joko_core::RelativePath; use jokoapi::end_point::mounts::Mount; use jokoapi::end_point::races::Race; use smol_str::SmolStr; - pub struct XotAttributeNameIDs { // xml tags pub overlay_data: NameId, @@ -32,7 +30,7 @@ pub struct XotAttributeNameIDs { pub default_enabled: NameId, pub display_name: NameId, pub name: NameId, - pub capital_name: NameId,//same than "name" but with a starting capital letter + pub capital_name: NameId, //same than "name" but with a starting capital letter pub separator: NameId, // inheritable attributes pub achievement_id: NameId, @@ -422,23 +420,18 @@ macro_rules! update_attribute_from_ele { fn parse_boolean(raw_value: &str) -> Option { let trimmed = raw_value.trim().to_lowercase(); match trimmed.as_ref() { - "true" => {Some(true)}, - "false" => {Some(false)}, + "true" => Some(true), + "false" => Some(false), _ => { - match trimmed.parse::() {//might entirely get rid of parsing - Ok(parsed_value) => { - match parsed_value { - 0 | 1 => { - Some(parsed_value == 1) - } - _ => None - } - } - Err(_e) => { - None - } + match trimmed.parse::() { + //might entirely get rid of parsing + Ok(parsed_value) => match parsed_value { + 0 | 1 => Some(parsed_value == 1), + _ => None, + }, + Err(_e) => None, } - }, + } } } macro_rules! update_attribute_bool_from_ele { diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index a7daf42..3073229 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -1,7 +1,7 @@ +use crate::{attributes::CommonAttributes, package::PackageImportReport}; use ordered_hash_map::OrderedHashMap; use tracing::debug; use uuid::Uuid; -use crate::{attributes::CommonAttributes, package::PackageImportReport}; #[derive(Debug, Clone)] pub struct RawCategory { @@ -26,21 +26,20 @@ pub struct Category { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub children: OrderedHashMap,//TODO: make a branch to test if having an Vec associated with global list of categories is faster. + pub children: OrderedHashMap, //TODO: make a branch to test if having an Vec associated with global list of categories is faster. } pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { let nb_matches = s.matches(pat).count(); assert!(nb_matches + 1 > n); - let res = s.split(pat) - .nth(n) - ; + let res = s.split(pat).nth(n); debug!("nth_chunk {} {} {:?}", s, n, res); res.unwrap().to_string() } pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { - let res = s.match_indices(pat) + let res = s + .match_indices(pat) .nth(n) .map(|(index, _)| s.split_at(index)) .map(|(left, _)| left.to_string()); @@ -51,7 +50,8 @@ pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { pub fn prefix_parent(s: &str, pat: char) -> Option { let n = s.matches(pat).count(); assert!(n > 0); - let res = s.match_indices(pat) + let res = s + .match_indices(pat) .nth(n - 1) .map(|(index, _)| s.split_at(index)) .map(|(left, _)| left.to_string()); @@ -59,8 +59,6 @@ pub fn prefix_parent(s: &str, pat: char) -> Option { res } - - impl Category { // Required method pub fn from(value: &RawCategory, parent: Option) -> Self { @@ -73,15 +71,23 @@ impl Category { relative_category_name: value.relative_category_name.clone(), full_category_name: value.full_category_name.clone(), parent: parent, - children: Default::default() + children: Default::default(), } } - fn per_route<'a>(categories: &'a mut OrderedHashMap, route: &Vec<&str>, depth: usize) -> Option<&'a mut Category> { + fn per_route<'a>( + categories: &'a mut OrderedHashMap, + route: &Vec<&str>, + depth: usize, + ) -> Option<&'a mut Category> { let mut route = route.clone(); route.reverse(); Category::_per_route(categories, &mut route, depth) } - fn _per_route<'a>(categories: &'a mut OrderedHashMap, route: &mut Vec<&str>, depth: usize) -> Option<&'a mut Category> { + fn _per_route<'a>( + categories: &'a mut OrderedHashMap, + route: &mut Vec<&str>, + depth: usize, + ) -> Option<&'a mut Category> { if let Some(relative_category_name) = route.pop() { for (_, cat) in categories { if cat.relative_category_name == relative_category_name { @@ -95,7 +101,11 @@ impl Category { } return None; } - fn per_uuid<'a>(categories: &'a mut OrderedHashMap, uuid: &Uuid, depth: usize) -> Option<&'a mut Category> { + fn per_uuid<'a>( + categories: &'a mut OrderedHashMap, + uuid: &Uuid, + depth: usize, + ) -> Option<&'a mut Category> { /* Do a look up in the tree based on uuid. Whole tree is scanned until a match is found. @@ -120,14 +130,14 @@ impl Category { let mut first_pass_categories = input_first_pass_categories.clone(); let mut second_pass_categories: OrderedHashMap = Default::default(); let mut need_a_pass: bool = true; - + let mut third_pass_categories: OrderedHashMap = Default::default(); let mut third_pass_categories_ref: Vec = Default::default(); let mut root: OrderedHashMap = Default::default(); let elaspsed_initialize = start_initialize.elapsed().unwrap_or_default(); report.telemetry.categories_reassemble.initialize = elaspsed_initialize.as_millis(); - + let start_multi_pass_missing_categories_creation = std::time::SystemTime::now(); let mut nb_pass_done = 0; while need_a_pass { @@ -136,21 +146,28 @@ impl Category { for (key, value) in first_pass_categories.iter() { debug!("reassemble_categories pass #{} {:?}", nb_pass_done, value); let mut to_insert = value.clone(); - if value.relative_category_name.matches('.').count() > 0 && value.relative_category_name == value.full_category_name { + if value.relative_category_name.matches('.').count() > 0 + && value.relative_category_name == value.full_category_name + { let mut n = 0; let mut last_name: Option = None; // This is an almost duplication of code of pack/mod.rs - while let Some(parent_name) = prefix_until_nth_char(&value.relative_category_name, '.', n) { + while let Some(parent_name) = + prefix_until_nth_char(&value.relative_category_name, '.', n) + { debug!("{} {}", parent_name, n); if let Some(parent_category) = first_pass_categories.get(&parent_name) { report.found_category_late(&parent_name, parent_category.guid); last_name = Some(parent_name.clone()); - } else if let Some(parent_category) = second_pass_categories.get(&parent_name) { + } else if let Some(parent_category) = + second_pass_categories.get(&parent_name) + { report.found_category_late(&parent_name, parent_category.guid); last_name = Some(parent_name.clone()); - }else{ + } else { let new_uuid = Uuid::new_v4(); - let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + let relative_category_name = + nth_chunk(&value.relative_category_name, '.', n); debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); let sources: OrderedHashMap = OrderedHashMap::new(); let to_insert = RawCategory { @@ -158,7 +175,7 @@ impl Category { guid: new_uuid, relative_category_name: relative_category_name.clone(), display_name: relative_category_name.clone(), - parent_name: prefix_until_nth_char(&parent_name, '.', n-1), + parent_name: prefix_until_nth_char(&parent_name, '.', n - 1), props: value.props.clone(), separator: false, full_category_name: parent_name.clone(), @@ -172,12 +189,21 @@ impl Category { n += 1; } for (requester_uuid, source_file_uuid) in value.sources.iter() { - report.found_category_late_with_details(&value.full_category_name, value.guid, requester_uuid, source_file_uuid); + report.found_category_late_with_details( + &value.full_category_name, + value.guid, + requester_uuid, + source_file_uuid, + ); } report.found_category_late(&value.full_category_name, value.guid); - to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); + to_insert.relative_category_name = + nth_chunk(&value.relative_category_name, '.', n); to_insert.display_name = to_insert.relative_category_name.clone(); - debug!("parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name); + debug!( + "parent_name: {:?}, new name: {}, old name: {}", + last_name, to_insert.relative_category_name, &value.relative_category_name + ); assert!(last_name.is_some()); to_insert.parent_name = last_name; } else { @@ -187,7 +213,7 @@ impl Category { } else { None } - }else { + } else { None }; debug!("insert as is {:?}", to_insert); @@ -199,8 +225,15 @@ impl Category { second_pass_categories.clear(); } } - let elaspsed_multi_pass_missing_categories_creation = start_multi_pass_missing_categories_creation.elapsed().unwrap_or_default(); - report.telemetry.categories_reassemble.missing_categories_creation = elaspsed_multi_pass_missing_categories_creation.as_millis(); + let elaspsed_multi_pass_missing_categories_creation = + start_multi_pass_missing_categories_creation + .elapsed() + .unwrap_or_default(); + report + .telemetry + .categories_reassemble + .missing_categories_creation = + elaspsed_multi_pass_missing_categories_creation.as_millis(); debug!("nb_pass_done {}", nb_pass_done); let start_parent_child_relationship = std::time::SystemTime::now(); @@ -214,27 +247,38 @@ impl Category { } else { None }; - - debug!("{} parent is {:?}", key , parent); + + debug!("{} parent is {:?}", key, parent); let cat = Category::from(&value, parent); let cat_ref = cat.guid.clone(); - if third_pass_categories.insert(cat.guid.clone(), cat).is_none() { + if third_pass_categories + .insert(cat.guid.clone(), cat) + .is_none() + { third_pass_categories_ref.push(cat_ref); } } - let elaspsed_parent_child_relationship = start_parent_child_relationship.elapsed().unwrap_or_default(); - report.telemetry.categories_reassemble.parent_child_relationship = elaspsed_parent_child_relationship.as_millis(); - + let elaspsed_parent_child_relationship = start_parent_child_relationship + .elapsed() + .unwrap_or_default(); + report + .telemetry + .categories_reassemble + .parent_child_relationship = elaspsed_parent_child_relationship.as_millis(); + debug!("third_pass_categories_ref"); let start_tree_insertion = std::time::SystemTime::now(); for full_category_uuid in third_pass_categories_ref { if let Some(cat) = third_pass_categories.remove(&full_category_uuid) { let mut route = Vec::from_iter(cat.full_category_name.split('.')); - route.pop();//it is now the parent route + route.pop(); //it is now the parent route if let Some(parent) = cat.parent { - if let Some(parent_category) = Category::per_route(&mut third_pass_categories, &route, 0) { + if let Some(parent_category) = + Category::per_route(&mut third_pass_categories, &route, 0) + { parent_category.children.insert(cat.guid.clone(), cat); - } else if let Some(parent_category) = Category::per_route(&mut root, &route, 0) { + } else if let Some(parent_category) = Category::per_route(&mut root, &route, 0) + { parent_category.children.insert(cat.guid.clone(), cat); } else { panic!("Could not find parent {} for {:?}", parent, cat); @@ -251,7 +295,4 @@ impl Category { debug!("reassemble_categories end {:?}", root); root } - - } - diff --git a/crates/joko_package_models/src/lib.rs b/crates/joko_package_models/src/lib.rs index df91d6c..a6f2fdf 100644 --- a/crates/joko_package_models/src/lib.rs +++ b/crates/joko_package_models/src/lib.rs @@ -1,4 +1,3 @@ - pub mod attributes; pub mod category; pub mod map; @@ -6,4 +5,3 @@ pub mod marker; pub mod package; pub mod route; pub mod trail; - diff --git a/crates/joko_package_models/src/map.rs b/crates/joko_package_models/src/map.rs index 2902c42..83a7a7d 100644 --- a/crates/joko_package_models/src/map.rs +++ b/crates/joko_package_models/src/map.rs @@ -1,8 +1,8 @@ -use uuid::Uuid; -use indexmap::IndexMap; use crate::marker::Marker; use crate::route::Route; use crate::trail::Trail; +use indexmap::IndexMap; +use uuid::Uuid; #[derive(Default, Debug, Clone)] pub struct MapData { @@ -10,4 +10,3 @@ pub struct MapData { pub routes: IndexMap, pub trails: IndexMap, } - diff --git a/crates/joko_package_models/src/marker.rs b/crates/joko_package_models/src/marker.rs index d4b446b..9f5e068 100644 --- a/crates/joko_package_models/src/marker.rs +++ b/crates/joko_package_models/src/marker.rs @@ -2,7 +2,6 @@ use crate::attributes::CommonAttributes; use glam::Vec3; use uuid::Uuid; - #[derive(Debug, Clone)] pub struct Marker { pub guid: Uuid, diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 8fb2e71..4f0e549 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -1,16 +1,15 @@ +use crate::category::{prefix_until_nth_char, Category}; +use crate::map::MapData; +use crate::marker::Marker; +use crate::route::{route_to_tbin, route_to_trail, Route}; +use crate::trail::{TBin, Trail}; use base64::Engine; use joko_core::RelativePath; -use serde::{Deserialize, Serialize, Serializer, Deserializer}; +use ordered_hash_map::OrderedHashMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::{BTreeMap, HashMap, HashSet}; use tracing::{debug, trace}; use uuid::Uuid; -use std::collections::{BTreeMap, HashMap, HashSet}; -use ordered_hash_map::OrderedHashMap; -use crate::marker::Marker; -use crate::route::{route_to_tbin, route_to_trail, Route}; -use crate::trail::{TBin, Trail}; -use crate::category::{prefix_until_nth_char, Category}; -use crate::map::MapData; - pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( &base64::alphabet::STANDARD, @@ -18,9 +17,10 @@ pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::Genera ); fn serialize_reference(reference: &ElementReference, serializer: S) -> Result -where S: Serializer +where + S: Serializer, { - match reference { + match reference { ElementReference::Uuid(uuid) => { let to_do = BASE64_ENGINE.encode(uuid); serializer.serialize_str(to_do.as_str()) @@ -32,7 +32,8 @@ where S: Serializer } fn deserialize_reference<'de, D>(deserializer: D) -> Result -where D: Deserializer<'de> +where + D: Deserializer<'de>, { let encoded_uuid_or_full_category_name = String::deserialize(deserializer)?; if let Ok(bytes) = BASE64_ENGINE.decode(encoded_uuid_or_full_category_name.as_bytes()) { @@ -41,20 +42,23 @@ where D: Deserializer<'de> let res = Uuid::from_bytes(uuid_bytes); Ok(ElementReference::Uuid(res)) } else { - Ok(ElementReference::Category(encoded_uuid_or_full_category_name)) + Ok(ElementReference::Category( + encoded_uuid_or_full_category_name, + )) } } - fn serialize_uuid_in_base64(uuid: &Uuid, serializer: S) -> Result -where S: Serializer +where + S: Serializer, { let to_do = BASE64_ENGINE.encode(uuid); serializer.serialize_str(to_do.as_str()) } fn deserialize_uuid_in_base64<'de, D>(deserializer: D) -> Result -where D: Deserializer<'de> +where + D: Deserializer<'de>, { let encoded = String::deserialize(deserializer)?; if let Ok(bytes) = BASE64_ENGINE.decode(encoded.as_bytes()) { @@ -63,16 +67,19 @@ where D: Deserializer<'de> let res = Uuid::from_bytes(uuid_bytes); Ok(res) } else { - Err(serde::de::Error::custom("Could not parse base64 encoded uuid")) + Err(serde::de::Error::custom( + "Could not parse base64 encoded uuid", + )) } - } - #[derive(Debug, Clone, Serialize, Deserialize)] struct PackageCategorySource { full_category_name: String, - #[serde(serialize_with= "serialize_uuid_in_base64", deserialize_with= "deserialize_uuid_in_base64")] + #[serde( + serialize_with = "serialize_uuid_in_base64", + deserialize_with = "deserialize_uuid_in_base64" + )] requester_uuid: Uuid, source_file_name: String, } @@ -84,26 +91,28 @@ enum ElementReference { #[derive(Debug, Clone, Serialize, Deserialize)] struct PackageElementSource { file_path: String, - #[serde(serialize_with= "serialize_reference", deserialize_with = "deserialize_reference")] + #[serde( + serialize_with = "serialize_reference", + deserialize_with = "deserialize_reference" + )] requester_reference: ElementReference, source_file_name: String, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportStatistics { - pub categories: usize, // total number of found categories + pub categories: usize, // total number of found categories pub missing_categories: usize, // categories that should be defined in a node - pub textures: usize, //total number of texture used (or should) - pub missing_textures: usize, // how many of the textures are missing + pub textures: usize, //total number of texture used (or should) + pub missing_textures: usize, // how many of the textures are missing pub entities: usize, // total number of tracked elements: categories, trails, markers, ... - pub markers: usize, // total number of markers - pub trails: usize, // total number of trails + pub markers: usize, // total number of markers + pub trails: usize, // total number of trails pub routes: usize, // total number of routes defined, they shall not count as trails even if imported as such - pub maps: usize, // total number of maps covered + pub maps: usize, // total number of maps covered pub source_files: usize, // total number of XML files } - #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct PackageImportReassembleTelemetry { pub total: u128, @@ -128,15 +137,15 @@ pub struct PackageImportReport { #[serde(skip)] pub uuid: Uuid, pub number_of: PackageImportStatistics, // count everything we can think of - pub telemetry: PackageImportTelemetry, // all the time spent in which step - late_discovered_categories: OrderedHashMap,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. - missing_categories: Vec,//categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + pub telemetry: PackageImportTelemetry, // all the time spent in which step + late_discovered_categories: OrderedHashMap, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + missing_categories: Vec, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. #[serde(skip)] _missing_categories_tracker: HashSet, // for tracking purpose to avoid duplicate #[serde(skip)] _missing_textures_tracker: HashSet, // for tracking purpose to avoid duplicate - missing_textures: Vec,//missing texture for display - missing_trails: Vec,//missing file for trail + missing_textures: Vec, //missing texture for display + missing_trails: Vec, //missing file for trail source_files: bimap::BiMap, //map of all files to uuid. When exporting this shall have to be reversed. } @@ -152,12 +161,11 @@ pub struct PackCore { pub categories: OrderedHashMap, pub all_categories: HashMap, pub entities_parents: HashMap, - pub active_source_files: BTreeMap,//TODO: have a reference containing pack name and maybe even path inside the package + pub active_source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, pub report: PackageImportReport, } - impl PackageImportReport { pub const REPORT_FILE_NAME: &'static str = "import_report.json"; @@ -165,7 +173,8 @@ impl PackageImportReport { self.number_of = Default::default(); } fn merge_partial(&mut self, partial_report: PackageImportReport) { - self.late_discovered_categories.extend(partial_report.late_discovered_categories); + self.late_discovered_categories + .extend(partial_report.late_discovered_categories); } pub fn is_category_discovered_late(&self, uuid: Uuid) -> bool { @@ -180,21 +189,32 @@ impl PackageImportReport { } pub fn found_category_late(&mut self, full_category_name: &String, category_uuid: Uuid) { - self.late_discovered_categories.insert(category_uuid, full_category_name.clone()); + self.late_discovered_categories + .insert(category_uuid, full_category_name.clone()); } - pub fn found_category_late_with_details(&mut self, full_category_name: &String, category_uuid: Uuid, requester_uuid: &Uuid, source_file_uuid: &Uuid) { + pub fn found_category_late_with_details( + &mut self, + full_category_name: &String, + category_uuid: Uuid, + requester_uuid: &Uuid, + source_file_uuid: &Uuid, + ) { self.found_category_late(full_category_name, category_uuid); let source_file_name = self.source_files.get_by_right(source_file_uuid).unwrap(); - + //for this to work we need to keep track of where each category was called and thus defined since late - self.missing_categories.push(PackageCategorySource{ + self.missing_categories.push(PackageCategorySource { full_category_name: full_category_name.clone(), requester_uuid: requester_uuid.clone(), - source_file_name: source_file_name.clone() + source_file_name: source_file_name.clone(), }); - if !self._missing_categories_tracker.contains(full_category_name) { + if !self + ._missing_categories_tracker + .contains(full_category_name) + { self.number_of.missing_categories += 1; - self._missing_categories_tracker.insert(full_category_name.clone()); + self._missing_categories_tracker + .insert(full_category_name.clone()); } } fn found_missing_texture(&mut self, file_path: &String) { @@ -202,13 +222,10 @@ impl PackageImportReport { self.number_of.missing_textures += 1; self._missing_textures_tracker.insert(file_path.clone()); } - } } - impl PackCore { - pub fn new() -> Self { let mut res = Self { all_categories: Default::default(), @@ -227,7 +244,7 @@ impl PackCore { } pub fn partial(all_categories: &HashMap) -> Self { // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. - let mut res: Self = Self::new(); + let mut res: Self = Self::new(); res.all_categories = all_categories.clone(); res } @@ -236,19 +253,25 @@ impl PackCore { self.maps.extend(partial_pack.maps); self.all_categories = partial_pack.all_categories; self.report.merge_partial(partial_pack.report); - self.active_source_files.extend(partial_pack.active_source_files); + self.active_source_files + .extend(partial_pack.active_source_files); self.tbins.extend(partial_pack.tbins); self.entities_parents.extend(partial_pack.entities_parents); } pub fn category_exists(&self, full_category_name: &String) -> bool { self.all_categories.contains_key(full_category_name) } - + pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { self.all_categories.get(full_category_name) } - pub fn get_or_create_category_uuid(&mut self, full_category_name: &String, requester_uuid: Uuid, source_file_uuid: &Uuid) -> Uuid { + pub fn get_or_create_category_uuid( + &mut self, + full_category_name: &String, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) -> Uuid { if let Some(category_uuid) = self.all_categories.get(full_category_name) { category_uuid.clone() } else { @@ -258,17 +281,29 @@ impl PackCore { let mut n = 0; let mut last_uuid: Option = None; - while let Some(parent_full_category_name) = prefix_until_nth_char(&full_category_name, '.', n) { + while let Some(parent_full_category_name) = + prefix_until_nth_char(&full_category_name, '.', n) + { n += 1; if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { //FIXME: might want to make the difference between impacted parents and actual missing category - self.report.found_category_late(&full_category_name, *parent_uuid); + self.report + .found_category_late(&full_category_name, *parent_uuid); last_uuid = Some(*parent_uuid); } else { let new_uuid = Uuid::new_v4(); - debug!("Partial create missing parent category: {} {}", parent_full_category_name, new_uuid); - self.all_categories.insert(parent_full_category_name.clone(), new_uuid); - self.report.found_category_late_with_details(&full_category_name, new_uuid, &requester_uuid, source_file_uuid); + debug!( + "Partial create missing parent category: {} {}", + parent_full_category_name, new_uuid + ); + self.all_categories + .insert(parent_full_category_name.clone(), new_uuid); + self.report.found_category_late_with_details( + &full_category_name, + new_uuid, + &requester_uuid, + source_file_uuid, + ); last_uuid = Some(new_uuid); } } @@ -280,13 +315,19 @@ impl PackCore { pub fn get_source_file_uuid(&mut self, source_file_name: &String) -> Uuid { // Must always exist when called since we registered the file already. - *self.report.source_files.get_by_left(source_file_name).unwrap() + *self + .report + .source_files + .get_by_left(source_file_name) + .unwrap() } pub fn register_source_file(&mut self, source_file_name: &String) -> Uuid { if !self.report.source_files.contains_left(source_file_name) { - let uuid_to_insert = Uuid::new_v4();//TODO: have a uuid built from current package name and source file name - self.report.source_files.insert(source_file_name.clone(), uuid_to_insert); + let uuid_to_insert = Uuid::new_v4(); //TODO: have a uuid built from current package name and source file name + self.report + .source_files + .insert(source_file_name.clone(), uuid_to_insert); self.report.number_of.source_files += 1; self.active_source_files.insert(uuid_to_insert, true); uuid_to_insert @@ -302,11 +343,19 @@ impl PackCore { self.report.number_of.textures += 1; } - pub fn register_uuid(&mut self, full_category_name: &String, uuid: &Uuid) -> Result{ + pub fn register_uuid( + &mut self, + full_category_name: &String, + uuid: &Uuid, + ) -> Result { if let Some(parent_uuid) = self.all_categories.get(full_category_name) { let mut uuid_to_insert = uuid.clone(); while self.entities_parents.contains_key(&uuid_to_insert) { - trace!("Uuid collision detected {} for elements in {}", uuid_to_insert, full_category_name); + trace!( + "Uuid collision detected {} for elements in {}", + uuid_to_insert, + full_category_name + ); uuid_to_insert = Uuid::new_v4(); } self.entities_parents.insert(uuid_to_insert, *parent_uuid); @@ -314,30 +363,49 @@ impl PackCore { Ok(uuid_to_insert) } else { //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - Err(miette::Error::msg(format!("Can't register world entity {} {}, no associated category found.", full_category_name, uuid))) + Err(miette::Error::msg(format!( + "Can't register world entity {} {}, no associated category found.", + full_category_name, uuid + ))) } } - pub fn register_marker(&mut self, full_category_name: String, mut marker: Marker) -> Result<(), miette::Error> { + pub fn register_marker( + &mut self, + full_category_name: String, + mut marker: Marker, + ) -> Result<(), miette::Error> { let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; marker.guid = uuid_to_insert; if !self.maps.contains_key(&marker.map_id) { self.maps.insert(marker.map_id, MapData::default()); self.report.number_of.maps += 1; } - self.maps.get_mut(&marker.map_id).unwrap().markers.insert(uuid_to_insert, marker); + self.maps + .get_mut(&marker.map_id) + .unwrap() + .markers + .insert(uuid_to_insert, marker); self.report.number_of.markers += 1; Ok(()) } - pub fn register_trail(&mut self, full_category_name: String, mut trail: Trail) -> Result<(), miette::Error> { + pub fn register_trail( + &mut self, + full_category_name: String, + mut trail: Trail, + ) -> Result<(), miette::Error> { let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; trail.guid = uuid_to_insert; if !self.maps.contains_key(&trail.map_id) { self.maps.insert(trail.map_id, MapData::default()); self.report.number_of.maps += 1; } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); + self.maps + .get_mut(&trail.map_id) + .unwrap() + .trails + .insert(uuid_to_insert, trail); self.report.number_of.trails += 1; Ok(()) } @@ -350,32 +418,47 @@ impl PackCore { let trail = route_to_trail(&route, &tbin_path); let tbin = route_to_tbin(&route); - self.tbins.insert(tbin_path, tbin);//there may be duplicates since we load and save each time + self.tbins.insert(tbin_path, tbin); //there may be duplicates since we load and save each time if !self.maps.contains_key(&trail.map_id) { self.maps.insert(trail.map_id, MapData::default()); self.report.number_of.maps += 1; } - self.maps.get_mut(&trail.map_id).unwrap().trails.insert(uuid_to_insert, trail); - self.maps.get_mut(&route.map_id).unwrap().routes.insert(uuid_to_insert, route); + self.maps + .get_mut(&trail.map_id) + .unwrap() + .trails + .insert(uuid_to_insert, trail); + self.maps + .get_mut(&route.map_id) + .unwrap() + .routes + .insert(uuid_to_insert, route); self.report.number_of.routes += 1; Ok(()) } - + pub fn register_categories(&mut self) { let mut entities_parents: HashMap = Default::default(); let mut all_categories: HashMap = Default::default(); - Self::recursive_register_categories(&mut entities_parents, &self.categories, &mut all_categories); + Self::recursive_register_categories( + &mut entities_parents, + &self.categories, + &mut all_categories, + ); self.entities_parents.extend(entities_parents); self.report.number_of.categories = all_categories.len(); self.all_categories = all_categories; } fn recursive_register_categories( - entities_parents: &mut HashMap, - categories: &OrderedHashMap, + entities_parents: &mut HashMap, + categories: &OrderedHashMap, all_categories: &mut HashMap, ) { for (_, cat) in categories.iter() { - debug!("Register category {} {} {:?}", cat.full_category_name, cat.guid, cat.parent); + debug!( + "Register category {} {} {:?}", + cat.full_category_name, cat.guid, cat.parent + ); all_categories.insert(cat.full_category_name.clone(), cat.guid); if let Some(parent) = cat.parent { entities_parents.insert(cat.guid, parent); @@ -384,32 +467,54 @@ impl PackCore { } } - pub fn found_missing_element_texture(&mut self, file_path: String, requester_uuid: Uuid, source_file_uuid: &Uuid) { + pub fn found_missing_element_texture( + &mut self, + file_path: String, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) { self.report.found_missing_texture(&file_path); - let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); - self.report.missing_textures.push(PackageElementSource{ + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_textures.push(PackageElementSource { file_path, requester_reference: ElementReference::Uuid(requester_uuid), - source_file_name: source_file_name.clone() + source_file_name: source_file_name.clone(), }); } - pub fn found_missing_inherited_texture(&mut self, file_path: String, full_category_name: String, source_file_uuid: &Uuid) { + pub fn found_missing_inherited_texture( + &mut self, + file_path: String, + full_category_name: String, + source_file_uuid: &Uuid, + ) { self.report.found_missing_texture(&file_path); - let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); - self.report.missing_textures.push(PackageElementSource{ + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_textures.push(PackageElementSource { file_path, requester_reference: ElementReference::Category(full_category_name), - source_file_name: source_file_name.clone() + source_file_name: source_file_name.clone(), }); - } - pub fn found_missing_trail(&mut self, file_path: &RelativePath, requester_uuid: Uuid, source_file_uuid: &Uuid) { - let source_file_name = self.report.source_file_uuid_to_name(source_file_uuid).unwrap(); - self.report.missing_trails.push(PackageElementSource{ + pub fn found_missing_trail( + &mut self, + file_path: &RelativePath, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) { + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_trails.push(PackageElementSource { file_path: file_path.as_str().to_string(), requester_reference: ElementReference::Uuid(requester_uuid), - source_file_name: source_file_name.clone() + source_file_name: source_file_name.clone(), }); - } -} \ No newline at end of file +} diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs index ef547ce..b24271a 100644 --- a/crates/joko_package_models/src/route.rs +++ b/crates/joko_package_models/src/route.rs @@ -1,8 +1,11 @@ +use glam::Vec3; use joko_core::RelativePath; use uuid::Uuid; -use glam::Vec3; -use crate::{attributes::CommonAttributes, trail::{TBin, Trail}}; +use crate::{ + attributes::CommonAttributes, + trail::{TBin, Trail}, +}; #[derive(Debug, Clone)] pub struct Route { @@ -17,10 +20,8 @@ pub struct Route { pub source_file_uuid: Uuid, } - - pub(crate) fn route_to_tbin(route: &Route) -> TBin { - assert!( route.path.len() > 1); + assert!(route.path.len() > 1); TBin { map_id: route.map_id, version: 0, @@ -41,8 +42,4 @@ pub(crate) fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { dynamic: true, source_file_uuid: route.source_file_uuid.clone(), } -} - - - - +} diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index 3e9c36f..bf2b2a6 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -5,8 +5,8 @@ use egui_render_three_d::{ }; use glam::Vec2; use joko_render_models::{ - marker::{MarkerVertex, MarkerObject}, - trail::TrailObject + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject, }; use tracing::{error, info, trace, warn}; @@ -16,17 +16,16 @@ const MARKER_VERTEX_STRIDE: i32 = std::mem::size_of::() as _; pub struct BillBoardRenderer { pub markers: Vec, pub trails: Vec, - pub markers_wip: Vec,//work in progress: this is where the markers are inserted - pub trails_wip: Vec,//work in progress: this is where the markers are inserted + pub markers_wip: Vec, //work in progress: this is where the markers are inserted + pub trails_wip: Vec, //work in progress: this is where the markers are inserted marker_program: NativeProgram, marker_vertex_buffer: NativeBuffer, marker_vertex_array: NativeVertexArray, - + trail_program: NativeProgram, trail_vertex_buffers: Vec, trail_vertex_arrays: Vec, } - const MARKER_VERTEX_SHADER: &str = include_str!("../shaders/marker.vs"); const MARKER_FRAGMENT_SHADER: &str = include_str!("../shaders/marker.fs"); @@ -36,15 +35,17 @@ const TRAIL_FRAGMENT_SHADER: &str = include_str!("../shaders/trail.fs"); impl BillBoardRenderer { pub fn new(gl: &Context) -> Self { unsafe { - let marker_program = new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); + let marker_program = + new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); gl_error!(gl); - let trail_shift_program = new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); + let trail_shift_program = + new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); gl_error!(gl); - + let marker_vertex_buffer = create_buffer(gl); let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); - + Self { markers: Vec::new(), markers_wip: Vec::new(), @@ -62,10 +63,11 @@ impl BillBoardRenderer { } } } - + pub fn swap(&mut self) { - trace!("swap UI to display {} markers, {} trails", - self.markers_wip.len(), + trace!( + "swap UI to display {} markers, {} trails", + self.markers_wip.len(), self.trails_wip.len() ); self.markers = std::mem::take(&mut self.markers_wip); @@ -109,9 +111,7 @@ impl BillBoardRenderer { for _ in 0..needs { let vb = unsafe { create_buffer(gl) }; self.trail_vertex_buffers.push(vb); - let trail_vertex_array = unsafe { - create_trail_array(gl, vb, 1) - }; + let trail_vertex_array = unsafe { create_trail_array(gl, vb, 1) }; self.trail_vertex_arrays.push(trail_vertex_array); } } @@ -145,14 +145,20 @@ impl BillBoardRenderer { gl_error!(gl); gl.active_texture(TEXTURE0); gl_error!(gl); - let scroll_texture: Vec2 = Vec2 { x: 0.0, y: (latest_time as f32 % 2.0) - 1.0};//TODO: manage speed in some configurations. per trail ? + let scroll_texture: Vec2 = Vec2 { + x: 0.0, + y: (latest_time as f32 % 2.0) - 1.0, + }; //TODO: manage speed in some configurations. per trail ? gl.uniform_2_f32_slice(Some(&NativeUniformLocation(3)), scroll_texture.as_ref()); //https://stackoverflow.com/questions/27771902/opengl-changing-texture-coordinates-on-the-fly //https://www.khronos.org/opengl/wiki/Uniform_(GLSL) - for ( (trail, trail_buffer), trail_array) - in self.trails.iter().zip(self.trail_vertex_buffers.iter()).zip(self.trail_vertex_arrays.iter() - ) { + for ((trail, trail_buffer), trail_array) in self + .trails + .iter() + .zip(self.trail_vertex_buffers.iter()) + .zip(self.trail_vertex_arrays.iter()) + { if let Some(texture) = textures.get(&trail.texture) { gl.bind_vertex_array(Some(*trail_array)); gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); @@ -162,8 +168,7 @@ impl BillBoardRenderer { view_proj.to_cols_array().as_ref(), ); gl_error!(gl); - - + gl.bind_vertex_buffer(0, Some(*trail_buffer), 0, MARKER_VERTEX_STRIDE); gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); gl.bind_texture(TEXTURE_2D, Some(texture.handle)); @@ -207,8 +212,6 @@ impl BillBoardRenderer { } } - - /// takes in strings containing vertex/fragment shaders and returns a Shaderprogram with them attached #[tracing::instrument(skip(gl))] pub fn new_program( @@ -306,7 +309,11 @@ unsafe fn create_marker_array(gl: &Context, vertex_buffer: NativeBuffer) -> Nati create_array(gl, vertex_buffer, 1) } -unsafe fn create_array(gl: &Context, vertex_buffer: NativeBuffer, binding_index: u32) -> NativeVertexArray { +unsafe fn create_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { let marker_vertex_array = gl.create_vertex_array().expect("failed to create egui vao"); gl.bind_vertex_array(Some(marker_vertex_array)); gl.bind_vertex_buffer(binding_index, Some(vertex_buffer), 0, MARKER_VERTEX_STRIDE); @@ -339,7 +346,11 @@ unsafe fn create_array(gl: &Context, vertex_buffer: NativeBuffer, binding_index: marker_vertex_array } -unsafe fn create_trail_array(gl: &Context, vertex_buffer: NativeBuffer, binding_index: u32) -> NativeVertexArray { +unsafe fn create_trail_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { let trail_vertex_array = create_array(gl, vertex_buffer, binding_index); gl.enable_vertex_array_attrib(trail_vertex_array, 5); gl.vertex_array_attrib_format_f32(trail_vertex_array, 5, 2, FLOAT, false, 36); diff --git a/crates/joko_render/src/gl.rs b/crates/joko_render/src/gl.rs index 9724f2f..8ac9626 100644 --- a/crates/joko_render/src/gl.rs +++ b/crates/joko_render/src/gl.rs @@ -6,4 +6,4 @@ macro_rules! gl_error { tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); } }}; -} \ No newline at end of file +} diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs index 0b02e64..9050354 100644 --- a/crates/joko_render/src/lib.rs +++ b/crates/joko_render/src/lib.rs @@ -1,3 +1,3 @@ -pub mod gl; pub mod billboard; -pub mod renderer; \ No newline at end of file +pub mod gl; +pub mod renderer; diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs index 544b49e..4b5a576 100644 --- a/crates/joko_render/src/renderer.rs +++ b/crates/joko_render/src/renderer.rs @@ -1,5 +1,5 @@ -use crate::gl_error; use crate::billboard::BillBoardRenderer; +use crate::gl_error; use egui_render_three_d::three_d; use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; @@ -16,10 +16,7 @@ use jokolink::MumbleLink; use jokolink::UIState; use three_d::prelude::*; -use joko_render_models::{ - marker::MarkerObject, - trail::TrailObject, -}; +use joko_render_models::{marker::MarkerObject, trail::TrailObject}; pub struct JokoRenderer { pub view_proj: Mat4, @@ -74,41 +71,41 @@ impl JokoRenderer { } /* - CRect GetMinimapRectangle() -{ - int w = mumbleLink.miniMap.compassWidth; - int h = mumbleLink.miniMap.compassHeight; + CRect GetMinimapRectangle() + { + int w = mumbleLink.miniMap.compassWidth; + int h = mumbleLink.miniMap.compassHeight; - CRect pos; - CRect size = App->GetRoot()->GetClientRect(); - float scale = GetWindowTooSmallScale(); + CRect pos; + CRect size = App->GetRoot()->GetClientRect(); + float scale = GetWindowTooSmallScale(); - pos.x1 = int( size.Width() - w * scale ); - pos.x2 = size.Width(); + pos.x1 = int( size.Width() - w * scale ); + pos.x2 = size.Width(); - if ( mumbleLink.isMinimapTopRight ) - { - pos.y1 = 1; - pos.y2 = int( h * scale + 1 ); - } - else - { - int delta = 37; - if ( mumbleLink.uiSize == 0 ) - delta = 33; - if ( mumbleLink.uiSize == 2 ) - delta = 41; - if ( mumbleLink.uiSize == 3 ) - delta = 45; + if ( mumbleLink.isMinimapTopRight ) + { + pos.y1 = 1; + pos.y2 = int( h * scale + 1 ); + } + else + { + int delta = 37; + if ( mumbleLink.uiSize == 0 ) + delta = 33; + if ( mumbleLink.uiSize == 2 ) + delta = 41; + if ( mumbleLink.uiSize == 3 ) + delta = 45; - pos.y1 = int( size.Height() - h * scale - delta * scale ); - pos.y2 = int( size.Height() - delta * scale ); - } + pos.y1 = int( size.Height() - h * scale - delta * scale ); + pos.y2 = int( size.Height() - delta * scale ); + } - return pos; -} - */ + return pos; + } + */ pub fn get_z_near() -> f32 { 1.0 } @@ -162,7 +159,7 @@ impl JokoRenderer { let client_width = (link.client_size.x) as f32; let client_height = (link.client_size.y) as f32; - + let cam_pos = if self.is_map_open { //TODO: validate values glam::Vec3{ @@ -225,7 +222,7 @@ impl JokoRenderer { pub fn add_trail(&mut self, trail_object: TrailObject) { self.billboard_renderer.trails_wip.push(trail_object); } - + pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { self.gl.prepare_frame(latest_framebuffer_size_getter); unsafe { @@ -259,7 +256,7 @@ impl JokoRenderer { self.cam_pos, &self.view_proj, &self.gl.glow_backend.painter.managed_textures, - latest_time + latest_time, ); } self.gl diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index 6c74e3d..2568e53 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -6,7 +6,7 @@ use crate::prelude::*; #[repr(u8)] #[derive(Debug, Clone, Copy)] pub enum Race { - Unknown = 1 << 1, + Unknown = 1 << 1, Asura = 1 << 2, Charr = 1 << 3, Human = 1 << 4, diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index 9853872..a3e22e7 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -43,4 +43,6 @@ egui = { workspace = true, features = ["serde"] } egui_extras = { workspace = true } uuid = { workspace = true } +toml = "0.8.12" +directories-next = "2.0.0" diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index f6d8b35..e88b95b 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -5,9 +5,15 @@ use miette::{Context, IntoDiagnostic, Result}; /// We will read a path from env `JOKOLAY_DATA_DIR` or create a folder at data_local_dir/jokolay, where data_local_dir is platform specific /// Inside this directory, we will store all of jokolay's data like configuration files, themes, logs etc.. -pub fn get_jokolay_path() -> Result { - let dir = get_jokolay_dir().unwrap(); - dir.canonicalize(".") +//TODO: isn't directories-next better for introspection ? +pub fn get_jokolay_path() -> Result { + if let Some(project_dir) = directories_next::ProjectDirs::from("com.jokolay", "", "jokolay") { + Ok(project_dir.data_local_dir().to_path_buf()) + } else { + Err(miette::miette!( + "getting project path failed for some reason" + )) + } } pub fn get_jokolay_dir() -> Result { @@ -27,7 +33,8 @@ pub fn get_jokolay_dir() -> Result { .wrap_err(jkl_path) .wrap_err("failed to open jokolay data dir")? } else { - let project_dir = cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah); + let project_dir = + cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah); let dir = project_dir .ok_or(miette::miette!( "getting project dirs failed for some reason" diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 2a06705..67dcf34 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,30 +1,36 @@ use std::{ collections::BTreeMap, - ops::DerefMut, - sync::{Arc, Mutex}, - thread + io::Write, + ops::DerefMut, + sync::{Arc, Mutex}, + thread, }; use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; -mod wm; mod mumble; -use uuid::Uuid; +mod ui_parameters; use init::{get_jokolay_dir, get_jokolay_path}; -use jmf::{message::{UIToBackMessage, UIToUIMessage}, PackageDataManager, PackageUIManager}; +use jmf::{ + message::{UIToBackMessage, UIToUIMessage}, + PackageDataManager, PackageUIManager, +}; +use uuid::Uuid; //use jmf::FileManager; -use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use crate::app::mumble::mumble_gui; +use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use jmf::message::BackToUIMessage; +use jmf::{ + build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, LoadedPackData, + LoadedPackTexture, +}; +use jmf::{import_pack_from_zip_file_path, ImportStatus}; use joko_render::renderer::JokoRenderer; use jokolink::{MumbleChanges, MumbleLink, MumbleManager}; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span}; -use jmf::{LoadedPackData, LoadedPackTexture, build_from_core, jokolay_to_working_path}; -use jmf::{ImportStatus, import_pack_from_zip_file_path}; - const MINIMAL_WINDOW_WIDTH: u32 = 640; const MINIMAL_WINDOW_HEIGHT: u32 = 480; @@ -35,27 +41,30 @@ struct JokolayUIState { link: Option, editable_mumble: bool, window_changed: bool, - list_of_textures_changed: bool,//Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display first_load_done: bool, - nb_running_tasks_on_back: i32,// store the number of running tasks in background thread - nb_running_tasks_on_network: i32,// store the number of running tasks (requests) in progress + nb_running_tasks_on_back: i32, // store the number of running tasks in background thread + nb_running_tasks_on_network: i32, // store the number of running tasks (requests) in progress import_status: Arc>, maximal_window_width: u32, maximal_window_height: u32, + root_path: std::path::PathBuf, } struct JokolayBackState { - choice_of_category_changed: bool,//Meant as an optimisation to only update when there is a change in UI + choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI read_ui_link: bool, copy_of_ui_link: Option, - working_path: std::path::PathBuf, + root_dir: Arc, + editable_path: std::path::PathBuf, + extract_path: std::path::PathBuf, } struct JokolayApp { mumble_manager: MumbleManager, package_manager: PackageDataManager, } struct JokolayGui { - frame_stats: wm::WindowStatistics, + ui_configuration: ui_parameters::JokolayUIConfiguration, menu_panel: MenuPanel, joko_renderer: JokoRenderer, egui_context: egui::Context, @@ -66,31 +75,31 @@ struct JokolayGui { } #[allow(unused)] pub struct Jokolay { - jokolay_dir: Arc, gui: Arc>, app: Arc>>, state_ui: JokolayUIState, state_back: JokolayBackState, } - impl Jokolay { - pub fn new(jokolay_dir: Arc, working_path: std::path::PathBuf) -> Result { + pub fn new(root_dir: Arc, root_path: std::path::PathBuf) -> Result { /* We have two mumble_managers, one for UI, one for backend, each keeping its own copy this avoid transmition between threads to read same data from system It happens anyway when the UI start the edit mode of the mumble link. */ + let mumble_data_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; let mumble_ui_manager = MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - + let data_packages: BTreeMap = Default::default(); let texture_packages: BTreeMap = Default::default(); - let package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&jokolay_dir))?; + let package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&root_dir))?; let mut package_ui_manager = PackageUIManager::new(texture_packages); - let mut theme_manager = ThemeManager::new(Arc::clone(&jokolay_dir)).wrap_err("failed to create theme manager")?; + let mut theme_manager = + ThemeManager::new(Arc::clone(&root_dir)).wrap_err("failed to create theme manager")?; let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); @@ -120,28 +129,36 @@ impl Jokolay { None } }); - + glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); - let frame_stats = wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _); - + + //TODO: load configuration from disk (ui.toml) + let editable_path = jokolay_to_editable_path(&root_path) + .to_str() + .unwrap() + .to_string(); + let ui_configuration = ui_parameters::JokolayUIConfiguration::new( + glfw_backend.glfw.get_time() as _, + editable_path.clone(), + ); + let menu_panel = MenuPanel::default(); package_ui_manager.late_init(&egui_context); Ok(Self { - jokolay_dir, gui: Arc::new(Mutex::new(JokolayGui { - frame_stats, + ui_configuration, joko_renderer, glfw_backend, egui_context, menu_panel, theme_manager, mumble_manager: mumble_ui_manager, - package_manager: package_ui_manager + package_manager: package_ui_manager, })), - app: Arc::new(Mutex::new(Box::new(JokolayApp{ + app: Arc::new(Mutex::new(Box::new(JokolayApp { mumble_manager: mumble_data_manager, package_manager: package_data_manager, }))), @@ -156,19 +173,21 @@ impl Jokolay { import_status: Default::default(), maximal_window_width: video_mode.unwrap().width, //TODO: what happens if change of screen ? maximal_window_height: video_mode.unwrap().height, + root_path: root_path.clone(), }, state_back: JokolayBackState { choice_of_category_changed: false, read_ui_link: false, copy_of_ui_link: Default::default(), - working_path, - } + root_dir, + editable_path: std::path::PathBuf::from(editable_path), + extract_path: std::path::PathBuf::from(jokolay_to_extract_path(&root_path)), + }, }) } fn start_background_loop( - jokolay_dir: Arc, - app: Arc>>, + app: Arc>>, state: JokolayBackState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, @@ -180,19 +199,19 @@ impl Jokolay { let mut app = app.lock().unwrap(); let JokolayApp { mumble_manager: _, - package_manager + package_manager, } = &mut app.deref_mut().as_mut(); - package_manager.load_all(Arc::clone(&jokolay_dir), &b2u_sender); + package_manager.load_all(Arc::clone(&state.root_dir), &b2u_sender); } Self::background_loop(Arc::clone(&app), state, b2u_sender, u2b_receiver); }); } fn handle_u2b_message( - package_manager: &mut PackageDataManager, + package_manager: &mut PackageDataManager, local_state: &mut JokolayBackState, b2u_sender: &std::sync::mpsc::Sender, - msg: UIToBackMessage + msg: UIToBackMessage, ) { match msg { UIToBackMessage::ActiveFiles(currently_used_files) => { @@ -201,11 +220,15 @@ impl Jokolay { local_state.choice_of_category_changed = true; } UIToBackMessage::CategoryActivationElementStatusChange(category_uuid, status) => { - tracing::trace!("Handling of UIToBackMessage::CategoryActivationElementStatusChange"); + tracing::trace!( + "Handling of UIToBackMessage::CategoryActivationElementStatusChange" + ); package_manager.category_set(category_uuid, status); } UIToBackMessage::CategoryActivationBranchStatusChange(category_uuid, status) => { - tracing::trace!("Handling of UIToBackMessage::CategoryActivationBranchStatusChange"); + tracing::trace!( + "Handling of UIToBackMessage::CategoryActivationBranchStatusChange" + ); package_manager.category_branch_set(category_uuid, status); } UIToBackMessage::CategoryActivationStatusChanged => { @@ -222,8 +245,9 @@ impl Jokolay { let mut deleted = Vec::new(); for pack_uuid in to_delete { if let Some(pack) = package_manager.packs.remove(&pack_uuid) { - if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) { - error!(?e, pack.name,"failed to remove pack"); + if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) + { + error!(?e, pack.name, "failed to remove pack"); } else { info!("deleted marker pack: {}", pack.name); deleted.push(pack_uuid); @@ -236,9 +260,12 @@ impl Jokolay { tracing::trace!("Handling of UIToBackMessage::ImportPack"); let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); let start = std::time::SystemTime::now(); - let result = import_pack_from_zip_file_path(file_path, &local_state.working_path); + let result = import_pack_from_zip_file_path(file_path, &local_state.extract_path); let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!("Loading of taco package from disk took {} ms", elaspsed.as_millis()); + tracing::info!( + "Loading of taco package from disk took {} ms", + elaspsed.as_millis() + ); match result { Ok((file_name, pack)) => { let _ = b2u_sender.send(BackToUIMessage::ImportedPack(file_name, pack)); @@ -262,45 +289,74 @@ impl Jokolay { local_state.copy_of_ui_link = link; } UIToBackMessage::ReloadPack => { - unimplemented!("Handling of UIToBackMessage::ReloadPack has not been implemented yet"); + unimplemented!( + "Handling of UIToBackMessage::ReloadPack has not been implemented yet" + ); } UIToBackMessage::SavePack(name, pack) => { tracing::trace!("Handling of UIToBackMessage::SavePack"); let name = name.as_str(); if package_manager.marker_packs_dir.exists(name) { - match package_manager.marker_packs_dir + match package_manager + .marker_packs_dir .remove_dir_all(name) - .into_diagnostic() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to delete already existing marker pack"); - } + .into_diagnostic() + { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to delete already existing marker pack"); } + } } if let Err(e) = package_manager.marker_packs_dir.create_dir_all(name) { error!(?e, "failed to create directory for pack"); } match package_manager.marker_packs_dir.open_dir(name) { Ok(dir) => { - let (data_pack, mut texture_pack, mut report) = build_from_core(name.to_string(), dir.into(), pack); + let (data_pack, mut texture_pack, mut report) = + build_from_core(name.to_string(), dir.into(), pack); tracing::trace!("Package loaded into data and texture"); let uuid_of_insertion = package_manager.save(data_pack, report.clone()); report.uuid = uuid_of_insertion; texture_pack.uuid = uuid_of_insertion; let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); - }, + } Err(e) => { - error!(?e, "failed to open marker pack directory to save pack {:?} {}", package_manager.marker_packs_dir, name); + error!( + ?e, + "failed to open marker pack directory to save pack {:?} {}", + package_manager.marker_packs_dir, + name + ); } }; } + UIToBackMessage::SaveUIConfiguration(serialized_string) => { + //let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(package_manager.tasks.count()+ 1)); //TODO: send update on screen + match local_state + .root_dir + .create(ui_parameters::UI_PARAMETERS_FILE_NAME) + { + Ok(mut file) => { + match file.write(serialized_string.as_bytes()).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to save UI configuration"); + } + } + } + Err(e) => { + error!(?e, "failed to open UI configuration file"); + } + } + } _ => { unimplemented!("Handling BackToUIMessage has not been implemented yet"); } } } fn background_loop( - app: Arc>>, + app: Arc>>, mut local_state: JokolayBackState, b2u_sender: std::sync::mpsc::Sender, u2b_receiver: std::sync::mpsc::Receiver, @@ -314,7 +370,7 @@ impl Jokolay { let mut app = app.lock().unwrap(); let JokolayApp { mumble_manager, - package_manager + package_manager, } = &mut app.deref_mut().as_mut(); while let Ok(msg) = u2b_receiver.try_recv() { @@ -323,26 +379,27 @@ impl Jokolay { } let link = if local_state.read_ui_link { local_state.copy_of_ui_link.as_ref() - }else { + } else { match mumble_manager.tick() { - Ok(ml) => { - ml - }, + Ok(ml) => ml, Err(e) => { error!(?e, "mumble manager tick error"); None } } }; - tracing::trace!("choice_of_category_changed: {}", local_state.choice_of_category_changed); - package_manager.tick( - &b2u_sender, - loop_index, - link, + tracing::trace!( + "choice_of_category_changed: {}", local_state.choice_of_category_changed ); + package_manager.tick( + &b2u_sender, + loop_index, + link, + local_state.choice_of_category_changed, + ); local_state.choice_of_category_changed = false; - + thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; } @@ -350,17 +407,20 @@ impl Jokolay { drop(span_guard); } - fn handle_u2u_message( - gui: &mut JokolayGui, - msg: UIToUIMessage - ) { + fn handle_u2u_message(gui: &mut JokolayGui, msg: UIToUIMessage) { match msg { UIToUIMessage::BulkMarkerObject(marker_objects) => { - tracing::debug!("Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len()); + tracing::debug!( + "Handling of UIToUIMessage::BulkMarkerObject {}", + marker_objects.len() + ); gui.joko_renderer.extend_markers(marker_objects); } UIToUIMessage::BulkTrailObject(trail_objects) => { - tracing::debug!("Handling of UIToUIMessage::BulkTrailObject {}", trail_objects.len()); + tracing::debug!( + "Handling of UIToUIMessage::BulkTrailObject {}", + trail_objects.len() + ); gui.joko_renderer.extend_trails(trail_objects); } UIToUIMessage::MarkerObject(mo) => { @@ -381,20 +441,21 @@ impl Jokolay { } } fn handle_b2u_message( - gui: &mut JokolayGui, + gui: &mut JokolayGui, local_state: &mut JokolayUIState, u2b_sender: &std::sync::mpsc::Sender, - msg: BackToUIMessage + msg: BackToUIMessage, ) { match msg { - BackToUIMessage::ActiveElements(active_elements) => { tracing::trace!("Handling of BackToUIMessage::ActiveElements"); - gui.package_manager.update_active_categories(&active_elements); + gui.package_manager + .update_active_categories(&active_elements); } BackToUIMessage::CurrentlyUsedFiles(currently_used_files) => { tracing::trace!("Handling of BackToUIMessage::CurrentlyUsedFiles"); - gui.package_manager.set_currently_used_files(currently_used_files); + gui.package_manager + .set_currently_used_files(currently_used_files); } BackToUIMessage::DeletedPacks(to_delete) => { tracing::trace!("Handling of BackToUIMessage::DeletedPacks"); @@ -405,12 +466,12 @@ impl Jokolay { } BackToUIMessage::ImportedPack(file_name, pack) => { tracing::trace!("Handling of BackToUIMessage::ImportedPack"); - *local_state.import_status.lock().unwrap() = ImportStatus::PackDone(file_name, pack, false); + *local_state.import_status.lock().unwrap() = + ImportStatus::PackDone(file_name, pack, false); } BackToUIMessage::ImportFailure(error) => { tracing::trace!("Handling of BackToUIMessage::ImportFailure"); *local_state.import_status.lock().unwrap() = ImportStatus::PackError(error); - } BackToUIMessage::LoadedPack(pack_texture, report) => { tracing::trace!("Handling of BackToUIMessage::LoadedPack"); @@ -418,9 +479,22 @@ impl Jokolay { local_state.import_status = Default::default(); let _ = u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); } - BackToUIMessage::MarkerTexture(pack_uuid, tex_path, marker_uuid, position, common_attributes) => { + BackToUIMessage::MarkerTexture( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + ) => { tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); - gui.package_manager.load_marker_texture(&gui.egui_context, pack_uuid, tex_path, marker_uuid, position, common_attributes); + gui.package_manager.load_marker_texture( + &gui.egui_context, + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + ); } BackToUIMessage::NbTasksRunning(nb_tasks) => { tracing::trace!("Handling of BackToUIMessage::NbTasksRunning"); @@ -428,7 +502,8 @@ impl Jokolay { } BackToUIMessage::PackageActiveElements(pack_uuid, active_elements) => { tracing::trace!("Handling of BackToUIMessage::PackageActiveElements"); - gui.package_manager.update_pack_active_categories(pack_uuid, &active_elements); + gui.package_manager + .update_pack_active_categories(pack_uuid, &active_elements); } BackToUIMessage::TextureSwapChain => { tracing::debug!("Handling of BackToUIMessage::TextureSwapChain"); @@ -437,7 +512,13 @@ impl Jokolay { } BackToUIMessage::TrailTexture(pack_uuid, tex_path, trail_uuid, common_attributes) => { tracing::trace!("Handling of BackToUIMessage::TrailTexture"); - gui.package_manager.load_trail_texture(&gui.egui_context, pack_uuid, tex_path, trail_uuid, common_attributes); + gui.package_manager.load_trail_texture( + &gui.egui_context, + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + ); } _ => { unimplemented!("Handling BackToUIMessage has not been implemented yet"); @@ -449,7 +530,12 @@ impl Jokolay { let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); - Self::start_background_loop(Arc::clone(&self.jokolay_dir), Arc::clone(&self.app), self.state_back, b2u_sender, u2b_receiver); + Self::start_background_loop( + Arc::clone(&self.app), + self.state_back, + b2u_sender, + u2b_receiver, + ); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); @@ -461,7 +547,11 @@ impl Jokolay { loop { { let mut nb_message_on_curent_loop: u128 = 0; - tracing::trace!("glfw event loop, {} frames, {} messages", nb_frames, nb_messages); + tracing::trace!( + "glfw event loop, {} frames, {} messages", + nb_frames, + nb_messages + ); if let Ok(mut import_status) = local_state.import_status.lock() { match &mut *import_status { @@ -485,7 +575,12 @@ impl Jokolay { if nb_message_on_curent_loop < max_nb_messages_per_loop { while let Ok(msg) = b2u_receiver.try_recv() { nb_messages += 1; - Self::handle_b2u_message(gui.deref_mut(), &mut local_state, &u2b_sender, msg); + Self::handle_b2u_message( + gui.deref_mut(), + &mut local_state, + &u2b_sender, + msg, + ); nb_message_on_curent_loop += 1; if nb_message_on_curent_loop == max_nb_messages_per_loop { break; @@ -494,20 +589,19 @@ impl Jokolay { } } - let mut gui = self.gui.lock().unwrap(); let JokolayGui { - frame_stats, + ui_configuration, menu_panel, joko_renderer, egui_context, glfw_backend, theme_manager, mumble_manager, - package_manager + package_manager, } = &mut gui.deref_mut(); let latest_time = glfw_backend.glfw.get_time(); - + let etx = egui_context.clone(); /* @@ -565,9 +659,9 @@ impl Jokolay { input.time = Some(latest_time); etx.begin_frame(input); - + // do all the non-gui stuff first - frame_stats.tick(latest_time); + ui_configuration.tick(latest_time); if local_state.editable_mumble { local_state.window_changed = true; local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); @@ -586,46 +680,51 @@ impl Jokolay { local_state.link = Some(link.clone()); } } - }, + } Err(e) => { error!(?e, "mumble manager tick error"); } } } - + // check if we need to change window position or size. if let Some(link) = local_state.link.as_ref() { if local_state.window_changed { - glfw_backend - .window - .set_pos(link.client_pos.x.max(MINIMAL_WINDOW_POSITION_X), link.client_pos.y.max(MINIMAL_WINDOW_POSITION_Y)); + glfw_backend.window.set_pos( + link.client_pos.x.max(MINIMAL_WINDOW_POSITION_X), + link.client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), + ); // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - let client_size_x = MINIMAL_WINDOW_WIDTH.max(link.client_size.x).min(local_state.maximal_window_width); - let client_size_y = MINIMAL_WINDOW_HEIGHT.max(link.client_size.y).min(local_state.maximal_window_height); + let client_size_x = MINIMAL_WINDOW_WIDTH + .max(link.client_size.x) + .min(local_state.maximal_window_width); + let client_size_y = MINIMAL_WINDOW_HEIGHT + .max(link.client_size.y) + .min(local_state.maximal_window_height); glfw_backend .window - .set_size( - (client_size_x - 1) as i32, - (client_size_y - 1) as i32 - ); + .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); } - if local_state.list_of_textures_changed || link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map) { + if local_state.list_of_textures_changed + || link.changes.contains(MumbleChanges::Position) + || link.changes.contains(MumbleChanges::Map) + { package_manager.tick( - &u2u_sender, - latest_time, - link, - JokoRenderer::get_z_near() + &u2u_sender, + latest_time, + link, + JokoRenderer::get_z_near(), ); local_state.list_of_textures_changed = false; } local_state.window_changed = false; } - + joko_renderer.tick(local_state.link.as_ref()); menu_panel.tick(&etx, local_state.link.as_ref()); - + // do the gui stuff now egui::Area::new("menu panel") .fixed_pos(menu_panel.pos) @@ -635,32 +734,46 @@ impl Jokolay { ui.style_mut().visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; ui.horizontal(|ui| { + //TODO: if any displayed, show an additional "hide all" ui.menu_button( egui::RichText::new("JKL") .size((MenuPanel::HEIGHT - 2.0) * menu_panel.ui_scaling_factor) .background_color(egui::Color32::TRANSPARENT), |ui| { ui.checkbox( - &mut menu_panel.show_window_manager, - "Show Window Manager", + &mut menu_panel.show_parameters_manager, + "Configuration", ); + ui.checkbox(&mut menu_panel.show_theme_window, "Themes"); ui.checkbox( &mut menu_panel.show_package_manager_window, - "Show Package Manager", + "Package Manager", ); ui.checkbox( &mut menu_panel.show_mumble_manager_window, - "Show Mumble Manager", - ); - ui.checkbox( - &mut menu_panel.show_theme_window, - "Show Theme Manager", + "Mumble Manager", ); ui.checkbox( &mut menu_panel.show_file_manager_window, - "Show File Manager", + "File Manager", ); - ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); + //ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); + if menu_panel.show_parameters_manager + || menu_panel.show_package_manager_window + || menu_panel.show_mumble_manager_window + || menu_panel.show_theme_window + || menu_panel.show_file_manager_window + || menu_panel.show_tracing_window + { + if ui.button("Close all panels").clicked() { + menu_panel.show_parameters_manager = false; + menu_panel.show_package_manager_window = false; + menu_panel.show_mumble_manager_window = false; + menu_panel.show_theme_window = false; + menu_panel.show_file_manager_window = false; + menu_panel.show_tracing_window = false; + } + } if ui.button("exit").clicked() { info!("exiting jokolay"); glfw_backend.window.set_should_close(true); @@ -668,22 +781,27 @@ impl Jokolay { }, ); package_manager.menu_ui( - &u2b_sender, - &u2u_sender, - ui, + &u2b_sender, + &u2u_sender, + ui, local_state.nb_running_tasks_on_back, local_state.nb_running_tasks_on_network, ); }); - } - ); - + }); + if let Some(link) = local_state.link.as_mut() { - mumble_gui(&u2b_sender, &etx, &mut menu_panel.show_mumble_manager_window, &mut local_state.editable_mumble, link); + mumble_gui( + &u2b_sender, + &etx, + &mut menu_panel.show_mumble_manager_window, + &mut local_state.editable_mumble, + link, + ); }; package_manager.gui( &u2b_sender, - &etx, + &etx, &mut menu_panel.show_package_manager_window, &local_state.import_status, &mut menu_panel.show_file_manager_window, @@ -691,7 +809,13 @@ impl Jokolay { ); JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); - frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); + ui_configuration.gui( + &u2b_sender, + &etx, + glfw_backend, + &mut menu_panel.show_parameters_manager, + &local_state.root_path, + ); // show notifications JokolayTracingLayer::show_notifications(&etx); @@ -726,16 +850,22 @@ impl Jokolay { } }; }*/ - + + let animation_time = if ui_configuration.display_parameters.animate { + latest_time + } else { + 0.0 + }; + joko_renderer.render_egui( etx.tessellate(shapes, etx.pixels_per_point()), textures_delta, glfw_backend.window_size_logical, - latest_time + animation_time, ); joko_renderer.present(); glfw_backend.window.swap_buffers(); - + nb_frames += 1; } drop(span_guard); @@ -750,9 +880,8 @@ pub fn start_jokolay() { panic!("failed to create jokolay_dir: {e:#?}"); } }; - let jokolay_path = get_jokolay_path().unwrap().as_std_path().to_path_buf(); - let working_path = jokolay_to_working_path(&jokolay_path); - + let jokolay_path = get_jokolay_path().unwrap(); + let log_file_flush_guard = match JokolayTracingLayer::install_tracing(&jokolay_dir) { Ok(g) => g, Err(e) => { @@ -773,7 +902,7 @@ pub fn start_jokolay() { ); } - match Jokolay::new(jokolay_dir.into(), working_path) { + match Jokolay::new(jokolay_dir.into(), jokolay_path) { Ok(jokolay) => { jokolay.enter_event_loop(); } @@ -834,7 +963,7 @@ pub struct MenuPanel { // show_settings_window: bool, show_package_manager_window: bool, show_mumble_manager_window: bool, - show_window_manager: bool, + show_parameters_manager: bool, show_file_manager_window: bool, } diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs index 07f1297..f543283 100644 --- a/crates/jokolay/src/app/mumble.rs +++ b/crates/jokolay/src/app/mumble.rs @@ -2,13 +2,12 @@ use egui::DragValue; use jmf::message::UIToBackMessage; use jokolink::MumbleLink; - pub fn mumble_gui( u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, + etx: &egui::Context, open: &mut bool, - editable_mumble: &mut bool, - link: &mut MumbleLink + editable_mumble: &mut bool, + link: &mut MumbleLink, ) { egui::Window::new("Mumble Manager") .open(open) @@ -26,7 +25,7 @@ pub fn mumble_gui( if *editable_mumble { ui.label( egui::RichText::new("Mumble is not live, values need to be manually updated.") - .color(egui::Color32::RED) + .color(egui::Color32::RED), ); editable_mumble_ui(ui, link); } else { @@ -78,10 +77,10 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { } else { ui.label("None"); } - + ui.end_row(); ui.label("compass"); - ui.horizontal(|ui|{ + ui.horizontal(|ui| { ui.add(DragValue::new(&mut link.compass_height)); ui.add(DragValue::new(&mut link.compass_width)); ui.add(DragValue::new(&mut link.compass_rotation)); @@ -97,7 +96,7 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.add(DragValue::new(&mut ratio)); ui.end_row(); ui.label("character"); - ui.horizontal(|ui|{ + ui.horizontal(|ui| { ui.label(&link.name); ui.label(format!("{:?}", link.race)); }); @@ -110,7 +109,7 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.add(DragValue::new(&mut link.map_type)); ui.end_row(); ui.label("world position"); - ui.horizontal(|ui|{ + ui.horizontal(|ui| { ui.add(DragValue::new(&mut link.map_center_x)); ui.add(DragValue::new(&mut link.map_center_y)); ui.add(DragValue::new(&mut link.map_scale)); @@ -150,7 +149,6 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { }); } - fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { egui::Grid::new("link grid") .num_columns(2) @@ -194,10 +192,10 @@ fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { } else { ui.label("None"); } - + ui.end_row(); ui.label("compass"); - ui.horizontal(|ui|{ + ui.horizontal(|ui| { ui.add(DragValue::new(&mut dummy_link.compass_height)); ui.add(DragValue::new(&mut dummy_link.compass_width)); ui.add(DragValue::new(&mut dummy_link.compass_rotation)); diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs new file mode 100644 index 0000000..51b9c87 --- /dev/null +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -0,0 +1,136 @@ +use egui_window_glfw_passthrough::GlfwBackend; + +use jmf::message::UIToBackMessage; +use serde::{Deserialize, Serialize}; + +pub const UI_PARAMETERS_FILE_NAME: &str = "ui.toml"; + +#[derive(Serialize, Deserialize)] +pub struct JokolayUIParameters { + pub visible_borders: bool, + pub animate: bool, + pub editable_path: String, + //TODO: folder path for custom work directory + //save configuration into a file + make backups of configuration +} + +pub struct JokolayUIConfiguration { + pub fps_last_reset: f64, + pub frame_count: u32, + pub total_frame_count: u32, + pub average_fps: u32, + pub display_parameters: JokolayUIParameters, +} + +impl JokolayUIConfiguration { + pub fn new(current_time: f64, editable_path: String) -> Self { + Self { + fps_last_reset: current_time, + frame_count: 0, + total_frame_count: 0, + average_fps: 0, + display_parameters: JokolayUIParameters { + visible_borders: false, + animate: true, + editable_path, + }, + } + } + + pub fn tick(&mut self, current_time: f64) { + self.total_frame_count += 1; + self.frame_count += 1; + if current_time - self.fps_last_reset > 1.0 { + self.average_fps = self.frame_count; + self.frame_count = 0; + self.fps_last_reset = current_time; + } + } + + pub fn gui( + &mut self, + u2b_sender: &std::sync::mpsc::Sender, + etx: &egui::Context, + wb: &mut GlfwBackend, + open: &mut bool, + root_path: &std::path::PathBuf, + ) { + let mut need_to_save = false; + egui::Window::new("Configuration") + .open(open) + .show(etx, |ui| { + egui::Grid::new("frame details") + .num_columns(2) + .show(ui, |ui| { + ui.label("FPS"); + ui.label(&format!("{}", self.average_fps)); + ui.end_row(); + ui.label("Frame count"); + ui.label(&format!("{}", self.total_frame_count)); + ui.end_row(); + ui.label("Overlay position"); + ui.label(&format!( + "x: {}; y: {}", + wb.window_position[0], wb.window_position[1] + )); + ui.end_row(); + ui.label("Overlay size"); + ui.label(&format!( + "width: {}, height: {}", + wb.framebuffer_size_physical[0], wb.framebuffer_size_physical[1] + )); + ui.end_row(); + + ui.label("Decorations (borders)") + .on_hover_text("Should the jokolay overlay window boreders be displayed"); + let is_decorated = wb.window.is_decorated(); + ui.horizontal(|ui|{ + let result = is_decorated; + if ui.selectable_label(result, "Visible").clicked() { + wb.window.set_decorated(true); + self.display_parameters.visible_borders = true; + need_to_save = true; + } + if ui.selectable_label(!result, "Hidden").clicked() { + wb.window.set_decorated(false); + self.display_parameters.visible_borders = false; + need_to_save = true; + } + }); + ui.end_row(); + + ui.label("Animation") + .on_hover_text("As an example, this toggle the animation of trails"); + ui.horizontal(|ui|{ + if ui.selectable_label(self.display_parameters.animate, "Enable").clicked() { + self.display_parameters.animate = true; + need_to_save = true; + } + if ui.selectable_label(!self.display_parameters.animate, "Disable").clicked() { + self.display_parameters.animate = false; + need_to_save = true; + } + }); + ui.end_row(); + ui.label("All files and preferences are saved into:"); + ui.label(root_path.to_str().unwrap()); + ui.end_row(); + + ui.label("Editable package directory") + .on_hover_text_at_pointer("This is where you can manually edit a package and have it regularly imported for validation."); + ui.text_edit_singleline(&mut self.display_parameters.editable_path); + }); + }); + if need_to_save { + match toml::to_string(&self.display_parameters) { + Ok(serialized_string) => { + let _ = + u2b_sender.send(UIToBackMessage::SaveUIConfiguration(serialized_string)); + } + Err(e) => { + tracing::error!(?e, "failed to serialize UI configuration"); + } + } + } + } +} diff --git a/crates/jokolay/src/app/wm.rs b/crates/jokolay/src/app/wm.rs deleted file mode 100644 index 4b99466..0000000 --- a/crates/jokolay/src/app/wm.rs +++ /dev/null @@ -1,75 +0,0 @@ -use egui_window_glfw_passthrough::GlfwBackend; - -pub struct WindowStatistics { - pub fps_last_reset: f64, - pub frame_count: u32, - pub total_frame_count: u32, - pub average_fps: u32, -} - -impl WindowStatistics { - pub fn new(current_time: f64) -> Self { - Self { - fps_last_reset: current_time, - frame_count: 0, - total_frame_count: 0, - average_fps: 0, - } - } - - pub fn tick(&mut self, current_time: f64) { - self.total_frame_count += 1; - self.frame_count += 1; - if current_time - self.fps_last_reset > 1.0 { - self.average_fps = self.frame_count; - self.frame_count = 0; - self.fps_last_reset = current_time; - } - } - - pub fn gui(&mut self, etx: &egui::Context, wb: &mut GlfwBackend, open: &mut bool) { - egui::Window::new("Window Manager") - .open(open) - .show(etx, |ui| { - egui::Grid::new("frame details") - .num_columns(2) - .show(ui, |ui| { - ui.label("fps"); - ui.label(&format!("{}", self.average_fps)); - ui.end_row(); - ui.label("frame count"); - ui.label(&format!("{}", self.total_frame_count)); - ui.end_row(); - ui.label("jokolay pos"); - ui.label(&format!( - "x: {}; y: {}", - wb.window_position[0], wb.window_position[1] - )); - ui.end_row(); - ui.label("jokolay size"); - ui.label(&format!( - "width: {}, height: {}", - wb.framebuffer_size_physical[0], wb.framebuffer_size_physical[1] - )); - ui.end_row(); - ui.label("decorations (borders)"); - let is_decorated = wb.window.is_decorated(); - let mut result = is_decorated; - if ui - .checkbox( - &mut result, - if is_decorated { - "borders visible" - } else { - "borders hidden" - }, - ) - .changed() - { - wb.window.set_decorated(result); - } - ui.end_row(); - }); - }); - } -} diff --git a/crates/jokolay/src/lib.rs b/crates/jokolay/src/lib.rs index e3aded2..5c4ccf5 100644 --- a/crates/jokolay/src/lib.rs +++ b/crates/jokolay/src/lib.rs @@ -2,4 +2,3 @@ mod app; mod manager; pub use app::start_jokolay; - diff --git a/crates/jokolay/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs index f49efea..769f3cf 100644 --- a/crates/jokolay/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -53,7 +53,8 @@ impl ThemeManager { const DEFAULT_THEME_NAME: &'static str = "default"; const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; pub fn new(jokolay_dir: Arc) -> Result { - jokolay_dir.create_dir_all(Self::THEME_MANAGER_DIR_NAME) + jokolay_dir + .create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() .wrap_err("failed to create theme manager dir")?; let theme_manager_dir: Arc = jokolay_dir @@ -61,7 +62,8 @@ impl ThemeManager { .into_diagnostic() .wrap_err("failed to open theme_manager dir")? .into(); - theme_manager_dir.create_dir_all(Self::THEMES_DIR_NAME) + theme_manager_dir + .create_dir_all(Self::THEMES_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; let themes_dir: Arc = theme_manager_dir @@ -70,7 +72,8 @@ impl ThemeManager { .wrap_err("failed to open themes dir")? .into(); - theme_manager_dir.create_dir_all(Self::FONTS_DIR_NAME) + theme_manager_dir + .create_dir_all(Self::FONTS_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; let fonts_dir: Arc = theme_manager_dir @@ -168,17 +171,19 @@ impl ThemeManager { } } if !theme_manager_dir.exists(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) { - theme_manager_dir.write( - format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME), - serde_json::to_vec_pretty(&ThemeManagerConfig::default()) - .into_diagnostic() - .wrap_err("failed to serialize theme manager config")?, - ) - .into_diagnostic() - .wrap_err("failed to write theme manager config to the theme manager dir")?; + theme_manager_dir + .write( + format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME), + serde_json::to_vec_pretty(&ThemeManagerConfig::default()) + .into_diagnostic() + .wrap_err("failed to serialize theme manager config")?, + ) + .into_diagnostic() + .wrap_err("failed to write theme manager config to the theme manager dir")?; } let config = serde_json::from_str( - &theme_manager_dir.read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) + &theme_manager_dir + .read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) .into_diagnostic() .wrap_err("failed to read theme manager config file")?, ) @@ -209,7 +214,7 @@ impl ThemeManager { error!(%self.config.default_theme, "failed to find the default theme in the loaded themes :("); } } - + pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { egui::Window::new("Theme Manager") .open(open) diff --git a/crates/jokolay/src/manager/trace/mod.rs b/crates/jokolay/src/manager/trace/mod.rs index d3e0f4f..7259647 100644 --- a/crates/jokolay/src/manager/trace/mod.rs +++ b/crates/jokolay/src/manager/trace/mod.rs @@ -11,7 +11,7 @@ pub struct JokolayTracingLayer; static JKL_TRACING_DATA: OnceLock> = OnceLock::new(); impl JokolayTracingLayer { - pub fn install_tracing<'l> ( + pub fn install_tracing<'l>( jokolay_dir: &'l Dir, ) -> Result { use tracing_subscriber::prelude::*; @@ -21,8 +21,7 @@ impl JokolayTracingLayer { use std::fs::File; use std::io::Write; let backtrace = std::backtrace::Backtrace::force_capture(); - let output = - if let Some(string) = info.payload().downcast_ref::() { + let output = if let Some(string) = info.payload().downcast_ref::() { format!("{string}") } else if let Some(str) = info.payload().downcast_ref::<&'static str>() { format!("{str}") @@ -36,7 +35,6 @@ impl JokolayTracingLayer { writeln!(&mut w, "Backtrace: {backtrace:}").unwrap(); })); - // get the log level let filter_layer = EnvFilter::try_from_env("JOKOLAY_LOG") .or_else(|_| EnvFilter::try_new("info,wgpu=warn,naga=warn")) diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs index efb280f..cc8b3a7 100644 --- a/crates/jokolink/src/lib.rs +++ b/crates/jokolink/src/lib.rs @@ -42,7 +42,6 @@ pub struct MumbleManager { backend: MumblePlatformImpl, /// latest mumble link link: MumbleLink, - } impl MumbleManager { pub fn new(name: &str, _jokolay_window_id: Option) -> Result { @@ -109,14 +108,8 @@ impl MumbleManager { if new_link.map_id != cml.context.map_id { changes.insert(MumbleChanges::Map); } - let client_pos = IVec2::new( - cml.context.client_pos[0], - cml.context.client_pos[1], - ); - let client_size = UVec2::new( - cml.context.client_size[0], - cml.context.client_size[1], - ); + let client_pos = IVec2::new(cml.context.client_pos[0], cml.context.client_pos[1]); + let client_size = UVec2::new(cml.context.client_size[0], cml.context.client_size[1]); if new_link.client_pos != client_pos { changes.insert(MumbleChanges::WindowPosition); @@ -181,4 +174,3 @@ impl MumbleManager { }) } } - diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs index 71fb27c..560ef18 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/jokolink/src/mumble/mod.rs @@ -115,7 +115,7 @@ impl Default for MumbleLink { dpi: Default::default(), dpi_scaling: 96, client_pos: Default::default(), - client_size: UVec2{x: 1024, y: 768}, + client_size: UVec2 { x: 1024, y: 768 }, changes: Default::default(), } } From 2836f6b48c48aa504e571089f6f8b48996845374 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 21 Apr 2024 17:00:02 +0200 Subject: [PATCH 34/54] add missing requirements --- Cargo.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a843c1e..65f6fcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1433,6 +1433,7 @@ dependencies = [ "tracing", "url", "uuid", + "walkdir", "xot", "zip", ] @@ -1506,6 +1507,7 @@ version = "0.2.1" dependencies = [ "cap-directories", "cap-std", + "directories-next", "egui", "egui_extras", "egui_window_glfw_passthrough", @@ -1523,6 +1525,7 @@ dependencies = [ "serde", "serde_json", "smol_str", + "toml", "tracing", "tracing-appender", "tracing-subscriber", @@ -2060,7 +2063,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -2351,6 +2354,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2411,6 +2423,15 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2715,11 +2736,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.12", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2729,7 +2765,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.6", ] [[package]] @@ -2967,6 +3016,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3273,6 +3332,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +dependencies = [ + "memchr", +] + [[package]] name = "winx" version = "0.36.3" From 38db9cc34e5c3af034850c80e5844733c1435a1b Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 21 Apr 2024 22:11:24 +0200 Subject: [PATCH 35/54] satisfy clippy --- crates/joko_core/src/lib.rs | 5 +- crates/joko_core/src/task/mod.rs | 6 +- crates/joko_package/src/io/deserialize.rs | 93 +++++----- crates/joko_package/src/io/mod.rs | 1 - crates/joko_package/src/io/serialize.rs | 9 +- .../joko_package/src/manager/pack/active.rs | 2 +- .../src/manager/pack/category_selection.rs | 31 ++-- .../src/manager/pack/file_selection.rs | 12 +- .../joko_package/src/manager/pack/loaded.rs | 34 ++-- crates/joko_package/src/manager/pack/mod.rs | 1 - crates/joko_package/src/manager/package.rs | 162 +++++++++--------- crates/joko_package/src/message.rs | 4 +- crates/joko_package_models/src/category.rs | 56 +++--- crates/joko_package_models/src/package.rs | 33 ++-- crates/joko_package_models/src/route.rs | 6 +- crates/joko_render/src/billboard.rs | 3 +- crates/jokoapi/src/end_point/races/mod.rs | 3 +- crates/jokolay/src/app/init.rs | 5 +- crates/jokolay/src/app/mod.rs | 89 +++++----- crates/jokolay/src/app/ui_parameters.rs | 2 +- crates/jokolay/src/manager/theme/mod.rs | 4 +- crates/jokolay/src/manager/trace/mod.rs | 8 +- crates/jokolink/src/linux/mod.rs | 1 + 23 files changed, 277 insertions(+), 293 deletions(-) diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 5a24a03..a0afaf8 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -25,10 +25,9 @@ pub struct RelativePath(SmolStr); #[allow(unused)] impl RelativePath { pub fn normalize(path: &str) -> String { - let normalized_slash = path.replace("\\", "/"); + let normalized_slash = path.replace('\\', "/"); let trimmed_path = normalized_slash.trim_start_matches('/'); - let lower_case = trimmed_path.to_lowercase(); - lower_case + trimmed_path.to_lowercase() } pub fn join_str(&self, path: &str) -> Self { diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs index a955f3a..3412182 100644 --- a/crates/joko_core/src/task/mod.rs +++ b/crates/joko_core/src/task/mod.rs @@ -42,10 +42,10 @@ where let thread_task = std::thread::spawn(move || { while let Ok(elt) = th_task_receiver.recv() { let _guard = scopeguard::guard(0, |_| { - nb_sender.send(-1); + let _ = nb_sender.send(-1); }); - nb_sender.send(1); - th_result_sender.send(f(elt)); + let _ = nb_sender.send(1); + let _ = th_result_sender.send(f(elt)); } }); let thread_nb = std::thread::spawn(move || { diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 235a703..32047b6 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -144,12 +144,12 @@ fn recursive_walk_dir_and_read_images_and_tbins( pack.register_texture(name, &path, bytes); } else if name.ends_with(".trl") { if let Some(tbs) = parse_tbin_from_slice(&bytes) { - let is_closed: bool = tbs.closed; + /*let is_closed: bool = tbs.closed; if is_closed { if tbs.iso_x {} if tbs.iso_y {} if tbs.iso_z {} - } + }*/ pack.tbins.insert(path, tbs.tbin); } else { info!("invalid tbin: {path}"); @@ -226,7 +226,8 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let mut iso_z = false; let mut closed = false; let mut resulting_nodes: Vec = Vec::new(); - if nodes.len() > 0 { + if !nodes.is_empty() { + //at least the first exist and can be accessed let ref_node = nodes[0]; let mut c_iso_x = true; let mut c_iso_y = true; @@ -375,7 +376,7 @@ fn parse_categories_recursive( } } - sources.insert(guid.clone(), source_file_uuid.clone()); + sources.insert(guid, *source_file_uuid); first_pass_categories.insert( full_category_name.clone(), RawCategory { @@ -424,10 +425,9 @@ fn parse_categories_from_normalized_file( let mut categories: OrderedHashMap = Default::default(); if od.name() == xot_names.overlay_data { parse_category_categories_xml_recursive( - &file_name, + file_name, &tree, tree.children(overlay_data_node), - pack, &mut categories, &xot_names, None, @@ -504,12 +504,12 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor .get_attribute(names._source_file_name) .unwrap_or_default(), ); - let source_file_uuid = if opt_source_file_uuid.is_err() { + let source_file_uuid = if let Ok(uuid) = opt_source_file_uuid { + uuid + } else { error!("Package corrupted, invalid source file uuid"); //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); Uuid::new_v4() - } else { - opt_source_file_uuid.unwrap() }; if let Some(source_file_name) = @@ -522,9 +522,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor } //There is no file name, only an uuid to register - target - .active_source_files - .insert(source_file_uuid.clone(), true); + target.active_source_files.insert(source_file_uuid, true); if child_element.name() == names.route { debug!("Found a route in core pack {:?}", child_element); @@ -534,7 +532,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor &poi_node, child_element, &full_category_name, - source_file_uuid.clone(), + source_file_uuid, ); if let Some(route) = route { target.register_route(route)?; @@ -569,7 +567,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor map_xml_str, child_element ))); } - let category_uuid = opt_cat_uuid.unwrap().clone(); //categories MUST exist, they have already been parsed + let category_uuid = opt_cat_uuid.unwrap(); //categories MUST exist, they have already been parsed let guid = raw_uid .and_then(|guid| { let mut buffer = [0u8; 20]; @@ -609,12 +607,12 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor position: [xpos, ypos, zpos].into(), map_id, category: full_category_name.clone(), - parent: category_uuid.clone(), + parent: *category_uuid, attrs: ca, guid, source_file_uuid, }; - target.register_marker(full_category_name, marker); + target.register_marker(full_category_name, marker)?; } else if child_element.name() == names.trail { debug!("Found a trail in core pack {:?}", child_element); let map_id = child_element @@ -626,7 +624,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor let trail = Trail { category: full_category_name.clone(), - parent: category_uuid.clone(), + parent: *category_uuid, map_id, props: ca, guid, @@ -644,10 +642,9 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor // a temporary recursive function to parse the marker category tree. fn parse_category_categories_xml_recursive( - file_name: &String, + _file_name: &String, //meant for future implementation of source file definition for categories tree: &Xot, tags: impl Iterator, - pack: &mut PackCore, cats: &mut OrderedHashMap, names: &XotAttributeNameIDs, parent_uuid: Option, @@ -712,10 +709,9 @@ fn parse_category_categories_xml_recursive( )); } parse_category_categories_xml_recursive( - file_name, + _file_name, tree, tree.children(tag), - pack, cats, names, Some(guid), @@ -727,7 +723,7 @@ fn parse_category_categories_xml_recursive( } else { let c = Category { guid, - parent: parent_uuid.clone(), + parent: parent_uuid, display_name: display_name.to_string(), relative_category_name: relative_category_name.to_string(), full_category_name: full_category_name.clone(), @@ -740,10 +736,9 @@ fn parse_category_categories_xml_recursive( cats.back_mut().unwrap() }; parse_category_categories_xml_recursive( - file_name, + _file_name, tree, tree.children(tag), - pack, &mut current_category.children, names, Some(guid), @@ -765,7 +760,7 @@ pub(crate) fn get_pack_from_taco_zip( extract_temporary_path: &std::path::PathBuf, ) -> Result { let mut taco_zip = vec![]; - std::fs::File::open(&input_path) + std::fs::File::open(input_path) .into_diagnostic()? .read_to_end(&mut taco_zip) .into_diagnostic()?; @@ -815,7 +810,7 @@ fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result Result Result = OrderedHashMap::new(); - sources.insert(guid.clone(), source_file_uuid.clone()); + sources.insert(guid, source_file_uuid); first_pass_categories.insert( full_category_name.clone(), RawCategory { @@ -1027,9 +1022,7 @@ fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result Result Result Result Option { let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(poi_element, &names); + common_attributes.update_common_attributes_from_element(poi_element, names); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); @@ -1262,8 +1255,8 @@ fn parse_marker( Some(Marker { position: [xpos, ypos, zpos].into(), map_id, - category: category_name.clone(), - parent: category_uuid.clone(), + category: category_name.to_owned(), + parent: *category_uuid, attrs: common_attributes, guid, source_file_uuid, @@ -1319,7 +1312,7 @@ fn parse_route( tree: &Xot, route_node: &Node, route_element: &Element, - category_name: &String, + category_name: &str, source_file_uuid: Uuid, ) -> Option { let mut path: Vec = Vec::new(); @@ -1350,7 +1343,7 @@ fn parse_route( info!("route element is missing name: {route_element:?}"); return None; } - let mut category: String = category_name.clone(); + let mut category: String = category_name.to_owned(); let mut category_uuid: Option = parse_optional_guid(names, route_element); let mut map_id: Option = route_element .get_attribute(names.map_id) @@ -1361,7 +1354,7 @@ fn parse_route( None => continue, }; if child.name() == names.poi { - let marker = parse_position(&names, child); + let marker = parse_position(names, child); path.push(marker); if category.is_empty() { if let Some(cat) = child.get_attribute(names.category) { @@ -1369,7 +1362,7 @@ fn parse_route( } } if category_uuid.is_none() { - category_uuid = parse_optional_guid(names, &child) + category_uuid = parse_optional_guid(names, child) } if map_id.is_none() { if let Some(node_map_id) = child @@ -1406,7 +1399,7 @@ fn parse_route( reset_range: reset_range.unwrap_or(0.0), map_id: map_id.unwrap(), name: name.unwrap().into(), - guid: parse_guid(names, &route_element), + guid: parse_guid(names, route_element), source_file_uuid, }) } @@ -1416,14 +1409,14 @@ fn parse_trail( names: &XotAttributeNameIDs, trail_element: &Element, guid: Uuid, - category_name: &String, + category_name: &str, category_uuid: &Uuid, source_file_uuid: Uuid, ) -> Option { //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(trail_element, &names); + common_attributes.update_common_attributes_from_element(trail_element, names); if let Some(tex) = common_attributes.get_texture() { if !pack.textures.contains_key(tex) { @@ -1432,6 +1425,8 @@ fn parse_trail( } } + #[allow(clippy::manual_map)] + // This is not exactly a manual map, we register something more in pack on some condition: a missing trail. if let Some(map_id) = trail_element .get_attribute(names.trail_data) .and_then(|trail_data| { @@ -1446,8 +1441,8 @@ fn parse_trail( }) { Some(Trail { - category: category_name.clone(), - parent: category_uuid.clone(), + category: category_name.to_owned(), + parent: *category_uuid, map_id, props: common_attributes, guid, diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs index 7c6d495..310a6bb 100644 --- a/crates/joko_package/src/io/mod.rs +++ b/crates/joko_package/src/io/mod.rs @@ -3,7 +3,6 @@ mod deserialize; mod error; -mod export; mod serialize; pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_normalized_folder}; diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index 902788e..5536ffe 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -4,6 +4,7 @@ use crate::{ }; use base64::Engine; use cap_std::fs_utf8::Dir; +use glam::Vec3; use joko_package_models::{ attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, }; @@ -143,8 +144,8 @@ pub(crate) fn save_pack_texture_to_dir( miette::miette!("failed to create parent dir of tbin: {tbin_path}") })?; } - let mut bytes: Vec = vec![]; - bytes.reserve(8 + tbin.nodes.len() * 12); + let mut bytes: Vec = + Vec::with_capacity(8 + tbin.nodes.len() * std::mem::size_of::()); bytes.extend_from_slice(&tbin.version.to_ne_bytes()); bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); for node in &tbin.nodes { @@ -175,7 +176,7 @@ fn recursive_cat_serializer( { let ele = tree.element_mut(cat_node).unwrap(); ele.set_attribute(names.display_name, &cat.display_name); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(cat.guid)); // let cat_name = tree.add_name(cat_name); ele.set_attribute(names.name, &cat.relative_category_name); // no point in serializing default values @@ -243,7 +244,7 @@ fn serialize_route_to_element( ); for pos in &route.path { let child = tree.new_element(names.poi); - tree.append(route_node, child); + tree.append(route_node, child).into_diagnostic()?; let child_elt = tree.element_mut(child).unwrap(); child_elt.set_attribute(names.xpos, format!("{}", pos.x)); child_elt.set_attribute(names.ypos, format!("{}", pos.y)); diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs index 4b61c28..4f3fd41 100644 --- a/crates/joko_package/src/manager/pack/active.rs +++ b/crates/joko_package/src/manager/pack/active.rs @@ -283,7 +283,7 @@ impl ActiveTrail { #[derive(Default, Clone)] pub(crate) struct CurrentMapData { /// the map to which the current map data belongs to - pub map_id: u32, + //pub map_id: u32, //pub active_elements: HashSet, /// The textures that are being used by the markers, so must be kept alive by this hashmap pub active_textures: OrderedHashMap, diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs index 6febf41..e7cc173 100644 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -34,8 +34,8 @@ impl<'a> SelectedCategoryManager { ) -> Self { let mut list_of_enabled_categories = Default::default(); CategorySelection::get_list_of_enabled_categories( - &selected_categories, - &categories, + selected_categories, + categories, &mut list_of_enabled_categories, &Default::default(), ); @@ -44,6 +44,7 @@ impl<'a> SelectedCategoryManager { data: list_of_enabled_categories, } } + #[allow(dead_code)] pub fn cloned_data(&self) -> OrderedHashMap { self.data.clone() } @@ -53,6 +54,7 @@ impl<'a> SelectedCategoryManager { pub fn get(&self, key: &Uuid) -> &CommonAttributes { self.data.get(key).unwrap() } + #[allow(dead_code)] pub fn len(&self) -> usize { self.data.len() } @@ -95,7 +97,7 @@ impl CategorySelection { uuid: Uuid, ) -> Option<&mut CategorySelection> { if selection.is_empty() { - return None; + None } else { for cat in selection.values_mut() { if cat.uuid == uuid { @@ -105,22 +107,23 @@ impl CategorySelection { return Some(res); } } - return None; + None } } + #[allow(dead_code)] pub fn recursive_populate_guids( selection: &mut OrderedHashMap, entities_parents: &mut HashMap, parent_uuid: Option, ) { - for (cat_name, cat) in selection.iter_mut() { + for cat in selection.values_mut() { if cat.uuid.is_nil() { cat.uuid = Uuid::new_v4(); } - cat.parent = parent_uuid.clone(); + cat.parent = parent_uuid; Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); - if parent_uuid.is_some() { - entities_parents.insert(cat.uuid, parent_uuid.unwrap().clone()); + if let Some(parent_uuid) = parent_uuid { + entities_parents.insert(cat.uuid, parent_uuid); } //assert!(cat.guid.len() > 0); } @@ -156,7 +159,7 @@ impl CategorySelection { status: bool, ) -> bool { if selection.is_empty() { - return false; + false } else { for cat in selection.values_mut() { if cat.separator { @@ -170,7 +173,7 @@ impl CategorySelection { return true; } } - return false; + false } } pub fn recursive_set_all( @@ -205,7 +208,7 @@ impl CategorySelection { is_active = true; } } - return is_active; + is_active } fn context_menu( @@ -233,7 +236,7 @@ impl CategorySelection { pub fn recursive_selection_ui( u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, + _u2u_sender: &std::sync::mpsc::Sender, selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, @@ -244,7 +247,7 @@ impl CategorySelection { return; } egui::ScrollArea::vertical().show(ui, |ui| { - for (name, cat) in selection.iter_mut() { + for cat in selection.values_mut() { if !cat.is_active && show_only_active && !cat.separator { continue; } @@ -278,7 +281,7 @@ impl CategorySelection { ui.menu_button(label, |ui: &mut egui::Ui| { Self::recursive_selection_ui( u2b_sender, - u2u_sender, + _u2u_sender, &mut cat.children, ui, is_dirty, diff --git a/crates/joko_package/src/manager/pack/file_selection.rs b/crates/joko_package/src/manager/pack/file_selection.rs index 52d6a7c..c3ddc03 100644 --- a/crates/joko_package/src/manager/pack/file_selection.rs +++ b/crates/joko_package/src/manager/pack/file_selection.rs @@ -5,7 +5,7 @@ use uuid::Uuid; pub struct SelectedFileManager { data: BTreeMap, } -impl<'a> SelectedFileManager { +impl SelectedFileManager { pub fn new( selected_files: &BTreeMap, pack_source_files: &BTreeMap, @@ -13,9 +13,9 @@ impl<'a> SelectedFileManager { ) -> Self { let mut list_of_enabled_files: BTreeMap = Default::default(); SelectedFileManager::recursive_get_full_names( - &selected_files, - &pack_source_files, - ¤tly_used_files, + selected_files, + pack_source_files, + currently_used_files, &mut list_of_enabled_files, ); Self { @@ -29,9 +29,10 @@ impl<'a> SelectedFileManager { list_of_enabled_files: &mut BTreeMap, ) { for (key, v) in currently_used_files.iter() { - list_of_enabled_files.insert(key.clone(), *v); + list_of_enabled_files.insert(*key, *v); } } + #[allow(dead_code)] pub fn cloned_data(&self) -> BTreeMap { self.data.clone() } @@ -39,6 +40,7 @@ impl<'a> SelectedFileManager { let default = false; self.data.is_empty() || *self.data.get(source_file_uuid).unwrap_or(&default) } + #[allow(dead_code)] pub fn len(&self) -> usize { self.data.len() } diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index d70352b..88d141a 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -98,7 +98,7 @@ pub struct LoadedPackTexture { selectable_categories: OrderedHashMap, current_map_data: CurrentMapData, activation_data: ActivationData, - active_elements: HashSet, //which are the active elements (loaded) + //active_elements: HashSet, //which are the active elements (loaded) _is_dirty: bool, } @@ -116,7 +116,7 @@ impl PackTasks { || self.save_data_task.lock().unwrap().is_running() } pub fn count(&self) -> i32 { - 0 + self.save_texture_task.lock().unwrap().count() + self.save_texture_task.lock().unwrap().count() + self.save_data_task.lock().unwrap().count() + self.load_all_packs_task.lock().unwrap().count() } @@ -154,6 +154,7 @@ impl PackTasks { self.load_all_packs_task.lock().unwrap().recv().unwrap() } + #[allow(dead_code, unused)] fn change_map( &self, pack: &mut LoadedPackData, @@ -162,7 +163,7 @@ impl PackTasks { currently_used_files: &BTreeMap, ) { //TODO - //self.load_map_task.lock().unwrap().send(pack); + unimplemented!(); } fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<()> { @@ -274,7 +275,7 @@ impl LoadedPackData { }) .flatten() .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&pack); + let cs = CategorySelection::default_from_pack_core(pack); match serde_json::to_string_pretty(&cs) { Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { @@ -384,10 +385,11 @@ impl LoadedPackData { self._is_dirty } + #[allow(clippy::too_many_arguments)] pub(crate) fn tick( &mut self, b2u_sender: &std::sync::mpsc::Sender, - loop_index: u128, + _loop_index: u128, link: &MumbleLink, currently_used_files: &BTreeMap, list_of_active_or_selected_elements_changed: bool, @@ -432,19 +434,18 @@ impl LoadedPackData { let selected_files_manager = SelectedFileManager::new( &self.selected_files, &self.source_files, - ¤tly_used_files, + currently_used_files, ); debug!("Start loading markers"); let mut nb_markers_attempt = 0; let mut nb_markers_loaded = 0; - for (_index, marker) in self + for marker in self .maps .get(&link.map_id) .unwrap_or(&Default::default()) .markers .values() - .enumerate() { nb_markers_attempt += 1; if selected_files_manager.is_selected(&marker.source_file_uuid) { @@ -528,13 +529,12 @@ impl LoadedPackData { debug!("Start loading trails"); let mut nb_trails_attempt = 0; let mut nb_trails_loaded = 0; - for (_index, trail) in self + for trail in self .maps .get(&link.map_id) .unwrap_or(&Default::default()) .trails .values() - .enumerate() { nb_trails_attempt += 1; if selected_files_manager.is_selected(&trail.source_file_uuid) { @@ -610,7 +610,7 @@ impl LoadedPackTexture { ui, &mut self._is_dirty, show_only_active, - &import_quality_report, + import_quality_report, ); }); if self._is_dirty { @@ -628,7 +628,7 @@ impl LoadedPackTexture { link: &MumbleLink, //next_on_screen: &mut HashSet, z_near: f32, - tasks: &PackTasks, + _tasks: &PackTasks, ) { tracing::trace!( "LoadedPackTexture.tick: {} {}-{} {}-{}", @@ -788,16 +788,16 @@ impl LoadedPackTexture { } } -pub fn jokolay_to_editable_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { +pub fn jokolay_to_editable_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { let marker_manager_path = jokolay_to_marker_path(jokolay_path); marker_manager_path.join(EDITABLE_PACKAGE_NAME) } -pub fn jokolay_to_extract_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { +pub fn jokolay_to_extract_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { jokolay_path.join(EXTRACT_DIRECTORY_NAME) } -pub fn jokolay_to_marker_path(jokolay_path: &std::path::PathBuf) -> std::path::PathBuf { +pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { jokolay_path .join(PACKAGE_MANAGER_DIRECTORY_NAME) .join(PACKAGES_DIRECTORY_NAME) @@ -975,9 +975,9 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo _is_dirty: false, activation_data, dir: Arc::clone(&pack_dir), - name: name, + name, tbins: core.tbins, - active_elements: Default::default(), + //active_elements: Default::default(), source_files: core.active_source_files, }; let report = core.report; diff --git a/crates/joko_package/src/manager/pack/mod.rs b/crates/joko_package/src/manager/pack/mod.rs index cd44dd6..908a692 100644 --- a/crates/joko_package/src/manager/pack/mod.rs +++ b/crates/joko_package/src/manager/pack/mod.rs @@ -1,7 +1,6 @@ pub mod activation; pub mod active; pub mod category_selection; -pub mod dirty; pub mod file_selection; pub mod import; pub mod loaded; diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package.rs index b559ca5..2891714 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package.rs @@ -152,13 +152,16 @@ impl PackageDataManager { //avoid duplicate, redundancy or loop continue; } - next_gen.push(p.clone()); + next_gen.push(*p); } } let to_insert = std::mem::replace(&mut current_generation, next_gen); result.extend(to_insert); } - unreachable!("The loop should always return"); + #[allow(unreachable_code)] // sillyness of some tools + { + unreachable!("The loop should always return") + } } pub fn get_active_elements_parents( @@ -190,80 +193,75 @@ impl PackageDataManager { let mut currently_used_files: BTreeMap = Default::default(); let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - match link { - Some(link) => { - //TODO: how to save/load the active files ? - let mut have_used_files_list_changed = false; - let map_changed = self.current_map_id != link.map_id; - self.current_map_id = link.map_id; - for pack in self.packs.values_mut() { - if let Some(current_map) = pack.maps.get(&link.map_id) { - for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) - { - currently_used_files.insert( - marker.source_file_uuid.clone(), - *self - .currently_used_files - .get(&marker.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } + if let Some(link) = link { + //TODO: how to save/load the active files ? + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { + currently_used_files.insert( + marker.source_file_uuid, + *self + .currently_used_files + .get(&marker.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); } - for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) - { - currently_used_files.insert( - trail.source_file_uuid.clone(), - *self - .currently_used_files - .get(&trail.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { + currently_used_files.insert( + trail.source_file_uuid, + *self + .currently_used_files + .get(&trail.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); } } } - let tasks = &self.tasks; - for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); - tasks.save_data(pack, pack.is_dirty()); - pack.tick( - &b2u_sender, - loop_index, - link, - ¤tly_used_files, - have_used_files_list_changed || choice_of_category_changed, - map_changed, - &tasks, - &mut categories_and_elements_to_be_loaded, - ); - std::mem::drop(span_guard); - } - if map_changed { - self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let _ = b2u_sender.send(BackToUIMessage::ActiveElements( - self.loaded_elements.clone(), - )); - } - if map_changed || have_used_files_list_changed || choice_of_category_changed { - //there is no point in sending a new list if nothing changed - let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles( - currently_used_files.clone(), - )); - self.currently_used_files = currently_used_files; - let _ = b2u_sender.send(BackToUIMessage::TextureSwapChain); - } } - None => {} - }; + let tasks = &self.tasks; + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + b2u_sender, + loop_index, + link, + ¤tly_used_files, + have_used_files_list_changed || choice_of_category_changed, + map_changed, + tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + if map_changed { + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + let _ = b2u_sender.send(BackToUIMessage::ActiveElements( + self.loaded_elements.clone(), + )); + } + if map_changed || have_used_files_list_changed || choice_of_category_changed { + //there is no point in sending a new list if nothing changed + let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles( + currently_used_files.clone(), + )); + self.currently_used_files = currently_used_files; + let _ = b2u_sender.send(BackToUIMessage::TextureSwapChain); + } + } } fn delete_packs(&mut self, to_delete: Vec) { @@ -282,7 +280,7 @@ impl PackageDataManager { self.tasks .save_report(Arc::clone(&data_pack.dir), report, true); self.tasks.save_data(&mut data_pack, true); - let mut uuid_to_insert = data_pack.uuid.clone(); + let mut uuid_to_insert = data_pack.uuid; while self.packs.contains_key(&uuid_to_insert) { //collision avoidance trace!( @@ -414,7 +412,7 @@ impl PackageUIManager { position: Vec3, common_attributes: CommonAttributes, ) { - self.packs.get_mut(&pack_uuid).map(|pack| { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { pack.load_marker_texture( egui_context, self.default_marker_texture.as_ref().unwrap(), @@ -423,7 +421,7 @@ impl PackageUIManager { position, common_attributes, ); - }); + }; } pub fn load_trail_texture( &mut self, @@ -433,15 +431,15 @@ impl PackageUIManager { trail_uuid: Uuid, common_attributes: CommonAttributes, ) { - self.packs.get_mut(&pack_uuid).map(|pack| { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { pack.load_trail_texture( egui_context, - &self.default_trail_texture.as_ref().unwrap(), + self.default_trail_texture.as_ref().unwrap(), &tex_path, trail_uuid, common_attributes, ); - }); + }; } fn pack_importer(import_status: Arc>) { @@ -478,7 +476,7 @@ impl PackageUIManager { for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); tasks.save_texture(pack, pack.is_dirty()); - pack.tick(&u2u_sender, timestamp, link, z_near, &tasks); + pack.tick(u2u_sender, timestamp, link, z_near, tasks); std::mem::drop(span_guard); } let _ = u2u_sender.send(UIToUIMessage::RenderSwapChain); @@ -498,10 +496,8 @@ impl PackageUIManager { if ui.button("Show everything").clicked() { self.show_only_active = false; } - } else { - if ui.button("Show only active").clicked() { - self.show_only_active = true; - } + } else if ui.button("Show only active").clicked() { + self.show_only_active = true; } if ui.button("Activate all elements").clicked() { self.category_set_all(true); @@ -522,7 +518,7 @@ impl PackageUIManager { u2u_sender, ui, self.show_only_active, - &import_quality_report, + import_quality_report, ); } }); diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index 034f6f4..066000e 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -51,7 +51,7 @@ pub enum UIToUIMessage { BulkMarkerObject(Vec), BulkTrailObject(Vec), //Present,// a render loop is finished and we can present it - MarkerObject(MarkerObject), + MarkerObject(Box), RenderSwapChain, // The list of elements to display was changed - TrailObject(TrailObject), + TrailObject(Box), } diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 3073229..60d5911 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -63,30 +63,28 @@ impl Category { // Required method pub fn from(value: &RawCategory, parent: Option) -> Self { Self { - guid: value.guid.clone(), + guid: value.guid, props: value.props.clone(), separator: value.separator, default_enabled: value.default_enabled, display_name: value.display_name.clone(), relative_category_name: value.relative_category_name.clone(), full_category_name: value.full_category_name.clone(), - parent: parent, + parent, children: Default::default(), } } fn per_route<'a>( categories: &'a mut OrderedHashMap, - route: &Vec<&str>, - depth: usize, + route: &[&str], ) -> Option<&'a mut Category> { - let mut route = route.clone(); + let mut route = route.to_owned(); route.reverse(); - Category::_per_route(categories, &mut route, depth) + Category::_per_route(categories, &mut route) } fn _per_route<'a>( categories: &'a mut OrderedHashMap, route: &mut Vec<&str>, - depth: usize, ) -> Option<&'a mut Category> { if let Some(relative_category_name) = route.pop() { for (_, cat) in categories { @@ -94,17 +92,17 @@ impl Category { if route.is_empty() { return Some(cat); } else { - return Category::_per_route(&mut cat.children, route, depth + 1); + return Category::_per_route(&mut cat.children, route); } } } } - return None; + None } + #[allow(dead_code)] fn per_uuid<'a>( categories: &'a mut OrderedHashMap, uuid: &Uuid, - depth: usize, ) -> Option<&'a mut Category> { /* Do a look up in the tree based on uuid. Whole tree is scanned until a match is found. @@ -115,12 +113,12 @@ impl Category { if &cat.guid == uuid { return Some(cat); } - let sub_res = Category::per_uuid(&mut cat.children, uuid, depth + 1); + let sub_res = Category::per_uuid(&mut cat.children, uuid); if sub_res.is_some() { return sub_res; } } - return None; + None } pub fn reassemble( input_first_pass_categories: &OrderedHashMap, @@ -208,11 +206,9 @@ impl Category { to_insert.parent_name = last_name; } else { to_insert.parent_name = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.full_category_name.clone()) - } else { - None - } + first_pass_categories + .get(parent_name) + .map(|parent_category| parent_category.full_category_name.clone()) } else { None }; @@ -239,22 +235,17 @@ impl Category { let start_parent_child_relationship = std::time::SystemTime::now(); for (key, value) in second_pass_categories { let parent = if let Some(parent_name) = &value.parent_name { - if let Some(parent_category) = first_pass_categories.get(parent_name) { - Some(parent_category.guid.clone()) - } else { - None - } + first_pass_categories + .get(parent_name) + .map(|parent_category| parent_category.guid) } else { None }; debug!("{} parent is {:?}", key, parent); let cat = Category::from(&value, parent); - let cat_ref = cat.guid.clone(); - if third_pass_categories - .insert(cat.guid.clone(), cat) - .is_none() - { + let cat_ref = cat.guid; + if third_pass_categories.insert(cat.guid, cat).is_none() { third_pass_categories_ref.push(cat_ref); } } @@ -274,17 +265,16 @@ impl Category { route.pop(); //it is now the parent route if let Some(parent) = cat.parent { if let Some(parent_category) = - Category::per_route(&mut third_pass_categories, &route, 0) - { - parent_category.children.insert(cat.guid.clone(), cat); - } else if let Some(parent_category) = Category::per_route(&mut root, &route, 0) + Category::per_route(&mut third_pass_categories, &route) { - parent_category.children.insert(cat.guid.clone(), cat); + parent_category.children.insert(cat.guid, cat); + } else if let Some(parent_category) = Category::per_route(&mut root, &route) { + parent_category.children.insert(cat.guid, cat); } else { panic!("Could not find parent {} for {:?}", parent, cat); } } else { - root.insert(cat.guid.clone(), cat); + root.insert(cat.guid, cat); } } else { panic!("Some bad logic at works"); diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 4f0e549..1d49e99 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -26,7 +26,7 @@ where serializer.serialize_str(to_do.as_str()) } ElementReference::Category(full_category_name) => { - serializer.serialize_str(&full_category_name.as_str()) + serializer.serialize_str(full_category_name.as_str()) } } } @@ -188,9 +188,9 @@ impl PackageImportReport { self.source_files.get_by_left(source_file_name) } - pub fn found_category_late(&mut self, full_category_name: &String, category_uuid: Uuid) { + pub fn found_category_late(&mut self, full_category_name: &str, category_uuid: Uuid) { self.late_discovered_categories - .insert(category_uuid, full_category_name.clone()); + .insert(category_uuid, full_category_name.to_owned()); } pub fn found_category_late_with_details( &mut self, @@ -205,7 +205,7 @@ impl PackageImportReport { //for this to work we need to keep track of where each category was called and thus defined since late self.missing_categories.push(PackageCategorySource { full_category_name: full_category_name.clone(), - requester_uuid: requester_uuid.clone(), + requester_uuid: *requester_uuid, source_file_name: source_file_name.clone(), }); if !self @@ -226,6 +226,7 @@ impl PackageImportReport { } impl PackCore { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let mut res = Self { all_categories: Default::default(), @@ -239,7 +240,7 @@ impl PackCore { uuid: Default::default(), }; res.uuid = Uuid::new_v4(); - res.report.uuid = res.uuid.clone(); + res.report.uuid = res.uuid; res } pub fn partial(all_categories: &HashMap) -> Self { @@ -273,7 +274,7 @@ impl PackCore { source_file_uuid: &Uuid, ) -> Uuid { if let Some(category_uuid) = self.all_categories.get(full_category_name) { - category_uuid.clone() + *category_uuid } else { //TODO: if import is "dirty", create missing category //TODO: default import mode is "strict" (get inspiration from HTML modes) @@ -282,13 +283,13 @@ impl PackCore { let mut n = 0; let mut last_uuid: Option = None; while let Some(parent_full_category_name) = - prefix_until_nth_char(&full_category_name, '.', n) + prefix_until_nth_char(full_category_name, '.', n) { n += 1; if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { //FIXME: might want to make the difference between impacted parents and actual missing category self.report - .found_category_late(&full_category_name, *parent_uuid); + .found_category_late(full_category_name, *parent_uuid); last_uuid = Some(*parent_uuid); } else { let new_uuid = Uuid::new_v4(); @@ -299,7 +300,7 @@ impl PackCore { self.all_categories .insert(parent_full_category_name.clone(), new_uuid); self.report.found_category_late_with_details( - &full_category_name, + full_category_name, new_uuid, &requester_uuid, source_file_uuid, @@ -349,7 +350,7 @@ impl PackCore { uuid: &Uuid, ) -> Result { if let Some(parent_uuid) = self.all_categories.get(full_category_name) { - let mut uuid_to_insert = uuid.clone(); + let mut uuid_to_insert = *uuid; while self.entities_parents.contains_key(&uuid_to_insert) { trace!( "Uuid collision detected {} for elements in {}", @@ -377,8 +378,8 @@ impl PackCore { ) -> Result<(), miette::Error> { let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; marker.guid = uuid_to_insert; - if !self.maps.contains_key(&marker.map_id) { - self.maps.insert(marker.map_id, MapData::default()); + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(marker.map_id) { + e.insert(MapData::default()); self.report.number_of.maps += 1; } self.maps @@ -397,8 +398,8 @@ impl PackCore { ) -> Result<(), miette::Error> { let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; trail.guid = uuid_to_insert; - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(trail.map_id) { + e.insert(MapData::default()); self.report.number_of.maps += 1; } self.maps @@ -419,8 +420,8 @@ impl PackCore { let tbin = route_to_tbin(&route); self.tbins.insert(tbin_path, tbin); //there may be duplicates since we load and save each time - if !self.maps.contains_key(&trail.map_id) { - self.maps.insert(trail.map_id, MapData::default()); + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(trail.map_id) { + e.insert(MapData::default()); self.report.number_of.maps += 1; } self.maps diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs index b24271a..e9a2db2 100644 --- a/crates/joko_package_models/src/route.rs +++ b/crates/joko_package_models/src/route.rs @@ -36,10 +36,10 @@ pub(crate) fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { Trail { map_id: route.map_id, category: route.category.clone(), - parent: route.parent.clone(), + parent: route.parent, guid: route.guid, - props: props, + props, dynamic: true, - source_file_uuid: route.source_file_uuid.clone(), + source_file_uuid: route.source_file_uuid, } } diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs index bf2b2a6..96ee03a 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render/src/billboard.rs @@ -94,8 +94,7 @@ impl BillBoardRenderer { let len = (trail.vertices.len() * std::mem::size_of::()) as u64; required_size_in_bytes = required_size_in_bytes.max(len); } - let mut vb = vec![]; - vb.reserve(self.markers.len() * 6 * std::mem::size_of::()); + let mut vb: Vec = Vec::with_capacity(self.markers.len() * 6); for marker_object in self.markers.iter() { vb.extend_from_slice(&marker_object.vertices); diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index 2568e53..b55837b 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -15,6 +15,7 @@ pub enum Race { } impl Race { + #[allow(dead_code)] fn from_link_id(race_id: u32) -> Race { match race_id { 0 => Race::Asura, @@ -22,7 +23,7 @@ impl Race { 2 => Race::Human, 3 => Race::Norn, 4 => Race::Sylvari, - _ => return Race::Unknown, + _ => Race::Unknown, } } } diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index e88b95b..f6c4e1f 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -19,10 +19,7 @@ pub fn get_jokolay_path() -> Result { pub fn get_jokolay_dir() -> Result { let authoratah = ambient_authority(); let jdir = if let Ok(env_dir) = std::env::var("JOKOLAY_DATA_DIR") { - let jkl_path = Utf8PathBuf::try_from(&env_dir) - .into_diagnostic() - .wrap_err(env_dir) - .wrap_err("failed to parse JOKOLAY_DATA_DIR")?; + let jkl_path = Utf8PathBuf::from(&env_dir); //may still be an invalid path cap_std::fs_utf8::Dir::create_ambient_dir_all(&jkl_path, authoratah) .into_diagnostic() diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 67dcf34..96f23be 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -56,7 +56,8 @@ struct JokolayBackState { read_ui_link: bool, copy_of_ui_link: Option, root_dir: Arc, - editable_path: std::path::PathBuf, + #[allow(dead_code)] + editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration extract_path: std::path::PathBuf, } struct JokolayApp { @@ -75,7 +76,7 @@ struct JokolayGui { } #[allow(unused)] pub struct Jokolay { - gui: Arc>, + gui: Box, app: Arc>>, state_ui: JokolayUIState, state_back: JokolayBackState, @@ -147,17 +148,21 @@ impl Jokolay { let menu_panel = MenuPanel::default(); package_ui_manager.late_init(&egui_context); + let gui = JokolayGui { + ui_configuration, + joko_renderer, + glfw_backend, + egui_context, + menu_panel, + theme_manager, + mumble_manager: mumble_ui_manager, + package_manager: package_ui_manager, + }; + //let gui = Mutex::new(gui); + //let gui = Arc::new(gui); + let gui = Box::new(gui); Ok(Self { - gui: Arc::new(Mutex::new(JokolayGui { - ui_configuration, - joko_renderer, - glfw_backend, - egui_context, - menu_panel, - theme_manager, - mumble_manager: mumble_ui_manager, - package_manager: package_ui_manager, - })), + gui, app: Arc::new(Mutex::new(Box::new(JokolayApp { mumble_manager: mumble_data_manager, package_manager: package_data_manager, @@ -181,7 +186,7 @@ impl Jokolay { copy_of_ui_link: Default::default(), root_dir, editable_path: std::path::PathBuf::from(editable_path), - extract_path: std::path::PathBuf::from(jokolay_to_extract_path(&root_path)), + extract_path: jokolay_to_extract_path(&root_path), }, }) } @@ -350,6 +355,7 @@ impl Jokolay { } } } + #[allow(unreachable_patterns)] _ => { unimplemented!("Handling BackToUIMessage has not been implemented yet"); } @@ -362,7 +368,7 @@ impl Jokolay { u2b_receiver: std::sync::mpsc::Receiver, ) { tracing::info!("entering background event loop"); - let span_guard = info_span!("background event loop").entered(); + let _span_guard = info_span!("background event loop").entered(); let mut loop_index: u128 = 0; let mut nb_messages: u128 = 0; loop { @@ -403,8 +409,11 @@ impl Jokolay { thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; } - unreachable!("Program broke out a never ending loop !"); - drop(span_guard); + #[allow(unreachable_code)] + { + drop(_span_guard); + unreachable!("Program broke out a never ending loop !") + } } fn handle_u2u_message(gui: &mut JokolayGui, msg: UIToUIMessage) { @@ -425,16 +434,17 @@ impl Jokolay { } UIToUIMessage::MarkerObject(mo) => { tracing::trace!("Handling of UIToUIMessage::MarkerObject"); - gui.joko_renderer.add_billboard(mo); + gui.joko_renderer.add_billboard(*mo); } UIToUIMessage::TrailObject(to) => { tracing::trace!("Handling of UIToUIMessage::TrailObject"); - gui.joko_renderer.add_trail(to); + gui.joko_renderer.add_trail(*to); } UIToUIMessage::RenderSwapChain => { tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); gui.joko_renderer.swap(); } + #[allow(unreachable_patterns)] _ => { unimplemented!("Handling UIToUIMessage has not been implemented yet"); } @@ -520,6 +530,7 @@ impl Jokolay { common_attributes, ); } + #[allow(unreachable_patterns)] _ => { unimplemented!("Handling BackToUIMessage has not been implemented yet"); } @@ -544,6 +555,7 @@ impl Jokolay { let mut nb_messages: u128 = 0; let max_nb_messages_per_loop: u128 = 100; //u2u_sender.send(UIToUIMessage::Present);// force a first drawing + let mut gui = *self.gui; loop { { let mut nb_message_on_curent_loop: u128 = 0; @@ -554,19 +566,15 @@ impl Jokolay { ); if let Ok(mut import_status) = local_state.import_status.lock() { - match &mut *import_status { - ImportStatus::LoadingPack(file_path) => { - let _ = u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); - *import_status = ImportStatus::WaitingLoading(file_path.clone()); - } - _ => {} + if let ImportStatus::LoadingPack(file_path) = &mut *import_status { + let _ = u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); } } //untested and might crash due to .unwrap() - let mut gui = self.gui.lock().unwrap(); while let Ok(msg) = u2u_receiver.try_recv() { nb_messages += 1; - Self::handle_u2u_message(gui.deref_mut(), msg); + Self::handle_u2u_message(&mut gui, msg); nb_message_on_curent_loop += 1; if nb_message_on_curent_loop == max_nb_messages_per_loop { break; @@ -575,12 +583,7 @@ impl Jokolay { if nb_message_on_curent_loop < max_nb_messages_per_loop { while let Ok(msg) = b2u_receiver.try_recv() { nb_messages += 1; - Self::handle_b2u_message( - gui.deref_mut(), - &mut local_state, - &u2b_sender, - msg, - ); + Self::handle_b2u_message(&mut gui, &mut local_state, &u2b_sender, msg); nb_message_on_curent_loop += 1; if nb_message_on_curent_loop == max_nb_messages_per_loop { break; @@ -589,7 +592,6 @@ impl Jokolay { } } - let mut gui = self.gui.lock().unwrap(); let JokolayGui { ui_configuration, menu_panel, @@ -599,7 +601,7 @@ impl Jokolay { theme_manager, mumble_manager, package_manager, - } = &mut gui.deref_mut(); + } = &mut gui; let latest_time = glfw_backend.glfw.get_time(); let etx = egui_context.clone(); @@ -758,21 +760,20 @@ impl Jokolay { "File Manager", ); //ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); - if menu_panel.show_parameters_manager + if (menu_panel.show_parameters_manager || menu_panel.show_package_manager_window || menu_panel.show_mumble_manager_window || menu_panel.show_theme_window || menu_panel.show_file_manager_window - || menu_panel.show_tracing_window + || menu_panel.show_tracing_window) + && ui.button("Close all panels").clicked() { - if ui.button("Close all panels").clicked() { - menu_panel.show_parameters_manager = false; - menu_panel.show_package_manager_window = false; - menu_panel.show_mumble_manager_window = false; - menu_panel.show_theme_window = false; - menu_panel.show_file_manager_window = false; - menu_panel.show_tracing_window = false; - } + menu_panel.show_parameters_manager = false; + menu_panel.show_package_manager_window = false; + menu_panel.show_mumble_manager_window = false; + menu_panel.show_theme_window = false; + menu_panel.show_file_manager_window = false; + menu_panel.show_tracing_window = false; } if ui.button("exit").clicked() { info!("exiting jokolay"); diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs index 51b9c87..cfba395 100644 --- a/crates/jokolay/src/app/ui_parameters.rs +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -53,7 +53,7 @@ impl JokolayUIConfiguration { etx: &egui::Context, wb: &mut GlfwBackend, open: &mut bool, - root_path: &std::path::PathBuf, + root_path: &std::path::Path, ) { let mut need_to_save = false; egui::Window::new("Configuration") diff --git a/crates/jokolay/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs index 769f3cf..0483e58 100644 --- a/crates/jokolay/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -81,7 +81,7 @@ impl ThemeManager { .into_diagnostic() .wrap_err("failed to open themes dir")? .into(); - if !fonts_dir.exists(&format!("{}.ttf", Self::DEFAULT_FONT_NAME)) { + if !fonts_dir.exists(format!("{}.ttf", Self::DEFAULT_FONT_NAME)) { fonts_dir .write( format!("{}.ttf", Self::DEFAULT_FONT_NAME), @@ -90,7 +90,7 @@ impl ThemeManager { .into_diagnostic() .wrap_err("failed to write roboto/default font file to fonts dir")?; } - if !themes_dir.exists(&format!("{}.json", Self::DEFAULT_THEME_NAME)) { + if !themes_dir.exists(format!("{}.json", Self::DEFAULT_THEME_NAME)) { themes_dir .write( format!("{}.json", Self::DEFAULT_THEME_NAME), diff --git a/crates/jokolay/src/manager/trace/mod.rs b/crates/jokolay/src/manager/trace/mod.rs index 7259647..932f1d4 100644 --- a/crates/jokolay/src/manager/trace/mod.rs +++ b/crates/jokolay/src/manager/trace/mod.rs @@ -11,8 +11,8 @@ pub struct JokolayTracingLayer; static JKL_TRACING_DATA: OnceLock> = OnceLock::new(); impl JokolayTracingLayer { - pub fn install_tracing<'l>( - jokolay_dir: &'l Dir, + pub fn install_tracing( + jokolay_dir: &Dir, ) -> Result { use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; @@ -22,9 +22,9 @@ impl JokolayTracingLayer { use std::io::Write; let backtrace = std::backtrace::Backtrace::force_capture(); let output = if let Some(string) = info.payload().downcast_ref::() { - format!("{string}") + string.to_string() } else if let Some(str) = info.payload().downcast_ref::<&'static str>() { - format!("{str}") + str.to_string() } else { format!("{info:?}") }; diff --git a/crates/jokolink/src/linux/mod.rs b/crates/jokolink/src/linux/mod.rs index d88aba8..f0adab4 100644 --- a/crates/jokolink/src/linux/mod.rs +++ b/crates/jokolink/src/linux/mod.rs @@ -24,6 +24,7 @@ impl MumbleLinuxImpl { pub fn new(link_name: &str) -> Result { let mumble_file_name = format!("/dev/shm/{link_name}"); info!("creating mumble file at {mumble_file_name}"); + #[allow(clippy::suspicious_open_options)] let mut mfile = File::options() .read(true) .write(true) // write/append is needed for the create flag From 721b466a2bd735c3b1ac87607dcb29191f3607b4 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 21 Apr 2024 22:18:21 +0200 Subject: [PATCH 36/54] update unsecure module --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65f6fcf..25fa700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2319,9 +2319,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", From 7818f5a1c278e17f37828e5d41fc3d688175305d Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 22 Apr 2024 12:09:43 +0200 Subject: [PATCH 37/54] fix windows compilation --- Cargo.lock | 95 ++++++++++++++++++++++++ crates/jokolink/Cargo.toml | 3 + crates/jokolink/src/mumble/ctypes.rs | 3 +- crates/jokolink/src/win/mod.rs | 104 +++++++++++++++------------ 4 files changed, 157 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25fa700..c360f70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1037,6 +1037,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "flate2" version = "1.0.28" @@ -1067,6 +1079,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -1332,6 +1353,26 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -1540,12 +1581,15 @@ dependencies = [ "enumflags2", "glam", "miette", + "notify", "num-derive", "num-traits", "serde", "serde_json", "time", "tracing", + "tracing-appender", + "tracing-subscriber", "widestring", "windows", "x11rb", @@ -1560,6 +1604,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1725,6 +1789,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "next-gen" version = "0.1.1" @@ -1777,6 +1853,25 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/crates/jokolink/Cargo.toml b/crates/jokolink/Cargo.toml index 99e9e95..5959ae3 100644 --- a/crates/jokolink/Cargo.toml +++ b/crates/jokolink/Cargo.toml @@ -39,3 +39,6 @@ windows = { version = "0.51.1", features = [ "Win32_System_Com", ] } arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } diff --git a/crates/jokolink/src/mumble/ctypes.rs b/crates/jokolink/src/mumble/ctypes.rs index db9d26f..72dd4ac 100644 --- a/crates/jokolink/src/mumble/ctypes.rs +++ b/crates/jokolink/src/mumble/ctypes.rs @@ -67,6 +67,7 @@ impl Default for CMumbleLink { } } } + impl CMumbleLink { /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { @@ -186,7 +187,7 @@ pub struct CMumbleContext { pub dpi: i32, /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. pub client_pos: [i32; 2], - pub client_size: [u32; 4], + pub client_size: [u32; 2], /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. pub padding: [u8; 96], } diff --git a/crates/jokolink/src/win/mod.rs b/crates/jokolink/src/win/mod.rs index 8910412..21ebc75 100644 --- a/crates/jokolink/src/win/mod.rs +++ b/crates/jokolink/src/win/mod.rs @@ -3,7 +3,7 @@ pub mod dll; //putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration -use crate::mumble::ctypes::*; +use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; use miette::{bail, Context, IntoDiagnostic, Result}; use notify::Watcher; use std::{ @@ -70,7 +70,8 @@ pub struct MumbleWinImpl { last_pos_size_check: Instant, /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. - client_pos_size: [i32; 4], + client_pos: [i32; 2], + client_size: [u32; 2], /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml /// 0 for false /// 1 for true @@ -99,6 +100,8 @@ pub struct MumbleWinImpl { */ } +unsafe impl Send for MumbleWinImpl {} + impl MumbleWinImpl { pub fn new(key: &str) -> Result { unsafe { @@ -173,7 +176,8 @@ impl MumbleWinImpl { last_pos_size_check: Instant::now(), // window_pos_size_without_borders: [0; 4], dpi_scaling, - client_pos_size: [0; 4], + client_pos: [0; 2], + client_size: [0; 2], dpi: 0, _gw2_config_watcher: gw2_config_watcher, gw2_config_changed, @@ -185,7 +189,7 @@ impl MumbleWinImpl { !self.process_handle.is_invalid() } pub fn get_cmumble_link(&mut self) -> CMumbleLink { - let mut link = unsafe { std::ptr::read_volatile(self.link_ptr) }; + let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; link.context.timestamp = OffsetDateTime::now_utc() .unix_timestamp_nanos() .to_le_bytes(); @@ -194,7 +198,8 @@ impl MumbleWinImpl { link.context.dpi_scaling = self.dpi_scaling; link.context.dpi = self.dpi; link.context.xid = self.xid; - link.context.client_pos_size = self.client_pos_size; + link.context.client_pos = self.client_pos; + link.context.client_size = self.client_size; link } /// This is the most important function which will be called every frame @@ -335,24 +340,26 @@ impl MumbleWinImpl { // return Ok(()); // } // }; - self.client_pos_size = - match get_client_rect_in_screen_coords(HWND(self.window_handle)) { - Ok(client_pos_size) => { - if self.client_pos_size != client_pos_size { - info!( - ?self.client_pos_size, - ?client_pos_size, - "window position size changed" - ); - } - client_pos_size + match get_client_rect_in_screen_coords(HWND(self.window_handle)) { + Ok((client_pos, client_size)) => { + if self.client_pos != client_pos || self.client_size != client_size { + info!( + ?self.client_pos, + ?client_pos, + ?self.client_size, + ?client_size, + "window position or size changed" + ); } - Err(e) => { - error!(?e, "failed to get client position size"); - self.reset(); // go back to being dead because it shouldn't usually fail - return Ok(()); - } - }; + self.client_pos = client_pos; + self.client_size = client_size; + } + Err(e) => { + error!(?e, "failed to get client position size"); + self.reset(); // go back to being dead because it shouldn't usually fail + return Ok(()); + } + }; } } } @@ -371,7 +378,8 @@ impl MumbleWinImpl { // self.window_pos_size = [0; 4]; // self.window_pos_size_without_borders = [0; 4]; self.dpi = 0; - self.client_pos_size = [0; 4]; + self.client_pos = [0; 2]; + self.client_size = [0; 2]; self.previous_pid = 0; self.xid = 0; } @@ -420,7 +428,7 @@ impl MumbleWinImpl { // now we have both process_handle and window_handle. We just need the window size to initialize our struct // this function only gets the suface/viewport pos/size without any borders/decoraitons. match get_client_rect_in_screen_coords(HWND(window_handle)) { - Ok(client_pos_size) => { + Ok((client_pos, client_size)) => { // this block is purely for logging purposes only to verify that all sizes are working properly. { // GetWindowRect includes drop shadow borders and titlebar @@ -489,7 +497,8 @@ impl MumbleWinImpl { info!(dpi, self.dpi, "dpi changed for gw2 window"); } info!( - ?client_pos_size, + ?client_pos, + ?client_size, dpi_awareness, dpi, pid, @@ -500,7 +509,8 @@ impl MumbleWinImpl { self.process_handle = process_handle; self.window_handle = window_handle; self.dpi = dpi; - self.client_pos_size = client_pos_size; + self.client_pos = client_pos; + self.client_size = client_size; self.last_ui_tick_update = Instant::now(); self.previous_pid = pid; } @@ -634,7 +644,7 @@ unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LP /// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. /// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). /// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. -fn get_window_pos_size(window_handle: isize) -> Result<[i32; 4]> { +fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -645,15 +655,15 @@ fn get_window_pos_size(window_handle: isize) -> Result<[i32; 4]> { if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { bail!("GetWindowRect call failed {e:#?}"); } - Ok([ - rect.left, - rect.top, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } -fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<[i32; 4]> { +fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -669,15 +679,15 @@ fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<[i32; 4]> ) { bail!("DwmGetWindowAttribute call failed {e:#?}"); } - Ok([ - rect.left, - rect.top, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } -fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<[i32; 4]> { +fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -695,12 +705,12 @@ fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<[i32; 4]> { if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { bail!("ClientToScreen call failed"); } - Ok([ - point.x, - point.y, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [point.x, point.y]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } impl Drop for MumbleWinImpl { From f93c9d0686c2e5ad461fa69d0bdb09bf80f7dbe5 Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 22 Apr 2024 12:14:36 +0200 Subject: [PATCH 38/54] fix unimplemented warning --- crates/joko_package/src/manager/pack/loaded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 88d141a..458f800 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -399,7 +399,7 @@ impl LoadedPackData { ) { //since the loading of texture is lazy, there is no problem when calling this regularly if map_changed || list_of_active_or_selected_elements_changed { - tasks.change_map(self, b2u_sender, link, currently_used_files); + //tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); let _ = b2u_sender.send(BackToUIMessage::PackageActiveElements( From 6bae62aa90b56b823a26a555a31a0cd05407bd07 Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 22 Apr 2024 12:15:41 +0200 Subject: [PATCH 39/54] fix unimplemented warning --- crates/joko_package/src/manager/pack/loaded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 458f800..7219efa 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -394,7 +394,7 @@ impl LoadedPackData { currently_used_files: &BTreeMap, list_of_active_or_selected_elements_changed: bool, map_changed: bool, - tasks: &PackTasks, + _tasks: &PackTasks, next_loaded: &mut HashSet, ) { //since the loading of texture is lazy, there is no problem when calling this regularly From b8effe9642099110acd5ce82bb70699a7c1b77b5 Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 22 Apr 2024 13:29:29 +0200 Subject: [PATCH 40/54] more fix to pass window compilation --- crates/jokolink/src/win/dll.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/jokolink/src/win/dll.rs b/crates/jokolink/src/win/dll.rs index cccbf92..ffe49bc 100644 --- a/crates/jokolink/src/win/dll.rs +++ b/crates/jokolink/src/win/dll.rs @@ -22,12 +22,13 @@ unsafe fn spawn_jokolink_thread() { d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); - match std::thread::Builder::new() + let th = std::thread::Builder::new() .name("jokolink thread".to_string()) .spawn(move || { d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); "jokolink thread quit" - }) { + }); + match th { Ok(handle) => { println!("spawned jokolink thread. handle: {handle:?}"); d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); @@ -411,6 +412,7 @@ pub mod d3d11 { &dest_path ); + #[allow(clippy::blocks_in_conditions)] let mut mfile = std::fs::File::options() .write(true) .create(true) From 7f0561d92086e0edcf3e897de989499610036001 Mon Sep 17 00:00:00 2001 From: moi Date: Mon, 22 Apr 2024 19:30:27 +0200 Subject: [PATCH 41/54] clippy warning in mumble file writing in dll --- crates/jokolink/src/win/dll.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/jokolink/src/win/dll.rs b/crates/jokolink/src/win/dll.rs index ffe49bc..721b5fe 100644 --- a/crates/jokolink/src/win/dll.rs +++ b/crates/jokolink/src/win/dll.rs @@ -412,7 +412,7 @@ pub mod d3d11 { &dest_path ); - #[allow(clippy::blocks_in_conditions)] + #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] let mut mfile = std::fs::File::options() .write(true) .create(true) From 055fe4a2c70489fefd040ac87399397408f19e1c Mon Sep 17 00:00:00 2001 From: moi Date: Tue, 23 Apr 2024 21:06:22 +0200 Subject: [PATCH 42/54] transfer github workflow to branch to be validated --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89eae68..b9e9c1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] name: check everything env: From e6f37cde3ebfa6506dd86cbd8ce4b9ede873ba85 Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 27 Apr 2024 18:15:06 +0200 Subject: [PATCH 43/54] add separation into components + basic agnostic transfer of data --- crates/joko_components/Cargo.toml | 13 + crates/joko_components/src/lib.rs | 171 ++++ crates/joko_core/Cargo.toml | 3 + crates/joko_core/src/lib.rs | 22 + crates/joko_core/src/serde_glam/mod.rs | 5 + crates/joko_core/src/serde_glam/vec2.rs | 155 ++++ crates/joko_core/src/serde_glam/vec3.rs | 58 ++ crates/joko_link/Cargo.toml | 47 ++ crates/joko_link/README.md | 58 ++ crates/joko_link/src/lib.rs | 270 +++++++ crates/joko_link/src/linux/mod.rs | 305 ++++++++ crates/joko_link/src/mumble/ctypes.rs | 288 +++++++ crates/joko_link/src/mumble/mod.rs | 173 +++++ crates/joko_link/src/win/dll.rs | 490 ++++++++++++ crates/joko_link/src/win/mod.rs | 735 ++++++++++++++++++ crates/joko_link_models/Cargo.toml | 47 ++ crates/joko_link_models/README.md | 58 ++ crates/joko_link_models/src/lib.rs | 33 + crates/joko_link_models/src/mumble/ctypes.rs | 288 +++++++ crates/joko_link_models/src/mumble/mod.rs | 173 +++++ crates/joko_package/Cargo.toml | 5 +- crates/joko_package/src/io/deserialize.rs | 167 ++-- crates/joko_package/src/io/serialize.rs | 103 ++- crates/joko_package/src/manager/mod.rs | 6 +- .../joko_package/src/manager/pack/active.rs | 68 +- .../src/manager/pack/category_selection.rs | 35 +- .../joko_package/src/manager/pack/import.rs | 4 +- .../joko_package/src/manager/pack/loaded.rs | 224 +++--- .../joko_package/src/manager/package_data.rs | 516 ++++++++++++ .../src/manager/{package.rs => package_ui.rs} | 590 +++++++------- crates/joko_package/src/message.rs | 36 +- crates/joko_package_models/src/attributes.rs | 18 +- crates/joko_package_models/src/category.rs | 3 +- crates/joko_package_models/src/map.rs | 3 +- crates/joko_package_models/src/marker.rs | 5 +- crates/joko_package_models/src/package.rs | 14 +- crates/joko_package_models/src/route.rs | 6 +- crates/joko_package_models/src/trail.rs | 10 +- crates/joko_plugins/Cargo.toml | 12 + crates/joko_plugins/src/lib.rs | 29 + crates/joko_render/Cargo.toml | 5 +- crates/joko_render/src/renderer.rs | 226 ++++-- crates/joko_render_models/Cargo.toml | 5 + crates/joko_render_models/src/lib.rs | 1 + crates/joko_render_models/src/marker.rs | 8 +- crates/joko_render_models/src/messages.rs | 20 + crates/joko_render_models/src/trail.rs | 4 +- crates/jokolay/Cargo.toml | 7 +- crates/jokolay/src/app/messages.rs | 3 + crates/jokolay/src/app/mod.rs | 609 +++++---------- crates/jokolay/src/app/mumble.rs | 89 ++- crates/jokolay/src/app/ui_parameters.rs | 10 +- 52 files changed, 5019 insertions(+), 1214 deletions(-) create mode 100644 crates/joko_components/Cargo.toml create mode 100644 crates/joko_components/src/lib.rs create mode 100644 crates/joko_core/src/serde_glam/mod.rs create mode 100644 crates/joko_core/src/serde_glam/vec2.rs create mode 100644 crates/joko_core/src/serde_glam/vec3.rs create mode 100644 crates/joko_link/Cargo.toml create mode 100644 crates/joko_link/README.md create mode 100644 crates/joko_link/src/lib.rs create mode 100644 crates/joko_link/src/linux/mod.rs create mode 100644 crates/joko_link/src/mumble/ctypes.rs create mode 100644 crates/joko_link/src/mumble/mod.rs create mode 100644 crates/joko_link/src/win/dll.rs create mode 100644 crates/joko_link/src/win/mod.rs create mode 100644 crates/joko_link_models/Cargo.toml create mode 100644 crates/joko_link_models/README.md create mode 100644 crates/joko_link_models/src/lib.rs create mode 100644 crates/joko_link_models/src/mumble/ctypes.rs create mode 100644 crates/joko_link_models/src/mumble/mod.rs create mode 100644 crates/joko_package/src/manager/package_data.rs rename crates/joko_package/src/manager/{package.rs => package_ui.rs} (61%) create mode 100644 crates/joko_plugins/Cargo.toml create mode 100644 crates/joko_plugins/src/lib.rs create mode 100644 crates/joko_render_models/src/messages.rs create mode 100644 crates/jokolay/src/app/messages.rs diff --git a/crates/joko_components/Cargo.toml b/crates/joko_components/Cargo.toml new file mode 100644 index 0000000..b13c465 --- /dev/null +++ b/crates/joko_components/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "joko_components" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = { workspace = true } +egui = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } +tokio = { workspace = true } diff --git a/crates/joko_components/src/lib.rs b/crates/joko_components/src/lib.rs new file mode 100644 index 0000000..8f49116 --- /dev/null +++ b/crates/joko_components/src/lib.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +pub trait JokolayComponentDeps { + /** + Names are external to traits and implementation. That way it is easy to change it without change in binary. + In case of first class components, name is hardcoded. + In case of plugins, name is part of a manifest and can be changed at will. + */ + // elements in peer(), requires() and notify() are mutually exclusives + fn peer(&self) -> Vec<&str> { + //by default, no other plugin bound + vec![] + } + fn requires(&self) -> Vec<&str> { + //by default, no requirement + vec![] + } + fn notify(&self) -> Vec<&str> { + //by default, no third party plugin + vec![] + } +} + +//could become a "dyn Message". +//std::any::Any is a trait +//TODO: It would have a wrap and unwrap ? +pub type ComponentDataExchange = Vec; +//pub type ComponentDataExchange = Box<[u8]>; +//pub type ComponentDataExchange = [u8; 1024]; +pub type PeerComponentChannel = ( + tokio::sync::mpsc::Receiver, + tokio::sync::mpsc::Sender, +); + +pub trait JokolayComponent +where + SharedStatus: Clone, +{ + fn flush_all_messages(&mut self) -> SharedStatus; + fn tick(&mut self, latest_time: f64) -> Option<&ComponentResult>; + fn bind( + &mut self, + deps: HashMap>, + bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap>, + notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ); //By default, there is no third party component, thus we can implement it as a noop + + /* + + // any extra information should come from configuration, which can be loaded from those two arguments. + Those roots are specific to the component, it cannot shared it with another component + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + ) -> Result; + + fn bind( + &mut self, + deps: HashMap, + bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap + notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) + https://docs.rs/dep-graph/latest/dep_graph/ + https://lib.rs/crates/petgraph + https://docs.rs/solvent/latest/solvent/ + => check "peer" is always mutual + => graph with the "peer" elements replaced by some merged id + => check there is no loop (there could be surprises) + => if there is no problem, then: + - build again the graph with UI plugins only and save one traversal (memory + file) + - build again the graph with back plugins only and save one traversal (memory + file) + => if there is a problem, do not save anything + + + + fn tick( + &mut self, + ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() + */ +} + +pub trait JokolayUIComponent +where + SharedStatus: Clone, +{ + fn flush_all_messages(&mut self) -> SharedStatus; + fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> Option<&ComponentResult>; + fn bind( + &mut self, + deps: HashMap>, + bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap>, + notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ); //By default, there is no third party component, thus we can implement it as a noop + + /* + + // any extra information should come from configuration, which can be loaded from those two arguments. + Those roots are specific to the component, it cannot shared it with another component + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + ) -> Result; + + fn bind( + &mut self, + deps: HashMap, + bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap + notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) + https://docs.rs/dep-graph/latest/dep_graph/ + https://lib.rs/crates/petgraph + https://docs.rs/solvent/latest/solvent/ + => check "peer" is always mutual + => graph with the "peer" elements replaced by some merged id + => check there is no loop (there could be surprises) + => if there is no problem, then: + - build again the graph with UI plugins only and save one traversal (memory + file) + - build again the graph with back plugins only and save one traversal (memory + file) + => if there is a problem, do not save anything + + + + fn tick( + &mut self, + ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() + */ +} + +//TODO: have a BackEndPlugin and UIPlugin + +pub struct ComponentManager { + data: HashMap>, +} + +impl ComponentManager { + pub fn new() -> Self { + Self { + data: Default::default(), + } + } + + pub fn register(&mut self, service_name: &str, co: Box) { + self.data.insert(service_name.to_owned(), co); + } + + pub fn build_routes(&mut self) -> Result<(), String> { + let mut known_services: HashMap = Default::default(); + let mut service_id = 0; + for (service_name, co) in self.data.iter() { + service_id += 1; + known_services.insert(service_name.clone(), service_id); + for peer_name in co.peer() { + if let Some(peer) = self.data.get(peer_name) { + if !peer.peer().contains(&service_name.as_str()) { + return Err(format!( + "Missmatch in peer between {} and {}", + service_name, peer_name + )); + } + } + } + } + unimplemented!( + "The algorithm to build and check dependancies between components is not implemented" + ) + } +} diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index 8569d10..19c231c 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -6,5 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bytemuck = { workspace = true } +glam = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } +serde = { workspace = true } diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index a0afaf8..f7864f2 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use serde::{Deserialize, Serialize}; use smol_str::SmolStr; /* @@ -11,6 +12,7 @@ each manager must have */ +pub mod serde_glam; pub mod task; /// This newtype is used to represents relative paths in marker packs @@ -22,6 +24,26 @@ pub mod task; /// 6. It doesn't mean that the path is valid. It may contain many of the utf-8 characters which are not valid path names on linux/windows #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RelativePath(SmolStr); + +impl Serialize for RelativePath { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} +impl<'de> Deserialize<'de> for RelativePath { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let r = s.parse().unwrap(); + Ok(r) + } +} + #[allow(unused)] impl RelativePath { pub fn normalize(path: &str) -> String { diff --git a/crates/joko_core/src/serde_glam/mod.rs b/crates/joko_core/src/serde_glam/mod.rs new file mode 100644 index 0000000..1401592 --- /dev/null +++ b/crates/joko_core/src/serde_glam/mod.rs @@ -0,0 +1,5 @@ +mod vec2; +mod vec3; + +pub use vec2::{IVec2, UVec2, Vec2}; +pub use vec3::Vec3; diff --git a/crates/joko_core/src/serde_glam/vec2.rs b/crates/joko_core/src/serde_glam/vec2.rs new file mode 100644 index 0000000..4f4e336 --- /dev/null +++ b/crates/joko_core/src/serde_glam/vec2.rs @@ -0,0 +1,155 @@ +use serde::{ + de::{SeqAccess, Visitor}, + Deserialize, Serialize, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vec2(pub glam::Vec2); +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct IVec2(pub glam::IVec2); +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct UVec2(pub glam::UVec2); + +impl From for glam::Vec2 { + fn from(src: Vec2) -> glam::Vec2 { + src.0 + } +} +impl From for glam::IVec2 { + fn from(src: IVec2) -> glam::IVec2 { + src.0 + } +} +impl From for glam::UVec2 { + fn from(src: UVec2) -> glam::UVec2 { + src.0 + } +} + +struct Vec2Deserializer; +impl<'de> Visitor<'de> for Vec2Deserializer { + type Value = Vec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Vec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: f32 = seq.next_element()?.unwrap(); + let y: f32 = seq.next_element()?.unwrap(); + let res = Vec2(glam::Vec2 { x, y }); + Ok(res) + } +} + +impl Serialize for Vec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for Vec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(Vec2Deserializer) + } +} + +struct IVec2Deserializer; +impl<'de> Visitor<'de> for IVec2Deserializer { + type Value = IVec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("IVec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: i32 = seq.next_element()?.unwrap(); + let y: i32 = seq.next_element()?.unwrap(); + let res = IVec2(glam::IVec2 { x, y }); + Ok(res) + } +} +impl Serialize for IVec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for IVec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(IVec2Deserializer) + } +} + +struct UVec2Deserializer; +impl<'de> Visitor<'de> for UVec2Deserializer { + type Value = UVec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("UVec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: u32 = seq.next_element()?.unwrap(); + let y: u32 = seq.next_element()?.unwrap(); + let res = UVec2(glam::UVec2 { x, y }); + Ok(res) + } +} + +impl Serialize for UVec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for UVec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(UVec2Deserializer) + } +} diff --git a/crates/joko_core/src/serde_glam/vec3.rs b/crates/joko_core/src/serde_glam/vec3.rs new file mode 100644 index 0000000..458108c --- /dev/null +++ b/crates/joko_core/src/serde_glam/vec3.rs @@ -0,0 +1,58 @@ +use serde::{ + de::{SeqAccess, Visitor}, + Deserialize, Serialize, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vec3(pub glam::Vec3); + +impl From for glam::Vec3 { + fn from(src: Vec3) -> glam::Vec3 { + src.0 + } +} + +struct Vec3Deserializer; +impl<'de> Visitor<'de> for Vec3Deserializer { + type Value = Vec3; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Vec3Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: f32 = seq.next_element()?.unwrap(); + let y: f32 = seq.next_element()?.unwrap(); + let z: f32 = seq.next_element()?.unwrap(); + let res = Vec3(glam::Vec3 { x, y, z }); + Ok(res) + } +} + +impl Serialize for Vec3 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.serialize_element(&self.0.z)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for Vec3 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(Vec3Deserializer) + } +} diff --git a/crates/joko_link/Cargo.toml b/crates/joko_link/Cargo.toml new file mode 100644 index 0000000..28f666b --- /dev/null +++ b/crates/joko_link/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "joko_link" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + + +[dependencies] +joko_core = { path = "../joko_core" } +joko_components = { path = "../joko_components" } +widestring = { version = "1", default-features = false, features = ["std"] } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +time = { workspace = true } +miette = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } + +[target.'cfg(unix)'.dependencies] +x11rb = { version = "0.12", default-features = false, features = [] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.51.1", features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_Graphics_Dwm", + "Win32_UI_HiDpi", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_System_Com", +] } +arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } diff --git a/crates/joko_link/README.md b/crates/joko_link/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link/src/lib.rs b/crates/joko_link/src/lib.rs new file mode 100644 index 0000000..79b74e1 --- /dev/null +++ b/crates/joko_link/src/lib.rs @@ -0,0 +1,270 @@ +//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory + +//! Joko link is designed to primarily get the MumbleLink or the window size +//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). +//! on windows, you can use it to create/open shared memory. +//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. +//! then, you can easily read the /dev/shm file from a any number of linux native applications. +//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. +//! + +mod mumble; +use std::vec; + +use enumflags2::BitFlags; +use joko_components::{JokolayComponent, JokolayComponentDeps}; +use joko_core::serde_glam::{IVec2, UVec2, Vec3}; +//use jokoapi::end_point::{mounts::Mount, races::Race}; +use miette::{IntoDiagnostic, Result, WrapErr}; +pub use mumble::*; +use serde_json::from_str; +use tracing::error; + +/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing +pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "windows")] +pub mod win; + +#[cfg(target_os = "linux")] +use linux::MumbleLinuxImpl as MumblePlatformImpl; +#[cfg(target_os = "windows")] +use win::MumbleWinImpl as MumblePlatformImpl; + +pub enum MessageToMumbleLinkBack { + BindedOnUI, + Autonomous, + Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. +} + +#[derive(Clone)] +pub struct MumbleLinkSharedState { + pub read_ui_link: bool, + pub copy_of_ui_link: Option, +} + +// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there +// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; +/// This primarily manages the mumble backend. +/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. +/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. +/// and every frame gets the latest mumble link data, and compares with the previous frame. +/// if any of the changed this frame, it will set the relevant changed flags so that plugins +/// or other parts of program which care can run the relevant code. +pub struct MumbleManager { + /// This abstracts over the windows and linux impl of mumble link functionality. + /// we use this to get the latest mumble link and latest window dimensions of the current mumble link + backend: MumblePlatformImpl, + is_ui: bool, + /// latest mumble link + link: MumbleLink, + channel_receiver: std::sync::mpsc::Receiver, + state: MumbleLinkSharedState, +} + +impl MumbleManager { + pub fn new(name: &str, is_ui: bool) -> Result { + let backend = MumblePlatformImpl::new(name)?; + let (_, receiver) = std::sync::mpsc::channel(); + Ok(Self { + backend, + link: Default::default(), + channel_receiver: receiver, + is_ui, + state: MumbleLinkSharedState { + read_ui_link: true, + copy_of_ui_link: None, + }, + }) + } + pub fn is_alive(&self) -> bool { + self.backend.is_alive() + } + fn handle_message(&mut self, msg: MessageToMumbleLinkBack) { + //let (b2u_sender, _) = package_manager.channels(); + match msg { + MessageToMumbleLinkBack::Autonomous => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); + self.state.read_ui_link = false; + } + MessageToMumbleLinkBack::BindedOnUI => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); + self.state.read_ui_link = true; + } + MessageToMumbleLinkBack::Value(link) => { + tracing::trace!("Handling of UIToBackMessage::MumbleLink"); + self.state.copy_of_ui_link = link; + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + } + } + } + fn _tick(&mut self) -> Result> { + if let Err(e) = self.backend.tick() { + error!(?e, "mumble backend tick error"); + return Ok(None); + } + + if !self.backend.is_alive() { + self.link.client_size.0.x = 0; + self.link.client_size.0.y = 0; + self.link.changes = BitFlags::all(); + return Ok(Some(&self.link)); + } + // backend is alive and tick is successful. time to get link + let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); + let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { + Default::default() + } else { + self.link.clone() + }; + + if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { + return Ok(None); + } + let mut changes: BitFlags = Default::default(); + // safety. as the link is valid, we can use as_ref + let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) + .into_diagnostic() + .wrap_err("failed to get widestring out of cml identity")? + .to_string() + .into_diagnostic() + .wrap_err("failed to convert widestring to cstring")?; + + let identity: ctypes::CIdentity = from_str(&json_string) + .into_diagnostic() + .wrap_err("failed to deserialize identity from json string")?; + let uisz = identity + .get_uisz() + .ok_or(miette::miette!("uisz is invalid"))?; + let server_address = if cml.context.server_address[0] == 2 { + let addr = cml.context.server_address; + std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() + } else { + std::net::Ipv4Addr::UNSPECIFIED.into() + }; + if new_link.ui_tick != cml.ui_tick { + changes.insert(MumbleChanges::UiTick); + } + if new_link.name != identity.name { + changes.insert(MumbleChanges::Character); + } + if new_link.map_id != cml.context.map_id { + changes.insert(MumbleChanges::Map); + } + let client_pos = IVec2(glam::IVec2::new( + cml.context.client_pos[0], + cml.context.client_pos[1], + )); + let client_size = UVec2(glam::UVec2::new( + cml.context.client_size[0], + cml.context.client_size[1], + )); + + if new_link.client_pos != client_pos { + changes.insert(MumbleChanges::WindowPosition); + } + if new_link.client_size != client_size { + changes.insert(MumbleChanges::WindowSize); + } + let cam_pos: glam::Vec3 = cml.f_camera_position.into(); + if new_link.cam_pos.0 != cam_pos { + changes.insert(MumbleChanges::Camera); + } + + let player_pos: glam::Vec3 = cml.f_avatar_position.into(); + if new_link.player_pos.0 != player_pos { + changes.insert(MumbleChanges::Position); + } + //let player_race = Self::get_race(identity.race); + + new_link = MumbleLink { + ui_tick: cml.ui_tick, + player_pos: Vec3(player_pos), + f_avatar_front: Vec3(cml.f_avatar_front.into()), + cam_pos: Vec3(cam_pos), + f_camera_front: Vec3(cml.f_camera_front.into()), + name: identity.name, + map_id: cml.context.map_id, + fov: identity.fov, + uisz, + // window_pos, + // window_size, + changes, + // window_pos_without_borders, + // window_size_without_borders, + dpi_scaling: cml.context.dpi_scaling, + dpi: cml.context.dpi, + client_pos, + client_size, + map_type: cml.context.map_type, + server_address, + shard_id: cml.context.shard_id, + instance: cml.context.instance, + build_id: cml.context.build_id, + ui_state: cml.context.get_ui_state(), + compass_width: cml.context.compass_width, + compass_height: cml.context.compass_height, + compass_rotation: cml.context.compass_rotation, + player_x: cml.context.player_x, + player_y: cml.context.player_y, + map_center_x: cml.context.map_center_x, + map_center_y: cml.context.map_center_y, + map_scale: cml.context.map_scale, + process_id: cml.context.process_id, + mount: cml.context.mount_index, + race: identity.race, + }; + self.link = new_link; + + Ok(if self.link.ui_tick == 0 { + None + } else { + Some(&self.link) + }) + } +} + +impl JokolayComponent for MumbleManager { + fn flush_all_messages(&mut self) -> MumbleLinkSharedState { + while let Ok(msg) = self.channel_receiver.try_recv() { + self.handle_message(msg); + } + self.state.clone() + } + + fn tick(&mut self, _latest_time: f64) -> Option<&MumbleLink> { + self._tick().unwrap_or(None) + } + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap< + u32, + tokio::sync::mpsc::Sender, + >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + } +} + +impl JokolayComponentDeps for MumbleManager { + //default is enough + fn peer(&self) -> Vec<&str> { + if self.is_ui { + vec!["mumble_link_back"] + } else { + vec!["mumble_link_ui"] + } + } +} diff --git a/crates/joko_link/src/linux/mod.rs b/crates/joko_link/src/linux/mod.rs new file mode 100644 index 0000000..f0adab4 --- /dev/null +++ b/crates/joko_link/src/linux/mod.rs @@ -0,0 +1,305 @@ +use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; +use miette::{Context, IntoDiagnostic, Result}; +use std::fs::File; +use std::io::{Read, Seek}; +use time::OffsetDateTime; +use tracing::info; +// use x11rb::protocol::xproto::{change_property, intern_atom, AtomEnum, GetGeometryReply, PropMode}; +// use x11rb::rust_connection::ConnectError; + +pub use x11rb::rust_connection::RustConnection; + +/// This is the bak +pub struct MumbleLinuxImpl { + mfile: File, + link_buffer: LinkBuffer, + /// we basically use this as the ui_tick of mumblelink + /// If this changed recently, it means jokolink is running (i.e. gw2 is running) + previous_jokolink_timestamp: i128, +} + +type LinkBuffer = Box<[u8; C_MUMBLE_LINK_SIZE_FULL]>; + +impl MumbleLinuxImpl { + pub fn new(link_name: &str) -> Result { + let mumble_file_name = format!("/dev/shm/{link_name}"); + info!("creating mumble file at {mumble_file_name}"); + #[allow(clippy::suspicious_open_options)] + let mut mfile = File::options() + .read(true) + .write(true) // write/append is needed for the create flag + .create(true) + .open(&mumble_file_name) + .into_diagnostic() + .wrap_err("failed to create mumble file")?; + let mut link_buffer = LinkBuffer::new([0u8; C_MUMBLE_LINK_SIZE_FULL]); + mfile.rewind().into_diagnostic()?; + mfile + .read(link_buffer.as_mut()) + .into_diagnostic() + .wrap_err("failed to get link buffer from mfile")?; + let previous_jokolink_timestamp = + unsafe { CMumbleLink::get_timestamp(link_buffer.as_ptr() as _) }; + Ok(MumbleLinuxImpl { + mfile, + link_buffer, + previous_jokolink_timestamp, + }) + } + pub fn tick(&mut self) -> Result<()> { + self.mfile.rewind().into_diagnostic()?; + self.mfile + .read(self.link_buffer.as_mut()) + .into_diagnostic() + .wrap_err("failed to get link buffer")?; + self.previous_jokolink_timestamp = + unsafe { CMumbleLink::get_timestamp(self.link_buffer.as_ptr() as _) }; + Ok(()) + } + pub fn is_alive(&self) -> bool { + OffsetDateTime::now_utc().unix_timestamp_nanos() - self.previous_jokolink_timestamp + < std::time::Duration::from_secs(1).as_nanos() as i128 + } + pub fn get_cmumble_link(&self) -> CMumbleLink { + if self.is_alive() { + unsafe { std::ptr::read(self.link_buffer.as_ptr() as _) } + } else { + Default::default() + } + } + // pub fn set_transient_for(&self) -> Result<()> { + // Ok(()) + // Ok(self + // .xc + // .set_transient_for(xid_from_buffer(&self.link_buffer))?) + // } +} + +// struct X11Connection { +// jokolay_window_id: u32, +// transient_for_atom: u32, +// // net_wm_pid_atom: u32, +// xc: RustConnection, +// } +// impl X11Connection { +// pub const WM_TRANSIENT_FOR: &'static str = "WM_TRANSIENT_FOR"; +// // pub const NET_WM_PID: &'static str = "_NET_WM_PID"; +// fn new(jokolay_window_id: u32) -> Result { +// let (xc, _) = RustConnection::connect(None).expect("failed to create x11 connection"); +// let transient_for_atom = intern_atom(&xc, true, Self::WM_TRANSIENT_FOR.as_bytes()) +// .map_err(|e| X11Error::AtomQueryError { +// source: e, +// atom_str: Self::WM_TRANSIENT_FOR, +// })? +// .reply() +// .map_err(|e| X11Error::AtomReplyError { +// source: e, +// atom_str: Self::WM_TRANSIENT_FOR, +// })? +// .atom; +// // let net_wm_pid_atom = intern_atom(&xc, true, Self::NET_WM_PID.as_bytes()) +// // .map_err(|e| X11Error::AtomQueryError { +// // source: e, +// // atom_str: Self::NET_WM_PID, +// // })? +// // .reply() +// // .map_err(|e| X11Error::AtomReplyError { +// // source: e, +// // atom_str: Self::NET_WM_PID, +// // })? +// // .atom; + +// Ok(Self { +// jokolay_window_id, +// transient_for_atom, +// xc, +// // net_wm_pid_atom, +// }) +// } +// pub fn set_transient_for(&self, parent_window: u32) -> Result<(), X11Error> { +// if let Ok(xst) = std::env::var("XDG_SESSION_TYPE") { +// if xst == "wayland" { +// tracing::warn!("skipping transient_for because we are on wayland"); +// return Ok(()); +// } +// if xst != "x11" { +// tracing::warn!("xdg session type is neither wayland not x11: {xst}"); +// } +// } +// assert_ne!(parent_window, 0); +// change_property( +// &self.xc, +// PropMode::REPLACE, +// self.jokolay_window_id, +// self.transient_for_atom, +// AtomEnum::WINDOW, +// 32, +// 1, +// &parent_window.to_ne_bytes(), +// ) +// .map_err(|e| X11Error::TransientForError { +// source: e, +// parent: parent_window, +// child: self.jokolay_window_id, +// })? +// .check() +// .map_err(|e| X11Error::TransientForReplyError { +// source: e, +// parent: parent_window, +// child: self.jokolay_window_id, +// })?; +// Ok(()) +// } + +// pub fn get_window_dimensions(&self, xid: u32) -> Result<[i32; 4]> { +// assert_ne!(xid, 0); +// let geometry = x11rb::protocol::xproto::get_geometry(&self.xc, xid) +// .into_diagnostic() +// .wrap_err("get geometry fn failed")? +// .reply() +// .into_diagnostic() +// .wrap_err("geometry reply is wrong")?; +// let translated_coordinates = x11rb::protocol::xproto::translate_coordinates( +// &self.xc, +// xid, +// geometry.root, +// geometry.x, +// geometry.y, +// ) +// .into_diagnostic() +// .wrap_err("failed to translate coords")? +// .reply() +// .into_diagnostic() +// .wrap_err("translate coords reply error")?; +// let x_outer = translated_coordinates.dst_x as i32; +// let y_outer = translated_coordinates.dst_y as i32; +// let width = geometry.width; +// let height = geometry.height; + +// tracing::debug!( +// "translated_x: {}, translated_y: {}, width: {}, height: {}, geo_x: {}, geo_y: {}", +// x_outer, +// y_outer, +// width, +// height, +// geometry.x, +// geometry.y +// ); +// Ok([x_outer, y_outer, width as _, height as _]) +// } +// // pub fn get_pid_from_xid(&self, xid: u32) -> Result { +// // assert_ne!(xid, 0); + +// // let pid_prop = get_property( +// // &self.xc, +// // false, +// // xid, +// // self.net_wm_pid_atom, +// // AtomEnum::CARDINAL, +// // 0, +// // 1, +// // ) +// // .expect("coudn't get _NET_WM_PID property gw2") +// // .reply() +// // .expect("reply for _NET_WM_PID property gw2 "); + +// // if pid_prop.bytes_after != 0 +// // && pid_prop.format != 32 +// // && pid_prop.value_len != 1 +// // && pid_prop.value.len() != 4 +// // { +// // panic!("invalid pid property {:#?}", pid_prop); +// // } +// // Ok(u32::from_ne_bytes(pid_prop.value.try_into().expect( +// // "pid property value has a bytes length of less than 4", +// // ))) +// // } +// } +// pub fn get_frame_extents(xc: &RustConnection, xid: u32) -> Result<(u32, u32, u32, u32)> { +// assert_ne!(xid, 0); +// let net_frame_extents_atom = intern_atom(&self.xc, true, b"_NET_FRAME_EXTENTS") +// .expect("coudn't intern atom for _NET_FRAME_EXTENTS ")? +// .reply() +// .expect("reply for intern atom for _NET_FRAME_EXTENTS")? +// .atom; +// let frame_prop = get_property( +// &self.xc, +// false, +// xid, +// net_frame_extents_atom, +// AtomEnum::ANY, +// 0, +// 100, +// ) +// .expect("coudn't get frame property gw2")? +// .reply() +// .expect("reply for frame property gw2")?; + +// if frame_prop.bytes_after != 0 { +// bail!( +// "bytes after in frame property is {}", +// frame_prop.bytes_after +// ); +// } +// if frame_prop.format != 32 { +// bail!("frame_prop format is {}", frame_prop.format); +// } +// if frame_prop.value_len != 4 { +// bail!("frame_prop value_len is {}", frame_prop.value_len); +// } +// if frame_prop.value.len() != 16 { +// bail!("frame_prop.value.len() is {}", frame_prop.value.len()); +// } +// // avoid bytemuck dependency and just do this raw. +// let mut arr = [0u8; 4]; +// arr.copy_from_slice(&frame_prop.value[0..4]); +// let left_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[4..8]); +// let right_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[8..12]); +// let top_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[12..16]); +// let bottom_border = u32::from_ne_bytes(arr); +// Ok((left_border, right_border, top_border, bottom_border)) +// } + +// pub fn get_gw2_pid(&mut self) -> Result { +// assert_ne!(self.gw2_window_handle, 0); +// let pid_atom = x11rb::protocol::xproto::intern_atom(&self.&self.xc, true, b"_NET_WM_PID") +// .expect("could not intern atom '_NET_WM_PID'")? +// .reply() +// .expect("reply error while interning '_NET_WM_PID'.")? +// .atom; +// let reply = x11rb::protocol::xproto::get_property( +// &self.&self.xc, +// false, +// self.gw2_window_handle, +// pid_atom, +// x11rb::protocol::xproto::AtomEnum::CARDINAL, +// 0, +// 1, +// ) +// .expect("could not request '_NET_WM_PID' for gw2 window handle ")? +// .reply() +// .expect("the reply for '_NET_WM_PID' of gw2 handle ")?; + +// let pid_format = 32; +// if pid_format != reply.format { +// bail!("pid_format is not 32. so, type is wrong"); +// } +// let pid_buffer_size = 4; +// if pid_buffer_size != reply.value.len() { +// bail!("pid_buffer is not 4 bytes"); +// } +// let value_len = 1; +// if value_len != reply.value_len { +// bail!("pid reply's value_len is not 1"); +// } +// let remaining_bytes_len = 0; +// if remaining_bytes_len != reply.bytes_after { +// bail!("we still have too many bytes remaining after reading '_NET_WM_PID'"); +// } +// let mut buffer = [0u8; 4]; +// buffer.copy_from_slice(&reply.value); +// Ok(u32::from_ne_bytes(buffer)) +// } diff --git a/crates/joko_link/src/mumble/ctypes.rs b/crates/joko_link/src/mumble/ctypes.rs new file mode 100644 index 0000000..72dd4ac --- /dev/null +++ b/crates/joko_link/src/mumble/ctypes.rs @@ -0,0 +1,288 @@ +use enumflags2::BitFlags; +use miette::bail; +use serde::{Deserialize, Serialize}; + +use crate::{UISize, UIState}; + +/// The total size of the CMumbleLink struct. used to know the amount of memory to give to win32 call that creates the shared memory +pub const C_MUMBLE_LINK_SIZE_FULL: usize = std::mem::size_of::(); +/// This is how much of the CMumbleLink memory that is actually useful and updated. the rest is just zeroed out. +pub const USEFUL_C_MUMBLE_LINK_SIZE: usize = 1196; + +/// The CMumblelink is how it is represented in the memory. But we rarely use it as it is and instead convert it into MumbleLink before using it for convenience +/// Many of the fields are documentad in the actual MumbleLink struct +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct CMumbleLink { + //// The ui_version will always be same as mumble doesn't change. we will come back to change it IF there's a new version. + pub ui_version: u32, + //// This tick represents the update count of the link (which is usually the frame count ) since mumble was initialized. not from the start of game, but the start of mumble + pub ui_tick: u32, + //// position of the character + pub f_avatar_position: [f32; 3], + //// direction towards which the character is facing + pub f_avatar_front: [f32; 3], + //// the up direction vector of the character. + pub f_avatar_top: [f32; 3], + //// The name of the character currently logged in + pub name: [u16; 256], + //// The position of the camera + pub f_camera_position: [f32; 3], + //// The direction towards which the camera is facing + pub f_camera_front: [f32; 3], + //// The up direction for the camera + pub f_camera_top: [f32; 3], + //// This is a widestring of json containing the serialized data of [CIdentity] + pub identity: [u16; 256], + //// The [Self::context] field is 256 bytes, but the game only uses the first few bytes. + //// The first 48 bytes are used by mumble to uniquely identify the map/instance/room of the player + //// So, this field is always set to 48 bytes. + //// But gw2 writes even more data for the sake of addon functionality like minimap position etc.. + //// So, adding another 37 bytes which gw2 writes to. The total length of context is roughly 88 bytes if we consider the alignment. + pub context_len: u32, + //// 88 bytes are useful context written by gw2. Jokolink writes some more additional data beyond the 88 bytes like + //// X11 ID or window size or the timestamp when it last wrote data to this link etc.. which is useful for linux native clients like jokolay + pub context: CMumbleContext, + // Useless for now. Nothing is ever written here. + // we will just remove this field and add the size when creating shared memory. + // no point in copying more than 5kb when we only care about the first 1kb. + // pub description: [u16; 2048], +} +impl Default for CMumbleLink { + fn default() -> Self { + Self { + ui_version: Default::default(), + ui_tick: Default::default(), + f_avatar_position: Default::default(), + f_avatar_front: Default::default(), + f_avatar_top: Default::default(), + name: [0; 256], + f_camera_position: Default::default(), + f_camera_front: Default::default(), + f_camera_top: Default::default(), + identity: [0; 256], + context_len: Default::default(), + context: Default::default(), + // description: [0; 2048], + } + } +} + +impl CMumbleLink { + /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read + pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { + unsafe { std::ptr::read_volatile(link_ptr) } + } + + /// Checks if the MumbleLink memory is actually initialized by checking if [CMumbleLink::ui_tick] is non-zero. + /// Even if it returns true because [`CMumbleLink::ui_tick`] is non-zero, it could be a remnant from an older gw2 process. + /// The only way to verify that gw2 is active (with a character logged into a map), is to check if the tick changed from last frame to current frame. + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + pub unsafe fn is_valid(link_ptr: *const CMumbleLink) -> bool { + unsafe { (*link_ptr).ui_tick > 0 } + } + + /// gets uitick if we want to know the frame number since initialization of CMumbleLink + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then return value will be zero + /// 3. Even if it is not zero, the ui_tick maybe a stale because the game is dead (or in map loading screen / character select screen / cutscene) + pub unsafe fn get_ui_tick(link_ptr: *const CMumbleLink) -> u32 { + (*link_ptr).ui_tick + } + /// gets the pid from [CMumbleLink::context] field + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then pid will be zero + /// 3. Even if it is initialized, the process could be dead and the pid may be reused for a different process now + pub unsafe fn get_pid(link_ptr: *const CMumbleLink) -> u32 { + (*link_ptr).context.process_id + } + // #[cfg(unix)] + // pub unsafe fn get_xid(link_ptr: *const CMumbleLink) -> u32 { + // (*link_ptr).context.xid + // } + // #[cfg(unix)] + // pub unsafe fn get_pos_size(link_ptr: *const CMumbleLink) -> [i32; 4] { + // (*link_ptr).context.client_pos_size + // } + /// This gets the timestamp written by `jokolink` + /// The return value is nanoseconds since unix_epoch. + /// This is an easy way to check that jokolink (and by extension gw2) is still alive even if ui_tick doesn't change. + /// This happens when gw2 is in character select screen or cutscene etc.. when ui_tick stops updating. + /// # Safety + /// 1. `link_ptr` must be valid and point to memory of atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If it is uninitialized, the return value could be zero + #[cfg(unix)] + pub unsafe fn get_timestamp(link_ptr: *const CMumbleLink) -> i128 { + let bytes = (*link_ptr).context.timestamp; + i128::from_le_bytes(bytes) + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +/// The mumble context as stored inside the context field of CMumbleLink. +/// the first 48 bytes Mumble uses for identification is upto `build_id` field +/// the rest of the fields after `build_id` are provided by gw2 for addon devs. +pub struct CMumbleContext { + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + pub server_address: [u8; 28], // contains sockaddr_in or sockaddr_in6 + /// Map ID + pub map_id: u32, + pub map_type: u32, + pub shard_id: u32, + pub instance: u32, + pub build_id: u32, + /// The fields until now are provided for mumble. + /// The rest of the data from here is what gw2 provides for the benefit of addons. + /// This is the current UI state of the game. refer to [UIState] + /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat + pub ui_state: u32, + pub compass_width: u16, // pixels + pub compass_height: u16, // pixels + pub compass_rotation: f32, // radians + pub player_x: f32, // continentCoords + pub player_y: f32, // continentCoords + pub map_center_x: f32, // continentCoords + pub map_center_y: f32, // continentCoords + pub map_scale: f32, + /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. + /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid + /// on windows, we use this to get window handle which can give us a window size. + /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid + /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 + pub process_id: u32, + /// refers to [Mount] + /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match api + pub mount_index: u8, + /// This is where the context fields provided by gw2 end. + /// From here on, these are custom fields set by jokolink.dll for the use of jokolay + /// These fields will be set before writing the link data to the `/dev/shm/MumbleLink` file from which jokolay can pick it up + /// + /// timestamp when jokolink wrote this data. unix nanoseconds + /// This timestamp will be written every frame by jokolink even if mumble link is uninitialized. + /// This is [i128] in little endian byte order. We use a byte array instead of [i128] directly because context is aligned to 4 by default. And + /// [i64]/[i128] will change that alignment to 8. This will lead to 4 bytes padding between [CMumbleLink::context_len] and [CMumbleLink::context] + /// + /// If jokolink doesn't write for more than 1 or 2 seconds, it can be safely assumed that gw2 was closed/crashed. + /// This is in nanoseconds since unix epoch in UTC timezone. + pub timestamp: [u8; 16], + /// This represents the x11 window id of the gw2 window. AFAIK, wine uses x11 only (no wayland), so this could be useful to set transient for + pub xid: u32, + /* + pub window_pos_size_without_borders: [i32; 4], + /// x, y, width, height of guild wars 2 window relative to top left corner of the screen. + /// This is populated with `GetWindowRect` fn + /// DPI aware. In screen coordinate. But includes drop shadow too :(. + pub window_pos_size: [i32; 4], + */ + /// dpi awareness of the gw2 process. Most probably will be `2` and below we have the relevant MS docs + /// DPI_AWARENESS_PER_MONITOR_AWARE + /// Value: 2 + /// Per monitor DPI aware. This process checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes. These processes are not automatically scaled by the system. + pub dpi_scaling: i32, + /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. + pub dpi: i32, + /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. + pub client_pos: [i32; 2], + pub client_size: [u32; 2], + /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. + pub padding: [u8; 96], +} +impl Default for CMumbleContext { + fn default() -> Self { + assert_eq!(std::mem::size_of::(), 256); + Self { + server_address: Default::default(), + map_id: Default::default(), + map_type: Default::default(), + shard_id: Default::default(), + instance: Default::default(), + build_id: Default::default(), + ui_state: Default::default(), + compass_width: Default::default(), + compass_height: Default::default(), + compass_rotation: Default::default(), + player_x: Default::default(), + player_y: Default::default(), + map_center_x: Default::default(), + map_center_y: Default::default(), + map_scale: Default::default(), + process_id: Default::default(), + mount_index: Default::default(), + timestamp: Default::default(), + // window_pos_size: Default::default(), + padding: [0; 96], + xid: Default::default(), + // window_pos_size_without_borders: Default::default(), + dpi_scaling: Default::default(), + dpi: Default::default(), + client_pos: Default::default(), + client_size: Default::default(), + } + } +} +impl CMumbleContext { + pub fn get_ui_state(&self) -> Option> { + BitFlags::from_bits(self.ui_state).ok() + } + + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + /// contains sockaddr_in or sockaddr_in6 + pub fn get_map_ip(&self) -> miette::Result { + if self.server_address[0] != 2 { + // add ipv6 support when gw2 servers add ipv6 support. + bail!("ipaddr parsing failed for CMumble Context"); + } + let ip = std::net::Ipv4Addr::from([ + self.server_address[4], + self.server_address[5], + self.server_address[6], + self.server_address[7], + ]); + Ok(ip) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] +#[serde(crate = "serde")] +/// The json structure of the Identity field inside Cmumblelink. +/// the json string is null terminated and utf-16 encoded. so, need to use +/// Widestring crate's U16Cstring to first parse the bytes and then, convert to +/// String before deserializing to CIdentity +pub struct CIdentity { + /// The name of the character + pub name: String, + /// The core profession id of the character. matches the ids of v2/professions endpoint + pub profession: u32, + /// Character's third specialization, or 0 if no specialization is present. See /v2/specializations for valid IDs. + pub spec: u32, + /// The race of the character. does not match api + pub race: u32, + /// API:2/maps + pub map_id: u32, + /// useless field from pre-megaserver days. is just shard_id from context struct + pub world_id: u32, + /// Team color per API:2/colors (0 = white) + pub team_color_id: u32, + /// Whether the character has a commander tag active + pub commander: bool, + /// Vertical field-of-view + pub fov: f32, + /// A value corresponding to the user's current UI scaling. + pub uisz: u32, +} + +impl CIdentity { + pub fn get_uisz(&self) -> Option { + Some(match self.uisz { + 0 => UISize::Small, + 1 => UISize::Normal, + 2 => UISize::Large, + 3 => UISize::Larger, + _ => return None, + }) + } +} diff --git a/crates/joko_link/src/mumble/mod.rs b/crates/joko_link/src/mumble/mod.rs new file mode 100644 index 0000000..16a38d3 --- /dev/null +++ b/crates/joko_link/src/mumble/mod.rs @@ -0,0 +1,173 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod ctypes; +use std::net::IpAddr; + +use enumflags2::{bitflags, BitFlags}; +use num_derive::FromPrimitive; +use num_derive::ToPrimitive; + +use joko_core::serde_glam::*; +use serde::{Deserialize, Serialize}; + +/// As the CMumbleLink has all the fields multiple +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MumbleLink { + /// ui tick. (more or less represents the frame number of gw2) + pub ui_tick: u32, + /// character position + pub player_pos: Vec3, + /// direction char is facing + pub f_avatar_front: Vec3, + /// camera position + pub cam_pos: Vec3, + /// direction camera is facing + pub f_camera_front: Vec3, + /// The name of the character + pub name: String, + /// API:2/maps + pub map_id: u32, + pub map_type: u32, + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + pub server_address: IpAddr, // contains sockaddr_in or sockaddr_in6 + pub shard_id: u32, + pub instance: u32, + pub build_id: u32, + /// The fields until now are provided for mumble. + /// The rest of the data from here is what gw2 provides for the benefit of addons. + /// This is the current UI state of the game. refer to [UIState] + /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat + pub ui_state: Option>, + pub compass_width: u16, // pixels + pub compass_height: u16, // pixels + pub compass_rotation: f32, // radians + pub player_x: f32, // continentCoords + pub player_y: f32, // continentCoords + pub map_center_x: f32, // continentCoords + pub map_center_y: f32, // continentCoords + pub map_scale: f32, + /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. + /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid + /// on windows, we use this to get window handle which can give us a window size. + /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid + /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 + pub process_id: u32, + /// refers to [Mount] + /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api + //pub mount: Option, + //pub race: Race, + pub mount: u8, + pub race: u32, + + /// Vertical field-of-view + pub fov: f32, + /// A value corresponding to the user's current UI scaling. + pub uisz: UISize, + // pub window_pos: IVec2, + // pub window_size: IVec2, + // pub window_pos_without_borders: IVec2, + // pub window_size_without_borders: IVec2, + /// This is the dpi of gw2 window. 96dpi is the default for a non-hidpi monitor with scaling 1.0 + /// for a scaling of 2.0, it becomes 192 and so on. + pub dpi: i32, + /// This is whether gw2 is scaling its UI elements to match the dpi. So, if the dpi is bigger than 96, gw2 will make text/ui bigger. + /// -1 means we couldn't get the setting from gw2's config file in appdata/roaming + /// 0 means scaling is disabled (false) + /// 1 means scaling is enabled (true). + pub dpi_scaling: i32, + /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* + pub client_pos: IVec2, + /// This is the size of gw2's viewport (width/height) in screen coordinates + pub client_size: UVec2, + /// changes since last mumble link update + pub changes: BitFlags, +} +impl Default for MumbleLink { + fn default() -> Self { + Self { + ui_tick: Default::default(), + player_pos: Default::default(), + f_avatar_front: Default::default(), + cam_pos: Default::default(), + f_camera_front: Default::default(), + name: String::from("This Is Jokolay Dummy"), + map_id: Default::default(), + map_type: Default::default(), + server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), + shard_id: Default::default(), + instance: Default::default(), + build_id: Default::default(), + ui_state: Default::default(), + compass_width: Default::default(), + compass_height: Default::default(), + compass_rotation: Default::default(), + player_x: Default::default(), + player_y: Default::default(), + map_center_x: Default::default(), + map_center_y: Default::default(), + map_scale: Default::default(), + process_id: Default::default(), + mount: Default::default(), + race: u32::MAX, + fov: 2.0, + uisz: Default::default(), + dpi: Default::default(), + dpi_scaling: 96, + client_pos: Default::default(), + client_size: UVec2(glam::UVec2 { x: 1024, y: 768 }), + changes: Default::default(), + } + } +} +/// These flags represent the changes in mumble link compared to previous values +#[bitflags] +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum MumbleChanges { + UiTick = 1, + Map = 1 << 1, + Character = 1 << 2, + WindowPosition = 1 << 3, + WindowSize = 1 << 4, + Camera = 1 << 5, + Position = 1 << 6, +} + +/// represents the ui scale set in settings -> graphics options -> interface size +#[derive( + Debug, + Clone, + Default, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + FromPrimitive, + ToPrimitive, +)] +#[serde(crate = "serde")] +pub enum UISize { + Small = 0, + #[default] + Normal = 1, + Large = 2, + Larger = 3, +} + +#[bitflags] +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +/// The Uistate enum to represent the status of the UI in game +pub enum UIState { + IsMapOpen = 0b00000001, + IsCompassTopRight = 0b00000010, + DoesCompassHaveRotationEnabled = 0b00000100, + GameHasFocus = 0b00001000, + InCompetitiveGamemode = 0b00010000, + TextboxFocus = 0b00100000, + IsInCombat = 0b01000000, +} diff --git a/crates/joko_link/src/win/dll.rs b/crates/joko_link/src/win/dll.rs new file mode 100644 index 0000000..721b5fe --- /dev/null +++ b/crates/joko_link/src/win/dll.rs @@ -0,0 +1,490 @@ +#![allow(non_snake_case)] + +arcdps::arcdps_export! { + name: "jokolink", + // This is just "joko" as hex bytes + sig: 0x6a6f6b6f, + init: init, + release: release, +} + +fn init() -> ::core::result::Result<(), Box> { + println!("jokolink init called by arcdps. spawning background thread for jokolink"); + unsafe { spawn_jokolink_thread() }; + Ok(()) +} +/// If no other thread has been spawned, this will spawn a new thread where jokolink will run +unsafe fn spawn_jokolink_thread() { + if d3d11::JOKOLINK_THREAD_HANDLE.is_none() { + let (quit_request_sender, quit_request_receiver) = std::sync::mpsc::sync_channel(0); + let (quit_response_sender, quit_response_receiver) = std::sync::mpsc::sync_channel(1); + + d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); + d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); + + let th = std::thread::Builder::new() + .name("jokolink thread".to_string()) + .spawn(move || { + d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); + "jokolink thread quit" + }); + match th { + Ok(handle) => { + println!("spawned jokolink thread. handle: {handle:?}"); + d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); + } + Err(e) => { + eprintln!("failed to spawn jokolink thread due to error {e:#?}"); + } + } + } else { + println!("jokolink thread has already been initialized, so skipping initialization."); + } +} +/// This is really unsafe, so we have to be careful +/// We cannot directly terminate thread because it might lead to some syncronization issues and cause a crash/deadlock +/// we HAVE to terminate the thread because otherwise, it will crash gw2 too. +/// So, we use channels to send a signal to jokolink thread to quit. +/// Then, we use another channel to wait and receive a signal that will be sent by jokolink thread when it terminates. +/// +/// We can't call `join` on the thread handle because.. like i said, it can lead to a deadlock/crash. +/// This applies whether we are loaded by game as d3d11.dll or by arcdps as an addon. +unsafe fn terminate_jokolink_thread() { + if let Some(sender) = d3d11::JOKOLINK_QUIT_REQUESTER.take() { + if let Err(e) = sender.send(()) { + eprintln!("failed to send quit signal due to error {e:#?}"); + } else { + println!("successfully sent the quit signal to the jokolink thread"); + } + } + if let Some(receiver) = d3d11::JOKOLINK_QUIT_RESPONDER.take() { + match receiver.recv() { + Ok(_) => { + println!("received quit response from jokolink thread"); + } + Err(e) => { + eprintln!("failed to receive quit response from jokolink thread. {e:#?}"); + } + } + } + if let Some(handle) = d3d11::JOKOLINK_THREAD_HANDLE.take() { + if handle.is_finished() { + println!("jokolink thread is finished"); + } else { + println!("jokolink thread is not yet finished, so waiting for it by joining the handle :(((("); + match handle.join() { + Ok(o) => { + println!("joined jokolink thread with return value: {o}"); + } + Err(e) => { + eprintln!("jokolink thread panic: {e:?}"); + } + } + } + } else { + println!("jokolink thread was never started. So, nothing to terminate"); + } +} +fn release() { + println!("jokolink release called by arcdps."); + unsafe { + terminate_jokolink_thread(); + } +} + +pub mod d3d11 { + use std::{ + sync::mpsc::{Receiver, SyncSender}, + thread::JoinHandle, + }; + + use windows::{ + core::*, + Win32::Foundation::*, + Win32::System::{ + LibraryLoader::{GetProcAddress, LoadLibraryA}, + SystemInformation::GetSystemDirectoryA, + // Threading::{CreateThread, TerminateThread, THREAD_CREATION_FLAGS}, + }, + }; + + /// Dll injection basics: + /// 1. You write a custom dll library exposing functions that match the names/signatures of the actual winapi functions + /// 2. Then, you place your custom dll library in gw2's executable directory. + /// 3. gw2 loads your dll and calls your functions thinking it is calling winapi functions. + /// 4. You will use this chance to do whatever you want, before forwarding the calls to the actual winapi functions + /// 5. So, we will load the dll from `system32` directory once. store it in [DLL_PTR] + /// 6. When a function is called, we check if the fn pointer is already loaded. If it is not, we get it from the dll pointer + static mut DLL_PTR: HMODULE = HMODULE(0); + static mut CREATE_DEVICE_FNPTR: Option< + unsafe extern "system" fn( + padapter: *mut ::core::ffi::c_void, + drivertype: i32, + software: HMODULE, + flags: u32, + pfeaturelevels: *const i32, + featurelevels: u32, + sdkversion: u32, + ppdevice: *mut *mut ::core::ffi::c_void, + pfeaturelevel: *mut i32, + ppimmediatecontext: *mut *mut ::core::ffi::c_void, + ) -> HRESULT, + > = None; + pub static mut JOKOLINK_THREAD_HANDLE: Option> = None; + + /// This is used to tell wine_main fn thread to quit. + pub static mut JOKOLINK_QUIT_REQUESTER: Option> = None; + /// This is used to wait for wine_main fn thread to quit and send us a signal + pub static mut JOKOLINK_QUIT_RESPONDER: Option> = None; + /// This function is called whenever the dll is loaded into process or thread, and whenever the dll is unloaded out of process/thread. + /// # Safety + /// Don't do *anything* complicated at all. It can easily lead to a deadlock + /// https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices + /// Improper synchronization within DllMain can cause an application to deadlock or access data or code in an uninitialized DLL. + #[no_mangle] + pub unsafe extern "system" fn DllMain( + _dll_module: HINSTANCE, + call_reason: u32, + _: *mut (), + ) -> bool { + match call_reason { + // process detach + 0 => { + // unlike attach + println!("jokolink dll is being detached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}."); + super::terminate_jokolink_thread(); + } + // process attach + 1 => { + // Sometimes, our dll might be attached/detached multiple times. And we don't want to start jokolink_thread everything time + // Instead, we only launch our jokolink thread when the D3D11CreateDevice is called + println!("jokolink dll has been attached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}"); + } + // thread attach and detach + 2 | 3 => { + // no need to do anything for thread attach and thread detach + } + // invalid values + rest => { + eprintln!("unrecognized dll main call reason: {rest}"); + } + } + true + } + /// This is the function we will "hook" into. + /// GW2 will call this function right after the "login window" when creating the main window + /// This is where we initialize our jokolink thread. + /// # Safety + /// Just need to load d3d11.dll from windows/system32 equivalent directory and call that function for gw2 + #[no_mangle] + pub unsafe extern "system" fn D3D11CreateDevice( + padapter: *mut ::core::ffi::c_void, + drivertype: i32, + software: HMODULE, + flags: u32, + pfeaturelevels: *const i32, + featurelevels: u32, + sdkversion: u32, + ppdevice: *mut *mut ::core::ffi::c_void, + pfeaturelevel: *mut i32, + ppimmediatecontext: *mut *mut ::core::ffi::c_void, + ) -> HRESULT { + if DLL_PTR.is_invalid() { + let mut path = [0u8; MAX_PATH as _]; + let len = GetSystemDirectoryA(Some(&mut path)) as usize; + // we make sure that len is not zero. It means that GetSystemDirectoryA fn didn't fail. + // we also check if length is above 200, because then we might be reaching the limit of maximum path length supported by windows. + if len == 0 || len > 200 { + eprintln!("the system directory path size is: {len}. So, i am quitting"); + return HRESULT::default(); + } + const D3D11_DLL_PATH: &str = "\\d3d11.dll\0"; + path[len..(len + D3D11_DLL_PATH.len())].copy_from_slice(D3D11_DLL_PATH.as_bytes()); + + match LoadLibraryA(PCSTR::from_raw(path.as_ptr())) { + Ok(p) => { + println!("successfully loaded library d3d11.dll "); + DLL_PTR = p; + } + Err(e) => { + eprintln!("could not load d3d11.dll from system path due to error: {e:#?}"); + return HRESULT::default(); + } + } + } else { + println!("d3d11.dll library is already loaded. So, skipping that"); + } + if CREATE_DEVICE_FNPTR.is_none() { + if let Some(p) = GetProcAddress(DLL_PTR, PCSTR("D3D11CreateDevice\0".as_ptr())) { + println!("successfully got proc address of D3D11CreateDevice"); + let _ = CREATE_DEVICE_FNPTR.insert(std::mem::transmute(p)); + } else { + eprintln!("could not load address of D3D11CreateDevice"); + } + } else { + println!("D3D11CreateDevice fn ptr is already loaded, so skipped that"); + } + if JOKOLINK_THREAD_HANDLE.is_none() { + println!("starting jokolink's wine_main on another thrad"); + + super::spawn_jokolink_thread(); + } + println!("calling D3D11CreateDevice fn"); + if let Some(p) = CREATE_DEVICE_FNPTR { + p( + padapter, + drivertype, + software, + flags, + pfeaturelevels, + featurelevels, + sdkversion, + ppdevice, + pfeaturelevel, + ppimmediatecontext, + ) + } else { + HRESULT::default() + } + } + + // unsafe extern "system" fn wine_main(_: *mut ::core::ffi::c_void) -> u32 { + // super::spawn_jokolink_thread(); + // 0 + // } + pub mod wine { + use crate::mumble::ctypes::*; + use crate::win::MumbleWinImpl; + use crate::DEFAULT_MUMBLELINK_NAME; + use miette::{Context, IntoDiagnostic, Result}; + use serde::{Deserialize, Serialize}; + use std::io::Write; + use std::io::{Seek, SeekFrom}; + use std::path::{Path, PathBuf}; + use std::str::FromStr; + use std::sync::mpsc::{Receiver, SyncSender}; + use std::time::Duration; + use tracing::{error, info}; + use tracing_subscriber::filter::LevelFilter; + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(default)] + pub struct JokolinkConfig { + pub loglevel: String, + pub logdir: PathBuf, + pub mumble_link_name: String, + pub interval: u32, + pub copy_dest_dir: PathBuf, + } + + impl Default for JokolinkConfig { + fn default() -> Self { + Self { + loglevel: "info".to_string(), + logdir: PathBuf::from("."), + mumble_link_name: DEFAULT_MUMBLELINK_NAME.to_string(), + interval: 5, + copy_dest_dir: PathBuf::from("z:\\dev\\shm"), + } + } + } + + pub fn wine_main( + quit_request_receiver: Receiver<()>, + quit_response_sender: SyncSender<()>, + ) { + if let Err(e) = std::panic::catch_unwind(move || { + let config = "./jokolink_config.json".to_string(); + let config = std::path::PathBuf::from(config); + if !config.exists() { + match std::fs::File::create(&config) { + Ok(mut f) => match serde_json::to_string_pretty(&JokolinkConfig::default()) + { + Ok(config_string) => { + if let Err(e) = f.write_all(config_string.as_bytes()) { + eprintln!( + "failed to write default config file due to error {e:#?}" + ); + } + } + Err(e) => { + eprintln!("failed to serialize default config due to error {e:#?}"); + } + }, + Err(e) => eprintln!("failed to create config.json due to error {e:#?}"), + } + } + let config: JokolinkConfig = match std::fs::File::open(&config) { + Ok(f) => match serde_json::from_reader(std::io::BufReader::new(f)) { + Ok(config) => config, + Err(e) => { + eprintln!("failed to deserialize config file due to error {e:#?}"); + return; + } + }, + Err(e) => { + eprintln!("failed to open config file due to error {e:#?}"); + return; + } + }; + println!("successfully loaded configuration file"); + match miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + .unicode(true) + .context_lines(4) + .with_cause_chain() + .build(), + ) + })) { + Ok(_) => { + println!("miette hook set"); + } + Err(e) => { + eprintln!("failed to set miette hook due to {e:#?}"); + } + } + let guard = match log_init( + LevelFilter::from_str(&config.loglevel).unwrap_or(LevelFilter::INFO), + &config.logdir, + Path::new("jokolink.log"), + ) { + Ok(g) => g, + Err(e) => { + eprintln!("failed to initiailize logging due to error {e:#?}"); + return; + } + }; + if let Err(e) = fake_main(config, quit_request_receiver) { + eprintln!("fake main exited due to error: {e:#?}"); + } + std::mem::drop(guard); + println!("dropped logfile guard"); + }) { + eprintln!("There was a panic in jokolink thread: {e:?}"); + } + println!("exiting wine_main function"); + match quit_response_sender.send(()) { + Ok(_) => { + println!("successfully sent quit response"); + } + Err(e) => { + eprintln!("failed to send quit response due to: {e:#?}"); + } + } + } + + fn fake_main(config: JokolinkConfig, quit_signal: Receiver<()>) -> Result<()> { + let refresh_inverval = Duration::from_millis(config.interval as u64); + + info!("Application Name: {}", env!("CARGO_PKG_NAME")); + info!("Application Version: {}", env!("CARGO_PKG_VERSION")); + info!("Application Authors: {}", env!("CARGO_PKG_AUTHORS")); + info!( + "Application Repository Link: {}", + env!("CARGO_PKG_REPOSITORY") + ); + info!("Application License: {}", env!("CARGO_PKG_LICENSE")); + + // info!("git version details: {}", git_version::git_version!()); + + info!( + "the file log lvl: {:?}, the logfile directory: {:?}", + &config.loglevel, &config.logdir + ); + info!("created app and initialized logging"); + info!("the mumble link names: {:#?}", &config.mumble_link_name); + info!( + "the mumble refresh interval in milliseconds: {:#?}", + refresh_inverval + ); + + info!( + "the path to which we write mumble data: {:#?}", + &config.copy_dest_dir + ); + let mumble_key = config.mumble_link_name.clone(); + + let dest_path = config.copy_dest_dir.join(&mumble_key); + + // create a shared memory file in /dev/shm/mumble_link_key_name so that jokolay can mumble stuff from there. + info!( + "creating the path to destination shm file: {:?}", + &dest_path + ); + + #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] + let mut mfile = std::fs::File::options() + .write(true) + .create(true) + .open(&dest_path) + .into_diagnostic() + .wrap_err_with(|| { + format!("failed to create shm file with path {:#?}", &dest_path) + })?; + // create shared memory using the mumble link key + let mut source = MumbleWinImpl::new(&mumble_key)?; + + loop { + if let Err(e) = source.tick() { + error!(?e, "mumble tick error"); + } + let link = source.get_cmumble_link(); + + let buffer: [u8; C_MUMBLE_LINK_SIZE_FULL] = + unsafe { std::ptr::read_volatile(&link as *const CMumbleLink as *const _) }; + mfile + .seek(SeekFrom::Start(0)) + .into_diagnostic() + .wrap_err("could not seek to start of shared memory file due to error")?; + + // write buffer to the file + mfile + .write(&buffer) + .into_diagnostic() + .wrap_err("could not write to shared memory file due to error")?; + match quit_signal.try_recv() { + Ok(_) => { + println!("received quit signal. returning from wine_main()"); + error!("received quit signal. returning from wine_main()"); + return Ok(()); + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + eprintln!("why is the quit signaller sender disconnected????"); + } + }, + } + // we sleep for a few milliseconds to avoid reading mumblelink too many times. we will read it around 100 to 200 times per second + std::thread::sleep(refresh_inverval); + } + } + + /// initializes global logging backend that is used by log macros + /// Takes in a filter for stdout/stderr, a filter for logfile and finally the path to logfile + pub fn log_init( + file_filter: LevelFilter, + log_directory: &Path, + log_file_name: &Path, + ) -> Result { + // let file_appender = tracing_appender::rolling::never(log_directory, log_file_name); + let file_path = log_directory.join(log_file_name); + let writer = std::io::BufWriter::new( + std::fs::File::create(&file_path) + .into_diagnostic() + .wrap_err_with(|| { + format!("failed to create logfile at path: {:#?}", &file_path) + })?, + ); + let (nb, guard) = tracing_appender::non_blocking(writer); + tracing_subscriber::fmt() + .with_writer(nb) + .with_max_level(file_filter) + .pretty() + .with_ansi(false) + .init(); + + Ok(guard) + } + } +} diff --git a/crates/joko_link/src/win/mod.rs b/crates/joko_link/src/win/mod.rs new file mode 100644 index 0000000..21ebc75 --- /dev/null +++ b/crates/joko_link/src/win/mod.rs @@ -0,0 +1,735 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod dll; +//putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration + +use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; +use miette::{bail, Context, IntoDiagnostic, Result}; +use notify::Watcher; +use std::{ + path::PathBuf, + str::FromStr, + time::{Duration, Instant}, +}; +use time::OffsetDateTime; +use tracing::{debug, error, info, warn}; +use windows::{ + core::PCSTR, + Win32::{ + Foundation::*, + Graphics::{ + Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}, + Gdi::ClientToScreen, + }, + System::{ + Com::CoTaskMemFree, + Memory::*, + Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}, + }, + UI::{ + HiDpi::{GetDpiForWindow, GetProcessDpiAwareness}, + Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath}, + WindowsAndMessaging::*, + }, + }, +}; + +/// This source will be the used to abstract the linux/windows way of getting MumbleLink +/// on windows, this represents the shared memory pointer to mumblelink, and as long as one of gw2 or a client like us is alive, the shared memory will stay alive +/// on linux, this will be a File in /dev/shm that will only exist if jokolink created it at some point in time. this lives in ram, so reading from it is pretty much free. +#[derive(Debug)] +pub struct MumbleWinImpl { + /// This is the pointer to shared memory which we mapped into our address space + /// This is NEVER null. Because we consider failing to create MumbleLink as a hard error. + /// ## Unsafe: + /// Must unmap this pointer when we are dropping + link_ptr: *const CMumbleLink, + /// This is the handle to shared memory. We must close the handle when we are quitting + /// This also never invalid. Because we consider failing to create MumbleLink as a hard error. + /// ## Unsafe: + /// Must close this handle when we are dropping + mumble_handle: HANDLE, + /// this is the previous ui_tick. We use this to check if there has been any change in mumble link memory + /// If there is a change, then we check if the new pid is the same as old pid + previous_ui_tick: u32, + /// This is the previous pid of the mumble link + /// If the current pid has changed, then it means we are dealing with a new gw2 process. + previous_pid: u32, + /// This is the process handle for gw2. + /// when we see a change in pid, we will close the handle (if its valid) and open a new handle to the new gw2 process + /// + /// This handle is very important, because its validity shows that the gw2 process is "alive". + /// If ui_tick has not changed for more than a second, then we will check using windows api if the process is still alive. + /// If not, we will reset everything in our struct except for last_pid and last_ui_tick. + process_handle: HANDLE, + /// if ui_tick updates, we set this to now. + /// If ui_tick doesn't update for more than 1 second AND we are alive, we will check if gw2 is still alive and reset the timestamp. + last_ui_tick_update: Instant, + /// if ui_tick changes this frame and we are alive, we get window size/pos of gw2 and reset this. + /// if we are not alive, then we simply skip this check. + last_pos_size_check: Instant, + + /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. + client_pos: [i32; 2], + client_size: [u32; 2], + /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + /// 0 for false + /// 1 for true + /// -1 for no idea. maybe because we couldn't find the config or read it or whatever. + /// I recommend just assuming that it is true when in doubt. Because the text is too small to read when dpi scaling is turned off. + dpi_scaling: i32, + /// DPI of the gw2 window + /// We get this via win32 api + dpi: i32, + /// This is the window handle of gw2. + /// This is automatically set when we try to get window size/pos. and will be reset if gw2 process dies or if we find a new gw2 process. + window_handle: isize, + /// X11 window id. This is only useful for jokolink when it is run as dll on wine + /// When the struct is initialized, we also try to get xid. and keep it here. On windows, we will just keep it at zero. + xid: u32, + /// This is the $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + /// But we get this programmatically via ShGetKnownFolderPath + _gw2_config_watcher: notify::RecommendedWatcher, + gw2_config_changed: std::sync::Arc, + gw2_config_path: PathBuf, /* + /// This is the position and size of gw2 window. This also includes a few hidden pixels around gw2 which serve as the border + /// Every time we check if the process is alive + window_pos_size: [i32; 4], + /// same as above. But we use DwmGetWindowAttribute, to exclude the drop shadow borders from the window rect + window_pos_size_without_borders: [i32; 4], + */ +} + +unsafe impl Send for MumbleWinImpl {} + +impl MumbleWinImpl { + pub fn new(key: &str) -> Result { + unsafe { + let (handle, link_ptr) = + create_link_shared_mem(key).wrap_err("failed to create mumblelink shm ")?; + let gw2_config_changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let gw2_config_path = { + let roaming_appdata_pwstr = SHGetKnownFolderPath( + &FOLDERID_RoamingAppData as *const _, + Default::default(), + HANDLE::default(), + ) + .into_diagnostic() + .wrap_err("failed to get known folder roaming app data path")?; + + let mut roaming_str = roaming_appdata_pwstr + .to_string() + .into_diagnostic() + .wrap_err("appdata/roaming is not a utf-8 path")?; + info!(roaming_str, "RoamingAppData path"); + CoTaskMemFree(Some(roaming_appdata_pwstr.0 as _)); + if !roaming_str.ends_with('\\') { + roaming_str.push('\\'); + } + roaming_str.push_str("Guild Wars 2\\GFXSettings.Gw2-64.exe.xml"); + info!(roaming_str, "gw2 config path"); + roaming_str + }; + let gw2_config_path = std::path::PathBuf::from_str(&gw2_config_path) + .into_diagnostic() + .wrap_err("failed to create pathbuf from gw2 config path in roaming appdata")?; + std::fs::create_dir_all(gw2_config_path.parent().unwrap()) + .into_diagnostic() + .wrap_err("failed to create gw2 config dir in appdata roaming ")?; + if !gw2_config_path.exists() { + std::fs::File::create(&gw2_config_path) + .into_diagnostic() + .wrap_err("failed to create empty gw2 config file ")?; + } + let dpi_scaling = check_dpi_scaling_enabled(&gw2_config_path); + + info!( + ?dpi_scaling, + ?gw2_config_path, + "dpi scaling when we are starting out" + ); + // lets just assume that the scaling is true by default + let dpi_scaling = dpi_scaling.unwrap_or(1); + gw2_config_changed.store(false, std::sync::atomic::Ordering::Relaxed); + let gw2_config_changed_2 = gw2_config_changed.clone(); + let mut gw2_config_watcher = notify::recommended_watcher(move |ev| { + debug!(?ev, "gw2 config changed"); + gw2_config_changed_2.store(true, std::sync::atomic::Ordering::Relaxed); + }) + .into_diagnostic() + .wrap_err("failed to create gw2 config directory watcher")?; + gw2_config_watcher + .watch(&gw2_config_path, notify::RecursiveMode::NonRecursive) + .into_diagnostic() + .wrap_err("faield to watch gw2 config dir")?; + + Ok(Self { + link_ptr, + mumble_handle: handle, + window_handle: 0, + last_ui_tick_update: Instant::now(), + previous_ui_tick: CMumbleLink::get_ui_tick(link_ptr), + // window_pos_size: [0; 4], + process_handle: HANDLE::default(), + previous_pid: 0, + xid: 0, + last_pos_size_check: Instant::now(), + // window_pos_size_without_borders: [0; 4], + dpi_scaling, + client_pos: [0; 2], + client_size: [0; 2], + dpi: 0, + _gw2_config_watcher: gw2_config_watcher, + gw2_config_changed, + gw2_config_path, + }) + } + } + pub fn is_alive(&self) -> bool { + !self.process_handle.is_invalid() + } + pub fn get_cmumble_link(&mut self) -> CMumbleLink { + let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; + link.context.timestamp = OffsetDateTime::now_utc() + .unix_timestamp_nanos() + .to_le_bytes(); + // link.context.window_pos_size = self.window_pos_size; + // link.context.window_pos_size_without_borders = self.window_pos_size_without_borders; + link.context.dpi_scaling = self.dpi_scaling; + link.context.dpi = self.dpi; + link.context.xid = self.xid; + link.context.client_pos = self.client_pos; + link.context.client_size = self.client_size; + link + } + /// This is the most important function which will be called every frame + /// 1. it gets the ui_tick from the link pointer + /// 2. checks if it has changed compared to previous ui_tick. If it didn't change, then we have nothing to do and we return. + /// 3. If it changed, we check if it is less than previous_ui_tick OR if the pid is differnet from previous_pid or if our process handle is invalid + /// 4. If any of the above conditions are true, we reset and reinitialize the gw2 process handle + window handle + window size etc.. + /// 5. If ui_tick simply increased and nothing else changed, then we proceed with the usual stuf which is check the timer and get updated window pos/size + pub fn tick(&mut self) -> Result<()> { + unsafe { + // if ui_tick is zero, we return + if !CMumbleLink::is_valid(self.link_ptr) { + // if we alive, that means ui_tick turned zero this frame for whatever reason, so we reset. + if self.is_alive() { + self.reset(); + } + return Ok(()); + } + let ui_tick = CMumbleLink::get_ui_tick(self.link_ptr); + let pid = CMumbleLink::get_pid(self.link_ptr); + let previous_ui_tick = self.previous_ui_tick; + // if ui tick didn't change. Then it means either we are in loading scree / character select screen or gw2 was closed (or crashed) + if ui_tick == previous_ui_tick { + // if we are not alive, then we just return because it just means mumble is not being updated. + // but if we are alive, then we need to check whehter gw2 is still alive (in loading screen) or dead + if self.is_alive() { + // we don't want to check every frame. Instead, we check in intervals of 3 seconds until gw2 finally loads into a map or it closes (so we can reset) + if self.last_ui_tick_update.elapsed() > Duration::from_secs(3) { + self.last_ui_tick_update = Instant::now(); + match check_process_alive(self.process_handle) { + Ok(alive) => { + if !alive { + self.reset(); + } + } + Err(e) => { + error!(?e, "failed to get GetExitCodeProcess"); + self.reset(); + } + } + } + } + return Ok(()); + } + // if ui_tick has changed, then we have some stuff to do. + if ui_tick < previous_ui_tick // only happens if process changes + || pid != self.previous_pid // gw2 process changed. need to get new handles/sizes etc.. + || !self.is_alive() + // if we are in reset status, then its our chance to reinitialize because mumble just updated. + { + info!(ui_tick, notify = 2u64, "found new gw2 process"); + self.reinitialize(); + } + // if reinitialization failed, then we can try again next frame. + // if we are alive, that means everything is working as expected. + // we update the previous ui_tick and check if we need to update window pos/size + if self.is_alive() { + self.last_ui_tick_update = Instant::now(); + self.previous_ui_tick = ui_tick; + // check in 2 seconds intervals because it rarely changes + if self.last_pos_size_check.elapsed() > Duration::from_secs(2) { + self.last_pos_size_check = Instant::now(); + + // self.window_pos_size = match get_window_pos_size(self.window_handle) { + // Ok(window_pos_size) => { + // if self.window_pos_size != window_pos_size { + // info!( + // ?self.window_pos_size, ?window_pos_size, + // "window position size changed" + // ); + // } + // window_pos_size + // } + // Err(e) => { + // error!(?e, "failed to get window position size"); + // self.reset(); // go back to being dead because it shouldn't usually fail + // return Ok(()); + // } + // }; + // let dpi_awareness = match GetProcessDpiAwareness(self.process_handle) { + // Ok(dpi) => dpi.0, + // Err(e) => { + // error!(?e, "failed to get dpi awareness"); + // 0 + // } + // }; + // if self.dpi_scaling != dpi_awareness { + // info!(dpi_awareness, self.dpi_scaling, "dpi scaling changed"); + // } + // self.dpi_scaling = dpi_awareness; + + let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; + if dpi != self.dpi { + info!(dpi, self.dpi, "dpi changed for gw2 window"); + } + if dpi == 0 { + error!(dpi, "invalid dpi value for guild wars 2"); + } + self.dpi = dpi; + // if the config changed, we will attempt to read dpi scaling. + // if we fail, we will just ignore it, and try again during next check of window pos (2 secs?) + // if we succeed, we will store false in the atomic bool + if self + .gw2_config_changed + .load(std::sync::atomic::Ordering::Relaxed) + { + match check_dpi_scaling_enabled(&self.gw2_config_path) { + Ok(dpi_scaling) => { + if self.dpi_scaling != dpi_scaling { + info!(self.dpi_scaling, dpi_scaling, "dpi scaling changed"); + } + self.dpi_scaling = dpi_scaling; + self.gw2_config_changed + .store(false, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => { + error!(notify = 0.0f64, ?e, "failed to open gw2 config file to check for dpi scaling changes"); + } + } + } + // self.window_pos_size_without_borders = + // match get_window_pos_size_without_borders(HWND(self.window_handle)) { + // Ok(window_pos_size_without_borders) => { + // if self.window_pos_size_without_borders + // != window_pos_size_without_borders + // { + // info!( + // ?self.window_pos_size_without_borders, + // ?window_pos_size_without_borders, + // "window position size changed" + // ); + // } + // window_pos_size_without_borders + // } + // Err(e) => { + // error!(?e, "failed to get window position size"); + // self.reset(); // go back to being dead because it shouldn't usually fail + // return Ok(()); + // } + // }; + match get_client_rect_in_screen_coords(HWND(self.window_handle)) { + Ok((client_pos, client_size)) => { + if self.client_pos != client_pos || self.client_size != client_size { + info!( + ?self.client_pos, + ?client_pos, + ?self.client_size, + ?client_size, + "window position or size changed" + ); + } + self.client_pos = client_pos; + self.client_size = client_size; + } + Err(e) => { + error!(?e, "failed to get client position size"); + self.reset(); // go back to being dead because it shouldn't usually fail + return Ok(()); + } + }; + } + } + } + Ok(()) + } + /// A function which clears all the gw2 related resources like process/window handles + unsafe fn reset(&mut self) { + warn!("resetting mumble data"); + self.window_handle = 0; + if !self.process_handle.is_invalid() { + if let Err(e) = CloseHandle(self.process_handle) { + error!(?e, "failed to close process handle of old gw2"); + } + } + self.process_handle = HANDLE::default(); + // self.window_pos_size = [0; 4]; + // self.window_pos_size_without_borders = [0; 4]; + self.dpi = 0; + self.client_pos = [0; 2]; + self.client_size = [0; 2]; + self.previous_pid = 0; + self.xid = 0; + } + unsafe fn reinitialize(&mut self) { + warn!("we are reinitializing our mumble data"); + info!( + "printing cmumblelink as it might be useful for debugging. {:?}", + self.get_cmumble_link() + ); + assert!( + CMumbleLink::is_valid(self.link_ptr), + "attempting to reinitialize when mumble is still unintialized" + ); + let pid = CMumbleLink::get_pid(self.link_ptr); + assert!(pid != 0, "attempting to initialize with pid == 0"); + self.reset(); + info!( + "ui_tick: {}. pid: {pid}", + CMumbleLink::get_ui_tick(self.link_ptr) + ); + match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { + Ok(process_handle) => { + info!("got process handle: {process_handle:?}"); + // get pid from mumble link + let mut window_handle = pid as isize; + + // enumerate windows and get the handle and assign it to the pid variable if the process id of the handle actually matches the pid + let _ = EnumWindows( + Some(get_handle_by_pid), + LPARAM(((&mut window_handle) as *mut isize) as isize), + ); + // if lparam_pid is still the same as pid, then we couldn't find the relevant window handle + if window_handle == pid as isize { + if let Err(e) = CloseHandle(process_handle) { + error!( + ?e, + "failed to close process handle when we couldn't get window handle." + ); + } + error!( + "failed to initialize mumble data because we couldn't find window handle" + ); + return; + } + info!("found window handle too. yay"); + // now we have both process_handle and window_handle. We just need the window size to initialize our struct + // this function only gets the suface/viewport pos/size without any borders/decoraitons. + match get_client_rect_in_screen_coords(HWND(window_handle)) { + Ok((client_pos, client_size)) => { + // this block is purely for logging purposes only to verify that all sizes are working properly. + { + // GetWindowRect includes drop shadow borders and titlebar + match get_window_pos_size(window_handle) { + Ok(pos_size) => { + info!( + ?pos_size, + "get window position and size using GetWindowRect" + ); + } + Err(e) => { + error!(?e, "failed to initialize mumble data because we coudln't get window position and size"); + } + } + // DwmGetWindowAttribute doesn't include drop shadow borders, but includes titlebar + match get_window_pos_size_without_borders(HWND(window_handle)) { + Ok(window_pos_size_without_borders) => { + info!(?window_pos_size_without_borders, "got window pos/size without borders using DwmGetWindowAttribute"); + } + Err(e) => { + error!( + ?e, + "failed to get window position size without borders" + ); + } + }; + } + // only useful in wine + match std::ffi::CString::new("__wine_x11_whole_window") { + Ok(atom_string) => { + let xid = + GetPropA(HWND(window_handle), PCSTR(atom_string.as_ptr() as _)); + // check if the xid is actually null + if xid.is_invalid() { + // will happen on windows. But this is harmless + info!(?xid, "xid is invalid. This is completely fine on windows. This is only for linux users"); + } else { + info!("found xid too <3. {xid:?}"); + self.xid = xid + .0 + .try_into() + .map_err(|e| { + error!( + ?e, + ?xid, + "failed to fit x11 window id into u32" + ); + }) + .unwrap_or_default(); + } + } + Err(e) => { + error!(?e, notify = 0u64, "impossible. But __wine_x11_whole_window apparently not a valid cstring."); + } + } + // again, just for logging purposes and verify against lutris settings of dpi + let dpi_awareness = match GetProcessDpiAwareness(process_handle) { + Ok(dpi) => dpi.0, + Err(e) => { + error!(?e, "failed to get dpi awareness"); + 0 + } + }; + let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; + if dpi != self.dpi { + info!(dpi, self.dpi, "dpi changed for gw2 window"); + } + info!( + ?client_pos, + ?client_size, + dpi_awareness, + dpi, + pid, + ?process_handle, + ?window_handle, + "reinitialization complete " + ); + self.process_handle = process_handle; + self.window_handle = window_handle; + self.dpi = dpi; + self.client_pos = client_pos; + self.client_size = client_size; + self.last_ui_tick_update = Instant::now(); + self.previous_pid = pid; + } + Err(e) => { + error!(?e, "failed to get client rect"); + } + } + } + Err(e) => { + error!(?e, pid, "failed to open process handle"); + } + } + } +} + +fn check_dpi_scaling_enabled(path: &std::path::Path) -> Result { + // from $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + // life is too short to parse an xml out of this file. just find the following strings + const DPI_SCALING_TRUE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="true"#; + const DPI_SCALING_FALSE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="false"#; + let contents = std::fs::read_to_string(path) + .into_diagnostic() + .wrap_err("failed to read gw2 file")?; + + if contents.contains(DPI_SCALING_FALSE) { + return Ok(0); + }; + if contents.contains(DPI_SCALING_TRUE) { + return Ok(1); + }; + error!(contents, "failed to read dpi scaling from gw2 config file"); + Ok(-1) +} +/// This function creates/opens the shared memory with the key as the name. +/// Then, it maps the shared memory into the address space of our process. +/// Finally, we are provided the Handle of shared memory and the pointer to the starting address of the mapped memory. +/// can fail if +/// 1. key is not a valid cstring +/// 2. creating shared memory fails +/// 3. mapping shared memory into our addres space fails and we get a null pointer instead +unsafe fn create_link_shared_mem(key: &str) -> Result<(HANDLE, *mut CMumbleLink)> { + info!("creating MumbleLink shared memory: {key}"); + // prepare the key as a cstr to pass to windows functions + let key_cstr = std::ffi::CString::new(key) + .into_diagnostic() + .wrap_err(miette::miette!("invalid mumble link name {key}"))?; + unsafe { + // create a Mumble Link shared memory file + // the file handle will need not be stored because when process exits, the handle will be dropped by windows + let file_handle = CreateFileMappingA( + INVALID_HANDLE_VALUE, + None, + PAGE_READWRITE, + 0, + C_MUMBLE_LINK_SIZE_FULL as u32 + 4096, // we add the size of description field here. + PCSTR(key_cstr.as_ptr() as _), + ) + .into_diagnostic() + .wrap_err("failed to create file mapping for MumbleLink")?; + // map the shared memory into the address space of our process using the handle we got from creating the shm + let cml_ptr = MapViewOfFile( + file_handle, + FILE_MAP_ALL_ACCESS, + 0, + 0, + C_MUMBLE_LINK_SIZE_FULL + 4096, // adding the description field size here + ) + .Value; + // check if we were successful + if cml_ptr.is_null() { + bail!( + "could not map view of file, error code: {:#?}", + GetLastError() + ) + } + Ok((file_handle, cml_ptr.cast())) + } +} + +unsafe fn check_process_alive(process_handle: HANDLE) -> Result { + let mut exit_code = 0u32; + GetExitCodeProcess(process_handle, &mut exit_code as *mut u32) + .into_diagnostic() + .wrap_err("failed to get exit code of process ")?; + Ok(exit_code == STATUS_PENDING.0 as u32) + + // this is slightly faster than using the GetExitCodeProcess method. + // GetExitCodeProcess takes around 3 us on average with lowest being 2.5 us. + // WaitForSingleObject takes around 2 us on average withe lowest being 1.5 us. + // let result = unsafe { WaitForSingleObject(process_handle, 0) }; + + // if result == WAIT_ABANDONED || result == WAIT_OBJECT_0 { + // Ok(false) + // } else if result == WAIT_TIMEOUT.0 { + // Ok(true) + // } else { + // bail!("WaitForSingleObject returned code: {:#?}", result) + // } +} +/// This function gets called by EnumWindows as a lambda function. it will be given a handle to all windows one by one, +/// and the pid of the process we want to match against that handle's pid. if handle's pid is matched against our pid, we will +/// assign the handle to our pid pointer so that the they can use it after EnumWindows returns +unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LPARAM) -> BOOL { + // gw2_pid is a long pointer TO a HWND. we cast gw2_pid from isize to a * mut isize. + let local_gw2_pid = *(gw2_pid_ptr.0 as *mut isize); + + // make a varible to hold the process id of a window handle given to us. + let mut window_handle_pid: u32 = 0; + // get the process id of the handle and then store it in the handle_pid variable. + GetWindowThreadProcessId(window_handle, Some((&mut window_handle_pid) as *mut u32)); + // if handle_pid is null, it means we failed to get the pid. so, we return true so that enumWindows can call us again with the handle to the next window. + if window_handle_pid == 0 { + info!("failed to get process id of window handle {window_handle:?}"); + return BOOL(1); + } + + info!("window handle {window_handle:?} has pid {window_handle_pid}"); + + // we check if the pid which gw2_pid references is equal to handle_pid + if local_gw2_pid == window_handle_pid as isize { + info!( + "successfully found the handle: {window_handle:?} of our gw2 with pid {local_gw2_pid}" + ); + // we now assign the window_handle to the memory pointed by gw2_pid pointer. + *(gw2_pid_ptr.0 as *mut isize) = window_handle.0; + return BOOL(0); + } + BOOL(1) +} +/// Quirk: GetWindowRect also includes the invisible "borders" which windows uses for resizing or whatever +/// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. +/// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). +/// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. +fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { + bail!("GetWindowRect call failed {e:#?}"); + } + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = DwmGetWindowAttribute( + window_handle, + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut RECT as _, + std::mem::size_of::() as _, + ) { + bail!("DwmGetWindowAttribute call failed {e:#?}"); + } + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = GetClientRect(window_handle, &mut rect as *mut RECT) { + bail!("GetClientRect call failed {e:#?}"); + } + let mut point: POINT = POINT { + x: rect.left, + y: rect.top, + }; + if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { + bail!("ClientToScreen call failed"); + } + let pos = [point.x, point.y]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +impl Drop for MumbleWinImpl { + fn drop(&mut self) { + unsafe { + warn!("dropping mumble link windows impl"); + if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { + Value: self.link_ptr as _, + }) { + error!(?e, "failed to unmap view of mumble file"); + } + if let Err(e) = CloseHandle(self.mumble_handle) { + error!(?e, "failed to close handle of mumble link ") + } + if !self.process_handle.is_invalid() { + if let Err(e) = CloseHandle(self.process_handle) { + error!(?e, "failed to close handle of mumble link ") + } + } + } + } +} diff --git a/crates/joko_link_models/Cargo.toml b/crates/joko_link_models/Cargo.toml new file mode 100644 index 0000000..28c8a40 --- /dev/null +++ b/crates/joko_link_models/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "joko_link_models" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + + +[dependencies] +joko_core = { path = "../joko_core" } +joko_components = { path = "../joko_components" } +widestring = { version = "1", default-features = false, features = ["std"] } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +time = { workspace = true } +miette = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } + +[target.'cfg(unix)'.dependencies] +x11rb = { version = "0.12", default-features = false, features = [] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.51.1", features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_Graphics_Dwm", + "Win32_UI_HiDpi", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_System_Com", +] } +arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } diff --git a/crates/joko_link_models/README.md b/crates/joko_link_models/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link_models/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs new file mode 100644 index 0000000..2c055e8 --- /dev/null +++ b/crates/joko_link_models/src/lib.rs @@ -0,0 +1,33 @@ +//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory + +//! Joko link is designed to primarily get the MumbleLink or the window size +//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). +//! on windows, you can use it to create/open shared memory. +//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. +//! then, you can easily read the /dev/shm file from a any number of linux native applications. +//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. +//! + +mod mumble; +use std::vec; + +use enumflags2::BitFlags; +use joko_components::{JokolayComponent, JokolayComponentDeps}; +use joko_core::serde_glam::{IVec2, UVec2, Vec3}; +//use jokoapi::end_point::{mounts::Mount, races::Race}; +use miette::{IntoDiagnostic, Result, WrapErr}; +pub use mumble::*; +use serde_json::from_str; +use tracing::error; + +pub enum MessageToMumbleLinkBack { + BindedOnUI, + Autonomous, + Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. +} + +#[derive(Clone)] +pub struct MumbleLinkSharedState { + pub read_ui_link: bool, + pub copy_of_ui_link: Option, +} diff --git a/crates/joko_link_models/src/mumble/ctypes.rs b/crates/joko_link_models/src/mumble/ctypes.rs new file mode 100644 index 0000000..72dd4ac --- /dev/null +++ b/crates/joko_link_models/src/mumble/ctypes.rs @@ -0,0 +1,288 @@ +use enumflags2::BitFlags; +use miette::bail; +use serde::{Deserialize, Serialize}; + +use crate::{UISize, UIState}; + +/// The total size of the CMumbleLink struct. used to know the amount of memory to give to win32 call that creates the shared memory +pub const C_MUMBLE_LINK_SIZE_FULL: usize = std::mem::size_of::(); +/// This is how much of the CMumbleLink memory that is actually useful and updated. the rest is just zeroed out. +pub const USEFUL_C_MUMBLE_LINK_SIZE: usize = 1196; + +/// The CMumblelink is how it is represented in the memory. But we rarely use it as it is and instead convert it into MumbleLink before using it for convenience +/// Many of the fields are documentad in the actual MumbleLink struct +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct CMumbleLink { + //// The ui_version will always be same as mumble doesn't change. we will come back to change it IF there's a new version. + pub ui_version: u32, + //// This tick represents the update count of the link (which is usually the frame count ) since mumble was initialized. not from the start of game, but the start of mumble + pub ui_tick: u32, + //// position of the character + pub f_avatar_position: [f32; 3], + //// direction towards which the character is facing + pub f_avatar_front: [f32; 3], + //// the up direction vector of the character. + pub f_avatar_top: [f32; 3], + //// The name of the character currently logged in + pub name: [u16; 256], + //// The position of the camera + pub f_camera_position: [f32; 3], + //// The direction towards which the camera is facing + pub f_camera_front: [f32; 3], + //// The up direction for the camera + pub f_camera_top: [f32; 3], + //// This is a widestring of json containing the serialized data of [CIdentity] + pub identity: [u16; 256], + //// The [Self::context] field is 256 bytes, but the game only uses the first few bytes. + //// The first 48 bytes are used by mumble to uniquely identify the map/instance/room of the player + //// So, this field is always set to 48 bytes. + //// But gw2 writes even more data for the sake of addon functionality like minimap position etc.. + //// So, adding another 37 bytes which gw2 writes to. The total length of context is roughly 88 bytes if we consider the alignment. + pub context_len: u32, + //// 88 bytes are useful context written by gw2. Jokolink writes some more additional data beyond the 88 bytes like + //// X11 ID or window size or the timestamp when it last wrote data to this link etc.. which is useful for linux native clients like jokolay + pub context: CMumbleContext, + // Useless for now. Nothing is ever written here. + // we will just remove this field and add the size when creating shared memory. + // no point in copying more than 5kb when we only care about the first 1kb. + // pub description: [u16; 2048], +} +impl Default for CMumbleLink { + fn default() -> Self { + Self { + ui_version: Default::default(), + ui_tick: Default::default(), + f_avatar_position: Default::default(), + f_avatar_front: Default::default(), + f_avatar_top: Default::default(), + name: [0; 256], + f_camera_position: Default::default(), + f_camera_front: Default::default(), + f_camera_top: Default::default(), + identity: [0; 256], + context_len: Default::default(), + context: Default::default(), + // description: [0; 2048], + } + } +} + +impl CMumbleLink { + /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read + pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { + unsafe { std::ptr::read_volatile(link_ptr) } + } + + /// Checks if the MumbleLink memory is actually initialized by checking if [CMumbleLink::ui_tick] is non-zero. + /// Even if it returns true because [`CMumbleLink::ui_tick`] is non-zero, it could be a remnant from an older gw2 process. + /// The only way to verify that gw2 is active (with a character logged into a map), is to check if the tick changed from last frame to current frame. + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + pub unsafe fn is_valid(link_ptr: *const CMumbleLink) -> bool { + unsafe { (*link_ptr).ui_tick > 0 } + } + + /// gets uitick if we want to know the frame number since initialization of CMumbleLink + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then return value will be zero + /// 3. Even if it is not zero, the ui_tick maybe a stale because the game is dead (or in map loading screen / character select screen / cutscene) + pub unsafe fn get_ui_tick(link_ptr: *const CMumbleLink) -> u32 { + (*link_ptr).ui_tick + } + /// gets the pid from [CMumbleLink::context] field + /// # Safety + /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then pid will be zero + /// 3. Even if it is initialized, the process could be dead and the pid may be reused for a different process now + pub unsafe fn get_pid(link_ptr: *const CMumbleLink) -> u32 { + (*link_ptr).context.process_id + } + // #[cfg(unix)] + // pub unsafe fn get_xid(link_ptr: *const CMumbleLink) -> u32 { + // (*link_ptr).context.xid + // } + // #[cfg(unix)] + // pub unsafe fn get_pos_size(link_ptr: *const CMumbleLink) -> [i32; 4] { + // (*link_ptr).context.client_pos_size + // } + /// This gets the timestamp written by `jokolink` + /// The return value is nanoseconds since unix_epoch. + /// This is an easy way to check that jokolink (and by extension gw2) is still alive even if ui_tick doesn't change. + /// This happens when gw2 is in character select screen or cutscene etc.. when ui_tick stops updating. + /// # Safety + /// 1. `link_ptr` must be valid and point to memory of atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size + /// 2. If it is uninitialized, the return value could be zero + #[cfg(unix)] + pub unsafe fn get_timestamp(link_ptr: *const CMumbleLink) -> i128 { + let bytes = (*link_ptr).context.timestamp; + i128::from_le_bytes(bytes) + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +/// The mumble context as stored inside the context field of CMumbleLink. +/// the first 48 bytes Mumble uses for identification is upto `build_id` field +/// the rest of the fields after `build_id` are provided by gw2 for addon devs. +pub struct CMumbleContext { + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + pub server_address: [u8; 28], // contains sockaddr_in or sockaddr_in6 + /// Map ID + pub map_id: u32, + pub map_type: u32, + pub shard_id: u32, + pub instance: u32, + pub build_id: u32, + /// The fields until now are provided for mumble. + /// The rest of the data from here is what gw2 provides for the benefit of addons. + /// This is the current UI state of the game. refer to [UIState] + /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat + pub ui_state: u32, + pub compass_width: u16, // pixels + pub compass_height: u16, // pixels + pub compass_rotation: f32, // radians + pub player_x: f32, // continentCoords + pub player_y: f32, // continentCoords + pub map_center_x: f32, // continentCoords + pub map_center_y: f32, // continentCoords + pub map_scale: f32, + /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. + /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid + /// on windows, we use this to get window handle which can give us a window size. + /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid + /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 + pub process_id: u32, + /// refers to [Mount] + /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match api + pub mount_index: u8, + /// This is where the context fields provided by gw2 end. + /// From here on, these are custom fields set by jokolink.dll for the use of jokolay + /// These fields will be set before writing the link data to the `/dev/shm/MumbleLink` file from which jokolay can pick it up + /// + /// timestamp when jokolink wrote this data. unix nanoseconds + /// This timestamp will be written every frame by jokolink even if mumble link is uninitialized. + /// This is [i128] in little endian byte order. We use a byte array instead of [i128] directly because context is aligned to 4 by default. And + /// [i64]/[i128] will change that alignment to 8. This will lead to 4 bytes padding between [CMumbleLink::context_len] and [CMumbleLink::context] + /// + /// If jokolink doesn't write for more than 1 or 2 seconds, it can be safely assumed that gw2 was closed/crashed. + /// This is in nanoseconds since unix epoch in UTC timezone. + pub timestamp: [u8; 16], + /// This represents the x11 window id of the gw2 window. AFAIK, wine uses x11 only (no wayland), so this could be useful to set transient for + pub xid: u32, + /* + pub window_pos_size_without_borders: [i32; 4], + /// x, y, width, height of guild wars 2 window relative to top left corner of the screen. + /// This is populated with `GetWindowRect` fn + /// DPI aware. In screen coordinate. But includes drop shadow too :(. + pub window_pos_size: [i32; 4], + */ + /// dpi awareness of the gw2 process. Most probably will be `2` and below we have the relevant MS docs + /// DPI_AWARENESS_PER_MONITOR_AWARE + /// Value: 2 + /// Per monitor DPI aware. This process checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes. These processes are not automatically scaled by the system. + pub dpi_scaling: i32, + /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. + pub dpi: i32, + /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. + pub client_pos: [i32; 2], + pub client_size: [u32; 2], + /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. + pub padding: [u8; 96], +} +impl Default for CMumbleContext { + fn default() -> Self { + assert_eq!(std::mem::size_of::(), 256); + Self { + server_address: Default::default(), + map_id: Default::default(), + map_type: Default::default(), + shard_id: Default::default(), + instance: Default::default(), + build_id: Default::default(), + ui_state: Default::default(), + compass_width: Default::default(), + compass_height: Default::default(), + compass_rotation: Default::default(), + player_x: Default::default(), + player_y: Default::default(), + map_center_x: Default::default(), + map_center_y: Default::default(), + map_scale: Default::default(), + process_id: Default::default(), + mount_index: Default::default(), + timestamp: Default::default(), + // window_pos_size: Default::default(), + padding: [0; 96], + xid: Default::default(), + // window_pos_size_without_borders: Default::default(), + dpi_scaling: Default::default(), + dpi: Default::default(), + client_pos: Default::default(), + client_size: Default::default(), + } + } +} +impl CMumbleContext { + pub fn get_ui_state(&self) -> Option> { + BitFlags::from_bits(self.ui_state).ok() + } + + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + /// contains sockaddr_in or sockaddr_in6 + pub fn get_map_ip(&self) -> miette::Result { + if self.server_address[0] != 2 { + // add ipv6 support when gw2 servers add ipv6 support. + bail!("ipaddr parsing failed for CMumble Context"); + } + let ip = std::net::Ipv4Addr::from([ + self.server_address[4], + self.server_address[5], + self.server_address[6], + self.server_address[7], + ]); + Ok(ip) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] +#[serde(crate = "serde")] +/// The json structure of the Identity field inside Cmumblelink. +/// the json string is null terminated and utf-16 encoded. so, need to use +/// Widestring crate's U16Cstring to first parse the bytes and then, convert to +/// String before deserializing to CIdentity +pub struct CIdentity { + /// The name of the character + pub name: String, + /// The core profession id of the character. matches the ids of v2/professions endpoint + pub profession: u32, + /// Character's third specialization, or 0 if no specialization is present. See /v2/specializations for valid IDs. + pub spec: u32, + /// The race of the character. does not match api + pub race: u32, + /// API:2/maps + pub map_id: u32, + /// useless field from pre-megaserver days. is just shard_id from context struct + pub world_id: u32, + /// Team color per API:2/colors (0 = white) + pub team_color_id: u32, + /// Whether the character has a commander tag active + pub commander: bool, + /// Vertical field-of-view + pub fov: f32, + /// A value corresponding to the user's current UI scaling. + pub uisz: u32, +} + +impl CIdentity { + pub fn get_uisz(&self) -> Option { + Some(match self.uisz { + 0 => UISize::Small, + 1 => UISize::Normal, + 2 => UISize::Large, + 3 => UISize::Larger, + _ => return None, + }) + } +} diff --git a/crates/joko_link_models/src/mumble/mod.rs b/crates/joko_link_models/src/mumble/mod.rs new file mode 100644 index 0000000..16a38d3 --- /dev/null +++ b/crates/joko_link_models/src/mumble/mod.rs @@ -0,0 +1,173 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod ctypes; +use std::net::IpAddr; + +use enumflags2::{bitflags, BitFlags}; +use num_derive::FromPrimitive; +use num_derive::ToPrimitive; + +use joko_core::serde_glam::*; +use serde::{Deserialize, Serialize}; + +/// As the CMumbleLink has all the fields multiple +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MumbleLink { + /// ui tick. (more or less represents the frame number of gw2) + pub ui_tick: u32, + /// character position + pub player_pos: Vec3, + /// direction char is facing + pub f_avatar_front: Vec3, + /// camera position + pub cam_pos: Vec3, + /// direction camera is facing + pub f_camera_front: Vec3, + /// The name of the character + pub name: String, + /// API:2/maps + pub map_id: u32, + pub map_type: u32, + /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. + pub server_address: IpAddr, // contains sockaddr_in or sockaddr_in6 + pub shard_id: u32, + pub instance: u32, + pub build_id: u32, + /// The fields until now are provided for mumble. + /// The rest of the data from here is what gw2 provides for the benefit of addons. + /// This is the current UI state of the game. refer to [UIState] + /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat + pub ui_state: Option>, + pub compass_width: u16, // pixels + pub compass_height: u16, // pixels + pub compass_rotation: f32, // radians + pub player_x: f32, // continentCoords + pub player_y: f32, // continentCoords + pub map_center_x: f32, // continentCoords + pub map_center_y: f32, // continentCoords + pub map_scale: f32, + /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. + /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid + /// on windows, we use this to get window handle which can give us a window size. + /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid + /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 + pub process_id: u32, + /// refers to [Mount] + /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api + //pub mount: Option, + //pub race: Race, + pub mount: u8, + pub race: u32, + + /// Vertical field-of-view + pub fov: f32, + /// A value corresponding to the user's current UI scaling. + pub uisz: UISize, + // pub window_pos: IVec2, + // pub window_size: IVec2, + // pub window_pos_without_borders: IVec2, + // pub window_size_without_borders: IVec2, + /// This is the dpi of gw2 window. 96dpi is the default for a non-hidpi monitor with scaling 1.0 + /// for a scaling of 2.0, it becomes 192 and so on. + pub dpi: i32, + /// This is whether gw2 is scaling its UI elements to match the dpi. So, if the dpi is bigger than 96, gw2 will make text/ui bigger. + /// -1 means we couldn't get the setting from gw2's config file in appdata/roaming + /// 0 means scaling is disabled (false) + /// 1 means scaling is enabled (true). + pub dpi_scaling: i32, + /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* + pub client_pos: IVec2, + /// This is the size of gw2's viewport (width/height) in screen coordinates + pub client_size: UVec2, + /// changes since last mumble link update + pub changes: BitFlags, +} +impl Default for MumbleLink { + fn default() -> Self { + Self { + ui_tick: Default::default(), + player_pos: Default::default(), + f_avatar_front: Default::default(), + cam_pos: Default::default(), + f_camera_front: Default::default(), + name: String::from("This Is Jokolay Dummy"), + map_id: Default::default(), + map_type: Default::default(), + server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), + shard_id: Default::default(), + instance: Default::default(), + build_id: Default::default(), + ui_state: Default::default(), + compass_width: Default::default(), + compass_height: Default::default(), + compass_rotation: Default::default(), + player_x: Default::default(), + player_y: Default::default(), + map_center_x: Default::default(), + map_center_y: Default::default(), + map_scale: Default::default(), + process_id: Default::default(), + mount: Default::default(), + race: u32::MAX, + fov: 2.0, + uisz: Default::default(), + dpi: Default::default(), + dpi_scaling: 96, + client_pos: Default::default(), + client_size: UVec2(glam::UVec2 { x: 1024, y: 768 }), + changes: Default::default(), + } + } +} +/// These flags represent the changes in mumble link compared to previous values +#[bitflags] +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum MumbleChanges { + UiTick = 1, + Map = 1 << 1, + Character = 1 << 2, + WindowPosition = 1 << 3, + WindowSize = 1 << 4, + Camera = 1 << 5, + Position = 1 << 6, +} + +/// represents the ui scale set in settings -> graphics options -> interface size +#[derive( + Debug, + Clone, + Default, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + FromPrimitive, + ToPrimitive, +)] +#[serde(crate = "serde")] +pub enum UISize { + Small = 0, + #[default] + Normal = 1, + Large = 2, + Larger = 3, +} + +#[bitflags] +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +/// The Uistate enum to represent the status of the UI in game +pub enum UIState { + IsMapOpen = 0b00000001, + IsCompassTopRight = 0b00000010, + DoesCompassHaveRotationEnabled = 0b00000100, + GameHasFocus = 0b00001000, + InCompetitiveGamemode = 0b00010000, + TextboxFocus = 0b00100000, + IsInCombat = 0b01000000, +} diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml index f83282d..564eb2f 100644 --- a/crates/joko_package/Cargo.toml +++ b/crates/joko_package/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # jmf deps # for marker packs base64 = "0.21.2" +bincode = { workspace = true } bytemuck = { workspace = true } cap-std = { workspace = true } cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings @@ -18,10 +19,11 @@ image = { version = "0.24", default-features = false, features = ["png"] } # for indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip itertools = { workspace = true } joko_core = { path = "../joko_core" } +joko_components = { path = "../joko_components" } joko_render_models = { path = "../joko_render_models" } joko_package_models = { path = "../joko_package_models" } jokoapi = { path = "../jokoapi" } -jokolink = { path = "../jokolink" } +joko_link = { path = "../joko_link" } miette = { workspace = true } once = "0.3.4" ordered_hash_map = { workspace = true } @@ -33,6 +35,7 @@ serde = { workspace = true } serde_json = { workspace = true } smol_str = { workspace = true } time = { workspace = true , features = ["serde"]} +tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs index 32047b6..ba0401e 100644 --- a/crates/joko_package/src/io/deserialize.rs +++ b/crates/joko_package/src/io/deserialize.rs @@ -1,4 +1,7 @@ -use joko_core::RelativePath; +use crate::BASE64_ENGINE; +use base64::Engine; +use cap_std::fs_utf8::{Dir, DirEntry}; +use joko_core::{serde_glam::Vec3, RelativePath}; use joko_package_models::{ attributes::{CommonAttributes, XotAttributeNameIDs}, category::{prefix_parent, Category, RawCategory}, @@ -7,12 +10,6 @@ use joko_package_models::{ route::Route, trail::{TBin, TBinStatus, Trail}, }; -use miette::{bail, Context, IntoDiagnostic, Result}; - -use crate::BASE64_ENGINE; -use base64::Engine; -use cap_std::fs_utf8::{Dir, DirEntry}; -use glam::Vec3; use ordered_hash_map::OrderedHashMap; use std::{collections::VecDeque, io::Read, str::FromStr}; use tracing::{debug, error, info, info_span, instrument, trace, warn}; @@ -24,7 +21,7 @@ const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; pub(crate) fn load_pack_core_from_normalized_folder( core_dir: &Dir, import_report: Option, -) -> Result { +) -> Result { //called from already parsed data let mut core_pack = PackCore::new(); if let Some(mut import_report) = import_report { @@ -39,7 +36,7 @@ pub(crate) fn load_pack_core_from_normalized_folder( &mut core_pack, &RelativePath::default(), ) - .wrap_err("failed to walk dir when loading a markerpack")?; + .or(Err("failed to walk dir when loading a markerpack"))?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!( "Loading of core package textures from disk took {} ms", @@ -49,29 +46,24 @@ pub(crate) fn load_pack_core_from_normalized_folder( //categories are required to register other objects let cats_xml = core_dir .read_to_string("categories.xml") - .into_diagnostic() - .wrap_err("failed to read categories.xml")?; + .or(Err("failed to read categories.xml"))?; let categories_file = String::from("categories.xml"); let parse_categories_file_start = std::time::SystemTime::now(); parse_categories_from_normalized_file(&categories_file, &cats_xml, &mut core_pack) - .wrap_err("failed to parse category file")?; + .or(Err("failed to parse category file"))?; let elapsed = parse_categories_file_start.elapsed().unwrap_or_default(); info!("parse_categories_file took {} ms", elapsed.as_millis()); // parse map data of the pack for entry in core_dir .entries() - .into_diagnostic() - .wrap_err("failed to read entries of pack dir")? + .or(Err("failed to read entries of pack dir"))? { - let dir_entry = entry - .into_diagnostic() - .wrap_err("entry error whiel reading xml files")?; + let dir_entry = entry.or(Err("entry error whiel reading xml files"))?; let name = dir_entry .file_name() - .into_diagnostic() - .wrap_err("map data entry name not utf-8")? + .or(Err("map data entry name not utf-8"))? .to_string(); if name.ends_with(".xml") { @@ -113,33 +105,24 @@ fn recursive_walk_dir_and_read_images_and_tbins( dir: &Dir, pack: &mut PackCore, parent_path: &RelativePath, -) -> Result<()> { - for entry in dir - .entries() - .into_diagnostic() - .wrap_err("failed to get directory entries")? - { - let entry = entry - .into_diagnostic() - .wrap_err("dir entry error when iterating dir entries")?; - let name = entry.file_name().into_diagnostic()?; +) -> Result<(), String> { + for entry in dir.entries().or(Err("failed to get directory entries"))? { + let entry = entry.or(Err("dir entry error when iterating dir entries"))?; + let name = entry.file_name().or(Err("No file name found"))?; let path = parent_path.join_str(&name); if entry .file_type() - .into_diagnostic() - .wrap_err("failed to get file type")? + .or(Err("failed to get file type"))? .is_file() { if path.ends_with(".png") || path.ends_with(".trl") { let mut bytes = vec![]; entry .open() - .into_diagnostic() - .wrap_err("failed to open file")? + .or(Err("failed to open file"))? .read_to_end(&mut bytes) - .into_diagnostic() - .wrap_err("failed to read file contents")?; + .or(Err("failed to read file contents"))?; if name.ends_with(".png") { pack.register_texture(name, &path, bytes); } else if name.ends_with(".trl") { @@ -158,7 +141,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( } } else { recursive_walk_dir_and_read_images_and_tbins( - &entry.open_dir().into_diagnostic()?, + &entry.open_dir().or(Err("Could not open directory"))?, pack, &path, )?; @@ -181,14 +164,14 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { map_id_bytes.copy_from_slice(&bytes[4..8]); let map_id = u32::from_ne_bytes(map_id_bytes); - let zero = Vec3 { + let zero = glam::Vec3 { x: 0.0, y: 0.0, z: 0.0, }; // this will either be empty vec or series of vec3s. - let nodes: VecDeque = bytes[8..] + let nodes: VecDeque = bytes[8..] .chunks_exact(12) .map(|float_bytes| { // make [f32 ;3] out of those 12 bytes @@ -216,7 +199,7 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { ]), ]; - Vec3::from_array(arr) + glam::Vec3::from_array(arr) }) .collect(); @@ -233,7 +216,7 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let mut c_iso_y = true; let mut c_iso_z = true; // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts - resulting_nodes.push(ref_node); + resulting_nodes.push(Vec3(ref_node)); for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { //ignore zeroes since they would be separators if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { @@ -241,11 +224,11 @@ fn parse_tbin_from_slice(bytes: &[u8]) -> Option { let mut current_cursor = distance_to_next_point; while current_cursor > MAX_TRAIL_CHUNK_LENGTH { let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); - resulting_nodes.push(c); + resulting_nodes.push(Vec3(c)); current_cursor -= MAX_TRAIL_CHUNK_LENGTH; } } - resulting_nodes.push(*b); + resulting_nodes.push(Vec3(*b)); } for node in &nodes { if resulting_nodes.len() > 1 { @@ -408,18 +391,12 @@ fn parse_categories_from_normalized_file( file_name: &String, cats_xml_str: &str, pack: &mut PackCore, -) -> Result<()> { +) -> Result<(), String> { let mut tree = xot::Xot::new(); let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); - let root_node = tree - .parse(cats_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; + let root_node = tree.parse(cats_xml_str).or(Err("invalid xml"))?; - let overlay_data_node = tree - .document_element(root_node) - .into_diagnostic() - .wrap_err("no doc element")?; + let overlay_data_node = tree.document_element(root_node).or(Err("no doc element"))?; if let Some(od) = tree.element(overlay_data_node) { let mut categories: OrderedHashMap = Default::default(); @@ -437,10 +414,10 @@ fn parse_categories_from_normalized_file( pack.categories = categories; pack.register_categories(); } else { - bail!("root tag is not OverlayData") + return Err("root tag is not OverlayData".to_string()); } } else { - bail!("doc element is not element???"); + return Err("doc element is not element???".to_string()); } Ok(()) } @@ -449,38 +426,34 @@ fn load_xml_from_normalized_file( file_name: &str, dir_entry: &DirEntry, target: &mut PackCore, -) -> Result<()> { +) -> Result<(), String> { let mut xml_str = String::new(); dir_entry .open() - .into_diagnostic() - .wrap_err("failed to open xml file")? + .or(Err("failed to open xml file"))? .read_to_string(&mut xml_str) - .into_diagnostic() - .wrap_err("faield to read xml string")?; + .or(Err("failed to read xml string"))?; //TODO: launch an async load of the file + make a priority queue to have current map first parse_map_xml_string(file_name, &xml_str, target) - .wrap_err_with(|| miette::miette!("error parsing file: {file_name}")) + .or(Err(format!("error parsing file: {file_name}"))) } -fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCore) -> Result<()> { +fn parse_map_xml_string( + file_name: &str, + map_xml_str: &str, + target: &mut PackCore, +) -> Result<(), String> { let mut tree = Xot::new(); - let root_node = tree - .parse(map_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; + let root_node = tree.parse(map_xml_str).or(Err("invalid xml"))?; let names = XotAttributeNameIDs::register_with_xot(&mut tree); let overlay_data_node = tree .document_element(root_node) - .into_diagnostic() - .wrap_err("missing doc element")?; + .or(Err("missing doc element"))?; - let overlay_data_element = tree - .element(overlay_data_node) - .ok_or_else(|| miette::miette!("no doc ele"))?; + let overlay_data_element = tree.element(overlay_data_node).ok_or("no doc ele")?; if overlay_data_element.name() != names.overlay_data { - bail!("root tag is not OverlayData"); + return Err("root tag is not OverlayData".to_string()); } let pois = tree .children(overlay_data_node) @@ -488,7 +461,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor Some(ele) => ele.name() == names.pois, None => false, }) - .ok_or_else(|| miette::miette!("missing pois node"))?; + .ok_or("missing pois node")?; for poi_node in tree.children(pois) { if let Some(child_element) = tree.element(poi_node) { @@ -562,10 +535,10 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor "Mandatory category missing, packge is corrupted {:?} {:?}", file_name, child_element ); - return Err(miette::Report::msg(format!( + return Err(format!( "Mandatory category missing, packge is corrupted {:?} {:?}", map_xml_str, child_element - ))); + )); } let category_uuid = opt_cat_uuid.unwrap(); //categories MUST exist, they have already been parsed let guid = raw_uid @@ -576,35 +549,35 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor .ok() .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) }) - .ok_or_else(|| miette::miette!("invalid guid {:?}", raw_uid))?; + .ok_or(format!("invalid guid {:?}", raw_uid))?; if child_element.name() == names.poi { debug!("Found a POI in core pack {:?}", child_element); let map_id = child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))?; + .ok_or("invalid mapid")?; let xpos = child_element .get_attribute(names.xpos) .unwrap_or_default() .parse::() - .into_diagnostic()?; + .or(Err("invalid x position"))?; let ypos = child_element .get_attribute(names.ypos) .unwrap_or_default() .parse::() - .into_diagnostic()?; + .or(Err("invalid y position"))?; let zpos = child_element .get_attribute(names.zpos) .unwrap_or_default() .parse::() - .into_diagnostic()?; + .or(Err("invalid z position"))?; let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); let marker = Marker { - position: [xpos, ypos, zpos].into(), + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), map_id, category: full_category_name.clone(), parent: *category_uuid, @@ -618,7 +591,7 @@ fn parse_map_xml_string(file_name: &str, map_xml_str: &str, target: &mut PackCor let map_id = child_element .get_attribute(names.map_id) .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))?; + .ok_or("invalid mapid")?; let mut ca = CommonAttributes::default(); ca.update_common_attributes_from_element(child_element, &names); @@ -649,7 +622,7 @@ fn parse_category_categories_xml_recursive( names: &XotAttributeNameIDs, parent_uuid: Option, parent_name: Option, -) -> Result<()> { +) -> Result<(), String> { for tag in tags { if let Some(ele) = tree.element(tag) { if ele.name() != names.marker_category { @@ -704,9 +677,10 @@ fn parse_category_categories_xml_recursive( ); if display_name.is_empty() { if parent_name.is_some() { - return Err(miette::Error::msg( - "Package is corrupted, please import it again with current version", - )); + return Err( + "Package is corrupted, please import it again with current version" + .to_string(), + ); } parse_category_categories_xml_recursive( _file_name, @@ -758,22 +732,21 @@ fn parse_category_categories_xml_recursive( pub(crate) fn get_pack_from_taco_zip( input_path: std::path::PathBuf, extract_temporary_path: &std::path::PathBuf, -) -> Result { +) -> Result { let mut taco_zip = vec![]; std::fs::File::open(input_path) - .into_diagnostic()? + .or(Err("Could not open target folder"))? .read_to_end(&mut taco_zip) - .into_diagnostic()?; + .or(Err("Could not read target folder"))?; let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) - .into_diagnostic() - .wrap_err("failed to read zip archive")?; + .or(Err("failed to read zip archive"))?; if extract_temporary_path.exists() { - std::fs::remove_dir_all(extract_temporary_path).into_diagnostic()?; + std::fs::remove_dir_all(extract_temporary_path).or(Err("Could not purge target folder"))?; } zip_archive .extract(extract_temporary_path) - .into_diagnostic()?; + .or(Err("Could not extract archive into target folder"))?; _get_pack_from_taco_folder(extract_temporary_path) } @@ -786,7 +759,7 @@ pub(crate) fn get_pack_from_taco_zip( /// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. #[instrument(skip_all)] -fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { +fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { let mut pack = PackCore::new(); // file paths of different file types @@ -796,7 +769,7 @@ fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result() .unwrap_or_default(); Some(Marker { - position: [xpos, ypos, zpos].into(), + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), map_id, category: category_name.to_owned(), parent: *category_uuid, @@ -1283,7 +1256,7 @@ fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { .unwrap_or_default() .parse::() .unwrap_or_default(); - Vec3 { x, y, z } + Vec3(glam::Vec3 { x, y, z }) } fn parse_route_category( @@ -1331,7 +1304,7 @@ fn parse_route( .unwrap_or_default() .parse::() .unwrap_or_default(); - let reset_position = Vec3::new(resetposx, resetposy, resetposz); + let reset_position = glam::Vec3::new(resetposx, resetposy, resetposz); let reset_range = route_element .get_attribute(names.reset_range) .and_then(|map_id| map_id.parse::().ok()); @@ -1395,7 +1368,7 @@ fn parse_route( category, parent: category_uuid.unwrap(), path, - reset_position, + reset_position: Vec3(reset_position), reset_range: reset_range.unwrap_or(0.0), map_id: map_id.unwrap(), name: name.unwrap().into(), diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs index 5536ffe..bcf3098 100644 --- a/crates/joko_package/src/io/serialize.rs +++ b/crates/joko_package/src/io/serialize.rs @@ -10,7 +10,7 @@ use joko_package_models::{ }; use miette::{Context, IntoDiagnostic, Result}; use ordered_hash_map::OrderedHashMap; -use std::io::Write; +use std::{fmt::format, io::Write}; use tracing::info; use uuid::Uuid; use xot::{Element, Node, SerializeOptions, Xot}; @@ -19,7 +19,7 @@ use xot::{Element, Node, SerializeOptions, Xot}; pub(crate) fn save_pack_data_to_dir( pack_data: &LoadedPackData, writing_directory: &Dir, -) -> Result<()> { +) -> Result<(), String> { // save categories info!( "Saving data pack {}, {} categories, {} maps", @@ -32,22 +32,17 @@ pub(crate) fn save_pack_data_to_dir( let od = tree.new_element(names.overlay_data); let root_node = tree .new_root(od) - .into_diagnostic() - .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) - .wrap_err("failed to serialize cats")?; + .or(Err("failed to create new root with overlay data node"))?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od)?; let cats = tree .with_serialize_options(SerializeOptions { pretty: true }) .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to convert cats xot to string")?; + .or(Err("failed to convert cats xot to string"))?; writing_directory .create("categories.xml") - .into_diagnostic() - .wrap_err("failed to create categories.xml")? + .or(Err("failed to create categories.xml"))? .write_all(cats.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write to categories.xml")?; + .or(Err("failed to write to categories.xml"))?; // save maps for (map_id, map_data) in pack_data.maps.iter() { if map_data.markers.is_empty() && map_data.trails.is_empty() { @@ -63,17 +58,14 @@ pub(crate) fn save_pack_data_to_dir( let od = tree.new_element(names.overlay_data); let root_node: Node = tree .new_root(od) - .into_diagnostic() - .wrap_err("failed to create root wiht overlay data for pois")?; + .or(Err("failed to create root wiht overlay data for pois"))?; let pois = tree.new_element(names.pois); tree.append(od, pois) - .into_diagnostic() - .wrap_err("faild to append pois to od node")?; + .or(Err("faild to append pois to od node"))?; for marker in map_data.markers.values() { let poi = tree.new_element(names.poi); tree.append(pois, poi) - .into_diagnostic() - .wrap_err("failed to append poi (marker) to pois")?; + .or(Err("failed to append poi (marker) to pois"))?; let ele = tree.element_mut(poi).unwrap(); serialize_marker_to_element(marker, ele, &names); } @@ -86,30 +78,26 @@ pub(crate) fn save_pack_data_to_dir( } let trail_node = tree.new_element(names.trail); tree.append(pois, trail_node) - .into_diagnostic() - .wrap_err("failed to append a trail node to pois")?; + .or(Err("failed to append a trail node to pois"))?; let ele = tree.element_mut(trail_node).unwrap(); serialize_trail_to_element(trail, ele, &names); } let map_xml = tree .with_serialize_options(SerializeOptions { pretty: true }) .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to serialize map data to string")?; + .or(Err("failed to serialize map data to string"))?; writing_directory .create(format!("{map_id}.xml")) - .into_diagnostic() - .wrap_err("failed to create map xml file")? + .or(Err("failed to create map xml file"))? .write_all(map_xml.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write map data to file")?; + .or(Err("failed to write map data to file"))?; } Ok(()) } pub(crate) fn save_pack_texture_to_dir( pack_texture: &LoadedPackTexture, writing_directory: &Dir, -) -> Result<()> { +) -> Result<(), String> { info!( "Saving texture pack {}, {} textures, {} tbins", pack_texture.name, @@ -119,47 +107,40 @@ pub(crate) fn save_pack_texture_to_dir( // save images for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { - writing_directory - .create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir for an image: {img_path}") - })?; + writing_directory.create_dir_all(parent).or(Err(format!( + "failed to create parent dir for an image: {img_path}" + )))?; } writing_directory .create(img_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .or(Err(format!("failed to create file for image: {img_path}")))? .write(img) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; + .or(Err(format!( + "failed to write image bytes to file: {img_path}" + )))?; } // save tbins for (tbin_path, tbin) in pack_texture.tbins.iter() { if let Some(parent) = tbin_path.parent() { - writing_directory - .create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir of tbin: {tbin_path}") - })?; + writing_directory.create_dir_all(parent).or(Err(format!( + "failed to create parent dir of tbin: {tbin_path}" + )))?; } let mut bytes: Vec = Vec::with_capacity(8 + tbin.nodes.len() * std::mem::size_of::()); bytes.extend_from_slice(&tbin.version.to_ne_bytes()); bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); for node in &tbin.nodes { + let node = &node.0; bytes.extend_from_slice(&node[0].to_ne_bytes()); bytes.extend_from_slice(&node[1].to_ne_bytes()); bytes.extend_from_slice(&node[2].to_ne_bytes()); } writing_directory .create(tbin_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .or(Err(format!("failed to create tbin file: {tbin_path}")))? .write_all(&bytes) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + .or(Err(format!("failed to write tbin to path: {tbin_path}")))?; } Ok(()) } @@ -169,10 +150,11 @@ fn recursive_cat_serializer( names: &XotAttributeNameIDs, cats: &OrderedHashMap, parent: Node, -) -> Result<()> { +) -> Result<(), String> { for (_, cat) in cats { let cat_node = tree.new_element(names.marker_category); - tree.append(parent, cat_node).into_diagnostic()?; + tree.append(parent, cat_node) + .or(Err("Could not insert category node"))?; { let ele = tree.element_mut(cat_node).unwrap(); ele.set_attribute(names.display_name, &cat.display_name); @@ -204,9 +186,10 @@ fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttri } fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.xpos, format!("{}", marker.position[0])); - ele.set_attribute(names.ypos, format!("{}", marker.position[1])); - ele.set_attribute(names.zpos, format!("{}", marker.position[2])); + let position = &marker.position.0; + ele.set_attribute(names.xpos, format!("{}", position[0])); + ele.set_attribute(names.ypos, format!("{}", position[1])); + ele.set_attribute(names.zpos, format!("{}", position[2])); ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); ele.set_attribute(names.map_id, format!("{}", marker.map_id)); ele.set_attribute(names.category, &marker.category); @@ -222,17 +205,17 @@ fn serialize_route_to_element( route: &Route, parent: &Node, names: &XotAttributeNameIDs, -) -> Result<()> { +) -> Result<(), String> { let route_node = tree.new_element(names.route); tree.append(*parent, route_node) - .into_diagnostic() - .wrap_err("failed to append route to pois")?; + .or(Err("failed to append route to pois"))?; let ele = tree.element_mut(route_node).unwrap(); + let reset_position = &route.reset_position.0; ele.set_attribute(names.category, route.category.clone()); - ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); - ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); - ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.resetposx, format!("{}", reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", reset_position[2])); ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); ele.set_attribute(names.name, route.name.clone()); ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); @@ -243,8 +226,10 @@ fn serialize_route_to_element( format!("{}", route.source_file_uuid), ); for pos in &route.path { + let pos = &pos.0; let child = tree.new_element(names.poi); - tree.append(route_node, child).into_diagnostic()?; + tree.append(route_node, child) + .or(Err("Could not inser child node"))?; let child_elt = tree.element_mut(child).unwrap(); child_elt.set_attribute(names.xpos, format!("{}", pos.x)); child_elt.set_attribute(names.ypos, format!("{}", pos.y)); diff --git a/crates/joko_package/src/manager/mod.rs b/crates/joko_package/src/manager/mod.rs index b49fe0f..8063da8 100644 --- a/crates/joko_package/src/manager/mod.rs +++ b/crates/joko_package/src/manager/mod.rs @@ -17,11 +17,13 @@ We will make not having a valid category/texture/tbin path as allowed. So, users */ mod pack; -mod package; +mod package_data; +mod package_ui; pub use pack::import::{import_pack_from_zip_file_path, ImportStatus}; pub use pack::loaded::{ build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, load_all_from_dir, LoadedPackData, LoadedPackTexture, }; -pub use package::{PackageDataManager, PackageUIManager}; +pub use package_data::PackageDataManager; +pub use package_ui::PackageUIManager; diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs index 4f3fd41..02120e7 100644 --- a/crates/joko_package/src/manager/pack/active.rs +++ b/crates/joko_package/src/manager/pack/active.rs @@ -3,17 +3,20 @@ use jokoapi::end_point::mounts::Mount; use ordered_hash_map::OrderedHashMap; use egui::TextureHandle; -use glam::{vec2, Vec2, Vec3}; use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::INCHES_PER_METER; -use joko_core::RelativePath; +use joko_core::{ + serde_glam::{Vec2, Vec3}, + RelativePath, +}; use joko_render_models::{ marker::{MarkerObject, MarkerVertex}, trail::TrailObject, }; -use jokolink::MumbleLink; +use joko_link::MumbleLink; /* - activation data with uuids and track the latest timestamp that will be activated @@ -77,9 +80,9 @@ impl ActiveMarker { .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); - let player_distance = pos.distance(link.player_pos); - let camera_distance = pos.distance(link.cam_pos); - let fade_near_far = Vec2::new(fade_near, fade_far); + let player_distance = pos.0.distance(link.player_pos.0); + let camera_distance = pos.0.distance(link.cam_pos.0); + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); let alpha = attrs.get_alpha().copied().unwrap_or(1.0); let color = attrs.get_color().copied().unwrap_or_default(); @@ -106,10 +109,10 @@ impl ActiveMarker { return None; } // markers are 1 meter in width/height by default - let mut pos = pos; + let mut pos = pos.0; pos.y += height_offset; - let direction_to_marker = link.cam_pos - pos; - let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); + let direction_to_marker = link.cam_pos.0 - pos; + let direction_to_side = direction_to_marker.normalize().cross(glam::Vec3::Y); let far_offset = { let dpi = if link.dpi_scaling <= 0 { @@ -117,7 +120,7 @@ impl ActiveMarker { } else { link.dpi as f32 } / 96.0; - let gw2_width = link.client_size.as_vec2().x / dpi; + let gw2_width = link.client_size.0.as_vec2().x / dpi; // offset (half width i.e. distance from center of the marker to the side of the marker) const SIDE_OFFSET_FAR: f32 = 1.0; @@ -141,30 +144,30 @@ impl ActiveMarker { let x_offset = far_offset; let y_offset = x_offset; // seems all markers are squares let bottom_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 1.0), + position: Vec3(pos - (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 1.0)), alpha, color, fade_near_far, }; let top_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 0.0), + position: Vec3(pos - (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 0.0)), alpha, color, fade_near_far, }; let top_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 0.0), + position: Vec3(pos + (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 0.0)), alpha, color, fade_near_far, }; let bottom_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 1.0), + position: Vec3(pos + (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 1.0)), alpha, color, fade_near_far, @@ -202,7 +205,7 @@ impl ActiveTrail { .copied() .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) / INCHES_PER_METER; - let fade_near_far = Vec2::new(fade_near, fade_far); + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); let color = attrs.get_color().copied().unwrap_or([0u8; 4]); // default taco width let horizontal_offset = 20.0 / INCHES_PER_METER; @@ -214,39 +217,42 @@ impl ActiveTrail { // trail mesh is split by separating different parts with a [0, 0, 0] // we will call each separate trail mesh as a "strip" of trail. // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. - for strip in positions.split(|&v| v == Vec3::ZERO) { + for strip in positions.split(|&v| v.0 == glam::Vec3::ZERO) { let mut y_offset = 1.0; for two_positions in strip.windows(2) { - let first = two_positions[0]; - let second = two_positions[1]; + let first = two_positions[0].0; + let second = two_positions[1].0; // right side of the vector from first to second - let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); + let right_side = (second - first) + .normalize() + .cross(glam::Vec3::Y) + .normalize(); let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; let first_left = MarkerVertex { - position: first - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, y_offset), + position: Vec3(first - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, y_offset)), alpha, color, fade_near_far, }; let first_right = MarkerVertex { - position: first + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, y_offset), + position: Vec3(first + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, y_offset)), alpha, color, fade_near_far, }; let second_left = MarkerVertex { - position: second - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, new_offset), + position: Vec3(second - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, new_offset)), alpha, color, fade_near_far, }; let second_right = MarkerVertex { - position: second + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, new_offset), + position: Vec3(second + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, new_offset)), alpha, color, fade_near_far, diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs index e7cc173..3ad1a3a 100644 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ b/crates/joko_package/src/manager/pack/category_selection.rs @@ -1,15 +1,18 @@ +use joko_components::ComponentDataExchange; use joko_package_models::{ attributes::CommonAttributes, category::Category, package::{PackCore, PackageImportReport}, }; +use joko_render_models::messages::UIToUIMessage; +use miette::{IntoDiagnostic, Result}; use ordered_hash_map::OrderedHashMap; use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::message::{UIToBackMessage, UIToUIMessage}; +use crate::message::MessageToPackageBack; #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct CategorySelection { @@ -212,31 +215,35 @@ impl CategorySelection { } fn context_menu( - u2b_sender: &std::sync::mpsc::Sender, + u2b_sender: &tokio::sync::mpsc::Sender, cs: &mut CategorySelection, ui: &mut egui::Ui, ) { if ui.button("Activate branch").clicked() { cs.is_selected = true; CategorySelection::recursive_set_all(&mut cs.children, true); - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange( - cs.uuid, true, - )); + let msg = bincode::serialize( + &MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true), + ) + .unwrap(); //shall crash if wrong serialization of messages + let _ = u2b_sender.send(msg); ui.close_menu(); } if ui.button("Deactivate branch").clicked() { CategorySelection::recursive_set_all(&mut cs.children, false); cs.is_selected = false; - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationBranchStatusChange( - cs.uuid, false, - )); + let msg = bincode::serialize( + &MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false), + ) + .unwrap(); //shall crash if wrong serialization of messages + let _ = u2b_sender.send(msg); ui.close_menu(); } } pub fn recursive_selection_ui( - u2b_sender: &std::sync::mpsc::Sender, - _u2u_sender: &std::sync::mpsc::Sender, + u2b_sender: &tokio::sync::mpsc::Sender, + _u2u_sender: &tokio::sync::mpsc::Sender, selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, @@ -257,12 +264,14 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { - let _ = u2b_sender.send( - UIToBackMessage::CategoryActivationElementStatusChange( + let msg = bincode::serialize( + &MessageToPackageBack::CategoryActivationElementStatusChange( cat.uuid, cat.is_selected, ), - ); + ) + .unwrap(); //shall crash if wrong serialization of messages + let _ = u2b_sender.send(msg); *is_dirty = true; } } diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs index 65135f8..0234a46 100644 --- a/crates/joko_package/src/manager/pack/import.rs +++ b/crates/joko_package/src/manager/pack/import.rs @@ -1,8 +1,6 @@ use joko_package_models::package::PackCore; use tracing::info; -use miette::Result; - #[derive(Debug, Default)] pub enum ImportStatus { #[default] @@ -17,7 +15,7 @@ pub enum ImportStatus { pub fn import_pack_from_zip_file_path( file_path: std::path::PathBuf, extract_temporary_path: &std::path::PathBuf, -) -> Result<(String, PackCore)> { +) -> Result<(String, PackCore), String> { info!("starting to get pack from taco"); crate::io::get_pack_from_taco_zip(file_path.clone(), extract_temporary_path).map(|pack| { ( diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs index 7219efa..223b475 100644 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ b/crates/joko_package/src/manager/pack/loaded.rs @@ -1,8 +1,10 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, + path::PathBuf, sync::Arc, }; +use joko_components::ComponentDataExchange; use joko_package_models::{ attributes::{Behavior, CommonAttributes}, category::Category, @@ -15,30 +17,32 @@ use ordered_hash_map::OrderedHashMap; use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; use image::EncodableLayout; +use serde::{Deserialize, Serialize}; use tracing::{debug, error, info, info_span, trace}; use uuid::Uuid; -use crate::message::BackToUIMessage; +use crate::message::MessageToPackageUI; use crate::{ io::{load_pack_core_from_normalized_folder, save_pack_data_to_dir, save_pack_texture_to_dir}, manager::{ pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, - package::EXTRACT_DIRECTORY_NAME, + package_data::EXTRACT_DIRECTORY_NAME, }, - message::{UIToBackMessage, UIToUIMessage}, + message::MessageToPackageBack, }; use joko_core::{ + serde_glam::Vec3, task::{AsyncTask, AsyncTaskGuard}, RelativePath, }; -use joko_render_models::trail::TrailObject; -use jokolink::MumbleLink; -use miette::{bail, Context, IntoDiagnostic, Result}; +use joko_render_models::{messages::UIToUIMessage, trail::TrailObject}; +use joko_link::MumbleLink; +use miette::{Context, IntoDiagnostic, Result}; use super::activation::{ActivationData, ActivationType}; use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; use crate::manager::pack::category_selection::CategorySelection; -use crate::manager::package::{ +use crate::manager::package_data::{ EDITABLE_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME, PACKAGES_DIRECTORY_NAME, PACKAGE_MANAGER_DIRECTORY_NAME, }; @@ -53,10 +57,11 @@ type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); //TODO: separate in front and back tasks pub(crate) struct PackTasks { //an object that can handle such tasks should be passed as argument of any function that may required an async action - save_texture_task: AsyncTask>, - save_data_task: AsyncTask>, - save_report_task: AsyncTask<(Arc, PackageImportReport), Result<()>>, - load_all_packs_task: AsyncTask, Result>, + save_texture_task: AsyncTask>, + save_data_task: AsyncTask>, + save_report_task: AsyncTask<(Arc, PackageImportReport), Result<(), String>>, + load_all_packs_task: + AsyncTask<(Arc, std::path::PathBuf), Result>, } //TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? @@ -81,21 +86,24 @@ pub struct LoadedPackData { active_elements: HashSet, //keep track of which elements are active } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct LoadedPackTexture { + //TODO: there is a need for a late loading of texture to avoid transmitting them (serialize) pub name: String, pub uuid: Uuid, /// The directory inside which the pack data is stored /// There should be a subdirectory called `core` which stores the pack core /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. /// eg: Active categories, activation data etc.. - pub dir: Arc, + //pub dir: Arc, + pub path: std::path::PathBuf, pub source_files: BTreeMap, pub tbins: HashMap, pub textures: HashMap>, /// The selection of categories which are "enabled" and markers belonging to these may be rendered selectable_categories: OrderedHashMap, + #[serde(skip)] current_map_data: CurrentMapData, activation_data: ActivationData, //active_elements: HashSet, //which are the active elements (loaded) @@ -122,6 +130,7 @@ impl PackTasks { } pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { + //saved on load, or change of list of what to display if status { std::mem::take(&mut texture_pack._is_dirty); let _ = self @@ -147,10 +156,14 @@ impl PackTasks { .send((target_dir, report)); } } - pub fn load_all_packs(&self, jokolay_dir: Arc) { - let _ = self.load_all_packs_task.lock().unwrap().send(jokolay_dir); + pub fn load_all_packs(&self, jokolay_dir: Arc, root_path: std::path::PathBuf) { + let _ = self + .load_all_packs_task + .lock() + .unwrap() + .send((jokolay_dir, root_path)); } - pub fn wait_for_load_all_packs(&self) -> Result { + pub fn wait_for_load_all_packs(&self) -> Result { self.load_all_packs_task.lock().unwrap().recv().unwrap() } @@ -158,21 +171,22 @@ impl PackTasks { fn change_map( &self, pack: &mut LoadedPackData, - b2u_sender: &std::sync::mpsc::Sender, + b2u_sender: &std::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, ) { //TODO - unimplemented!(); + unimplemented!("PackTask::change_map is not implemented"); } - fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<()> { - trace!("Save texture package {:?}", pack_texture.dir); + fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<(), String> { + trace!("Save texture package {:?}", pack_texture.path); + let std_file = std::fs::OpenOptions::new() + .open(&pack_texture.path) + .or(Err("Could not open file"))?; + let dir = cap_std::fs_utf8::Dir::from_std_file(std_file); match serde_json::to_string_pretty(&pack_texture.selectable_categories) { - Ok(cs_json) => match pack_texture - .dir - .write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) - { + Ok(cs_json) => match dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { Ok(_) => { debug!("wrote cat selections to disk after creating a default from pack"); } @@ -185,10 +199,7 @@ impl PackTasks { } } match serde_json::to_string_pretty(&pack_texture.activation_data) { - Ok(ad_json) => match pack_texture - .dir - .write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) - { + Ok(ad_json) => match dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { Ok(_) => { debug!("wrote activation to disk after creating a default from pack"); } @@ -200,32 +211,28 @@ impl PackTasks { error!(?e, "failed to serialize activation"); } } - let writing_directory = pack_texture - .dir + let writing_directory = dir .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; + .or(Err("failed to open core pack directory"))?; save_pack_texture_to_dir(&pack_texture, &writing_directory)?; Ok(()) } - fn async_save_data(pack_data: LoadedPackData) -> Result<()> { + fn async_save_data(pack_data: LoadedPackData) -> Result<(), String> { trace!("Save data package {:?}", pack_data.dir); pack_data .dir .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to create xmlpack directory")?; + .or(Err("failed to create xmlpack directory"))?; let writing_directory = pack_data .dir .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; + .or(Err("failed to open core pack directory"))?; save_pack_data_to_dir(&pack_data, &writing_directory)?; Ok(()) } - fn async_save_report(input: (Arc, PackageImportReport)) -> Result<()> { + fn async_save_report(input: (Arc, PackageImportReport)) -> Result<(), String> { let (writing_directory, report) = input; trace!("Save report package {:?}", writing_directory); match serde_json::to_string_pretty(&report) { @@ -314,22 +321,20 @@ impl LoadedPackData { }) .flatten() } - pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { + pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { if !pack_dir .try_exists(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? + .or(Err("failed to check if pack core exists"))? { - bail!("pack core doesn't exist in this pack"); + return Err("pack core doesn't exist in this pack".to_string()); } let core_dir = pack_dir .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; + .or(Err("failed to open core pack directory"))?; let start = std::time::SystemTime::now(); let import_report = LoadedPackData::load_import_report(&pack_dir); let core = load_pack_core_from_normalized_folder(&core_dir, import_report) - .wrap_err("failed to load pack from dir")?; + .or(Err("failed to load pack from dir"))?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!( "Loading of package from disk {} took {} ms", @@ -388,7 +393,7 @@ impl LoadedPackData { #[allow(clippy::too_many_arguments)] pub(crate) fn tick( &mut self, - b2u_sender: &std::sync::mpsc::Sender, + b2u_sender: &tokio::sync::mpsc::Sender, _loop_index: u128, link: &MumbleLink, currently_used_files: &BTreeMap, @@ -402,10 +407,10 @@ impl LoadedPackData { //tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - let _ = b2u_sender.send(BackToUIMessage::PackageActiveElements( - self.uuid, - active_elements.clone(), - )); + let _ = b2u_sender.send( + MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()) + .into(), + ); self.active_elements = active_elements.clone(); next_loaded.extend(active_elements); } @@ -413,7 +418,7 @@ impl LoadedPackData { fn on_map_changed( &mut self, - b2u_sender: &std::sync::mpsc::Sender, + b2u_sender: &tokio::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, active_elements: &mut HashSet, @@ -505,13 +510,16 @@ impl LoadedPackData { } } if let Some(tex_path) = common_attributes.get_icon_file() { - let _ = b2u_sender.send(BackToUIMessage::MarkerTexture( - self.uuid, - tex_path.clone(), - marker.guid, - marker.position, - common_attributes, - )); + let _ = b2u_sender.send( + MessageToPackageUI::MarkerTexture( + self.uuid, + tex_path.clone(), + marker.guid, + marker.position, + common_attributes, + ) + .into(), + ); } else { debug!("no texture attribute on this marker"); } @@ -545,12 +553,15 @@ impl LoadedPackData { let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - let _ = b2u_sender.send(BackToUIMessage::TrailTexture( - self.uuid, - tex_path.clone(), - trail.guid, - common_attributes, - )); + let _ = b2u_sender.send( + MessageToPackageUI::TrailTexture( + self.uuid, + tex_path.clone(), + trail.guid, + common_attributes, + ) + .into(), + ); } else { debug!("no texture attribute on this trail"); } @@ -595,8 +606,8 @@ impl LoadedPackTexture { } pub fn category_sub_menu( &mut self, - u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, + u2b_sender: &tokio::sync::mpsc::Sender, + u2u_sender: &tokio::sync::mpsc::Sender, ui: &mut egui::Ui, show_only_active: bool, import_quality_report: &PackageImportReport, @@ -614,7 +625,9 @@ impl LoadedPackTexture { ); }); if self._is_dirty { - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); + let msg = + bincode::serialize(&MessageToPackageBack::CategoryActivationStatusChanged).unwrap(); //shall crash if wrong serialization of messages + let _ = u2b_sender.send(msg); } } @@ -623,13 +636,13 @@ impl LoadedPackTexture { } pub(crate) fn tick( &mut self, - u2u_sender: &std::sync::mpsc::Sender, + u2u_sender: &tokio::sync::mpsc::Sender, _timestamp: f64, link: &MumbleLink, //next_on_screen: &mut HashSet, z_near: f32, _tasks: &PackTasks, - ) { + ) -> Result<()> { tracing::trace!( "LoadedPackTexture.tick: {} {}-{} {}-{}", self.name, @@ -649,7 +662,9 @@ impl LoadedPackTexture { self.name, marker_objects.len() ); - let _ = u2u_sender.send(UIToUIMessage::BulkMarkerObject(marker_objects)); + let msg = bincode::serialize(&UIToUIMessage::BulkMarkerObject(marker_objects)) + .into_diagnostic()?; + let _ = u2u_sender.send(msg); let mut trail_objects = Vec::new(); for trail in self.current_map_data.active_trails.values() { trail_objects.push(TrailObject { @@ -663,7 +678,10 @@ impl LoadedPackTexture { self.name, trail_objects.len() ); - let _ = u2u_sender.send(UIToUIMessage::BulkTrailObject(trail_objects)); + let msg = + bincode::serialize(&UIToUIMessage::BulkTrailObject(trail_objects)).into_diagnostic()?; + let _ = u2u_sender.send(msg); + Ok(()) } pub fn swap(&mut self) { @@ -685,13 +703,14 @@ impl LoadedPackTexture { default_tex_id: &TextureHandle, tex_path: &RelativePath, marker_uuid: Uuid, - position: glam::Vec3, + position: Vec3, common_attributes: CommonAttributes, ) { if !self.current_map_data.active_textures.contains_key(tex_path) { if let Some(tex) = self.textures.get(tex_path) { let img = image::load_from_memory(tex).unwrap(); + //TODO: insertion must happen inside the UI => egui_context should never be transmitted on a tick() self.current_map_data.active_textures.insert( tex_path.clone(), egui_context.load_texture( @@ -851,31 +870,41 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { Ok(marker_packs_dir) } -pub fn load_all_from_dir(jokolay_dir: Arc) -> Result { - let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; +pub fn load_all_from_dir( + input: (Arc, std::path::PathBuf), +) -> Result { + let (jokolay_dir, root_path) = input; + let marker_packs_dir = + jokolay_to_marker_dir(&jokolay_dir).or(Err("Failed to open packages directory"))?; + let marker_packs_path = jokolay_to_marker_path(&root_path); let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); let mut report_packs: BTreeMap = Default::default(); for entry in marker_packs_dir .entries() - .into_diagnostic() - .wrap_err("failed to get entries of marker packs dir")? + .or(Err("failed to get entries of marker packs dir"))? { - let entry = entry.into_diagnostic()?; - if entry.metadata().into_diagnostic()?.is_file() { + let entry = entry.or(Err("Failed to read packages directory"))?; + if entry + .metadata() + .or(Err("Could not read folder metadata"))? + .is_file() + { continue; } if let Ok(name) = entry.file_name() { - let pack_dir = entry - .open_dir() - .into_diagnostic() - .wrap_err(format!("failed to open pack entry as directory: {}", name))?; + let pack_path = marker_packs_path.join(&name); + let pack_dir = entry.open_dir().or(Err(format!( + "failed to open pack entry as directory: {}", + name + )))?; { if name == EDITABLE_PACKAGE_NAME { //TODO: have a version of loading that does not involve already ingested packages if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { - let lp = build_from_core(name.clone(), pack_dir.into(), pack_core); + let lp = + build_from_core(name.clone(), pack_dir.into(), pack_path, pack_core); let (data, tex, report) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); @@ -886,7 +915,7 @@ pub fn load_all_from_dir(jokolay_dir: Arc) -> Result { } else { let span_guard = info_span!("loading pack from dir", name).entered(); - match build_from_dir(name.clone(), pack_dir.into()) { + match build_from_dir(name.clone(), pack_dir.into(), pack_path) { Ok(lp) => { let (data, tex, report) = lp; data_packs.insert(data.uuid, data); @@ -905,33 +934,40 @@ pub fn load_all_from_dir(jokolay_dir: Arc) -> Result { Ok((data_packs, texture_packs, report_packs)) } -fn build_from_dir(name: String, pack_dir: Arc) -> Result { +fn build_from_dir( + name: String, + pack_dir: Arc, + pack_path: PathBuf, +) -> Result { if !pack_dir .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? + .or(Err("failed to check if pack core exists"))? { - bail!("pack core doesn't exist in this pack"); + return Err("pack core doesn't exist in this pack".to_string()); } let core_dir = pack_dir .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; + .or(Err("failed to open core pack directory"))?; let start = std::time::SystemTime::now(); let import_report = LoadedPackData::load_import_report(&pack_dir); let core = load_pack_core_from_normalized_folder(&core_dir, import_report) - .wrap_err("failed to load pack from dir")?; + .or(Err("failed to load pack from dir"))?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!( "Loading of package from disk {} took {} ms", name, elaspsed.as_millis() ); - let res = build_from_core(name.clone(), pack_dir, core); + let res = build_from_core(name.clone(), pack_dir, pack_path, core); Ok(res) } -pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> ImportTriplet { +pub fn build_from_core( + name: String, + pack_dir: Arc, + path: PathBuf, + core: PackCore, +) -> ImportTriplet { let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); let data = LoadedPackData { name: name.clone(), @@ -974,7 +1010,7 @@ pub fn build_from_core(name: String, pack_dir: Arc, core: PackCore) -> Impo current_map_data: Default::default(), _is_dirty: false, activation_data, - dir: Arc::clone(&pack_dir), + path, name, tbins: core.tbins, //active_elements: Default::default(), diff --git a/crates/joko_package/src/manager/package_data.rs b/crates/joko_package/src/manager/package_data.rs new file mode 100644 index 0000000..4d92339 --- /dev/null +++ b/crates/joko_package/src/manager/package_data.rs @@ -0,0 +1,516 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, +}; + +use cap_std::fs_utf8::Dir; +use joko_components::ComponentDataExchange; +use joko_package_models::package::PackageImportReport; + +use tracing::{error, info, info_span, trace}; + +use crate::{ + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, message::MessageToPackageBack, +}; +use joko_link::{MumbleLink, MumbleLinkSharedState}; +use miette::{IntoDiagnostic, Result}; +use uuid::Uuid; + +use crate::manager::pack::loaded::{LoadedPackData, PackTasks}; +use crate::message::MessageToPackageUI; + +use super::pack::loaded::{jokolay_to_marker_dir, jokolay_to_marker_path}; + +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose +pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading +pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite +pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package + // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; + +#[derive(Clone)] +pub struct PackageBackSharedState { + choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI + pub root_dir: Arc, + pub root_path: std::path::PathBuf, + #[allow(dead_code)] + pub editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration + extract_path: std::path::PathBuf, +} + +/// It manage everything that has to do with marker packs. +/// 1. imports, loads, saves and exports marker packs. +/// 2. maintains the categories selection data for every pack +/// 3. contains activation data globally and per character +/// 4. When we load into a map, it filters the markers and runs the logic every frame +/// 1. If a marker needs to be activated (based on player position or whatever) +/// 2. marker needs to be drawn +/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture +/// 4. render that marker use joko_render + +#[must_use] +pub struct PackageDataManager { + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + //_marker_manager_dir: Arc, + /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. + /// The name of the child directory is the name of the pack + //pub marker_packs_dir: Arc, + pub marker_packs_path: std::path::PathBuf, + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + pub packs: BTreeMap, + tasks: PackTasks, + current_map_id: u32, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, + + pub currently_used_files: BTreeMap, + parents: HashMap, + loaded_elements: HashSet, + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + pub state: PackageBackSharedState, +} + +impl PackageDataManager { + /// Creates a new instance of [MarkerManager]. + /// 1. It opens the marker manager directory + /// 2. loads its configuration + /// 3. opens the packs directory + /// 4. loads all the packs + /// 5. loads all the activation data + /// 6. returns self + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + ) -> Result { + let marker_packs_dir = jokolay_to_marker_dir(&root_dir)?; + let marker_packs_path = jokolay_to_marker_path(root_path); + //TODO: load configuration from disk (ui.toml) + let editable_path = jokolay_to_editable_path(root_path.clone()) + .to_str() + .unwrap() + .to_string(); + let state = PackageBackSharedState { + choice_of_category_changed: false, + root_dir, + root_path: root_path.to_owned(), + editable_path: std::path::PathBuf::from(editable_path), + extract_path: jokolay_to_extract_path(root_path), + }; + Ok(Self { + packs: Default::default(), + tasks: PackTasks::new(), + //marker_packs_dir: Arc::new(marker_packs_dir), + marker_packs_path, + current_map_id: 0, + save_interval: 0.0, + currently_used_files: Default::default(), + parents: Default::default(), + loaded_elements: Default::default(), + channel_sender, + channel_receiver, + state, + }) + } + + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_set(uuid, status) { + break; + } + } + } + + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_branch_set(uuid, status) { + break; + } + } + } + + pub fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> HashSet + where + I: Iterator, + { + let iter = input.into_iter(); + let mut result: HashSet = HashSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; + } + next_gen.push(*p); + } + } + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + #[allow(unreachable_code)] // sillyness of some tools + { + unreachable!("The loop should always return") + } + } + + pub fn get_active_elements_parents( + &mut self, + categories_and_elements_to_be_loaded: HashSet, + ) { + trace!( + "There are {} active elements", + categories_and_elements_to_be_loaded.len() + ); + + //first merge the parents to iterate overit + let mut parents: HashMap = Default::default(); + for pack in self.packs.values_mut() { + parents.extend(pack.entities_parents.clone()); + } + self.parents = parents; + //then climb up the tree of parent's categories + self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); + } + + fn handle_message(&mut self, msg: MessageToPackageBack) { + //let (b2u_sender, _) = package_manager.channels(); + match msg { + MessageToPackageBack::ActiveFiles(currently_used_files) => { + tracing::trace!("Handling of MessageToPackageBack::ActiveFiles"); + self.set_currently_used_files(currently_used_files); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategoryActivationElementStatusChange(category_uuid, status) => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationElementStatusChange" + ); + self.category_set(category_uuid, status); + } + MessageToPackageBack::CategoryActivationBranchStatusChange(category_uuid, status) => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationBranchStatusChange" + ); + self.category_branch_set(category_uuid, status); + } + MessageToPackageBack::CategoryActivationStatusChanged => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationStatusChanged" + ); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategorySetAll(status) => { + tracing::trace!("Handling of MessageToPackageBack::CategorySetAll"); + self.category_set_all(status); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::DeletePacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageBack::DeletePacks"); + let std_file = std::fs::OpenOptions::new() + .open(&self.marker_packs_path) + .or(Err("Could not open file")) + .unwrap(); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let mut deleted = Vec::new(); + + for pack_uuid in to_delete { + if let Some(pack) = self.packs.remove(&pack_uuid) { + if let Err(e) = marker_packs_dir.remove_dir_all(&pack.name) { + error!(?e, pack.name, "failed to remove pack"); + } else { + info!("deleted marker pack: {}", pack.name); + deleted.push(pack_uuid); + } + } + } + let _ = self + .channel_sender + .send(MessageToPackageUI::DeletedPacks(deleted).into()); + } + MessageToPackageBack::ImportPack(file_path) => { + tracing::trace!("Handling of MessageToPackageBack::ImportPack"); + let _ = self + .channel_sender + .send(MessageToPackageUI::NbTasksRunning(1).into()); + let start = std::time::SystemTime::now(); + let result = import_pack_from_zip_file_path(file_path, &self.state.extract_path); + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of taco package from disk took {} ms", + elaspsed.as_millis() + ); + match result { + Ok((file_name, pack)) => { + let _ = self + .channel_sender + .send(MessageToPackageUI::ImportedPack(file_name, pack).into()); + } + Err(e) => { + let _ = self + .channel_sender + .send(MessageToPackageUI::ImportFailure(e).into()); + } + } + let _ = self + .channel_sender + .send(MessageToPackageUI::NbTasksRunning(0).into()); + } + MessageToPackageBack::ReloadPack => { + unimplemented!( + "Handling of MessageToPackageBack::ReloadPack has not been implemented yet" + ); + } + MessageToPackageBack::SavePack(name, pack) => { + tracing::trace!("Handling of MessageToPackageBack::SavePack"); + let std_file = std::fs::OpenOptions::new() + .open(&self.marker_packs_path) + .or(Err("Could not open file")) + .unwrap(); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let name = name.as_str(); + if marker_packs_dir.exists(name) { + match marker_packs_dir.remove_dir_all(name).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to delete already existing marker pack"); + } + } + } + if let Err(e) = marker_packs_dir.create_dir_all(name) { + error!(?e, "failed to create directory for pack"); + } + match marker_packs_dir.open_dir(name) { + Ok(dir) => { + let pack_path = self.marker_packs_path.join(name); + let (data_pack, mut texture_pack, mut report) = + build_from_core(name.to_string(), dir.into(), pack_path, pack); + tracing::trace!("Package loaded into data and texture"); + let uuid_of_insertion = self.save(data_pack, report.clone()); + report.uuid = uuid_of_insertion; + texture_pack.uuid = uuid_of_insertion; + let _ = self + .channel_sender + .send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); + } + Err(e) => { + error!( + ?e, + "failed to open marker pack directory to save pack {:?} {}", + self.marker_packs_path, + name + ); + } + }; + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + } + } + } + + pub fn flush_all_messages(&mut self) -> PackageBackSharedState { + tracing::trace!( + "choice_of_category_changed: {}", + self.state.choice_of_category_changed + ); + + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + let msg = bincode::deserialize(&msg).unwrap(); + messages.push(msg); + } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() + } + + pub fn tick( + &mut self, + loop_index: u128, + ms: &MumbleLinkSharedState, + link: Option<&MumbleLink>, + ) { + let mut currently_used_files: BTreeMap = Default::default(); + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + + let link = if ms.read_ui_link { + ms.copy_of_ui_link.as_ref() + } else { + link + }; + + if let Some(link) = link { + //TODO: how to save/load the active files ? + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { + currently_used_files.insert( + marker.source_file_uuid, + *self + .currently_used_files + .get(&marker.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { + currently_used_files.insert( + trail.source_file_uuid, + *self + .currently_used_files + .get(&trail.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + } + } + let tasks = &self.tasks; + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + let _ = self + .channel_sender + .send(MessageToPackageUI::NbTasksRunning(tasks.count()).into()); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + &self.channel_sender, + loop_index, + link, + ¤tly_used_files, + have_used_files_list_changed || self.state.choice_of_category_changed, + map_changed, + tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + if map_changed { + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + let _ = self + .channel_sender + .send(MessageToPackageUI::ActiveElements(self.loaded_elements.clone()).into()); + } + if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed + { + //there is no point in sending a new list if nothing changed + let _ = self.channel_sender.send( + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()).into(), + ); + self.currently_used_files = currently_used_files; + let _ = self + .channel_sender + .send(MessageToPackageUI::TextureSwapChain.into()); + } + } + self.state.choice_of_category_changed = false; + } + + fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } + pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == data_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks + .save_report(Arc::clone(&data_pack.dir), report, true); + self.tasks.save_data(&mut data_pack, true); + let mut uuid_to_insert = data_pack.uuid; + while self.packs.contains_key(&uuid_to_insert) { + //collision avoidance + trace!( + "Uuid collision detected for {} for package {}", + uuid_to_insert, + data_pack.name + ); + uuid_to_insert = Uuid::new_v4(); + } + data_pack.uuid = uuid_to_insert; + self.packs.insert(uuid_to_insert, data_pack); + uuid_to_insert + } + + pub fn load_all(&mut self) { + once::assert_has_not_been_called!("Early load must happen only once"); + // Called only once at application start. + let _ = self + .channel_sender + .send(MessageToPackageUI::NbTasksRunning(1).into()); + self.tasks.load_all_packs( + Arc::clone(&self.state.root_dir), + self.state.root_path.clone(), + ); + if let Ok((data_packages, texture_packages, report_packages)) = + self.tasks.wait_for_load_all_packs() + { + for (uuid, data_pack) in data_packages { + self.packs.insert(uuid, data_pack); + } + for ((_, texture_pack), (_, report)) in + std::iter::zip(texture_packages, report_packages) + { + let _ = self + .channel_sender + .send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); + } + + let _ = self + .channel_sender + .send(MessageToPackageUI::NbTasksRunning(0).into()); + } + let _ = self + .channel_sender + .send(MessageToPackageUI::FirstLoadDone.into()); + } +} diff --git a/crates/joko_package/src/manager/package.rs b/crates/joko_package/src/manager/package_ui.rs similarity index 61% rename from crates/joko_package/src/manager/package.rs rename to crates/joko_package/src/manager/package_ui.rs index 2891714..4c07332 100644 --- a/crates/joko_package/src/manager/package.rs +++ b/crates/joko_package/src/manager/package_ui.rs @@ -1,65 +1,35 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, sync::{Arc, Mutex}, }; -use cap_std::fs_utf8::Dir; use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; -use glam::Vec3; use image::EncodableLayout; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; +use joko_render_models::messages::UIToUIMessage; use tracing::{info_span, trace}; -use crate::message::{UIToBackMessage, UIToUIMessage}; -use joko_core::RelativePath; -use jokolink::MumbleLink; +use crate::message::MessageToPackageBack; +use joko_components::{ComponentDataExchange, JokolayUIComponent, PeerComponentChannel}; +use joko_core::{serde_glam::Vec3, RelativePath}; +use joko_link::{MumbleChanges, MumbleLink}; use miette::Result; use uuid::Uuid; use crate::manager::pack::import::ImportStatus; -use crate::manager::pack::loaded::{LoadedPackData, LoadedPackTexture, PackTasks}; -use crate::message::BackToUIMessage; - -use super::pack::loaded::jokolay_to_marker_dir; - -pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose -pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose -pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading -pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite -pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package - // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; - -/// It manage everything that has to do with marker packs. -/// 1. imports, loads, saves and exports marker packs. -/// 2. maintains the categories selection data for every pack -/// 3. contains activation data globally and per character -/// 4. When we load into a map, it filters the markers and runs the logic every frame -/// 1. If a marker needs to be activated (based on player position or whatever) -/// 2. marker needs to be drawn -/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture -/// 4. render that marker use joko_render -#[must_use] -pub struct PackageDataManager { - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - //_marker_manager_dir: Arc, - /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. - /// The name of the child directory is the name of the pack - pub marker_packs_dir: Arc, - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - pub packs: BTreeMap, - tasks: PackTasks, - current_map_id: u32, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, - - pub currently_used_files: BTreeMap, - parents: HashMap, - loaded_elements: HashSet, +use crate::manager::pack::loaded::{LoadedPackTexture, PackTasks}; +use crate::message::MessageToPackageUI; + +//FIXME: there is an interest to merge the PackageUIManager and the render +#[derive(Clone)] +pub struct PackageUISharedState { + list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + first_load_done: bool, + nb_running_tasks_on_back: i32, // store the number of running tasks in background thread + import_status: Arc>, } + #[must_use] pub struct PackageUIManager { default_marker_texture: Option, @@ -72,276 +42,165 @@ pub struct PackageUIManager { all_files_activation_status: bool, // this consume a change of display event show_only_active: bool, pack_details: Option, // if filled, display the details of the package + + delayed_marker_texture: Vec<(Uuid, RelativePath, Uuid, Vec3, CommonAttributes)>, + delayed_trail_texture: Vec<(Uuid, RelativePath, Uuid, CommonAttributes)>, + + //egui_context: &'l egui::Context, //TODO: remove, this is not the proper place to be, or if it is, badly used + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + sender_u2u: Option>, + receiver_mumblelink: Option>, + receiver_near_scene: Option>, + state: PackageUISharedState, } -impl PackageDataManager { - /// Creates a new instance of [MarkerManager]. - /// 1. It opens the marker manager directory - /// 2. loads its configuration - /// 3. opens the packs directory - /// 4. loads all the packs - /// 5. loads all the activation data - /// 6. returns self - pub fn new(packs: BTreeMap, jokolay_dir: Arc) -> Result { - let marker_packs_dir = jokolay_to_marker_dir(&jokolay_dir)?; - Ok(Self { - packs, +impl PackageUIManager { + pub fn new( + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + ) -> Self { + let state = PackageUISharedState { + list_of_textures_changed: false, + first_load_done: false, + nb_running_tasks_on_back: 0, + import_status: Default::default(), + }; + Self { + packs: Default::default(), tasks: PackTasks::new(), - marker_packs_dir: Arc::new(marker_packs_dir), - //_marker_manager_dir: marker_manager_dir.into(), - current_map_id: 0, - save_interval: 0.0, - currently_used_files: Default::default(), - parents: Default::default(), - loaded_elements: Default::default(), - }) - } + reports: Default::default(), + default_marker_texture: None, + default_trail_texture: None, - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { - self.currently_used_files = currently_used_files; - } + all_files_activation_status: false, + show_only_active: true, + currently_used_files: Default::default(), // UI copy to (de-)activate files + pack_details: None, - pub fn category_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_set(uuid, status) { - break; - } + delayed_marker_texture: Default::default(), + delayed_trail_texture: Default::default(), + channel_sender, + channel_receiver, + sender_u2u: None, + receiver_mumblelink: None, + receiver_near_scene: None, + state, } } - pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_branch_set(uuid, status) { - break; + fn handle_message(&mut self, msg: MessageToPackageUI) { + match msg { + MessageToPackageUI::ActiveElements(active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::ActiveElements"); + self.update_active_categories(&active_elements); } - } - } - - pub fn category_set_all(&mut self, status: bool) { - for pack in self.packs.values_mut() { - pack.category_set_all(status); - } - } - - pub fn register(&mut self, element: Uuid, parent: Uuid) { - self.parents.insert(element, parent); - } - pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { - self.parents.get(element) - } - pub fn get_parents<'a, I>(&self, input: I) -> HashSet - where - I: Iterator, - { - let iter = input.into_iter(); - let mut result: HashSet = HashSet::new(); - let mut current_generation: Vec = Vec::new(); - for elt in iter { - current_generation.push(*elt) - } - //info!("starts with {}", current_generation.len()); - loop { - if current_generation.is_empty() { - //info!("ends with {}", result.len()); - return result; + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files) => { + tracing::trace!("Handling of MessageToPackageUI::CurrentlyUsedFiles"); + self.set_currently_used_files(currently_used_files); } - let mut next_gen: Vec = Vec::new(); - for elt in current_generation.iter() { - if let Some(p) = self.get_parent(elt) { - if result.contains(p) { - //avoid duplicate, redundancy or loop - continue; - } - next_gen.push(*p); - } + MessageToPackageUI::DeletedPacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageUI::DeletedPacks"); + self.delete_packs(to_delete); } - let to_insert = std::mem::replace(&mut current_generation, next_gen); - result.extend(to_insert); - } - #[allow(unreachable_code)] // sillyness of some tools - { - unreachable!("The loop should always return") - } - } - - pub fn get_active_elements_parents( - &mut self, - categories_and_elements_to_be_loaded: HashSet, - ) { - trace!( - "There are {} active elements", - categories_and_elements_to_be_loaded.len() - ); - - //first merge the parents to iterate overit - let mut parents: HashMap = Default::default(); - for pack in self.packs.values_mut() { - parents.extend(pack.entities_parents.clone()); - } - self.parents = parents; - //then climb up the tree of parent's categories - self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); - } - - pub fn tick( - &mut self, - b2u_sender: &std::sync::mpsc::Sender, - loop_index: u128, - link: Option<&MumbleLink>, - choice_of_category_changed: bool, - ) { - let mut currently_used_files: BTreeMap = Default::default(); - let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - - if let Some(link) = link { - //TODO: how to save/load the active files ? - let mut have_used_files_list_changed = false; - let map_changed = self.current_map_id != link.map_id; - self.current_map_id = link.map_id; - for pack in self.packs.values_mut() { - if let Some(current_map) = pack.maps.get(&link.map_id) { - for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { - currently_used_files.insert( - marker.source_file_uuid, - *self - .currently_used_files - .get(&marker.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } - } - for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { - currently_used_files.insert( - trail.source_file_uuid, - *self - .currently_used_files - .get(&trail.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } - } - } + MessageToPackageUI::FirstLoadDone => { + self.state.first_load_done = true; } - let tasks = &self.tasks; - for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(tasks.count())); - tasks.save_data(pack, pack.is_dirty()); - pack.tick( - b2u_sender, - loop_index, - link, - ¤tly_used_files, - have_used_files_list_changed || choice_of_category_changed, - map_changed, - tasks, - &mut categories_and_elements_to_be_loaded, - ); - std::mem::drop(span_guard); + MessageToPackageUI::ImportedPack(file_name, pack) => { + tracing::trace!("Handling of MessageToPackageUI::ImportedPack"); + *self.state.import_status.lock().unwrap() = + ImportStatus::PackDone(file_name, pack, false); + } + MessageToPackageUI::ImportFailure(message) => { + tracing::trace!("Handling of MessageToPackageUI::ImportFailure"); + *self.state.import_status.lock().unwrap() = + ImportStatus::PackError(miette::Report::msg(message)); } - if map_changed { - self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let _ = b2u_sender.send(BackToUIMessage::ActiveElements( - self.loaded_elements.clone(), + MessageToPackageUI::LoadedPack(pack_texture, report) => { + tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); + self.save(pack_texture, report); + self.state.import_status = Default::default(); + let _ = self + .channel_sender + .send(MessageToPackageBack::CategoryActivationStatusChanged.into()); + } + MessageToPackageUI::MarkerTexture( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::MarkerTexture"); + //FIXME: make it a TODO on tick() + self.delayed_marker_texture.push(( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, )); } - if map_changed || have_used_files_list_changed || choice_of_category_changed { - //there is no point in sending a new list if nothing changed - let _ = b2u_sender.send(BackToUIMessage::CurrentlyUsedFiles( - currently_used_files.clone(), + MessageToPackageUI::NbTasksRunning(nb_tasks) => { + tracing::trace!("Handling of MessageToPackageUI::NbTasksRunning"); + self.state.nb_running_tasks_on_back = nb_tasks; + } + MessageToPackageUI::PackageActiveElements(pack_uuid, active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::PackageActiveElements"); + self.update_pack_active_categories(pack_uuid, &active_elements); + } + MessageToPackageUI::TextureSwapChain => { + tracing::debug!("Handling of MessageToPackageUI::TextureSwapChain"); + self.swap(); + self.state.list_of_textures_changed = true; + } + MessageToPackageUI::TrailTexture( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::TrailTexture"); + self.delayed_trail_texture.push(( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, )); - self.currently_used_files = currently_used_files; - let _ = b2u_sender.send(BackToUIMessage::TextureSwapChain); } - } - } - - fn delete_packs(&mut self, to_delete: Vec) { - for uuid in to_delete { - self.packs.remove(&uuid); - } - } - pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { - let mut to_delete: Vec = Vec::new(); - for (uuid, pack) in self.packs.iter() { - if pack.name == data_pack.name { - to_delete.push(*uuid); + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageUI has not been implemented yet"); } } - self.delete_packs(to_delete); - self.tasks - .save_report(Arc::clone(&data_pack.dir), report, true); - self.tasks.save_data(&mut data_pack, true); - let mut uuid_to_insert = data_pack.uuid; - while self.packs.contains_key(&uuid_to_insert) { - //collision avoidance - trace!( - "Uuid collision detected for {} for package {}", - uuid_to_insert, - data_pack.name - ); - uuid_to_insert = Uuid::new_v4(); - } - data_pack.uuid = uuid_to_insert; - self.packs.insert(uuid_to_insert, data_pack); - uuid_to_insert } - pub fn load_all( - &mut self, - jokolay_dir: Arc, - b2u_sender: &std::sync::mpsc::Sender, - ) { - once::assert_has_not_been_called!("Early load must happen only once"); - // Called only once at application start. - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); - self.tasks.load_all_packs(jokolay_dir); - if let Ok((data_packages, texture_packages, report_packages)) = - self.tasks.wait_for_load_all_packs() - { - for (uuid, data_pack) in data_packages { - self.packs.insert(uuid, data_pack); + pub fn flush_all_messages(&mut self) -> PackageUISharedState { + if let Ok(mut import_status) = self.state.import_status.lock() { + if let ImportStatus::LoadingPack(file_path) = &mut *import_status { + let _ = self + .channel_sender + .send(MessageToPackageBack::ImportPack(file_path.clone()).into()); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); } - for ((_, texture_pack), (_, report)) in - std::iter::zip(texture_packages, report_packages) - { - let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); - } - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); } - let _ = b2u_sender.send(BackToUIMessage::FirstLoadDone); - } -} - -impl PackageUIManager { - pub fn new(packs: BTreeMap) -> Self { - Self { - packs, - tasks: PackTasks::new(), - reports: Default::default(), - default_marker_texture: None, - default_trail_texture: None, - - all_files_activation_status: false, - show_only_active: true, - currently_used_files: Default::default(), // UI copy to (de-)activate files - pack_details: None, + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + let msg = bincode::deserialize(&msg).unwrap(); + messages.push(msg); } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() } - pub fn late_init(&mut self, etx: &egui::Context) { + pub fn late_init(&mut self, egui_context: &egui::Context) { + //TODO: make it even later, at another place if self.default_marker_texture.is_none() { let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); let size = [img.width() as _, img.height() as _]; - self.default_marker_texture = Some(etx.load_texture( + self.default_marker_texture = Some(egui_context.load_texture( "default marker", ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), egui::TextureOptions { @@ -355,7 +214,7 @@ impl PackageUIManager { let img = image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); let size = [img.width() as _, img.height() as _]; - self.default_trail_texture = Some(etx.load_texture( + self.default_trail_texture = Some(egui_context.load_texture( "default trail", ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), egui::TextureOptions { @@ -405,8 +264,8 @@ impl PackageUIManager { pub fn load_marker_texture( &mut self, - egui_context: &egui::Context, pack_uuid: Uuid, + egui_context: &egui::Context, tex_path: RelativePath, marker_uuid: Uuid, position: Vec3, @@ -425,8 +284,8 @@ impl PackageUIManager { } pub fn load_trail_texture( &mut self, - egui_context: &egui::Context, pack_uuid: Uuid, + egui_context: &egui::Context, tex_path: RelativePath, trail_uuid: Uuid, common_attributes: CommonAttributes, @@ -465,28 +324,27 @@ impl PackageUIManager { } } - pub fn tick( - &mut self, - u2u_sender: &std::sync::mpsc::Sender, - timestamp: f64, - link: &MumbleLink, - z_near: f32, - ) { + pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { let tasks = &self.tasks; + let sender_u2u = self.sender_u2u.as_ref().unwrap(); for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); tasks.save_texture(pack, pack.is_dirty()); - pack.tick(u2u_sender, timestamp, link, z_near, tasks); - std::mem::drop(span_guard); } - let _ = u2u_sender.send(UIToUIMessage::RenderSwapChain); - //u2u_sender.send(UIToUIMessage::Present); + if link.changes.contains(MumbleChanges::Position) + || link.changes.contains(MumbleChanges::Map) + { + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + pack.tick(&sender_u2u, timestamp, link, z_near, tasks); + std::mem::drop(span_guard); + } + let _ = sender_u2u.send(UIToUIMessage::RenderSwapChain.into()); + } + Ok(()) } pub fn menu_ui( &mut self, - u2b_sender: &std::sync::mpsc::Sender, - u2u_sender: &std::sync::mpsc::Sender, ui: &mut egui::Ui, nb_running_tasks_on_back: i32, nb_running_tasks_on_network: i32, @@ -501,11 +359,15 @@ impl PackageUIManager { } if ui.button("Activate all elements").clicked() { self.category_set_all(true); - let _ = u2b_sender.send(UIToBackMessage::CategorySetAll(true)); + let _ = self + .channel_sender + .send(MessageToPackageBack::CategorySetAll(true).into()); } if ui.button("Deactivate all elements").clicked() { self.category_set_all(false); - let _ = u2b_sender.send(UIToBackMessage::CategorySetAll(false)); + let _ = self + .channel_sender + .send(MessageToPackageBack::CategorySetAll(false).into()); } for (pack, import_quality_report) in @@ -513,9 +375,10 @@ impl PackageUIManager { { //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + let u2u_sender = self.sender_u2u.as_ref().unwrap(); pack.category_sub_menu( - u2b_sender, - u2u_sender, + &self.channel_sender, + &u2u_sender, ui, self.show_only_active, import_quality_report, @@ -566,12 +429,7 @@ impl PackageUIManager { egui::Color32::from_rgb(color_ui, color_back, color_network) } - fn gui_file_manager( - &mut self, - event_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - open: &mut bool, - ) { + fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool) { let mut files_changed = false; Window::new("File Manager") .open(open) @@ -660,9 +518,9 @@ impl PackageUIManager { Ok(()) }); if files_changed { - let _ = event_sender.send(UIToBackMessage::ActiveFiles( - self.currently_used_files.clone(), - )); + let _ = self + .channel_sender + .send(MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()).into()); } } @@ -724,7 +582,6 @@ impl PackageUIManager { } fn gui_package_list( &mut self, - u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, import_status: &Arc>, open: &mut bool, @@ -751,7 +608,7 @@ impl PackageUIManager { ui.end_row(); } if !to_delete.is_empty() { - let _ = u2b_sender.send(UIToBackMessage::DeletePacks(to_delete)); + let _ = self.channel_sender.send(MessageToPackageBack::DeletePacks(to_delete).into()); } }); }); @@ -782,7 +639,7 @@ impl PackageUIManager { ui.text_edit_singleline(name); }); if ui.button("save").clicked() { - let _ = u2b_sender.send(UIToBackMessage::SavePack(name.clone(), pack.clone())); + let _ = self.channel_sender.send(MessageToPackageBack::SavePack(name.clone(), pack.clone()).into()); } } } @@ -805,21 +662,14 @@ impl PackageUIManager { } pub fn gui( &mut self, - u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, is_marker_open: &mut bool, import_status: &Arc>, is_file_open: &mut bool, first_load_done: bool, ) { - self.gui_package_list( - u2b_sender, - etx, - import_status, - is_marker_open, - first_load_done, - ); - self.gui_file_manager(u2b_sender, etx, is_file_open); + self.gui_package_list(etx, import_status, is_marker_open, first_load_done); + self.gui_file_manager(etx, is_file_open); } pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { @@ -839,3 +689,83 @@ impl PackageUIManager { self.reports.insert(report.uuid, report); } } + +//TODO: there is a need for a more complex input according to deps +impl JokolayUIComponent for PackageUIManager { + fn flush_all_messages(&mut self) -> PackageUISharedState { + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + let msg = bincode::deserialize(&msg).unwrap(); + messages.push(msg); + } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() + } + + fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> Option<&()> { + let raw_link = self + .receiver_mumblelink + .as_mut() + .unwrap() + .blocking_recv() + .unwrap(); + let link: &MumbleLink = &bincode::deserialize(&raw_link).unwrap(); + + for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in + std::mem::take(&mut self.delayed_marker_texture) + { + self.load_marker_texture( + pack_uuid, + egui_context, + tex_path, + marker_uuid, + position, + common_attributes, + ); + } + for (pack_uuid, tex_path, trail_uuid, common_attributes) in + std::mem::take(&mut self.delayed_trail_texture) + { + self.load_trail_texture( + pack_uuid, + egui_context, + tex_path, + trail_uuid, + common_attributes, + ); + } + + let raw_z_near = self + .receiver_near_scene + .as_mut() + .unwrap() + .blocking_recv() + .unwrap(); + let z_near: f32 = bincode::deserialize(&raw_z_near).unwrap(); + let _ = self._tick(timestamp, link, z_near); + None + } + fn bind( + &mut self, + mut deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + mut _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + mut notify: std::collections::HashMap< + u32, + tokio::sync::mpsc::Sender, + >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + self.sender_u2u = notify.remove(&0); + self.receiver_mumblelink = deps.remove(&0); + self.receiver_near_scene = deps.remove(&1); + unimplemented!("PackageUIManager component binding is not implemented") + } +} diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs index 066000e..dd3e117 100644 --- a/crates/joko_package/src/message.rs +++ b/crates/joko_package/src/message.rs @@ -1,27 +1,27 @@ use std::collections::{BTreeMap, HashSet}; +use joko_components::ComponentDataExchange; use joko_package_models::{ attributes::CommonAttributes, package::{PackCore, PackageImportReport}, }; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use glam::Vec3; - -use joko_core::RelativePath; +use joko_core::{serde_glam::Vec3, RelativePath}; use joko_render_models::{marker::MarkerObject, trail::TrailObject}; -use jokolink::MumbleLink; use crate::LoadedPackTexture; -pub enum BackToUIMessage { +#[derive(Serialize, Deserialize)] +pub enum MessageToPackageUI { ActiveElements(HashSet), //list of all elements that are loaded for current map CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display LoadedPack(LoadedPackTexture, PackageImportReport), //push a loaded pack to UI DeletedPacks(Vec), //push a deleted set of packs to UI FirstLoadDone, ImportedPack(String, PackCore), - ImportFailure(miette::Report), + ImportFailure(String), MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), //MumbleLink(Option), //MumbleLinkChanged,//tell there is a need to resize @@ -31,7 +31,14 @@ pub enum BackToUIMessage { TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), } -pub enum UIToBackMessage { +impl From for ComponentDataExchange { + fn from(src: MessageToPackageUI) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } +} + +#[derive(Serialize, Deserialize)] +pub enum MessageToPackageBack { ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. CategoryActivationBranchStatusChange(Uuid, bool), //same, for a whole branch @@ -39,19 +46,12 @@ pub enum UIToBackMessage { CategorySetAll(bool), //signal all categories should be now at this status DeletePacks(Vec), //uuid of the pack to delete ImportPack(std::path::PathBuf), - MumbleLinkBindedOnUI, - MumbleLinkAutonomous, - MumbleLink(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. ReloadPack, SavePack(String, PackCore), - SaveUIConfiguration(String), } -pub enum UIToUIMessage { - BulkMarkerObject(Vec), - BulkTrailObject(Vec), - //Present,// a render loop is finished and we can present it - MarkerObject(Box), - RenderSwapChain, // The list of elements to display was changed - TrailObject(Box), +impl From for ComponentDataExchange { + fn from(src: MessageToPackageBack) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } } diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index 627733f..b70119a 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -1,8 +1,9 @@ use std::str::FromStr; use enumflags2::{bitflags, BitFlags}; -use glam::Vec3; use itertools::Itertools; +use joko_core::serde_glam::Vec3; +use serde::{Deserialize, Serialize}; use tracing::info; use xot::{Element, NameId, Xot}; @@ -549,7 +550,7 @@ macro_rules! setters_for_bool_attributes { } common_attributes_struct_macro!( /// the struct we use for inheritance from category/other markers. - #[derive(Debug, Clone, Default)] + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CommonAttributes { /// An ID for an achievement from the GW2 API. Markers with the corresponding achievement ID will be hidden if the ID is marked as "done" for the API key that's entered in TacO. achievement_id: u32, @@ -708,7 +709,7 @@ impl CommonAttributes { Ok(f) => { if let Some(x) = array.get_mut(index) { *x = f; - self.rotate = array.into(); + self.rotate = Vec3(glam::Vec3::from_array(array.into())); self.active_attributes.insert(ActiveAttributes::rotate); } } @@ -825,7 +826,10 @@ impl CommonAttributes { if self.active_attributes.contains(ActiveAttributes::rotate) { ele.set_attribute( names.rotate, - format!("{},{},{}", self.rotate.x, self.rotate.y, self.rotate.z), + format!( + "{},{},{}", + self.rotate.0.x, self.rotate.0.y, self.rotate.0.z + ), ); } // spec vector @@ -1016,7 +1020,7 @@ pub enum ActiveAttributes { trail_scale = 1 << 56, trigger_range = 1 << 57, } -#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] pub enum Behavior { #[default] AlwaysVisible, @@ -1113,7 +1117,7 @@ impl ToString for Profession { self.as_ref().to_string() } } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub enum Cull { #[default] None, @@ -1194,7 +1198,7 @@ impl ToString for Festival { } } /// Filter for which specializations (the third traitline) will the marker be active for -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[repr(u8)] pub enum Specialization { Dueling = 0, diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 60d5911..086cb54 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -1,5 +1,6 @@ use crate::{attributes::CommonAttributes, package::PackageImportReport}; use ordered_hash_map::OrderedHashMap; +use serde::{Deserialize, Serialize}; use tracing::debug; use uuid::Uuid; @@ -16,7 +17,7 @@ pub struct RawCategory { pub sources: OrderedHashMap, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Category { pub guid: Uuid, pub parent: Option, diff --git a/crates/joko_package_models/src/map.rs b/crates/joko_package_models/src/map.rs index 83a7a7d..a35d361 100644 --- a/crates/joko_package_models/src/map.rs +++ b/crates/joko_package_models/src/map.rs @@ -2,9 +2,10 @@ use crate::marker::Marker; use crate::route::Route; use crate::trail::Trail; use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct MapData { pub markers: IndexMap, pub routes: IndexMap, diff --git a/crates/joko_package_models/src/marker.rs b/crates/joko_package_models/src/marker.rs index 9f5e068..5258442 100644 --- a/crates/joko_package_models/src/marker.rs +++ b/crates/joko_package_models/src/marker.rs @@ -1,8 +1,9 @@ use crate::attributes::CommonAttributes; -use glam::Vec3; +use joko_core::serde_glam::Vec3; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Marker { pub guid: Uuid, pub parent: Uuid, diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 1d49e99..45c6a6d 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -149,7 +149,7 @@ pub struct PackageImportReport { source_files: bimap::BiMap, //map of all files to uuid. When exporting this shall have to be reversed. } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PackCore { /* PackCore is a temporary holder of data @@ -348,7 +348,7 @@ impl PackCore { &mut self, full_category_name: &String, uuid: &Uuid, - ) -> Result { + ) -> Result { if let Some(parent_uuid) = self.all_categories.get(full_category_name) { let mut uuid_to_insert = *uuid; while self.entities_parents.contains_key(&uuid_to_insert) { @@ -364,10 +364,10 @@ impl PackCore { Ok(uuid_to_insert) } else { //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. - Err(miette::Error::msg(format!( + Err(format!( "Can't register world entity {} {}, no associated category found.", full_category_name, uuid - ))) + )) } } @@ -375,7 +375,7 @@ impl PackCore { &mut self, full_category_name: String, mut marker: Marker, - ) -> Result<(), miette::Error> { + ) -> Result<(), String> { let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; marker.guid = uuid_to_insert; if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(marker.map_id) { @@ -395,7 +395,7 @@ impl PackCore { &mut self, full_category_name: String, mut trail: Trail, - ) -> Result<(), miette::Error> { + ) -> Result<(), String> { let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; trail.guid = uuid_to_insert; if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(trail.map_id) { @@ -411,7 +411,7 @@ impl PackCore { Ok(()) } - pub fn register_route(&mut self, mut route: Route) -> Result<(), miette::Error> { + pub fn register_route(&mut self, mut route: Route) -> Result<(), String> { let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); let tbin_path: RelativePath = file_name.parse().unwrap(); let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs index e9a2db2..f5e825a 100644 --- a/crates/joko_package_models/src/route.rs +++ b/crates/joko_package_models/src/route.rs @@ -1,5 +1,5 @@ -use glam::Vec3; -use joko_core::RelativePath; +use joko_core::{serde_glam::Vec3, RelativePath}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ @@ -7,7 +7,7 @@ use crate::{ trail::{TBin, Trail}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Route { pub category: String, pub parent: Uuid, diff --git a/crates/joko_package_models/src/trail.rs b/crates/joko_package_models/src/trail.rs index 6669463..33e8398 100644 --- a/crates/joko_package_models/src/trail.rs +++ b/crates/joko_package_models/src/trail.rs @@ -1,8 +1,10 @@ +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::attributes::CommonAttributes; +use joko_core::serde_glam::*; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Trail { pub guid: Uuid, pub parent: Uuid, @@ -13,13 +15,13 @@ pub struct Trail { pub source_file_uuid: Uuid, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TBin { pub map_id: u32, pub version: u32, - pub nodes: Vec, + pub nodes: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TBinStatus { pub tbin: TBin, pub iso_x: bool, diff --git a/crates/joko_plugins/Cargo.toml b/crates/joko_plugins/Cargo.toml new file mode 100644 index 0000000..98b1e9d --- /dev/null +++ b/crates/joko_plugins/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "joko_plugins" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +joko_components = { path = "../joko_components" } +scopeguard = "1.2.0" +smol_str = { workspace = true } +tokio = { workspace = true } diff --git a/crates/joko_plugins/src/lib.rs b/crates/joko_plugins/src/lib.rs new file mode 100644 index 0000000..b4c801c --- /dev/null +++ b/crates/joko_plugins/src/lib.rs @@ -0,0 +1,29 @@ +use joko_components::{ + ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, +}; + +pub struct JokolayPlugin {} + +pub struct JokolayPluginManager {} + +impl JokolayComponent<(), ()> for JokolayPlugin { + fn flush_all_messages(&mut self) -> () {} + fn tick(&mut self, timestamp: f64) -> Option<&()> { + None + } + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + } +} +impl JokolayComponentDeps for JokolayPlugin {} diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml index e0b3e34..40a2af4 100644 --- a/crates/joko_render/Cargo.toml +++ b/crates/joko_render/Cargo.toml @@ -8,14 +8,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bincode = { workspace = true } bytemuck = { workspace = true } glam = { workspace = true, features = ["bytemuck"] } tracing = { workspace = true } egui = { workspace = true } egui_render_three_d = { version = "*" } egui_window_glfw_passthrough = { version = "0.8" } +tokio = { workspace = true } -jokolink = { path = "../jokolink" } +joko_components = { path = "../joko_components" } +joko_link = { path = "../joko_link" } joko_render_models = { path = "../joko_render_models" } diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs index 4b5a576..9c49027 100644 --- a/crates/joko_render/src/renderer.rs +++ b/crates/joko_render/src/renderer.rs @@ -12,8 +12,11 @@ use egui_render_three_d::ThreeDBackend; use egui_render_three_d::ThreeDConfig; use egui_window_glfw_passthrough::GlfwBackend; use glam::Mat4; -use jokolink::MumbleLink; -use jokolink::UIState; +use joko_components::JokolayComponent; +use joko_components::JokolayComponentDeps; +use joko_render_models::messages::UIToUIMessage; +use joko_link::MumbleLink; +use joko_link::UIState; use three_d::prelude::*; use joko_render_models::{marker::MarkerObject, trail::TrailObject}; @@ -27,6 +30,7 @@ pub struct JokoRenderer { pub is_map_open: bool, pub billboard_renderer: BillBoardRenderer, pub gl: egui_render_three_d::ThreeDBackend, + channel_receiver: Option>, } impl JokoRenderer { @@ -67,6 +71,7 @@ impl JokoRenderer { gl: backend, billboard_renderer, cam_pos: Default::default(), + channel_receiver: None, } } @@ -130,7 +135,146 @@ impl JokoRenderer { ]; } */ - pub fn tick(&mut self, link: Option<&MumbleLink>) { + fn handle_u2u_message(&mut self, msg: UIToUIMessage) { + match msg { + UIToUIMessage::BulkMarkerObject(marker_objects) => { + tracing::debug!( + "Handling of UIToUIMessage::BulkMarkerObject {}", + marker_objects.len() + ); + self.extend_markers(marker_objects); + } + UIToUIMessage::BulkTrailObject(trail_objects) => { + tracing::debug!( + "Handling of UIToUIMessage::BulkTrailObject {}", + trail_objects.len() + ); + self.extend_trails(trail_objects); + } + UIToUIMessage::MarkerObject(mo) => { + tracing::trace!("Handling of UIToUIMessage::MarkerObject"); + self.add_billboard(*mo); + } + UIToUIMessage::TrailObject(to) => { + tracing::trace!("Handling of UIToUIMessage::TrailObject"); + self.add_trail(*to); + } + UIToUIMessage::RenderSwapChain => { + tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); + self.swap(); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling UIToUIMessage has not been implemented yet"); + } + } + } + + pub fn extend_markers(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip.extend(marker_objects); + } + pub fn add_billboard(&mut self, marker_object: MarkerObject) { + self.billboard_renderer.markers_wip.push(marker_object); + } + + pub fn extend_trails(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip.extend(trail_objects); + } + pub fn add_trail(&mut self, trail_object: TrailObject) { + self.billboard_renderer.trails_wip.push(trail_object); + } + + pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { + self.gl.prepare_frame(latest_framebuffer_size_getter); + unsafe { + let gl = self.gl.context.clone(); + gl_error!(gl); + // self.gl.context.set_viewport(self.viewport); + self.gl.context.set_scissor(ScissorBox::new_at_origo( + self.viewport.width, + self.viewport.height, + )); + self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); + self.gl + .context + .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); + gl_error!(gl); + } + } + + pub fn render_egui( + &mut self, + meshes: Vec, + textures_delta: egui::TexturesDelta, + logical_screen_size: [f32; 2], + latest_time: f64, + ) { + if self.has_link && !self.is_map_open { + self.billboard_renderer + .prepare_render_data(&self.gl.context); + self.billboard_renderer.render( + &self.gl.context, + self.cam_pos, + &self.view_proj, + &self.gl.glow_backend.painter.managed_textures, + latest_time, + ); + } + self.gl + .render_egui(meshes, textures_delta, logical_screen_size); + } + + pub fn present(&mut self) {} + + pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { + tracing::info!(?latest_size, "resizing framebuffer"); + + self.viewport = Viewport { + x: 0, + y: 0, + width: latest_size[0], + height: latest_size[1], + }; + self.gl.resize_framebuffer(latest_size); + } +} + +impl JokolayComponentDeps for JokoRenderer {} +impl JokolayComponent<(), ()> for JokoRenderer { + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap< + u32, + tokio::sync::mpsc::Sender, + >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + self.channel_receiver = input_notification.remove(&0); + } + fn flush_all_messages(&mut self) -> () { + let channel_receiver = self.channel_receiver.as_mut().unwrap(); + + //two steps reading due to self mutability required by channel + let mut messages = Vec::new(); + while let Ok(msg) = channel_receiver.try_recv() { + let msg: UIToUIMessage = bincode::deserialize(&msg).unwrap(); + messages.push(msg); + } + for msg in messages { + self.handle_u2u_message(msg); + } + () + } + fn tick(&mut self, _latest_time: f64) -> Option<&()> { + let link: Option<&MumbleLink> = None; if let Some(link) = link { //x positive => east //y positive => ascention @@ -142,7 +286,7 @@ impl JokoRenderer { }; //TODO: change perspective is map is open - let center = link.cam_pos + link.f_camera_front; + let center = link.cam_pos.0 + link.f_camera_front.0; let cam_pos = link.cam_pos; /* let map_pos_x = (link.player_x - link.map_center_x) / 1.64; @@ -172,7 +316,7 @@ impl JokoRenderer { };*/ let camera = Camera::new_perspective( self.viewport, - cam_pos.to_array().into(), + cam_pos.0.to_array().into(), center.to_array().into(), Vector3::unit_y(), Rad(link.fov), @@ -195,7 +339,7 @@ impl JokoRenderer { println!("player: {} {} {}", link.player_pos.x, link.player_pos.y, link.player_pos.z); */ - let view = Mat4::look_at_lh(cam_pos, center, glam::Vec3::Y); + let view = Mat4::look_at_lh(cam_pos.0, center, glam::Vec3::Y); let proj = Mat4::perspective_lh( link.fov, self.viewport.aspect(), @@ -203,77 +347,11 @@ impl JokoRenderer { Self::get_z_far(), ); self.view_proj = proj * view; - self.cam_pos = cam_pos; + self.cam_pos = cam_pos.0; self.has_link = true; } else { self.has_link = false; } - } - pub fn extend_markers(&mut self, marker_objects: Vec) { - self.billboard_renderer.markers_wip.extend(marker_objects); - } - pub fn add_billboard(&mut self, marker_object: MarkerObject) { - self.billboard_renderer.markers_wip.push(marker_object); - } - - pub fn extend_trails(&mut self, trail_objects: Vec) { - self.billboard_renderer.trails_wip.extend(trail_objects); - } - pub fn add_trail(&mut self, trail_object: TrailObject) { - self.billboard_renderer.trails_wip.push(trail_object); - } - - pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - self.gl.prepare_frame(latest_framebuffer_size_getter); - unsafe { - let gl = self.gl.context.clone(); - gl_error!(gl); - // self.gl.context.set_viewport(self.viewport); - self.gl.context.set_scissor(ScissorBox::new_at_origo( - self.viewport.width, - self.viewport.height, - )); - self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); - self.gl - .context - .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); - gl_error!(gl); - } - } - - pub fn render_egui( - &mut self, - meshes: Vec, - textures_delta: egui::TexturesDelta, - logical_screen_size: [f32; 2], - latest_time: f64, - ) { - if self.has_link && !self.is_map_open { - self.billboard_renderer - .prepare_render_data(&self.gl.context); - self.billboard_renderer.render( - &self.gl.context, - self.cam_pos, - &self.view_proj, - &self.gl.glow_backend.painter.managed_textures, - latest_time, - ); - } - self.gl - .render_egui(meshes, textures_delta, logical_screen_size); - } - - pub fn present(&mut self) {} - - pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { - tracing::info!(?latest_size, "resizing framebuffer"); - - self.viewport = Viewport { - x: 0, - y: 0, - width: latest_size[0], - height: latest_size[1], - }; - self.gl.resize_framebuffer(latest_size); + None } } diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml index 3f95c4f..761b3ae 100644 --- a/crates/joko_render_models/Cargo.toml +++ b/crates/joko_render_models/Cargo.toml @@ -8,6 +8,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bincode = { workspace = true } bytemuck = { workspace = true } glam = { workspace = true, features = ["bytemuck"] } +serde = { workspace = true } + +joko_core = { path = "../joko_core" } +joko_components = { path = "../joko_components" } diff --git a/crates/joko_render_models/src/lib.rs b/crates/joko_render_models/src/lib.rs index 5fa3de1..fc275d3 100644 --- a/crates/joko_render_models/src/lib.rs +++ b/crates/joko_render_models/src/lib.rs @@ -1,2 +1,3 @@ pub mod marker; +pub mod messages; pub mod trail; diff --git a/crates/joko_render_models/src/marker.rs b/crates/joko_render_models/src/marker.rs index 39aa922..7fae322 100644 --- a/crates/joko_render_models/src/marker.rs +++ b/crates/joko_render_models/src/marker.rs @@ -1,7 +1,9 @@ -use glam::{Vec2, Vec3}; +use serde::{Deserialize, Serialize}; + +use joko_core::serde_glam::*; #[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize)] pub struct MarkerVertex { pub position: Vec3, pub alpha: f32, @@ -10,7 +12,7 @@ pub struct MarkerVertex { pub color: [u8; 4], } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct MarkerObject { /// The six vertices that make up the marker quad pub vertices: [MarkerVertex; 6], diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs new file mode 100644 index 0000000..c3058b2 --- /dev/null +++ b/crates/joko_render_models/src/messages.rs @@ -0,0 +1,20 @@ +use joko_components::ComponentDataExchange; +use serde::{Deserialize, Serialize}; + +use crate::{marker::MarkerObject, trail::TrailObject}; + +#[derive(Serialize, Deserialize)] +pub enum UIToUIMessage { + BulkMarkerObject(Vec), + BulkTrailObject(Vec), + //Present,// a render loop is finished and we can present it + MarkerObject(Box), + RenderSwapChain, // The list of elements to display was changed + TrailObject(Box), +} + +impl From for ComponentDataExchange { + fn from(src: UIToUIMessage) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } +} diff --git a/crates/joko_render_models/src/trail.rs b/crates/joko_render_models/src/trail.rs index a80da51..5ae46e3 100644 --- a/crates/joko_render_models/src/trail.rs +++ b/crates/joko_render_models/src/trail.rs @@ -1,8 +1,10 @@ use std::sync::Arc; +use serde::{Deserialize, Serialize}; + use crate::marker::MarkerVertex; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrailObject { pub vertices: Arc<[MarkerVertex]>, pub texture: u64, diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index a3e22e7..4c669a6 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -14,10 +14,12 @@ wayland = ["egui_window_glfw_passthrough/wayland"] [dependencies] enumflags2 = { workspace = true } -#joko_core = { path = "../joko_core" } +joko_core = { path = "../joko_core" } +joko_components = { path = "../joko_components" } +joko_plugins = { path = "../joko_plugins" } joko_render = { path = "../joko_render" } jmf = { path = "../joko_package", package = "joko_package" } -jokolink = { path = "../jokolink" } +joko_link = { path = "../joko_link" } egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users # and `Dir` from cap-dirs doesn't allow us to get the path. @@ -28,6 +30,7 @@ tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } miette = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true } indexmap = { workspace = true } ringbuffer = { workspace = true } diff --git a/crates/jokolay/src/app/messages.rs b/crates/jokolay/src/app/messages.rs new file mode 100644 index 0000000..5d14d68 --- /dev/null +++ b/crates/jokolay/src/app/messages.rs @@ -0,0 +1,3 @@ +pub enum MessageToApplicationBack { + SaveUIConfiguration(String), +} diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 96f23be..21f4c76 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeMap, + collections::HashMap, io::Write, ops::DerefMut, sync::{Arc, Mutex}, @@ -9,39 +9,35 @@ use std::{ use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; mod init; +mod messages; mod mumble; mod ui_parameters; use init::{get_jokolay_dir, get_jokolay_path}; -use jmf::{ - message::{UIToBackMessage, UIToUIMessage}, - PackageDataManager, PackageUIManager, +use jmf::{PackageDataManager, PackageUIManager}; +use joko_components::{ + ComponentDataExchange, ComponentManager, JokolayComponent, JokolayUIComponent, }; -use uuid::Uuid; -//use jmf::FileManager; use crate::app::mumble::mumble_gui; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; -use jmf::message::BackToUIMessage; -use jmf::{ - build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, LoadedPackData, - LoadedPackTexture, -}; -use jmf::{import_pack_from_zip_file_path, ImportStatus}; +use jmf::jokolay_to_editable_path; +use jmf::ImportStatus; use joko_render::renderer::JokoRenderer; -use jokolink::{MumbleChanges, MumbleLink, MumbleManager}; +use joko_link::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleManager}; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span}; +use self::messages::MessageToApplicationBack; + const MINIMAL_WINDOW_WIDTH: u32 = 640; const MINIMAL_WINDOW_HEIGHT: u32 = 480; const MINIMAL_WINDOW_POSITION_X: i32 = 0; const MINIMAL_WINDOW_POSITION_Y: i32 = 0; -struct JokolayUIState { +pub struct JokolayUIState { link: Option, editable_mumble: bool, window_changed: bool, - list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display first_load_done: bool, nb_running_tasks_on_back: i32, // store the number of running tasks in background thread nb_running_tasks_on_network: i32, // store the number of running tasks (requests) in progress @@ -51,15 +47,6 @@ struct JokolayUIState { root_path: std::path::PathBuf, } -struct JokolayBackState { - choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI - read_ui_link: bool, - copy_of_ui_link: Option, - root_dir: Arc, - #[allow(dead_code)] - editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration - extract_path: std::path::PathBuf, -} struct JokolayApp { mumble_manager: MumbleManager, package_manager: PackageDataManager, @@ -79,7 +66,6 @@ pub struct Jokolay { gui: Box, app: Arc>>, state_ui: JokolayUIState, - state_back: JokolayBackState, } impl Jokolay { @@ -90,15 +76,64 @@ impl Jokolay { It happens anyway when the UI start the edit mode of the mumble link. */ + let mut component_manager = ComponentManager::new(); + let mumble_data_manager = - MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; + MumbleManager::new("MumbleLink", false).wrap_err("failed to create mumble manager")?; let mumble_ui_manager = - MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; + MumbleManager::new("MumbleLink", true).wrap_err("failed to create mumble manager")?; + + let dummy_plugin = Box::new(joko_plugins::JokolayPlugin {}); + component_manager.register("dummy_plugin", dummy_plugin); + component_manager.register( + "mumble_link_ui", + Box::new( + MumbleManager::new("MumbleLink", true) + .wrap_err("failed to create mumble manager")?, + ), + ); + component_manager.register( + "mumble_link_back", + Box::new( + MumbleManager::new("MumbleLink", false) + .wrap_err("failed to create mumble manager")?, + ), + ); + + match component_manager.build_routes() { + Ok(_) => {} + Err(e) => { + error!(?e, "Could not build component routes"); + } + } + + let (b2u_sender, b2u_receiver) = tokio::sync::mpsc::channel(10); + let (u2b_sender, u2b_receiver) = tokio::sync::mpsc::channel(10); + /* + components can be migrated to plugins + root_path/ + ui.toml + components/ + mumble_link/ + ... + theme_manager/ + ... + package_ui/ + ... + package_data/ + ... + plugins/ + plugin1 + plugin2 + ... + */ - let data_packages: BTreeMap = Default::default(); - let texture_packages: BTreeMap = Default::default(); - let package_data_manager = PackageDataManager::new(data_packages, Arc::clone(&root_dir))?; - let mut package_ui_manager = PackageUIManager::new(texture_packages); + let package_data_manager = PackageDataManager::new( + Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay + &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay + u2b_receiver, // to be removed since dynamically inserted on tick (once Plugin is implemented) + b2u_sender, //TODO: list of bounded & notify + )?; let mut theme_manager = ThemeManager::new(Arc::clone(&root_dir)).wrap_err("failed to create theme manager")?; @@ -130,12 +165,14 @@ impl Jokolay { None } }); + let maximal_window_width = video_mode.unwrap().width; + let maximal_window_height = video_mode.unwrap().height; + let mut package_ui_manager = PackageUIManager::new(b2u_receiver, u2b_sender); glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); - //TODO: load configuration from disk (ui.toml) let editable_path = jokolay_to_editable_path(&root_path) .to_str() .unwrap() @@ -161,41 +198,31 @@ impl Jokolay { //let gui = Mutex::new(gui); //let gui = Arc::new(gui); let gui = Box::new(gui); + let state_ui = JokolayUIState { + link: Some(MumbleLink::default()), + editable_mumble: false, + window_changed: true, + first_load_done: false, + nb_running_tasks_on_back: 0, + nb_running_tasks_on_network: 0, + import_status: Default::default(), + maximal_window_width, //TODO: what happens if change of screen ? + maximal_window_height, + root_path, + }; Ok(Self { gui, app: Arc::new(Mutex::new(Box::new(JokolayApp { mumble_manager: mumble_data_manager, package_manager: package_data_manager, }))), - state_ui: JokolayUIState { - link: Some(MumbleLink::default()), - editable_mumble: false, - window_changed: true, - list_of_textures_changed: false, - first_load_done: false, - nb_running_tasks_on_back: 0, - nb_running_tasks_on_network: 0, - import_status: Default::default(), - maximal_window_width: video_mode.unwrap().width, //TODO: what happens if change of screen ? - maximal_window_height: video_mode.unwrap().height, - root_path: root_path.clone(), - }, - state_back: JokolayBackState { - choice_of_category_changed: false, - read_ui_link: false, - copy_of_ui_link: Default::default(), - root_dir, - editable_path: std::path::PathBuf::from(editable_path), - extract_path: jokolay_to_extract_path(&root_path), - }, + state_ui, }) } fn start_background_loop( app: Arc>>, - state: JokolayBackState, - b2u_sender: std::sync::mpsc::Sender, - u2b_receiver: std::sync::mpsc::Receiver, + u2gb_receiver: std::sync::mpsc::Receiver, ) { let _background_thread = std::thread::spawn(move || { // Load the directory with packages in the background process @@ -206,142 +233,16 @@ impl Jokolay { mumble_manager: _, package_manager, } = &mut app.deref_mut().as_mut(); - package_manager.load_all(Arc::clone(&state.root_dir), &b2u_sender); + package_manager.load_all(); } - Self::background_loop(Arc::clone(&app), state, b2u_sender, u2b_receiver); + let _ = Self::background_loop(Arc::clone(&app), u2gb_receiver); }); } - fn handle_u2b_message( - package_manager: &mut PackageDataManager, - local_state: &mut JokolayBackState, - b2u_sender: &std::sync::mpsc::Sender, - msg: UIToBackMessage, - ) { + fn handle_app_message(root_dir: Arc, msg: MessageToApplicationBack) { match msg { - UIToBackMessage::ActiveFiles(currently_used_files) => { - tracing::trace!("Handling of UIToBackMessage::ActiveFiles"); - package_manager.set_currently_used_files(currently_used_files); - local_state.choice_of_category_changed = true; - } - UIToBackMessage::CategoryActivationElementStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of UIToBackMessage::CategoryActivationElementStatusChange" - ); - package_manager.category_set(category_uuid, status); - } - UIToBackMessage::CategoryActivationBranchStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of UIToBackMessage::CategoryActivationBranchStatusChange" - ); - package_manager.category_branch_set(category_uuid, status); - } - UIToBackMessage::CategoryActivationStatusChanged => { - tracing::trace!("Handling of UIToBackMessage::CategoryActivationStatusChanged"); - local_state.choice_of_category_changed = true; - } - UIToBackMessage::CategorySetAll(status) => { - tracing::trace!("Handling of UIToBackMessage::CategorySetAll"); - package_manager.category_set_all(status); - local_state.choice_of_category_changed = true; - } - UIToBackMessage::DeletePacks(to_delete) => { - tracing::trace!("Handling of UIToBackMessage::DeletePacks"); - let mut deleted = Vec::new(); - for pack_uuid in to_delete { - if let Some(pack) = package_manager.packs.remove(&pack_uuid) { - if let Err(e) = package_manager.marker_packs_dir.remove_dir_all(&pack.name) - { - error!(?e, pack.name, "failed to remove pack"); - } else { - info!("deleted marker pack: {}", pack.name); - deleted.push(pack_uuid); - } - } - } - let _ = b2u_sender.send(BackToUIMessage::DeletedPacks(deleted)); - } - UIToBackMessage::ImportPack(file_path) => { - tracing::trace!("Handling of UIToBackMessage::ImportPack"); - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(1)); - let start = std::time::SystemTime::now(); - let result = import_pack_from_zip_file_path(file_path, &local_state.extract_path); - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of taco package from disk took {} ms", - elaspsed.as_millis() - ); - match result { - Ok((file_name, pack)) => { - let _ = b2u_sender.send(BackToUIMessage::ImportedPack(file_name, pack)); - } - Err(e) => { - let _ = b2u_sender.send(BackToUIMessage::ImportFailure(e)); - } - } - let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(0)); - } - UIToBackMessage::MumbleLinkAutonomous => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); - local_state.read_ui_link = false; - } - UIToBackMessage::MumbleLinkBindedOnUI => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); - local_state.read_ui_link = true; - } - UIToBackMessage::MumbleLink(link) => { - tracing::trace!("Handling of UIToBackMessage::MumbleLink"); - local_state.copy_of_ui_link = link; - } - UIToBackMessage::ReloadPack => { - unimplemented!( - "Handling of UIToBackMessage::ReloadPack has not been implemented yet" - ); - } - UIToBackMessage::SavePack(name, pack) => { - tracing::trace!("Handling of UIToBackMessage::SavePack"); - let name = name.as_str(); - if package_manager.marker_packs_dir.exists(name) { - match package_manager - .marker_packs_dir - .remove_dir_all(name) - .into_diagnostic() - { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to delete already existing marker pack"); - } - } - } - if let Err(e) = package_manager.marker_packs_dir.create_dir_all(name) { - error!(?e, "failed to create directory for pack"); - } - match package_manager.marker_packs_dir.open_dir(name) { - Ok(dir) => { - let (data_pack, mut texture_pack, mut report) = - build_from_core(name.to_string(), dir.into(), pack); - tracing::trace!("Package loaded into data and texture"); - let uuid_of_insertion = package_manager.save(data_pack, report.clone()); - report.uuid = uuid_of_insertion; - texture_pack.uuid = uuid_of_insertion; - let _ = b2u_sender.send(BackToUIMessage::LoadedPack(texture_pack, report)); - } - Err(e) => { - error!( - ?e, - "failed to open marker pack directory to save pack {:?} {}", - package_manager.marker_packs_dir, - name - ); - } - }; - } - UIToBackMessage::SaveUIConfiguration(serialized_string) => { - //let _ = b2u_sender.send(BackToUIMessage::NbTasksRunning(package_manager.tasks.count()+ 1)); //TODO: send update on screen - match local_state - .root_dir - .create(ui_parameters::UI_PARAMETERS_FILE_NAME) - { + MessageToApplicationBack::SaveUIConfiguration(serialized_string) => { + match root_dir.create(ui_parameters::UI_PARAMETERS_FILE_NAME) { Ok(mut file) => { match file.write(serialized_string.as_bytes()).into_diagnostic() { Ok(_) => {} @@ -361,50 +262,42 @@ impl Jokolay { } } } + fn background_loop( app: Arc>>, - mut local_state: JokolayBackState, - b2u_sender: std::sync::mpsc::Sender, - u2b_receiver: std::sync::mpsc::Receiver, - ) { + u2gb_receiver: std::sync::mpsc::Receiver, + ) -> Result<()> { tracing::info!("entering background event loop"); let _span_guard = info_span!("background event loop").entered(); let mut loop_index: u128 = 0; - let mut nb_messages: u128 = 0; + let start = std::time::SystemTime::now(); loop { - tracing::trace!("background loop tick: {} {}", loop_index, nb_messages); + tracing::trace!("background loop tick: {}", loop_index); let mut app = app.lock().unwrap(); let JokolayApp { mumble_manager, package_manager, } = &mut app.deref_mut().as_mut(); - while let Ok(msg) = u2b_receiver.try_recv() { - Self::handle_u2b_message(package_manager, &mut local_state, &b2u_sender, msg); - nb_messages += 1; + /* + TODO: for each plugin, run it from the ones without any dep to those that require those values => depgraph of plugins + + back-end deps: + package_manager -requires-> link + + front-end deps: + render -requires-> package_manager + package_manager -requires-> link + */ + while let Ok(msg) = u2gb_receiver.try_recv() { + Self::handle_app_message(Arc::clone(&package_manager.state.root_dir), msg); } - let link = if local_state.read_ui_link { - local_state.copy_of_ui_link.as_ref() - } else { - match mumble_manager.tick() { - Ok(ml) => ml, - Err(e) => { - error!(?e, "mumble manager tick error"); - None - } - } - }; - tracing::trace!( - "choice_of_category_changed: {}", - local_state.choice_of_category_changed - ); - package_manager.tick( - &b2u_sender, - loop_index, - link, - local_state.choice_of_category_changed, - ); - local_state.choice_of_category_changed = false; + + let ms = mumble_manager.flush_all_messages(); + + let link = mumble_manager.tick(start.elapsed().into_diagnostic()?.as_secs_f64()); + package_manager.flush_all_messages(); + package_manager.tick(loop_index, &ms, link); thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; @@ -416,146 +309,62 @@ impl Jokolay { } } - fn handle_u2u_message(gui: &mut JokolayGui, msg: UIToUIMessage) { - match msg { - UIToUIMessage::BulkMarkerObject(marker_objects) => { - tracing::debug!( - "Handling of UIToUIMessage::BulkMarkerObject {}", - marker_objects.len() - ); - gui.joko_renderer.extend_markers(marker_objects); - } - UIToUIMessage::BulkTrailObject(trail_objects) => { - tracing::debug!( - "Handling of UIToUIMessage::BulkTrailObject {}", - trail_objects.len() - ); - gui.joko_renderer.extend_trails(trail_objects); - } - UIToUIMessage::MarkerObject(mo) => { - tracing::trace!("Handling of UIToUIMessage::MarkerObject"); - gui.joko_renderer.add_billboard(*mo); - } - UIToUIMessage::TrailObject(to) => { - tracing::trace!("Handling of UIToUIMessage::TrailObject"); - gui.joko_renderer.add_trail(*to); - } - UIToUIMessage::RenderSwapChain => { - tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); - gui.joko_renderer.swap(); - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling UIToUIMessage has not been implemented yet"); - } - } - } - fn handle_b2u_message( - gui: &mut JokolayGui, - local_state: &mut JokolayUIState, - u2b_sender: &std::sync::mpsc::Sender, - msg: BackToUIMessage, - ) { - match msg { - BackToUIMessage::ActiveElements(active_elements) => { - tracing::trace!("Handling of BackToUIMessage::ActiveElements"); - gui.package_manager - .update_active_categories(&active_elements); - } - BackToUIMessage::CurrentlyUsedFiles(currently_used_files) => { - tracing::trace!("Handling of BackToUIMessage::CurrentlyUsedFiles"); - gui.package_manager - .set_currently_used_files(currently_used_files); - } - BackToUIMessage::DeletedPacks(to_delete) => { - tracing::trace!("Handling of BackToUIMessage::DeletedPacks"); - gui.package_manager.delete_packs(to_delete); - } - BackToUIMessage::FirstLoadDone => { - local_state.first_load_done = true; - } - BackToUIMessage::ImportedPack(file_name, pack) => { - tracing::trace!("Handling of BackToUIMessage::ImportedPack"); - *local_state.import_status.lock().unwrap() = - ImportStatus::PackDone(file_name, pack, false); - } - BackToUIMessage::ImportFailure(error) => { - tracing::trace!("Handling of BackToUIMessage::ImportFailure"); - *local_state.import_status.lock().unwrap() = ImportStatus::PackError(error); - } - BackToUIMessage::LoadedPack(pack_texture, report) => { - tracing::trace!("Handling of BackToUIMessage::LoadedPack"); - gui.package_manager.save(pack_texture, report); - local_state.import_status = Default::default(); - let _ = u2b_sender.send(UIToBackMessage::CategoryActivationStatusChanged); - } - BackToUIMessage::MarkerTexture( - pack_uuid, - tex_path, - marker_uuid, - position, - common_attributes, - ) => { - tracing::trace!("Handling of BackToUIMessage::MarkerTexture"); - gui.package_manager.load_marker_texture( - &gui.egui_context, - pack_uuid, - tex_path, - marker_uuid, - position, - common_attributes, - ); - } - BackToUIMessage::NbTasksRunning(nb_tasks) => { - tracing::trace!("Handling of BackToUIMessage::NbTasksRunning"); - local_state.nb_running_tasks_on_back = nb_tasks; - } - BackToUIMessage::PackageActiveElements(pack_uuid, active_elements) => { - tracing::trace!("Handling of BackToUIMessage::PackageActiveElements"); - gui.package_manager - .update_pack_active_categories(pack_uuid, &active_elements); - } - BackToUIMessage::TextureSwapChain => { - tracing::debug!("Handling of BackToUIMessage::TextureSwapChain"); - gui.package_manager.swap(); - local_state.list_of_textures_changed = true; - } - BackToUIMessage::TrailTexture(pack_uuid, tex_path, trail_uuid, common_attributes) => { - tracing::trace!("Handling of BackToUIMessage::TrailTexture"); - gui.package_manager.load_trail_texture( - &gui.egui_context, - pack_uuid, - tex_path, - trail_uuid, - common_attributes, - ); - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling BackToUIMessage has not been implemented yet"); - } - } - } - pub fn enter_event_loop(self) { - let (b2u_sender, b2u_receiver) = std::sync::mpsc::channel(); - let (u2b_sender, u2b_receiver) = std::sync::mpsc::channel(); - let (u2u_sender, u2u_receiver) = std::sync::mpsc::channel(); - Self::start_background_loop( - Arc::clone(&self.app), - self.state_back, - b2u_sender, - u2b_receiver, - ); + //TODO: all .tick() functions should have the same interface + /* + TODO: proper routing of a package to another + when loading a plugin, there is a relationship defined with another: either "requires" or "bind" or "notify" + - In case of "bind" the other plugin has to agree with it. + - In case of "requires" then the output of both "flush_all_messages" and "tick" of said requirement shall be passed to the plugin. + - In case of "notify" then a channel to send message is open. + => no loop when registering + => check for missing dep + => when a value is pushed it should be a broadcast (an immutable ref for each consumer), trashed at end of the loop. + => https://docs.rs/tokio/latest/tokio/sync/broadcast/ + channels for notifications should carry the source. One cannot trust the source since they are third part. Or accept to not know the source. Hence in the contract, can be ignored. + in a flush_all_messages, input notification must be drained + Name of the plugin defines the feature/service it provides. If a replacement is wished, one need to overwrite the plugin with another provider. + + Once validated all works properly with the existing code, we can create a PluginManager and PluginInstance, with each instance of the later being a rust wrapper around some plugin definition. + It'll act as the interface between our code and plugin world. It is basically an overhead which could be optimized later. + */ + + let (u2gb_sender, u2gb_receiver) = std::sync::mpsc::channel(); + let (u2mb_sender, u2mb_receiver) = std::sync::mpsc::channel(); //FIXME: route the data to the consumers. + let (u2u_sender, u2u_receiver) = tokio::sync::mpsc::channel(1); + + Self::start_background_loop(Arc::clone(&self.app), u2gb_receiver); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); - let mut local_state = self.state_ui; let mut nb_frames: u128 = 0; let mut nb_messages: u128 = 0; let max_nb_messages_per_loop: u128 = 100; - //u2u_sender.send(UIToUIMessage::Present);// force a first drawing let mut gui = *self.gui; + let mut local_state = self.state_ui; + + //TODO: in "deps", broadcast link and z_near at each loop: link, JokoRenderer::get_z_near() + let mut input_notification: HashMap< + u32, + tokio::sync::mpsc::Receiver, + > = Default::default(); //for renderer + input_notification.insert(0, u2u_receiver); + gui.joko_renderer.bind( + Default::default(), + Default::default(), + input_notification, + Default::default(), + ); + + let mut notifier: HashMap> = + Default::default(); //for package manager + notifier.insert(0, u2u_sender); + gui.package_manager.bind( + Default::default(), + Default::default(), + Default::default(), + notifier, + ); loop { { let mut nb_message_on_curent_loop: u128 = 0; @@ -565,32 +374,12 @@ impl Jokolay { nb_messages ); - if let Ok(mut import_status) = local_state.import_status.lock() { - if let ImportStatus::LoadingPack(file_path) = &mut *import_status { - let _ = u2b_sender.send(UIToBackMessage::ImportPack(file_path.clone())); - *import_status = ImportStatus::WaitingLoading(file_path.clone()); - } - } - //untested and might crash due to .unwrap() - while let Ok(msg) = u2u_receiver.try_recv() { - nb_messages += 1; - Self::handle_u2u_message(&mut gui, msg); - nb_message_on_curent_loop += 1; - if nb_message_on_curent_loop == max_nb_messages_per_loop { - break; - } - } if nb_message_on_curent_loop < max_nb_messages_per_loop { - while let Ok(msg) = b2u_receiver.try_recv() { - nb_messages += 1; - Self::handle_b2u_message(&mut gui, &mut local_state, &u2b_sender, msg); - nb_message_on_curent_loop += 1; - if nb_message_on_curent_loop == max_nb_messages_per_loop { - break; - } - } + gui.package_manager.flush_all_messages(); } } + //TODO: one could wrap the egui_context into a plugin result so that it can be used from other plugins + //TODO: same for the UI as a notified element. let JokolayGui { ui_configuration, @@ -667,24 +456,22 @@ impl Jokolay { if local_state.editable_mumble { local_state.window_changed = true; local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); - let _ = u2b_sender.send(UIToBackMessage::MumbleLink(local_state.link.clone())); + let _ = u2mb_sender.send(MessageToMumbleLinkBack::Value(local_state.link.clone())); } else { let is_mumble_alive = mumble_manager.is_alive(); - match mumble_manager.tick() { - Ok(ml) => { - if let Some(link) = ml { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - local_state.window_changed = true; - } - if is_mumble_alive { - local_state.link = Some(link.clone()); - } + match mumble_manager.tick(latest_time) { + Some(link) => { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + local_state.window_changed = true; + } + if is_mumble_alive { + local_state.link = Some(link.clone()); } } - Err(e) => { - error!(?e, "mumble manager tick error"); + _ => { + error!("mumble manager tick error"); } } } @@ -692,39 +479,30 @@ impl Jokolay { // check if we need to change window position or size. if let Some(link) = local_state.link.as_ref() { if local_state.window_changed { + let client_pos = &link.client_pos.0; + let client_size = &link.client_size.0; glfw_backend.window.set_pos( - link.client_pos.x.max(MINIMAL_WINDOW_POSITION_X), - link.client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), + client_pos.x.max(MINIMAL_WINDOW_POSITION_X), + client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), ); // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 let client_size_x = MINIMAL_WINDOW_WIDTH - .max(link.client_size.x) + .max(client_size.x) .min(local_state.maximal_window_width); let client_size_y = MINIMAL_WINDOW_HEIGHT - .max(link.client_size.y) + .max(client_size.y) .min(local_state.maximal_window_height); glfw_backend .window .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); } - if local_state.list_of_textures_changed - || link.changes.contains(MumbleChanges::Position) - || link.changes.contains(MumbleChanges::Map) - { - package_manager.tick( - &u2u_sender, - latest_time, - link, - JokoRenderer::get_z_near(), - ); - local_state.list_of_textures_changed = false; - } + package_manager.tick(latest_time, &egui_context); local_state.window_changed = false; } - joko_renderer.tick(local_state.link.as_ref()); + joko_renderer.tick(latest_time); menu_panel.tick(&etx, local_state.link.as_ref()); // do the gui stuff now @@ -782,8 +560,6 @@ impl Jokolay { }, ); package_manager.menu_ui( - &u2b_sender, - &u2u_sender, ui, local_state.nb_running_tasks_on_back, local_state.nb_running_tasks_on_network, @@ -793,7 +569,7 @@ impl Jokolay { if let Some(link) = local_state.link.as_mut() { mumble_gui( - &u2b_sender, + &u2mb_sender, &etx, &mut menu_panel.show_mumble_manager_window, &mut local_state.editable_mumble, @@ -801,7 +577,6 @@ impl Jokolay { ); }; package_manager.gui( - &u2b_sender, &etx, &mut menu_panel.show_package_manager_window, &local_state.import_status, @@ -811,7 +586,7 @@ impl Jokolay { JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); theme_manager.gui(&etx, &mut menu_panel.show_theme_window); ui_configuration.gui( - &u2b_sender, + &u2gb_sender, &etx, glfw_backend, &mut menu_panel.show_parameters_manager, @@ -971,7 +746,7 @@ pub struct MenuPanel { impl MenuPanel { pub const WIDTH: f32 = 288.0; pub const HEIGHT: f32 = 27.0; - pub fn tick(&mut self, etx: &egui::Context, link: Option<&jokolink::MumbleLink>) { + pub fn tick(&mut self, etx: &egui::Context, link: Option<&joko_link::MumbleLink>) { let mut ui_scaling_factor = 1.0; if let Some(link) = link.as_ref() { let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { @@ -986,8 +761,8 @@ impl MenuPanel { let min_width = MINIMAL_WINDOW_WIDTH as f32 * gw2_scale; let min_height = MINIMAL_WINDOW_HEIGHT as f32 * gw2_scale; - let gw2_width = link.client_size.x.max(MINIMAL_WINDOW_WIDTH) as f32; - let gw2_height = link.client_size.y.max(MINIMAL_WINDOW_HEIGHT) as f32; + let gw2_width = link.client_size.0.x.max(MINIMAL_WINDOW_WIDTH) as f32; + let gw2_height = link.client_size.0.y.max(MINIMAL_WINDOW_HEIGHT) as f32; let min_width_ratio = min_width.min(gw2_width) / min_width; let min_height_ratio = min_height.min(gw2_height) / min_height; @@ -1003,7 +778,7 @@ impl MenuPanel { } } -fn convert_uisz_to_scale(uisize: jokolink::UISize) -> f32 { +fn convert_uisz_to_scale(uisize: joko_link::UISize) -> f32 { const SMALL: f32 = 288.0; const NORMAL: f32 = 319.0; const LARGE: f32 = 355.0; @@ -1013,10 +788,10 @@ fn convert_uisz_to_scale(uisize: jokolink::UISize) -> f32 { const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; match uisize { - jokolink::UISize::Small => SMALL_SCALING_RATIO, - jokolink::UISize::Normal => NORMAL_SCALING_RATIO, - jokolink::UISize::Large => LARGE_SCALING_RATIO, - jokolink::UISize::Larger => LARGER_SCALING_RATIO, + joko_link::UISize::Small => SMALL_SCALING_RATIO, + joko_link::UISize::Normal => NORMAL_SCALING_RATIO, + joko_link::UISize::Large => LARGE_SCALING_RATIO, + joko_link::UISize::Larger => LARGER_SCALING_RATIO, } } /* diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs index f543283..fa7201d 100644 --- a/crates/jokolay/src/app/mumble.rs +++ b/crates/jokolay/src/app/mumble.rs @@ -1,9 +1,8 @@ use egui::DragValue; -use jmf::message::UIToBackMessage; -use jokolink::MumbleLink; +use joko_link::{MessageToMumbleLinkBack, MumbleLink}; pub fn mumble_gui( - u2b_sender: &std::sync::mpsc::Sender, + u2mb_sender: &std::sync::mpsc::Sender, etx: &egui::Context, open: &mut bool, editable_mumble: &mut bool, @@ -15,11 +14,11 @@ pub fn mumble_gui( ui.horizontal(|ui| { if ui.selectable_label(!*editable_mumble, "live").clicked() { *editable_mumble = false; - let _ = u2b_sender.send(UIToBackMessage::MumbleLinkAutonomous); + let _ = u2mb_sender.send(MessageToMumbleLinkBack::Autonomous); } if ui.selectable_label(*editable_mumble, "editable").clicked() { *editable_mumble = true; - let _ = u2b_sender.send(UIToBackMessage::MumbleLinkBindedOnUI); + let _ = u2mb_sender.send(MessageToMumbleLinkBack::BindedOnUI); } }); if *editable_mumble { @@ -45,30 +44,34 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.end_row(); ui.label("player position"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.player_pos.x)); - ui.add(DragValue::new(&mut link.player_pos.y)); - ui.add(DragValue::new(&mut link.player_pos.z)); + let player_pos = &mut link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); }); ui.end_row(); ui.label("player direction"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_avatar_front.x)); - ui.add(DragValue::new(&mut link.f_avatar_front.y)); - ui.add(DragValue::new(&mut link.f_avatar_front.z)); + let f_avatar_front = &mut link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); }); ui.end_row(); ui.label("camera position"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.cam_pos.x)); - ui.add(DragValue::new(&mut link.cam_pos.y)); - ui.add(DragValue::new(&mut link.cam_pos.z)); + let cam_pos = &mut link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); }); ui.end_row(); ui.label("camera direction"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_camera_front.x)); - ui.add(DragValue::new(&mut link.f_camera_front.y)); - ui.add(DragValue::new(&mut link.f_camera_front.z)); + let f_camera_front = &mut link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); }); ui.end_row(); ui.label("ui state"); @@ -91,7 +94,7 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.add(DragValue::new(&mut link.fov)); ui.end_row(); ui.label("w/h ratio"); - let ratio = link.client_size.as_vec2(); + let ratio = link.client_size.0.as_vec2(); let mut ratio = ratio.x / ratio.y; ui.add(DragValue::new(&mut ratio)); ui.end_row(); @@ -130,14 +133,16 @@ fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { ui.end_row(); ui.label("client pos"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_pos.x)); - ui.add(DragValue::new(&mut link.client_pos.y)); + let client_pos = &mut link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); }); ui.end_row(); ui.label("client size"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_size.x)); - ui.add(DragValue::new(&mut link.client_size.y)); + let client_size = &mut link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); }); ui.end_row(); ui.label("dpi scaling"); @@ -159,30 +164,34 @@ fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { ui.end_row(); ui.label("player position"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.player_pos.x)); - ui.add(DragValue::new(&mut dummy_link.player_pos.y)); - ui.add(DragValue::new(&mut dummy_link.player_pos.z)); + let player_pos = &mut dummy_link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); }); ui.end_row(); ui.label("player direction"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.x)); - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.y)); - ui.add(DragValue::new(&mut dummy_link.f_avatar_front.z)); + let f_avatar_front = &mut dummy_link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); }); ui.end_row(); ui.label("camera position"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.cam_pos.x)); - ui.add(DragValue::new(&mut dummy_link.cam_pos.y)); - ui.add(DragValue::new(&mut dummy_link.cam_pos.z)); + let cam_pos = &mut dummy_link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); }); ui.end_row(); ui.label("camera direction"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.f_camera_front.x)); - ui.add(DragValue::new(&mut dummy_link.f_camera_front.y)); - ui.add(DragValue::new(&mut dummy_link.f_camera_front.z)); + let f_camera_front = &mut dummy_link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); }); ui.end_row(); @@ -206,7 +215,7 @@ fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { ui.add(DragValue::new(&mut dummy_link.fov)); ui.end_row(); ui.label("w/h ratio"); - let ratio = dummy_link.client_size.as_vec2(); + let ratio = dummy_link.client_size.0.as_vec2(); let mut ratio = ratio.x / ratio.y; ui.add(DragValue::new(&mut ratio)); ui.end_row(); @@ -233,14 +242,16 @@ fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { ui.end_row(); ui.label("client pos"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.client_pos.x)); - ui.add(DragValue::new(&mut dummy_link.client_pos.y)); + let client_pos = &mut dummy_link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); }); ui.end_row(); ui.label("client size"); ui.horizontal(|ui| { - ui.add(DragValue::new(&mut dummy_link.client_size.x)); - ui.add(DragValue::new(&mut dummy_link.client_size.y)); + let client_size = &mut dummy_link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); }); ui.end_row(); ui.label("dpi scaling"); diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs index cfba395..027fd3e 100644 --- a/crates/jokolay/src/app/ui_parameters.rs +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -1,8 +1,9 @@ use egui_window_glfw_passthrough::GlfwBackend; -use jmf::message::UIToBackMessage; use serde::{Deserialize, Serialize}; +use super::messages::MessageToApplicationBack; + pub const UI_PARAMETERS_FILE_NAME: &str = "ui.toml"; #[derive(Serialize, Deserialize)] @@ -49,7 +50,7 @@ impl JokolayUIConfiguration { pub fn gui( &mut self, - u2b_sender: &std::sync::mpsc::Sender, + u2b_sender: &std::sync::mpsc::Sender, etx: &egui::Context, wb: &mut GlfwBackend, open: &mut bool, @@ -124,8 +125,9 @@ impl JokolayUIConfiguration { if need_to_save { match toml::to_string(&self.display_parameters) { Ok(serialized_string) => { - let _ = - u2b_sender.send(UIToBackMessage::SaveUIConfiguration(serialized_string)); + let _ = u2b_sender.send(MessageToApplicationBack::SaveUIConfiguration( + serialized_string, + )); } Err(e) => { tracing::error!(?e, "failed to serialize UI configuration"); From b33cd8e16e120b22ac4af4c36dd881545ca89ccb Mon Sep 17 00:00:00 2001 From: moi Date: Sat, 27 Apr 2024 21:01:32 +0200 Subject: [PATCH 44/54] more clean up and clarify where are the managers (future components) so they shall be easier to migrate to plugins in the future --- crates/joko_component_manager/Cargo.toml | 14 + crates/joko_component_manager/src/lib.rs | 47 + crates/joko_component_models/Cargo.toml | 13 + crates/joko_component_models/src/lib.rs | 131 + crates/joko_link_manager/Cargo.toml | 48 + crates/joko_link_manager/README.md | 58 + crates/joko_link_manager/src/lib.rs | 258 ++ crates/joko_link_manager/src/linux/mod.rs | 305 ++ crates/joko_link_manager/src/win/dll.rs | 490 +++ crates/joko_link_manager/src/win/mod.rs | 735 +++++ crates/joko_link_models/Cargo.toml | 1 - crates/joko_link_models/src/lib.rs | 8 - crates/joko_package_manager/Cargo.toml | 57 + crates/joko_package_manager/README.md | 87 + crates/joko_package_manager/build.rs | 14 + crates/joko_package_manager/images/marker.png | Bin 0 -> 173015 bytes .../joko_package_manager/images/question.png | Bin 0 -> 4248 bytes crates/joko_package_manager/images/trail.png | Bin 0 -> 6896 bytes .../images/trail_black.png | Bin 0 -> 2293 bytes .../images/trail_rainbow.png | Bin 0 -> 16987 bytes .../src/io/deserialize.rs | 1610 ++++++++++ crates/joko_package_manager/src/io/error.rs | 1 + crates/joko_package_manager/src/io/export.rs | 264 ++ crates/joko_package_manager/src/io/mod.rs | 9 + .../joko_package_manager/src/io/serialize.rs | 240 ++ crates/joko_package_manager/src/io/test.xml | 12 + .../src/io/xmlfile_schema.xsd | 394 +++ crates/joko_package_manager/src/lib.rs | 41 + .../joko_package_manager/src/manager/mod.rs | 29 + .../src/manager/pack/activation.rs | 20 + .../src/manager/pack/active.rs | 302 ++ .../src/manager/pack/category_selection.rs | 301 ++ .../src/manager/pack/dirty.rs | 28 + .../src/manager/pack/entry.rs | 6 + .../src/manager/pack/file_selection.rs | 47 + .../src/manager/pack/import.rs | 29 + .../src/manager/pack/list.rs | 6 + .../src/manager/pack/loaded.rs | 1016 +++++++ .../src/manager/pack/mod.rs | 6 + .../src/manager/package_data.rs | 515 ++++ .../src/manager/package_ui.rs | 770 +++++ crates/joko_package_manager/src/message.rs | 61 + .../vendor/rapid/license.txt | 52 + .../vendor/rapid/rapid.cpp | 66 + .../vendor/rapid/rapid.hpp | 7 + .../vendor/rapid/rapidxml.hpp | 2645 +++++++++++++++++ .../vendor/rapid/rapidxml_iterators.hpp | 295 ++ .../vendor/rapid/rapidxml_print.hpp | 422 +++ .../vendor/rapid/rapidxml_utils.hpp | 56 + crates/joko_package_models/src/attributes.rs | 2 +- crates/joko_plugin_manager/Cargo.toml | 12 + crates/joko_plugin_manager/src/lib.rs | 29 + crates/joko_render_manager/Cargo.toml | 24 + crates/joko_render_manager/shaders/marker.fs | 18 + crates/joko_render_manager/shaders/marker.vs | 39 + .../joko_render_manager/shaders/marker.wgsl | 0 .../shaders/player_visibility.wgsl | 24 + crates/joko_render_manager/shaders/trail.fs | 22 + crates/joko_render_manager/shaders/trail.vs | 37 + crates/joko_render_manager/src/billboard.rs | 360 +++ crates/joko_render_manager/src/gl.rs | 9 + crates/joko_render_manager/src/lib.rs | 3 + crates/joko_render_manager/src/renderer.rs | 354 +++ crates/joko_render_models/Cargo.toml | 2 +- crates/joko_render_models/src/messages.rs | 9 +- crates/jokolay/Cargo.toml | 13 +- crates/jokolay/src/app/mod.rs | 56 +- crates/jokolay/src/app/mumble.rs | 8 +- 68 files changed, 12479 insertions(+), 58 deletions(-) create mode 100644 crates/joko_component_manager/Cargo.toml create mode 100644 crates/joko_component_manager/src/lib.rs create mode 100644 crates/joko_component_models/Cargo.toml create mode 100644 crates/joko_component_models/src/lib.rs create mode 100644 crates/joko_link_manager/Cargo.toml create mode 100644 crates/joko_link_manager/README.md create mode 100644 crates/joko_link_manager/src/lib.rs create mode 100644 crates/joko_link_manager/src/linux/mod.rs create mode 100644 crates/joko_link_manager/src/win/dll.rs create mode 100644 crates/joko_link_manager/src/win/mod.rs create mode 100644 crates/joko_package_manager/Cargo.toml create mode 100644 crates/joko_package_manager/README.md create mode 100644 crates/joko_package_manager/build.rs create mode 100644 crates/joko_package_manager/images/marker.png create mode 100644 crates/joko_package_manager/images/question.png create mode 100644 crates/joko_package_manager/images/trail.png create mode 100644 crates/joko_package_manager/images/trail_black.png create mode 100644 crates/joko_package_manager/images/trail_rainbow.png create mode 100644 crates/joko_package_manager/src/io/deserialize.rs create mode 100644 crates/joko_package_manager/src/io/error.rs create mode 100644 crates/joko_package_manager/src/io/export.rs create mode 100644 crates/joko_package_manager/src/io/mod.rs create mode 100644 crates/joko_package_manager/src/io/serialize.rs create mode 100644 crates/joko_package_manager/src/io/test.xml create mode 100644 crates/joko_package_manager/src/io/xmlfile_schema.xsd create mode 100644 crates/joko_package_manager/src/lib.rs create mode 100644 crates/joko_package_manager/src/manager/mod.rs create mode 100644 crates/joko_package_manager/src/manager/pack/activation.rs create mode 100644 crates/joko_package_manager/src/manager/pack/active.rs create mode 100644 crates/joko_package_manager/src/manager/pack/category_selection.rs create mode 100644 crates/joko_package_manager/src/manager/pack/dirty.rs create mode 100644 crates/joko_package_manager/src/manager/pack/entry.rs create mode 100644 crates/joko_package_manager/src/manager/pack/file_selection.rs create mode 100644 crates/joko_package_manager/src/manager/pack/import.rs create mode 100644 crates/joko_package_manager/src/manager/pack/list.rs create mode 100644 crates/joko_package_manager/src/manager/pack/loaded.rs create mode 100644 crates/joko_package_manager/src/manager/pack/mod.rs create mode 100644 crates/joko_package_manager/src/manager/package_data.rs create mode 100644 crates/joko_package_manager/src/manager/package_ui.rs create mode 100644 crates/joko_package_manager/src/message.rs create mode 100644 crates/joko_package_manager/vendor/rapid/license.txt create mode 100644 crates/joko_package_manager/vendor/rapid/rapid.cpp create mode 100644 crates/joko_package_manager/vendor/rapid/rapid.hpp create mode 100644 crates/joko_package_manager/vendor/rapid/rapidxml.hpp create mode 100644 crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp create mode 100644 crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp create mode 100644 crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp create mode 100644 crates/joko_plugin_manager/Cargo.toml create mode 100644 crates/joko_plugin_manager/src/lib.rs create mode 100644 crates/joko_render_manager/Cargo.toml create mode 100644 crates/joko_render_manager/shaders/marker.fs create mode 100644 crates/joko_render_manager/shaders/marker.vs create mode 100644 crates/joko_render_manager/shaders/marker.wgsl create mode 100644 crates/joko_render_manager/shaders/player_visibility.wgsl create mode 100644 crates/joko_render_manager/shaders/trail.fs create mode 100644 crates/joko_render_manager/shaders/trail.vs create mode 100644 crates/joko_render_manager/src/billboard.rs create mode 100644 crates/joko_render_manager/src/gl.rs create mode 100644 crates/joko_render_manager/src/lib.rs create mode 100644 crates/joko_render_manager/src/renderer.rs diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml new file mode 100644 index 0000000..ae8b444 --- /dev/null +++ b/crates/joko_component_manager/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "joko_component_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = { workspace = true } +egui = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } +tokio = { workspace = true } +joko_component_models = { path = "../joko_component_models" } \ No newline at end of file diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs new file mode 100644 index 0000000..b19803d --- /dev/null +++ b/crates/joko_component_manager/src/lib.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use joko_component_models::JokolayComponentDeps; + +pub struct ComponentManager { + data: HashMap>, +} + +impl ComponentManager { + pub fn new() -> Self { + Self { + data: Default::default(), + } + } + + pub fn register(&mut self, service_name: &str, co: Box) { + self.data.insert(service_name.to_owned(), co); + } + + pub fn build_routes(&mut self) -> Result<(), String> { + let mut known_services: HashMap = Default::default(); + let mut service_id = 0; + for (service_name, co) in self.data.iter() { + service_id += 1; + known_services.insert(service_name.clone(), service_id); + for peer_name in co.peer() { + if let Some(peer) = self.data.get(peer_name) { + if !peer.peer().contains(&service_name.as_str()) { + return Err(format!( + "Missmatch in peer between {} and {}", + service_name, peer_name + )); + } + } + } + } + unimplemented!( + "The algorithm to build and check dependancies between components is not implemented" + ) + } +} + +impl Default for ComponentManager { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/joko_component_models/Cargo.toml b/crates/joko_component_models/Cargo.toml new file mode 100644 index 0000000..7ae5c56 --- /dev/null +++ b/crates/joko_component_models/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "joko_component_models" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = { workspace = true } +egui = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } +tokio = { workspace = true } diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs new file mode 100644 index 0000000..bc74e15 --- /dev/null +++ b/crates/joko_component_models/src/lib.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; + +//could become a "dyn Message". +//std::any::Any is a trait +//TODO: It would have a wrap and unwrap ? +pub type ComponentDataExchange = Vec; +//pub type ComponentDataExchange = Box<[u8]>; +//pub type ComponentDataExchange = [u8; 1024]; +pub type PeerComponentChannel = ( + tokio::sync::mpsc::Receiver, + tokio::sync::mpsc::Sender, +); + +pub trait JokolayComponentDeps { + /** + Names are external to traits and implementation. That way it is easy to change it without change in binary. + In case of first class components, name is hardcoded. + In case of plugins, name is part of a manifest and can be changed at will. + */ + // elements in peer(), requires() and notify() are mutually exclusives + fn peer(&self) -> Vec<&str> { + //by default, no other plugin bound + vec![] + } + fn requires(&self) -> Vec<&str> { + //by default, no requirement + vec![] + } + fn notify(&self) -> Vec<&str> { + //by default, no third party plugin + vec![] + } +} + +pub trait JokolayComponent +where + SharedStatus: Clone, +{ + fn flush_all_messages(&mut self) -> SharedStatus; + fn tick(&mut self, latest_time: f64) -> Option<&ComponentResult>; + fn bind( + &mut self, + deps: HashMap>, + bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap>, + notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ); //By default, there is no third party component, thus we can implement it as a noop + + /* + + // any extra information should come from configuration, which can be loaded from those two arguments. + Those roots are specific to the component, it cannot shared it with another component + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + ) -> Result; + + fn bind( + &mut self, + deps: HashMap, + bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap + notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) + https://docs.rs/dep-graph/latest/dep_graph/ + https://lib.rs/crates/petgraph + https://docs.rs/solvent/latest/solvent/ + => check "peer" is always mutual + => graph with the "peer" elements replaced by some merged id + => check there is no loop (there could be surprises) + => if there is no problem, then: + - build again the graph with UI plugins only and save one traversal (memory + file) + - build again the graph with back plugins only and save one traversal (memory + file) + => if there is a problem, do not save anything + + + + fn tick( + &mut self, + ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() + */ +} + +pub trait JokolayUIComponent +where + SharedStatus: Clone, +{ + fn flush_all_messages(&mut self) -> SharedStatus; + fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> Option<&ComponentResult>; + fn bind( + &mut self, + deps: HashMap>, + bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap>, + notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ); //By default, there is no third party component, thus we can implement it as a noop + + /* + + // any extra information should come from configuration, which can be loaded from those two arguments. + Those roots are specific to the component, it cannot shared it with another component + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + ) -> Result; + + fn bind( + &mut self, + deps: HashMap, + bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap + notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) + https://docs.rs/dep-graph/latest/dep_graph/ + https://lib.rs/crates/petgraph + https://docs.rs/solvent/latest/solvent/ + => check "peer" is always mutual + => graph with the "peer" elements replaced by some merged id + => check there is no loop (there could be surprises) + => if there is no problem, then: + - build again the graph with UI plugins only and save one traversal (memory + file) + - build again the graph with back plugins only and save one traversal (memory + file) + => if there is a problem, do not save anything + + + + fn tick( + &mut self, + ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() + */ +} diff --git a/crates/joko_link_manager/Cargo.toml b/crates/joko_link_manager/Cargo.toml new file mode 100644 index 0000000..eabd405 --- /dev/null +++ b/crates/joko_link_manager/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "joko_link_manager" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + + +[dependencies] +joko_core = { path = "../joko_core" } +joko_link_models = { path = "../joko_link_models" } +joko_component_models = { path = "../joko_component_models" } +widestring = { version = "1", default-features = false, features = ["std"] } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +time = { workspace = true } +miette = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } + +[target.'cfg(unix)'.dependencies] +x11rb = { version = "0.12", default-features = false, features = [] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.51.1", features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_Graphics_Dwm", + "Win32_UI_HiDpi", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_System_Com", +] } +arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } diff --git a/crates/joko_link_manager/README.md b/crates/joko_link_manager/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link_manager/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs new file mode 100644 index 0000000..766f8b5 --- /dev/null +++ b/crates/joko_link_manager/src/lib.rs @@ -0,0 +1,258 @@ +//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory + +//! Joko link is designed to primarily get the MumbleLink or the window size +//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). +//! on windows, you can use it to create/open shared memory. +//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. +//! then, you can easily read the /dev/shm file from a any number of linux native applications. +//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. +//! + +use std::vec; + +use enumflags2::BitFlags; +use joko_component_models::{ + ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, +}; +use joko_core::serde_glam::{IVec2, UVec2, Vec3}; +use joko_link_models::{ + ctypes, MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkSharedState, +}; +//use jokoapi::end_point::{mounts::Mount, races::Race}; +use miette::{IntoDiagnostic, Result, WrapErr}; +use serde_json::from_str; +use tracing::error; + +/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing +pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "windows")] +pub mod win; + +#[cfg(target_os = "linux")] +use linux::MumbleLinuxImpl as MumblePlatformImpl; +#[cfg(target_os = "windows")] +use win::MumbleWinImpl as MumblePlatformImpl; + +// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there +// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; +/// This primarily manages the mumble backend. +/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. +/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. +/// and every frame gets the latest mumble link data, and compares with the previous frame. +/// if any of the changed this frame, it will set the relevant changed flags so that plugins +/// or other parts of program which care can run the relevant code. +pub struct MumbleManager { + /// This abstracts over the windows and linux impl of mumble link functionality. + /// we use this to get the latest mumble link and latest window dimensions of the current mumble link + backend: MumblePlatformImpl, + is_ui: bool, + /// latest mumble link + link: MumbleLink, + channel_receiver: std::sync::mpsc::Receiver, + state: MumbleLinkSharedState, +} + +impl MumbleManager { + pub fn new(name: &str, is_ui: bool) -> Result { + let backend = MumblePlatformImpl::new(name)?; + let (_, receiver) = std::sync::mpsc::channel(); + Ok(Self { + backend, + link: Default::default(), + channel_receiver: receiver, + is_ui, + state: MumbleLinkSharedState { + read_ui_link: true, + copy_of_ui_link: None, + }, + }) + } + pub fn is_alive(&self) -> bool { + self.backend.is_alive() + } + fn handle_message(&mut self, msg: MessageToMumbleLinkBack) { + //let (b2u_sender, _) = package_manager.channels(); + match msg { + MessageToMumbleLinkBack::Autonomous => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); + self.state.read_ui_link = false; + } + MessageToMumbleLinkBack::BindedOnUI => { + tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); + self.state.read_ui_link = true; + } + MessageToMumbleLinkBack::Value(link) => { + tracing::trace!("Handling of UIToBackMessage::MumbleLink"); + self.state.copy_of_ui_link = link; + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + } + } + } + fn _tick(&mut self) -> Result> { + if let Err(e) = self.backend.tick() { + error!(?e, "mumble backend tick error"); + return Ok(None); + } + + if !self.backend.is_alive() { + self.link.client_size.0.x = 0; + self.link.client_size.0.y = 0; + self.link.changes = BitFlags::all(); + return Ok(Some(&self.link)); + } + // backend is alive and tick is successful. time to get link + let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); + let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { + Default::default() + } else { + self.link.clone() + }; + + if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { + return Ok(None); + } + let mut changes: BitFlags = Default::default(); + // safety. as the link is valid, we can use as_ref + let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) + .into_diagnostic() + .wrap_err("failed to get widestring out of cml identity")? + .to_string() + .into_diagnostic() + .wrap_err("failed to convert widestring to cstring")?; + + let identity: ctypes::CIdentity = from_str(&json_string) + .into_diagnostic() + .wrap_err("failed to deserialize identity from json string")?; + let uisz = identity + .get_uisz() + .ok_or(miette::miette!("uisz is invalid"))?; + let server_address = if cml.context.server_address[0] == 2 { + let addr = cml.context.server_address; + std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() + } else { + std::net::Ipv4Addr::UNSPECIFIED.into() + }; + if new_link.ui_tick != cml.ui_tick { + changes.insert(MumbleChanges::UiTick); + } + if new_link.name != identity.name { + changes.insert(MumbleChanges::Character); + } + if new_link.map_id != cml.context.map_id { + changes.insert(MumbleChanges::Map); + } + let client_pos = IVec2(glam::IVec2::new( + cml.context.client_pos[0], + cml.context.client_pos[1], + )); + let client_size = UVec2(glam::UVec2::new( + cml.context.client_size[0], + cml.context.client_size[1], + )); + + if new_link.client_pos != client_pos { + changes.insert(MumbleChanges::WindowPosition); + } + if new_link.client_size != client_size { + changes.insert(MumbleChanges::WindowSize); + } + let cam_pos: glam::Vec3 = cml.f_camera_position.into(); + if new_link.cam_pos.0 != cam_pos { + changes.insert(MumbleChanges::Camera); + } + + let player_pos: glam::Vec3 = cml.f_avatar_position.into(); + if new_link.player_pos.0 != player_pos { + changes.insert(MumbleChanges::Position); + } + //let player_race = Self::get_race(identity.race); + + new_link = MumbleLink { + ui_tick: cml.ui_tick, + player_pos: Vec3(player_pos), + f_avatar_front: Vec3(cml.f_avatar_front.into()), + cam_pos: Vec3(cam_pos), + f_camera_front: Vec3(cml.f_camera_front.into()), + name: identity.name, + map_id: cml.context.map_id, + fov: identity.fov, + uisz, + // window_pos, + // window_size, + changes, + // window_pos_without_borders, + // window_size_without_borders, + dpi_scaling: cml.context.dpi_scaling, + dpi: cml.context.dpi, + client_pos, + client_size, + map_type: cml.context.map_type, + server_address, + shard_id: cml.context.shard_id, + instance: cml.context.instance, + build_id: cml.context.build_id, + ui_state: cml.context.get_ui_state(), + compass_width: cml.context.compass_width, + compass_height: cml.context.compass_height, + compass_rotation: cml.context.compass_rotation, + player_x: cml.context.player_x, + player_y: cml.context.player_y, + map_center_x: cml.context.map_center_x, + map_center_y: cml.context.map_center_y, + map_scale: cml.context.map_scale, + process_id: cml.context.process_id, + mount: cml.context.mount_index, + race: identity.race, + }; + self.link = new_link; + + Ok(if self.link.ui_tick == 0 { + None + } else { + Some(&self.link) + }) + } +} + +impl JokolayComponent for MumbleManager { + fn flush_all_messages(&mut self) -> MumbleLinkSharedState { + while let Ok(msg) = self.channel_receiver.try_recv() { + self.handle_message(msg); + } + self.state.clone() + } + + fn tick(&mut self, _latest_time: f64) -> Option<&MumbleLink> { + self._tick().unwrap_or(None) + } + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + } +} + +impl JokolayComponentDeps for MumbleManager { + //default is enough + fn peer(&self) -> Vec<&str> { + if self.is_ui { + vec!["mumble_link_back"] + } else { + vec!["mumble_link_ui"] + } + } +} diff --git a/crates/joko_link_manager/src/linux/mod.rs b/crates/joko_link_manager/src/linux/mod.rs new file mode 100644 index 0000000..f0adab4 --- /dev/null +++ b/crates/joko_link_manager/src/linux/mod.rs @@ -0,0 +1,305 @@ +use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; +use miette::{Context, IntoDiagnostic, Result}; +use std::fs::File; +use std::io::{Read, Seek}; +use time::OffsetDateTime; +use tracing::info; +// use x11rb::protocol::xproto::{change_property, intern_atom, AtomEnum, GetGeometryReply, PropMode}; +// use x11rb::rust_connection::ConnectError; + +pub use x11rb::rust_connection::RustConnection; + +/// This is the bak +pub struct MumbleLinuxImpl { + mfile: File, + link_buffer: LinkBuffer, + /// we basically use this as the ui_tick of mumblelink + /// If this changed recently, it means jokolink is running (i.e. gw2 is running) + previous_jokolink_timestamp: i128, +} + +type LinkBuffer = Box<[u8; C_MUMBLE_LINK_SIZE_FULL]>; + +impl MumbleLinuxImpl { + pub fn new(link_name: &str) -> Result { + let mumble_file_name = format!("/dev/shm/{link_name}"); + info!("creating mumble file at {mumble_file_name}"); + #[allow(clippy::suspicious_open_options)] + let mut mfile = File::options() + .read(true) + .write(true) // write/append is needed for the create flag + .create(true) + .open(&mumble_file_name) + .into_diagnostic() + .wrap_err("failed to create mumble file")?; + let mut link_buffer = LinkBuffer::new([0u8; C_MUMBLE_LINK_SIZE_FULL]); + mfile.rewind().into_diagnostic()?; + mfile + .read(link_buffer.as_mut()) + .into_diagnostic() + .wrap_err("failed to get link buffer from mfile")?; + let previous_jokolink_timestamp = + unsafe { CMumbleLink::get_timestamp(link_buffer.as_ptr() as _) }; + Ok(MumbleLinuxImpl { + mfile, + link_buffer, + previous_jokolink_timestamp, + }) + } + pub fn tick(&mut self) -> Result<()> { + self.mfile.rewind().into_diagnostic()?; + self.mfile + .read(self.link_buffer.as_mut()) + .into_diagnostic() + .wrap_err("failed to get link buffer")?; + self.previous_jokolink_timestamp = + unsafe { CMumbleLink::get_timestamp(self.link_buffer.as_ptr() as _) }; + Ok(()) + } + pub fn is_alive(&self) -> bool { + OffsetDateTime::now_utc().unix_timestamp_nanos() - self.previous_jokolink_timestamp + < std::time::Duration::from_secs(1).as_nanos() as i128 + } + pub fn get_cmumble_link(&self) -> CMumbleLink { + if self.is_alive() { + unsafe { std::ptr::read(self.link_buffer.as_ptr() as _) } + } else { + Default::default() + } + } + // pub fn set_transient_for(&self) -> Result<()> { + // Ok(()) + // Ok(self + // .xc + // .set_transient_for(xid_from_buffer(&self.link_buffer))?) + // } +} + +// struct X11Connection { +// jokolay_window_id: u32, +// transient_for_atom: u32, +// // net_wm_pid_atom: u32, +// xc: RustConnection, +// } +// impl X11Connection { +// pub const WM_TRANSIENT_FOR: &'static str = "WM_TRANSIENT_FOR"; +// // pub const NET_WM_PID: &'static str = "_NET_WM_PID"; +// fn new(jokolay_window_id: u32) -> Result { +// let (xc, _) = RustConnection::connect(None).expect("failed to create x11 connection"); +// let transient_for_atom = intern_atom(&xc, true, Self::WM_TRANSIENT_FOR.as_bytes()) +// .map_err(|e| X11Error::AtomQueryError { +// source: e, +// atom_str: Self::WM_TRANSIENT_FOR, +// })? +// .reply() +// .map_err(|e| X11Error::AtomReplyError { +// source: e, +// atom_str: Self::WM_TRANSIENT_FOR, +// })? +// .atom; +// // let net_wm_pid_atom = intern_atom(&xc, true, Self::NET_WM_PID.as_bytes()) +// // .map_err(|e| X11Error::AtomQueryError { +// // source: e, +// // atom_str: Self::NET_WM_PID, +// // })? +// // .reply() +// // .map_err(|e| X11Error::AtomReplyError { +// // source: e, +// // atom_str: Self::NET_WM_PID, +// // })? +// // .atom; + +// Ok(Self { +// jokolay_window_id, +// transient_for_atom, +// xc, +// // net_wm_pid_atom, +// }) +// } +// pub fn set_transient_for(&self, parent_window: u32) -> Result<(), X11Error> { +// if let Ok(xst) = std::env::var("XDG_SESSION_TYPE") { +// if xst == "wayland" { +// tracing::warn!("skipping transient_for because we are on wayland"); +// return Ok(()); +// } +// if xst != "x11" { +// tracing::warn!("xdg session type is neither wayland not x11: {xst}"); +// } +// } +// assert_ne!(parent_window, 0); +// change_property( +// &self.xc, +// PropMode::REPLACE, +// self.jokolay_window_id, +// self.transient_for_atom, +// AtomEnum::WINDOW, +// 32, +// 1, +// &parent_window.to_ne_bytes(), +// ) +// .map_err(|e| X11Error::TransientForError { +// source: e, +// parent: parent_window, +// child: self.jokolay_window_id, +// })? +// .check() +// .map_err(|e| X11Error::TransientForReplyError { +// source: e, +// parent: parent_window, +// child: self.jokolay_window_id, +// })?; +// Ok(()) +// } + +// pub fn get_window_dimensions(&self, xid: u32) -> Result<[i32; 4]> { +// assert_ne!(xid, 0); +// let geometry = x11rb::protocol::xproto::get_geometry(&self.xc, xid) +// .into_diagnostic() +// .wrap_err("get geometry fn failed")? +// .reply() +// .into_diagnostic() +// .wrap_err("geometry reply is wrong")?; +// let translated_coordinates = x11rb::protocol::xproto::translate_coordinates( +// &self.xc, +// xid, +// geometry.root, +// geometry.x, +// geometry.y, +// ) +// .into_diagnostic() +// .wrap_err("failed to translate coords")? +// .reply() +// .into_diagnostic() +// .wrap_err("translate coords reply error")?; +// let x_outer = translated_coordinates.dst_x as i32; +// let y_outer = translated_coordinates.dst_y as i32; +// let width = geometry.width; +// let height = geometry.height; + +// tracing::debug!( +// "translated_x: {}, translated_y: {}, width: {}, height: {}, geo_x: {}, geo_y: {}", +// x_outer, +// y_outer, +// width, +// height, +// geometry.x, +// geometry.y +// ); +// Ok([x_outer, y_outer, width as _, height as _]) +// } +// // pub fn get_pid_from_xid(&self, xid: u32) -> Result { +// // assert_ne!(xid, 0); + +// // let pid_prop = get_property( +// // &self.xc, +// // false, +// // xid, +// // self.net_wm_pid_atom, +// // AtomEnum::CARDINAL, +// // 0, +// // 1, +// // ) +// // .expect("coudn't get _NET_WM_PID property gw2") +// // .reply() +// // .expect("reply for _NET_WM_PID property gw2 "); + +// // if pid_prop.bytes_after != 0 +// // && pid_prop.format != 32 +// // && pid_prop.value_len != 1 +// // && pid_prop.value.len() != 4 +// // { +// // panic!("invalid pid property {:#?}", pid_prop); +// // } +// // Ok(u32::from_ne_bytes(pid_prop.value.try_into().expect( +// // "pid property value has a bytes length of less than 4", +// // ))) +// // } +// } +// pub fn get_frame_extents(xc: &RustConnection, xid: u32) -> Result<(u32, u32, u32, u32)> { +// assert_ne!(xid, 0); +// let net_frame_extents_atom = intern_atom(&self.xc, true, b"_NET_FRAME_EXTENTS") +// .expect("coudn't intern atom for _NET_FRAME_EXTENTS ")? +// .reply() +// .expect("reply for intern atom for _NET_FRAME_EXTENTS")? +// .atom; +// let frame_prop = get_property( +// &self.xc, +// false, +// xid, +// net_frame_extents_atom, +// AtomEnum::ANY, +// 0, +// 100, +// ) +// .expect("coudn't get frame property gw2")? +// .reply() +// .expect("reply for frame property gw2")?; + +// if frame_prop.bytes_after != 0 { +// bail!( +// "bytes after in frame property is {}", +// frame_prop.bytes_after +// ); +// } +// if frame_prop.format != 32 { +// bail!("frame_prop format is {}", frame_prop.format); +// } +// if frame_prop.value_len != 4 { +// bail!("frame_prop value_len is {}", frame_prop.value_len); +// } +// if frame_prop.value.len() != 16 { +// bail!("frame_prop.value.len() is {}", frame_prop.value.len()); +// } +// // avoid bytemuck dependency and just do this raw. +// let mut arr = [0u8; 4]; +// arr.copy_from_slice(&frame_prop.value[0..4]); +// let left_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[4..8]); +// let right_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[8..12]); +// let top_border = u32::from_ne_bytes(arr); +// arr.copy_from_slice(&frame_prop.value[12..16]); +// let bottom_border = u32::from_ne_bytes(arr); +// Ok((left_border, right_border, top_border, bottom_border)) +// } + +// pub fn get_gw2_pid(&mut self) -> Result { +// assert_ne!(self.gw2_window_handle, 0); +// let pid_atom = x11rb::protocol::xproto::intern_atom(&self.&self.xc, true, b"_NET_WM_PID") +// .expect("could not intern atom '_NET_WM_PID'")? +// .reply() +// .expect("reply error while interning '_NET_WM_PID'.")? +// .atom; +// let reply = x11rb::protocol::xproto::get_property( +// &self.&self.xc, +// false, +// self.gw2_window_handle, +// pid_atom, +// x11rb::protocol::xproto::AtomEnum::CARDINAL, +// 0, +// 1, +// ) +// .expect("could not request '_NET_WM_PID' for gw2 window handle ")? +// .reply() +// .expect("the reply for '_NET_WM_PID' of gw2 handle ")?; + +// let pid_format = 32; +// if pid_format != reply.format { +// bail!("pid_format is not 32. so, type is wrong"); +// } +// let pid_buffer_size = 4; +// if pid_buffer_size != reply.value.len() { +// bail!("pid_buffer is not 4 bytes"); +// } +// let value_len = 1; +// if value_len != reply.value_len { +// bail!("pid reply's value_len is not 1"); +// } +// let remaining_bytes_len = 0; +// if remaining_bytes_len != reply.bytes_after { +// bail!("we still have too many bytes remaining after reading '_NET_WM_PID'"); +// } +// let mut buffer = [0u8; 4]; +// buffer.copy_from_slice(&reply.value); +// Ok(u32::from_ne_bytes(buffer)) +// } diff --git a/crates/joko_link_manager/src/win/dll.rs b/crates/joko_link_manager/src/win/dll.rs new file mode 100644 index 0000000..721b5fe --- /dev/null +++ b/crates/joko_link_manager/src/win/dll.rs @@ -0,0 +1,490 @@ +#![allow(non_snake_case)] + +arcdps::arcdps_export! { + name: "jokolink", + // This is just "joko" as hex bytes + sig: 0x6a6f6b6f, + init: init, + release: release, +} + +fn init() -> ::core::result::Result<(), Box> { + println!("jokolink init called by arcdps. spawning background thread for jokolink"); + unsafe { spawn_jokolink_thread() }; + Ok(()) +} +/// If no other thread has been spawned, this will spawn a new thread where jokolink will run +unsafe fn spawn_jokolink_thread() { + if d3d11::JOKOLINK_THREAD_HANDLE.is_none() { + let (quit_request_sender, quit_request_receiver) = std::sync::mpsc::sync_channel(0); + let (quit_response_sender, quit_response_receiver) = std::sync::mpsc::sync_channel(1); + + d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); + d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); + + let th = std::thread::Builder::new() + .name("jokolink thread".to_string()) + .spawn(move || { + d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); + "jokolink thread quit" + }); + match th { + Ok(handle) => { + println!("spawned jokolink thread. handle: {handle:?}"); + d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); + } + Err(e) => { + eprintln!("failed to spawn jokolink thread due to error {e:#?}"); + } + } + } else { + println!("jokolink thread has already been initialized, so skipping initialization."); + } +} +/// This is really unsafe, so we have to be careful +/// We cannot directly terminate thread because it might lead to some syncronization issues and cause a crash/deadlock +/// we HAVE to terminate the thread because otherwise, it will crash gw2 too. +/// So, we use channels to send a signal to jokolink thread to quit. +/// Then, we use another channel to wait and receive a signal that will be sent by jokolink thread when it terminates. +/// +/// We can't call `join` on the thread handle because.. like i said, it can lead to a deadlock/crash. +/// This applies whether we are loaded by game as d3d11.dll or by arcdps as an addon. +unsafe fn terminate_jokolink_thread() { + if let Some(sender) = d3d11::JOKOLINK_QUIT_REQUESTER.take() { + if let Err(e) = sender.send(()) { + eprintln!("failed to send quit signal due to error {e:#?}"); + } else { + println!("successfully sent the quit signal to the jokolink thread"); + } + } + if let Some(receiver) = d3d11::JOKOLINK_QUIT_RESPONDER.take() { + match receiver.recv() { + Ok(_) => { + println!("received quit response from jokolink thread"); + } + Err(e) => { + eprintln!("failed to receive quit response from jokolink thread. {e:#?}"); + } + } + } + if let Some(handle) = d3d11::JOKOLINK_THREAD_HANDLE.take() { + if handle.is_finished() { + println!("jokolink thread is finished"); + } else { + println!("jokolink thread is not yet finished, so waiting for it by joining the handle :(((("); + match handle.join() { + Ok(o) => { + println!("joined jokolink thread with return value: {o}"); + } + Err(e) => { + eprintln!("jokolink thread panic: {e:?}"); + } + } + } + } else { + println!("jokolink thread was never started. So, nothing to terminate"); + } +} +fn release() { + println!("jokolink release called by arcdps."); + unsafe { + terminate_jokolink_thread(); + } +} + +pub mod d3d11 { + use std::{ + sync::mpsc::{Receiver, SyncSender}, + thread::JoinHandle, + }; + + use windows::{ + core::*, + Win32::Foundation::*, + Win32::System::{ + LibraryLoader::{GetProcAddress, LoadLibraryA}, + SystemInformation::GetSystemDirectoryA, + // Threading::{CreateThread, TerminateThread, THREAD_CREATION_FLAGS}, + }, + }; + + /// Dll injection basics: + /// 1. You write a custom dll library exposing functions that match the names/signatures of the actual winapi functions + /// 2. Then, you place your custom dll library in gw2's executable directory. + /// 3. gw2 loads your dll and calls your functions thinking it is calling winapi functions. + /// 4. You will use this chance to do whatever you want, before forwarding the calls to the actual winapi functions + /// 5. So, we will load the dll from `system32` directory once. store it in [DLL_PTR] + /// 6. When a function is called, we check if the fn pointer is already loaded. If it is not, we get it from the dll pointer + static mut DLL_PTR: HMODULE = HMODULE(0); + static mut CREATE_DEVICE_FNPTR: Option< + unsafe extern "system" fn( + padapter: *mut ::core::ffi::c_void, + drivertype: i32, + software: HMODULE, + flags: u32, + pfeaturelevels: *const i32, + featurelevels: u32, + sdkversion: u32, + ppdevice: *mut *mut ::core::ffi::c_void, + pfeaturelevel: *mut i32, + ppimmediatecontext: *mut *mut ::core::ffi::c_void, + ) -> HRESULT, + > = None; + pub static mut JOKOLINK_THREAD_HANDLE: Option> = None; + + /// This is used to tell wine_main fn thread to quit. + pub static mut JOKOLINK_QUIT_REQUESTER: Option> = None; + /// This is used to wait for wine_main fn thread to quit and send us a signal + pub static mut JOKOLINK_QUIT_RESPONDER: Option> = None; + /// This function is called whenever the dll is loaded into process or thread, and whenever the dll is unloaded out of process/thread. + /// # Safety + /// Don't do *anything* complicated at all. It can easily lead to a deadlock + /// https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices + /// Improper synchronization within DllMain can cause an application to deadlock or access data or code in an uninitialized DLL. + #[no_mangle] + pub unsafe extern "system" fn DllMain( + _dll_module: HINSTANCE, + call_reason: u32, + _: *mut (), + ) -> bool { + match call_reason { + // process detach + 0 => { + // unlike attach + println!("jokolink dll is being detached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}."); + super::terminate_jokolink_thread(); + } + // process attach + 1 => { + // Sometimes, our dll might be attached/detached multiple times. And we don't want to start jokolink_thread everything time + // Instead, we only launch our jokolink thread when the D3D11CreateDevice is called + println!("jokolink dll has been attached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}"); + } + // thread attach and detach + 2 | 3 => { + // no need to do anything for thread attach and thread detach + } + // invalid values + rest => { + eprintln!("unrecognized dll main call reason: {rest}"); + } + } + true + } + /// This is the function we will "hook" into. + /// GW2 will call this function right after the "login window" when creating the main window + /// This is where we initialize our jokolink thread. + /// # Safety + /// Just need to load d3d11.dll from windows/system32 equivalent directory and call that function for gw2 + #[no_mangle] + pub unsafe extern "system" fn D3D11CreateDevice( + padapter: *mut ::core::ffi::c_void, + drivertype: i32, + software: HMODULE, + flags: u32, + pfeaturelevels: *const i32, + featurelevels: u32, + sdkversion: u32, + ppdevice: *mut *mut ::core::ffi::c_void, + pfeaturelevel: *mut i32, + ppimmediatecontext: *mut *mut ::core::ffi::c_void, + ) -> HRESULT { + if DLL_PTR.is_invalid() { + let mut path = [0u8; MAX_PATH as _]; + let len = GetSystemDirectoryA(Some(&mut path)) as usize; + // we make sure that len is not zero. It means that GetSystemDirectoryA fn didn't fail. + // we also check if length is above 200, because then we might be reaching the limit of maximum path length supported by windows. + if len == 0 || len > 200 { + eprintln!("the system directory path size is: {len}. So, i am quitting"); + return HRESULT::default(); + } + const D3D11_DLL_PATH: &str = "\\d3d11.dll\0"; + path[len..(len + D3D11_DLL_PATH.len())].copy_from_slice(D3D11_DLL_PATH.as_bytes()); + + match LoadLibraryA(PCSTR::from_raw(path.as_ptr())) { + Ok(p) => { + println!("successfully loaded library d3d11.dll "); + DLL_PTR = p; + } + Err(e) => { + eprintln!("could not load d3d11.dll from system path due to error: {e:#?}"); + return HRESULT::default(); + } + } + } else { + println!("d3d11.dll library is already loaded. So, skipping that"); + } + if CREATE_DEVICE_FNPTR.is_none() { + if let Some(p) = GetProcAddress(DLL_PTR, PCSTR("D3D11CreateDevice\0".as_ptr())) { + println!("successfully got proc address of D3D11CreateDevice"); + let _ = CREATE_DEVICE_FNPTR.insert(std::mem::transmute(p)); + } else { + eprintln!("could not load address of D3D11CreateDevice"); + } + } else { + println!("D3D11CreateDevice fn ptr is already loaded, so skipped that"); + } + if JOKOLINK_THREAD_HANDLE.is_none() { + println!("starting jokolink's wine_main on another thrad"); + + super::spawn_jokolink_thread(); + } + println!("calling D3D11CreateDevice fn"); + if let Some(p) = CREATE_DEVICE_FNPTR { + p( + padapter, + drivertype, + software, + flags, + pfeaturelevels, + featurelevels, + sdkversion, + ppdevice, + pfeaturelevel, + ppimmediatecontext, + ) + } else { + HRESULT::default() + } + } + + // unsafe extern "system" fn wine_main(_: *mut ::core::ffi::c_void) -> u32 { + // super::spawn_jokolink_thread(); + // 0 + // } + pub mod wine { + use crate::mumble::ctypes::*; + use crate::win::MumbleWinImpl; + use crate::DEFAULT_MUMBLELINK_NAME; + use miette::{Context, IntoDiagnostic, Result}; + use serde::{Deserialize, Serialize}; + use std::io::Write; + use std::io::{Seek, SeekFrom}; + use std::path::{Path, PathBuf}; + use std::str::FromStr; + use std::sync::mpsc::{Receiver, SyncSender}; + use std::time::Duration; + use tracing::{error, info}; + use tracing_subscriber::filter::LevelFilter; + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(default)] + pub struct JokolinkConfig { + pub loglevel: String, + pub logdir: PathBuf, + pub mumble_link_name: String, + pub interval: u32, + pub copy_dest_dir: PathBuf, + } + + impl Default for JokolinkConfig { + fn default() -> Self { + Self { + loglevel: "info".to_string(), + logdir: PathBuf::from("."), + mumble_link_name: DEFAULT_MUMBLELINK_NAME.to_string(), + interval: 5, + copy_dest_dir: PathBuf::from("z:\\dev\\shm"), + } + } + } + + pub fn wine_main( + quit_request_receiver: Receiver<()>, + quit_response_sender: SyncSender<()>, + ) { + if let Err(e) = std::panic::catch_unwind(move || { + let config = "./jokolink_config.json".to_string(); + let config = std::path::PathBuf::from(config); + if !config.exists() { + match std::fs::File::create(&config) { + Ok(mut f) => match serde_json::to_string_pretty(&JokolinkConfig::default()) + { + Ok(config_string) => { + if let Err(e) = f.write_all(config_string.as_bytes()) { + eprintln!( + "failed to write default config file due to error {e:#?}" + ); + } + } + Err(e) => { + eprintln!("failed to serialize default config due to error {e:#?}"); + } + }, + Err(e) => eprintln!("failed to create config.json due to error {e:#?}"), + } + } + let config: JokolinkConfig = match std::fs::File::open(&config) { + Ok(f) => match serde_json::from_reader(std::io::BufReader::new(f)) { + Ok(config) => config, + Err(e) => { + eprintln!("failed to deserialize config file due to error {e:#?}"); + return; + } + }, + Err(e) => { + eprintln!("failed to open config file due to error {e:#?}"); + return; + } + }; + println!("successfully loaded configuration file"); + match miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + .unicode(true) + .context_lines(4) + .with_cause_chain() + .build(), + ) + })) { + Ok(_) => { + println!("miette hook set"); + } + Err(e) => { + eprintln!("failed to set miette hook due to {e:#?}"); + } + } + let guard = match log_init( + LevelFilter::from_str(&config.loglevel).unwrap_or(LevelFilter::INFO), + &config.logdir, + Path::new("jokolink.log"), + ) { + Ok(g) => g, + Err(e) => { + eprintln!("failed to initiailize logging due to error {e:#?}"); + return; + } + }; + if let Err(e) = fake_main(config, quit_request_receiver) { + eprintln!("fake main exited due to error: {e:#?}"); + } + std::mem::drop(guard); + println!("dropped logfile guard"); + }) { + eprintln!("There was a panic in jokolink thread: {e:?}"); + } + println!("exiting wine_main function"); + match quit_response_sender.send(()) { + Ok(_) => { + println!("successfully sent quit response"); + } + Err(e) => { + eprintln!("failed to send quit response due to: {e:#?}"); + } + } + } + + fn fake_main(config: JokolinkConfig, quit_signal: Receiver<()>) -> Result<()> { + let refresh_inverval = Duration::from_millis(config.interval as u64); + + info!("Application Name: {}", env!("CARGO_PKG_NAME")); + info!("Application Version: {}", env!("CARGO_PKG_VERSION")); + info!("Application Authors: {}", env!("CARGO_PKG_AUTHORS")); + info!( + "Application Repository Link: {}", + env!("CARGO_PKG_REPOSITORY") + ); + info!("Application License: {}", env!("CARGO_PKG_LICENSE")); + + // info!("git version details: {}", git_version::git_version!()); + + info!( + "the file log lvl: {:?}, the logfile directory: {:?}", + &config.loglevel, &config.logdir + ); + info!("created app and initialized logging"); + info!("the mumble link names: {:#?}", &config.mumble_link_name); + info!( + "the mumble refresh interval in milliseconds: {:#?}", + refresh_inverval + ); + + info!( + "the path to which we write mumble data: {:#?}", + &config.copy_dest_dir + ); + let mumble_key = config.mumble_link_name.clone(); + + let dest_path = config.copy_dest_dir.join(&mumble_key); + + // create a shared memory file in /dev/shm/mumble_link_key_name so that jokolay can mumble stuff from there. + info!( + "creating the path to destination shm file: {:?}", + &dest_path + ); + + #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] + let mut mfile = std::fs::File::options() + .write(true) + .create(true) + .open(&dest_path) + .into_diagnostic() + .wrap_err_with(|| { + format!("failed to create shm file with path {:#?}", &dest_path) + })?; + // create shared memory using the mumble link key + let mut source = MumbleWinImpl::new(&mumble_key)?; + + loop { + if let Err(e) = source.tick() { + error!(?e, "mumble tick error"); + } + let link = source.get_cmumble_link(); + + let buffer: [u8; C_MUMBLE_LINK_SIZE_FULL] = + unsafe { std::ptr::read_volatile(&link as *const CMumbleLink as *const _) }; + mfile + .seek(SeekFrom::Start(0)) + .into_diagnostic() + .wrap_err("could not seek to start of shared memory file due to error")?; + + // write buffer to the file + mfile + .write(&buffer) + .into_diagnostic() + .wrap_err("could not write to shared memory file due to error")?; + match quit_signal.try_recv() { + Ok(_) => { + println!("received quit signal. returning from wine_main()"); + error!("received quit signal. returning from wine_main()"); + return Ok(()); + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + eprintln!("why is the quit signaller sender disconnected????"); + } + }, + } + // we sleep for a few milliseconds to avoid reading mumblelink too many times. we will read it around 100 to 200 times per second + std::thread::sleep(refresh_inverval); + } + } + + /// initializes global logging backend that is used by log macros + /// Takes in a filter for stdout/stderr, a filter for logfile and finally the path to logfile + pub fn log_init( + file_filter: LevelFilter, + log_directory: &Path, + log_file_name: &Path, + ) -> Result { + // let file_appender = tracing_appender::rolling::never(log_directory, log_file_name); + let file_path = log_directory.join(log_file_name); + let writer = std::io::BufWriter::new( + std::fs::File::create(&file_path) + .into_diagnostic() + .wrap_err_with(|| { + format!("failed to create logfile at path: {:#?}", &file_path) + })?, + ); + let (nb, guard) = tracing_appender::non_blocking(writer); + tracing_subscriber::fmt() + .with_writer(nb) + .with_max_level(file_filter) + .pretty() + .with_ansi(false) + .init(); + + Ok(guard) + } + } +} diff --git a/crates/joko_link_manager/src/win/mod.rs b/crates/joko_link_manager/src/win/mod.rs new file mode 100644 index 0000000..21ebc75 --- /dev/null +++ b/crates/joko_link_manager/src/win/mod.rs @@ -0,0 +1,735 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod dll; +//putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration + +use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; +use miette::{bail, Context, IntoDiagnostic, Result}; +use notify::Watcher; +use std::{ + path::PathBuf, + str::FromStr, + time::{Duration, Instant}, +}; +use time::OffsetDateTime; +use tracing::{debug, error, info, warn}; +use windows::{ + core::PCSTR, + Win32::{ + Foundation::*, + Graphics::{ + Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}, + Gdi::ClientToScreen, + }, + System::{ + Com::CoTaskMemFree, + Memory::*, + Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}, + }, + UI::{ + HiDpi::{GetDpiForWindow, GetProcessDpiAwareness}, + Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath}, + WindowsAndMessaging::*, + }, + }, +}; + +/// This source will be the used to abstract the linux/windows way of getting MumbleLink +/// on windows, this represents the shared memory pointer to mumblelink, and as long as one of gw2 or a client like us is alive, the shared memory will stay alive +/// on linux, this will be a File in /dev/shm that will only exist if jokolink created it at some point in time. this lives in ram, so reading from it is pretty much free. +#[derive(Debug)] +pub struct MumbleWinImpl { + /// This is the pointer to shared memory which we mapped into our address space + /// This is NEVER null. Because we consider failing to create MumbleLink as a hard error. + /// ## Unsafe: + /// Must unmap this pointer when we are dropping + link_ptr: *const CMumbleLink, + /// This is the handle to shared memory. We must close the handle when we are quitting + /// This also never invalid. Because we consider failing to create MumbleLink as a hard error. + /// ## Unsafe: + /// Must close this handle when we are dropping + mumble_handle: HANDLE, + /// this is the previous ui_tick. We use this to check if there has been any change in mumble link memory + /// If there is a change, then we check if the new pid is the same as old pid + previous_ui_tick: u32, + /// This is the previous pid of the mumble link + /// If the current pid has changed, then it means we are dealing with a new gw2 process. + previous_pid: u32, + /// This is the process handle for gw2. + /// when we see a change in pid, we will close the handle (if its valid) and open a new handle to the new gw2 process + /// + /// This handle is very important, because its validity shows that the gw2 process is "alive". + /// If ui_tick has not changed for more than a second, then we will check using windows api if the process is still alive. + /// If not, we will reset everything in our struct except for last_pid and last_ui_tick. + process_handle: HANDLE, + /// if ui_tick updates, we set this to now. + /// If ui_tick doesn't update for more than 1 second AND we are alive, we will check if gw2 is still alive and reset the timestamp. + last_ui_tick_update: Instant, + /// if ui_tick changes this frame and we are alive, we get window size/pos of gw2 and reset this. + /// if we are not alive, then we simply skip this check. + last_pos_size_check: Instant, + + /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. + client_pos: [i32; 2], + client_size: [u32; 2], + /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + /// 0 for false + /// 1 for true + /// -1 for no idea. maybe because we couldn't find the config or read it or whatever. + /// I recommend just assuming that it is true when in doubt. Because the text is too small to read when dpi scaling is turned off. + dpi_scaling: i32, + /// DPI of the gw2 window + /// We get this via win32 api + dpi: i32, + /// This is the window handle of gw2. + /// This is automatically set when we try to get window size/pos. and will be reset if gw2 process dies or if we find a new gw2 process. + window_handle: isize, + /// X11 window id. This is only useful for jokolink when it is run as dll on wine + /// When the struct is initialized, we also try to get xid. and keep it here. On windows, we will just keep it at zero. + xid: u32, + /// This is the $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + /// But we get this programmatically via ShGetKnownFolderPath + _gw2_config_watcher: notify::RecommendedWatcher, + gw2_config_changed: std::sync::Arc, + gw2_config_path: PathBuf, /* + /// This is the position and size of gw2 window. This also includes a few hidden pixels around gw2 which serve as the border + /// Every time we check if the process is alive + window_pos_size: [i32; 4], + /// same as above. But we use DwmGetWindowAttribute, to exclude the drop shadow borders from the window rect + window_pos_size_without_borders: [i32; 4], + */ +} + +unsafe impl Send for MumbleWinImpl {} + +impl MumbleWinImpl { + pub fn new(key: &str) -> Result { + unsafe { + let (handle, link_ptr) = + create_link_shared_mem(key).wrap_err("failed to create mumblelink shm ")?; + let gw2_config_changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let gw2_config_path = { + let roaming_appdata_pwstr = SHGetKnownFolderPath( + &FOLDERID_RoamingAppData as *const _, + Default::default(), + HANDLE::default(), + ) + .into_diagnostic() + .wrap_err("failed to get known folder roaming app data path")?; + + let mut roaming_str = roaming_appdata_pwstr + .to_string() + .into_diagnostic() + .wrap_err("appdata/roaming is not a utf-8 path")?; + info!(roaming_str, "RoamingAppData path"); + CoTaskMemFree(Some(roaming_appdata_pwstr.0 as _)); + if !roaming_str.ends_with('\\') { + roaming_str.push('\\'); + } + roaming_str.push_str("Guild Wars 2\\GFXSettings.Gw2-64.exe.xml"); + info!(roaming_str, "gw2 config path"); + roaming_str + }; + let gw2_config_path = std::path::PathBuf::from_str(&gw2_config_path) + .into_diagnostic() + .wrap_err("failed to create pathbuf from gw2 config path in roaming appdata")?; + std::fs::create_dir_all(gw2_config_path.parent().unwrap()) + .into_diagnostic() + .wrap_err("failed to create gw2 config dir in appdata roaming ")?; + if !gw2_config_path.exists() { + std::fs::File::create(&gw2_config_path) + .into_diagnostic() + .wrap_err("failed to create empty gw2 config file ")?; + } + let dpi_scaling = check_dpi_scaling_enabled(&gw2_config_path); + + info!( + ?dpi_scaling, + ?gw2_config_path, + "dpi scaling when we are starting out" + ); + // lets just assume that the scaling is true by default + let dpi_scaling = dpi_scaling.unwrap_or(1); + gw2_config_changed.store(false, std::sync::atomic::Ordering::Relaxed); + let gw2_config_changed_2 = gw2_config_changed.clone(); + let mut gw2_config_watcher = notify::recommended_watcher(move |ev| { + debug!(?ev, "gw2 config changed"); + gw2_config_changed_2.store(true, std::sync::atomic::Ordering::Relaxed); + }) + .into_diagnostic() + .wrap_err("failed to create gw2 config directory watcher")?; + gw2_config_watcher + .watch(&gw2_config_path, notify::RecursiveMode::NonRecursive) + .into_diagnostic() + .wrap_err("faield to watch gw2 config dir")?; + + Ok(Self { + link_ptr, + mumble_handle: handle, + window_handle: 0, + last_ui_tick_update: Instant::now(), + previous_ui_tick: CMumbleLink::get_ui_tick(link_ptr), + // window_pos_size: [0; 4], + process_handle: HANDLE::default(), + previous_pid: 0, + xid: 0, + last_pos_size_check: Instant::now(), + // window_pos_size_without_borders: [0; 4], + dpi_scaling, + client_pos: [0; 2], + client_size: [0; 2], + dpi: 0, + _gw2_config_watcher: gw2_config_watcher, + gw2_config_changed, + gw2_config_path, + }) + } + } + pub fn is_alive(&self) -> bool { + !self.process_handle.is_invalid() + } + pub fn get_cmumble_link(&mut self) -> CMumbleLink { + let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; + link.context.timestamp = OffsetDateTime::now_utc() + .unix_timestamp_nanos() + .to_le_bytes(); + // link.context.window_pos_size = self.window_pos_size; + // link.context.window_pos_size_without_borders = self.window_pos_size_without_borders; + link.context.dpi_scaling = self.dpi_scaling; + link.context.dpi = self.dpi; + link.context.xid = self.xid; + link.context.client_pos = self.client_pos; + link.context.client_size = self.client_size; + link + } + /// This is the most important function which will be called every frame + /// 1. it gets the ui_tick from the link pointer + /// 2. checks if it has changed compared to previous ui_tick. If it didn't change, then we have nothing to do and we return. + /// 3. If it changed, we check if it is less than previous_ui_tick OR if the pid is differnet from previous_pid or if our process handle is invalid + /// 4. If any of the above conditions are true, we reset and reinitialize the gw2 process handle + window handle + window size etc.. + /// 5. If ui_tick simply increased and nothing else changed, then we proceed with the usual stuf which is check the timer and get updated window pos/size + pub fn tick(&mut self) -> Result<()> { + unsafe { + // if ui_tick is zero, we return + if !CMumbleLink::is_valid(self.link_ptr) { + // if we alive, that means ui_tick turned zero this frame for whatever reason, so we reset. + if self.is_alive() { + self.reset(); + } + return Ok(()); + } + let ui_tick = CMumbleLink::get_ui_tick(self.link_ptr); + let pid = CMumbleLink::get_pid(self.link_ptr); + let previous_ui_tick = self.previous_ui_tick; + // if ui tick didn't change. Then it means either we are in loading scree / character select screen or gw2 was closed (or crashed) + if ui_tick == previous_ui_tick { + // if we are not alive, then we just return because it just means mumble is not being updated. + // but if we are alive, then we need to check whehter gw2 is still alive (in loading screen) or dead + if self.is_alive() { + // we don't want to check every frame. Instead, we check in intervals of 3 seconds until gw2 finally loads into a map or it closes (so we can reset) + if self.last_ui_tick_update.elapsed() > Duration::from_secs(3) { + self.last_ui_tick_update = Instant::now(); + match check_process_alive(self.process_handle) { + Ok(alive) => { + if !alive { + self.reset(); + } + } + Err(e) => { + error!(?e, "failed to get GetExitCodeProcess"); + self.reset(); + } + } + } + } + return Ok(()); + } + // if ui_tick has changed, then we have some stuff to do. + if ui_tick < previous_ui_tick // only happens if process changes + || pid != self.previous_pid // gw2 process changed. need to get new handles/sizes etc.. + || !self.is_alive() + // if we are in reset status, then its our chance to reinitialize because mumble just updated. + { + info!(ui_tick, notify = 2u64, "found new gw2 process"); + self.reinitialize(); + } + // if reinitialization failed, then we can try again next frame. + // if we are alive, that means everything is working as expected. + // we update the previous ui_tick and check if we need to update window pos/size + if self.is_alive() { + self.last_ui_tick_update = Instant::now(); + self.previous_ui_tick = ui_tick; + // check in 2 seconds intervals because it rarely changes + if self.last_pos_size_check.elapsed() > Duration::from_secs(2) { + self.last_pos_size_check = Instant::now(); + + // self.window_pos_size = match get_window_pos_size(self.window_handle) { + // Ok(window_pos_size) => { + // if self.window_pos_size != window_pos_size { + // info!( + // ?self.window_pos_size, ?window_pos_size, + // "window position size changed" + // ); + // } + // window_pos_size + // } + // Err(e) => { + // error!(?e, "failed to get window position size"); + // self.reset(); // go back to being dead because it shouldn't usually fail + // return Ok(()); + // } + // }; + // let dpi_awareness = match GetProcessDpiAwareness(self.process_handle) { + // Ok(dpi) => dpi.0, + // Err(e) => { + // error!(?e, "failed to get dpi awareness"); + // 0 + // } + // }; + // if self.dpi_scaling != dpi_awareness { + // info!(dpi_awareness, self.dpi_scaling, "dpi scaling changed"); + // } + // self.dpi_scaling = dpi_awareness; + + let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; + if dpi != self.dpi { + info!(dpi, self.dpi, "dpi changed for gw2 window"); + } + if dpi == 0 { + error!(dpi, "invalid dpi value for guild wars 2"); + } + self.dpi = dpi; + // if the config changed, we will attempt to read dpi scaling. + // if we fail, we will just ignore it, and try again during next check of window pos (2 secs?) + // if we succeed, we will store false in the atomic bool + if self + .gw2_config_changed + .load(std::sync::atomic::Ordering::Relaxed) + { + match check_dpi_scaling_enabled(&self.gw2_config_path) { + Ok(dpi_scaling) => { + if self.dpi_scaling != dpi_scaling { + info!(self.dpi_scaling, dpi_scaling, "dpi scaling changed"); + } + self.dpi_scaling = dpi_scaling; + self.gw2_config_changed + .store(false, std::sync::atomic::Ordering::Relaxed); + } + Err(e) => { + error!(notify = 0.0f64, ?e, "failed to open gw2 config file to check for dpi scaling changes"); + } + } + } + // self.window_pos_size_without_borders = + // match get_window_pos_size_without_borders(HWND(self.window_handle)) { + // Ok(window_pos_size_without_borders) => { + // if self.window_pos_size_without_borders + // != window_pos_size_without_borders + // { + // info!( + // ?self.window_pos_size_without_borders, + // ?window_pos_size_without_borders, + // "window position size changed" + // ); + // } + // window_pos_size_without_borders + // } + // Err(e) => { + // error!(?e, "failed to get window position size"); + // self.reset(); // go back to being dead because it shouldn't usually fail + // return Ok(()); + // } + // }; + match get_client_rect_in_screen_coords(HWND(self.window_handle)) { + Ok((client_pos, client_size)) => { + if self.client_pos != client_pos || self.client_size != client_size { + info!( + ?self.client_pos, + ?client_pos, + ?self.client_size, + ?client_size, + "window position or size changed" + ); + } + self.client_pos = client_pos; + self.client_size = client_size; + } + Err(e) => { + error!(?e, "failed to get client position size"); + self.reset(); // go back to being dead because it shouldn't usually fail + return Ok(()); + } + }; + } + } + } + Ok(()) + } + /// A function which clears all the gw2 related resources like process/window handles + unsafe fn reset(&mut self) { + warn!("resetting mumble data"); + self.window_handle = 0; + if !self.process_handle.is_invalid() { + if let Err(e) = CloseHandle(self.process_handle) { + error!(?e, "failed to close process handle of old gw2"); + } + } + self.process_handle = HANDLE::default(); + // self.window_pos_size = [0; 4]; + // self.window_pos_size_without_borders = [0; 4]; + self.dpi = 0; + self.client_pos = [0; 2]; + self.client_size = [0; 2]; + self.previous_pid = 0; + self.xid = 0; + } + unsafe fn reinitialize(&mut self) { + warn!("we are reinitializing our mumble data"); + info!( + "printing cmumblelink as it might be useful for debugging. {:?}", + self.get_cmumble_link() + ); + assert!( + CMumbleLink::is_valid(self.link_ptr), + "attempting to reinitialize when mumble is still unintialized" + ); + let pid = CMumbleLink::get_pid(self.link_ptr); + assert!(pid != 0, "attempting to initialize with pid == 0"); + self.reset(); + info!( + "ui_tick: {}. pid: {pid}", + CMumbleLink::get_ui_tick(self.link_ptr) + ); + match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { + Ok(process_handle) => { + info!("got process handle: {process_handle:?}"); + // get pid from mumble link + let mut window_handle = pid as isize; + + // enumerate windows and get the handle and assign it to the pid variable if the process id of the handle actually matches the pid + let _ = EnumWindows( + Some(get_handle_by_pid), + LPARAM(((&mut window_handle) as *mut isize) as isize), + ); + // if lparam_pid is still the same as pid, then we couldn't find the relevant window handle + if window_handle == pid as isize { + if let Err(e) = CloseHandle(process_handle) { + error!( + ?e, + "failed to close process handle when we couldn't get window handle." + ); + } + error!( + "failed to initialize mumble data because we couldn't find window handle" + ); + return; + } + info!("found window handle too. yay"); + // now we have both process_handle and window_handle. We just need the window size to initialize our struct + // this function only gets the suface/viewport pos/size without any borders/decoraitons. + match get_client_rect_in_screen_coords(HWND(window_handle)) { + Ok((client_pos, client_size)) => { + // this block is purely for logging purposes only to verify that all sizes are working properly. + { + // GetWindowRect includes drop shadow borders and titlebar + match get_window_pos_size(window_handle) { + Ok(pos_size) => { + info!( + ?pos_size, + "get window position and size using GetWindowRect" + ); + } + Err(e) => { + error!(?e, "failed to initialize mumble data because we coudln't get window position and size"); + } + } + // DwmGetWindowAttribute doesn't include drop shadow borders, but includes titlebar + match get_window_pos_size_without_borders(HWND(window_handle)) { + Ok(window_pos_size_without_borders) => { + info!(?window_pos_size_without_borders, "got window pos/size without borders using DwmGetWindowAttribute"); + } + Err(e) => { + error!( + ?e, + "failed to get window position size without borders" + ); + } + }; + } + // only useful in wine + match std::ffi::CString::new("__wine_x11_whole_window") { + Ok(atom_string) => { + let xid = + GetPropA(HWND(window_handle), PCSTR(atom_string.as_ptr() as _)); + // check if the xid is actually null + if xid.is_invalid() { + // will happen on windows. But this is harmless + info!(?xid, "xid is invalid. This is completely fine on windows. This is only for linux users"); + } else { + info!("found xid too <3. {xid:?}"); + self.xid = xid + .0 + .try_into() + .map_err(|e| { + error!( + ?e, + ?xid, + "failed to fit x11 window id into u32" + ); + }) + .unwrap_or_default(); + } + } + Err(e) => { + error!(?e, notify = 0u64, "impossible. But __wine_x11_whole_window apparently not a valid cstring."); + } + } + // again, just for logging purposes and verify against lutris settings of dpi + let dpi_awareness = match GetProcessDpiAwareness(process_handle) { + Ok(dpi) => dpi.0, + Err(e) => { + error!(?e, "failed to get dpi awareness"); + 0 + } + }; + let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; + if dpi != self.dpi { + info!(dpi, self.dpi, "dpi changed for gw2 window"); + } + info!( + ?client_pos, + ?client_size, + dpi_awareness, + dpi, + pid, + ?process_handle, + ?window_handle, + "reinitialization complete " + ); + self.process_handle = process_handle; + self.window_handle = window_handle; + self.dpi = dpi; + self.client_pos = client_pos; + self.client_size = client_size; + self.last_ui_tick_update = Instant::now(); + self.previous_pid = pid; + } + Err(e) => { + error!(?e, "failed to get client rect"); + } + } + } + Err(e) => { + error!(?e, pid, "failed to open process handle"); + } + } + } +} + +fn check_dpi_scaling_enabled(path: &std::path::Path) -> Result { + // from $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml + // life is too short to parse an xml out of this file. just find the following strings + const DPI_SCALING_TRUE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="true"#; + const DPI_SCALING_FALSE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="false"#; + let contents = std::fs::read_to_string(path) + .into_diagnostic() + .wrap_err("failed to read gw2 file")?; + + if contents.contains(DPI_SCALING_FALSE) { + return Ok(0); + }; + if contents.contains(DPI_SCALING_TRUE) { + return Ok(1); + }; + error!(contents, "failed to read dpi scaling from gw2 config file"); + Ok(-1) +} +/// This function creates/opens the shared memory with the key as the name. +/// Then, it maps the shared memory into the address space of our process. +/// Finally, we are provided the Handle of shared memory and the pointer to the starting address of the mapped memory. +/// can fail if +/// 1. key is not a valid cstring +/// 2. creating shared memory fails +/// 3. mapping shared memory into our addres space fails and we get a null pointer instead +unsafe fn create_link_shared_mem(key: &str) -> Result<(HANDLE, *mut CMumbleLink)> { + info!("creating MumbleLink shared memory: {key}"); + // prepare the key as a cstr to pass to windows functions + let key_cstr = std::ffi::CString::new(key) + .into_diagnostic() + .wrap_err(miette::miette!("invalid mumble link name {key}"))?; + unsafe { + // create a Mumble Link shared memory file + // the file handle will need not be stored because when process exits, the handle will be dropped by windows + let file_handle = CreateFileMappingA( + INVALID_HANDLE_VALUE, + None, + PAGE_READWRITE, + 0, + C_MUMBLE_LINK_SIZE_FULL as u32 + 4096, // we add the size of description field here. + PCSTR(key_cstr.as_ptr() as _), + ) + .into_diagnostic() + .wrap_err("failed to create file mapping for MumbleLink")?; + // map the shared memory into the address space of our process using the handle we got from creating the shm + let cml_ptr = MapViewOfFile( + file_handle, + FILE_MAP_ALL_ACCESS, + 0, + 0, + C_MUMBLE_LINK_SIZE_FULL + 4096, // adding the description field size here + ) + .Value; + // check if we were successful + if cml_ptr.is_null() { + bail!( + "could not map view of file, error code: {:#?}", + GetLastError() + ) + } + Ok((file_handle, cml_ptr.cast())) + } +} + +unsafe fn check_process_alive(process_handle: HANDLE) -> Result { + let mut exit_code = 0u32; + GetExitCodeProcess(process_handle, &mut exit_code as *mut u32) + .into_diagnostic() + .wrap_err("failed to get exit code of process ")?; + Ok(exit_code == STATUS_PENDING.0 as u32) + + // this is slightly faster than using the GetExitCodeProcess method. + // GetExitCodeProcess takes around 3 us on average with lowest being 2.5 us. + // WaitForSingleObject takes around 2 us on average withe lowest being 1.5 us. + // let result = unsafe { WaitForSingleObject(process_handle, 0) }; + + // if result == WAIT_ABANDONED || result == WAIT_OBJECT_0 { + // Ok(false) + // } else if result == WAIT_TIMEOUT.0 { + // Ok(true) + // } else { + // bail!("WaitForSingleObject returned code: {:#?}", result) + // } +} +/// This function gets called by EnumWindows as a lambda function. it will be given a handle to all windows one by one, +/// and the pid of the process we want to match against that handle's pid. if handle's pid is matched against our pid, we will +/// assign the handle to our pid pointer so that the they can use it after EnumWindows returns +unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LPARAM) -> BOOL { + // gw2_pid is a long pointer TO a HWND. we cast gw2_pid from isize to a * mut isize. + let local_gw2_pid = *(gw2_pid_ptr.0 as *mut isize); + + // make a varible to hold the process id of a window handle given to us. + let mut window_handle_pid: u32 = 0; + // get the process id of the handle and then store it in the handle_pid variable. + GetWindowThreadProcessId(window_handle, Some((&mut window_handle_pid) as *mut u32)); + // if handle_pid is null, it means we failed to get the pid. so, we return true so that enumWindows can call us again with the handle to the next window. + if window_handle_pid == 0 { + info!("failed to get process id of window handle {window_handle:?}"); + return BOOL(1); + } + + info!("window handle {window_handle:?} has pid {window_handle_pid}"); + + // we check if the pid which gw2_pid references is equal to handle_pid + if local_gw2_pid == window_handle_pid as isize { + info!( + "successfully found the handle: {window_handle:?} of our gw2 with pid {local_gw2_pid}" + ); + // we now assign the window_handle to the memory pointed by gw2_pid pointer. + *(gw2_pid_ptr.0 as *mut isize) = window_handle.0; + return BOOL(0); + } + BOOL(1) +} +/// Quirk: GetWindowRect also includes the invisible "borders" which windows uses for resizing or whatever +/// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. +/// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). +/// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. +fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { + bail!("GetWindowRect call failed {e:#?}"); + } + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = DwmGetWindowAttribute( + window_handle, + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut RECT as _, + std::mem::size_of::() as _, + ) { + bail!("DwmGetWindowAttribute call failed {e:#?}"); + } + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { + unsafe { + let mut rect: RECT = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if let Err(e) = GetClientRect(window_handle, &mut rect as *mut RECT) { + bail!("GetClientRect call failed {e:#?}"); + } + let mut point: POINT = POINT { + x: rect.left, + y: rect.top, + }; + if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { + bail!("ClientToScreen call failed"); + } + let pos = [point.x, point.y]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) + } +} +impl Drop for MumbleWinImpl { + fn drop(&mut self) { + unsafe { + warn!("dropping mumble link windows impl"); + if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { + Value: self.link_ptr as _, + }) { + error!(?e, "failed to unmap view of mumble file"); + } + if let Err(e) = CloseHandle(self.mumble_handle) { + error!(?e, "failed to close handle of mumble link ") + } + if !self.process_handle.is_invalid() { + if let Err(e) = CloseHandle(self.process_handle) { + error!(?e, "failed to close handle of mumble link ") + } + } + } + } +} diff --git a/crates/joko_link_models/Cargo.toml b/crates/joko_link_models/Cargo.toml index 28c8a40..ca50e28 100644 --- a/crates/joko_link_models/Cargo.toml +++ b/crates/joko_link_models/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "lib"] [dependencies] joko_core = { path = "../joko_core" } -joko_components = { path = "../joko_components" } widestring = { version = "1", default-features = false, features = ["std"] } num-derive = { version = "0", default-features = false } num-traits = { version = "0", default-features = false } diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs index 2c055e8..5df2625 100644 --- a/crates/joko_link_models/src/lib.rs +++ b/crates/joko_link_models/src/lib.rs @@ -9,16 +9,8 @@ //! mod mumble; -use std::vec; -use enumflags2::BitFlags; -use joko_components::{JokolayComponent, JokolayComponentDeps}; -use joko_core::serde_glam::{IVec2, UVec2, Vec3}; -//use jokoapi::end_point::{mounts::Mount, races::Race}; -use miette::{IntoDiagnostic, Result, WrapErr}; pub use mumble::*; -use serde_json::from_str; -use tracing::error; pub enum MessageToMumbleLinkBack { BindedOnUI, diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml new file mode 100644 index 0000000..acbac60 --- /dev/null +++ b/crates/joko_package_manager/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "joko_package_manager" +version = "0.2.1" +edition = "2021" + +[dependencies] +# jmf deps +# for marker packs +base64 = "0.21.2" +bincode = { workspace = true } +bytemuck = { workspace = true } +cap-std = { workspace = true } +cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings +data-encoding = "2.4.0" +egui = { workspace = true } +enumflags2 = { workspace = true } +glam = { workspace = true } +image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +joko_component_models = { path = "../joko_component_models" } +joko_render_models = { path = "../joko_render_models" } +joko_package_models = { path = "../joko_package_models" } +jokoapi = { path = "../jokoapi" } +joko_link_models = { path = "../joko_link_models" } +miette = { workspace = true } +once = "0.3.4" +ordered_hash_map = { workspace = true } +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } +rayon = { workspace = true } +rfd = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +smol_str = { workspace = true } +time = { workspace = true , features = ["serde"]} +tokio = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } +zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) +walkdir = "2.5.0" + + + +[dev-dependencies] +# jmf deps +rstest = { version = "0", default-features = false } +# rstest_reuse = "0.3.0" +similar-asserts = "1" + + +[build-dependencies] +# for rapidxml +cxx-build = { version = "1" } diff --git a/crates/joko_package_manager/README.md b/crates/joko_package_manager/README.md new file mode 100644 index 0000000..cbbf8d6 --- /dev/null +++ b/crates/joko_package_manager/README.md @@ -0,0 +1,87 @@ + +## Status +still in early stages of development + + + + +### RapidXML Integration +Taco uses RapidXML, which is very very lenient in its parsing. +this led to marker packs not caring about their xml being valid xml. +Blish instead created a custom parsing library to deal with this and have workarounds for known issues. + +rapidxml does fix these issues itself when we roundtrip xml through it. so, we have a function called `rapid_filter` which takes in xml string and returns a "filtered" xml string that fixes a bunch of issues like escaping special characters like +ampersand, gt, lt etc.. with proper xml formatting i.e `&`, `>` etc.. + +Sources of rapidxml are in the vendor folder. it is a custom fork from https://github.com/timniederhausen/rapidxml which +added some fixes / enhancements. its stil a mess with compiler warnings, but whatever. + +we use cxxbridge crate. +`rapid.hpp` is our header with declaration for `rapid_filter` inside `rapid` namespace. (includes `joko_marker_format/src/lib.rs.h`) +`lib.rs` has extern declaration which has the same signature but in rust. (includes `joko_marker_format/vendor/rapid/rapid.hpp`) +`build.rs` has the compilation instructions. it uses `lib.rs` extern declaration, `rapid.cpp` as compilation unit as it + contains the definition of `rapid_filter` and finally outputs a `librapid.a` for linking. + +with this, we now filter the xml with `rapid_filter` before deserializing it in rust. if we still have errors we just +complain about it. + + + +### XML Marker Format +Marker Pack + +1. Textures + 1. identified by the relative path. case sensitive. But to accommodate case-insensitive MS windows packs, we will convert all paths to lowercase when importing. + 2. png format. + 3. need to convert to a srgba texture and upload to gpu to use it + 4. mostly tiny images. here's the composition of tekkit's pack textures + +| count | dimensions | +|-------|---------------| +| 630 | 100x100 | +| 7 | 150x150 | +| 89 | 200x200 | +| 683 | 250x250 | +| 42 | 256x256 | +| 435 | 500x500 | + +2. Tbins + 1. binary data of a series of vec3 positions. + mapid + a version (just ver 2 for now) + 2. need to generate a mesh to be usable to upload on gpu. different mesh for 2d map / minimap. trail_scale an affect width of the generated mesh + 3. anim_speed attr needs dynamic texture coords (probably based on time delta offset) + 4. color attribute requires blending. + 5. uses texture + 6. can be statically or dynamically filtered (culled). but no cooldowns. + +3. MarkerCategories + 1. create a tree structure of menu to be displayed. + 2. identified by their name (and parents in the hierarchy) as a unique path. + 3. can be enabled or disabled. need to persist this data in activation data or somewhere else. + 4. enabled / disabled categories act as dynamic filters for markers / trails. + 5. attributes get inherited by children unless overrided. and also inehrited by the markers / trails. + 6. can be enabled / disabled by a marker action (toggle_category attribute) +4. Markers + 1. render a quad. either billbaord or static rotation. + 2. needs texture + alpha attribute + color attribute for blending. + 3. alpha is also affected by fadenear and fadefar attributes. + 4. static filters like ingamevisibility or map visibility or minimap visibility. + 5. can display text via info / tip-description. + 6. dynamic filters like behavior + race + profession + specialization + mount + map type + category + festival + achievement. + 7. size is determined by texture + minSize / maxSize + scale. map quad rendering affected by scale on map and mapdisplaysize attribute + 8. triggers actions of behavior + copy-message (copy clipboard) + bounce?? + toggling category based on player proximity and pressing of a special action key (usually F) +5. Trails + 1. render the tbin mesh. + 2. same filters as marker + 3. no triggering / activation / cooldowns though. + + +3D: +1. can match blish +2. need to ignore certain attributes like minSize and maxSize. + + +2D: +1. can match taco +2. more performance because 2d? + + diff --git a/crates/joko_package_manager/build.rs b/crates/joko_package_manager/build.rs new file mode 100644 index 0000000..062e89b --- /dev/null +++ b/crates/joko_package_manager/build.rs @@ -0,0 +1,14 @@ +fn main() { + cxx_build::bridge("src/lib.rs") // our extern declaration in rust for rapid_filter + .file("vendor/rapid/rapid.cpp") // our compilation unit containing definition + .warnings(false) + .extra_warnings(false) + .compile("rapid"); // name of library = librapid.a + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=vendor/rapid/rapid.cpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapid.hpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapidxml.hpp"); + println!("cargo:rerun-if-changed=vendor/rapid/rapidxml_print.hpp"); + // shadow_rs::new().expect("failed to run shadow"); +} diff --git a/crates/joko_package_manager/images/marker.png b/crates/joko_package_manager/images/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..294a322e8475b221dbee3297868b719baa40f656 GIT binary patch literal 173015 zcmd3MRZ|>H(r6+1RAkYRiIAb7pwQ&yq|~9Hp#Oo;P>AsVI(?T4%l|M}by*3hnn{wQe+ZnF zxUx7D)UO1TR};WL9LY)Uiz^fqdf$HyddRWD913a;RbEP5)64MGAEAI$`tBk1TT8~m z%b4OtT4?^X;$bJfax@T2N>fVgJ(wObvgnpo?tDkpx^mLrnn9UnS(Tudl9{8_uYVL5_sBbkAk@AFs8=&JorjlDM6&YF1sXM@(R9 zHLd?gL=5C?4A3j7Y`r%M+lh_={%PxMeNOAqS~X~Mby9M6$J{Xa-(0BV0bxwrtJ}wc zXlW~R%4WJ_Eq;j?%nr|^oBTUysgx18ItW*!g9R{;=_Y5t?b#w1RqCBL6%_>tjqBh-is^Ny5=LW-dXOZ_XdBmKCn7Gx71DHmWCL_e8AfOK{h^y)_ULmzVR6``jIn?Pc|c#840(=pE9)SbCX}H z0#HXZq*5a(x~(;i|8TyB#-MXx;sXy+yq0pZt3LTzX!v5fEmoPi27UX3y4ePh=-%Ld zSnp^>cv!U%8-~a@{9@w#=b*?`Lb`3UpYh+u41S!#Onwno&)dM|eOrM*UxjB4B8DN|F0_Vb!lkBh;{x^ez-*kz!&3S5m#+KpLx%@a=iDtx5PrSZGB9K==ZC$UHG z?>Aum21QL$6RmytY0lyv*E0~gbHcmarSXEI^*kPyc)T0P8_2>PoeT?BtkZOL2yp)M zq4PH(p+DGtFzX8b+sEN_m8Zr8Lv5>F4&?Jss86P7%it01gR_4J0$-^2;T9JCcE>JL z6+bvX(dcljAKJP~w*pXLY`O{N=`tE{?IO~`ZD`%Zt%)(yq@aZ}4DsKNV>e;5nDz2p ziwKYc25da@;vAGNPtbg)d&SPW*XKpNd6tVhUnZNx#H}vXl3|^(SsIW8O|%|{CKswv zFQqG^Z7{Iwqg;pBmcy;v8<{%LlU-|p6(d3~{t|YV)zWMjt=@kCl)21yikhDH^dmJ2 zG{4*Vi@L5f)4pQ2K6Xr)4v$Y?@KfpVOexdEtfFd5y&FR88Gl1uMQ&bY== zbb+m@#F6|(mj*8`LtktUitLRBDi-N|j=SF#|U~y8WhV43yweF@i`p<+TyV z*i9o$W`v?Ur9|J>_0Ypqq8nG&`VJ=oR8qbdsFX z8c}0!nCjKuO2{*A&t7D4{ODEu`FA@5H$|nN^FoH{c#>ITQ*DzQ&B_nY@N+zmxq{(c zfq||35Lskmw7B^v3^Z+`KI%OmkvSVrT-6uUGL^h4OXuTX`l3%>#%BAD|1=nv`wW*J zW9aV_sN%C4Au={=I4BLwC;+Hl)#**cKEFX`&YhadA3$9PS{%$?8layIlsyxLa z)48Y3E)S~cSrZeQSBG&C@$uOgjq?ms5IAuqFO0`x>GGIr@BSwz1;i^t9L$ZTwU+7z^2St z4zyMnYTl{9Z_o{0N9DH~2oDKR0+~!3>@n1hnt>weX?0nLeT9 zWdM0UWI6m~{NRNymU<;+DOsNZ+d82?mIc$P_cV9=i!@Q9JJac|>ADdSUiO7WV?^u6lmOD?{$`7&sg@YS$^5Ob`5E zk~t&XSfAMO#O?m0R?*&9rQLyd$!d+;n*V8x^r$8+)G+kW@e z$-eL+Ufl#!P+4BEprH3va?;4aLXC6|ar-+C0apXF9I9{NiWWu-=eFD=sC<9N6)@!Z z7-G&sFp*@LuA&wB2!++ykY*uBXd*<;c;?zWlbtj=h@G&otFiu~ZJ4*wQtx;(y;jeW zBp+IMvk5Dr_*z0*O+|hgMfr~WEyQ8JBiZKeTTW784>s65<$F#DwQ%BBXhZT>d0;4H zKrGke>BoxXpt|TILFt?{a5MT8OlBm~!poHK-Q8nvtCID$!VK9wbeJ-R z?AWi$sW1?>_@3b>O!%#2)g@P*BqNFVEJ?9wG+(#F8+DZCRh{@N%0DmP2;sy3QD_xa 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%0D>~L^060CsHx=NV^<)QR_ycX_mn5bPDsp8-9 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�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 E19PVjH2?qr literal 0 HcmV?d00001 diff --git a/crates/joko_package_manager/images/trail_black.png b/crates/joko_package_manager/images/trail_black.png new file mode 100644 index 0000000000000000000000000000000000000000..d4326e68a6bdc79ef11a27f2deee592e3dbc4382 GIT binary patch literal 2293 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#QHWK~#8N?VU%6 zWknQ*$DA?eoE_=%sJa1|knwU2G`c6$D4`bAp<*yx2@UBEAsK zlRt`uVrQ|kuqqLQ|A6>Sj2iS|ZWDV6s{%3Y^W-!5Wci~8e3)MZdBTZeDPa{V627O! zOw;}-z7o%iqXjua^{OceWS@V4xKNPrZ6IbVasa#nzLyWg&*BeZWiGaH3^@Dz$y>d- zaEsVQkW5;cYj5$8U>i4TFvkI__+D{@urk)U;tBDC7&VY{;d}9%z${{ksZOQm0H=ww z#J+;HyKAi|FpD-7?~89lRc2AC0l=ESrMOHSC~)&aiQT~Ff*rz#g2eDIVI>p;e!pPv zK5FU?#jRp@VI|ZF0+WaBSzD8*D=?NG75pEnP-#oR1>ziWm{?83cI-$5*=}wpJ{KQ} ze?+k=B>-FL41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@ z;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LY ziI(MnCR2O~3P|_`#rOm{1^{dRd~vB@@I#tBU5UhSBf(6CS%g2~_OdZVw$FgG=JREa znsRf8D&lT}-^H4x$nqZ+JUzLt`7^)Kw`L-z$~JyoMldsIsmBZ;p$y2uy<2Bd|iuD@($uN z@v$JMu~{^vtp!y**3D7tX$_Fe+$k1_?38i_pqYbu{;7h!bgiw}{9d~X@&bAZ-WCK# z7D^QWv~gkd(9A($rPjf3gtvj2#f(Psx0B~sI27$lN)6i$J^s!6$U62zbQ>*~s zxE3dNjuA9c^j+A*6pt{A=rrbddXkwSMgUB8KELnE;(mgV0YK)vvA9CeFfqNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNw zI$slb-`3p3Hjz?tP`PcX=W+n_Tn>Pq%K?y|u!K2k7R4HN0OTj^;8zBp!4)C^e6?CZ z3K0NLiPtCoXpJErAP&ax8C)TZAZpyWT7V5q)yr<+%=$#UDxMYGdPZF!cpeaHV7Rvr zH;_Xf)VxF09HtW;Nc85@f6(Ulq!@$qmTLKWS#TvCByAGuLVV%C62su9U<<)i+V-~a z+Xa1!qyDZK@LhSP>`2`B%~kQUxmSSGJ6r&DoaoI!5P7xf6hYtNp@PCn+rgwcgQHwr z3du+Bs5p*iPI>oz+D=L8uDlZOUMwt6^d!_#-yFz7c6`t@)( zt;AyRN%&&k7EW%_TmWSy6w^LWK7-#?!q*n+^jrW{%{zxn%K=FESo66!nsqt0W5@JQ z>o&)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<` zi>iE~R4&>{knnYB@^l4H{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m z0f4hd!pGA^fhM=XZ`520$sLEytL2KRBIXzXtfw3;zg$q#9jB@nTUgNS47eJWAfSrq zOCiS;88&Nvskeo52^M)v5z1`_I9yyKnv0}zo%W~GeuAwc)x>xBLC(r408STN9(j}~ z`SM7*p~+b|-Ah*ySHG}($x^ul0Ok(2hw@2AUAmY7a4NYc$!5_sA^=RAPd2+>^0odY^5p#!N&1-%eac~x+i%TGqnnke!0F#G(KIiUydE`XB zgQgaiL-GOY2T9DL7y*F2i^InZz#3>xhyoIhC%FN$0OjQ9W>LrhAj)yW@bizC~KvM?`e{ P00000NkvXXu0mjf7Ck~> literal 0 HcmV?d00001 diff --git a/crates/joko_package_manager/images/trail_rainbow.png b/crates/joko_package_manager/images/trail_rainbow.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3ff6d305a60bb6a05c6418b6417c832ac14c16 GIT binary patch literal 16987 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 literal 0 HcmV?d00001 diff --git a/crates/joko_package_manager/src/io/deserialize.rs b/crates/joko_package_manager/src/io/deserialize.rs new file mode 100644 index 0000000..ba0401e --- /dev/null +++ b/crates/joko_package_manager/src/io/deserialize.rs @@ -0,0 +1,1610 @@ +use crate::BASE64_ENGINE; +use base64::Engine; +use cap_std::fs_utf8::{Dir, DirEntry}; +use joko_core::{serde_glam::Vec3, RelativePath}; +use joko_package_models::{ + attributes::{CommonAttributes, XotAttributeNameIDs}, + category::{prefix_parent, Category, RawCategory}, + marker::Marker, + package::{PackCore, PackageImportReport}, + route::Route, + trail::{TBin, TBinStatus, Trail}, +}; +use ordered_hash_map::OrderedHashMap; +use std::{collections::VecDeque, io::Read, str::FromStr}; +use tracing::{debug, error, info, info_span, instrument, trace, warn}; +use uuid::Uuid; +use xot::{Element, Node, Xot}; + +const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; + +pub(crate) fn load_pack_core_from_normalized_folder( + core_dir: &Dir, + import_report: Option, +) -> Result { + //called from already parsed data + let mut core_pack = PackCore::new(); + if let Some(mut import_report) = import_report { + import_report.reset_counters(); + import_report.uuid = core_pack.uuid; + core_pack.report = import_report; + } + // walks the directory and loads all files into the hashmap + let start = std::time::SystemTime::now(); + recursive_walk_dir_and_read_images_and_tbins( + core_dir, + &mut core_pack, + &RelativePath::default(), + ) + .or(Err("failed to walk dir when loading a markerpack"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of core package textures from disk took {} ms", + elaspsed.as_millis() + ); + + //categories are required to register other objects + let cats_xml = core_dir + .read_to_string("categories.xml") + .or(Err("failed to read categories.xml"))?; + let categories_file = String::from("categories.xml"); + let parse_categories_file_start = std::time::SystemTime::now(); + parse_categories_from_normalized_file(&categories_file, &cats_xml, &mut core_pack) + .or(Err("failed to parse category file"))?; + let elapsed = parse_categories_file_start.elapsed().unwrap_or_default(); + info!("parse_categories_file took {} ms", elapsed.as_millis()); + + // parse map data of the pack + for entry in core_dir + .entries() + .or(Err("failed to read entries of pack dir"))? + { + let dir_entry = entry.or(Err("entry error whiel reading xml files"))?; + + let name = dir_entry + .file_name() + .or(Err("map data entry name not utf-8"))? + .to_string(); + + if name.ends_with(".xml") { + if let Some(name_as_str) = name.strip_suffix(".xml") { + match name_as_str { + "categories" => { + //already done + } + file_name => { + // parse map file + let span_guard = info_span!("load file", file_name).entered(); + //let mut partial_pack = PackCore::partial(&core_pack.all_categories); + load_xml_from_normalized_file(file_name, &dir_entry, &mut core_pack)?; + //core_pack.merge_partial(partial_pack); + std::mem::drop(span_guard); + } + } + } + } else { + trace!("file ignored: {name}") + } + } + info!( + "Entities registered (category + markers): {}", + core_pack.entities_parents.len() + ); + info!("Categories registered: {}", core_pack.all_categories.len()); + info!( + "Markers registered: {}", + core_pack.entities_parents.len() - core_pack.all_categories.len() + ); + info!("Maps registered: {}", core_pack.maps.len()); + info!("Textures registered: {}", core_pack.textures.len()); + info!("Trail binaries registered: {}", core_pack.tbins.len()); + Ok(core_pack) +} + +fn recursive_walk_dir_and_read_images_and_tbins( + dir: &Dir, + pack: &mut PackCore, + parent_path: &RelativePath, +) -> Result<(), String> { + for entry in dir.entries().or(Err("failed to get directory entries"))? { + let entry = entry.or(Err("dir entry error when iterating dir entries"))?; + let name = entry.file_name().or(Err("No file name found"))?; + let path = parent_path.join_str(&name); + + if entry + .file_type() + .or(Err("failed to get file type"))? + .is_file() + { + if path.ends_with(".png") || path.ends_with(".trl") { + let mut bytes = vec![]; + entry + .open() + .or(Err("failed to open file"))? + .read_to_end(&mut bytes) + .or(Err("failed to read file contents"))?; + if name.ends_with(".png") { + pack.register_texture(name, &path, bytes); + } else if name.ends_with(".trl") { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + /*let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + }*/ + pack.tbins.insert(path, tbs.tbin); + } else { + info!("invalid tbin: {path}"); + } + } + } + } else { + recursive_walk_dir_and_read_images_and_tbins( + &entry.open_dir().or(Err("Could not open directory"))?, + pack, + &path, + )?; + } + } + Ok(()) +} +fn parse_tbin_from_slice(bytes: &[u8]) -> Option { + let content_length = bytes.len(); + // content_length must be atleast 8 to contain version + map_id + if content_length < 8 { + info!("failed to parse tbin because the len is less than 8"); + return None; + } + + let mut version_bytes = [0_u8; 4]; + version_bytes.copy_from_slice(&bytes[4..8]); + let version = u32::from_ne_bytes(version_bytes); + let mut map_id_bytes = [0_u8; 4]; + map_id_bytes.copy_from_slice(&bytes[4..8]); + let map_id = u32::from_ne_bytes(map_id_bytes); + + let zero = glam::Vec3 { + x: 0.0, + y: 0.0, + z: 0.0, + }; + + // this will either be empty vec or series of vec3s. + let nodes: VecDeque = bytes[8..] + .chunks_exact(12) + .map(|float_bytes| { + // make [f32 ;3] out of those 12 bytes + let arr = [ + f32::from_le_bytes([ + // first float + float_bytes[0], + float_bytes[1], + float_bytes[2], + float_bytes[3], + ]), + f32::from_le_bytes([ + // second float + float_bytes[4], + float_bytes[5], + float_bytes[6], + float_bytes[7], + ]), + f32::from_le_bytes([ + // third float + float_bytes[8], + float_bytes[9], + float_bytes[10], + float_bytes[11], + ]), + ]; + + glam::Vec3::from_array(arr) + }) + .collect(); + + //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. + let mut iso_x = false; + let mut iso_y = false; + let mut iso_z = false; + let mut closed = false; + let mut resulting_nodes: Vec = Vec::new(); + if !nodes.is_empty() { + //at least the first exist and can be accessed + let ref_node = nodes[0]; + let mut c_iso_x = true; + let mut c_iso_y = true; + let mut c_iso_z = true; + // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts + resulting_nodes.push(Vec3(ref_node)); + for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { + //ignore zeroes since they would be separators + if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { + let distance_to_next_point = a.distance_squared(*b); + let mut current_cursor = distance_to_next_point; + while current_cursor > MAX_TRAIL_CHUNK_LENGTH { + let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); + resulting_nodes.push(Vec3(c)); + current_cursor -= MAX_TRAIL_CHUNK_LENGTH; + } + } + resulting_nodes.push(Vec3(*b)); + } + for node in &nodes { + if resulting_nodes.len() > 1 { + //TODO: load epsilon from a configuration somewhere, with a default value + if (node.x - ref_node.x).abs() < 0.1 { + c_iso_x = false; + } + if (node.y - ref_node.y).abs() < 0.1 { + c_iso_y = false; + } + if (node.z - ref_node.z).abs() < 0.1 { + c_iso_z = false; + } + } + } + iso_x = c_iso_x; + iso_y = c_iso_y; + iso_z = c_iso_z; + if nodes.len() > 1 { + // TODO: get this threshold from configuration + closed = nodes + .front() + .unwrap() + .distance(*nodes.back().unwrap()) + .abs() + < 0.1 + } + } + Some(TBinStatus { + tbin: TBin { + map_id, + version, + nodes: resulting_nodes, + }, + iso_x, + iso_y, + iso_z, + closed, + }) +} + +fn parse_categories( + pack: &mut PackCore, + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut OrderedHashMap, + names: &XotAttributeNameIDs, + source_file_uuid: &Uuid, +) { + //called once per file + parse_categories_recursive( + pack, + tree, + tags, + first_pass_categories, + names, + None, + source_file_uuid, + ) +} + +// a recursive function to parse the marker category tree. +fn parse_categories_recursive( + pack: &mut PackCore, + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut OrderedHashMap, + names: &XotAttributeNameIDs, + parent_name: Option, + source_file_uuid: &Uuid, +) { + for tag in tags { + let ele = match tree.element(tag) { + Some(ele) => ele, + None => continue, + }; + if ele.name() != names.marker_category { + continue; + } + + let name = ele + .get_attribute(names.name) + .or(ele.get_attribute(names.capital_name)) + .unwrap_or_default() + .to_lowercase(); + if name.is_empty() { + continue; + } + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(ele, names); + let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); + + let separator = ele + .get_attribute(names.separator) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or_default(); + + let default_enabled = ele + .get_attribute(names.default_enabled) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or(true); + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, name) + } else { + name.to_string() + }; + let guid = parse_guid(names, ele); + trace!( + "recursive_marker_category_parser {} {} {:?}", + name, + guid, + parent_name + ); + if !first_pass_categories.contains_key(&full_category_name) { + let mut sources: OrderedHashMap = OrderedHashMap::new(); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_inherited_texture( + icon_file.as_str().to_string(), + full_category_name.clone(), + source_file_uuid, + ); + } + } + + sources.insert(guid, *source_file_uuid); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + guid, + parent_name: parent_name.clone(), + display_name: display_name.to_string(), + relative_category_name: name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: common_attributes, + sources, + }, + ); + } + parse_categories_recursive( + pack, + tree, + tree.children(tag), + first_pass_categories, + names, + Some(full_category_name), + source_file_uuid, + ); + } +} + +fn parse_categories_from_normalized_file( + file_name: &String, + cats_xml_str: &str, + pack: &mut PackCore, +) -> Result<(), String> { + let mut tree = xot::Xot::new(); + let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); + let root_node = tree.parse(cats_xml_str).or(Err("invalid xml"))?; + + let overlay_data_node = tree.document_element(root_node).or(Err("no doc element"))?; + + if let Some(od) = tree.element(overlay_data_node) { + let mut categories: OrderedHashMap = Default::default(); + if od.name() == xot_names.overlay_data { + parse_category_categories_xml_recursive( + file_name, + &tree, + tree.children(overlay_data_node), + &mut categories, + &xot_names, + None, + None, + )?; + trace!("loaded categories: {:?}", categories); + pack.categories = categories; + pack.register_categories(); + } else { + return Err("root tag is not OverlayData".to_string()); + } + } else { + return Err("doc element is not element???".to_string()); + } + Ok(()) +} + +fn load_xml_from_normalized_file( + file_name: &str, + dir_entry: &DirEntry, + target: &mut PackCore, +) -> Result<(), String> { + let mut xml_str = String::new(); + dir_entry + .open() + .or(Err("failed to open xml file"))? + .read_to_string(&mut xml_str) + .or(Err("failed to read xml string"))?; + //TODO: launch an async load of the file + make a priority queue to have current map first + parse_map_xml_string(file_name, &xml_str, target) + .or(Err(format!("error parsing file: {file_name}"))) +} + +fn parse_map_xml_string( + file_name: &str, + map_xml_str: &str, + target: &mut PackCore, +) -> Result<(), String> { + let mut tree = Xot::new(); + let root_node = tree.parse(map_xml_str).or(Err("invalid xml"))?; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let overlay_data_node = tree + .document_element(root_node) + .or(Err("missing doc element"))?; + + let overlay_data_element = tree.element(overlay_data_node).ok_or("no doc ele")?; + + if overlay_data_element.name() != names.overlay_data { + return Err("root tag is not OverlayData".to_string()); + } + let pois = tree + .children(overlay_data_node) + .find(|node| match tree.element(*node) { + Some(ele) => ele.name() == names.pois, + None => false, + }) + .ok_or("missing pois node")?; + + for poi_node in tree.children(pois) { + if let Some(child_element) = tree.element(poi_node) { + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + let span_guard = info_span!("category", full_category_name).entered(); + + let opt_source_file_uuid = Uuid::from_str( + child_element + .get_attribute(names._source_file_name) + .unwrap_or_default(), + ); + let source_file_uuid = if let Ok(uuid) = opt_source_file_uuid { + uuid + } else { + error!("Package corrupted, invalid source file uuid"); + //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); + Uuid::new_v4() + }; + + if let Some(source_file_name) = + target.report.source_file_uuid_to_name(&source_file_uuid) + { + let source_file_name = source_file_name.clone(); // this is to bypass borrow checker which has no idea this cannot be changed + target.register_source_file(&source_file_name); + } else { + println!("{:?}", source_file_uuid); + } + + //There is no file name, only an uuid to register + target.active_source_files.insert(source_file_uuid, true); + + if child_element.name() == names.route { + debug!("Found a route in core pack {:?}", child_element); + let route = parse_route( + &names, + &tree, + &poi_node, + child_element, + &full_category_name, + source_file_uuid, + ); + if let Some(route) = route { + target.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + panic!( + "full_category_name is empty {:?} {:?}", + map_xml_str, child_element + ); + } + let raw_uid = child_element.get_attribute(names.guid); + if raw_uid.is_none() { + info!( + "This POI is either invalid or inside a Route {:?}", + child_element + ); + span_guard.exit(); + continue; + } + //FIXME: this needs to be changed for partial load + let opt_cat_uuid = target.get_category_uuid(&full_category_name); + if opt_cat_uuid.is_none() { + error!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + file_name, child_element + ); + return Err(format!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + map_xml_str, child_element + )); + } + let category_uuid = opt_cat_uuid.unwrap(); //categories MUST exist, they have already been parsed + let guid = raw_uid + .and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + }) + .ok_or(format!("invalid guid {:?}", raw_uid))?; + + if child_element.name() == names.poi { + debug!("Found a POI in core pack {:?}", child_element); + let map_id = child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or("invalid mapid")?; + + let xpos = child_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .or(Err("invalid x position"))?; + let ypos = child_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .or(Err("invalid y position"))?; + let zpos = child_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .or(Err("invalid z position"))?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + let marker = Marker { + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), + map_id, + category: full_category_name.clone(), + parent: *category_uuid, + attrs: ca, + guid, + source_file_uuid, + }; + target.register_marker(full_category_name, marker)?; + } else if child_element.name() == names.trail { + debug!("Found a trail in core pack {:?}", child_element); + let map_id = child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or("invalid mapid")?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + let trail = Trail { + category: full_category_name.clone(), + parent: *category_uuid, + map_id, + props: ca, + guid, + dynamic: false, + source_file_uuid, + }; + target.register_trail(full_category_name, trail)?; + } + } + span_guard.exit(); + } + } + Ok(()) +} + +// a temporary recursive function to parse the marker category tree. +fn parse_category_categories_xml_recursive( + _file_name: &String, //meant for future implementation of source file definition for categories + tree: &Xot, + tags: impl Iterator, + cats: &mut OrderedHashMap, + names: &XotAttributeNameIDs, + parent_uuid: Option, + parent_name: Option, +) -> Result<(), String> { + for tag in tags { + if let Some(ele) = tree.element(tag) { + if ele.name() != names.marker_category { + continue; + } + + let relative_category_name = ele + .get_attribute(names.name) + .or(ele + .get_attribute(names.display_name) + .or(ele.get_attribute(names.capital_name))) + .unwrap_or_default() + .to_lowercase(); + if relative_category_name.is_empty() { + info!("category doesn't have a name attribute: {ele:#?}"); + continue; + } + let span_guard = info_span!("category", relative_category_name).entered(); + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(ele, names); + + let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); + + let separator = match ele.get_attribute(names.separator).unwrap_or("0") { + "0" => false, + "1" => true, + ors => { + info!("separator attribute has invalid value: {ors}"); + false + } + }; + + let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { + "0" => false, + "1" => true, + ors => { + info!("default_enabled attribute has invalid value: {ors}"); + true + } + }; + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, relative_category_name) + } else { + relative_category_name.to_string() + }; + let guid = parse_guid(names, ele); + trace!( + "recursive_marker_category_parser_categories_xml {} {} {:?}", + full_category_name, + guid, + parent_uuid + ); + if display_name.is_empty() { + if parent_name.is_some() { + return Err( + "Package is corrupted, please import it again with current version" + .to_string(), + ); + } + parse_category_categories_xml_recursive( + _file_name, + tree, + tree.children(tag), + cats, + names, + Some(guid), + Some(full_category_name), + )?; + } else { + let current_category = if let Some(c) = cats.get_mut(&guid) { + c + } else { + let c = Category { + guid, + parent: parent_uuid, + display_name: display_name.to_string(), + relative_category_name: relative_category_name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: ca, + children: Default::default(), + }; + cats.insert(guid, c); + cats.back_mut().unwrap() + }; + parse_category_categories_xml_recursive( + _file_name, + tree, + tree.children(tag), + &mut current_category.children, + names, + Some(guid), + Some(full_category_name), + )?; + }; + + std::mem::drop(span_guard); + } else { + //it may be a comment, a space, anything + //info!("In file {}, ignore node {:?}", file_name, tag); + } + } + Ok(()) +} + +pub(crate) fn get_pack_from_taco_zip( + input_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result { + let mut taco_zip = vec![]; + std::fs::File::open(input_path) + .or(Err("Could not open target folder"))? + .read_to_end(&mut taco_zip) + .or(Err("Could not read target folder"))?; + + let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) + .or(Err("failed to read zip archive"))?; + if extract_temporary_path.exists() { + std::fs::remove_dir_all(extract_temporary_path).or(Err("Could not purge target folder"))?; + } + zip_archive + .extract(extract_temporary_path) + .or(Err("Could not extract archive into target folder"))?; + + _get_pack_from_taco_folder(extract_temporary_path) +} + +/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. +/// will return error if there's an issue with zipfile. +/// +/// but any other errors like invalid attributes or missing markers etc.. will just be logged. +/// the intention is "best effort" parsing and not "validating" xml marker packs. +/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. + +#[instrument(skip_all)] +fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { + let mut pack = PackCore::new(); + + // file paths of different file types + let mut images = vec![]; + let mut tbins = vec![]; + let mut xmls = vec![]; + // we collect the names first, because reading a file from zip is a mutating operation. + // So, we can't iterate AND read the file at the same time + for entry in walkdir::WalkDir::new(package_path).into_iter() { + let entry = entry.or(Err("Could not walk directory"))?; + let path_as_string = entry + .path() + .strip_prefix(package_path) + .unwrap() + .to_str() + .unwrap() + .to_string(); + if path_as_string.ends_with(".png") { + images.push(path_as_string); + } else if path_as_string.ends_with(".trl") { + tbins.push(path_as_string); + } else if path_as_string.ends_with(".xml") { + xmls.push(path_as_string); + } else if path_as_string.replace('\\', "/").ends_with('/') { + // directory. so, we can silently ignore this. + } else { + //info!("ignoring file: {name}"); + } + } + xmls.sort(); //build back the intended order in folder, since zip_archive may not give the files in order. + let start_texture_loading = std::time::SystemTime::now(); + for file_path in images { + let span = info_span!("load image", file_path).entered(); + let relative_file_path: RelativePath = file_path.parse().unwrap(); + if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { + match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { + Ok(_) => { + pack.register_texture(file_path, &relative_file_path, bytes); + } + Err(e) => { + info!(?e, "failed to parse image file"); + } + } + } + std::mem::drop(span); + } + + for file_path in tbins { + let span = info_span!("load tbin", file_path).entered(); + let relative_path: RelativePath = file_path.parse().unwrap(); + if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + /*let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + }*/ + assert!( + pack.tbins.insert(relative_path, tbs.tbin).is_none(), + "duplicate tbin file {file_path}" + ); + } else { + info!("failed to parse tbin from slice: {relative_path}"); + } + } else { + info!(file_path, "failed to read tbin from zipfile"); + } + std::mem::drop(span); + } + let elapsed_texture_loading = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.texture_loading = elapsed_texture_loading.as_millis(); + tracing::info!( + "Loading of taco package textures from disk took {} ms", + elapsed_texture_loading.as_millis() + ); + + let span_guard_categories = info_span!("deserialize xml: categories").entered(); + let start_categories_loading = std::time::SystemTime::now(); + //first pass: categories only + let span_guard_first_pass = + info_span!("deserialize xml first pass: load MarkerCategory").entered(); + let mut first_pass_categories: OrderedHashMap = Default::default(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml first pass: load file", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + parse_categories( + &mut pack, + &tree, + tree.children(od), + &mut first_pass_categories, + &names, + &source_file_uuid, + ); + drop(span_guard); + } + span_guard_first_pass.exit(); + let elaspsed_first_pass = start_categories_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_first_pass = elaspsed_first_pass.as_millis(); + + //second pass: orphan categories + let span_guard_second_pass = + info_span!("deserialize xml second pass: orphan categories").entered(); + let start_categories_loading_second_pass = std::time::SystemTime::now(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml second pass: load file", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + debug!("missing overlay data tag"); + continue; + } + }; + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + debug!("missing pois tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let mut full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + if full_category_name.is_empty() { + if child_element.name() == names.route { + // If route, take the first element inside + if let Some(category) = + parse_route_category(&names, &tree, &child_node, child_element) + { + if category.is_empty() { + continue; + } + full_category_name = category; + } else { + continue; + } + } else { + continue; + } + } + let guid = parse_guid(&names, child_element); + if !pack.category_exists(&full_category_name) + && !first_pass_categories.contains_key(&full_category_name) + { + let category_uuid = Uuid::new_v4(); + let mut sources: OrderedHashMap = OrderedHashMap::new(); + sources.insert(guid, source_file_uuid); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + default_enabled: true, + guid: category_uuid, + parent_name: prefix_parent(&full_category_name, '.'), + display_name: full_category_name.clone(), + full_category_name: full_category_name.clone(), + relative_category_name: full_category_name.clone(), + props: Default::default(), + separator: false, + sources, + }, + ); + debug!( + "There is an orphan missing category '{}' which was created", + full_category_name + ); + } else { + let cat = first_pass_categories.get_mut(&full_category_name); + cat.unwrap().sources.insert(guid, source_file_uuid); + } + } + drop(span_guard); + } + span_guard_second_pass.exit(); + + let elaspsed_second_pass = start_categories_loading_second_pass + .elapsed() + .unwrap_or_default(); + pack.report.telemetry.categories_second_pass = elaspsed_second_pass.as_millis(); + + let start_categories_reassemble = std::time::SystemTime::now(); + pack.categories = Category::reassemble(&first_pass_categories, &mut pack.report); + let elaspsed_reassemble = start_categories_reassemble.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_reassemble.total = elaspsed_reassemble.as_millis(); + + let start_categories_registering = std::time::SystemTime::now(); + pack.register_categories(); + let elaspsed_categories_registering = + start_categories_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_registering = elaspsed_categories_registering.as_millis(); + + let elaspsed = start_categories_loading.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of taco package categories from disk took {} ms, {} + {} + {}", + elaspsed.as_millis(), + elaspsed_first_pass.as_millis(), + elaspsed_second_pass.as_millis(), + elaspsed_reassemble.as_millis(), + ); + + //third and last pass: elements + let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); + let start_elements_registering = std::time::SystemTime::now(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml third pass load file ", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + debug!("missing POIs tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + debug!("import element: {:?}", child_element); + if child_element.name() == names.route { + let route = parse_route( + &names, + &tree, + &child_node, + child_element, + &full_category_name, + source_file_uuid, + ); + if let Some(mut route) = route { + //one must not create category anymore + route.parent = *pack.get_category_uuid(&route.category).unwrap(); + pack.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + info!("full_category_name is empty {:?}", child_element); + continue; + } + if !pack.category_exists(&full_category_name) { + panic!( + "Missing category {}, previous pass should have taken care of this", + full_category_name + ); + } + let guid = parse_guid(&names, child_element); + let category_uuid = + pack.get_or_create_category_uuid(&full_category_name, guid, &source_file_uuid); + if child_element.name() == names.poi { + if let Some(marker) = parse_marker( + &mut pack, + &names, + child_element, + guid, + &full_category_name, + &category_uuid, + source_file_uuid, + ) { + pack.register_marker(full_category_name, marker)?; + } else { + debug!("Could not parse POI"); + } + } else if child_element.name() == names.trail { + if let Some(trail) = parse_trail( + &mut pack, + &names, + child_element, + guid, + &full_category_name, + &category_uuid, + source_file_uuid, + ) { + pack.register_trail(full_category_name, trail)?; + } else { + debug!("Could not parse Trail"); + } + } else { + info!("unknown element: {:?}", child_element); + } + } + } + + drop(span_guard); + } + span_guard_third_pass.exit(); + span_guard_categories.exit(); + let elaspsed_elements_registering = start_elements_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.elements_registering = elaspsed_elements_registering.as_millis(); + + let elapsed_import = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.total = elapsed_import.as_millis(); + Ok(pack) +} + +fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { + child.get_attribute(names.guid).and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + .or_else(|| { + info!(guid, "failed to deserialize guid"); + None + }) + }) +} +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid { + parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) +} + +fn parse_marker( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + poi_element: &Element, + guid: Uuid, + category_name: &str, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(poi_element, names); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_element_texture( + icon_file.as_str().to_string(), + guid, + &source_file_uuid, + ); + } + } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { + debug!(icf, "marker's icon file attribute failed to parse"); + pack.found_missing_element_texture(icf.to_string(), guid, &source_file_uuid); + } + + if let Some(map_id) = poi_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + let xpos = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let ypos = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let zpos = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Some(Marker { + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), + map_id, + category: category_name.to_owned(), + parent: *category_uuid, + attrs: common_attributes, + guid, + source_file_uuid, + }) + } else { + debug!("missing map id"); + None + } +} + +fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { + let x = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let y = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let z = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Vec3(glam::Vec3 { x, y, z }) +} + +fn parse_route_category( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, +) -> Option { + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + if let Some(cat) = child.get_attribute(names.category) { + return Some(cat.to_string()); + } + } + } + info!("Could not find a category for route element: {route_element:?}"); + None +} + +fn parse_route( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, + category_name: &str, + source_file_uuid: Uuid, +) -> Option { + let mut path: Vec = Vec::new(); + let resetposx = route_element + .get_attribute(names.resetposx) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposy = route_element + .get_attribute(names.resetposy) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposz = route_element + .get_attribute(names.resetposz) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let reset_position = glam::Vec3::new(resetposx, resetposy, resetposz); + let reset_range = route_element + .get_attribute(names.reset_range) + .and_then(|map_id| map_id.parse::().ok()); + let name = route_element + .get_attribute(names.name) + .or(route_element.get_attribute(names.capital_name)); + + if name.is_none() { + info!("route element is missing name: {route_element:?}"); + return None; + } + let mut category: String = category_name.to_owned(); + let mut category_uuid: Option = parse_optional_guid(names, route_element); + let mut map_id: Option = route_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()); + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + let marker = parse_position(names, child); + path.push(marker); + if category.is_empty() { + if let Some(cat) = child.get_attribute(names.category) { + category = cat.to_string(); + } + } + if category_uuid.is_none() { + category_uuid = parse_optional_guid(names, child) + } + if map_id.is_none() { + if let Some(node_map_id) = child + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + map_id = Some(node_map_id); + } + } + } + } + if category.is_empty() { + info!("Could not find a category for route element: {route_element:?}"); + return None; + } + if map_id.is_none() { + info!("Could not find a map_id for route element: {route_element:?}"); + return None; + } + if category_uuid.is_none() { + info!("Could not find a uuid for route element: {route_element:?}"); + return None; + } + debug!( + "found route with {:?} elements {route_element:?}", + path.len() + ); + + Some(Route { + category, + parent: category_uuid.unwrap(), + path, + reset_position: Vec3(reset_position), + reset_range: reset_range.unwrap_or(0.0), + map_id: map_id.unwrap(), + name: name.unwrap().into(), + guid: parse_guid(names, route_element), + source_file_uuid, + }) +} + +fn parse_trail( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + trail_element: &Element, + guid: Uuid, + category_name: &str, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { + //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html + + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(trail_element, names); + + if let Some(tex) = common_attributes.get_texture() { + if !pack.textures.contains_key(tex) { + info!(%tex, "failed to find this texture in this pack"); + pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_uuid); + } + } + + #[allow(clippy::manual_map)] + // This is not exactly a manual map, we register something more in pack on some condition: a missing trail. + if let Some(map_id) = trail_element + .get_attribute(names.trail_data) + .and_then(|trail_data| { + //fix the path which may be a mix of windows and linux path + let file_path: RelativePath = trail_data.parse().unwrap(); + if let Some(tb) = pack.tbins.get(&file_path) { + Some(tb.map_id) + } else { + pack.found_missing_trail(&file_path, guid, &source_file_uuid); + None + } + }) + { + Some(Trail { + category: category_name.to_owned(), + parent: *category_uuid, + map_id, + props: common_attributes, + guid, + dynamic: false, + source_file_uuid, + }) + } else { + /*let td = trail_element.get_attribute(names.trail_data); + let file_path: RelativePath = td.unwrap_or_default().parse().unwrap(); + //pack.report.found_orphan_trail(&file_path, guid, &source_file_name); + let tbin = pack.tbins.get(&file_path).map(|tbin| (tbin.map_id, tbin.version)); + info!("missing map_id: {td:?} {file_path} {tbin:?}"); + */ + None + } +} + +#[instrument(skip(zip_archive))] +fn read_file_bytes_from_zip_by_name( + name: &str, + zip_archive: &mut zip::ZipArchive, +) -> Option> { + let mut bytes = vec![]; + match zip_archive.by_name(name) { + Ok(mut file) => match file.read_to_end(&mut bytes) { + Ok(size) => { + if size == 0 { + info!("empty file {name}"); + } else { + return Some(bytes); + } + } + Err(e) => { + info!(?e, "failed to read file"); + } + }, + Err(e) => { + info!(?e, "failed to get file from zip"); + } + } + None +} + +// #[cfg(test)] +// mod test { + +// use indexmap::IndexMap; +// use rstest::*; + +// use semver::Version; +// use similar_asserts::assert_eq; +// use std::io::Write; +// use std::sync::Arc; + +// use zip::write::FileOptions; +// use zip::ZipWriter; + +// use crate::{ +// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, +// INCHES_PER_METER, +// }; + +// const TEST_XML: &str = include_str!("test.xml"); +// const TEST_MARKER_PNG_NAME: &str = "marker.png"; +// const TEST_TRL_NAME: &str = "basic.trl"; + +// #[fixture] +// #[once] +// fn test_zip() -> Vec { +// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); +// // category.xml +// writer +// .start_file("category.xml", FileOptions::default()) +// .expect("failed to create category.xml"); +// writer +// .write_all(TEST_XML.as_bytes()) +// .expect("failed to write category.xml"); +// // marker.png +// writer +// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) +// .expect("failed to create marker.png"); +// writer +// .write_all(MARKER_PNG) +// .expect("failed to write marker.png"); +// // basic.trl +// writer +// .start_file(TEST_TRL_NAME, FileOptions::default()) +// .expect("failed to create basic trail"); +// writer +// .write_all(&0u32.to_ne_bytes()) +// .expect("failed to write version"); +// writer +// .write_all(&15u32.to_ne_bytes()) +// .expect("failed to write mapid "); +// writer +// .write_all(bytemuck::cast_slice(&[0f32; 3])) +// .expect("failed to write first node"); +// // done +// writer +// .finish() +// .expect("failed to finalize zip") +// .into_inner() +// } + +// #[fixture] +// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { +// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); +// assert_eq!(file_entries.len(), 3); +// let test_xml = std::str::from_utf8( +// file_entries +// .get(String::new("category.xml")) +// .expect("failed to get category.xml"), +// ) +// .expect("failed to get str from category.xml contents"); +// assert_eq!(test_xml, TEST_XML); +// let test_marker_png = file_entries +// .get(String::new("marker.png")) +// .expect("failed to get marker.png"); +// assert_eq!(test_marker_png, MARKER_PNG); +// file_entries +// } +// #[fixture] +// #[once] +// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { +// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); +// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); +// assert_eq!(pack.tbins.len(), 1); +// assert_eq!(pack.textures.len(), 1); +// assert_eq!( +// pack.textures +// .get(String::new(TEST_MARKER_PNG_NAME)) +// .expect("failed to get marker.png from textures"), +// MARKER_PNG +// ); + +// let tbin = pack +// .tbins +// .get(String::new(TEST_TRL_NAME)) +// .expect("failed to get basic trail") +// .clone(); + +// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); +// pack +// } + +// // #[rstest] +// // fn test_tag(test_pack: &Pack) { +// // let mut test_category_menu = CategoryMenu::default(); +// // let parent_path = String::new("parent"); +// // let child1_path = String::new("parent/child1"); +// // let subchild_path = String::new("parent/child1/subchild"); +// // let child2_path = String::new("parent/child2"); +// // test_category_menu.create_category(subchild_path); +// // test_category_menu.create_category(child2_path); +// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); +// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); +// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); +// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); + +// // assert_eq!(test_category_menu, test_pack.category_menu) +// // } + +// #[rstest] +// fn test_markers(test_pack: &Pack) { +// let marker = test_pack +// .markers +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// marker.props.texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); +// } +// #[rstest] +// fn test_trails(test_pack: &Pack) { +// let trail = test_pack +// .trails +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// trail.props.tbin.as_ref().unwrap(), +// String::new(TEST_TRL_NAME) +// ); +// assert_eq!( +// trail.props.trail_texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// } +// } diff --git a/crates/joko_package_manager/src/io/error.rs b/crates/joko_package_manager/src/io/error.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/joko_package_manager/src/io/error.rs @@ -0,0 +1 @@ + diff --git a/crates/joko_package_manager/src/io/export.rs b/crates/joko_package_manager/src/io/export.rs new file mode 100644 index 0000000..73fc2f2 --- /dev/null +++ b/crates/joko_package_manager/src/io/export.rs @@ -0,0 +1,264 @@ +use crate::{ + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::Dir; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, + route::Route, trail::Trail, +}; +use miette::{Context, IntoDiagnostic, Result}; +use ordered_hash_map::OrderedHashMap; +use std::io::Write; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +pub(crate) fn export_package_v2( + pack: &PackCore, + writing_directory: &Dir, + name: String, +) -> Result<()> { + Ok(()) +} + +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn export_package_v1( + pack_data: &LoadedPackData, + pack_textures: &LoadedPackData, + writing_directory: &Dir, +) -> Result<()> { + // save categories + info!( + "Saving data pack {}, {} categories, {} maps", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len() + ); + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create new root with overlay data node")?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) + .wrap_err("failed to serialize cats")?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to convert cats xot to string")?; + writing_directory + .create("categories.xml") + .into_diagnostic() + .wrap_err("failed to create categories.xml")? + .write_all(cats.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write to categories.xml")?; + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create root wiht overlay data for pois")?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .into_diagnostic() + .wrap_err("faild to append pois to od node")?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .into_diagnostic() + .wrap_err("failed to append poi (marker) to pois")?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .into_diagnostic() + .wrap_err("failed to append a trail node to pois")?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to serialize map data to string")?; + writing_directory + .create(format!("{map_id}.xml")) + .into_diagnostic() + .wrap_err("failed to create map xml file")? + .write_all(map_xml.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write map data to file")?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<()> { + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + writing_directory + .create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir for an image: {img_path}") + })?; + } + writing_directory + .create(img_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .write(img) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + writing_directory + .create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir of tbin: {tbin_path}") + })?; + } + let mut bytes: Vec = vec![]; + bytes.reserve(8 + tbin.nodes.len() * 12); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + writing_directory + .create(tbin_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .write_all(&bytes) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &OrderedHashMap, + parent: Node, +) -> Result<()> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node).into_diagnostic()?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.xpos, format!("{}", marker.position[0])); + ele.set_attribute(names.ypos, format!("{}", marker.position[1])); + ele.set_attribute(names.zpos, format!("{}", marker.position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<()> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .into_diagnostic() + .wrap_err("failed to append route to pois")?; + let ele = tree.element_mut(route_node).unwrap(); + + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); + for pos in &route.path { + let child = tree.new_element(names.poi); + tree.append(route_node, child); + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} diff --git a/crates/joko_package_manager/src/io/mod.rs b/crates/joko_package_manager/src/io/mod.rs new file mode 100644 index 0000000..310a6bb --- /dev/null +++ b/crates/joko_package_manager/src/io/mod.rs @@ -0,0 +1,9 @@ +//! This modules primarily deals with serializing and deserializing xml data from marker packs +//! + +mod deserialize; +mod error; +mod serialize; + +pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_normalized_folder}; +pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; diff --git a/crates/joko_package_manager/src/io/serialize.rs b/crates/joko_package_manager/src/io/serialize.rs new file mode 100644 index 0000000..edd838a --- /dev/null +++ b/crates/joko_package_manager/src/io/serialize.rs @@ -0,0 +1,240 @@ +use crate::{ + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::Dir; +use glam::Vec3; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, +}; +use miette::Result; +use ordered_hash_map::OrderedHashMap; +use std::io::Write; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn save_pack_data_to_dir( + pack_data: &LoadedPackData, + writing_directory: &Dir, +) -> Result<(), String> { + // save categories + info!( + "Saving data pack {}, {} categories, {} maps", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len() + ); + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .or(Err("failed to create new root with overlay data node"))?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od)?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .or(Err("failed to convert cats xot to string"))?; + writing_directory + .create("categories.xml") + .or(Err("failed to create categories.xml"))? + .write_all(cats.as_bytes()) + .or(Err("failed to write to categories.xml"))?; + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .or(Err("failed to create root wiht overlay data for pois"))?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .or(Err("faild to append pois to od node"))?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .or(Err("failed to append poi (marker) to pois"))?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .or(Err("failed to append a trail node to pois"))?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .or(Err("failed to serialize map data to string"))?; + writing_directory + .create(format!("{map_id}.xml")) + .or(Err("failed to create map xml file"))? + .write_all(map_xml.as_bytes()) + .or(Err("failed to write map data to file"))?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<(), String> { + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + writing_directory.create_dir_all(parent).or(Err(format!( + "failed to create parent dir for an image: {img_path}" + )))?; + } + let amount = writing_directory + .create(img_path.as_str()) + .or(Err(format!("failed to create file for image: {img_path}")))? + .write(img); + if amount.is_err() { + return Err(format!("failed to write image bytes to file: {img_path}")); + } + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + writing_directory.create_dir_all(parent).or(Err(format!( + "failed to create parent dir of tbin: {tbin_path}" + )))?; + } + let mut bytes: Vec = + Vec::with_capacity(8 + tbin.nodes.len() * std::mem::size_of::()); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + let node = &node.0; + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + writing_directory + .create(tbin_path.as_str()) + .or(Err(format!("failed to create tbin file: {tbin_path}")))? + .write_all(&bytes) + .or(Err(format!("failed to write tbin to path: {tbin_path}")))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &OrderedHashMap, + parent: Node, +) -> Result<(), String> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node) + .or(Err("Could not insert category node"))?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + let position = &marker.position.0; + ele.set_attribute(names.xpos, format!("{}", position[0])); + ele.set_attribute(names.ypos, format!("{}", position[1])); + ele.set_attribute(names.zpos, format!("{}", position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<(), String> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .or(Err("failed to append route to pois"))?; + let ele = tree.element_mut(route_node).unwrap(); + + let reset_position = &route.reset_position.0; + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); + for pos in &route.path { + let pos = &pos.0; + let child = tree.new_element(names.poi); + tree.append(route_node, child) + .or(Err("Could not inser child node"))?; + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} diff --git a/crates/joko_package_manager/src/io/test.xml b/crates/joko_package_manager/src/io/test.xml new file mode 100644 index 0000000..3b50657 --- /dev/null +++ b/crates/joko_package_manager/src/io/test.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/joko_package_manager/src/io/xmlfile_schema.xsd b/crates/joko_package_manager/src/io/xmlfile_schema.xsd new file mode 100644 index 0000000..895a0ac --- /dev/null +++ b/crates/joko_package_manager/src/io/xmlfile_schema.xsd @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/joko_package_manager/src/lib.rs b/crates/joko_package_manager/src/lib.rs new file mode 100644 index 0000000..d6e90e7 --- /dev/null +++ b/crates/joko_package_manager/src/lib.rs @@ -0,0 +1,41 @@ +//! ReadOnly XML marker packs support for Jokolay +//! +//! + +pub(crate) mod io; +pub(crate) mod manager; +pub mod message; + +pub use manager::{ + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, load_all_from_dir, ImportStatus, LoadedPackData, LoadedPackTexture, + PackageDataManager, PackageUIManager, +}; + +// for compile time build info like pkg version or build timestamp or git hash etc.. +// shadow_rs::shadow!(build); + +// to filter the xml with rapidxml first +#[cxx::bridge(namespace = "rapid")] +mod ffi { + unsafe extern "C++" { + include!("joko_package_manager/vendor/rapid/rapid.hpp"); + pub fn rapid_filter(src_xml: String) -> String; + + } +} + +pub fn rapid_filter_rust(src_xml: String) -> String { + ffi::rapid_filter(src_xml) +} + +pub const INCHES_PER_METER: f32 = 39.37; + +pub fn is_default(t: &T) -> bool { + t == &T::default() +} + +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); diff --git a/crates/joko_package_manager/src/manager/mod.rs b/crates/joko_package_manager/src/manager/mod.rs new file mode 100644 index 0000000..8063da8 --- /dev/null +++ b/crates/joko_package_manager/src/manager/mod.rs @@ -0,0 +1,29 @@ +//! How should the pack be stored by jokolay? +//! 1. Inside a directory called packs, we will have a separate directory for each pack. +//! 2. the name of the directory will serve as an ID for each pack. +//! 3. Inside the directory, we will have +//! 1. categories.xml -> The xml file which contains the whole category tree +//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. +//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. + +/* +expensive: +categories being a tree with order among siblings (better to use a tree crate?) +markers/trails referring to a category via full path. +editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. + +We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. + +*/ + +mod pack; +mod package_data; +mod package_ui; + +pub use pack::import::{import_pack_from_zip_file_path, ImportStatus}; +pub use pack::loaded::{ + build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, load_all_from_dir, + LoadedPackData, LoadedPackTexture, +}; +pub use package_data::PackageDataManager; +pub use package_ui::PackageUIManager; diff --git a/crates/joko_package_manager/src/manager/pack/activation.rs b/crates/joko_package_manager/src/manager/pack/activation.rs new file mode 100644 index 0000000..71f76ff --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/activation.rs @@ -0,0 +1,20 @@ +use indexmap::IndexMap; +use uuid::Uuid; + +/// This is the activation data per pack +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct ActivationData { + /// this is for markers which are global and only activate once regardless of account + pub global: IndexMap, + /// this is the activation data per character + /// for markers which trigger once per character + pub character: IndexMap>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ActivationType { + /// clean these up when the map is changed + ReappearOnMapChange, + /// clean these up when the timestamp is reached + TimeStamp(time::OffsetDateTime), + Instance(std::net::IpAddr), +} diff --git a/crates/joko_package_manager/src/manager/pack/active.rs b/crates/joko_package_manager/src/manager/pack/active.rs new file mode 100644 index 0000000..03ed04d --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/active.rs @@ -0,0 +1,302 @@ +use joko_package_models::attributes::CommonAttributes; +use jokoapi::end_point::mounts::Mount; +use ordered_hash_map::OrderedHashMap; + +use egui::TextureHandle; +use indexmap::IndexMap; +use uuid::Uuid; + +use crate::INCHES_PER_METER; +use joko_core::{ + serde_glam::{Vec2, Vec3}, + RelativePath, +}; +use joko_link_models::MumbleLink; +use joko_render_models::{ + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject, +}; + +/* +- activation data with uuids and track the latest timestamp that will be activated +- category activation data -> track and changes to propagate to markers of this map +- current active markers, which will keep track of their original marker, so as to propagate any changes easily +*/ +#[derive(Clone)] +pub struct ActiveTrail { + pub trail_object: TrailObject, + pub texture_handle: TextureHandle, +} +/// This is an active marker. +/// It stores all the info that we need to scan every frame +#[derive(Clone)] +pub(crate) struct ActiveMarker { + /// texture id from managed textures + pub texture_id: u64, + /// owned texture handle to keep it alive + pub _texture: TextureHandle, + /// position + pub pos: Vec3, + /// billboard must not be bigger than this size in pixels + pub max_pixel_size: f32, + /// billboard must not be smaller than this size in pixels + pub min_pixel_size: f32, + pub common_attributes: CommonAttributes, +} + +pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0; // in game metric, for GW2, inches + +impl ActiveMarker { + pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { + let Self { + texture_id, + pos, + common_attributes: attrs, + _texture, + max_pixel_size, + min_pixel_size, + .. + } = self; + // let width = *width; + // let height = *height; + let texture_id = *texture_id; + let pos = *pos; + // filters + if let Some(mounts) = attrs.get_mount() { + if let Some(current) = Mount::try_from_mumble_link(link.mount) { + if !mounts.contains(current) { + return None; + } + } else { + return None; + } + } + let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; + let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); + let player_distance = pos.0.distance(link.player_pos.0); + let camera_distance = pos.0.distance(link.cam_pos.0); + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); + + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let color = attrs.get_color().copied().unwrap_or_default(); + /* + 1. we need to filter the markers + 1. statically - mapid, character, map_type, race, profession + 2. dynamically - achievement, behavior, mount, fade_far, cull + 3. force hide/show by user discretion + 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior + 3. store the state for these markers activation data, and temporary data like bounce + */ + /* + skip if: + alpha is 0.0 + achievement id/bit is done (maybe this should be at map filter level?) + behavior (activation) + cull + distance > fade_far + visibility (ingame/map/minimap) + mount + specialization + */ + if fade_far > 0.0 && player_distance > fade_far { + return None; + } + // markers are 1 meter in width/height by default + let mut pos = pos.0; + pos.y += height_offset; + let direction_to_marker = link.cam_pos.0 - pos; + let direction_to_side = direction_to_marker.normalize().cross(glam::Vec3::Y); + + let far_offset = { + let dpi = if link.dpi_scaling <= 0 { + 96.0 + } else { + link.dpi as f32 + } / 96.0; + let gw2_width = link.client_size.0.as_vec2().x / dpi; + + // offset (half width i.e. distance from center of the marker to the side of the marker) + const SIDE_OFFSET_FAR: f32 = 1.0; + // the size of the projected on to the near plane + let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); + // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width + let near_offset_in_pixels = near_offset * gw2_width; + + // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width + let near_offset_in_pixels = near_offset_in_pixels + .clamp(*min_pixel_size, *max_pixel_size) + .min(gw2_width / 2.0); + + let near_offset_of_marker = near_offset_in_pixels / gw2_width; + near_offset_of_marker * camera_distance / z_near + }; + // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; + // we want to map 100 pixels to one meter in game + // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard + // But, i will ignore that as that makes markers too small + let x_offset = far_offset; + let y_offset = x_offset; // seems all markers are squares + let bottom_left = MarkerVertex { + position: Vec3(pos - (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 1.0)), + alpha, + color, + fade_near_far, + }; + + let top_left = MarkerVertex { + position: Vec3(pos - (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 0.0)), + alpha, + color, + fade_near_far, + }; + let top_right = MarkerVertex { + position: Vec3(pos + (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 0.0)), + alpha, + color, + fade_near_far, + }; + let bottom_right = MarkerVertex { + position: Vec3(pos + (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 1.0)), + alpha, + color, + fade_near_far, + }; + let vertices = [ + top_left, + bottom_left, + bottom_right, + bottom_right, + top_right, + top_left, + ]; + Some(MarkerObject { + vertices, + texture: texture_id, + distance: player_distance, + }) + } +} + +impl ActiveTrail { + pub fn get_vertices_and_texture( + attrs: &CommonAttributes, + positions: &[Vec3], + texture: TextureHandle, + ) -> Option { + // can't have a trail without atleast two nodes + if positions.len() < 2 { + return None; + } + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); + let color = attrs.get_color().copied().unwrap_or([0u8; 4]); + // default taco width + let horizontal_offset = 20.0 / INCHES_PER_METER; + // scale it trail scale + let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); + let height = horizontal_offset * 2.0; + + let mut vertices = vec![]; + // trail mesh is split by separating different parts with a [0, 0, 0] + // we will call each separate trail mesh as a "strip" of trail. + // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. + for strip in positions.split(|&v| v.0 == glam::Vec3::ZERO) { + let mut y_offset = 1.0; + for two_positions in strip.windows(2) { + let first = two_positions[0].0; + let second = two_positions[1].0; + // right side of the vector from first to second + let right_side = (second - first) + .normalize() + .cross(glam::Vec3::Y) + .normalize(); + + let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; + let first_left = MarkerVertex { + position: Vec3(first - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, y_offset)), + alpha, + color, + fade_near_far, + }; + let first_right = MarkerVertex { + position: Vec3(first + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, y_offset)), + alpha, + color, + fade_near_far, + }; + let second_left = MarkerVertex { + position: Vec3(second - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, new_offset)), + alpha, + color, + fade_near_far, + }; + let second_right = MarkerVertex { + position: Vec3(second + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, new_offset)), + alpha, + color, + fade_near_far, + }; + y_offset = if new_offset.is_sign_positive() { + new_offset + } else { + 1.0 - new_offset.fract().abs() + }; + vertices.extend([ + second_left, + first_left, + first_right, + first_right, + second_right, + second_left, + ]); + } + } + + Some(ActiveTrail { + trail_object: TrailObject { + vertices: vertices.into(), + texture: match texture.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }, + }, + texture_handle: texture, + }) + } +} + +#[derive(Default, Clone)] +pub(crate) struct CurrentMapData { + /// the map to which the current map data belongs to + //pub map_id: u32, + //pub active_elements: HashSet, + /// The textures that are being used by the markers, so must be kept alive by this hashmap + pub active_textures: OrderedHashMap, + /// The key is the index of the marker in the map markers + /// Their position in the map markers serves as their "id" as uuids can be duplicates. + pub active_markers: IndexMap, + pub wip_markers: IndexMap, + /// The key is the position/index of this trail in the map trails. same as markers + pub active_trails: IndexMap, + pub wip_trails: IndexMap, +} diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs new file mode 100644 index 0000000..b2d141e --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -0,0 +1,301 @@ +use joko_component_models::ComponentDataExchange; +use joko_package_models::{ + attributes::CommonAttributes, + category::Category, + package::{PackCore, PackageImportReport}, +}; +use ordered_hash_map::OrderedHashMap; +use std::collections::{HashMap, HashSet}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::message::MessageToPackageBack; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct CategorySelection { + //#[serde(skip)] + pub uuid: Uuid, //FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information + #[serde(skip)] + pub parent: Option, + pub is_selected: bool, //has it been selected in configuration to be displayed + pub is_active: bool, //currently being displayed (i.e.: active) + pub separator: bool, + pub display_name: String, + pub children: OrderedHashMap, +} + +pub struct SelectedCategoryManager { + data: OrderedHashMap, +} +impl<'a> SelectedCategoryManager { + pub fn new( + selected_categories: &OrderedHashMap, + categories: &OrderedHashMap, + ) -> Self { + let mut list_of_enabled_categories = Default::default(); + CategorySelection::get_list_of_enabled_categories( + selected_categories, + categories, + &mut list_of_enabled_categories, + &Default::default(), + ); + + Self { + data: list_of_enabled_categories, + } + } + #[allow(dead_code)] + pub fn cloned_data(&self) -> OrderedHashMap { + self.data.clone() + } + pub fn is_selected(&self, category: &Uuid) -> bool { + self.data.contains_key(category) + } + pub fn get(&self, key: &Uuid) -> &CommonAttributes { + self.data.get(key).unwrap() + } + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.data.len() + } + pub fn keys(&'a self) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { + self.data.keys() + } +} + +impl CategorySelection { + pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { + let mut selectable_categories = OrderedHashMap::new(); + Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); + selectable_categories + } + fn get_list_of_enabled_categories( + selection: &OrderedHashMap, + categories: &OrderedHashMap, + list_of_enabled_categories: &mut OrderedHashMap, + parent_common_attributes: &CommonAttributes, + ) { + for (_, cat) in categories { + if let Some(selectable_category) = selection.get(&cat.relative_category_name) { + if !selectable_category.is_selected { + continue; + } + let mut common_attributes = cat.props.clone(); + common_attributes.inherit_if_attr_none(parent_common_attributes); + Self::get_list_of_enabled_categories( + &selectable_category.children, + &cat.children, + list_of_enabled_categories, + &common_attributes, + ); + list_of_enabled_categories.insert(cat.guid, common_attributes); + } + } + } + pub fn get( + selection: &mut OrderedHashMap, + uuid: Uuid, + ) -> Option<&mut CategorySelection> { + if selection.is_empty() { + None + } else { + for cat in selection.values_mut() { + if cat.uuid == uuid { + return Some(cat); + } + if let Some(res) = Self::get(&mut cat.children, uuid) { + return Some(res); + } + } + None + } + } + #[allow(dead_code)] + pub fn recursive_populate_guids( + selection: &mut OrderedHashMap, + entities_parents: &mut HashMap, + parent_uuid: Option, + ) { + for cat in selection.values_mut() { + if cat.uuid.is_nil() { + cat.uuid = Uuid::new_v4(); + } + cat.parent = parent_uuid; + Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); + if let Some(parent_uuid) = parent_uuid { + entities_parents.insert(cat.uuid, parent_uuid); + } + //assert!(cat.guid.len() > 0); + } + } + fn recursive_create_selectable_categories( + selectable_categories: &mut OrderedHashMap, + cats: &OrderedHashMap, + ) { + for (_, cat) in cats.iter() { + if !selectable_categories.contains_key(&cat.relative_category_name) { + let to_insert = CategorySelection { + uuid: cat.guid, + parent: cat.parent, + is_selected: cat.default_enabled, + is_active: !cat.separator, //by default separators are not considered active since they contain nothing + separator: cat.separator, + display_name: cat.display_name.clone(), + children: Default::default(), + }; + //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); + selectable_categories.insert(cat.relative_category_name.clone(), to_insert); + } + let s = selectable_categories + .get_mut(&cat.relative_category_name) + .unwrap(); + Self::recursive_create_selectable_categories(&mut s.children, &cat.children); + } + } + + pub fn recursive_set( + selection: &mut OrderedHashMap, + uuid: Uuid, + status: bool, + ) -> bool { + if selection.is_empty() { + false + } else { + for cat in selection.values_mut() { + if cat.separator { + continue; + } + if cat.uuid == uuid { + cat.is_selected = status; + return true; + } + if Self::recursive_set(&mut cat.children, uuid, status) { + return true; + } + } + false + } + } + pub fn recursive_set_all( + selection: &mut OrderedHashMap, + status: bool, + ) { + if selection.is_empty() { + return; + } + for cat in selection.values_mut() { + if cat.separator { + continue; + } + cat.is_selected = status; + Self::recursive_set_all(&mut cat.children, status); + } + } + + pub fn recursive_update_active_categories( + selection: &mut OrderedHashMap, + active_elements: &HashSet, + ) -> bool { + let mut is_active = false; + if selection.is_empty() { + //println!("recursive_update_active_categories is_empty"); + return is_active; + } + for cat in selection.values_mut() { + cat.is_active = active_elements.contains(&cat.uuid) + || Self::recursive_update_active_categories(&mut cat.children, active_elements); + if cat.is_active { + is_active = true; + } + } + is_active + } + + fn context_menu( + u2b_sender: &tokio::sync::mpsc::Sender, + cs: &mut CategorySelection, + ui: &mut egui::Ui, + ) { + if ui.button("Activate branch").clicked() { + cs.is_selected = true; + CategorySelection::recursive_set_all(&mut cs.children, true); + let _ = u2b_sender.blocking_send( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true).into(), + ); + ui.close_menu(); + } + if ui.button("Deactivate branch").clicked() { + CategorySelection::recursive_set_all(&mut cs.children, false); + cs.is_selected = false; + let _ = u2b_sender.blocking_send( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false).into(), + ); + ui.close_menu(); + } + } + + pub fn recursive_selection_ui( + u2b_sender: &tokio::sync::mpsc::Sender, + _u2u_sender: &tokio::sync::mpsc::Sender, + selection: &mut OrderedHashMap, + ui: &mut egui::Ui, + is_dirty: &mut bool, + show_only_active: bool, + import_quality_report: &PackageImportReport, + ) { + if selection.is_empty() { + return; + } + egui::ScrollArea::vertical().show(ui, |ui| { + for cat in selection.values_mut() { + if !cat.is_active && show_only_active && !cat.separator { + continue; + } + ui.horizontal(|ui| { + if cat.separator { + ui.add_space(3.0); + } else { + let cb = ui.checkbox(&mut cat.is_selected, ""); + if cb.changed() { + let _ = u2b_sender.blocking_send( + MessageToPackageBack::CategoryActivationElementStatusChange( + cat.uuid, + cat.is_selected, + ) + .into(), + ); + *is_dirty = true; + } + } + //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); + let color = if import_quality_report.is_category_discovered_late(cat.uuid) { + egui::Color32::LIGHT_RED + } else if cat.is_active { + egui::Color32::LIGHT_GREEN + } else { + egui::Color32::GRAY + }; + let label = egui::RichText::new(&cat.display_name).color(color); + if cat.children.is_empty() { + ui.label(label); + } else { + ui.menu_button(label, |ui: &mut egui::Ui| { + Self::recursive_selection_ui( + u2b_sender, + _u2u_sender, + &mut cat.children, + ui, + is_dirty, + show_only_active, + import_quality_report, + ); + }) + .response + .context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); + } + }); + } + }); + } +} diff --git a/crates/joko_package_manager/src/manager/pack/dirty.rs b/crates/joko_package_manager/src/manager/pack/dirty.rs new file mode 100644 index 0000000..f7fd509 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/dirty.rs @@ -0,0 +1,28 @@ +use ordered_hash_map::OrderedHashSet; + +use joko_core::RelativePath; + +#[derive(Debug, Default, Clone)] +pub(crate) struct DirtyMarker { + pub all: bool, + /// whether categories need to be saved + pub categories: bool, + /// whether selected categories needs to be saved + pub selected_categories: bool, + /// Whether any mapdata needs saving + pub map: OrderedHashSet, + /// whether any texture needs saving + pub texture: OrderedHashSet, + /// whether any tbin needs saving + pub tbin: OrderedHashSet, +} + +impl DirtyMarker { + pub fn is_dirty(&self) -> bool { + self.categories + || self.selected_categories + || !self.map.is_empty() + || !self.texture.is_empty() + || !self.tbin.is_empty() + } +} diff --git a/crates/joko_package_manager/src/manager/pack/entry.rs b/crates/joko_package_manager/src/manager/pack/entry.rs new file mode 100644 index 0000000..ad78681 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/entry.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub struct PackEntry { + pub url: url::Url, + pub description: String, +} + diff --git a/crates/joko_package_manager/src/manager/pack/file_selection.rs b/crates/joko_package_manager/src/manager/pack/file_selection.rs new file mode 100644 index 0000000..c3ddc03 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/file_selection.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeMap; + +use uuid::Uuid; + +pub struct SelectedFileManager { + data: BTreeMap, +} +impl SelectedFileManager { + pub fn new( + selected_files: &BTreeMap, + pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + ) -> Self { + let mut list_of_enabled_files: BTreeMap = Default::default(); + SelectedFileManager::recursive_get_full_names( + selected_files, + pack_source_files, + currently_used_files, + &mut list_of_enabled_files, + ); + Self { + data: list_of_enabled_files, + } + } + fn recursive_get_full_names( + _selected_files: &BTreeMap, + _pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + list_of_enabled_files: &mut BTreeMap, + ) { + for (key, v) in currently_used_files.iter() { + list_of_enabled_files.insert(*key, *v); + } + } + #[allow(dead_code)] + pub fn cloned_data(&self) -> BTreeMap { + self.data.clone() + } + pub fn is_selected(&self, source_file_uuid: &Uuid) -> bool { + let default = false; + self.data.is_empty() || *self.data.get(source_file_uuid).unwrap_or(&default) + } + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.data.len() + } +} diff --git a/crates/joko_package_manager/src/manager/pack/import.rs b/crates/joko_package_manager/src/manager/pack/import.rs new file mode 100644 index 0000000..0234a46 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/import.rs @@ -0,0 +1,29 @@ +use joko_package_models::package::PackCore; +use tracing::info; + +#[derive(Debug, Default)] +pub enum ImportStatus { + #[default] + UnInitialized, + WaitingForFileChooser, + LoadingPack(std::path::PathBuf), + WaitingLoading(std::path::PathBuf), + PackDone(String, PackCore, bool), + PackError(miette::Report), +} + +pub fn import_pack_from_zip_file_path( + file_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result<(String, PackCore), String> { + info!("starting to get pack from taco"); + crate::io::get_pack_from_taco_zip(file_path.clone(), extract_temporary_path).map(|pack| { + ( + file_path + .file_name() + .map(|ostr| ostr.to_string_lossy().to_string()) + .unwrap_or_default(), + pack, + ) + }) +} diff --git a/crates/joko_package_manager/src/manager/pack/list.rs b/crates/joko_package_manager/src/manager/pack/list.rs new file mode 100644 index 0000000..499fe2f --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/list.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Default)] +pub struct PackList { + pub packs: BTreeMap, +} + + diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs new file mode 100644 index 0000000..33b2ee9 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -0,0 +1,1016 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + path::PathBuf, + sync::Arc, +}; + +use joko_component_models::ComponentDataExchange; +use joko_package_models::{ + attributes::{Behavior, CommonAttributes}, + category::Category, + map::MapData, + package::{PackCore, PackageImportReport}, + trail::TBin, +}; +use ordered_hash_map::OrderedHashMap; + +use cap_std::fs_utf8::Dir; +use egui::{ColorImage, TextureHandle}; +use image::EncodableLayout; +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, info, info_span, trace}; +use uuid::Uuid; + +use crate::message::MessageToPackageUI; +use crate::{ + io::{load_pack_core_from_normalized_folder, save_pack_data_to_dir, save_pack_texture_to_dir}, + manager::{ + pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, + package_data::EXTRACT_DIRECTORY_NAME, + }, + message::MessageToPackageBack, +}; +use joko_core::{ + serde_glam::Vec3, + task::{AsyncTask, AsyncTaskGuard}, + RelativePath, +}; +use joko_link_models::MumbleLink; +use joko_render_models::{messages::UIToUIMessage, trail::TrailObject}; +use miette::{Context, IntoDiagnostic, Result}; + +use super::activation::{ActivationData, ActivationType}; +use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; +use crate::manager::pack::category_selection::CategorySelection; +use crate::manager::package_data::{ + EDITABLE_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME, PACKAGES_DIRECTORY_NAME, + PACKAGE_MANAGER_DIRECTORY_NAME, +}; + +type ImportAllTriplet = ( + BTreeMap, + BTreeMap, + BTreeMap, +); +type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); + +//TODO: separate in front and back tasks +pub(crate) struct PackTasks { + //an object that can handle such tasks should be passed as argument of any function that may required an async action + save_texture_task: AsyncTask>, + save_data_task: AsyncTask>, + save_report_task: AsyncTask<(Arc

, PackageImportReport), Result<(), String>>, + load_all_packs_task: + AsyncTask<(Arc, std::path::PathBuf), Result>, +} + +//TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? +#[derive(Clone)] +pub struct LoadedPackData { + pub name: String, + pub uuid: Uuid, + pub dir: Arc, + /// The actual xml pack. + //pub core: PackCore, + pub categories: OrderedHashMap, + pub all_categories: HashMap, + pub source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, + selected_files: BTreeMap, + _is_dirty: bool, //there was an edition in the package itself + + // loca copy in the data side of what is exposed in UI + selectable_categories: OrderedHashMap, + pub entities_parents: HashMap, + activation_data: ActivationData, + active_elements: HashSet, //keep track of which elements are active +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct LoadedPackTexture { + //TODO: there is a need for a late loading of texture to avoid transmitting them (serialize) + pub name: String, + pub uuid: Uuid, + /// The directory inside which the pack data is stored + /// There should be a subdirectory called `core` which stores the pack core + /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. + /// eg: Active categories, activation data etc.. + //pub dir: Arc, + pub path: std::path::PathBuf, + pub source_files: BTreeMap, + pub tbins: HashMap, + pub textures: HashMap>, + + /// The selection of categories which are "enabled" and markers belonging to these may be rendered + selectable_categories: OrderedHashMap, + #[serde(skip)] + current_map_data: CurrentMapData, + activation_data: ActivationData, + //active_elements: HashSet, //which are the active elements (loaded) + _is_dirty: bool, +} + +impl PackTasks { + pub fn new() -> Self { + Self { + save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), + save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + save_report_task: AsyncTaskGuard::new(PackTasks::async_save_report), + load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), + } + } + pub fn is_running(&self) -> bool { + self.save_texture_task.lock().unwrap().is_running() + || self.save_data_task.lock().unwrap().is_running() + } + pub fn count(&self) -> i32 { + self.save_texture_task.lock().unwrap().count() + + self.save_data_task.lock().unwrap().count() + + self.load_all_packs_task.lock().unwrap().count() + } + + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { + //saved on load, or change of list of what to display + if status { + std::mem::take(&mut texture_pack._is_dirty); + let _ = self + .save_texture_task + .lock() + .unwrap() + .send(texture_pack.clone()); + } + } + + pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { + if status { + std::mem::take(&mut data_pack._is_dirty); + let _ = self.save_data_task.lock().unwrap().send(data_pack.clone()); + } + } + pub fn save_report(&self, target_dir: Arc, report: PackageImportReport, status: bool) { + if status { + let _ = self + .save_report_task + .lock() + .unwrap() + .send((target_dir, report)); + } + } + pub fn load_all_packs(&self, jokolay_dir: Arc, root_path: std::path::PathBuf) { + let _ = self + .load_all_packs_task + .lock() + .unwrap() + .send((jokolay_dir, root_path)); + } + pub fn wait_for_load_all_packs(&self) -> Result { + self.load_all_packs_task.lock().unwrap().recv().unwrap() + } + + #[allow(dead_code, unused)] + fn change_map( + &self, + pack: &mut LoadedPackData, + b2u_sender: &std::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + ) { + //TODO + unimplemented!("PackTask::change_map is not implemented"); + } + + fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<(), String> { + trace!("Save texture package {:?}", pack_texture.path); + let std_file = std::fs::OpenOptions::new() + .open(&pack_texture.path) + .or(Err("Could not open file"))?; + let dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + match serde_json::to_string_pretty(&pack_texture.selectable_categories) { + Ok(cs_json) => match dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + match serde_json::to_string_pretty(&pack_texture.activation_data) { + Ok(ad_json) => match dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { + Ok(_) => { + debug!("wrote activation to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write activation data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } + let writing_directory = dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .or(Err("failed to open core pack directory"))?; + save_pack_texture_to_dir(&pack_texture, &writing_directory)?; + Ok(()) + } + + fn async_save_data(pack_data: LoadedPackData) -> Result<(), String> { + trace!("Save data package {:?}", pack_data.dir); + pack_data + .dir + .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) + .or(Err("failed to create xmlpack directory"))?; + let writing_directory = pack_data + .dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .or(Err("failed to open core pack directory"))?; + save_pack_data_to_dir(&pack_data, &writing_directory)?; + Ok(()) + } + + fn async_save_report(input: (Arc, PackageImportReport)) -> Result<(), String> { + let (writing_directory, report) = input; + trace!("Save report package {:?}", writing_directory); + match serde_json::to_string_pretty(&report) { + Ok(cs_json) => { + match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote import quality report to disk"); + } + Err(e) => { + debug!(?e, "failed to write import quality report to disk"); + } + } + } + Err(e) => { + error!(?e, "failed to serialize import quality report"); + } + } + Ok(()) + } +} + +impl LoadedPackData { + const CORE_PACK_DIR_NAME: &'static str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; + + fn load_selectable_categories( + pack_dir: &Arc, + pack: &PackCore, + ) -> OrderedHashMap { + //FIXME: we need to patch those categories from the one in the files + (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { + match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize category data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_else(|| { + let cs = CategorySelection::default_from_pack_core(pack); + match serde_json::to_string_pretty(&cs) { + Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { + Ok(_) => { + debug!("wrote cat selections to disk after creating a default from pack"); + } + Err(e) => { + debug!(?e, "failed to write category data to disk"); + } + }, + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + cs + }) + } + + fn load_import_report(pack_dir: &Arc) -> Option { + //FIXME: we need to patch those categories from the one in the files + (if pack_dir.is_file(PackageImportReport::REPORT_FILE_NAME) { + match pack_dir.read_to_string(PackageImportReport::REPORT_FILE_NAME) { + Ok(cd_json) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize import report"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of import report"); + None + } + } + } else { + None + }) + .flatten() + } + pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { + if !pack_dir + .try_exists(Self::CORE_PACK_DIR_NAME) + .or(Err("failed to check if pack core exists"))? + { + return Err("pack core doesn't exist in this pack".to_string()); + } + let core_dir = pack_dir + .open_dir(Self::CORE_PACK_DIR_NAME) + .or(Err("failed to open core pack directory"))?; + let start = std::time::SystemTime::now(); + let import_report = LoadedPackData::load_import_report(&pack_dir); + let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + .or(Err("failed to load pack from dir"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); + + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. + //let selectable_categories = CategorySelection::default_from_pack_core(&core); + let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); + + Ok(LoadedPackData { + name, + uuid: core.uuid, + dir: pack_dir, + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.active_source_files, + _is_dirty: false, + active_elements: Default::default(), + activation_data: Default::default(), + selectable_categories, + entities_parents: core.entities_parents, + }) + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { + if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { + self._is_dirty = true; + true + } else { + false + } + } + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { + if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { + cs.is_selected = status; + self._is_dirty = true; + if CategorySelection::recursive_set(&mut cs.children, uuid, status) { + return true; + } + } + false + } + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn tick( + &mut self, + b2u_sender: &tokio::sync::mpsc::Sender, + _loop_index: u128, + link: &MumbleLink, + currently_used_files: &BTreeMap, + list_of_active_or_selected_elements_changed: bool, + map_changed: bool, + _tasks: &PackTasks, + next_loaded: &mut HashSet, + ) { + //since the loading of texture is lazy, there is no problem when calling this regularly + if map_changed || list_of_active_or_selected_elements_changed { + //tasks.change_map(self, b2u_sender, link, currently_used_files); + let mut active_elements: HashSet = Default::default(); + self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); + let _ = b2u_sender.blocking_send( + MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()) + .into(), + ); + self.active_elements = active_elements.clone(); + next_loaded.extend(active_elements); + } + } + + fn on_map_changed( + &mut self, + b2u_sender: &tokio::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + active_elements: &mut HashSet, + ) { + info!(link.map_id, "current map data is updated. {}", self.name); + if link.map_id == 0 { + info!("No map do not do anything"); + return; + } + debug!( + "Start building SelectedCategoryManager {}", + self.selectable_categories.len() + ); + let selected_categories_manager = + SelectedCategoryManager::new(&self.selectable_categories, &self.categories); + + debug!("Start building SelectedFileManager"); + let selected_files_manager = SelectedFileManager::new( + &self.selected_files, + &self.source_files, + currently_used_files, + ); + + debug!("Start loading markers"); + let mut nb_markers_attempt = 0; + let mut nb_markers_loaded = 0; + for marker in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .markers + .values() + { + nb_markers_attempt += 1; + if selected_files_manager.is_selected(&marker.source_file_uuid) { + active_elements.insert(marker.guid); + active_elements.insert(marker.parent); + if selected_categories_manager.is_selected(&marker.parent) { + let category_attributes = selected_categories_manager.get(&marker.parent); + let mut common_attributes = marker.attrs.clone(); // why a clone ? + common_attributes.inherit_if_attr_none(category_attributes); + let key = &marker.guid; + if let Some(behavior) = common_attributes.get_behavior() { + if match behavior { + Behavior::AlwaysVisible => false, + Behavior::ReappearOnMapChange + | Behavior::ReappearOnDailyReset + | Behavior::OnlyVisibleBeforeActivation + | Behavior::ReappearAfterTimer + | Behavior::ReappearOnMapReset + | Behavior::WeeklyReset => { + self.activation_data.global.contains_key(key) + } + Behavior::OncePerInstance => self + .activation_data + .global + .get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default(), + Behavior::DailyPerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| a.contains_key(key)) + .unwrap_or_default(), + Behavior::OncePerInstancePerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| { + a.get(key) + .map(|a| match a { + ActivationType::Instance(a) => { + a == &link.server_address + } + _ => false, + }) + .unwrap_or_default() + }) + .unwrap_or_default(), + Behavior::WvWObjective => { + false // ??? + } + } { + continue; + } + } + if let Some(tex_path) = common_attributes.get_icon_file() { + let _ = b2u_sender.blocking_send( + MessageToPackageUI::MarkerTexture( + self.uuid, + tex_path.clone(), + marker.guid, + marker.position, + common_attributes, + ) + .into(), + ); + } else { + debug!("no texture attribute on this marker"); + } + + nb_markers_loaded += 1; + } else { + debug!( + "category {} = {} is not enabled", + marker.category, marker.parent + ); + } + } + } + + debug!("Start loading trails"); + let mut nb_trails_attempt = 0; + let mut nb_trails_loaded = 0; + for trail in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .trails + .values() + { + nb_trails_attempt += 1; + if selected_files_manager.is_selected(&trail.source_file_uuid) { + active_elements.insert(trail.guid); + active_elements.insert(trail.parent); + if selected_categories_manager.is_selected(&trail.parent) { + let category_attributes = selected_categories_manager.get(&trail.parent); + let mut common_attributes = trail.props.clone(); + common_attributes.inherit_if_attr_none(category_attributes); + if let Some(tex_path) = common_attributes.get_texture() { + let _ = b2u_sender.blocking_send( + MessageToPackageUI::TrailTexture( + self.uuid, + tex_path.clone(), + trail.guid, + common_attributes, + ) + .into(), + ); + } else { + debug!("no texture attribute on this trail"); + } + nb_trails_loaded += 1; + } else { + debug!( + "category {} = {} is not enabled", + trail.category, trail.parent + ); + } + } + } + info!( + "Load notifications for {} on map {}: {}/{} markers and {}/{} trails", + self.name, + link.map_id, + nb_markers_loaded, + nb_markers_attempt, + nb_trails_loaded, + nb_trails_attempt + ); + debug!( + "active categories: {:?}", + selected_categories_manager.keys() + ); + } +} + +impl LoadedPackTexture { + const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; + + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + CategorySelection::recursive_update_active_categories( + &mut self.selectable_categories, + active_elements, + ); + } + pub fn category_sub_menu( + &mut self, + u2b_sender: &tokio::sync::mpsc::Sender, + u2u_sender: &tokio::sync::mpsc::Sender, + ui: &mut egui::Ui, + show_only_active: bool, + import_quality_report: &PackageImportReport, + ) { + //it is important to generate a new id each time to avoid collision + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + u2b_sender, + u2u_sender, + &mut self.selectable_categories, + ui, + &mut self._is_dirty, + show_only_active, + import_quality_report, + ); + }); + if self._is_dirty { + let _ = u2b_sender + .blocking_send(MessageToPackageBack::CategoryActivationStatusChanged.into()); + } + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub(crate) fn tick( + &mut self, + u2u_sender: &tokio::sync::mpsc::Sender, + _timestamp: f64, + link: &MumbleLink, + //next_on_screen: &mut HashSet, + z_near: f32, + _tasks: &PackTasks, + ) -> Result<()> { + tracing::trace!( + "LoadedPackTexture.tick: {} {}-{} {}-{}", + self.name, + self.current_map_data.active_markers.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.active_trails.len(), + self.current_map_data.wip_trails.len(), + ); + let mut marker_objects = Vec::new(); + for marker in self.current_map_data.active_markers.values() { + if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { + marker_objects.push(mo); + } + } + tracing::trace!( + "LoadedPackTexture.tick: {}, markers {}", + self.name, + marker_objects.len() + ); + let _ = u2u_sender.blocking_send(UIToUIMessage::BulkMarkerObject(marker_objects).into()); + let mut trail_objects = Vec::new(); + for trail in self.current_map_data.active_trails.values() { + trail_objects.push(TrailObject { + vertices: trail.trail_object.vertices.clone(), + texture: trail.trail_object.texture, + }); + //next_on_screen.insert(*uuid); + } + tracing::trace!( + "LoadedPackTexture.tick: {}, trails {}", + self.name, + trail_objects.len() + ); + let _ = u2u_sender.blocking_send(UIToUIMessage::BulkTrailObject(trail_objects).into()); + Ok(()) + } + + pub fn swap(&mut self) { + info!( + "swap {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.wip_trails.len() + ); + self.current_map_data.active_markers = + std::mem::take(&mut self.current_map_data.wip_markers); + self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); + } + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + + //TODO: insertion must happen inside the UI => egui_context should never be transmitted on a tick() + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + error!(%tex_path, "failed to find this icon texture"); + } + } + let th = self + .current_map_data + .active_textures + .get(tex_path) + .unwrap_or(default_tex_id); + let texture_id = match th.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }; + + let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size + let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size + let am = ActiveMarker { + texture_id, + _texture: th.clone(), + common_attributes, + pos: position, + max_pixel_size, + min_pixel_size, + }; + self.current_map_data.wip_markers.insert(marker_uuid, am); + } + + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + error!(%tex_path, "failed to find this trail texture"); + } + } else { + trace!("Trail texture already loaded {:?}", tex_path); + } + let texture_path = common_attributes.get_texture(); + let th = texture_path + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_tex_id); + + let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); + tbin + } else { + info!(?trail_uuid, "missing tbin path"); + return; + }; + let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { + tbin + } else { + info!(%tbin_path, "failed to find tbin"); + return; + }; + if let Some(active_trail) = + ActiveTrail::get_vertices_and_texture(&common_attributes, &tbin.nodes, th.clone()) + { + self.current_map_data + .wip_trails + .insert(trail_uuid, active_trail); + } else { + info!("Cannot display {texture_path:?}") + } + } +} + +pub fn jokolay_to_editable_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + let marker_manager_path = jokolay_to_marker_path(jokolay_path); + marker_manager_path.join(EDITABLE_PACKAGE_NAME) +} + +pub fn jokolay_to_extract_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + jokolay_path.join(EXTRACT_DIRECTORY_NAME) +} + +pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(PACKAGES_DIRECTORY_NAME) +} + +pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { + jokolay_dir + .create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to create marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; + let marker_manager_dir = jokolay_dir + .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to open marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; + + marker_manager_dir + .create_dir_all(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to create marker packs directory {}", + PACKAGES_DIRECTORY_NAME + ))?; + let marker_packs_dir = marker_manager_dir + .open_dir(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to open marker packs dir {}", + PACKAGES_DIRECTORY_NAME + ))?; + + marker_packs_dir + .create_dir_all(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + let editable_package = marker_packs_dir + .open_dir(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + + editable_package + .create_dir_all("data") + .into_diagnostic() + .wrap_err("failed to create data folder for editable package")?; + + Ok(marker_packs_dir) +} + +pub fn load_all_from_dir( + input: (Arc, std::path::PathBuf), +) -> Result { + let (jokolay_dir, root_path) = input; + let marker_packs_dir = + jokolay_to_marker_dir(&jokolay_dir).or(Err("Failed to open packages directory"))?; + let marker_packs_path = jokolay_to_marker_path(&root_path); + let mut data_packs: BTreeMap = Default::default(); + let mut texture_packs: BTreeMap = Default::default(); + let mut report_packs: BTreeMap = Default::default(); + + for entry in marker_packs_dir + .entries() + .or(Err("failed to get entries of marker packs dir"))? + { + let entry = entry.or(Err("Failed to read packages directory"))?; + if entry + .metadata() + .or(Err("Could not read folder metadata"))? + .is_file() + { + continue; + } + if let Ok(name) = entry.file_name() { + let pack_path = marker_packs_path.join(&name); + let pack_dir = entry.open_dir().or(Err(format!( + "failed to open pack entry as directory: {}", + name + )))?; + { + if name == EDITABLE_PACKAGE_NAME { + //TODO: have a version of loading that does not involve already ingested packages + if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { + let lp = + build_from_core(name.clone(), pack_dir.into(), pack_path, pack_core); + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + } else if name == LOCAL_EXPANDED_PACKAGE_NAME { + //ignore this package, it'll be overwriten + } else { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name.clone(), pack_dir.into(), pack_path) { + Ok(lp) => { + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + Err(e) => { + error!(?e, "failed to load pack from directory: {}", name); + } + } + drop(span_guard); + } + } + } + } + Ok((data_packs, texture_packs, report_packs)) +} + +fn build_from_dir( + name: String, + pack_dir: Arc, + pack_path: PathBuf, +) -> Result { + if !pack_dir + .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) + .or(Err("failed to check if pack core exists"))? + { + return Err("pack core doesn't exist in this pack".to_string()); + } + let core_dir = pack_dir + .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) + .or(Err("failed to open core pack directory"))?; + let start = std::time::SystemTime::now(); + let import_report = LoadedPackData::load_import_report(&pack_dir); + let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + .or(Err("failed to load pack from dir"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); + let res = build_from_core(name.clone(), pack_dir, pack_path, core); + Ok(res) +} + +pub fn build_from_core( + name: String, + pack_dir: Arc, + path: PathBuf, + core: PackCore, +) -> ImportTriplet { + let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); + let data = LoadedPackData { + name: name.clone(), + uuid: core.uuid, + dir: Arc::clone(&pack_dir), + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.active_source_files.clone(), + _is_dirty: false, + activation_data: Default::default(), + active_elements: Default::default(), + selectable_categories: selectable_categories.clone(), + entities_parents: core.entities_parents, + }; + let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_default(); + let tex = LoadedPackTexture { + uuid: core.uuid, + selectable_categories, + textures: core.textures, + current_map_data: Default::default(), + _is_dirty: false, + activation_data, + path, + name, + tbins: core.tbins, + //active_elements: Default::default(), + source_files: core.active_source_files, + }; + let report = core.report; + (data, tex, report) +} diff --git a/crates/joko_package_manager/src/manager/pack/mod.rs b/crates/joko_package_manager/src/manager/pack/mod.rs new file mode 100644 index 0000000..908a692 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/mod.rs @@ -0,0 +1,6 @@ +pub mod activation; +pub mod active; +pub mod category_selection; +pub mod file_selection; +pub mod import; +pub mod loaded; diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs new file mode 100644 index 0000000..3f5a8bc --- /dev/null +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -0,0 +1,515 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, +}; + +use cap_std::fs_utf8::Dir; +use joko_component_models::ComponentDataExchange; +use joko_package_models::package::PackageImportReport; + +use tracing::{error, info, info_span, trace}; + +use crate::{ + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, message::MessageToPackageBack, +}; +use joko_link_models::{MumbleLink, MumbleLinkSharedState}; +use miette::{IntoDiagnostic, Result}; +use uuid::Uuid; + +use crate::manager::pack::loaded::{LoadedPackData, PackTasks}; +use crate::message::MessageToPackageUI; + +use super::pack::loaded::jokolay_to_marker_path; + +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose +pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading +pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite +pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package + // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; + +#[derive(Clone)] +pub struct PackageBackSharedState { + choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI + pub root_dir: Arc, + pub root_path: std::path::PathBuf, + #[allow(dead_code)] + pub editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration + extract_path: std::path::PathBuf, +} + +/// It manage everything that has to do with marker packs. +/// 1. imports, loads, saves and exports marker packs. +/// 2. maintains the categories selection data for every pack +/// 3. contains activation data globally and per character +/// 4. When we load into a map, it filters the markers and runs the logic every frame +/// 1. If a marker needs to be activated (based on player position or whatever) +/// 2. marker needs to be drawn +/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture +/// 4. render that marker use joko_render + +#[must_use] +pub struct PackageDataManager { + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + //_marker_manager_dir: Arc, + /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. + /// The name of the child directory is the name of the pack + //pub marker_packs_dir: Arc, + pub marker_packs_path: std::path::PathBuf, + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + pub packs: BTreeMap, + tasks: PackTasks, + current_map_id: u32, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, + + pub currently_used_files: BTreeMap, + parents: HashMap, + loaded_elements: HashSet, + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + pub state: PackageBackSharedState, +} + +impl PackageDataManager { + /// Creates a new instance of [MarkerManager]. + /// 1. It opens the marker manager directory + /// 2. loads its configuration + /// 3. opens the packs directory + /// 4. loads all the packs + /// 5. loads all the activation data + /// 6. returns self + pub fn new( + root_dir: Arc, + root_path: &std::path::Path, + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + ) -> Result { + let marker_packs_path = jokolay_to_marker_path(root_path); + //TODO: load configuration from disk (ui.toml) + let editable_path = jokolay_to_editable_path(root_path) + .to_str() + .unwrap() + .to_string(); + let state = PackageBackSharedState { + choice_of_category_changed: false, + root_dir, + root_path: root_path.to_owned(), + editable_path: std::path::PathBuf::from(editable_path), + extract_path: jokolay_to_extract_path(root_path), + }; + Ok(Self { + packs: Default::default(), + tasks: PackTasks::new(), + //marker_packs_dir: Arc::new(marker_packs_dir), + marker_packs_path, + current_map_id: 0, + save_interval: 0.0, + currently_used_files: Default::default(), + parents: Default::default(), + loaded_elements: Default::default(), + channel_sender, + channel_receiver, + state, + }) + } + + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_set(uuid, status) { + break; + } + } + } + + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_branch_set(uuid, status) { + break; + } + } + } + + pub fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> HashSet + where + I: Iterator, + { + let iter = input.into_iter(); + let mut result: HashSet = HashSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; + } + next_gen.push(*p); + } + } + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + #[allow(unreachable_code)] // sillyness of some tools + { + unreachable!("The loop should always return") + } + } + + pub fn get_active_elements_parents( + &mut self, + categories_and_elements_to_be_loaded: HashSet, + ) { + trace!( + "There are {} active elements", + categories_and_elements_to_be_loaded.len() + ); + + //first merge the parents to iterate overit + let mut parents: HashMap = Default::default(); + for pack in self.packs.values_mut() { + parents.extend(pack.entities_parents.clone()); + } + self.parents = parents; + //then climb up the tree of parent's categories + self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); + } + + fn handle_message(&mut self, msg: MessageToPackageBack) { + //let (b2u_sender, _) = package_manager.channels(); + match msg { + MessageToPackageBack::ActiveFiles(currently_used_files) => { + tracing::trace!("Handling of MessageToPackageBack::ActiveFiles"); + self.set_currently_used_files(currently_used_files); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategoryActivationElementStatusChange(category_uuid, status) => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationElementStatusChange" + ); + self.category_set(category_uuid, status); + } + MessageToPackageBack::CategoryActivationBranchStatusChange(category_uuid, status) => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationBranchStatusChange" + ); + self.category_branch_set(category_uuid, status); + } + MessageToPackageBack::CategoryActivationStatusChanged => { + tracing::trace!( + "Handling of MessageToPackageBack::CategoryActivationStatusChanged" + ); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategorySetAll(status) => { + tracing::trace!("Handling of MessageToPackageBack::CategorySetAll"); + self.category_set_all(status); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::DeletePacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageBack::DeletePacks"); + let std_file = std::fs::OpenOptions::new() + .open(&self.marker_packs_path) + .or(Err("Could not open file")) + .unwrap(); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let mut deleted = Vec::new(); + + for pack_uuid in to_delete { + if let Some(pack) = self.packs.remove(&pack_uuid) { + if let Err(e) = marker_packs_dir.remove_dir_all(&pack.name) { + error!(?e, pack.name, "failed to remove pack"); + } else { + info!("deleted marker pack: {}", pack.name); + deleted.push(pack_uuid); + } + } + } + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::DeletedPacks(deleted).into()); + } + MessageToPackageBack::ImportPack(file_path) => { + tracing::trace!("Handling of MessageToPackageBack::ImportPack"); + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::NbTasksRunning(1).into()); + let start = std::time::SystemTime::now(); + let result = import_pack_from_zip_file_path(file_path, &self.state.extract_path); + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of taco package from disk took {} ms", + elaspsed.as_millis() + ); + match result { + Ok((file_name, pack)) => { + let _ = self.channel_sender.blocking_send( + MessageToPackageUI::ImportedPack(file_name, pack).into(), + ); + } + Err(e) => { + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::ImportFailure(e).into()); + } + } + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::NbTasksRunning(0).into()); + } + MessageToPackageBack::ReloadPack => { + unimplemented!( + "Handling of MessageToPackageBack::ReloadPack has not been implemented yet" + ); + } + MessageToPackageBack::SavePack(name, pack) => { + tracing::trace!("Handling of MessageToPackageBack::SavePack"); + let std_file = std::fs::OpenOptions::new() + .open(&self.marker_packs_path) + .or(Err("Could not open file")) + .unwrap(); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let name = name.as_str(); + if marker_packs_dir.exists(name) { + match marker_packs_dir.remove_dir_all(name).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to delete already existing marker pack"); + } + } + } + if let Err(e) = marker_packs_dir.create_dir_all(name) { + error!(?e, "failed to create directory for pack"); + } + match marker_packs_dir.open_dir(name) { + Ok(dir) => { + let pack_path = self.marker_packs_path.join(name); + let (data_pack, mut texture_pack, mut report) = + build_from_core(name.to_string(), dir.into(), pack_path, pack); + tracing::trace!("Package loaded into data and texture"); + let uuid_of_insertion = self.save(data_pack, report.clone()); + report.uuid = uuid_of_insertion; + texture_pack.uuid = uuid_of_insertion; + let _ = self.channel_sender.blocking_send( + MessageToPackageUI::LoadedPack(texture_pack, report).into(), + ); + } + Err(e) => { + error!( + ?e, + "failed to open marker pack directory to save pack {:?} {}", + self.marker_packs_path, + name + ); + } + }; + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + } + } + } + + pub fn flush_all_messages(&mut self) -> PackageBackSharedState { + tracing::trace!( + "choice_of_category_changed: {}", + self.state.choice_of_category_changed + ); + + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + let msg = bincode::deserialize(&msg).unwrap(); + messages.push(msg); + } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() + } + + pub fn tick( + &mut self, + loop_index: u128, + ms: &MumbleLinkSharedState, + link: Option<&MumbleLink>, + ) { + let mut currently_used_files: BTreeMap = Default::default(); + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + + let link = if ms.read_ui_link { + ms.copy_of_ui_link.as_ref() + } else { + link + }; + + if let Some(link) = link { + //TODO: how to save/load the active files ? + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { + currently_used_files.insert( + marker.source_file_uuid, + *self + .currently_used_files + .get(&marker.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { + currently_used_files.insert( + trail.source_file_uuid, + *self + .currently_used_files + .get(&trail.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + } + } + let tasks = &self.tasks; + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::NbTasksRunning(tasks.count()).into()); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + &self.channel_sender, + loop_index, + link, + ¤tly_used_files, + have_used_files_list_changed || self.state.choice_of_category_changed, + map_changed, + tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + if map_changed { + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + let _ = self.channel_sender.blocking_send( + MessageToPackageUI::ActiveElements(self.loaded_elements.clone()).into(), + ); + } + if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed + { + //there is no point in sending a new list if nothing changed + let _ = self.channel_sender.blocking_send( + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()).into(), + ); + self.currently_used_files = currently_used_files; + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::TextureSwapChain.into()); + } + } + self.state.choice_of_category_changed = false; + } + + fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } + pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == data_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks + .save_report(Arc::clone(&data_pack.dir), report, true); + self.tasks.save_data(&mut data_pack, true); + let mut uuid_to_insert = data_pack.uuid; + while self.packs.contains_key(&uuid_to_insert) { + //collision avoidance + trace!( + "Uuid collision detected for {} for package {}", + uuid_to_insert, + data_pack.name + ); + uuid_to_insert = Uuid::new_v4(); + } + data_pack.uuid = uuid_to_insert; + self.packs.insert(uuid_to_insert, data_pack); + uuid_to_insert + } + + pub fn load_all(&mut self) { + once::assert_has_not_been_called!("Early load must happen only once"); + // Called only once at application start. + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::NbTasksRunning(1).into()); + self.tasks.load_all_packs( + Arc::clone(&self.state.root_dir), + self.state.root_path.clone(), + ); + if let Ok((data_packages, texture_packages, report_packages)) = + self.tasks.wait_for_load_all_packs() + { + for (uuid, data_pack) in data_packages { + self.packs.insert(uuid, data_pack); + } + for ((_, texture_pack), (_, report)) in + std::iter::zip(texture_packages, report_packages) + { + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); + } + + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::NbTasksRunning(0).into()); + } + let _ = self + .channel_sender + .blocking_send(MessageToPackageUI::FirstLoadDone.into()); + } +} diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs new file mode 100644 index 0000000..6693838 --- /dev/null +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -0,0 +1,770 @@ +use std::{ + collections::{BTreeMap, HashSet}, + sync::{Arc, Mutex}, +}; + +use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; +use image::EncodableLayout; +use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; + +use joko_render_models::messages::UIToUIMessage; +use tracing::{info_span, trace}; + +use crate::message::MessageToPackageBack; +use joko_component_models::{ComponentDataExchange, JokolayUIComponent, PeerComponentChannel}; +use joko_core::{serde_glam::Vec3, RelativePath}; +use joko_link_models::{MumbleChanges, MumbleLink}; +use miette::Result; +use uuid::Uuid; + +use crate::manager::pack::import::ImportStatus; +use crate::manager::pack::loaded::{LoadedPackTexture, PackTasks}; +use crate::message::MessageToPackageUI; + +//FIXME: there is an interest to merge the PackageUIManager and the render +#[derive(Clone)] +pub struct PackageUISharedState { + list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + first_load_done: bool, + nb_running_tasks_on_back: i32, // store the number of running tasks in background thread + import_status: Arc>, +} + +#[must_use] +pub struct PackageUIManager { + default_marker_texture: Option, + default_trail_texture: Option, + packs: BTreeMap, + reports: BTreeMap, + tasks: PackTasks, + + currently_used_files: BTreeMap, + all_files_activation_status: bool, // this consume a change of display event + show_only_active: bool, + pack_details: Option, // if filled, display the details of the package + + delayed_marker_texture: Vec<(Uuid, RelativePath, Uuid, Vec3, CommonAttributes)>, + delayed_trail_texture: Vec<(Uuid, RelativePath, Uuid, CommonAttributes)>, + + //egui_context: &'l egui::Context, //TODO: remove, this is not the proper place to be, or if it is, badly used + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + sender_u2u: Option>, + receiver_mumblelink: Option>, + receiver_near_scene: Option>, + state: PackageUISharedState, +} + +impl PackageUIManager { + pub fn new( + channel_receiver: tokio::sync::mpsc::Receiver, + channel_sender: tokio::sync::mpsc::Sender, + ) -> Self { + let state = PackageUISharedState { + list_of_textures_changed: false, + first_load_done: false, + nb_running_tasks_on_back: 0, + import_status: Default::default(), + }; + Self { + packs: Default::default(), + tasks: PackTasks::new(), + reports: Default::default(), + default_marker_texture: None, + default_trail_texture: None, + + all_files_activation_status: false, + show_only_active: true, + currently_used_files: Default::default(), // UI copy to (de-)activate files + pack_details: None, + + delayed_marker_texture: Default::default(), + delayed_trail_texture: Default::default(), + channel_sender, + channel_receiver, + sender_u2u: None, + receiver_mumblelink: None, + receiver_near_scene: None, + state, + } + } + + fn handle_message(&mut self, msg: MessageToPackageUI) { + match msg { + MessageToPackageUI::ActiveElements(active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::ActiveElements"); + self.update_active_categories(&active_elements); + } + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files) => { + tracing::trace!("Handling of MessageToPackageUI::CurrentlyUsedFiles"); + self.set_currently_used_files(currently_used_files); + } + MessageToPackageUI::DeletedPacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageUI::DeletedPacks"); + self.delete_packs(to_delete); + } + MessageToPackageUI::FirstLoadDone => { + self.state.first_load_done = true; + } + MessageToPackageUI::ImportedPack(file_name, pack) => { + tracing::trace!("Handling of MessageToPackageUI::ImportedPack"); + *self.state.import_status.lock().unwrap() = + ImportStatus::PackDone(file_name, pack, false); + } + MessageToPackageUI::ImportFailure(message) => { + tracing::trace!("Handling of MessageToPackageUI::ImportFailure"); + *self.state.import_status.lock().unwrap() = + ImportStatus::PackError(miette::Report::msg(message)); + } + MessageToPackageUI::LoadedPack(pack_texture, report) => { + tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); + self.save(pack_texture, report); + self.state.import_status = Default::default(); + let _ = self + .channel_sender + .blocking_send(MessageToPackageBack::CategoryActivationStatusChanged.into()); + } + MessageToPackageUI::MarkerTexture( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::MarkerTexture"); + //FIXME: make it a TODO on tick() + self.delayed_marker_texture.push(( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + )); + } + MessageToPackageUI::NbTasksRunning(nb_tasks) => { + tracing::trace!("Handling of MessageToPackageUI::NbTasksRunning"); + self.state.nb_running_tasks_on_back = nb_tasks; + } + MessageToPackageUI::PackageActiveElements(pack_uuid, active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::PackageActiveElements"); + self.update_pack_active_categories(pack_uuid, &active_elements); + } + MessageToPackageUI::TextureSwapChain => { + tracing::debug!("Handling of MessageToPackageUI::TextureSwapChain"); + self.swap(); + self.state.list_of_textures_changed = true; + } + MessageToPackageUI::TrailTexture( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::TrailTexture"); + self.delayed_trail_texture.push(( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + )); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageUI has not been implemented yet"); + } + } + } + + pub fn flush_all_messages(&mut self) -> PackageUISharedState { + if let Ok(mut import_status) = self.state.import_status.lock() { + if let ImportStatus::LoadingPack(file_path) = &mut *import_status { + let _ = self + .channel_sender + .blocking_send(MessageToPackageBack::ImportPack(file_path.clone()).into()); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); + } + } + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + let msg = bincode::deserialize(&msg).unwrap(); + messages.push(msg); + } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() + } + + pub fn late_init(&mut self, egui_context: &egui::Context) { + //TODO: make it even later, at another place + if self.default_marker_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_marker_texture = Some(egui_context.load_texture( + "default marker", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + if self.default_trail_texture.is_none() { + let img = + image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_trail_texture = Some(egui_context.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + } + + pub fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + self.reports.remove(&uuid); + } + } + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for pack in self.packs.values_mut() { + pack.update_active_categories(active_elements); + } + } + + pub fn update_pack_active_categories( + &mut self, + pack_uuid: Uuid, + active_elements: &HashSet, + ) { + trace!("There are {} active elements", active_elements.len()); + for (uuid, pack) in self.packs.iter_mut() { + if uuid == &pack_uuid { + pack.update_active_categories(active_elements); + break; + } + } + } + pub fn swap(&mut self) { + for pack in self.packs.values_mut() { + pack.swap(); + } + } + + pub fn load_marker_texture( + &mut self, + pack_uuid: Uuid, + egui_context: &egui::Context, + tex_path: RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { + pack.load_marker_texture( + egui_context, + self.default_marker_texture.as_ref().unwrap(), + &tex_path, + marker_uuid, + position, + common_attributes, + ); + }; + } + pub fn load_trail_texture( + &mut self, + pack_uuid: Uuid, + egui_context: &egui::Context, + tex_path: RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { + pack.load_trail_texture( + egui_context, + self.default_trail_texture.as_ref().unwrap(), + &tex_path, + trail_uuid, + common_attributes, + ); + }; + } + + fn pack_importer(import_status: Arc>) { + //called when a new pack is imported + rayon::spawn(move || { + *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + + if let Some(file_path) = rfd::FileDialog::new() + .add_filter("taco", &["zip", "taco"]) + .pick_file() + { + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); + } else { + *import_status.lock().unwrap() = + ImportStatus::PackError(miette::miette!("file chooser was cancelled")); + } + }); + } + + fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { + let tasks = &self.tasks; + let sender_u2u = self.sender_u2u.as_ref().unwrap(); + for pack in self.packs.values_mut() { + tasks.save_texture(pack, pack.is_dirty()); + } + if link.changes.contains(MumbleChanges::Position) + || link.changes.contains(MumbleChanges::Map) + { + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + pack.tick(sender_u2u, timestamp, link, z_near, tasks)?; + std::mem::drop(span_guard); + } + let _ = sender_u2u.blocking_send(UIToUIMessage::RenderSwapChain.into()); + } + Ok(()) + } + + pub fn menu_ui( + &mut self, + ui: &mut egui::Ui, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, + ) { + ui.menu_button("Markers", |ui| { + if self.show_only_active { + if ui.button("Show everything").clicked() { + self.show_only_active = false; + } + } else if ui.button("Show only active").clicked() { + self.show_only_active = true; + } + if ui.button("Activate all elements").clicked() { + self.category_set_all(true); + let _ = self + .channel_sender + .blocking_send(MessageToPackageBack::CategorySetAll(true).into()); + } + if ui.button("Deactivate all elements").clicked() { + self.category_set_all(false); + let _ = self + .channel_sender + .blocking_send(MessageToPackageBack::CategorySetAll(false).into()); + } + + for (pack, import_quality_report) in + std::iter::zip(self.packs.values_mut(), self.reports.values()) + { + //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; + //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + let u2u_sender = self.sender_u2u.as_ref().unwrap(); + pack.category_sub_menu( + &self.channel_sender, + u2u_sender, + ui, + self.show_only_active, + import_quality_report, + ); + } + }); + if self.tasks.is_running() + || nb_running_tasks_on_back > 0 + || nb_running_tasks_on_network > 0 + { + let sp = egui::Spinner::new() + .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); + ui.add(sp); + } + } + pub fn status_as_color( + &self, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, + ) -> egui::Color32 { + //we can choose whatever color code we want to focus on load, save, network queries, anything. + let nb_running_tasks_on_ui = self.tasks.count(); + //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 + let color_ui = if nb_running_tasks_on_ui > 0 { + let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; + let res = nb_ui_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_back = if nb_running_tasks_on_back > 0 { + let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; + let res = nb_bask_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_network = if nb_running_tasks_on_network > 0 { + let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; + let res = nb_network_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + egui::Color32::from_rgb(color_ui, color_back, color_network) + } + + fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool) { + let mut files_changed = false; + Window::new("File Manager") + .open(open) + .show(etx, |ui| -> Result<()> { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Grid::new("link grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + let mut all_files_toggle = false; + ui.horizontal(|ui| { + if ui.button("activate all").clicked() { + self.all_files_activation_status = true; + all_files_toggle = true; + files_changed = true; + } + if ui.button("deactivate all").clicked() { + self.all_files_activation_status = false; + all_files_toggle = true; + files_changed = true; + } + }); + //ui.label("Trails"); + //ui.label("Markers"); + ui.end_row(); + + for pack in self.packs.values_mut() { + //TODO: first loop to list what is active per pack, to not display all packs + let report = self.reports.get(&pack.uuid).unwrap(); + let mut pack_files_toggle = false; + let mut pack_files_activation_status = true; + ui.horizontal(|ui| { + ui.label(&pack.name); + if ui.button("activate all").clicked() { + pack_files_activation_status = true; + pack_files_toggle = true; + files_changed = true; + } + if ui.button("deactivate all").clicked() { + pack_files_activation_status = false; + pack_files_toggle = true; + files_changed = true; + } + }); + ui.end_row(); + for source_file_uuid in pack.source_files.keys() { + if let Some(is_selected) = + self.currently_used_files.get_mut(source_file_uuid) + { + if all_files_toggle { + *is_selected = self.all_files_activation_status; + } + if pack_files_toggle { + *is_selected = pack_files_activation_status; + } + ui.add_space(3.0); + //reports may be corrupted or not loaded, files are there + if let Some(source_file_name) = + report.source_file_uuid_to_name(source_file_uuid) + { + //format the file from reports and packages + prefix with the package name + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_name), + ); + if cb.changed() { + files_changed = true; + } + } else { + // Import report is corrupted, only print reference + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_uuid), + ); + if cb.changed() { + files_changed = true; + } + } + ui.end_row(); + } + } + } + ui.end_row(); + }) + }); + Ok(()) + }); + if files_changed { + let _ = self.channel_sender.blocking_send( + MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()).into(), + ); + } + } + + fn gui_package_details(&mut self, ui: &mut Ui, uuid: Uuid) { + // protection against deletion while displaying details + if let Some(pack) = self.packs.get(&uuid) { + if let Some(report) = self.reports.get(&uuid) { + let collapsing = + CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + let header_response = collapsing + .open(Some(true)) + .show(ui, |ui| { + egui::Grid::new("packs details") + .striped(true) + .show(ui, |ui| { + let number_of = &report.number_of; + ui.label("categories"); + ui.label(format!("{}", number_of.categories)); + ui.end_row(); + ui.label("missing_categories"); + ui.label(format!("{}", number_of.missing_categories)); + ui.end_row(); + ui.label("textures"); + ui.label(format!("{}", number_of.textures)); + ui.end_row(); + ui.label("missing_textures"); + ui.label(format!("{}", number_of.missing_textures)); + ui.end_row(); + ui.label("entities"); + ui.label(format!("{}", number_of.entities)); + ui.end_row(); + ui.label("markers"); + ui.label(format!("{}", number_of.markers)); + ui.end_row(); + ui.label("trails"); + ui.label(format!("{}", number_of.trails)); + ui.end_row(); + ui.label("routes"); + ui.label(format!("{}", number_of.routes)); + ui.end_row(); + ui.label("maps"); + ui.label(format!("{}", number_of.maps)); + ui.end_row(); + ui.label("source_files"); + ui.label(format!("{}", number_of.source_files)); + ui.end_row(); + }) + }) + .header_response; + if header_response.clicked() { + self.pack_details = None; + } + } else { + self.pack_details = None; + } + } else { + self.pack_details = None; + } + } + fn gui_package_list( + &mut self, + etx: &egui::Context, + import_status: &Arc>, + open: &mut bool, + first_load_done: bool, + ) { + Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { + CollapsingHeader::new("Loaded Packs").show(ui, |ui| { + egui::Grid::new("packs").striped(true).show(ui, |ui| { + if !first_load_done { + ui.label("Loading in progress..."); + } + let mut to_delete = vec![]; + for pack in self.packs.values() { + ui.label(pack.name.clone()); + if ui.button("delete").clicked() { + to_delete.push(pack.uuid); + } + if ui.button("Details").clicked() { + self.pack_details = Some(pack.uuid); + } + if ui.button("Export").clicked() { + //TODO + } + ui.end_row(); + } + if !to_delete.is_empty() { + let _ = self.channel_sender.blocking_send(MessageToPackageBack::DeletePacks(to_delete).into()); + } + }); + }); + if let Some(uuid) = self.pack_details { + self.gui_package_details(ui, uuid); + } else if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + Self::pack_importer(Arc::clone(import_status)); + } + //ui.label("import not started yet"); + } + ImportStatus::WaitingForFileChooser => { + ui.label( + "wailting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + if *saved { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } else { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + if ui.button("save").clicked() { + let _ = self.channel_sender.blocking_send(MessageToPackageBack::SavePack(name.clone(), pack.clone()).into()); + } + } + } + ImportStatus::PackError(e) => { + let error_msg = format!("failed to import pack due to error: {e:#?}"); + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + *status = ImportStatus::UnInitialized; + } + ui.colored_label( + egui::Color32::RED, + error_msg, + ); + } + } + } + + Ok(()) + }); + } + pub fn gui( + &mut self, + etx: &egui::Context, + is_marker_open: &mut bool, + import_status: &Arc>, + is_file_open: &mut bool, + first_load_done: bool, + ) { + self.gui_package_list(etx, import_status, is_marker_open, first_load_done); + self.gui_file_manager(etx, is_file_open); + } + + pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { + /* + We save in a file with the name of the package, while we keep track of it from a uuid point of view. + It means we can have duplicates unless package with same name is deleted. + */ + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == texture_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks.save_texture(&mut texture_pack, true); + self.packs.insert(texture_pack.uuid, texture_pack); + self.reports.insert(report.uuid, report); + } +} + +//TODO: there is a need for a more complex input according to deps +impl JokolayUIComponent for PackageUIManager { + fn flush_all_messages(&mut self) -> PackageUISharedState { + let mut messages = Vec::new(); + while let Ok(msg) = self.channel_receiver.try_recv() { + messages.push(msg.into()); + } + for msg in messages { + self.handle_message(msg); + } + self.state.clone() + } + + fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> Option<&()> { + let raw_link = self + .receiver_mumblelink + .as_mut() + .unwrap() + .blocking_recv() + .unwrap(); + let link: &MumbleLink = &bincode::deserialize(&raw_link).unwrap(); + + for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in + std::mem::take(&mut self.delayed_marker_texture) + { + self.load_marker_texture( + pack_uuid, + egui_context, + tex_path, + marker_uuid, + position, + common_attributes, + ); + } + for (pack_uuid, tex_path, trail_uuid, common_attributes) in + std::mem::take(&mut self.delayed_trail_texture) + { + self.load_trail_texture( + pack_uuid, + egui_context, + tex_path, + trail_uuid, + common_attributes, + ); + } + + let raw_z_near = self + .receiver_near_scene + .as_mut() + .unwrap() + .blocking_recv() + .unwrap(); + let z_near: f32 = bincode::deserialize(&raw_z_near).unwrap(); + let _ = self._tick(timestamp, link, z_near); + None + } + fn bind( + &mut self, + mut deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + mut _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + mut notify: std::collections::HashMap< + u32, + tokio::sync::mpsc::Sender, + >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + self.sender_u2u = notify.remove(&0); + self.receiver_mumblelink = deps.remove(&0); + self.receiver_near_scene = deps.remove(&1); + unimplemented!("PackageUIManager component binding is not implemented") + } +} diff --git a/crates/joko_package_manager/src/message.rs b/crates/joko_package_manager/src/message.rs new file mode 100644 index 0000000..949aad7 --- /dev/null +++ b/crates/joko_package_manager/src/message.rs @@ -0,0 +1,61 @@ +use std::collections::{BTreeMap, HashSet}; + +use joko_component_models::ComponentDataExchange; +use joko_package_models::{ + attributes::CommonAttributes, + package::{PackCore, PackageImportReport}, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use joko_core::{serde_glam::Vec3, RelativePath}; + +use crate::LoadedPackTexture; + +#[derive(Serialize, Deserialize)] +pub enum MessageToPackageUI { + ActiveElements(HashSet), //list of all elements that are loaded for current map + CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display + LoadedPack(LoadedPackTexture, PackageImportReport), //push a loaded pack to UI + DeletedPacks(Vec), //push a deleted set of packs to UI + FirstLoadDone, + ImportedPack(String, PackCore), + ImportFailure(String), + MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), + NbTasksRunning(i32), //tell the number of taks running in background + PackageActiveElements(Uuid, HashSet), // first is the package reference, second is the list of active elements in the package. + TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain + TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), +} + +impl From for ComponentDataExchange { + fn from(src: MessageToPackageUI) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } +} + +#[allow(clippy::from_over_into)] +impl Into for ComponentDataExchange { + fn into(self) -> MessageToPackageUI { + bincode::deserialize(&self).unwrap() + } +} + +#[derive(Serialize, Deserialize)] +pub enum MessageToPackageBack { + ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. + CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationBranchStatusChange(Uuid, bool), //same, for a whole branch + CategoryActivationStatusChanged, //something happened that needs to reload the whole set + CategorySetAll(bool), //signal all categories should be now at this status + DeletePacks(Vec), //uuid of the pack to delete + ImportPack(std::path::PathBuf), + ReloadPack, + SavePack(String, PackCore), +} + +impl From for ComponentDataExchange { + fn from(src: MessageToPackageBack) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } +} diff --git a/crates/joko_package_manager/vendor/rapid/license.txt b/crates/joko_package_manager/vendor/rapid/license.txt new file mode 100644 index 0000000..e5ecd23 --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/license.txt @@ -0,0 +1,52 @@ +Use of this software is granted under one of the following two licenses, +to be chosen freely by the user. + +1. Boost Software License - Version 1.0 - August 17th, 2003 +=============================================================================== + +Copyright (c) 2006, 2007 Marcin Kalicinski + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +2. The MIT License +=============================================================================== + +Copyright (c) 2006, 2007 Marcin Kalicinski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/crates/joko_package_manager/vendor/rapid/rapid.cpp b/crates/joko_package_manager/vendor/rapid/rapid.cpp new file mode 100644 index 0000000..680f0fa --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapid.cpp @@ -0,0 +1,66 @@ +#include "joko_package_manager/vendor/rapid/rapid.hpp" +#include "joko_package_manager/vendor/rapid/rapidxml.hpp" +#include "joko_package_manager/vendor/rapid/rapidxml_print.hpp" +#include "joko_package_manager/src/lib.rs.h" +#include +#include +#include +#include +void remove_duplicate_nodes(rapidxml::xml_node *node) +{ + + std::set duplicates; + rapidxml::xml_attribute *attr = node->first_attribute(); + while (attr) + { + std::string name(attr->name(), attr->name_size()); + if (duplicates.count(name) == 1) + { + rapidxml::xml_attribute *prev = attr; + attr = attr->next_attribute(); + node->remove_attribute(prev); + } + else + { + duplicates.insert(name); + attr = attr->next_attribute(); + } + } + for (rapidxml::xml_node *child = node->first_node(); child; child = child->next_sibling()) + { + remove_duplicate_nodes(child); + } +} + +namespace rapid +{ + + rust::String rapid_filter(rust::String src_xml) + { + // return std::string(src_xml); + std::string src = static_cast(src_xml); + std::string dst; + using namespace rapidxml; + // create document + xml_document doc; + // rapid xml throws exception if there's a parsing error + try + { + // parse the xml text. if there's exceptions we go to catch block from here + doc.parse<0>((char *)src.c_str()); + // delete all the duplicate attributes, so that there's no obvious errors for rust deserializers + for (rapidxml::xml_node *child = doc.first_node(); child; child = child->next_sibling()) + { + remove_duplicate_nodes(child); + } + std::ostringstream oss; + oss << doc; + dst = oss.str(); + } + catch (const parse_error &e) + { + return ""; + } + return dst; + } +} diff --git a/crates/joko_package_manager/vendor/rapid/rapid.hpp b/crates/joko_package_manager/vendor/rapid/rapid.hpp new file mode 100644 index 0000000..760a161 --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapid.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "joko_package_manager/src/lib.rs.h" +#include "rust/cxx.h" + +namespace rapid { + rust::String rapid_filter(rust::String src_xml); +} \ No newline at end of file diff --git a/crates/joko_package_manager/vendor/rapid/rapidxml.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml.hpp new file mode 100644 index 0000000..d025eef --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapidxml.hpp @@ -0,0 +1,2645 @@ +#ifndef RAPIDXML_HPP_INCLUDED +#define RAPIDXML_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/13 01:46:17 $ +//! \file rapidxml.hpp This file contains rapidxml parser and DOM implementation + +// If standard library is disabled, user must provide implementations of required functions and typedefs +#if !defined(RAPIDXML_NO_STDLIB) + #include // For std::size_t + #include // (Optional.) For std::strlen, ... + #include // (Optional.) For std::wcslen, ... + #include // For assert + #include // For placement new +#endif + +// RAPIDXML_NOEXCEPT: Expands to 'noexcept' on supported compilers. +#if !defined(RAPIDXML_NOEXCEPT) +# if !defined(RAPIDXML_DISABLE_NOEXCEPT) +# if defined(__clang__) +# if __has_feature(__cxx_noexcept__) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# elif defined(__GNUC__) +# if ((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4) +# if defined(__GXX_EXPERIMENTAL_CXX0X__) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# endif +# elif defined(_MSC_VER) && (_MSC_VER >= 1900) +# define RAPIDXML_NOEXCEPT noexcept(true) +# endif +# endif +# if !defined(RAPIDXML_NOEXCEPT) +# define RAPIDXML_NOEXCEPT +# endif +#endif + +// On MSVC, disable "conditional expression is constant" warning (level 4). +// This warning is almost impossible to avoid with certain types of templated code +#ifdef _MSC_VER + #pragma warning(push) + #pragma warning(disable:4127) // Conditional expression is constant +#endif + +/////////////////////////////////////////////////////////////////////////// +// RAPIDXML_PARSE_ERROR + +#if defined(RAPIDXML_NO_EXCEPTIONS) + +#define RAPIDXML_PARSE_ERROR(what, where) { parse_error_handler(what, where); assert(0); } + +namespace rapidxml +{ + //! When exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, + //! this function is called to notify user about the error. + //! It must be defined by the user. + //!

+ //! This function cannot return. If it does, the results are undefined. + //!

+ //! A very simple definition might look like that: + //!
+    //! void %rapidxml::%parse_error_handler(const char *what, void *where)
+    //! {
+    //!     std::cout << "Parse error: " << what << "\n";
+    //!     std::abort();
+    //! }
+    //! 
+ //! \param what Human readable description of the error. + //! \param where Pointer to character data where error was detected. + void parse_error_handler(const char *what, void *where); +} + +#else + +#include // For std::exception + +#define RAPIDXML_PARSE_ERROR(what, where) throw parse_error(what, where) + +namespace rapidxml +{ + + //! Parse error exception. + //! This exception is thrown by the parser when an error occurs. + //! Use what() function to get human-readable error message. + //! Use where() function to get a pointer to position within source text where error was detected. + //!

+ //! If throwing exceptions by the parser is undesirable, + //! it can be disabled by defining RAPIDXML_NO_EXCEPTIONS macro before rapidxml.hpp is included. + //! This will cause the parser to call rapidxml::parse_error_handler() function instead of throwing an exception. + //! This function must be defined by the user. + //!

+ //! This class derives from std::exception class. + class parse_error: public std::exception + { + public: + //! Constructs parse error + parse_error(const char *what, void *where) + : m_what(what) + , m_where(where) + { + } + + //! Gets human readable description of error. + //! \return Pointer to null terminated description of the error. + virtual const char *what() const throw() + { + return m_what; + } + + //! Gets pointer to character data where error happened. + //! Ch should be the same as char type of xml_document that produced the error. + //! \return Pointer to location within the parsed string where error occured. + template + Ch *where() const + { + return reinterpret_cast(m_where); + } + + private: + const char *m_what; + void *m_where; + }; +} + +#endif + +/////////////////////////////////////////////////////////////////////////// +// Pool sizes + +#ifndef RAPIDXML_STATIC_POOL_SIZE + // Size of static memory block of memory_pool. + // Define RAPIDXML_STATIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. + // No dynamic memory allocations are performed by memory_pool until static memory is exhausted. + #define RAPIDXML_STATIC_POOL_SIZE (64 * 1024) +#endif + +#ifndef RAPIDXML_DYNAMIC_POOL_SIZE + // Size of dynamic memory block of memory_pool. + // Define RAPIDXML_DYNAMIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. + // After the static block is exhausted, dynamic blocks with approximately this size are allocated by memory_pool. + #define RAPIDXML_DYNAMIC_POOL_SIZE (64 * 1024) +#endif + +#ifndef RAPIDXML_ALIGNMENT + // Memory allocation alignment. + // Define RAPIDXML_ALIGNMENT before including rapidxml.hpp if you want to override the default value, which is the size of pointer. + // All memory allocations for nodes, attributes and strings will be aligned to this value. + // This must be a power of 2 and at least 1, otherwise memory_pool will not work. + #define RAPIDXML_ALIGNMENT sizeof(void *) +#endif + +namespace rapidxml +{ + // Forward declarations + template class xml_node; + template class xml_attribute; + template class xml_document; + + //! Enumeration listing all node types produced by the parser. + //! Use xml_node::type() function to query node type. + enum node_type + { + node_document, //!< A document node. Name and value are empty. + node_element, //!< An element node. Name contains element name. Value contains text of first data node. + node_data, //!< A data node. Name is empty. Value contains data text. + node_cdata, //!< A CDATA node. Name is empty. Value contains data text. + node_comment, //!< A comment node. Name is empty. Value contains comment text. + node_declaration, //!< A declaration node. Name and value are empty. Declaration parameters (version, encoding and standalone) are in node attributes. + node_doctype, //!< A DOCTYPE node. Name is empty. Value contains DOCTYPE text. + node_pi //!< A PI node. Name contains target. Value contains instructions. + }; + + /////////////////////////////////////////////////////////////////////// + // Parsing flags + + //! Parse flag instructing the parser to not create data nodes. + //! Text of first data node will still be placed in value of parent element, unless rapidxml::parse_no_element_values flag is also specified. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_data_nodes = 0x1; + + //! Parse flag instructing the parser to not use text of first data node as a value of parent element. + //! Can be combined with other flags by use of | operator. + //! Note that child data nodes of element node take precendence over its value when printing. + //! That is, if element has one or more child data nodes and a value, the value will be ignored. + //! Use rapidxml::parse_no_data_nodes flag to prevent creation of data nodes if you want to manipulate data using values of elements. + //!

+ //! See xml_document::parse() function. + const int parse_no_element_values = 0x2; + + //! Parse flag instructing the parser to not place zero terminators after strings in the source text. + //! By default zero terminators are placed, modifying source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_string_terminators = 0x4; + + //! Parse flag instructing the parser to not translate entities in the source text. + //! By default entities are translated, modifying source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_entity_translation = 0x8; + + //! Parse flag instructing the parser to disable UTF-8 handling and assume plain 8 bit characters. + //! By default, UTF-8 handling is enabled. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_no_utf8 = 0x10; + + //! Parse flag instructing the parser to create XML declaration node. + //! By default, declaration node is not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_declaration_node = 0x20; + + //! Parse flag instructing the parser to create comments nodes. + //! By default, comment nodes are not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_comment_nodes = 0x40; + + //! Parse flag instructing the parser to create DOCTYPE node. + //! By default, doctype node is not created. + //! Although W3C specification allows at most one DOCTYPE node, RapidXml will silently accept documents with more than one. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_doctype_node = 0x80; + + //! Parse flag instructing the parser to create PI nodes. + //! By default, PI nodes are not created. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_pi_nodes = 0x100; + + //! Parse flag instructing the parser to validate closing tag names. + //! If not set, name inside closing tag is irrelevant to the parser. + //! By default, closing tags are not validated. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_validate_closing_tags = 0x200; + + //! Parse flag instructing the parser to trim all leading and trailing whitespace of data nodes. + //! By default, whitespace is not trimmed. + //! This flag does not cause the parser to modify source text. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_trim_whitespace = 0x400; + + //! Parse flag instructing the parser to condense all whitespace runs of data nodes to a single space character. + //! Trimming of leading and trailing whitespace of data is controlled by rapidxml::parse_trim_whitespace flag. + //! By default, whitespace is not normalized. + //! If this flag is specified, source text will be modified. + //! Can be combined with other flags by use of | operator. + //!

+ //! See xml_document::parse() function. + const int parse_normalize_whitespace = 0x800; + + // Compound flags + + //! Parse flags which represent default behaviour of the parser. + //! This is always equal to 0, so that all other flags can be simply ored together. + //! Normally there is no need to inconveniently disable flags by anding with their negated (~) values. + //! This also means that meaning of each flag is a negation of the default setting. + //! For example, if flag name is rapidxml::parse_no_utf8, it means that utf-8 is enabled by default, + //! and using the flag will disable it. + //!

+ //! See xml_document::parse() function. + const int parse_default = 0; + + //! A combination of parse flags that forbids any modifications of the source text. + //! This also results in faster parsing. However, note that the following will occur: + //!
    + //!
  • names and values of nodes will not be zero terminated, you have to use xml_base::name_size() and xml_base::value_size() functions to determine where name and value ends
  • + //!
  • entities will not be translated
  • + //!
  • whitespace will not be normalized
  • + //!
+ //! See xml_document::parse() function. + const int parse_non_destructive = parse_no_string_terminators | parse_no_entity_translation; + + //! A combination of parse flags resulting in fastest possible parsing, without sacrificing important data. + //!

+ //! See xml_document::parse() function. + const int parse_fastest = parse_non_destructive | parse_no_data_nodes; + + //! A combination of parse flags resulting in largest amount of data being extracted. + //! This usually results in slowest parsing. + //!

+ //! See xml_document::parse() function. + const int parse_full = parse_declaration_node | parse_comment_nodes | parse_doctype_node | parse_pi_nodes | parse_validate_closing_tags; + + /////////////////////////////////////////////////////////////////////// + // Internals + + //! \cond internal + namespace internal + { + + // Struct that contains lookup tables for the parser + // It must be a template to allow correct linking (because it has static data members, which are defined in a header file). + template + struct lookup_tables + { + static const unsigned char lookup_whitespace[256]; // Whitespace table + static const unsigned char lookup_node_name[256]; // Node name table + static const unsigned char lookup_text[256]; // Text table + static const unsigned char lookup_text_pure_no_ws[256]; // Text table + static const unsigned char lookup_text_pure_with_ws[256]; // Text table + static const unsigned char lookup_attribute_name[256]; // Attribute name table + static const unsigned char lookup_attribute_data_1[256]; // Attribute data table with single quote + static const unsigned char lookup_attribute_data_1_pure[256]; // Attribute data table with single quote + static const unsigned char lookup_attribute_data_2[256]; // Attribute data table with double quotes + static const unsigned char lookup_attribute_data_2_pure[256]; // Attribute data table with double quotes + static const unsigned char lookup_digits[256]; // Digits + static const unsigned char lookup_upcase[256]; // To uppercase conversion table for ASCII characters + }; + + // Find length of the string + template + inline std::size_t measure(const Ch *p) RAPIDXML_NOEXCEPT + { + const Ch *tmp = p; + while (*tmp) + ++tmp; + return tmp - p; + } + +#if !defined(RAPIDXML_NO_STDLIB) + inline std::size_t measure(const char* p) RAPIDXML_NOEXCEPT + { return std::strlen(p); } + + inline std::size_t measure(const wchar_t* p) RAPIDXML_NOEXCEPT + { return std::wcslen(p); } +#endif + + // Compare strings for equality + template + inline bool compare(const Ch *p1, std::size_t size1, const Ch *p2, + std::size_t size2, bool case_sensitive) RAPIDXML_NOEXCEPT + { + if (size1 != size2) + return false; + if (case_sensitive) + { + for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) + if (*p1 != *p2) + return false; + } + else + { + for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) + if (lookup_tables<0>::lookup_upcase[static_cast(*p1)] != lookup_tables<0>::lookup_upcase[static_cast(*p2)]) + return false; + } + return true; + } + } + //! \endcond + + /////////////////////////////////////////////////////////////////////// + // Memory pool + + //! This class is used by the parser to create new nodes and attributes, without overheads of dynamic memory allocation. + //! In most cases, you will not need to use this class directly. + //! However, if you need to create nodes manually or modify names/values of nodes, + //! you are encouraged to use memory_pool of relevant xml_document to allocate the memory. + //! Not only is this faster than allocating them by using new operator, + //! but also their lifetime will be tied to the lifetime of document, + //! possibly simplyfing memory management. + //!

+ //! Call allocate_node() or allocate_attribute() functions to obtain new nodes or attributes from the pool. + //! You can also call allocate_string() function to allocate strings. + //! Such strings can then be used as names or values of nodes without worrying about their lifetime. + //! Note that there is no free() function -- all allocations are freed at once when clear() function is called, + //! or when the pool is destroyed. + //!

+ //! It is also possible to create a standalone memory_pool, and use it + //! to allocate nodes, whose lifetime will not be tied to any document. + //!

+ //! Pool maintains RAPIDXML_STATIC_POOL_SIZE bytes of statically allocated memory. + //! Until static memory is exhausted, no dynamic memory allocations are done. + //! When static memory is exhausted, pool allocates additional blocks of memory of size RAPIDXML_DYNAMIC_POOL_SIZE each, + //! by using global new[] and delete[] operators. + //! This behaviour can be changed by setting custom allocation routines. + //! Use set_allocator() function to set them. + //!

+ //! Allocations for nodes, attributes and strings are aligned at RAPIDXML_ALIGNMENT bytes. + //! This value defaults to the size of pointer on target architecture. + //!

+ //! To obtain absolutely top performance from the parser, + //! it is important that all nodes are allocated from a single, contiguous block of memory. + //! Otherwise, cache misses when jumping between two (or more) disjoint blocks of memory can slow down parsing quite considerably. + //! If required, you can tweak RAPIDXML_STATIC_POOL_SIZE, RAPIDXML_DYNAMIC_POOL_SIZE and RAPIDXML_ALIGNMENT + //! to obtain best wasted memory to performance compromise. + //! To do it, define their values before rapidxml.hpp file is included. + //! \param Ch Character type of created nodes. + template + class memory_pool + { + + public: + + //! \cond internal + typedef void *(alloc_func)(std::size_t); // Type of user-defined function used to allocate memory + typedef void (free_func)(void *); // Type of user-defined function used to free memory + //! \endcond + + //! Constructs empty pool with default allocator functions. + memory_pool() RAPIDXML_NOEXCEPT + : m_alloc_func(0) + , m_free_func(0) + { + init(); + } + + //! Destroys pool and frees all the memory. + //! This causes memory occupied by nodes allocated by the pool to be freed. + //! Nodes allocated from the pool are no longer valid. + ~memory_pool() + { + clear(); + } + + //! Allocates a new node from the pool, and optionally assigns name and value to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param type Type of node to create. + //! \param name Name to assign to the node, or 0 to assign no name. + //! \param value Value to assign to the node, or 0 to assign no value. + //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. + //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. + //! \return Pointer to allocated node. This pointer will never be NULL. + xml_node *allocate_node(node_type type, + const Ch *name = 0, const Ch *value = 0, + std::size_t name_size = 0, std::size_t value_size = 0) + { + void *memory = allocate_aligned(sizeof(xml_node)); + xml_node *node = new(memory) xml_node(type); + if (name) + { + if (name_size > 0) + node->name(name, name_size); + else + node->name(name); + } + if (value) + { + if (value_size > 0) + node->value(value, value_size); + else + node->value(value); + } + return node; + } + + //! Allocates a new attribute from the pool, and optionally assigns name and value to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param name Name to assign to the attribute, or 0 to assign no name. + //! \param value Value to assign to the attribute, or 0 to assign no value. + //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. + //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. + //! \return Pointer to allocated attribute. This pointer will never be NULL. + xml_attribute *allocate_attribute(const Ch *name = 0, const Ch *value = 0, + std::size_t name_size = 0, std::size_t value_size = 0) + { + void *memory = allocate_aligned(sizeof(xml_attribute)); + xml_attribute *attribute = new(memory) xml_attribute; + if (name) + { + if (name_size > 0) + attribute->name(name, name_size); + else + attribute->name(name); + } + if (value) + { + if (value_size > 0) + attribute->value(value, value_size); + else + attribute->value(value); + } + return attribute; + } + + //! Allocates a char array of given size from the pool, and optionally copies a given string to it. + //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. + //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function + //! will call rapidxml::parse_error_handler() function. + //! \param source String to initialize the allocated memory with, or 0 to not initialize it. + //! \param size Number of characters to allocate, or zero to calculate it automatically from source string length; if size is 0, source string must be specified and null terminated. + //! \return Pointer to allocated char array. This pointer will never be NULL. + Ch *allocate_string(const Ch *source = 0, std::size_t size = 0) + { + assert(source || size); // Either source or size (or both) must be specified + if (size == 0) + size = internal::measure(source) + 1; + Ch *result = static_cast(allocate_aligned(size * sizeof(Ch))); + if (source) + for (std::size_t i = 0; i < size; ++i) + result[i] = source[i]; + return result; + } + + //! Clones an xml_node and its hierarchy of child nodes and attributes. + //! Nodes and attributes are allocated from this memory pool. + //! Names and values are not cloned, they are shared between the clone and the source. + //! Result node can be optionally specified as a second parameter, + //! in which case its contents will be replaced with cloned source node. + //! This is useful when you want to clone entire document. + //! \param source Node to clone. + //! \param result Node to put results in, or 0 to automatically allocate result node + //! \return Pointer to cloned node. This pointer will never be NULL. + xml_node *clone_node(const xml_node *source, xml_node *result = 0) + { + // Prepare result node + if (result) + { + result->remove_all_attributes(); + result->remove_all_nodes(); + result->type(source->type()); + } + else + result = allocate_node(source->type()); + + // Clone name and value + result->name(source->name(), source->name_size()); + result->value(source->value(), source->value_size()); + + // Clone child nodes and attributes + for (xml_node *child = source->first_node(); child; child = child->next_sibling()) + result->append_node(clone_node(child)); + for (xml_attribute *attr = source->first_attribute(); attr; attr = attr->next_attribute()) + result->append_attribute(allocate_attribute(attr->name(), attr->value(), attr->name_size(), attr->value_size())); + + return result; + } + + //! Clears the pool. + //! This causes memory occupied by nodes allocated by the pool to be freed. + //! Any nodes or strings allocated from the pool will no longer be valid. + void clear() RAPIDXML_NOEXCEPT + { + while (m_begin != m_static_memory) + { + char *previous_begin = reinterpret_cast
(align(m_begin))->previous_begin; + if (m_free_func) + m_free_func(m_begin); + else + delete[] m_begin; + m_begin = previous_begin; + } + init(); + } + + //! Sets or resets the user-defined memory allocation functions for the pool. + //! This can only be called when no memory is allocated from the pool yet, otherwise results are undefined. + //! Allocation function must not return invalid pointer on failure. It should either throw, + //! stop the program, or use longjmp() function to pass control to other place of program. + //! If it returns invalid pointer, results are undefined. + //!

+ //! User defined allocation functions must have the following forms: + //!
+ //!
void *allocate(std::size_t size); + //!
void free(void *pointer); + //!

+ //! \param af Allocation function, or 0 to restore default function + //! \param ff Free function, or 0 to restore default function + void set_allocator(alloc_func *af, free_func *ff) + { + assert(m_begin == m_static_memory && m_ptr == align(m_begin)); // Verify that no memory is allocated yet + m_alloc_func = af; + m_free_func = ff; + } + + private: + + struct header + { + char *previous_begin; + }; + + void init() RAPIDXML_NOEXCEPT + { + m_begin = m_static_memory; + m_ptr = align(m_begin); + m_end = m_static_memory + sizeof(m_static_memory); + } + + char *align(char *ptr) const RAPIDXML_NOEXCEPT + { + std::size_t alignment = ((RAPIDXML_ALIGNMENT - (std::size_t(ptr) & (RAPIDXML_ALIGNMENT - 1))) & (RAPIDXML_ALIGNMENT - 1)); + return ptr + alignment; + } + + char *allocate_raw(std::size_t size) + { + // Allocate + void *memory; + if (m_alloc_func) // Allocate memory using either user-specified allocation function or global operator new[] + { + memory = m_alloc_func(size); + assert(memory); // Allocator is not allowed to return 0, on failure it must either throw, stop the program or use longjmp + } + else + { + memory = new char[size]; +#ifdef RAPIDXML_NO_EXCEPTIONS + if (!memory) // If exceptions are disabled, verify memory allocation, because new will not be able to throw bad_alloc + RAPIDXML_PARSE_ERROR("out of memory", 0); +#endif + } + return static_cast(memory); + } + + void *allocate_aligned(std::size_t size) + { + // Calculate aligned pointer + char *result = align(m_ptr); + + // If not enough memory left in current pool, allocate a new pool + if (result + size > m_end) + { + // Calculate required pool size (may be bigger than RAPIDXML_DYNAMIC_POOL_SIZE) + std::size_t pool_size = RAPIDXML_DYNAMIC_POOL_SIZE; + if (pool_size < size) + pool_size = size; + + // Allocate + std::size_t alloc_size = sizeof(header) + (2 * RAPIDXML_ALIGNMENT - 2) + pool_size; // 2 alignments required in worst case: one for header, one for actual allocation + char *raw_memory = allocate_raw(alloc_size); + + // Setup new pool in allocated memory + char *pool = align(raw_memory); + header *new_header = reinterpret_cast
(pool); + new_header->previous_begin = m_begin; + m_begin = raw_memory; + m_ptr = pool + sizeof(header); + m_end = raw_memory + alloc_size; + + // Calculate aligned pointer again using new pool + result = align(m_ptr); + } + + // Update pool and return aligned pointer + m_ptr = result + size; + return result; + } + + char *m_begin; // Start of raw memory making up current pool + char *m_ptr; // First free byte in current pool + char *m_end; // One past last available byte in current pool + char m_static_memory[RAPIDXML_STATIC_POOL_SIZE]; // Static raw memory + alloc_func *m_alloc_func; // Allocator function, or 0 if default is to be used + free_func *m_free_func; // Free function, or 0 if default is to be used + }; + + /////////////////////////////////////////////////////////////////////////// + // XML base + + //! Base class for xml_node and xml_attribute implementing common functions: + //! name(), name_size(), value(), value_size() and parent(). + //! \param Ch Character type to use + template + class xml_base + { + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + // Construct a base with empty name, value and parent + xml_base() RAPIDXML_NOEXCEPT + : m_name(0) + , m_value(0) + , m_parent(0) + , m_offset(0) + { + } + + /////////////////////////////////////////////////////////////////////////// + // Node data access + + //! Gets name of the node. + //! Interpretation of name depends on type of node. + //! Note that name will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. + //!

+ //! Use name_size() function to determine length of the name. + //! \return Name of node, or empty string if node has no name. + Ch *name() const RAPIDXML_NOEXCEPT + { + return m_name ? m_name : nullstr(); + } + + //! Gets size of node name, not including terminator character. + //! This function works correctly irrespective of whether name is or is not zero terminated. + //! \return Size of node name, in characters. + std::size_t name_size() const RAPIDXML_NOEXCEPT + { + return m_name ? m_name_size : 0; + } + + //! Gets value of node. + //! Interpretation of value depends on type of node. + //! Note that value will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. + //!

+ //! Use value_size() function to determine length of the value. + //! \return Value of node, or empty string if node has no value. + Ch *value() const RAPIDXML_NOEXCEPT + { + return m_value ? m_value : nullstr(); + } + + //! Gets size of node value, not including terminator character. + //! This function works correctly irrespective of whether value is or is not zero terminated. + //! \return Size of node value, in characters. + std::size_t value_size() const RAPIDXML_NOEXCEPT + { + return m_value ? m_value_size : 0; + } + + //! Get the start offset of this node inside the source string. + Ch *offset() const RAPIDXML_NOEXCEPT + { + return m_offset; + } + + /////////////////////////////////////////////////////////////////////////// + // Node modification + + //! Sets name of node to a non zero-terminated string. + //! See \ref ownership_of_strings. + //!

+ //! Note that node does not own its name or value, it only stores a pointer to it. + //! It will not delete or otherwise free the pointer on destruction. + //! It is reponsibility of the user to properly manage lifetime of the string. + //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - + //! on destruction of the document the string will be automatically freed. + //!

+ //! Size of name must be specified separately, because name does not have to be zero terminated. + //! Use name(const Ch *) function to have the length automatically calculated (string must be zero terminated). + //! \param name Name of node to set. Does not have to be zero terminated. + //! \param size Size of name, in characters. This does not include zero terminator, if one is present. + void name(const Ch *name, std::size_t size) RAPIDXML_NOEXCEPT + { + m_name = const_cast(name); + m_name_size = size; + } + + //! Sets name of node to a zero-terminated string. + //! See also \ref ownership_of_strings and xml_node::name(const Ch *, std::size_t). + //! \param name Name of node to set. Must be zero terminated. + void name(const Ch *name) RAPIDXML_NOEXCEPT + { + this->name(name, internal::measure(name)); + } + + //! Sets value of node to a non zero-terminated string. + //! See \ref ownership_of_strings. + //!

+ //! Note that node does not own its name or value, it only stores a pointer to it. + //! It will not delete or otherwise free the pointer on destruction. + //! It is reponsibility of the user to properly manage lifetime of the string. + //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - + //! on destruction of the document the string will be automatically freed. + //!

+ //! Size of value must be specified separately, because it does not have to be zero terminated. + //! Use value(const Ch *) function to have the length automatically calculated (string must be zero terminated). + //!

+ //! If an element has a child node of type node_data, it will take precedence over element value when printing. + //! If you want to manipulate data of elements using values, use parser flag rapidxml::parse_no_data_nodes to prevent creation of data nodes by the parser. + //! \param value value of node to set. Does not have to be zero terminated. + //! \param size Size of value, in characters. This does not include zero terminator, if one is present. + void value(const Ch *value, std::size_t size) RAPIDXML_NOEXCEPT + { + m_value = const_cast(value); + m_value_size = size; + } + + //! Sets value of node to a zero-terminated string. + //! See also \ref ownership_of_strings and xml_node::value(const Ch *, std::size_t). + //! \param value Vame of node to set. Must be zero terminated. + void value(const Ch *value) RAPIDXML_NOEXCEPT + { + this->value(value, internal::measure(value)); + } + + //! Sets the offset inside the source string. + //! This is only intended for debugging purposes. + void offset(Ch *offset) RAPIDXML_NOEXCEPT + { + m_offset = offset; + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets node parent. + //! \return Pointer to parent node, or 0 if there is no parent. + xml_node *parent() const RAPIDXML_NOEXCEPT + { + return m_parent; + } + + protected: + + // Return empty string + static Ch *nullstr() RAPIDXML_NOEXCEPT + { + static Ch zero = Ch('\0'); + return &zero; + } + + Ch *m_name; // Name of node, or 0 if no name + Ch *m_value; // Value of node, or 0 if no value + std::size_t m_name_size; // Length of node name, or undefined of no name + std::size_t m_value_size; // Length of node value, or undefined if no value + xml_node *m_parent; // Pointer to parent node, or 0 if none + Ch *m_offset; // Start offset of this node inside the string + }; + + //! Class representing attribute node of XML document. + //! Each attribute has name and value strings, which are available through name() and value() functions (inherited from xml_base). + //! Note that after parse, both name and value of attribute will point to interior of source text used for parsing. + //! Thus, this text must persist in memory for the lifetime of attribute. + //! \param Ch Character type to use. + template + class xml_attribute: public xml_base + { + + friend class xml_node; + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + //! Constructs an empty attribute with the specified type. + //! Consider using memory_pool of appropriate xml_document if allocating attributes manually. + xml_attribute() + { + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets document of which attribute is a child. + //! \return Pointer to document that contains this attribute, or 0 if there is no parent document. + xml_document *document() const + { + if (xml_node *node = this->parent()) + { + while (node->parent()) + node = node->parent(); + return node->type() == node_document ? static_cast *>(node) : 0; + } + else + return 0; + } + + //! Gets previous attribute, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return previous attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *previous_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_prev_attribute; attribute; attribute = attribute->m_prev_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return this->m_parent ? m_prev_attribute : 0; + } + + //! Gets next attribute, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return next attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *next_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_next_attribute; attribute; attribute = attribute->m_next_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return this->m_parent ? m_next_attribute : 0; + } + + private: + + xml_attribute *m_prev_attribute; // Pointer to previous sibling of attribute, or 0 if none; only valid if parent is non-zero + xml_attribute *m_next_attribute; // Pointer to next sibling of attribute, or 0 if none; only valid if parent is non-zero + + }; + + /////////////////////////////////////////////////////////////////////////// + // XML node + + //! Class representing a node of XML document. + //! Each node may have associated name and value strings, which are available through name() and value() functions. + //! Interpretation of name and value depends on type of the node. + //! Type of node can be determined by using type() function. + //!

+ //! Note that after parse, both name and value of node, if any, will point interior of source text used for parsing. + //! Thus, this text must persist in the memory for the lifetime of node. + //! \param Ch Character type to use. + template + class xml_node: public xml_base + { + + public: + + /////////////////////////////////////////////////////////////////////////// + // Construction & destruction + + //! Constructs an empty node with the specified type. + //! Consider using memory_pool of appropriate document to allocate nodes manually. + //! \param type Type of node to construct. + xml_node(node_type type) + : m_type(type) + , m_first_node(0) + , m_first_attribute(0) + { + } + + /////////////////////////////////////////////////////////////////////////// + // Node data access + + //! Gets type of node. + //! \return Type of node. + node_type type() const + { + return m_type; + } + + /////////////////////////////////////////////////////////////////////////// + // Related nodes access + + //! Gets document of which node is a child. + //! \return Pointer to document that contains this node, or 0 if there is no parent document. + xml_document *document() const + { + xml_node *node = const_cast *>(this); + while (node->parent()) + node = node->parent(); + return node->type() == node_document ? static_cast *>(node) : 0; + } + + //! Gets first child node, optionally matching node name. + //! \param name Name of child to find, or 0 to return first child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found child, or 0 if not found. + xml_node *first_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *child = m_first_node; child; child = child->next_sibling()) + if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) + return child; + return 0; + } + else + return m_first_node; + } + + //! Gets last child node, optionally matching node name. + //! Behaviour is undefined if node has no children. + //! Use first_node() to test if node has children. + //! \param name Name of child to find, or 0 to return last child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found child, or 0 if not found. + xml_node *last_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(m_first_node); // Cannot query for last child if node has no children + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *child = m_last_node; child; child = child->previous_sibling()) + if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) + return child; + return 0; + } + else + return m_last_node; + } + + //! Gets previous sibling node, optionally matching node name. + //! Behaviour is undefined if node has no parent. + //! Use parent() to test if node has a parent. + //! \param name Name of sibling to find, or 0 to return previous sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found sibling, or 0 if not found. + xml_node *previous_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(this->m_parent); // Cannot query for siblings if node has no parent + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *sibling = m_prev_sibling; sibling; sibling = sibling->m_prev_sibling) + if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) + return sibling; + return 0; + } + else + return m_prev_sibling; + } + + //! Gets next sibling node, optionally matching node name. + //! Behaviour is undefined if node has no parent. + //! Use parent() to test if node has a parent. + //! \param name Name of sibling to find, or 0 to return next sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found sibling, or 0 if not found. + xml_node *next_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + assert(this->m_parent); // Cannot query for siblings if node has no parent + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_node *sibling = m_next_sibling; sibling; sibling = sibling->m_next_sibling) + if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) + return sibling; + return 0; + } + else + return m_next_sibling; + } + + //! Gets first attribute of node, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return first attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *first_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_first_attribute; attribute; attribute = attribute->m_next_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return m_first_attribute; + } + + //! Gets last attribute of node, optionally matching attribute name. + //! \param name Name of attribute to find, or 0 to return last attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero + //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string + //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters + //! \return Pointer to found attribute, or 0 if not found. + xml_attribute *last_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const + { + if (name) + { + if (name_size == 0) + name_size = internal::measure(name); + for (xml_attribute *attribute = m_last_attribute; attribute; attribute = attribute->m_prev_attribute) + if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) + return attribute; + return 0; + } + else + return m_first_attribute ? m_last_attribute : 0; + } + + /////////////////////////////////////////////////////////////////////////// + // Node modification + + //! Sets type of node. + //! \param type Type of node to set. + void type(node_type type) + { + m_type = type; + } + + /////////////////////////////////////////////////////////////////////////// + // Node manipulation + + //! Prepends a new child node. + //! The prepended child becomes the first child, and all existing children are moved one position back. + //! \param child Node to prepend. + void prepend_node(xml_node *child) + { + assert(child && !child->parent() && child->type() != node_document); + if (first_node()) + { + child->m_next_sibling = m_first_node; + m_first_node->m_prev_sibling = child; + } + else + { + child->m_next_sibling = 0; + m_last_node = child; + } + m_first_node = child; + child->m_parent = this; + child->m_prev_sibling = 0; + } + + //! Appends a new child node. + //! The appended child becomes the last child. + //! \param child Node to append. + void append_node(xml_node *child) + { + assert(child && !child->parent() && child->type() != node_document); + if (first_node()) + { + child->m_prev_sibling = m_last_node; + m_last_node->m_next_sibling = child; + } + else + { + child->m_prev_sibling = 0; + m_first_node = child; + } + m_last_node = child; + child->m_parent = this; + child->m_next_sibling = 0; + } + + //! Inserts a new child node at specified place inside the node. + //! All children after and including the specified node are moved one position back. + //! \param where Place where to insert the child, or 0 to insert at the back. + //! \param child Node to insert. + void insert_node(xml_node *where, xml_node *child) + { + assert(!where || where->parent() == this); + assert(child && !child->parent() && child->type() != node_document); + if (where == m_first_node) + prepend_node(child); + else if (where == 0) + append_node(child); + else + { + child->m_prev_sibling = where->m_prev_sibling; + child->m_next_sibling = where; + where->m_prev_sibling->m_next_sibling = child; + where->m_prev_sibling = child; + child->m_parent = this; + } + } + + //! Removes first child node. + //! If node has no children, behaviour is undefined. + //! Use first_node() to test if node has children. + void remove_first_node() + { + assert(first_node()); + xml_node *child = m_first_node; + m_first_node = child->m_next_sibling; + if (child->m_next_sibling) + child->m_next_sibling->m_prev_sibling = 0; + else + m_last_node = 0; + child->m_parent = 0; + } + + //! Removes last child of the node. + //! If node has no children, behaviour is undefined. + //! Use first_node() to test if node has children. + void remove_last_node() + { + assert(first_node()); + xml_node *child = m_last_node; + if (child->m_prev_sibling) + { + m_last_node = child->m_prev_sibling; + child->m_prev_sibling->m_next_sibling = 0; + } + else + m_first_node = 0; + child->m_parent = 0; + } + + //! Removes specified child from the node + // \param where Pointer to child to be removed. + void remove_node(xml_node *where) + { + assert(where && where->parent() == this); + assert(first_node()); + if (where == m_first_node) + remove_first_node(); + else if (where == m_last_node) + remove_last_node(); + else + { + where->m_prev_sibling->m_next_sibling = where->m_next_sibling; + where->m_next_sibling->m_prev_sibling = where->m_prev_sibling; + where->m_parent = 0; + } + } + + //! Removes all child nodes (but not attributes). + void remove_all_nodes() + { + for (xml_node *node = first_node(); node; node = node->m_next_sibling) + node->m_parent = 0; + m_first_node = 0; + } + + //! Prepends a new attribute to the node. + //! \param attribute Attribute to prepend. + void prepend_attribute(xml_attribute *attribute) + { + assert(attribute && !attribute->parent()); + if (first_attribute()) + { + attribute->m_next_attribute = m_first_attribute; + m_first_attribute->m_prev_attribute = attribute; + } + else + { + attribute->m_next_attribute = 0; + m_last_attribute = attribute; + } + m_first_attribute = attribute; + attribute->m_parent = this; + attribute->m_prev_attribute = 0; + } + + //! Appends a new attribute to the node. + //! \param attribute Attribute to append. + void append_attribute(xml_attribute *attribute) + { + assert(attribute && !attribute->parent()); + if (first_attribute()) + { + attribute->m_prev_attribute = m_last_attribute; + m_last_attribute->m_next_attribute = attribute; + } + else + { + attribute->m_prev_attribute = 0; + m_first_attribute = attribute; + } + m_last_attribute = attribute; + attribute->m_parent = this; + attribute->m_next_attribute = 0; + } + + //! Inserts a new attribute at specified place inside the node. + //! All attributes after and including the specified attribute are moved one position back. + //! \param where Place where to insert the attribute, or 0 to insert at the back. + //! \param attribute Attribute to insert. + void insert_attribute(xml_attribute *where, xml_attribute *attribute) + { + assert(!where || where->parent() == this); + assert(attribute && !attribute->parent()); + if (where == m_first_attribute) + prepend_attribute(attribute); + else if (where == 0) + append_attribute(attribute); + else + { + attribute->m_prev_attribute = where->m_prev_attribute; + attribute->m_next_attribute = where; + where->m_prev_attribute->m_next_attribute = attribute; + where->m_prev_attribute = attribute; + attribute->m_parent = this; + } + } + + //! Removes first attribute of the node. + //! If node has no attributes, behaviour is undefined. + //! Use first_attribute() to test if node has attributes. + void remove_first_attribute() + { + assert(first_attribute()); + xml_attribute *attribute = m_first_attribute; + if (attribute->m_next_attribute) + { + attribute->m_next_attribute->m_prev_attribute = 0; + } + else + m_last_attribute = 0; + attribute->m_parent = 0; + m_first_attribute = attribute->m_next_attribute; + } + + //! Removes last attribute of the node. + //! If node has no attributes, behaviour is undefined. + //! Use first_attribute() to test if node has attributes. + void remove_last_attribute() + { + assert(first_attribute()); + xml_attribute *attribute = m_last_attribute; + if (attribute->m_prev_attribute) + { + attribute->m_prev_attribute->m_next_attribute = 0; + m_last_attribute = attribute->m_prev_attribute; + } + else + m_first_attribute = 0; + attribute->m_parent = 0; + } + + //! Removes specified attribute from node. + //! \param where Pointer to attribute to be removed. + void remove_attribute(xml_attribute *where) + { + assert(first_attribute() && where->parent() == this); + if (where == m_first_attribute) + remove_first_attribute(); + else if (where == m_last_attribute) + remove_last_attribute(); + else + { + where->m_prev_attribute->m_next_attribute = where->m_next_attribute; + where->m_next_attribute->m_prev_attribute = where->m_prev_attribute; + where->m_parent = 0; + } + } + + //! Removes all attributes of node. + void remove_all_attributes() + { + for (xml_attribute *attribute = first_attribute(); attribute; attribute = attribute->m_next_attribute) + attribute->m_parent = 0; + m_first_attribute = 0; + } + + private: + + /////////////////////////////////////////////////////////////////////////// + // Restrictions + + // No copying + xml_node(const xml_node &); + void operator =(const xml_node &); + + /////////////////////////////////////////////////////////////////////////// + // Data members + + // Note that some of the pointers below have UNDEFINED values if certain other pointers are 0. + // This is required for maximum performance, as it allows the parser to omit initialization of + // unneded/redundant values. + // + // The rules are as follows: + // 1. first_node and first_attribute contain valid pointers, or 0 if node has no children/attributes respectively + // 2. last_node and last_attribute are valid only if node has at least one child/attribute respectively, otherwise they contain garbage + // 3. prev_sibling and next_sibling are valid only if node has a parent, otherwise they contain garbage + + node_type m_type; // Type of node; always valid + xml_node *m_first_node; // Pointer to first child node, or 0 if none; always valid + xml_node *m_last_node; // Pointer to last child node, or 0 if none; this value is only valid if m_first_node is non-zero + xml_attribute *m_first_attribute; // Pointer to first attribute of node, or 0 if none; always valid + xml_attribute *m_last_attribute; // Pointer to last attribute of node, or 0 if none; this value is only valid if m_first_attribute is non-zero + xml_node *m_prev_sibling; // Pointer to previous sibling of node, or 0 if none; this value is only valid if m_parent is non-zero + xml_node *m_next_sibling; // Pointer to next sibling of node, or 0 if none; this value is only valid if m_parent is non-zero + + }; + + /////////////////////////////////////////////////////////////////////////// + // XML document + + //! This class represents root of the DOM hierarchy. + //! It is also an xml_node and a memory_pool through public inheritance. + //! Use parse() function to build a DOM tree from a zero-terminated XML text string. + //! parse() function allocates memory for nodes and attributes by using functions of xml_document, + //! which are inherited from memory_pool. + //! To access root node of the document, use the document itself, as if it was an xml_node. + //! \param Ch Character type to use. + template + class xml_document: public xml_node, public memory_pool + { + + public: + + //! Constructs empty XML document + xml_document() + : xml_node(node_document) + { + } + + //! Parses zero-terminated XML string according to given flags. + //! Passed string will be modified by the parser, unless rapidxml::parse_non_destructive flag is used. + //! The string must persist for the lifetime of the document. + //! In case of error, rapidxml::parse_error exception will be thrown. + //!

+ //! If you want to parse contents of a file, you must first load the file into the memory, and pass pointer to its beginning. + //! Make sure that data is zero-terminated. + //!

+ //! Document can be parsed into multiple times. + //! Each new call to parse removes previous nodes and attributes (if any), but does not clear memory pool. + //! \param text XML data to parse; pointer is non-const to denote fact that this data may be modified by the parser. + template + void parse(Ch *text) + { + assert(text); + + // Remove current contents + this->remove_all_nodes(); + this->remove_all_attributes(); + + // Parse BOM, if any + parse_bom(text); + + // Parse children + while (1) + { + // Skip whitespace before node + skip(text); + if (*text == 0) + break; + + // Parse and append new child + if (*text == Ch('<')) + { + ++text; // Skip '<' + if (xml_node *node = parse_node(text)) + this->append_node(node); + } + else + RAPIDXML_PARSE_ERROR("expected <", text); + } + + } + + //! Clears the document by deleting all nodes and clearing the memory pool. + //! All nodes owned by document pool are destroyed. + void clear() + { + this->remove_all_nodes(); + this->remove_all_attributes(); + memory_pool::clear(); + } + + private: + + /////////////////////////////////////////////////////////////////////// + // Internal character utility functions + + // Detect whitespace character + struct whitespace_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_whitespace[static_cast(ch)]; + } + }; + + // Detect node name character + struct node_name_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_node_name[static_cast(ch)]; + } + }; + + // Detect attribute name character + struct attribute_name_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_attribute_name[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) + struct text_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) that does not require processing + struct text_pure_no_ws_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text_pure_no_ws[static_cast(ch)]; + } + }; + + // Detect text character (PCDATA) that does not require processing + struct text_pure_with_ws_pred + { + static unsigned char test(Ch ch) + { + return internal::lookup_tables<0>::lookup_text_pure_with_ws[static_cast(ch)]; + } + }; + + // Detect attribute value character + template + struct attribute_value_pred + { + static unsigned char test(Ch ch) + { + if (Quote == Ch('\'')) + return internal::lookup_tables<0>::lookup_attribute_data_1[static_cast(ch)]; + if (Quote == Ch('\"')) + return internal::lookup_tables<0>::lookup_attribute_data_2[static_cast(ch)]; + return 0; // Should never be executed, to avoid warnings on Comeau + } + }; + + // Detect attribute value character + template + struct attribute_value_pure_pred + { + static unsigned char test(Ch ch) + { + if (Quote == Ch('\'')) + return internal::lookup_tables<0>::lookup_attribute_data_1_pure[static_cast(ch)]; + if (Quote == Ch('\"')) + return internal::lookup_tables<0>::lookup_attribute_data_2_pure[static_cast(ch)]; + return 0; // Should never be executed, to avoid warnings on Comeau + } + }; + + // Insert coded character, using UTF8 or 8-bit ASCII + template + static void insert_coded_character(Ch *&text, unsigned long code) + { + if (Flags & parse_no_utf8) + { + // Insert 8-bit ASCII character + // Todo: possibly verify that code is less than 256 and use replacement char otherwise? + text[0] = static_cast(code); + text += 1; + } + else + { + // Insert UTF8 sequence + if (code < 0x80) // 1 byte sequence + { + text[0] = static_cast(code); + text += 1; + } + else if (code < 0x800) // 2 byte sequence + { + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xC0); + text += 2; + } + else if (code < 0x10000) // 3 byte sequence + { + text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xE0); + text += 3; + } + else if (code < 0x110000) // 4 byte sequence + { + text[3] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; + text[0] = static_cast(code | 0xF0); + text += 4; + } + else // Invalid, only codes up to 0x10FFFF are allowed in Unicode + { + RAPIDXML_PARSE_ERROR("invalid numeric character entity", text); + } + } + } + + // Skip characters until predicate evaluates to true + template + static void skip(Ch *&text) + { + Ch *tmp = text; + while (StopPred::test(*tmp)) + ++tmp; + text = tmp; + } + + // Skip characters until predicate evaluates to true while doing the following: + // - replacing XML character entity references with proper characters (' & " < > &#...;) + // - condensing whitespace sequences to single space character + template + static Ch *skip_and_expand_character_refs(Ch *&text) + { + // If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip + if (Flags & parse_no_entity_translation && + !(Flags & parse_normalize_whitespace) && + !(Flags & parse_trim_whitespace)) + { + skip(text); + return text; + } + + // Use simple skip until first modification is detected + skip(text); + + // Use translation skip + Ch *src = text; + Ch *dest = src; + while (StopPred::test(*src)) + { + // If entity translation is enabled + if (!(Flags & parse_no_entity_translation)) + { + // Test if replacement is needed + if (src[0] == Ch('&')) + { + switch (src[1]) + { + + // & ' + case Ch('a'): + if (src[2] == Ch('m') && src[3] == Ch('p') && src[4] == Ch(';')) + { + *dest = Ch('&'); + ++dest; + src += 5; + continue; + } + if (src[2] == Ch('p') && src[3] == Ch('o') && src[4] == Ch('s') && src[5] == Ch(';')) + { + *dest = Ch('\''); + ++dest; + src += 6; + continue; + } + break; + + // " + case Ch('q'): + if (src[2] == Ch('u') && src[3] == Ch('o') && src[4] == Ch('t') && src[5] == Ch(';')) + { + *dest = Ch('"'); + ++dest; + src += 6; + continue; + } + break; + + // > + case Ch('g'): + if (src[2] == Ch('t') && src[3] == Ch(';')) + { + *dest = Ch('>'); + ++dest; + src += 4; + continue; + } + break; + + // < + case Ch('l'): + if (src[2] == Ch('t') && src[3] == Ch(';')) + { + *dest = Ch('<'); + ++dest; + src += 4; + continue; + } + break; + + // &#...; - assumes ASCII + case Ch('#'): + if (src[2] == Ch('x')) + { + unsigned long code = 0; + src += 3; // Skip &#x + while (1) + { + unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; + if (digit == 0xFF) + break; + code = code * 16 + digit; + ++src; + } + insert_coded_character(dest, code); // Put character in output + } + else + { + unsigned long code = 0; + src += 2; // Skip &# + while (1) + { + unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; + if (digit == 0xFF) + break; + code = code * 10 + digit; + ++src; + } + insert_coded_character(dest, code); // Put character in output + } + if (*src == Ch(';')) + ++src; + else + RAPIDXML_PARSE_ERROR("expected ;", src); + continue; + + // Something else + default: + // Ignore, just copy '&' verbatim + break; + + } + } + } + + // If whitespace condensing is enabled + if (Flags & parse_normalize_whitespace) + { + // Test if condensing is needed + if (whitespace_pred::test(*src)) + { + *dest = Ch(' '); ++dest; // Put single space in dest + ++src; // Skip first whitespace char + // Skip remaining whitespace chars + while (whitespace_pred::test(*src)) + ++src; + continue; + } + } + + // No replacement, only copy character + *dest++ = *src++; + + } + + // Return new end + text = src; + return dest; + + } + + /////////////////////////////////////////////////////////////////////// + // Internal parsing functions + + // Parse BOM, if any + template + void parse_bom(Ch *&text) + { + // UTF-8? + if (static_cast(text[0]) == 0xEF && + static_cast(text[1]) == 0xBB && + static_cast(text[2]) == 0xBF) + { + text += 3; // Skup utf-8 bom + } + } + + // Parse XML declaration ( + xml_node *parse_xml_declaration(Ch *&text) + { + // If parsing of declaration is disabled + if (!(Flags & parse_declaration_node)) + { + // Skip until end of declaration + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 2; // Skip '?>' + return 0; + } + + // Create declaration + xml_node *declaration = this->allocate_node(node_declaration); + declaration->offset(text); + + // Skip whitespace before attributes or ?> + skip(text); + + // Parse declaration attributes + parse_node_attributes(text, declaration); + + // Skip ?> + if (text[0] != Ch('?') || text[1] != Ch('>')) + RAPIDXML_PARSE_ERROR("expected ?>", text); + text += 2; + + return declaration; + } + + // Parse XML comment (' + return 0; // Do not produce comment node + } + + // Remember value start + Ch *value = text; + + // Skip until end of comment + while (text[0] != Ch('-') || text[1] != Ch('-') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Create comment node + xml_node *comment = this->allocate_node(node_comment); + comment->offset(value); + comment->value(value, text - value); + + // Place zero terminator after comment value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 3; // Skip '-->' + return comment; + } + + // Parse DOCTYPE + template + xml_node *parse_doctype(Ch *&text) + { + // Remember value start + Ch *value = text; + + // Skip to > + while (*text != Ch('>')) + { + // Determine character type + switch (*text) + { + + // If '[' encountered, scan for matching ending ']' using naive algorithm with depth + // This works for all W3C test files except for 2 most wicked + case Ch('['): + { + ++text; // Skip '[' + int depth = 1; + while (depth > 0) + { + switch (*text) + { + case Ch('['): ++depth; break; + case Ch(']'): --depth; break; + case 0: RAPIDXML_PARSE_ERROR("unexpected end of data", text); + } + ++text; + } + break; + } + + // Error on end of text + case Ch('\0'): + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + + // Other character, skip it + default: + ++text; + + } + } + + // If DOCTYPE nodes enabled + if (Flags & parse_doctype_node) + { + // Create a new doctype node + xml_node *doctype = this->allocate_node(node_doctype); + doctype->offset(value); + doctype->value(value, text - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 1; // skip '>' + return doctype; + } + else + { + text += 1; // skip '>' + return 0; + } + + } + + // Parse PI + template + xml_node *parse_pi(Ch *&text) + { + // If creation of PI nodes is enabled + if (Flags & parse_pi_nodes) + { + // Create pi node + xml_node *pi = this->allocate_node(node_pi); + pi->offset(text); + + // Extract PI target name + Ch *name = text; + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected PI target", text); + pi->name(name, text - name); + + // Skip whitespace between pi target and pi + skip(text); + + // Remember start of pi + Ch *value = text; + + // Skip to '?>' + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (*text == Ch('\0')) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Set pi value (verbatim, no entity expansion or whitespace normalization) + pi->value(value, text - value); + + // Place zero terminator after name and value + if (!(Flags & parse_no_string_terminators)) + { + pi->name()[pi->name_size()] = Ch('\0'); + pi->value()[pi->value_size()] = Ch('\0'); + } + + text += 2; // Skip '?>' + return pi; + } + else + { + // Skip to '?>' + while (text[0] != Ch('?') || text[1] != Ch('>')) + { + if (*text == Ch('\0')) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 2; // Skip '?>' + return 0; + } + } + + // Parse and append data + // Return character that ends data. + // This is necessary because this character might have been overwritten by a terminating 0 + template + Ch parse_and_append_data(xml_node *node, Ch *&text, Ch *contents_start) + { + // Backup to contents start if whitespace trimming is disabled + if (!(Flags & parse_trim_whitespace)) + text = contents_start; + + // Skip until end of data + Ch *value = text, *end; + if (Flags & parse_normalize_whitespace) + end = skip_and_expand_character_refs(text); + else + end = skip_and_expand_character_refs(text); + + // Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after > + if (Flags & parse_trim_whitespace) + { + if (Flags & parse_normalize_whitespace) + { + // Whitespace is already condensed to single space characters by skipping function, so just trim 1 char off the end + if (*(end - 1) == Ch(' ')) + --end; + } + else + { + // Backup until non-whitespace character is found + while (whitespace_pred::test(*(end - 1))) + --end; + } + } + + // If characters are still left between end and value (this test is only necessary if normalization is enabled) + // Create new data node + if (!(Flags & parse_no_data_nodes)) + { + xml_node *data = this->allocate_node(node_data); + data->value(value, end - value); + node->append_node(data); + } + + // Add data to parent node if no data exists yet + if (!(Flags & parse_no_element_values)) + if (*node->value() == Ch('\0')) + node->value(value, end - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + { + Ch ch = *text; + *end = Ch('\0'); + return ch; // Return character that ends data; this is required because zero terminator overwritten it + } + + // Return character that ends data + return *text; + } + + // Parse CDATA + template + xml_node *parse_cdata(Ch *&text) + { + // If CDATA is disabled + if (Flags & parse_no_data_nodes) + { + // Skip until end of cdata + while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + text += 3; // Skip ]]> + return 0; // Do not produce CDATA node + } + + // Skip until end of cdata + Ch *value = text; + while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) + { + if (!text[0]) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + + // Create new cdata node + xml_node *cdata = this->allocate_node(node_cdata); + cdata->offset(value); + cdata->value(value, text - value); + + // Place zero terminator after value + if (!(Flags & parse_no_string_terminators)) + *text = Ch('\0'); + + text += 3; // Skip ]]> + return cdata; + } + + // Parse element node + template + xml_node *parse_element(Ch *&text) + { + // Create element node + xml_node *element = this->allocate_node(node_element); + element->offset(text); + + // Extract element name + Ch *name = text; + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected element name", text); + element->name(name, text - name); + + // Skip whitespace between element name and attributes or > + skip(text); + + // Parse attributes, if any + parse_node_attributes(text, element); + + // Determine ending type + if (*text == Ch('>')) + { + ++text; + parse_node_contents(text, element); + } + else if (*text == Ch('/')) + { + ++text; + if (*text != Ch('>')) + RAPIDXML_PARSE_ERROR("expected >", text); + ++text; + } + else + RAPIDXML_PARSE_ERROR("expected >", text); + + // Place zero terminator after name + if (!(Flags & parse_no_string_terminators)) + element->name()[element->name_size()] = Ch('\0'); + + // Return parsed element + return element; + } + + // Determine node type, and parse it + template + xml_node *parse_node(Ch *&text) + { + // Parse proper node type + switch (text[0]) + { + + // <... + default: + // Parse and append element node + return parse_element(text); + + // (text); + } + else + { + // Parse PI + return parse_pi(text); + } + + // (text); + } + break; + + // (text); + } + break; + + // (text); + } + + } // switch + + // Attempt to skip other, unrecognized node types starting with ')) + { + if (*text == 0) + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + ++text; + } + ++text; // Skip '>' + return 0; // No node recognized + + } + } + + // Parse contents of the node - children, data etc. + template + void parse_node_contents(Ch *&text, xml_node *node) + { + // For all children and text + while (1) + { + // Skip whitespace between > and node contents + Ch *contents_start = text; // Store start of node contents before whitespace is skipped + skip(text); + Ch next_char = *text; + + // After data nodes, instead of continuing the loop, control jumps here. + // This is because zero termination inside parse_and_append_data() function + // would wreak havoc with the above code. + // Also, skipping whitespace after data nodes is unnecessary. + after_data_node: + + // Determine what comes next: node closing, child node, data node, or 0? + switch (next_char) + { + + // Node closing or child node + case Ch('<'): + if (text[1] == Ch('/')) + { + // Node closing + text += 2; // Skip '(text); + if (!internal::compare(node->name(), node->name_size(), closing_name, text - closing_name, true)) + RAPIDXML_PARSE_ERROR("invalid closing tag name", text); + } + else + { + // No validation, just skip name + skip(text); + } + // Skip remaining whitespace after node name + skip(text); + if (*text != Ch('>')) + RAPIDXML_PARSE_ERROR("expected >", text); + ++text; // Skip '>' + return; // Node closed, finished parsing contents + } + else + { + // Child node + ++text; // Skip '<' + if (xml_node *child = parse_node(text)) + node->append_node(child); + } + break; + + // End of data - error + case Ch('\0'): + RAPIDXML_PARSE_ERROR("unexpected end of data", text); + + // Data node + default: + next_char = parse_and_append_data(node, text, contents_start); + goto after_data_node; // Bypass regular processing after data nodes + + } + } + } + + // Parse XML attributes of the node + template + void parse_node_attributes(Ch *&text, xml_node *node) + { + // For all attributes + while (attribute_name_pred::test(*text)) + { + // Extract attribute name + Ch *name = text; + ++text; // Skip first character of attribute name + skip(text); + if (text == name) + RAPIDXML_PARSE_ERROR("expected attribute name", name); + + // Create new attribute + xml_attribute *attribute = this->allocate_attribute(); + attribute->name(name, text - name); + node->append_attribute(attribute); + + // Skip whitespace after attribute name + skip(text); + + // Skip = + if (*text != Ch('=')) + RAPIDXML_PARSE_ERROR("expected =", text); + ++text; + + // Add terminating zero after name + if (!(Flags & parse_no_string_terminators)) + attribute->name()[attribute->name_size()] = 0; + + // Skip whitespace after = + skip(text); + + // Skip quote and remember if it was ' or " + Ch quote = *text; + if (quote != Ch('\'') && quote != Ch('"')) + RAPIDXML_PARSE_ERROR("expected ' or \"", text); + ++text; + + // Extract attribute value and expand char refs in it + Ch *value = text, *end; + const int AttFlags = Flags & ~parse_normalize_whitespace; // No whitespace normalization in attributes + if (quote == Ch('\'')) + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + else + end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); + + // Set attribute value + attribute->value(value, end - value); + + // Make sure that end quote is present + if (*text != quote) + RAPIDXML_PARSE_ERROR("expected ' or \"", text); + ++text; // Skip quote + + // Add terminating zero after value + if (!(Flags & parse_no_string_terminators)) + attribute->value()[attribute->value_size()] = 0; + + // Skip whitespace after attribute value + skip(text); + } + } + + }; + + //! \cond internal + namespace internal + { + + // Whitespace (space \n \r \t) + template + const unsigned char lookup_tables::lookup_whitespace[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // F + }; + + // Node name (anything but space \n \r \t / > ? \0) + template + const unsigned char lookup_tables::lookup_node_name[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) (anything but < \0) + template + const unsigned char lookup_tables::lookup_text[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) that does not require processing when ws normalization is disabled + // (anything but < \0 &) + template + const unsigned char lookup_tables::lookup_text_pure_no_ws[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Text (i.e. PCDATA) that does not require processing when ws normalizationis is enabled + // (anything but < \0 & space \n \r \t) + template + const unsigned char lookup_tables::lookup_text_pure_with_ws[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute name (anything but space \n \r \t / < > = ? ! \0) + template + const unsigned char lookup_tables::lookup_attribute_name[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with single quote (anything but ' \0) + template + const unsigned char lookup_tables::lookup_attribute_data_1[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with single quote that does not require processing (anything but ' \0 &) + template + const unsigned char lookup_tables::lookup_attribute_data_1_pure[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with double quote (anything but " \0) + template + const unsigned char lookup_tables::lookup_attribute_data_2[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Attribute data with double quote that does not require processing (anything but " \0 &) + template + const unsigned char lookup_tables::lookup_attribute_data_2_pure[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F + }; + + // Digits (dec and hex, 255 denotes end of numeric character reference) + template + const unsigned char lookup_tables::lookup_digits[256] = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3 + 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5 + 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9 + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 // F + }; + + // Upper case conversion + template + const unsigned char lookup_tables::lookup_upcase[256] = + { + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A B C D E F + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, // 0 + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, // 1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, // 2 + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // 3 + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 4 + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, // 5 + 96, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 6 + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 123,124,125,126,127, // 7 + 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, // 8 + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, // 9 + 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175, // A + 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, // B + 192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207, // C + 208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223, // D + 224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239, // E + 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255 // F + }; + } + //! \endcond + +} + +// Undefine internal macros +#undef RAPIDXML_PARSE_ERROR + +// On MSVC, restore warnings state +#ifdef _MSC_VER + #pragma warning(pop) +#endif + +#endif diff --git a/crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp new file mode 100644 index 0000000..68cf57f --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp @@ -0,0 +1,295 @@ +#ifndef RAPIDXML_ITERATORS_HPP_INCLUDED +#define RAPIDXML_ITERATORS_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/15 23:02:39 $ +//! \file rapidxml_iterators.hpp This file contains rapidxml iterators + +#include "rapidxml.hpp" + +namespace rapidxml +{ + const unsigned int iterate_check_name = 1 << 0; + const unsigned int iterate_case_sensitive = 1 << 1; + + //! Iterator of child nodes of xml_node + template + class node_iterator + { + public: + typedef xml_node *value_type; + typedef const value_type& reference; + typedef xml_node *pointer; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + + node_iterator() + : m_cur(0) + , m_prev(0) + , m_flags(0) + { + } + + node_iterator(xml_node* node, xml_node* prev, + unsigned char flags) + : m_cur(node) + , m_prev(prev) + , m_flags(flags) + { + } + + reference operator*() const + { + assert(m_cur); + return m_cur; + } + + pointer operator->() const + { + assert(m_cur); + return m_cur; + } + + node_iterator& operator++() + { + increment(); + return *this; + } + + node_iterator operator++(int) + { + node_iterator tmp = *this; + increment(); + return tmp; + } + + node_iterator& operator--() + { + decrement(); + return *this; + } + + node_iterator operator--(int) + { + node_iterator tmp = *this; + decrement(); + return tmp; + } + + bool operator==(const node_iterator &rhs) const + { + return m_cur == rhs.m_cur; + } + + bool operator!=(const node_iterator &rhs) const + { + return m_cur != rhs.m_cur; + } + + private: + void increment() + { + assert(m_cur && "Attempted to increment end iterator"); + m_prev = m_cur; + + if (m_flags & iterate_check_name) + m_cur = m_cur->next_sibling( + m_cur->name(), m_cur->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_cur = m_cur->next_sibling(); + } + + void decrement() + { + assert(m_prev && "Attempted to decrement begin iterator"); + m_cur = m_prev; + + if (m_flags & iterate_check_name) + m_prev = m_prev->previous_sibling( + m_prev->name(), m_prev->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_prev = m_prev->previous_sibling(); + } + + xml_node *m_cur; + xml_node *m_prev; + unsigned char m_flags; + }; + + //! Iterator of child attributes of xml_node + template + class attribute_iterator + { + public: + typedef xml_attribute *value_type; + typedef const value_type& reference; + typedef xml_attribute *pointer; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + + attribute_iterator() + : m_cur(0) + , m_prev(0) + , m_flags(0) + { + } + + attribute_iterator(xml_attribute* attr, xml_attribute* prev, + unsigned char flags) + : m_cur(attr) + , m_prev(prev) + , m_flags(flags) + { + } + + reference operator*() const + { + assert(m_cur); + return m_cur; + } + + pointer operator->() const + { + assert(m_cur); + return m_cur; + } + + attribute_iterator& operator++() + { + increment(); + return *this; + } + + attribute_iterator operator++(int) + { + attribute_iterator tmp = *this; + increment(); + return tmp; + } + + attribute_iterator& operator--() + { + decrement(); + return *this; + } + + attribute_iterator operator--(int) + { + attribute_iterator tmp = *this; + decrement(); + return tmp; + } + + bool operator==(const attribute_iterator &rhs) const + { + return m_cur == rhs.m_cur; + } + + bool operator!=(const attribute_iterator &rhs) const + { + return m_cur != rhs.m_cur; + } + + private: + void increment() + { + assert(m_cur && "Attempted to increment end iterator"); + m_prev = m_cur; + + if (m_flags & iterate_check_name) + m_cur = m_cur->next_attribute( + m_cur->name(), m_cur->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_cur = m_cur->next_attribute(); + } + + void decrement() + { + assert(m_prev && "Attempted to decrement begin iterator"); + m_cur = m_prev; + + if (m_flags & iterate_check_name) + m_prev = m_prev->previous_attribute( + m_prev->name(), m_prev->name_size(), + !!(m_flags & iterate_case_sensitive)); + else + m_prev = m_prev->previous_attribute(); + } + + xml_attribute* m_cur; + xml_attribute* m_prev; + unsigned char m_flags; + }; + + // Range-based for loop support + template + class iterator_range + { + public: + typedef Iterator const_iterator; + typedef Iterator iterator; + + iterator_range(Iterator first, Iterator last) + : m_first(first) + , m_last(last) + { + } + + Iterator begin() const { return m_first; } + Iterator end() const { return m_last; } + + private: + Iterator m_first; + Iterator m_last; + }; + + template + iterator_range> nodes(const xml_node* node, + const Ch* name = 0, + std::size_t name_size = 0, + bool case_sensitive = true) + { + unsigned char flags = 0; + if (name) + flags |= iterate_check_name; + if (case_sensitive) + flags |= iterate_case_sensitive; + + xml_node* first = + node->first_node(name, name_size, case_sensitive); + xml_node* last = first ? + node->last_node(name, name_size, case_sensitive) : nullptr; + + node_iterator begin(first, 0, flags); + node_iterator end(0, last, flags); + return iterator_range>(begin, end); + } + + template + iterator_range> attributes(const xml_node* node, + const Ch *name = 0, + std::size_t name_size = 0, + bool case_sensitive = true) + { + unsigned char flags = 0; + if (name) + flags |= iterate_check_name; + if (case_sensitive) + flags |= iterate_case_sensitive; + + xml_attribute* first = + node->first_attribute(name, name_size, case_sensitive); + xml_attribute* last = + node->last_attribute(name, name_size, case_sensitive); + + attribute_iterator begin(first, 0, flags); + attribute_iterator end(0, last, flags); + return iterator_range>(begin, end); + } +} + +#endif diff --git a/crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp new file mode 100644 index 0000000..ae80e1f --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp @@ -0,0 +1,422 @@ +#ifndef RAPIDXML_PRINT_HPP_INCLUDED +#define RAPIDXML_PRINT_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/13 01:46:17 $ +//! \file rapidxml_print.hpp This file contains rapidxml printer implementation + +#include "rapidxml.hpp" + +// Only include streams if not disabled +#ifndef RAPIDXML_NO_STREAMS + #include + #include +#endif + +namespace rapidxml +{ + + /////////////////////////////////////////////////////////////////////// + // Printing flags + + const int print_no_indenting = 0x1; //!< Printer flag instructing the printer to suppress indenting of XML. See print() function. + + /////////////////////////////////////////////////////////////////////// + // Internal + + //! \cond internal + namespace internal + { + + /////////////////////////////////////////////////////////////////////////// + // Internal character operations + + // Copy characters from given range to given output iterator + template + inline OutIt copy_chars(const Ch *begin, const Ch *end, OutIt out) + { + while (begin != end) + *out++ = *begin++; + return out; + } + + // Copy characters from given range to given output iterator and expand + // characters into references (< > ' " &) + template + inline OutIt copy_and_expand_chars(const Ch *begin, const Ch *end, Ch noexpand, OutIt out) + { + while (begin != end) + { + if (*begin == noexpand) + { + *out++ = *begin; // No expansion, copy character + } + else + { + switch (*begin) + { + case Ch('<'): + *out++ = Ch('&'); *out++ = Ch('l'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('>'): + *out++ = Ch('&'); *out++ = Ch('g'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('\''): + *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('p'); *out++ = Ch('o'); *out++ = Ch('s'); *out++ = Ch(';'); + break; + case Ch('"'): + *out++ = Ch('&'); *out++ = Ch('q'); *out++ = Ch('u'); *out++ = Ch('o'); *out++ = Ch('t'); *out++ = Ch(';'); + break; + case Ch('&'): + *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('m'); *out++ = Ch('p'); *out++ = Ch(';'); + break; + default: + *out++ = *begin; // No expansion, copy character + } + } + ++begin; // Step to next character + } + return out; + } + + // Fill given output iterator with repetitions of the same character + template + inline OutIt fill_chars(OutIt out, int n, Ch ch) + { + for (int i = 0; i < n; ++i) + *out++ = ch; + return out; + } + + // Find character + template + inline bool find_char(const Ch *begin, const Ch *end) + { + while (begin != end) + if (*begin++ == ch) + return true; + return false; + } + + /////////////////////////////////////////////////////////////////////////// + // Internal printing operations + + template + OutIt print_node(OutIt out, const xml_node *node, int flags, int indent); + + // Print children of the node + template + inline OutIt print_children(OutIt out, const xml_node *node, int flags, int indent) + { + for (xml_node *child = node->first_node(); child; child = child->next_sibling()) + out = print_node(out, child, flags, indent); + return out; + } + + // Print attributes of the node + template + inline OutIt print_attributes(OutIt out, const xml_node *node, int flags) + { + for (xml_attribute *attribute = node->first_attribute(); attribute; attribute = attribute->next_attribute()) + { + if (attribute->name() && attribute->value()) + { + // Print attribute name + *out = Ch(' '), ++out; + out = copy_chars(attribute->name(), attribute->name() + attribute->name_size(), out); + *out = Ch('='), ++out; + // Print attribute value using appropriate quote type + if (find_char(attribute->value(), attribute->value() + attribute->value_size())) + { + *out = Ch('\''), ++out; + out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('"'), out); + *out = Ch('\''), ++out; + } + else + { + *out = Ch('"'), ++out; + out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('\''), out); + *out = Ch('"'), ++out; + } + } + } + return out; + } + + // Print data node + template + inline OutIt print_data_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_data); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); + return out; + } + + // Print data node + template + inline OutIt print_cdata_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_cdata); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'); ++out; + *out = Ch('!'); ++out; + *out = Ch('['); ++out; + *out = Ch('C'); ++out; + *out = Ch('D'); ++out; + *out = Ch('A'); ++out; + *out = Ch('T'); ++out; + *out = Ch('A'); ++out; + *out = Ch('['); ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch(']'); ++out; + *out = Ch(']'); ++out; + *out = Ch('>'); ++out; + return out; + } + + // Print element node + template + inline OutIt print_element_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_element); + + // Print element name and attributes, if any + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + out = print_attributes(out, node, flags); + + // If node is childless + if (node->value_size() == 0 && !node->first_node()) + { + // Print childless node tag ending + *out = Ch('/'), ++out; + *out = Ch('>'), ++out; + } + else + { + // Print normal node tag ending + *out = Ch('>'), ++out; + + // Test if node contains a single data node only (and no other nodes) + xml_node *child = node->first_node(); + if (!child) + { + // If node has no children, only print its value without indenting + out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); + } + else if (child->next_sibling() == 0 && child->type() == node_data) + { + // If node has a sole data child, only print its value without indenting + out = copy_and_expand_chars(child->value(), child->value() + child->value_size(), Ch(0), out); + } + else + { + // Print all children with full indenting + if (!(flags & print_no_indenting)) + *out = Ch('\n'), ++out; + out = print_children(out, node, flags, indent + 1); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + } + + // Print node end + *out = Ch('<'), ++out; + *out = Ch('/'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + *out = Ch('>'), ++out; + } + return out; + } + + // Print declaration node + template + inline OutIt print_declaration_node(OutIt out, const xml_node *node, int flags, int indent) + { + // Print declaration start + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('?'), ++out; + *out = Ch('x'), ++out; + *out = Ch('m'), ++out; + *out = Ch('l'), ++out; + + // Print attributes + out = print_attributes(out, node, flags); + + // Print declaration end + *out = Ch('?'), ++out; + *out = Ch('>'), ++out; + + return out; + } + + // Print comment node + template + inline OutIt print_comment_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_comment); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('!'), ++out; + *out = Ch('-'), ++out; + *out = Ch('-'), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('-'), ++out; + *out = Ch('-'), ++out; + *out = Ch('>'), ++out; + return out; + } + + // Print doctype node + template + inline OutIt print_doctype_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_doctype); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('!'), ++out; + *out = Ch('D'), ++out; + *out = Ch('O'), ++out; + *out = Ch('C'), ++out; + *out = Ch('T'), ++out; + *out = Ch('Y'), ++out; + *out = Ch('P'), ++out; + *out = Ch('E'), ++out; + *out = Ch(' '), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('>'), ++out; + return out; + } + + // Print pi node + template + inline OutIt print_pi_node(OutIt out, const xml_node *node, int flags, int indent) + { + assert(node->type() == node_pi); + if (!(flags & print_no_indenting)) + out = fill_chars(out, indent, Ch('\t')); + *out = Ch('<'), ++out; + *out = Ch('?'), ++out; + out = copy_chars(node->name(), node->name() + node->name_size(), out); + *out = Ch(' '), ++out; + out = copy_chars(node->value(), node->value() + node->value_size(), out); + *out = Ch('?'), ++out; + *out = Ch('>'), ++out; + return out; + } + + // Print node + template + inline OutIt print_node(OutIt out, const xml_node *node, int flags, int indent) + { + // Print proper node type + switch (node->type()) + { + // Document + case node_document: + out = print_children(out, node, flags, indent); + break; + + // Element + case node_element: + out = print_element_node(out, node, flags, indent); + break; + + // Data + case node_data: + out = print_data_node(out, node, flags, indent); + break; + + // CDATA + case node_cdata: + out = print_cdata_node(out, node, flags, indent); + break; + + // Declaration + case node_declaration: + out = print_declaration_node(out, node, flags, indent); + break; + + // Comment + case node_comment: + out = print_comment_node(out, node, flags, indent); + break; + + // Doctype + case node_doctype: + out = print_doctype_node(out, node, flags, indent); + break; + + // Pi + case node_pi: + out = print_pi_node(out, node, flags, indent); + break; + + // Unknown + default: + assert(0); + break; + } + + // If indenting not disabled, add line break after node + if (!(flags & print_no_indenting)) + *out = Ch('\n'), ++out; + + // Return modified iterator + return out; + } + } + //! \endcond + + /////////////////////////////////////////////////////////////////////////// + // Printing + + //! Prints XML to given output iterator. + //! \param out Output iterator to print to. + //! \param node Node to be printed. Pass xml_document to print entire document. + //! \param flags Flags controlling how XML is printed. + //! \return Output iterator pointing to position immediately after last character of printed text. + template + inline OutIt print(OutIt out, const xml_node &node, int flags = 0) + { + return internal::print_node(out, &node, flags, 0); + } + +#ifndef RAPIDXML_NO_STREAMS + + //! Prints XML to given output stream. + //! \param out Output stream to print to. + //! \param node Node to be printed. Pass xml_document to print entire document. + //! \param flags Flags controlling how XML is printed. + //! \return Output stream. + template + inline std::basic_ostream &print(std::basic_ostream &out, const xml_node &node, int flags = 0) + { + print(std::ostream_iterator(out), node, flags); + return out; + } + + //! Prints formatted XML to given output stream. Uses default printing flags. Use print() function to customize printing process. + //! \param out Output stream to print to. + //! \param node Node to be printed. + //! \return Output stream. + template + inline std::basic_ostream &operator <<(std::basic_ostream &out, const xml_node &node) + { + return print(out, node); + } + +#endif + +} + +#endif diff --git a/crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp new file mode 100644 index 0000000..91cf83e --- /dev/null +++ b/crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp @@ -0,0 +1,56 @@ +#ifndef RAPIDXML_UTILS_HPP_INCLUDED +#define RAPIDXML_UTILS_HPP_INCLUDED + +// Copyright (C) 2006, 2009 Marcin Kalicinski +// Version 1.13 +// Revision $DateTime: 2009/05/15 23:02:39 $ +//! \file rapidxml_utils.hpp This file contains high-level rapidxml utilities that can be useful +//! in certain simple scenarios. They should probably not be used if maximizing performance is the main objective. + +#include "rapidxml.hpp" +#include + +namespace rapidxml +{ + //! Counts children of node. Time complexity is O(n). + //! \return Number of children of node + template + inline std::size_t count_children(const xml_node *node, + const Ch* name = 0, + std::size_t name_size = 0) + { + if (name && name_size == 0) + name_size = internal::measure(name); + + xml_node *child = node->first_node(name, name_size); + std::size_t count = 0; + while (child) + { + ++count; + child = child->next_sibling(name, name_size); + } + return count; + } + + //! Counts attributes of node. Time complexity is O(n). + //! \return Number of attributes of node + template + inline std::size_t count_attributes(const xml_node *node, + const Ch* name = 0, + std::size_t name_size = 0) + { + if (name && name_size == 0) + name_size = internal::measure(name); + + xml_attribute *attr = node->first_attribute(name, name_size); + std::size_t count = 0; + while (attr) + { + ++count; + attr = attr->next_attribute(name, name_size); + } + return count; + } +} + +#endif diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index b70119a..0daa425 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -709,7 +709,7 @@ impl CommonAttributes { Ok(f) => { if let Some(x) = array.get_mut(index) { *x = f; - self.rotate = Vec3(glam::Vec3::from_array(array.into())); + self.rotate = Vec3(glam::Vec3::from_array(array)); self.active_attributes.insert(ActiveAttributes::rotate); } } diff --git a/crates/joko_plugin_manager/Cargo.toml b/crates/joko_plugin_manager/Cargo.toml new file mode 100644 index 0000000..79043dc --- /dev/null +++ b/crates/joko_plugin_manager/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "joko_plugin_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +joko_component_models = { path = "../joko_component_models" } +scopeguard = "1.2.0" +smol_str = { workspace = true } +tokio = { workspace = true } diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs new file mode 100644 index 0000000..e612d7b --- /dev/null +++ b/crates/joko_plugin_manager/src/lib.rs @@ -0,0 +1,29 @@ +use joko_component_models::{ + ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, +}; + +pub struct JokolayPlugin {} + +pub struct JokolayPluginManager {} + +impl JokolayComponent<(), ()> for JokolayPlugin { + fn flush_all_messages(&mut self) {} + fn tick(&mut self, _timestamp: f64) -> Option<&()> { + None + } + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + _input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + } +} +impl JokolayComponentDeps for JokolayPlugin {} diff --git a/crates/joko_render_manager/Cargo.toml b/crates/joko_render_manager/Cargo.toml new file mode 100644 index 0000000..de8b2fa --- /dev/null +++ b/crates/joko_render_manager/Cargo.toml @@ -0,0 +1,24 @@ +# Define all structures that can be sent through asynchronous messages + +[package] +name = "joko_render_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bincode = { workspace = true } +bytemuck = { workspace = true } +glam = { workspace = true, features = ["bytemuck"] } +tracing = { workspace = true } +egui = { workspace = true } +egui_render_three_d = { version = "*" } +egui_window_glfw_passthrough = { version = "0.8" } +tokio = { workspace = true } + + +joko_component_models = { path = "../joko_component_models" } +joko_link_models = { path = "../joko_link_models" } +joko_render_models = { path = "../joko_render_models" } + diff --git a/crates/joko_render_manager/shaders/marker.fs b/crates/joko_render_manager/shaders/marker.fs new file mode 100644 index 0000000..90dad16 --- /dev/null +++ b/crates/joko_render_manager/shaders/marker.fs @@ -0,0 +1,18 @@ +#version 450 + +layout(location = 0) in vec2 vtex_coord; +layout(location = 1) in float valpha; +layout(location = 2) in vec4 vcolor; + +layout(location = 0) out vec4 ocolor; + +layout(location = 1) uniform sampler2D sam; + +void main() { + vec4 color = texture(sam, vtex_coord, -2.0); + color.a = color.a * valpha; + if (color.a < 0.01) { + discard; + } + ocolor = color; +} diff --git a/crates/joko_render_manager/shaders/marker.vs b/crates/joko_render_manager/shaders/marker.vs new file mode 100644 index 0000000..adbb641 --- /dev/null +++ b/crates/joko_render_manager/shaders/marker.vs @@ -0,0 +1,39 @@ +#version 450 + +layout(location = 0) in vec4 position; +layout(location = 1) in float alpha; +layout(location = 2) in vec2 tex_coord; +layout(location = 3) in vec2 fade_near_far; +layout(location = 4) in vec4 color; + +layout(location = 0) out vec2 vtex_coord; +layout(location = 1) out float valpha; +layout(location = 2) out vec4 vcolor; + + +layout(location = 0) uniform vec3 camera_pos; +// location 1 is for sampler in frag shader +layout(location = 2) uniform mat4 transform; + + +void main( +) { + valpha = alpha; + vtex_coord = tex_coord; + gl_Position = transform * position; + vcolor = color; + + float dist = distance(camera_pos, position.xyz); + if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { + // if distance is exactly fade_near, we will multiply with 1.0 + // if its more, then we will multiply with how far we are in between fade_near and fade_far + float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); + // The actual alpha + valpha *= ratio; + } + if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { + valpha = 0.0; + } +} + + diff --git a/crates/joko_render_manager/shaders/marker.wgsl b/crates/joko_render_manager/shaders/marker.wgsl new file mode 100644 index 0000000..e69de29 diff --git a/crates/joko_render_manager/shaders/player_visibility.wgsl b/crates/joko_render_manager/shaders/player_visibility.wgsl new file mode 100644 index 0000000..ba3c564 --- /dev/null +++ b/crates/joko_render_manager/shaders/player_visibility.wgsl @@ -0,0 +1,24 @@ + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) ndc_pos: vec2 +}; + +@vertex +fn vs_main( + @location(0) position: vec2, +) -> VertexOutput { + var result: VertexOutput; + + result.position = vec4(position.xy, 0.5, 1.0); + result.ndc_pos = position; + return result; +} + + +@fragment +fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { + let alpha: f32 = distance(vertex.ndc_pos.xy, vec2(0.0, 0.0)); + return vec4(0.0, 0.0, 0.0, pow(alpha, 5.0) / 2.0); +} + diff --git a/crates/joko_render_manager/shaders/trail.fs b/crates/joko_render_manager/shaders/trail.fs new file mode 100644 index 0000000..e8ca1d4 --- /dev/null +++ b/crates/joko_render_manager/shaders/trail.fs @@ -0,0 +1,22 @@ +#version 450 + +layout(location = 0) in vec2 vtex_coord; +layout(location = 1) in float valpha; +layout(location = 2) in vec4 vcolor; + +layout(location = 0) out vec4 ocolor; + +layout(location = 1) uniform sampler2D sam; // wrap_s = "REPEAT" wrap_t = "REPEAT" +layout(location = 3) uniform vec2 scroll_texture; + +void main() { + //vec4 color = texture(sam, vec2 (vtex_coord.x + scroll_texture.x, vtex_coord.y + scroll_texture.y), -2.0); + vec4 color = texture(sam, vec2 (vtex_coord.x + 0.0, vtex_coord.y + scroll_texture.y), -2.0); + //vec4 color = texture(sam, vtex_coord + scroll_texture); + //vec4 color = texture(sam, vtex_coord, -2.0);//original working + color.a = color.a * valpha; + if (color.a < 0.01) { + discard; + } + ocolor = color; +} diff --git a/crates/joko_render_manager/shaders/trail.vs b/crates/joko_render_manager/shaders/trail.vs new file mode 100644 index 0000000..c8f04b6 --- /dev/null +++ b/crates/joko_render_manager/shaders/trail.vs @@ -0,0 +1,37 @@ +#version 450 + +layout(location = 0) in vec4 position; +layout(location = 1) in float alpha; +layout(location = 2) in vec2 tex_coord; +layout(location = 3) in vec2 fade_near_far; +layout(location = 4) in vec4 color; + + +layout(location = 0) out vec2 vtex_coord; +layout(location = 1) out float valpha; +layout(location = 2) out vec4 vcolor; + +layout(location = 0) uniform vec3 camera_pos; +// location 1 is for sampler in frag shader +layout(location = 2) uniform mat4 transform; +// location 3 is for scroll_texture + +void main( +) { + valpha = alpha; + vtex_coord = tex_coord; + gl_Position = transform * position; + vcolor = color; + + float dist = distance(camera_pos, position.xyz); + if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { + // if distance is exactly fade_near, we will multiply with 1.0 + // if its more, then we will multiply with how far we are in between fade_near and fade_far + float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); + // The actual alpha + valpha *= ratio; + } + if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { + valpha = 0.0; + } +} diff --git a/crates/joko_render_manager/src/billboard.rs b/crates/joko_render_manager/src/billboard.rs new file mode 100644 index 0000000..96ee03a --- /dev/null +++ b/crates/joko_render_manager/src/billboard.rs @@ -0,0 +1,360 @@ +use egui::ahash::HashMap; +use egui_render_three_d::{ + three_d::{context::*, Context, HasContext}, + GpuTexture, +}; +use glam::Vec2; +use joko_render_models::{ + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject, +}; +use tracing::{error, info, trace, warn}; + +use crate::gl_error; + +const MARKER_VERTEX_STRIDE: i32 = std::mem::size_of::() as _; +pub struct BillBoardRenderer { + pub markers: Vec, + pub trails: Vec, + pub markers_wip: Vec, //work in progress: this is where the markers are inserted + pub trails_wip: Vec, //work in progress: this is where the markers are inserted + marker_program: NativeProgram, + marker_vertex_buffer: NativeBuffer, + marker_vertex_array: NativeVertexArray, + + trail_program: NativeProgram, + trail_vertex_buffers: Vec, + trail_vertex_arrays: Vec, +} + +const MARKER_VERTEX_SHADER: &str = include_str!("../shaders/marker.vs"); +const MARKER_FRAGMENT_SHADER: &str = include_str!("../shaders/marker.fs"); +const TRAIL_VERTEX_SHADER: &str = include_str!("../shaders/trail.vs"); +const TRAIL_FRAGMENT_SHADER: &str = include_str!("../shaders/trail.fs"); + +impl BillBoardRenderer { + pub fn new(gl: &Context) -> Self { + unsafe { + let marker_program = + new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); + gl_error!(gl); + + let trail_shift_program = + new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); + gl_error!(gl); + + let marker_vertex_buffer = create_buffer(gl); + let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); + + Self { + markers: Vec::new(), + markers_wip: Vec::new(), + + marker_program, + marker_vertex_buffer, + marker_vertex_array, + + trails: Vec::new(), + trails_wip: Vec::new(), + + trail_program: trail_shift_program, + trail_vertex_buffers: Default::default(), + trail_vertex_arrays: Default::default(), + } + } + } + + pub fn swap(&mut self) { + trace!( + "swap UI to display {} markers, {} trails", + self.markers_wip.len(), + self.trails_wip.len() + ); + self.markers = std::mem::take(&mut self.markers_wip); + self.trails = std::mem::take(&mut self.trails_wip); + } + + pub fn prepare_render_data(&mut self, gl: &Context) { + /* + TODO: map view (view from above) + trim down the trails too far ? + fatten them ? + */ + unsafe { + gl_error!(gl); + } + // sort by depth + self.markers.sort_unstable_by(|first, second| { + first.distance.total_cmp(&second.distance).reverse() // we need the farther markers (more distance from camera) to be rendered first, for correct alpha blending + }); + + let mut required_size_in_bytes = + (self.markers.len() * 6 * std::mem::size_of::()) as u64; + for trail in self.trails.iter() { + let len = (trail.vertices.len() * std::mem::size_of::()) as u64; + required_size_in_bytes = required_size_in_bytes.max(len); + } + let mut vb: Vec = Vec::with_capacity(self.markers.len() * 6); + + for marker_object in self.markers.iter() { + vb.extend_from_slice(&marker_object.vertices); + } + unsafe { + gl_error!(gl); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); + gl.buffer_data_u8_slice(ARRAY_BUFFER, bytemuck::cast_slice(&vb), DYNAMIC_DRAW); + gl_error!(gl); + } + if self.trails.len() > self.trail_vertex_buffers.len() { + let needs = self.trails.len() - self.trail_vertex_buffers.len(); + for _ in 0..needs { + let vb = unsafe { create_buffer(gl) }; + self.trail_vertex_buffers.push(vb); + let trail_vertex_array = unsafe { create_trail_array(gl, vb, 1) }; + self.trail_vertex_arrays.push(trail_vertex_array); + } + } + for (trail, trail_buffer) in self.trails.iter().zip(self.trail_vertex_buffers.iter()) { + unsafe { + gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); + gl.buffer_data_u8_slice( + ARRAY_BUFFER, + bytemuck::cast_slice(trail.vertices.as_ref()), + DYNAMIC_DRAW, + ); + } + } + unsafe { + gl_error!(gl); + } + } + pub fn render( + &self, + gl: &Context, + cam_pos: glam::Vec3, + view_proj: &glam::Mat4, + textures: &HashMap, + latest_time: f64, + ) { + unsafe { + gl_error!(gl); + gl.disable(SCISSOR_TEST); + + gl.use_program(Some(self.trail_program)); + gl_error!(gl); + gl.active_texture(TEXTURE0); + gl_error!(gl); + let scroll_texture: Vec2 = Vec2 { + x: 0.0, + y: (latest_time as f32 % 2.0) - 1.0, + }; //TODO: manage speed in some configurations. per trail ? + + gl.uniform_2_f32_slice(Some(&NativeUniformLocation(3)), scroll_texture.as_ref()); + //https://stackoverflow.com/questions/27771902/opengl-changing-texture-coordinates-on-the-fly + //https://www.khronos.org/opengl/wiki/Uniform_(GLSL) + for ((trail, trail_buffer), trail_array) in self + .trails + .iter() + .zip(self.trail_vertex_buffers.iter()) + .zip(self.trail_vertex_arrays.iter()) + { + if let Some(texture) = textures.get(&trail.texture) { + gl.bind_vertex_array(Some(*trail_array)); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + + gl.bind_vertex_buffer(0, Some(*trail_buffer), 0, MARKER_VERTEX_STRIDE); + gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); + gl.bind_texture(TEXTURE_2D, Some(texture.handle)); + gl.bind_sampler(0, Some(texture.sampler)); + gl_error!(gl); + gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl_error!(gl); + + /* + gl.polygon_mode(FRONT_AND_BACK, LINE); + gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl.polygon_mode(FRONT_AND_BACK, FILL); + gl_error!(gl); + */ + } + } + gl.use_program(Some(self.marker_program)); + gl_error!(gl); + gl.bind_vertex_array(Some(self.marker_vertex_array)); + gl_error!(gl); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + gl.bind_vertex_buffer(0, Some(self.marker_vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); + for (index, mo) in self.markers.iter().enumerate() { + let index: u32 = index.try_into().unwrap(); + if let Some(texture) = textures.get(&mo.texture) { + gl.bind_texture(TEXTURE_2D, Some(texture.handle)); + gl.bind_sampler(0, Some(texture.sampler)); + gl.draw_arrays(TRIANGLES, index as i32 * 6, 6); + } + } + gl_error!(gl); + gl.bind_vertex_array(None); + } + } +} + +/// takes in strings containing vertex/fragment shaders and returns a Shaderprogram with them attached +#[tracing::instrument(skip(gl))] +pub fn new_program( + gl: &Context, + vertex_shader_source: &str, + fragment_shader_source: &str, + _geometry_shader_source: Option<&str>, +) -> NativeProgram { + //https://www.khronos.org/opengl/wiki/Shader_Compilation#Program_setup + unsafe { + gl_error!(gl); + + let program = gl.create_program().unwrap(); + let vertex_shader = gl.create_shader(VERTEX_SHADER).unwrap(); + gl.shader_source(vertex_shader, vertex_shader_source); + gl.compile_shader(vertex_shader); + if !gl.get_shader_compile_status(vertex_shader) { + let e = gl.get_shader_info_log(vertex_shader); + error!("{}", &e); + panic!("vertex shader compilation error: {}", &e); + } + let frag_shader = gl.create_shader(FRAGMENT_SHADER).unwrap(); + gl.shader_source(frag_shader, fragment_shader_source); + gl.compile_shader(frag_shader); + if !gl.get_shader_compile_status(frag_shader) { + let e = gl.get_shader_info_log(frag_shader); + error!("frag shader compilation error:{}", &e); + panic!("frag shader compilation error: {}", &e); + } + gl.attach_shader(program, vertex_shader); + gl.attach_shader(program, frag_shader); + // let geometry_shader; + // geometry_shader = gl.create_shader(glow::GEOMETRY_SHADER).unwrap(); + // if let Some(gss) = geometry_shader_source { + // gl.shader_source(geometry_shader, gss); + // gl.compile_shader(geometry_shader); + // if !gl.get_shader_compile_status(geometry_shader) { + // let e = gl.get_shader_info_log(geometry_shader); + // error!("frag shader compilation error:{}", &e); + // panic!("geometry shader compilation error: {}", &e); + // } + // gl.attach_shader(shader_program, geometry_shader); + // } + gl.link_program(program); + if !gl.get_program_link_status(program) { + let e = gl.get_program_info_log(program); + error!("shader program link error: {}", &e); + panic!("shader program link error: {}", &e); + } + gl.delete_shader(vertex_shader); + // if geometry_shader_source.is_some() { + // gl.delete_shader(geometry_shader); + // } + gl.delete_shader(frag_shader); + gl_error!(gl); + let active_attribute_count = gl.get_active_attributes(program); + let mut shader_info = format!("Shader Info:\nvertex attributes: {active_attribute_count}"); + for index in 0..active_attribute_count { + if let Some(attr) = gl.get_active_attribute(program, index) { + let location = gl.get_attrib_location(program, &attr.name); + shader_info = format!("{shader_info}\n{} @ {}", attr.name, location.unwrap()); + } else { + warn!("attribute with index {index} doesn't exist"); + } + } + let active_uniform_count = gl.get_active_uniforms(program); + shader_info = format!("{shader_info}\nuniform locations:{active_uniform_count}"); + for index in 0..active_uniform_count { + if let Some(attr) = gl.get_active_uniform(program, index) { + let location = gl.get_uniform_location(program, &attr.name); + shader_info = format!("{shader_info}\n{} @ {}", attr.name, location.unwrap().0); + } else { + warn!("uniform with index {index} doesn't exist"); + } + } + info!("{shader_info}"); + program + } +} +unsafe fn create_buffer(gl: &Context) -> NativeBuffer { + gl_error!(gl); + let vb = gl.create_buffer().expect("failed to create vb for markers"); + gl_error!(gl); + + gl.bind_vertex_array(None); + gl.bind_buffer(ARRAY_BUFFER, Some(vb)); + gl_error!(gl); + + gl.bind_buffer(ARRAY_BUFFER, None); + gl_error!(gl); + vb +} + +unsafe fn create_marker_array(gl: &Context, vertex_buffer: NativeBuffer) -> NativeVertexArray { + create_array(gl, vertex_buffer, 1) +} + +unsafe fn create_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { + let marker_vertex_array = gl.create_vertex_array().expect("failed to create egui vao"); + gl.bind_vertex_array(Some(marker_vertex_array)); + gl.bind_vertex_buffer(binding_index, Some(vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 0); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 0, 3, FLOAT, false, 0); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 0, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 1); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 1, 1, FLOAT, false, 12); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 1, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 2); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 2, 2, FLOAT, false, 16); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 2, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 3); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 3, 2, FLOAT, false, 24); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 3, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 4); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 4, 4, UNSIGNED_BYTE, true, 32); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 4, 0); + gl_error!(gl); + marker_vertex_array +} + +unsafe fn create_trail_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { + let trail_vertex_array = create_array(gl, vertex_buffer, binding_index); + gl.enable_vertex_array_attrib(trail_vertex_array, 5); + gl.vertex_array_attrib_format_f32(trail_vertex_array, 5, 2, FLOAT, false, 36); + gl.vertex_array_attrib_binding_f32(trail_vertex_array, 5, 0); + gl_error!(gl); + + trail_vertex_array +} diff --git a/crates/joko_render_manager/src/gl.rs b/crates/joko_render_manager/src/gl.rs new file mode 100644 index 0000000..8ac9626 --- /dev/null +++ b/crates/joko_render_manager/src/gl.rs @@ -0,0 +1,9 @@ +#[macro_export] +macro_rules! gl_error { + ($gl:expr) => {{ + let e = $gl.get_error(); + if e != egui_render_three_d::three_d::context::NO_ERROR { + tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); + } + }}; +} diff --git a/crates/joko_render_manager/src/lib.rs b/crates/joko_render_manager/src/lib.rs new file mode 100644 index 0000000..9050354 --- /dev/null +++ b/crates/joko_render_manager/src/lib.rs @@ -0,0 +1,3 @@ +pub mod billboard; +pub mod gl; +pub mod renderer; diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs new file mode 100644 index 0000000..5dfebdf --- /dev/null +++ b/crates/joko_render_manager/src/renderer.rs @@ -0,0 +1,354 @@ +use crate::billboard::BillBoardRenderer; +use crate::gl_error; +use egui_render_three_d::three_d; +use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; +use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; +use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; +use egui_render_three_d::three_d::Camera; +use egui_render_three_d::three_d::HasContext; +use egui_render_three_d::three_d::ScissorBox; +use egui_render_three_d::three_d::Viewport; +use egui_render_three_d::ThreeDBackend; +use egui_render_three_d::ThreeDConfig; +use egui_window_glfw_passthrough::GlfwBackend; +use glam::Mat4; +use joko_component_models::ComponentDataExchange; +use joko_component_models::JokolayComponent; +use joko_component_models::JokolayComponentDeps; +use joko_component_models::PeerComponentChannel; +use joko_link_models::MumbleLink; +use joko_link_models::UIState; +use joko_render_models::messages::UIToUIMessage; +use three_d::prelude::*; + +use joko_render_models::{marker::MarkerObject, trail::TrailObject}; + +pub struct JokoRenderer { + pub view_proj: Mat4, + pub cam_pos: glam::Vec3, + pub camera: Camera, + pub viewport: Viewport, + pub has_link: bool, + pub is_map_open: bool, + pub billboard_renderer: BillBoardRenderer, + pub gl: egui_render_three_d::ThreeDBackend, + channel_receiver: Option>, +} + +impl JokoRenderer { + pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { + let glfw = glfw_backend.glfw.clone(); + let backend = ThreeDBackend::new( + ThreeDConfig { + glow_config: Default::default(), + }, + |s| glfw.get_proc_address_raw(s), + //glfw_backend.window.raw_window_handle(), + glfw_backend.framebuffer_size_physical, + ); + let viewport = Viewport { + x: 0, + y: 0, + width: glfw_backend.framebuffer_size_physical[0], + height: glfw_backend.framebuffer_size_physical[1], + }; + let gl = &backend.context; + unsafe { gl_error!(gl) }; + let billboard_renderer = BillBoardRenderer::new(gl); + unsafe { gl_error!(gl) }; + Self { + viewport, + view_proj: Default::default(), + camera: Camera::new_perspective( + viewport, + [0.0, 0.0, 0.0].into(), + [0.0, 0.0, 0.0].into(), + Vector3::unit_y(), + Deg(90.0), + 1.0, + 5000.0, + ), + has_link: false, + is_map_open: false, + gl: backend, + billboard_renderer, + cam_pos: Default::default(), + channel_receiver: None, + } + } + + /* + CRect GetMinimapRectangle() + { + int w = mumbleLink.miniMap.compassWidth; + int h = mumbleLink.miniMap.compassHeight; + + CRect pos; + CRect size = App->GetRoot()->GetClientRect(); + float scale = GetWindowTooSmallScale(); + + pos.x1 = int( size.Width() - w * scale ); + pos.x2 = size.Width(); + + + if ( mumbleLink.isMinimapTopRight ) + { + pos.y1 = 1; + pos.y2 = int( h * scale + 1 ); + } + else + { + int delta = 37; + if ( mumbleLink.uiSize == 0 ) + delta = 33; + if ( mumbleLink.uiSize == 2 ) + delta = 41; + if ( mumbleLink.uiSize == 3 ) + delta = 45; + + pos.y1 = int( size.Height() - h * scale - delta * scale ); + pos.y2 = int( size.Height() - delta * scale ); + } + + return pos; + } + */ + pub fn get_z_near() -> f32 { + 1.0 + } + pub fn get_z_far() -> f32 { + 1000.0 + } + pub fn swap(&mut self) { + self.billboard_renderer.swap(); + } + /* + //https://wiki.guildwars2.com/wiki/API:1/event_details#Coordinate_recalculation + fn _scale_coords(continent_rect, map_rect, coords){ + continent_width = continent_rect[1].x - continent_rect[0].x; + continent_height = continent_rect[1].y - continent_rect[0].y; + map_width = map_rect[1].x - map_rect[0].x; + map_height = map_rect[1].y - map_rect[0].y; + position_on_map_x = coords.x - map_rect[0].x; + position_on_map_y = coords.y - map_rect[1].y; + return [ + Math.round( continent_rect[0].x + ( 1 * position_on_map_x / map_width * continent_width ) ), + Math.round( continent_rect[0].y + (-1 * position_on_map_y / map_height * continent_height ) ) + ]; + } + */ + fn handle_u2u_message(&mut self, msg: UIToUIMessage) { + match msg { + UIToUIMessage::BulkMarkerObject(marker_objects) => { + tracing::debug!( + "Handling of UIToUIMessage::BulkMarkerObject {}", + marker_objects.len() + ); + self.extend_markers(marker_objects); + } + UIToUIMessage::BulkTrailObject(trail_objects) => { + tracing::debug!( + "Handling of UIToUIMessage::BulkTrailObject {}", + trail_objects.len() + ); + self.extend_trails(trail_objects); + } + UIToUIMessage::MarkerObject(mo) => { + tracing::trace!("Handling of UIToUIMessage::MarkerObject"); + self.add_billboard(*mo); + } + UIToUIMessage::TrailObject(to) => { + tracing::trace!("Handling of UIToUIMessage::TrailObject"); + self.add_trail(*to); + } + UIToUIMessage::RenderSwapChain => { + tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); + self.swap(); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling UIToUIMessage has not been implemented yet"); + } + } + } + + pub fn extend_markers(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip.extend(marker_objects); + } + pub fn add_billboard(&mut self, marker_object: MarkerObject) { + self.billboard_renderer.markers_wip.push(marker_object); + } + + pub fn extend_trails(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip.extend(trail_objects); + } + pub fn add_trail(&mut self, trail_object: TrailObject) { + self.billboard_renderer.trails_wip.push(trail_object); + } + + pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { + self.gl.prepare_frame(latest_framebuffer_size_getter); + unsafe { + let gl = self.gl.context.clone(); + gl_error!(gl); + // self.gl.context.set_viewport(self.viewport); + self.gl.context.set_scissor(ScissorBox::new_at_origo( + self.viewport.width, + self.viewport.height, + )); + self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); + self.gl + .context + .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); + gl_error!(gl); + } + } + + pub fn render_egui( + &mut self, + meshes: Vec, + textures_delta: egui::TexturesDelta, + logical_screen_size: [f32; 2], + latest_time: f64, + ) { + if self.has_link && !self.is_map_open { + self.billboard_renderer + .prepare_render_data(&self.gl.context); + self.billboard_renderer.render( + &self.gl.context, + self.cam_pos, + &self.view_proj, + &self.gl.glow_backend.painter.managed_textures, + latest_time, + ); + } + self.gl + .render_egui(meshes, textures_delta, logical_screen_size); + } + + pub fn present(&mut self) {} + + pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { + tracing::info!(?latest_size, "resizing framebuffer"); + + self.viewport = Viewport { + x: 0, + y: 0, + width: latest_size[0], + height: latest_size[1], + }; + self.gl.resize_framebuffer(latest_size); + } +} + +impl JokolayComponentDeps for JokoRenderer {} +impl JokolayComponent<(), ()> for JokoRenderer { + fn bind( + &mut self, + _deps: std::collections::HashMap< + u32, + tokio::sync::broadcast::Receiver, + >, + _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut input_notification: std::collections::HashMap< + u32, + tokio::sync::mpsc::Receiver, + >, + _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + self.channel_receiver = input_notification.remove(&0); + } + fn flush_all_messages(&mut self) { + let channel_receiver = self.channel_receiver.as_mut().unwrap(); + + //two steps reading due to self mutability required by channel + let mut messages = Vec::new(); + while let Ok(msg) = channel_receiver.try_recv() { + messages.push(msg.into()); + } + for msg in messages { + self.handle_u2u_message(msg); + } + } + fn tick(&mut self, _latest_time: f64) -> Option<&()> { + let link: Option<&MumbleLink> = None; + if let Some(link) = link { + //x positive => east + //y positive => ascention + //z positive => north + self.is_map_open = if let Some(ui_state) = link.ui_state { + ui_state.contains(UIState::IsMapOpen) + } else { + false + }; + + //TODO: change perspective is map is open + let center = link.cam_pos.0 + link.f_camera_front.0; + let cam_pos = link.cam_pos; + /* + let map_pos_x = (link.player_x - link.map_center_x) / 1.64; + let map_pos_y = (link.map_center_y - link.player_y) / 1.64; + let center = if self.is_map_open { + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 100.0, + z: link.player_pos.z - map_pos_y, + } + } else { + link.cam_pos + link.f_camera_front //default old one + }; + + let client_width = (link.client_size.x) as f32; + let client_height = (link.client_size.y) as f32; + + let cam_pos = if self.is_map_open { + //TODO: validate values + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 101.0, + z: link.player_pos.z - map_pos_y, + } + }else { + link.cam_pos //default old one + };*/ + let camera = Camera::new_perspective( + self.viewport, + cam_pos.0.to_array().into(), + center.to_array().into(), + Vector3::unit_y(), + Rad(link.fov), + Self::get_z_near(), + Self::get_z_far(), + ); + self.camera = camera; + /* + is_map_open: + target camera direction: 0 -20 1 + have trails seen from further + have trails fatter drawing + + println!("client: {} {} {} {}", client_width, client_height, client_width.div(client_height), client_height.div(client_width)); + println!("map scale: {}", link.map_scale); + println!("map position: {} {}", map_pos_x, map_pos_y); + println!("cam: {} {} {}", cam_pos.x, cam_pos.y, cam_pos.z); + println!("center: {} {} {}", center.x, center.y, center.z); + println!("H: {}", cam_pos.y - center.y); + println!("player: {} {} {}", link.player_pos.x, link.player_pos.y, link.player_pos.z); + */ + + let view = Mat4::look_at_lh(cam_pos.0, center, glam::Vec3::Y); + let proj = Mat4::perspective_lh( + link.fov, + self.viewport.aspect(), + Self::get_z_near(), + Self::get_z_far(), + ); + self.view_proj = proj * view; + self.cam_pos = cam_pos.0; + self.has_link = true; + } else { + self.has_link = false; + } + None + } +} diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml index 761b3ae..9449c18 100644 --- a/crates/joko_render_models/Cargo.toml +++ b/crates/joko_render_models/Cargo.toml @@ -14,5 +14,5 @@ glam = { workspace = true, features = ["bytemuck"] } serde = { workspace = true } joko_core = { path = "../joko_core" } -joko_components = { path = "../joko_components" } +joko_component_models = { path = "../joko_component_models" } diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs index c3058b2..50b4443 100644 --- a/crates/joko_render_models/src/messages.rs +++ b/crates/joko_render_models/src/messages.rs @@ -1,4 +1,4 @@ -use joko_components::ComponentDataExchange; +use joko_component_models::ComponentDataExchange; use serde::{Deserialize, Serialize}; use crate::{marker::MarkerObject, trail::TrailObject}; @@ -18,3 +18,10 @@ impl From for ComponentDataExchange { bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages } } + +#[allow(clippy::from_over_into)] +impl Into for ComponentDataExchange { + fn into(self) -> UIToUIMessage { + bincode::deserialize(&self).unwrap() + } +} diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index 4c669a6..cfee5ed 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -14,12 +14,13 @@ wayland = ["egui_window_glfw_passthrough/wayland"] [dependencies] enumflags2 = { workspace = true } -joko_core = { path = "../joko_core" } -joko_components = { path = "../joko_components" } -joko_plugins = { path = "../joko_plugins" } -joko_render = { path = "../joko_render" } -jmf = { path = "../joko_package", package = "joko_package" } -joko_link = { path = "../joko_link" } +joko_component_manager = { path = "../joko_component_manager" } +joko_component_models = { path = "../joko_component_models" } +joko_plugin_manager = { path = "../joko_plugin_manager" } +joko_render_manager = { path = "../joko_render_manager" } +joko_package_manager = { path = "../joko_package_manager" } +joko_link_manager = { path = "../joko_link_manager" } +joko_link_models = { path = "../joko_link_models" } egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users # and `Dir` from cap-dirs doesn't allow us to get the path. diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 21f4c76..ab7d3cf 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -8,22 +8,23 @@ use std::{ use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; +use joko_plugin_manager::JokolayPlugin; mod init; mod messages; mod mumble; mod ui_parameters; -use init::{get_jokolay_dir, get_jokolay_path}; -use jmf::{PackageDataManager, PackageUIManager}; -use joko_components::{ - ComponentDataExchange, ComponentManager, JokolayComponent, JokolayUIComponent, -}; use crate::app::mumble::mumble_gui; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; - -use jmf::jokolay_to_editable_path; -use jmf::ImportStatus; -use joko_render::renderer::JokoRenderer; -use joko_link::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleManager}; +use init::{get_jokolay_dir, get_jokolay_path}; +use joko_component_manager::ComponentManager; +use joko_component_models::{ComponentDataExchange, JokolayComponent, JokolayUIComponent}; +use joko_package_manager::{PackageDataManager, PackageUIManager}; + +use joko_link_manager::MumbleManager; +use joko_link_models::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, UISize}; +use joko_package_manager::jokolay_to_editable_path; +use joko_package_manager::ImportStatus; +use joko_render_manager::renderer::JokoRenderer; use miette::{Context, IntoDiagnostic, Result}; use tracing::{error, info, info_span}; @@ -83,7 +84,7 @@ impl Jokolay { let mumble_ui_manager = MumbleManager::new("MumbleLink", true).wrap_err("failed to create mumble manager")?; - let dummy_plugin = Box::new(joko_plugins::JokolayPlugin {}); + let dummy_plugin = Box::new(JokolayPlugin {}); component_manager.register("dummy_plugin", dummy_plugin); component_manager.register( "mumble_link_ui", @@ -330,16 +331,13 @@ impl Jokolay { */ let (u2gb_sender, u2gb_receiver) = std::sync::mpsc::channel(); - let (u2mb_sender, u2mb_receiver) = std::sync::mpsc::channel(); //FIXME: route the data to the consumers. + let (u2mb_sender, u2mb_receiver) = tokio::sync::mpsc::channel(1); //FIXME: route the data to the consumers. let (u2u_sender, u2u_receiver) = tokio::sync::mpsc::channel(1); Self::start_background_loop(Arc::clone(&self.app), u2gb_receiver); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); - let mut nb_frames: u128 = 0; - let mut nb_messages: u128 = 0; - let max_nb_messages_per_loop: u128 = 100; let mut gui = *self.gui; let mut local_state = self.state_ui; @@ -366,18 +364,6 @@ impl Jokolay { notifier, ); loop { - { - let mut nb_message_on_curent_loop: u128 = 0; - tracing::trace!( - "glfw event loop, {} frames, {} messages", - nb_frames, - nb_messages - ); - - if nb_message_on_curent_loop < max_nb_messages_per_loop { - gui.package_manager.flush_all_messages(); - } - } //TODO: one could wrap the egui_context into a plugin result so that it can be used from other plugins //TODO: same for the UI as a notified element. @@ -498,7 +484,7 @@ impl Jokolay { .window .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); } - package_manager.tick(latest_time, &egui_context); + package_manager.tick(latest_time, egui_context); local_state.window_changed = false; } @@ -641,8 +627,6 @@ impl Jokolay { ); joko_renderer.present(); glfw_backend.window.swap_buffers(); - - nb_frames += 1; } drop(span_guard); } @@ -746,7 +730,7 @@ pub struct MenuPanel { impl MenuPanel { pub const WIDTH: f32 = 288.0; pub const HEIGHT: f32 = 27.0; - pub fn tick(&mut self, etx: &egui::Context, link: Option<&joko_link::MumbleLink>) { + pub fn tick(&mut self, etx: &egui::Context, link: Option<&MumbleLink>) { let mut ui_scaling_factor = 1.0; if let Some(link) = link.as_ref() { let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { @@ -778,7 +762,7 @@ impl MenuPanel { } } -fn convert_uisz_to_scale(uisize: joko_link::UISize) -> f32 { +fn convert_uisz_to_scale(uisize: UISize) -> f32 { const SMALL: f32 = 288.0; const NORMAL: f32 = 319.0; const LARGE: f32 = 355.0; @@ -788,10 +772,10 @@ fn convert_uisz_to_scale(uisize: joko_link::UISize) -> f32 { const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; match uisize { - joko_link::UISize::Small => SMALL_SCALING_RATIO, - joko_link::UISize::Normal => NORMAL_SCALING_RATIO, - joko_link::UISize::Large => LARGE_SCALING_RATIO, - joko_link::UISize::Larger => LARGER_SCALING_RATIO, + UISize::Small => SMALL_SCALING_RATIO, + UISize::Normal => NORMAL_SCALING_RATIO, + UISize::Large => LARGE_SCALING_RATIO, + UISize::Larger => LARGER_SCALING_RATIO, } } /* diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs index fa7201d..163f7e2 100644 --- a/crates/jokolay/src/app/mumble.rs +++ b/crates/jokolay/src/app/mumble.rs @@ -1,8 +1,8 @@ use egui::DragValue; -use joko_link::{MessageToMumbleLinkBack, MumbleLink}; +use joko_link_models::{MessageToMumbleLinkBack, MumbleLink}; pub fn mumble_gui( - u2mb_sender: &std::sync::mpsc::Sender, + u2mb_sender: &tokio::sync::mpsc::Sender, etx: &egui::Context, open: &mut bool, editable_mumble: &mut bool, @@ -14,11 +14,11 @@ pub fn mumble_gui( ui.horizontal(|ui| { if ui.selectable_label(!*editable_mumble, "live").clicked() { *editable_mumble = false; - let _ = u2mb_sender.send(MessageToMumbleLinkBack::Autonomous); + let _ = u2mb_sender.blocking_send(MessageToMumbleLinkBack::Autonomous); } if ui.selectable_label(*editable_mumble, "editable").clicked() { *editable_mumble = true; - let _ = u2mb_sender.send(MessageToMumbleLinkBack::BindedOnUI); + let _ = u2mb_sender.blocking_send(MessageToMumbleLinkBack::BindedOnUI); } }); if *editable_mumble { From c63cd21d0caf680b722153ee3f728b1b9e050458 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 28 Apr 2024 02:39:08 +0200 Subject: [PATCH 45/54] more clean up and clarify where are the managers (future components) so they shall be easier to migrate to plugins in the future --- Cargo.lock | 181 +- Cargo.toml | 49 +- crates/joko_component_manager/Cargo.toml | 5 +- crates/joko_component_manager/src/lib.rs | 237 +- crates/joko_component_models/src/lib.rs | 27 +- crates/joko_components/Cargo.toml | 13 - crates/joko_components/src/lib.rs | 171 -- crates/joko_core/src/serde_glam/vec2.rs | 9 +- crates/joko_core/src/serde_glam/vec3.rs | 9 +- crates/joko_link/Cargo.toml | 47 - crates/joko_link/README.md | 58 - crates/joko_link/src/lib.rs | 270 -- crates/joko_link/src/linux/mod.rs | 305 -- crates/joko_link/src/mumble/ctypes.rs | 288 -- crates/joko_link/src/mumble/mod.rs | 173 -- crates/joko_link/src/win/dll.rs | 490 --- crates/joko_link/src/win/mod.rs | 735 ----- crates/joko_link_models/src/mumble/mod.rs | 2 +- crates/joko_package/Cargo.toml | 57 - crates/joko_package/README.md | 87 - crates/joko_package/build.rs | 14 - crates/joko_package/images/marker.png | Bin 173015 -> 0 bytes crates/joko_package/images/question.png | Bin 4248 -> 0 bytes crates/joko_package/images/trail.png | Bin 6896 -> 0 bytes crates/joko_package/images/trail_black.png | Bin 2293 -> 0 bytes crates/joko_package/images/trail_rainbow.png | Bin 16987 -> 0 bytes crates/joko_package/src/io/deserialize.rs | 1610 ---------- crates/joko_package/src/io/error.rs | 1 - crates/joko_package/src/io/export.rs | 264 -- crates/joko_package/src/io/mod.rs | 9 - crates/joko_package/src/io/serialize.rs | 240 -- crates/joko_package/src/io/test.xml | 12 - crates/joko_package/src/io/xmlfile_schema.xsd | 394 --- crates/joko_package/src/lib.rs | 41 - crates/joko_package/src/manager/mod.rs | 29 - .../src/manager/pack/activation.rs | 20 - .../joko_package/src/manager/pack/active.rs | 303 -- .../src/manager/pack/category_selection.rs | 308 -- crates/joko_package/src/manager/pack/dirty.rs | 28 - crates/joko_package/src/manager/pack/entry.rs | 6 - .../src/manager/pack/file_selection.rs | 47 - .../joko_package/src/manager/pack/import.rs | 29 - crates/joko_package/src/manager/pack/list.rs | 6 - .../joko_package/src/manager/pack/loaded.rs | 1021 ------- crates/joko_package/src/manager/pack/mod.rs | 6 - .../joko_package/src/manager/package_data.rs | 516 ---- crates/joko_package/src/manager/package_ui.rs | 771 ----- crates/joko_package/src/message.rs | 57 - crates/joko_package/vendor/rapid/license.txt | 52 - crates/joko_package/vendor/rapid/rapid.cpp | 66 - crates/joko_package/vendor/rapid/rapid.hpp | 7 - crates/joko_package/vendor/rapid/rapidxml.hpp | 2645 ----------------- .../vendor/rapid/rapidxml_iterators.hpp | 295 -- .../vendor/rapid/rapidxml_print.hpp | 422 --- .../vendor/rapid/rapidxml_utils.hpp | 56 - crates/joko_package_models/src/attributes.rs | 2 +- crates/joko_plugin_manager/src/lib.rs | 6 +- crates/joko_plugins/Cargo.toml | 12 - crates/joko_plugins/src/lib.rs | 29 - crates/joko_render/Cargo.toml | 24 - crates/joko_render/shaders/marker.fs | 18 - crates/joko_render/shaders/marker.vs | 39 - crates/joko_render/shaders/marker.wgsl | 0 .../shaders/player_visibility.wgsl | 24 - crates/joko_render/shaders/trail.fs | 22 - crates/joko_render/shaders/trail.vs | 37 - crates/joko_render/src/billboard.rs | 360 --- crates/joko_render/src/gl.rs | 9 - crates/joko_render/src/lib.rs | 3 - crates/joko_render/src/renderer.rs | 357 --- crates/jokoapi/src/end_point/races/mod.rs | 2 +- crates/jokoapi/src/lib.rs | 2 +- crates/jokolay/src/app/mod.rs | 4 +- crates/jokolink/Cargo.toml | 44 - crates/jokolink/README.md | 58 - crates/jokolink/src/lib.rs | 176 -- crates/jokolink/src/linux/mod.rs | 305 -- crates/jokolink/src/mumble/ctypes.rs | 288 -- crates/jokolink/src/mumble/mod.rs | 174 -- crates/jokolink/src/win/dll.rs | 490 --- crates/jokolink/src/win/mod.rs | 735 ----- 81 files changed, 442 insertions(+), 15266 deletions(-) delete mode 100644 crates/joko_components/Cargo.toml delete mode 100644 crates/joko_components/src/lib.rs delete mode 100644 crates/joko_link/Cargo.toml delete mode 100644 crates/joko_link/README.md delete mode 100644 crates/joko_link/src/lib.rs delete mode 100644 crates/joko_link/src/linux/mod.rs delete mode 100644 crates/joko_link/src/mumble/ctypes.rs delete mode 100644 crates/joko_link/src/mumble/mod.rs delete mode 100644 crates/joko_link/src/win/dll.rs delete mode 100644 crates/joko_link/src/win/mod.rs delete mode 100644 crates/joko_package/Cargo.toml delete mode 100644 crates/joko_package/README.md delete mode 100644 crates/joko_package/build.rs delete mode 100644 crates/joko_package/images/marker.png delete mode 100644 crates/joko_package/images/question.png delete mode 100644 crates/joko_package/images/trail.png delete mode 100644 crates/joko_package/images/trail_black.png delete mode 100644 crates/joko_package/images/trail_rainbow.png delete mode 100644 crates/joko_package/src/io/deserialize.rs delete mode 100644 crates/joko_package/src/io/error.rs delete mode 100644 crates/joko_package/src/io/export.rs delete mode 100644 crates/joko_package/src/io/mod.rs delete mode 100644 crates/joko_package/src/io/serialize.rs delete mode 100644 crates/joko_package/src/io/test.xml delete mode 100644 crates/joko_package/src/io/xmlfile_schema.xsd delete mode 100644 crates/joko_package/src/lib.rs delete mode 100644 crates/joko_package/src/manager/mod.rs delete mode 100644 crates/joko_package/src/manager/pack/activation.rs delete mode 100644 crates/joko_package/src/manager/pack/active.rs delete mode 100644 crates/joko_package/src/manager/pack/category_selection.rs delete mode 100644 crates/joko_package/src/manager/pack/dirty.rs delete mode 100644 crates/joko_package/src/manager/pack/entry.rs delete mode 100644 crates/joko_package/src/manager/pack/file_selection.rs delete mode 100644 crates/joko_package/src/manager/pack/import.rs delete mode 100644 crates/joko_package/src/manager/pack/list.rs delete mode 100644 crates/joko_package/src/manager/pack/loaded.rs delete mode 100644 crates/joko_package/src/manager/pack/mod.rs delete mode 100644 crates/joko_package/src/manager/package_data.rs delete mode 100644 crates/joko_package/src/manager/package_ui.rs delete mode 100644 crates/joko_package/src/message.rs delete mode 100644 crates/joko_package/vendor/rapid/license.txt delete mode 100644 crates/joko_package/vendor/rapid/rapid.cpp delete mode 100644 crates/joko_package/vendor/rapid/rapid.hpp delete mode 100644 crates/joko_package/vendor/rapid/rapidxml.hpp delete mode 100644 crates/joko_package/vendor/rapid/rapidxml_iterators.hpp delete mode 100644 crates/joko_package/vendor/rapid/rapidxml_print.hpp delete mode 100644 crates/joko_package/vendor/rapid/rapidxml_utils.hpp delete mode 100644 crates/joko_plugins/Cargo.toml delete mode 100644 crates/joko_plugins/src/lib.rs delete mode 100644 crates/joko_render/Cargo.toml delete mode 100644 crates/joko_render/shaders/marker.fs delete mode 100644 crates/joko_render/shaders/marker.vs delete mode 100644 crates/joko_render/shaders/marker.wgsl delete mode 100644 crates/joko_render/shaders/player_visibility.wgsl delete mode 100644 crates/joko_render/shaders/trail.fs delete mode 100644 crates/joko_render/shaders/trail.vs delete mode 100644 crates/joko_render/src/billboard.rs delete mode 100644 crates/joko_render/src/gl.rs delete mode 100644 crates/joko_render/src/lib.rs delete mode 100644 crates/joko_render/src/renderer.rs delete mode 100644 crates/jokolink/Cargo.toml delete mode 100644 crates/jokolink/README.md delete mode 100644 crates/jokolink/src/lib.rs delete mode 100644 crates/jokolink/src/linux/mod.rs delete mode 100644 crates/jokolink/src/mumble/ctypes.rs delete mode 100644 crates/jokolink/src/mumble/mod.rs delete mode 100644 crates/jokolink/src/win/dll.rs delete mode 100644 crates/jokolink/src/win/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c360f70..d54ab15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1049,6 +1058,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -1425,11 +1440,40 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "joko_component_manager" +version = "0.2.1" +dependencies = [ + "bincode", + "egui", + "joko_component_models", + "petgraph", + "scopeguard", + "smol_str", + "solvent", + "tokio", + "tracing", +] + +[[package]] +name = "joko_component_models" +version = "0.2.1" +dependencies = [ + "bincode", + "egui", + "scopeguard", + "smol_str", + "tokio", +] + [[package]] name = "joko_core" version = "0.2.1" dependencies = [ + "bytemuck", + "glam", "scopeguard", + "serde", "smol_str", ] @@ -1438,10 +1482,61 @@ name = "joko_ext" version = "0.1.0" [[package]] -name = "joko_package" +name = "joko_link_manager" +version = "0.2.1" +dependencies = [ + "arcdps", + "enumflags2", + "glam", + "joko_component_models", + "joko_core", + "joko_link_models", + "miette", + "notify", + "num-derive", + "num-traits", + "serde", + "serde_json", + "time", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "widestring", + "windows", + "x11rb", +] + +[[package]] +name = "joko_link_models" +version = "0.2.1" +dependencies = [ + "arcdps", + "enumflags2", + "glam", + "joko_core", + "miette", + "notify", + "num-derive", + "num-traits", + "serde", + "serde_json", + "time", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "widestring", + "windows", + "x11rb", +] + +[[package]] +name = "joko_package_manager" version = "0.2.1" dependencies = [ "base64", + "bincode", "bytemuck", "cap-std", "cxx", @@ -1453,11 +1548,12 @@ dependencies = [ "image", "indexmap", "itertools", + "joko_component_models", "joko_core", + "joko_link_models", "joko_package_models", "joko_render_models", "jokoapi", - "jokolink", "miette", "once", "ordered_hash_map", @@ -1471,6 +1567,7 @@ dependencies = [ "similar-asserts", "smol_str", "time", + "tokio", "tracing", "url", "uuid", @@ -1510,16 +1607,29 @@ dependencies = [ ] [[package]] -name = "joko_render" +name = "joko_plugin_manager" version = "0.2.1" dependencies = [ + "joko_component_models", + "scopeguard", + "smol_str", + "tokio", +] + +[[package]] +name = "joko_render_manager" +version = "0.2.1" +dependencies = [ + "bincode", "bytemuck", "egui", "egui_render_three_d", "egui_window_glfw_passthrough", "glam", + "joko_component_models", + "joko_link_models", "joko_render_models", - "jokolink", + "tokio", "tracing", ] @@ -1527,8 +1637,12 @@ dependencies = [ name = "joko_render_models" version = "0.2.1" dependencies = [ + "bincode", "bytemuck", "glam", + "joko_component_models", + "joko_core", + "serde", ] [[package]] @@ -1555,9 +1669,13 @@ dependencies = [ "enumflags2", "glam", "indexmap", - "joko_package", - "joko_render", - "jokolink", + "joko_component_manager", + "joko_component_models", + "joko_link_manager", + "joko_link_models", + "joko_package_manager", + "joko_plugin_manager", + "joko_render_manager", "miette", "rayon", "rfd", @@ -1566,6 +1684,7 @@ dependencies = [ "serde", "serde_json", "smol_str", + "tokio", "toml", "tracing", "tracing-appender", @@ -1573,28 +1692,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "jokolink" -version = "0.2.1" -dependencies = [ - "arcdps", - "enumflags2", - "glam", - "miette", - "notify", - "num-derive", - "num-traits", - "serde", - "serde_json", - "time", - "tracing", - "tracing-appender", - "tracing-subscriber", - "widestring", - "windows", - "x11rb", -] - [[package]] name = "js-sys" version = "0.3.69" @@ -2041,6 +2138,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.11.2" @@ -2627,6 +2734,12 @@ dependencies = [ "serde", ] +[[package]] +name = "solvent" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" + [[package]] name = "spin" version = "0.9.8" @@ -2831,6 +2944,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "toml" version = "0.8.12" diff --git a/Cargo.toml b/Cargo.toml index 01442e5..57d6033 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,18 @@ [workspace] members = [ - "crates/joko_render", + "crates/joko_render_manager", "crates/joko_render_models", - "crates/joko_package", + "crates/joko_package_manager", "crates/joko_package_models", - "crates/jokolink", + "crates/joko_link_manager", + "crates/joko_link_models", "crates/jokoapi", "crates/jokolay", "crates/joko_core", + "crates/joko_component_manager", + "crates/joko_component_models", + "crates/joko_plugin_manager", "crates/joko_ext", ] resolver = "2" @@ -16,34 +20,33 @@ resolver = "2" [workspace.dependencies] #https://docs.rs/tracing/latest/tracing/level_filters/index.html +bincode = "1.3.3" bytemuck = { version = "1", features = ["derive"] } -tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } -ringbuffer = { version = "0.14" } -egui = { version = "0.26" } -egui_extras = { version = "0.26" } cap-directories = { version = "2" } cap-std = { version = "2", features = ["fs_utf8"] } -serde = { version = "*", features = ["derive"] } -miette = { version = "*", features = ["fancy"] } -url = { version = "*", features = ["serde"] } -serde_json = { version = "*" } -rayon = { version = "*" } -paste = { version = "*" } +egui = { version = "0.26" } +egui_extras = { version = "0.26" } +enumflags2 = { version = "*", features = ["serde"] } glam = { version = "*", features = ["fast-math"] } -time = { version = "*" } -ureq = { version = "*" } -enumflags2 = { version = "*" } indexmap = { version = "2" } -rfd = { version = "*" } -smol_str = { version = "*" } -uuid = { version = "*" } itertools = { version = "*" } +miette = { version = "*", features = ["fancy"] } ordered_hash_map = { version = "*", features= ["serde"] } +paste = { version = "*" } +rayon = { version = "*" } +rfd = { version = "*" } +ringbuffer = { version = "0.14" } +serde = { version = "*", features = ["derive"] } +serde_json = { version = "*" } +smol_str = { version = "*", features = ["serde"] } +time = { version = "*" } +tokio = { version = "1.37.0", features = ["sync"] } +tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } tracing-appender = { version = "*" } -tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "time", -] } # for ErrorLayer +tracing-subscriber = { version = "0.3", features = ["env-filter", "time",] } # for ErrorLayer +ureq = { version = "*" } +url = { version = "*", features = ["serde"] } +uuid = { version = "*" } #https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml index ae8b444..589a975 100644 --- a/crates/joko_component_manager/Cargo.toml +++ b/crates/joko_component_manager/Cargo.toml @@ -11,4 +11,7 @@ egui = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } tokio = { workspace = true } -joko_component_models = { path = "../joko_component_models" } \ No newline at end of file +joko_component_models = { path = "../joko_component_models" } +solvent = "0.8.3" +petgraph = "0.6.4" +tracing.workspace = true diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index b19803d..39643a7 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -1,11 +1,39 @@ use std::collections::HashMap; use joko_component_models::JokolayComponentDeps; +use petgraph::{csr::IndexType, graph::NodeIndex, stable_graph::StableDiGraph, visit::IntoNodeIdentifiers, Direction}; +use tracing::trace; pub struct ComponentManager { data: HashMap>, } + +fn get_invocation_order(my_graph: &mut StableDiGraph) -> Vec +where + N: std::cmp::Ord, + Ix: IndexType +{ + let mut invocation_order = Vec::new(); + + //peel nodes one by one + while my_graph.externals(Direction::Outgoing).count() > 0 { + let mut to_delete = Vec::new(); + for external_node in my_graph.externals(Direction::Outgoing) { + to_delete.push(external_node); + } + let mut current_level_invocation_order = Vec::new(); + for external_node in to_delete { + current_level_invocation_order.push(my_graph.remove_node(external_node).unwrap()); + } + current_level_invocation_order.sort();//This grant a deterministic order regardless of circumstances + invocation_order.extend(current_level_invocation_order); + } + //if there is a cycle, there are remaining nodes + invocation_order +} + + impl ComponentManager { pub fn new() -> Self { Self { @@ -18,22 +46,116 @@ impl ComponentManager { } pub fn build_routes(&mut self) -> Result<(), String> { - let mut known_services: HashMap = Default::default(); + /* + + fn bind( + &mut self, + deps: HashMap, + bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + input_notification: HashMap + notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) + https://docs.rs/dep-graph/latest/dep_graph/ + https://lib.rs/crates/petgraph + https://docs.rs/solvent/latest/solvent/ + => check "peer" is always mutual + => graph with the "peer" elements replaced by some merged id + => check there is no loop (there could be surprises) + => if there is no problem, then: + - build again the graph with UI plugins only and save one traversal (memory + file) + - build again the graph with back plugins only and save one traversal (memory + file) + => if there is a problem, do not save anything + + fn tick( + &mut self, + ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() + */ + + type G = petgraph::stable_graph::StableDiGraph; + + let mut known_services: HashMap > = Default::default(); + let mut depgraph: G = G::default(); + let mut translation: HashMap, NodeIndex> = Default::default(); let mut service_id = 0; for (service_name, co) in self.data.iter() { - service_id += 1; - known_services.insert(service_name.clone(), service_id); + let service_name = service_name.clone(); + if !known_services.contains_key(&service_name) { + let node_id = depgraph.add_node(service_id); + service_id += 1; + known_services.insert(service_name.clone(), node_id); + } + trace!("node: {}, peers: {:?}", service_name, co.peer()); for peer_name in co.peer() { - if let Some(peer) = self.data.get(peer_name) { + let peer_name = peer_name.to_string(); + if !known_services.contains_key(&peer_name) { + let node_id = depgraph.add_node(service_id); + service_id += 1; + known_services.insert(peer_name.clone(), node_id); + } + if let Some(peer) = self.data.get(&peer_name) { if !peer.peer().contains(&service_name.as_str()) { return Err(format!( - "Missmatch in peer between {} and {}", + "Missmatch in peers: '{}' asked for '{}' to be a peer, reverse is not true", service_name, peer_name )); } + let parent_id = *known_services.get(&service_name).unwrap(); + let peer_id = *known_services.get(&peer_name).unwrap(); + let merged_id = parent_id.min(peer_id); + translation.insert(parent_id, merged_id); + translation.insert(peer_id, merged_id); + } + } + } + //If we reached here, it means all peers agree + + let mut requirements_graph = depgraph.clone(); + let mut notification_graph = depgraph.clone(); + + for (service_name, co) in self.data.iter() { + let node_id = *known_services.get(service_name).unwrap(); + let service_id = *translation.get(&node_id).or(Some(&node_id)).unwrap(); + trace!("node: {}, requires: {:?}", service_name, co.requires()); + for required_service_name in co.requires() { + let required_service_id = *known_services.get(required_service_name).unwrap(); + let required_service_id = *translation + .get(&required_service_id) + .or(Some(&required_service_id)) + .unwrap(); + if service_id != required_service_id { + depgraph.add_edge(service_id, required_service_id, 1); + //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. + requirements_graph.add_edge(service_id, required_service_id, 1); + } + } + trace!("node: {}, notify: {:?}", service_name, co.notify()); + for notified_service_name in co.notify() { + let notified_service_id = *known_services.get(notified_service_name).unwrap(); + let notified_service_id = *translation + .get(¬ified_service_id) + .or(Some(¬ified_service_id)) + .unwrap(); + if service_id != notified_service_id { + depgraph.add_edge(notified_service_id, service_id, 1); + //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. + notification_graph.add_edge(notified_service_id, service_id, 1); } } } + + let invocation_order = get_invocation_order(&mut depgraph); + if depgraph.node_count() > 0 { + return Err(format!("Found a cyclic dependancy between {:?}", depgraph.node_identifiers())); + } + trace!("services: {:?}", known_services); + trace!("invocation_order: {:?}", invocation_order); + /* + TODO: make use of: + requirements graph + notification graph + invocation order + */ + unimplemented!( "The algorithm to build and check dependancies between components is not implemented" ) @@ -45,3 +167,108 @@ impl Default for ComponentManager { Self::new() } } + + + +#[test] +fn test_invocation_order_1() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + let d = my_graph.add_node("d".to_string()); + let _e = my_graph.add_node("e".to_string()); + + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + my_graph.add_edge(c, d, 1); + my_graph.add_edge(a, d, 1); + + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); +} + + +#[test] +fn test_invocation_order_2() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); +} + + +#[test] +fn test_invocation_order_3() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); +} + +#[test] +fn test_invocation_order_4() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); +} + +#[test] +fn test_duplicate_node_value() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + let _doublon = my_graph.add_node(3);// same value, considered as a separate node + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); +} + diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs index bc74e15..5728d29 100644 --- a/crates/joko_component_models/src/lib.rs +++ b/crates/joko_component_models/src/lib.rs @@ -48,36 +48,10 @@ where /* - // any extra information should come from configuration, which can be loaded from those two arguments. - Those roots are specific to the component, it cannot shared it with another component pub fn new( root_dir: Arc, root_path: &std::path::Path, ) -> Result; - - fn bind( - &mut self, - deps: HashMap, - bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap - notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) - https://docs.rs/dep-graph/latest/dep_graph/ - https://lib.rs/crates/petgraph - https://docs.rs/solvent/latest/solvent/ - => check "peer" is always mutual - => graph with the "peer" elements replaced by some merged id - => check there is no loop (there could be surprises) - => if there is no problem, then: - - build again the graph with UI plugins only and save one traversal (memory + file) - - build again the graph with back plugins only and save one traversal (memory + file) - => if there is a problem, do not save anything - - - - fn tick( - &mut self, - ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() */ } @@ -114,6 +88,7 @@ where https://docs.rs/dep-graph/latest/dep_graph/ https://lib.rs/crates/petgraph https://docs.rs/solvent/latest/solvent/ + https://lib.rs/crates/cargo-depgraph => check "peer" is always mutual => graph with the "peer" elements replaced by some merged id => check there is no loop (there could be surprises) diff --git a/crates/joko_components/Cargo.toml b/crates/joko_components/Cargo.toml deleted file mode 100644 index b13c465..0000000 --- a/crates/joko_components/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "joko_components" -version = "0.2.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bincode = { workspace = true } -egui = { workspace = true } -scopeguard = "1.2.0" -smol_str = { workspace = true } -tokio = { workspace = true } diff --git a/crates/joko_components/src/lib.rs b/crates/joko_components/src/lib.rs deleted file mode 100644 index 8f49116..0000000 --- a/crates/joko_components/src/lib.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::collections::HashMap; - -pub trait JokolayComponentDeps { - /** - Names are external to traits and implementation. That way it is easy to change it without change in binary. - In case of first class components, name is hardcoded. - In case of plugins, name is part of a manifest and can be changed at will. - */ - // elements in peer(), requires() and notify() are mutually exclusives - fn peer(&self) -> Vec<&str> { - //by default, no other plugin bound - vec![] - } - fn requires(&self) -> Vec<&str> { - //by default, no requirement - vec![] - } - fn notify(&self) -> Vec<&str> { - //by default, no third party plugin - vec![] - } -} - -//could become a "dyn Message". -//std::any::Any is a trait -//TODO: It would have a wrap and unwrap ? -pub type ComponentDataExchange = Vec; -//pub type ComponentDataExchange = Box<[u8]>; -//pub type ComponentDataExchange = [u8; 1024]; -pub type PeerComponentChannel = ( - tokio::sync::mpsc::Receiver, - tokio::sync::mpsc::Sender, -); - -pub trait JokolayComponent -where - SharedStatus: Clone, -{ - fn flush_all_messages(&mut self) -> SharedStatus; - fn tick(&mut self, latest_time: f64) -> Option<&ComponentResult>; - fn bind( - &mut self, - deps: HashMap>, - bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap>, - notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ); //By default, there is no third party component, thus we can implement it as a noop - - /* - - // any extra information should come from configuration, which can be loaded from those two arguments. - Those roots are specific to the component, it cannot shared it with another component - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - ) -> Result; - - fn bind( - &mut self, - deps: HashMap, - bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap - notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) - https://docs.rs/dep-graph/latest/dep_graph/ - https://lib.rs/crates/petgraph - https://docs.rs/solvent/latest/solvent/ - => check "peer" is always mutual - => graph with the "peer" elements replaced by some merged id - => check there is no loop (there could be surprises) - => if there is no problem, then: - - build again the graph with UI plugins only and save one traversal (memory + file) - - build again the graph with back plugins only and save one traversal (memory + file) - => if there is a problem, do not save anything - - - - fn tick( - &mut self, - ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() - */ -} - -pub trait JokolayUIComponent -where - SharedStatus: Clone, -{ - fn flush_all_messages(&mut self) -> SharedStatus; - fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> Option<&ComponentResult>; - fn bind( - &mut self, - deps: HashMap>, - bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap>, - notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ); //By default, there is no third party component, thus we can implement it as a noop - - /* - - // any extra information should come from configuration, which can be loaded from those two arguments. - Those roots are specific to the component, it cannot shared it with another component - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - ) -> Result; - - fn bind( - &mut self, - deps: HashMap, - bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap - notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) - https://docs.rs/dep-graph/latest/dep_graph/ - https://lib.rs/crates/petgraph - https://docs.rs/solvent/latest/solvent/ - => check "peer" is always mutual - => graph with the "peer" elements replaced by some merged id - => check there is no loop (there could be surprises) - => if there is no problem, then: - - build again the graph with UI plugins only and save one traversal (memory + file) - - build again the graph with back plugins only and save one traversal (memory + file) - => if there is a problem, do not save anything - - - - fn tick( - &mut self, - ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() - */ -} - -//TODO: have a BackEndPlugin and UIPlugin - -pub struct ComponentManager { - data: HashMap>, -} - -impl ComponentManager { - pub fn new() -> Self { - Self { - data: Default::default(), - } - } - - pub fn register(&mut self, service_name: &str, co: Box) { - self.data.insert(service_name.to_owned(), co); - } - - pub fn build_routes(&mut self) -> Result<(), String> { - let mut known_services: HashMap = Default::default(); - let mut service_id = 0; - for (service_name, co) in self.data.iter() { - service_id += 1; - known_services.insert(service_name.clone(), service_id); - for peer_name in co.peer() { - if let Some(peer) = self.data.get(peer_name) { - if !peer.peer().contains(&service_name.as_str()) { - return Err(format!( - "Missmatch in peer between {} and {}", - service_name, peer_name - )); - } - } - } - } - unimplemented!( - "The algorithm to build and check dependancies between components is not implemented" - ) - } -} diff --git a/crates/joko_core/src/serde_glam/vec2.rs b/crates/joko_core/src/serde_glam/vec2.rs index 4f4e336..908ab95 100644 --- a/crates/joko_core/src/serde_glam/vec2.rs +++ b/crates/joko_core/src/serde_glam/vec2.rs @@ -4,7 +4,7 @@ use serde::{ }; #[repr(C)] -#[derive(Copy, Clone, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct Vec2(pub glam::Vec2); #[repr(C)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -29,6 +29,13 @@ impl From for glam::UVec2 { } } +unsafe impl bytemuck::Pod for Vec2 {} +unsafe impl bytemuck::Zeroable for Vec2 { + fn zeroed() -> Self { + Self::default() + } +} + struct Vec2Deserializer; impl<'de> Visitor<'de> for Vec2Deserializer { type Value = Vec2; diff --git a/crates/joko_core/src/serde_glam/vec3.rs b/crates/joko_core/src/serde_glam/vec3.rs index 458108c..8446f25 100644 --- a/crates/joko_core/src/serde_glam/vec3.rs +++ b/crates/joko_core/src/serde_glam/vec3.rs @@ -4,7 +4,7 @@ use serde::{ }; #[repr(C)] -#[derive(Copy, Clone, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct Vec3(pub glam::Vec3); impl From for glam::Vec3 { @@ -13,6 +13,13 @@ impl From for glam::Vec3 { } } +unsafe impl bytemuck::Pod for Vec3 {} +unsafe impl bytemuck::Zeroable for Vec3 { + fn zeroed() -> Self { + Self::default() + } +} + struct Vec3Deserializer; impl<'de> Visitor<'de> for Vec3Deserializer { type Value = Vec3; diff --git a/crates/joko_link/Cargo.toml b/crates/joko_link/Cargo.toml deleted file mode 100644 index 28f666b..0000000 --- a/crates/joko_link/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "joko_link" -version = "0.2.1" -edition = "2021" -[lib] -crate-type = ["cdylib", "lib"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] - - -[dependencies] -joko_core = { path = "../joko_core" } -joko_components = { path = "../joko_components" } -widestring = { version = "1", default-features = false, features = ["std"] } -num-derive = { version = "0", default-features = false } -num-traits = { version = "0", default-features = false } -enumflags2 = { workspace = true } -time = { workspace = true } -miette = { workspace = true } -tracing = { workspace = true } -serde = { workspace = true } -glam = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } - -[target.'cfg(unix)'.dependencies] -x11rb = { version = "0.12", default-features = false, features = [] } - -[target.'cfg(windows)'.dependencies] -windows = { version = "0.51.1", features = [ - "Win32_System_Memory", - "Win32_Foundation", - "Win32_Security", - "Win32_UI_WindowsAndMessaging", - "Win32_System_Threading", - "Win32_System_LibraryLoader", - "Win32_System_SystemInformation", - "Win32_Graphics_Dwm", - "Win32_UI_HiDpi", - "Win32_Graphics_Gdi", - "Win32_UI_Shell", - "Win32_System_Com", -] } -arcdps = { version = "*", default-features = false } -notify = {version = "*" } -tracing-appender = {version = "*" } -tracing-subscriber = {version = "*" } diff --git a/crates/joko_link/README.md b/crates/joko_link/README.md deleted file mode 100644 index 5962a47..0000000 --- a/crates/joko_link/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# jokolink -A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). - -it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. - -## Precaution -This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. - -> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` - -## Installation -1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) -2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` -3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts -4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. - - -## Configuration -Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. - - * loglevel: - default: "info" - type: string - possible_values: ["trace", "debug", "info", "warn", "error"] - help: the log level of the application. - - * logdir: - default: "." // current working directory - type: directory path - help: a path to a directory, where jokolink will create jokolink.log file - - * mumble_link_name: - default: "MumbleLink" - type: string - help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name - - * interval - default: 5 - type: unsigned integer (positive integer) - help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. - - * copy_dest_dir: - default: "z:\\dev\\shm" - type: directory path - help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). - - -## Verification : -1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. -2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. -3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. -4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. - - - -## Cross Compilation -To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. -`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link/src/lib.rs b/crates/joko_link/src/lib.rs deleted file mode 100644 index 79b74e1..0000000 --- a/crates/joko_link/src/lib.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory - -//! Joko link is designed to primarily get the MumbleLink or the window size -//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). -//! on windows, you can use it to create/open shared memory. -//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. -//! then, you can easily read the /dev/shm file from a any number of linux native applications. -//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. -//! - -mod mumble; -use std::vec; - -use enumflags2::BitFlags; -use joko_components::{JokolayComponent, JokolayComponentDeps}; -use joko_core::serde_glam::{IVec2, UVec2, Vec3}; -//use jokoapi::end_point::{mounts::Mount, races::Race}; -use miette::{IntoDiagnostic, Result, WrapErr}; -pub use mumble::*; -use serde_json::from_str; -use tracing::error; - -/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing -pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; -#[cfg(target_os = "linux")] -pub mod linux; -#[cfg(target_os = "windows")] -pub mod win; - -#[cfg(target_os = "linux")] -use linux::MumbleLinuxImpl as MumblePlatformImpl; -#[cfg(target_os = "windows")] -use win::MumbleWinImpl as MumblePlatformImpl; - -pub enum MessageToMumbleLinkBack { - BindedOnUI, - Autonomous, - Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. -} - -#[derive(Clone)] -pub struct MumbleLinkSharedState { - pub read_ui_link: bool, - pub copy_of_ui_link: Option, -} - -// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there -// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; -/// This primarily manages the mumble backend. -/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. -/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. -/// and every frame gets the latest mumble link data, and compares with the previous frame. -/// if any of the changed this frame, it will set the relevant changed flags so that plugins -/// or other parts of program which care can run the relevant code. -pub struct MumbleManager { - /// This abstracts over the windows and linux impl of mumble link functionality. - /// we use this to get the latest mumble link and latest window dimensions of the current mumble link - backend: MumblePlatformImpl, - is_ui: bool, - /// latest mumble link - link: MumbleLink, - channel_receiver: std::sync::mpsc::Receiver, - state: MumbleLinkSharedState, -} - -impl MumbleManager { - pub fn new(name: &str, is_ui: bool) -> Result { - let backend = MumblePlatformImpl::new(name)?; - let (_, receiver) = std::sync::mpsc::channel(); - Ok(Self { - backend, - link: Default::default(), - channel_receiver: receiver, - is_ui, - state: MumbleLinkSharedState { - read_ui_link: true, - copy_of_ui_link: None, - }, - }) - } - pub fn is_alive(&self) -> bool { - self.backend.is_alive() - } - fn handle_message(&mut self, msg: MessageToMumbleLinkBack) { - //let (b2u_sender, _) = package_manager.channels(); - match msg { - MessageToMumbleLinkBack::Autonomous => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); - self.state.read_ui_link = false; - } - MessageToMumbleLinkBack::BindedOnUI => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); - self.state.read_ui_link = true; - } - MessageToMumbleLinkBack::Value(link) => { - tracing::trace!("Handling of UIToBackMessage::MumbleLink"); - self.state.copy_of_ui_link = link; - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling MessageToPackageBack has not been implemented yet"); - } - } - } - fn _tick(&mut self) -> Result> { - if let Err(e) = self.backend.tick() { - error!(?e, "mumble backend tick error"); - return Ok(None); - } - - if !self.backend.is_alive() { - self.link.client_size.0.x = 0; - self.link.client_size.0.y = 0; - self.link.changes = BitFlags::all(); - return Ok(Some(&self.link)); - } - // backend is alive and tick is successful. time to get link - let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); - let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { - Default::default() - } else { - self.link.clone() - }; - - if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { - return Ok(None); - } - let mut changes: BitFlags = Default::default(); - // safety. as the link is valid, we can use as_ref - let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) - .into_diagnostic() - .wrap_err("failed to get widestring out of cml identity")? - .to_string() - .into_diagnostic() - .wrap_err("failed to convert widestring to cstring")?; - - let identity: ctypes::CIdentity = from_str(&json_string) - .into_diagnostic() - .wrap_err("failed to deserialize identity from json string")?; - let uisz = identity - .get_uisz() - .ok_or(miette::miette!("uisz is invalid"))?; - let server_address = if cml.context.server_address[0] == 2 { - let addr = cml.context.server_address; - std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() - } else { - std::net::Ipv4Addr::UNSPECIFIED.into() - }; - if new_link.ui_tick != cml.ui_tick { - changes.insert(MumbleChanges::UiTick); - } - if new_link.name != identity.name { - changes.insert(MumbleChanges::Character); - } - if new_link.map_id != cml.context.map_id { - changes.insert(MumbleChanges::Map); - } - let client_pos = IVec2(glam::IVec2::new( - cml.context.client_pos[0], - cml.context.client_pos[1], - )); - let client_size = UVec2(glam::UVec2::new( - cml.context.client_size[0], - cml.context.client_size[1], - )); - - if new_link.client_pos != client_pos { - changes.insert(MumbleChanges::WindowPosition); - } - if new_link.client_size != client_size { - changes.insert(MumbleChanges::WindowSize); - } - let cam_pos: glam::Vec3 = cml.f_camera_position.into(); - if new_link.cam_pos.0 != cam_pos { - changes.insert(MumbleChanges::Camera); - } - - let player_pos: glam::Vec3 = cml.f_avatar_position.into(); - if new_link.player_pos.0 != player_pos { - changes.insert(MumbleChanges::Position); - } - //let player_race = Self::get_race(identity.race); - - new_link = MumbleLink { - ui_tick: cml.ui_tick, - player_pos: Vec3(player_pos), - f_avatar_front: Vec3(cml.f_avatar_front.into()), - cam_pos: Vec3(cam_pos), - f_camera_front: Vec3(cml.f_camera_front.into()), - name: identity.name, - map_id: cml.context.map_id, - fov: identity.fov, - uisz, - // window_pos, - // window_size, - changes, - // window_pos_without_borders, - // window_size_without_borders, - dpi_scaling: cml.context.dpi_scaling, - dpi: cml.context.dpi, - client_pos, - client_size, - map_type: cml.context.map_type, - server_address, - shard_id: cml.context.shard_id, - instance: cml.context.instance, - build_id: cml.context.build_id, - ui_state: cml.context.get_ui_state(), - compass_width: cml.context.compass_width, - compass_height: cml.context.compass_height, - compass_rotation: cml.context.compass_rotation, - player_x: cml.context.player_x, - player_y: cml.context.player_y, - map_center_x: cml.context.map_center_x, - map_center_y: cml.context.map_center_y, - map_scale: cml.context.map_scale, - process_id: cml.context.process_id, - mount: cml.context.mount_index, - race: identity.race, - }; - self.link = new_link; - - Ok(if self.link.ui_tick == 0 { - None - } else { - Some(&self.link) - }) - } -} - -impl JokolayComponent for MumbleManager { - fn flush_all_messages(&mut self) -> MumbleLinkSharedState { - while let Ok(msg) = self.channel_receiver.try_recv() { - self.handle_message(msg); - } - self.state.clone() - } - - fn tick(&mut self, _latest_time: f64) -> Option<&MumbleLink> { - self._tick().unwrap_or(None) - } - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - _input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap< - u32, - tokio::sync::mpsc::Sender, - >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - } -} - -impl JokolayComponentDeps for MumbleManager { - //default is enough - fn peer(&self) -> Vec<&str> { - if self.is_ui { - vec!["mumble_link_back"] - } else { - vec!["mumble_link_ui"] - } - } -} diff --git a/crates/joko_link/src/linux/mod.rs b/crates/joko_link/src/linux/mod.rs deleted file mode 100644 index f0adab4..0000000 --- a/crates/joko_link/src/linux/mod.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; -use miette::{Context, IntoDiagnostic, Result}; -use std::fs::File; -use std::io::{Read, Seek}; -use time::OffsetDateTime; -use tracing::info; -// use x11rb::protocol::xproto::{change_property, intern_atom, AtomEnum, GetGeometryReply, PropMode}; -// use x11rb::rust_connection::ConnectError; - -pub use x11rb::rust_connection::RustConnection; - -/// This is the bak -pub struct MumbleLinuxImpl { - mfile: File, - link_buffer: LinkBuffer, - /// we basically use this as the ui_tick of mumblelink - /// If this changed recently, it means jokolink is running (i.e. gw2 is running) - previous_jokolink_timestamp: i128, -} - -type LinkBuffer = Box<[u8; C_MUMBLE_LINK_SIZE_FULL]>; - -impl MumbleLinuxImpl { - pub fn new(link_name: &str) -> Result { - let mumble_file_name = format!("/dev/shm/{link_name}"); - info!("creating mumble file at {mumble_file_name}"); - #[allow(clippy::suspicious_open_options)] - let mut mfile = File::options() - .read(true) - .write(true) // write/append is needed for the create flag - .create(true) - .open(&mumble_file_name) - .into_diagnostic() - .wrap_err("failed to create mumble file")?; - let mut link_buffer = LinkBuffer::new([0u8; C_MUMBLE_LINK_SIZE_FULL]); - mfile.rewind().into_diagnostic()?; - mfile - .read(link_buffer.as_mut()) - .into_diagnostic() - .wrap_err("failed to get link buffer from mfile")?; - let previous_jokolink_timestamp = - unsafe { CMumbleLink::get_timestamp(link_buffer.as_ptr() as _) }; - Ok(MumbleLinuxImpl { - mfile, - link_buffer, - previous_jokolink_timestamp, - }) - } - pub fn tick(&mut self) -> Result<()> { - self.mfile.rewind().into_diagnostic()?; - self.mfile - .read(self.link_buffer.as_mut()) - .into_diagnostic() - .wrap_err("failed to get link buffer")?; - self.previous_jokolink_timestamp = - unsafe { CMumbleLink::get_timestamp(self.link_buffer.as_ptr() as _) }; - Ok(()) - } - pub fn is_alive(&self) -> bool { - OffsetDateTime::now_utc().unix_timestamp_nanos() - self.previous_jokolink_timestamp - < std::time::Duration::from_secs(1).as_nanos() as i128 - } - pub fn get_cmumble_link(&self) -> CMumbleLink { - if self.is_alive() { - unsafe { std::ptr::read(self.link_buffer.as_ptr() as _) } - } else { - Default::default() - } - } - // pub fn set_transient_for(&self) -> Result<()> { - // Ok(()) - // Ok(self - // .xc - // .set_transient_for(xid_from_buffer(&self.link_buffer))?) - // } -} - -// struct X11Connection { -// jokolay_window_id: u32, -// transient_for_atom: u32, -// // net_wm_pid_atom: u32, -// xc: RustConnection, -// } -// impl X11Connection { -// pub const WM_TRANSIENT_FOR: &'static str = "WM_TRANSIENT_FOR"; -// // pub const NET_WM_PID: &'static str = "_NET_WM_PID"; -// fn new(jokolay_window_id: u32) -> Result { -// let (xc, _) = RustConnection::connect(None).expect("failed to create x11 connection"); -// let transient_for_atom = intern_atom(&xc, true, Self::WM_TRANSIENT_FOR.as_bytes()) -// .map_err(|e| X11Error::AtomQueryError { -// source: e, -// atom_str: Self::WM_TRANSIENT_FOR, -// })? -// .reply() -// .map_err(|e| X11Error::AtomReplyError { -// source: e, -// atom_str: Self::WM_TRANSIENT_FOR, -// })? -// .atom; -// // let net_wm_pid_atom = intern_atom(&xc, true, Self::NET_WM_PID.as_bytes()) -// // .map_err(|e| X11Error::AtomQueryError { -// // source: e, -// // atom_str: Self::NET_WM_PID, -// // })? -// // .reply() -// // .map_err(|e| X11Error::AtomReplyError { -// // source: e, -// // atom_str: Self::NET_WM_PID, -// // })? -// // .atom; - -// Ok(Self { -// jokolay_window_id, -// transient_for_atom, -// xc, -// // net_wm_pid_atom, -// }) -// } -// pub fn set_transient_for(&self, parent_window: u32) -> Result<(), X11Error> { -// if let Ok(xst) = std::env::var("XDG_SESSION_TYPE") { -// if xst == "wayland" { -// tracing::warn!("skipping transient_for because we are on wayland"); -// return Ok(()); -// } -// if xst != "x11" { -// tracing::warn!("xdg session type is neither wayland not x11: {xst}"); -// } -// } -// assert_ne!(parent_window, 0); -// change_property( -// &self.xc, -// PropMode::REPLACE, -// self.jokolay_window_id, -// self.transient_for_atom, -// AtomEnum::WINDOW, -// 32, -// 1, -// &parent_window.to_ne_bytes(), -// ) -// .map_err(|e| X11Error::TransientForError { -// source: e, -// parent: parent_window, -// child: self.jokolay_window_id, -// })? -// .check() -// .map_err(|e| X11Error::TransientForReplyError { -// source: e, -// parent: parent_window, -// child: self.jokolay_window_id, -// })?; -// Ok(()) -// } - -// pub fn get_window_dimensions(&self, xid: u32) -> Result<[i32; 4]> { -// assert_ne!(xid, 0); -// let geometry = x11rb::protocol::xproto::get_geometry(&self.xc, xid) -// .into_diagnostic() -// .wrap_err("get geometry fn failed")? -// .reply() -// .into_diagnostic() -// .wrap_err("geometry reply is wrong")?; -// let translated_coordinates = x11rb::protocol::xproto::translate_coordinates( -// &self.xc, -// xid, -// geometry.root, -// geometry.x, -// geometry.y, -// ) -// .into_diagnostic() -// .wrap_err("failed to translate coords")? -// .reply() -// .into_diagnostic() -// .wrap_err("translate coords reply error")?; -// let x_outer = translated_coordinates.dst_x as i32; -// let y_outer = translated_coordinates.dst_y as i32; -// let width = geometry.width; -// let height = geometry.height; - -// tracing::debug!( -// "translated_x: {}, translated_y: {}, width: {}, height: {}, geo_x: {}, geo_y: {}", -// x_outer, -// y_outer, -// width, -// height, -// geometry.x, -// geometry.y -// ); -// Ok([x_outer, y_outer, width as _, height as _]) -// } -// // pub fn get_pid_from_xid(&self, xid: u32) -> Result { -// // assert_ne!(xid, 0); - -// // let pid_prop = get_property( -// // &self.xc, -// // false, -// // xid, -// // self.net_wm_pid_atom, -// // AtomEnum::CARDINAL, -// // 0, -// // 1, -// // ) -// // .expect("coudn't get _NET_WM_PID property gw2") -// // .reply() -// // .expect("reply for _NET_WM_PID property gw2 "); - -// // if pid_prop.bytes_after != 0 -// // && pid_prop.format != 32 -// // && pid_prop.value_len != 1 -// // && pid_prop.value.len() != 4 -// // { -// // panic!("invalid pid property {:#?}", pid_prop); -// // } -// // Ok(u32::from_ne_bytes(pid_prop.value.try_into().expect( -// // "pid property value has a bytes length of less than 4", -// // ))) -// // } -// } -// pub fn get_frame_extents(xc: &RustConnection, xid: u32) -> Result<(u32, u32, u32, u32)> { -// assert_ne!(xid, 0); -// let net_frame_extents_atom = intern_atom(&self.xc, true, b"_NET_FRAME_EXTENTS") -// .expect("coudn't intern atom for _NET_FRAME_EXTENTS ")? -// .reply() -// .expect("reply for intern atom for _NET_FRAME_EXTENTS")? -// .atom; -// let frame_prop = get_property( -// &self.xc, -// false, -// xid, -// net_frame_extents_atom, -// AtomEnum::ANY, -// 0, -// 100, -// ) -// .expect("coudn't get frame property gw2")? -// .reply() -// .expect("reply for frame property gw2")?; - -// if frame_prop.bytes_after != 0 { -// bail!( -// "bytes after in frame property is {}", -// frame_prop.bytes_after -// ); -// } -// if frame_prop.format != 32 { -// bail!("frame_prop format is {}", frame_prop.format); -// } -// if frame_prop.value_len != 4 { -// bail!("frame_prop value_len is {}", frame_prop.value_len); -// } -// if frame_prop.value.len() != 16 { -// bail!("frame_prop.value.len() is {}", frame_prop.value.len()); -// } -// // avoid bytemuck dependency and just do this raw. -// let mut arr = [0u8; 4]; -// arr.copy_from_slice(&frame_prop.value[0..4]); -// let left_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[4..8]); -// let right_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[8..12]); -// let top_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[12..16]); -// let bottom_border = u32::from_ne_bytes(arr); -// Ok((left_border, right_border, top_border, bottom_border)) -// } - -// pub fn get_gw2_pid(&mut self) -> Result { -// assert_ne!(self.gw2_window_handle, 0); -// let pid_atom = x11rb::protocol::xproto::intern_atom(&self.&self.xc, true, b"_NET_WM_PID") -// .expect("could not intern atom '_NET_WM_PID'")? -// .reply() -// .expect("reply error while interning '_NET_WM_PID'.")? -// .atom; -// let reply = x11rb::protocol::xproto::get_property( -// &self.&self.xc, -// false, -// self.gw2_window_handle, -// pid_atom, -// x11rb::protocol::xproto::AtomEnum::CARDINAL, -// 0, -// 1, -// ) -// .expect("could not request '_NET_WM_PID' for gw2 window handle ")? -// .reply() -// .expect("the reply for '_NET_WM_PID' of gw2 handle ")?; - -// let pid_format = 32; -// if pid_format != reply.format { -// bail!("pid_format is not 32. so, type is wrong"); -// } -// let pid_buffer_size = 4; -// if pid_buffer_size != reply.value.len() { -// bail!("pid_buffer is not 4 bytes"); -// } -// let value_len = 1; -// if value_len != reply.value_len { -// bail!("pid reply's value_len is not 1"); -// } -// let remaining_bytes_len = 0; -// if remaining_bytes_len != reply.bytes_after { -// bail!("we still have too many bytes remaining after reading '_NET_WM_PID'"); -// } -// let mut buffer = [0u8; 4]; -// buffer.copy_from_slice(&reply.value); -// Ok(u32::from_ne_bytes(buffer)) -// } diff --git a/crates/joko_link/src/mumble/ctypes.rs b/crates/joko_link/src/mumble/ctypes.rs deleted file mode 100644 index 72dd4ac..0000000 --- a/crates/joko_link/src/mumble/ctypes.rs +++ /dev/null @@ -1,288 +0,0 @@ -use enumflags2::BitFlags; -use miette::bail; -use serde::{Deserialize, Serialize}; - -use crate::{UISize, UIState}; - -/// The total size of the CMumbleLink struct. used to know the amount of memory to give to win32 call that creates the shared memory -pub const C_MUMBLE_LINK_SIZE_FULL: usize = std::mem::size_of::(); -/// This is how much of the CMumbleLink memory that is actually useful and updated. the rest is just zeroed out. -pub const USEFUL_C_MUMBLE_LINK_SIZE: usize = 1196; - -/// The CMumblelink is how it is represented in the memory. But we rarely use it as it is and instead convert it into MumbleLink before using it for convenience -/// Many of the fields are documentad in the actual MumbleLink struct -#[derive(Debug, Clone, Copy)] -#[repr(C)] -pub struct CMumbleLink { - //// The ui_version will always be same as mumble doesn't change. we will come back to change it IF there's a new version. - pub ui_version: u32, - //// This tick represents the update count of the link (which is usually the frame count ) since mumble was initialized. not from the start of game, but the start of mumble - pub ui_tick: u32, - //// position of the character - pub f_avatar_position: [f32; 3], - //// direction towards which the character is facing - pub f_avatar_front: [f32; 3], - //// the up direction vector of the character. - pub f_avatar_top: [f32; 3], - //// The name of the character currently logged in - pub name: [u16; 256], - //// The position of the camera - pub f_camera_position: [f32; 3], - //// The direction towards which the camera is facing - pub f_camera_front: [f32; 3], - //// The up direction for the camera - pub f_camera_top: [f32; 3], - //// This is a widestring of json containing the serialized data of [CIdentity] - pub identity: [u16; 256], - //// The [Self::context] field is 256 bytes, but the game only uses the first few bytes. - //// The first 48 bytes are used by mumble to uniquely identify the map/instance/room of the player - //// So, this field is always set to 48 bytes. - //// But gw2 writes even more data for the sake of addon functionality like minimap position etc.. - //// So, adding another 37 bytes which gw2 writes to. The total length of context is roughly 88 bytes if we consider the alignment. - pub context_len: u32, - //// 88 bytes are useful context written by gw2. Jokolink writes some more additional data beyond the 88 bytes like - //// X11 ID or window size or the timestamp when it last wrote data to this link etc.. which is useful for linux native clients like jokolay - pub context: CMumbleContext, - // Useless for now. Nothing is ever written here. - // we will just remove this field and add the size when creating shared memory. - // no point in copying more than 5kb when we only care about the first 1kb. - // pub description: [u16; 2048], -} -impl Default for CMumbleLink { - fn default() -> Self { - Self { - ui_version: Default::default(), - ui_tick: Default::default(), - f_avatar_position: Default::default(), - f_avatar_front: Default::default(), - f_avatar_top: Default::default(), - name: [0; 256], - f_camera_position: Default::default(), - f_camera_front: Default::default(), - f_camera_top: Default::default(), - identity: [0; 256], - context_len: Default::default(), - context: Default::default(), - // description: [0; 2048], - } - } -} - -impl CMumbleLink { - /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read - pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { - unsafe { std::ptr::read_volatile(link_ptr) } - } - - /// Checks if the MumbleLink memory is actually initialized by checking if [CMumbleLink::ui_tick] is non-zero. - /// Even if it returns true because [`CMumbleLink::ui_tick`] is non-zero, it could be a remnant from an older gw2 process. - /// The only way to verify that gw2 is active (with a character logged into a map), is to check if the tick changed from last frame to current frame. - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - pub unsafe fn is_valid(link_ptr: *const CMumbleLink) -> bool { - unsafe { (*link_ptr).ui_tick > 0 } - } - - /// gets uitick if we want to know the frame number since initialization of CMumbleLink - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then return value will be zero - /// 3. Even if it is not zero, the ui_tick maybe a stale because the game is dead (or in map loading screen / character select screen / cutscene) - pub unsafe fn get_ui_tick(link_ptr: *const CMumbleLink) -> u32 { - (*link_ptr).ui_tick - } - /// gets the pid from [CMumbleLink::context] field - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then pid will be zero - /// 3. Even if it is initialized, the process could be dead and the pid may be reused for a different process now - pub unsafe fn get_pid(link_ptr: *const CMumbleLink) -> u32 { - (*link_ptr).context.process_id - } - // #[cfg(unix)] - // pub unsafe fn get_xid(link_ptr: *const CMumbleLink) -> u32 { - // (*link_ptr).context.xid - // } - // #[cfg(unix)] - // pub unsafe fn get_pos_size(link_ptr: *const CMumbleLink) -> [i32; 4] { - // (*link_ptr).context.client_pos_size - // } - /// This gets the timestamp written by `jokolink` - /// The return value is nanoseconds since unix_epoch. - /// This is an easy way to check that jokolink (and by extension gw2) is still alive even if ui_tick doesn't change. - /// This happens when gw2 is in character select screen or cutscene etc.. when ui_tick stops updating. - /// # Safety - /// 1. `link_ptr` must be valid and point to memory of atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If it is uninitialized, the return value could be zero - #[cfg(unix)] - pub unsafe fn get_timestamp(link_ptr: *const CMumbleLink) -> i128 { - let bytes = (*link_ptr).context.timestamp; - i128::from_le_bytes(bytes) - } -} - -#[derive(Debug, Clone, Copy)] -#[repr(C)] -/// The mumble context as stored inside the context field of CMumbleLink. -/// the first 48 bytes Mumble uses for identification is upto `build_id` field -/// the rest of the fields after `build_id` are provided by gw2 for addon devs. -pub struct CMumbleContext { - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - pub server_address: [u8; 28], // contains sockaddr_in or sockaddr_in6 - /// Map ID - pub map_id: u32, - pub map_type: u32, - pub shard_id: u32, - pub instance: u32, - pub build_id: u32, - /// The fields until now are provided for mumble. - /// The rest of the data from here is what gw2 provides for the benefit of addons. - /// This is the current UI state of the game. refer to [UIState] - /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: u32, - pub compass_width: u16, // pixels - pub compass_height: u16, // pixels - pub compass_rotation: f32, // radians - pub player_x: f32, // continentCoords - pub player_y: f32, // continentCoords - pub map_center_x: f32, // continentCoords - pub map_center_y: f32, // continentCoords - pub map_scale: f32, - /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. - /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid - /// on windows, we use this to get window handle which can give us a window size. - /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid - /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 - pub process_id: u32, - /// refers to [Mount] - /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match api - pub mount_index: u8, - /// This is where the context fields provided by gw2 end. - /// From here on, these are custom fields set by jokolink.dll for the use of jokolay - /// These fields will be set before writing the link data to the `/dev/shm/MumbleLink` file from which jokolay can pick it up - /// - /// timestamp when jokolink wrote this data. unix nanoseconds - /// This timestamp will be written every frame by jokolink even if mumble link is uninitialized. - /// This is [i128] in little endian byte order. We use a byte array instead of [i128] directly because context is aligned to 4 by default. And - /// [i64]/[i128] will change that alignment to 8. This will lead to 4 bytes padding between [CMumbleLink::context_len] and [CMumbleLink::context] - /// - /// If jokolink doesn't write for more than 1 or 2 seconds, it can be safely assumed that gw2 was closed/crashed. - /// This is in nanoseconds since unix epoch in UTC timezone. - pub timestamp: [u8; 16], - /// This represents the x11 window id of the gw2 window. AFAIK, wine uses x11 only (no wayland), so this could be useful to set transient for - pub xid: u32, - /* - pub window_pos_size_without_borders: [i32; 4], - /// x, y, width, height of guild wars 2 window relative to top left corner of the screen. - /// This is populated with `GetWindowRect` fn - /// DPI aware. In screen coordinate. But includes drop shadow too :(. - pub window_pos_size: [i32; 4], - */ - /// dpi awareness of the gw2 process. Most probably will be `2` and below we have the relevant MS docs - /// DPI_AWARENESS_PER_MONITOR_AWARE - /// Value: 2 - /// Per monitor DPI aware. This process checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes. These processes are not automatically scaled by the system. - pub dpi_scaling: i32, - /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. - pub dpi: i32, - /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. - pub client_pos: [i32; 2], - pub client_size: [u32; 2], - /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. - pub padding: [u8; 96], -} -impl Default for CMumbleContext { - fn default() -> Self { - assert_eq!(std::mem::size_of::(), 256); - Self { - server_address: Default::default(), - map_id: Default::default(), - map_type: Default::default(), - shard_id: Default::default(), - instance: Default::default(), - build_id: Default::default(), - ui_state: Default::default(), - compass_width: Default::default(), - compass_height: Default::default(), - compass_rotation: Default::default(), - player_x: Default::default(), - player_y: Default::default(), - map_center_x: Default::default(), - map_center_y: Default::default(), - map_scale: Default::default(), - process_id: Default::default(), - mount_index: Default::default(), - timestamp: Default::default(), - // window_pos_size: Default::default(), - padding: [0; 96], - xid: Default::default(), - // window_pos_size_without_borders: Default::default(), - dpi_scaling: Default::default(), - dpi: Default::default(), - client_pos: Default::default(), - client_size: Default::default(), - } - } -} -impl CMumbleContext { - pub fn get_ui_state(&self) -> Option> { - BitFlags::from_bits(self.ui_state).ok() - } - - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - /// contains sockaddr_in or sockaddr_in6 - pub fn get_map_ip(&self) -> miette::Result { - if self.server_address[0] != 2 { - // add ipv6 support when gw2 servers add ipv6 support. - bail!("ipaddr parsing failed for CMumble Context"); - } - let ip = std::net::Ipv4Addr::from([ - self.server_address[4], - self.server_address[5], - self.server_address[6], - self.server_address[7], - ]); - Ok(ip) - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] -#[serde(crate = "serde")] -/// The json structure of the Identity field inside Cmumblelink. -/// the json string is null terminated and utf-16 encoded. so, need to use -/// Widestring crate's U16Cstring to first parse the bytes and then, convert to -/// String before deserializing to CIdentity -pub struct CIdentity { - /// The name of the character - pub name: String, - /// The core profession id of the character. matches the ids of v2/professions endpoint - pub profession: u32, - /// Character's third specialization, or 0 if no specialization is present. See /v2/specializations for valid IDs. - pub spec: u32, - /// The race of the character. does not match api - pub race: u32, - /// API:2/maps - pub map_id: u32, - /// useless field from pre-megaserver days. is just shard_id from context struct - pub world_id: u32, - /// Team color per API:2/colors (0 = white) - pub team_color_id: u32, - /// Whether the character has a commander tag active - pub commander: bool, - /// Vertical field-of-view - pub fov: f32, - /// A value corresponding to the user's current UI scaling. - pub uisz: u32, -} - -impl CIdentity { - pub fn get_uisz(&self) -> Option { - Some(match self.uisz { - 0 => UISize::Small, - 1 => UISize::Normal, - 2 => UISize::Large, - 3 => UISize::Larger, - _ => return None, - }) - } -} diff --git a/crates/joko_link/src/mumble/mod.rs b/crates/joko_link/src/mumble/mod.rs deleted file mode 100644 index 16a38d3..0000000 --- a/crates/joko_link/src/mumble/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -pub mod ctypes; -use std::net::IpAddr; - -use enumflags2::{bitflags, BitFlags}; -use num_derive::FromPrimitive; -use num_derive::ToPrimitive; - -use joko_core::serde_glam::*; -use serde::{Deserialize, Serialize}; - -/// As the CMumbleLink has all the fields multiple -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MumbleLink { - /// ui tick. (more or less represents the frame number of gw2) - pub ui_tick: u32, - /// character position - pub player_pos: Vec3, - /// direction char is facing - pub f_avatar_front: Vec3, - /// camera position - pub cam_pos: Vec3, - /// direction camera is facing - pub f_camera_front: Vec3, - /// The name of the character - pub name: String, - /// API:2/maps - pub map_id: u32, - pub map_type: u32, - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - pub server_address: IpAddr, // contains sockaddr_in or sockaddr_in6 - pub shard_id: u32, - pub instance: u32, - pub build_id: u32, - /// The fields until now are provided for mumble. - /// The rest of the data from here is what gw2 provides for the benefit of addons. - /// This is the current UI state of the game. refer to [UIState] - /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: Option>, - pub compass_width: u16, // pixels - pub compass_height: u16, // pixels - pub compass_rotation: f32, // radians - pub player_x: f32, // continentCoords - pub player_y: f32, // continentCoords - pub map_center_x: f32, // continentCoords - pub map_center_y: f32, // continentCoords - pub map_scale: f32, - /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. - /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid - /// on windows, we use this to get window handle which can give us a window size. - /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid - /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 - pub process_id: u32, - /// refers to [Mount] - /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api - //pub mount: Option, - //pub race: Race, - pub mount: u8, - pub race: u32, - - /// Vertical field-of-view - pub fov: f32, - /// A value corresponding to the user's current UI scaling. - pub uisz: UISize, - // pub window_pos: IVec2, - // pub window_size: IVec2, - // pub window_pos_without_borders: IVec2, - // pub window_size_without_borders: IVec2, - /// This is the dpi of gw2 window. 96dpi is the default for a non-hidpi monitor with scaling 1.0 - /// for a scaling of 2.0, it becomes 192 and so on. - pub dpi: i32, - /// This is whether gw2 is scaling its UI elements to match the dpi. So, if the dpi is bigger than 96, gw2 will make text/ui bigger. - /// -1 means we couldn't get the setting from gw2's config file in appdata/roaming - /// 0 means scaling is disabled (false) - /// 1 means scaling is enabled (true). - pub dpi_scaling: i32, - /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* - pub client_pos: IVec2, - /// This is the size of gw2's viewport (width/height) in screen coordinates - pub client_size: UVec2, - /// changes since last mumble link update - pub changes: BitFlags, -} -impl Default for MumbleLink { - fn default() -> Self { - Self { - ui_tick: Default::default(), - player_pos: Default::default(), - f_avatar_front: Default::default(), - cam_pos: Default::default(), - f_camera_front: Default::default(), - name: String::from("This Is Jokolay Dummy"), - map_id: Default::default(), - map_type: Default::default(), - server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), - shard_id: Default::default(), - instance: Default::default(), - build_id: Default::default(), - ui_state: Default::default(), - compass_width: Default::default(), - compass_height: Default::default(), - compass_rotation: Default::default(), - player_x: Default::default(), - player_y: Default::default(), - map_center_x: Default::default(), - map_center_y: Default::default(), - map_scale: Default::default(), - process_id: Default::default(), - mount: Default::default(), - race: u32::MAX, - fov: 2.0, - uisz: Default::default(), - dpi: Default::default(), - dpi_scaling: 96, - client_pos: Default::default(), - client_size: UVec2(glam::UVec2 { x: 1024, y: 768 }), - changes: Default::default(), - } - } -} -/// These flags represent the changes in mumble link compared to previous values -#[bitflags] -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum MumbleChanges { - UiTick = 1, - Map = 1 << 1, - Character = 1 << 2, - WindowPosition = 1 << 3, - WindowSize = 1 << 4, - Camera = 1 << 5, - Position = 1 << 6, -} - -/// represents the ui scale set in settings -> graphics options -> interface size -#[derive( - Debug, - Clone, - Default, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - FromPrimitive, - ToPrimitive, -)] -#[serde(crate = "serde")] -pub enum UISize { - Small = 0, - #[default] - Normal = 1, - Large = 2, - Larger = 3, -} - -#[bitflags] -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -/// The Uistate enum to represent the status of the UI in game -pub enum UIState { - IsMapOpen = 0b00000001, - IsCompassTopRight = 0b00000010, - DoesCompassHaveRotationEnabled = 0b00000100, - GameHasFocus = 0b00001000, - InCompetitiveGamemode = 0b00010000, - TextboxFocus = 0b00100000, - IsInCombat = 0b01000000, -} diff --git a/crates/joko_link/src/win/dll.rs b/crates/joko_link/src/win/dll.rs deleted file mode 100644 index 721b5fe..0000000 --- a/crates/joko_link/src/win/dll.rs +++ /dev/null @@ -1,490 +0,0 @@ -#![allow(non_snake_case)] - -arcdps::arcdps_export! { - name: "jokolink", - // This is just "joko" as hex bytes - sig: 0x6a6f6b6f, - init: init, - release: release, -} - -fn init() -> ::core::result::Result<(), Box> { - println!("jokolink init called by arcdps. spawning background thread for jokolink"); - unsafe { spawn_jokolink_thread() }; - Ok(()) -} -/// If no other thread has been spawned, this will spawn a new thread where jokolink will run -unsafe fn spawn_jokolink_thread() { - if d3d11::JOKOLINK_THREAD_HANDLE.is_none() { - let (quit_request_sender, quit_request_receiver) = std::sync::mpsc::sync_channel(0); - let (quit_response_sender, quit_response_receiver) = std::sync::mpsc::sync_channel(1); - - d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); - d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); - - let th = std::thread::Builder::new() - .name("jokolink thread".to_string()) - .spawn(move || { - d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); - "jokolink thread quit" - }); - match th { - Ok(handle) => { - println!("spawned jokolink thread. handle: {handle:?}"); - d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); - } - Err(e) => { - eprintln!("failed to spawn jokolink thread due to error {e:#?}"); - } - } - } else { - println!("jokolink thread has already been initialized, so skipping initialization."); - } -} -/// This is really unsafe, so we have to be careful -/// We cannot directly terminate thread because it might lead to some syncronization issues and cause a crash/deadlock -/// we HAVE to terminate the thread because otherwise, it will crash gw2 too. -/// So, we use channels to send a signal to jokolink thread to quit. -/// Then, we use another channel to wait and receive a signal that will be sent by jokolink thread when it terminates. -/// -/// We can't call `join` on the thread handle because.. like i said, it can lead to a deadlock/crash. -/// This applies whether we are loaded by game as d3d11.dll or by arcdps as an addon. -unsafe fn terminate_jokolink_thread() { - if let Some(sender) = d3d11::JOKOLINK_QUIT_REQUESTER.take() { - if let Err(e) = sender.send(()) { - eprintln!("failed to send quit signal due to error {e:#?}"); - } else { - println!("successfully sent the quit signal to the jokolink thread"); - } - } - if let Some(receiver) = d3d11::JOKOLINK_QUIT_RESPONDER.take() { - match receiver.recv() { - Ok(_) => { - println!("received quit response from jokolink thread"); - } - Err(e) => { - eprintln!("failed to receive quit response from jokolink thread. {e:#?}"); - } - } - } - if let Some(handle) = d3d11::JOKOLINK_THREAD_HANDLE.take() { - if handle.is_finished() { - println!("jokolink thread is finished"); - } else { - println!("jokolink thread is not yet finished, so waiting for it by joining the handle :(((("); - match handle.join() { - Ok(o) => { - println!("joined jokolink thread with return value: {o}"); - } - Err(e) => { - eprintln!("jokolink thread panic: {e:?}"); - } - } - } - } else { - println!("jokolink thread was never started. So, nothing to terminate"); - } -} -fn release() { - println!("jokolink release called by arcdps."); - unsafe { - terminate_jokolink_thread(); - } -} - -pub mod d3d11 { - use std::{ - sync::mpsc::{Receiver, SyncSender}, - thread::JoinHandle, - }; - - use windows::{ - core::*, - Win32::Foundation::*, - Win32::System::{ - LibraryLoader::{GetProcAddress, LoadLibraryA}, - SystemInformation::GetSystemDirectoryA, - // Threading::{CreateThread, TerminateThread, THREAD_CREATION_FLAGS}, - }, - }; - - /// Dll injection basics: - /// 1. You write a custom dll library exposing functions that match the names/signatures of the actual winapi functions - /// 2. Then, you place your custom dll library in gw2's executable directory. - /// 3. gw2 loads your dll and calls your functions thinking it is calling winapi functions. - /// 4. You will use this chance to do whatever you want, before forwarding the calls to the actual winapi functions - /// 5. So, we will load the dll from `system32` directory once. store it in [DLL_PTR] - /// 6. When a function is called, we check if the fn pointer is already loaded. If it is not, we get it from the dll pointer - static mut DLL_PTR: HMODULE = HMODULE(0); - static mut CREATE_DEVICE_FNPTR: Option< - unsafe extern "system" fn( - padapter: *mut ::core::ffi::c_void, - drivertype: i32, - software: HMODULE, - flags: u32, - pfeaturelevels: *const i32, - featurelevels: u32, - sdkversion: u32, - ppdevice: *mut *mut ::core::ffi::c_void, - pfeaturelevel: *mut i32, - ppimmediatecontext: *mut *mut ::core::ffi::c_void, - ) -> HRESULT, - > = None; - pub static mut JOKOLINK_THREAD_HANDLE: Option> = None; - - /// This is used to tell wine_main fn thread to quit. - pub static mut JOKOLINK_QUIT_REQUESTER: Option> = None; - /// This is used to wait for wine_main fn thread to quit and send us a signal - pub static mut JOKOLINK_QUIT_RESPONDER: Option> = None; - /// This function is called whenever the dll is loaded into process or thread, and whenever the dll is unloaded out of process/thread. - /// # Safety - /// Don't do *anything* complicated at all. It can easily lead to a deadlock - /// https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices - /// Improper synchronization within DllMain can cause an application to deadlock or access data or code in an uninitialized DLL. - #[no_mangle] - pub unsafe extern "system" fn DllMain( - _dll_module: HINSTANCE, - call_reason: u32, - _: *mut (), - ) -> bool { - match call_reason { - // process detach - 0 => { - // unlike attach - println!("jokolink dll is being detached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}."); - super::terminate_jokolink_thread(); - } - // process attach - 1 => { - // Sometimes, our dll might be attached/detached multiple times. And we don't want to start jokolink_thread everything time - // Instead, we only launch our jokolink thread when the D3D11CreateDevice is called - println!("jokolink dll has been attached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}"); - } - // thread attach and detach - 2 | 3 => { - // no need to do anything for thread attach and thread detach - } - // invalid values - rest => { - eprintln!("unrecognized dll main call reason: {rest}"); - } - } - true - } - /// This is the function we will "hook" into. - /// GW2 will call this function right after the "login window" when creating the main window - /// This is where we initialize our jokolink thread. - /// # Safety - /// Just need to load d3d11.dll from windows/system32 equivalent directory and call that function for gw2 - #[no_mangle] - pub unsafe extern "system" fn D3D11CreateDevice( - padapter: *mut ::core::ffi::c_void, - drivertype: i32, - software: HMODULE, - flags: u32, - pfeaturelevels: *const i32, - featurelevels: u32, - sdkversion: u32, - ppdevice: *mut *mut ::core::ffi::c_void, - pfeaturelevel: *mut i32, - ppimmediatecontext: *mut *mut ::core::ffi::c_void, - ) -> HRESULT { - if DLL_PTR.is_invalid() { - let mut path = [0u8; MAX_PATH as _]; - let len = GetSystemDirectoryA(Some(&mut path)) as usize; - // we make sure that len is not zero. It means that GetSystemDirectoryA fn didn't fail. - // we also check if length is above 200, because then we might be reaching the limit of maximum path length supported by windows. - if len == 0 || len > 200 { - eprintln!("the system directory path size is: {len}. So, i am quitting"); - return HRESULT::default(); - } - const D3D11_DLL_PATH: &str = "\\d3d11.dll\0"; - path[len..(len + D3D11_DLL_PATH.len())].copy_from_slice(D3D11_DLL_PATH.as_bytes()); - - match LoadLibraryA(PCSTR::from_raw(path.as_ptr())) { - Ok(p) => { - println!("successfully loaded library d3d11.dll "); - DLL_PTR = p; - } - Err(e) => { - eprintln!("could not load d3d11.dll from system path due to error: {e:#?}"); - return HRESULT::default(); - } - } - } else { - println!("d3d11.dll library is already loaded. So, skipping that"); - } - if CREATE_DEVICE_FNPTR.is_none() { - if let Some(p) = GetProcAddress(DLL_PTR, PCSTR("D3D11CreateDevice\0".as_ptr())) { - println!("successfully got proc address of D3D11CreateDevice"); - let _ = CREATE_DEVICE_FNPTR.insert(std::mem::transmute(p)); - } else { - eprintln!("could not load address of D3D11CreateDevice"); - } - } else { - println!("D3D11CreateDevice fn ptr is already loaded, so skipped that"); - } - if JOKOLINK_THREAD_HANDLE.is_none() { - println!("starting jokolink's wine_main on another thrad"); - - super::spawn_jokolink_thread(); - } - println!("calling D3D11CreateDevice fn"); - if let Some(p) = CREATE_DEVICE_FNPTR { - p( - padapter, - drivertype, - software, - flags, - pfeaturelevels, - featurelevels, - sdkversion, - ppdevice, - pfeaturelevel, - ppimmediatecontext, - ) - } else { - HRESULT::default() - } - } - - // unsafe extern "system" fn wine_main(_: *mut ::core::ffi::c_void) -> u32 { - // super::spawn_jokolink_thread(); - // 0 - // } - pub mod wine { - use crate::mumble::ctypes::*; - use crate::win::MumbleWinImpl; - use crate::DEFAULT_MUMBLELINK_NAME; - use miette::{Context, IntoDiagnostic, Result}; - use serde::{Deserialize, Serialize}; - use std::io::Write; - use std::io::{Seek, SeekFrom}; - use std::path::{Path, PathBuf}; - use std::str::FromStr; - use std::sync::mpsc::{Receiver, SyncSender}; - use std::time::Duration; - use tracing::{error, info}; - use tracing_subscriber::filter::LevelFilter; - #[derive(Debug, Clone, Serialize, Deserialize)] - #[serde(default)] - pub struct JokolinkConfig { - pub loglevel: String, - pub logdir: PathBuf, - pub mumble_link_name: String, - pub interval: u32, - pub copy_dest_dir: PathBuf, - } - - impl Default for JokolinkConfig { - fn default() -> Self { - Self { - loglevel: "info".to_string(), - logdir: PathBuf::from("."), - mumble_link_name: DEFAULT_MUMBLELINK_NAME.to_string(), - interval: 5, - copy_dest_dir: PathBuf::from("z:\\dev\\shm"), - } - } - } - - pub fn wine_main( - quit_request_receiver: Receiver<()>, - quit_response_sender: SyncSender<()>, - ) { - if let Err(e) = std::panic::catch_unwind(move || { - let config = "./jokolink_config.json".to_string(); - let config = std::path::PathBuf::from(config); - if !config.exists() { - match std::fs::File::create(&config) { - Ok(mut f) => match serde_json::to_string_pretty(&JokolinkConfig::default()) - { - Ok(config_string) => { - if let Err(e) = f.write_all(config_string.as_bytes()) { - eprintln!( - "failed to write default config file due to error {e:#?}" - ); - } - } - Err(e) => { - eprintln!("failed to serialize default config due to error {e:#?}"); - } - }, - Err(e) => eprintln!("failed to create config.json due to error {e:#?}"), - } - } - let config: JokolinkConfig = match std::fs::File::open(&config) { - Ok(f) => match serde_json::from_reader(std::io::BufReader::new(f)) { - Ok(config) => config, - Err(e) => { - eprintln!("failed to deserialize config file due to error {e:#?}"); - return; - } - }, - Err(e) => { - eprintln!("failed to open config file due to error {e:#?}"); - return; - } - }; - println!("successfully loaded configuration file"); - match miette::set_hook(Box::new(|_| { - Box::new( - miette::MietteHandlerOpts::new() - .unicode(true) - .context_lines(4) - .with_cause_chain() - .build(), - ) - })) { - Ok(_) => { - println!("miette hook set"); - } - Err(e) => { - eprintln!("failed to set miette hook due to {e:#?}"); - } - } - let guard = match log_init( - LevelFilter::from_str(&config.loglevel).unwrap_or(LevelFilter::INFO), - &config.logdir, - Path::new("jokolink.log"), - ) { - Ok(g) => g, - Err(e) => { - eprintln!("failed to initiailize logging due to error {e:#?}"); - return; - } - }; - if let Err(e) = fake_main(config, quit_request_receiver) { - eprintln!("fake main exited due to error: {e:#?}"); - } - std::mem::drop(guard); - println!("dropped logfile guard"); - }) { - eprintln!("There was a panic in jokolink thread: {e:?}"); - } - println!("exiting wine_main function"); - match quit_response_sender.send(()) { - Ok(_) => { - println!("successfully sent quit response"); - } - Err(e) => { - eprintln!("failed to send quit response due to: {e:#?}"); - } - } - } - - fn fake_main(config: JokolinkConfig, quit_signal: Receiver<()>) -> Result<()> { - let refresh_inverval = Duration::from_millis(config.interval as u64); - - info!("Application Name: {}", env!("CARGO_PKG_NAME")); - info!("Application Version: {}", env!("CARGO_PKG_VERSION")); - info!("Application Authors: {}", env!("CARGO_PKG_AUTHORS")); - info!( - "Application Repository Link: {}", - env!("CARGO_PKG_REPOSITORY") - ); - info!("Application License: {}", env!("CARGO_PKG_LICENSE")); - - // info!("git version details: {}", git_version::git_version!()); - - info!( - "the file log lvl: {:?}, the logfile directory: {:?}", - &config.loglevel, &config.logdir - ); - info!("created app and initialized logging"); - info!("the mumble link names: {:#?}", &config.mumble_link_name); - info!( - "the mumble refresh interval in milliseconds: {:#?}", - refresh_inverval - ); - - info!( - "the path to which we write mumble data: {:#?}", - &config.copy_dest_dir - ); - let mumble_key = config.mumble_link_name.clone(); - - let dest_path = config.copy_dest_dir.join(&mumble_key); - - // create a shared memory file in /dev/shm/mumble_link_key_name so that jokolay can mumble stuff from there. - info!( - "creating the path to destination shm file: {:?}", - &dest_path - ); - - #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] - let mut mfile = std::fs::File::options() - .write(true) - .create(true) - .open(&dest_path) - .into_diagnostic() - .wrap_err_with(|| { - format!("failed to create shm file with path {:#?}", &dest_path) - })?; - // create shared memory using the mumble link key - let mut source = MumbleWinImpl::new(&mumble_key)?; - - loop { - if let Err(e) = source.tick() { - error!(?e, "mumble tick error"); - } - let link = source.get_cmumble_link(); - - let buffer: [u8; C_MUMBLE_LINK_SIZE_FULL] = - unsafe { std::ptr::read_volatile(&link as *const CMumbleLink as *const _) }; - mfile - .seek(SeekFrom::Start(0)) - .into_diagnostic() - .wrap_err("could not seek to start of shared memory file due to error")?; - - // write buffer to the file - mfile - .write(&buffer) - .into_diagnostic() - .wrap_err("could not write to shared memory file due to error")?; - match quit_signal.try_recv() { - Ok(_) => { - println!("received quit signal. returning from wine_main()"); - error!("received quit signal. returning from wine_main()"); - return Ok(()); - } - Err(e) => match e { - std::sync::mpsc::TryRecvError::Empty => {} - std::sync::mpsc::TryRecvError::Disconnected => { - eprintln!("why is the quit signaller sender disconnected????"); - } - }, - } - // we sleep for a few milliseconds to avoid reading mumblelink too many times. we will read it around 100 to 200 times per second - std::thread::sleep(refresh_inverval); - } - } - - /// initializes global logging backend that is used by log macros - /// Takes in a filter for stdout/stderr, a filter for logfile and finally the path to logfile - pub fn log_init( - file_filter: LevelFilter, - log_directory: &Path, - log_file_name: &Path, - ) -> Result { - // let file_appender = tracing_appender::rolling::never(log_directory, log_file_name); - let file_path = log_directory.join(log_file_name); - let writer = std::io::BufWriter::new( - std::fs::File::create(&file_path) - .into_diagnostic() - .wrap_err_with(|| { - format!("failed to create logfile at path: {:#?}", &file_path) - })?, - ); - let (nb, guard) = tracing_appender::non_blocking(writer); - tracing_subscriber::fmt() - .with_writer(nb) - .with_max_level(file_filter) - .pretty() - .with_ansi(false) - .init(); - - Ok(guard) - } - } -} diff --git a/crates/joko_link/src/win/mod.rs b/crates/joko_link/src/win/mod.rs deleted file mode 100644 index 21ebc75..0000000 --- a/crates/joko_link/src/win/mod.rs +++ /dev/null @@ -1,735 +0,0 @@ -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -pub mod dll; -//putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration - -use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; -use miette::{bail, Context, IntoDiagnostic, Result}; -use notify::Watcher; -use std::{ - path::PathBuf, - str::FromStr, - time::{Duration, Instant}, -}; -use time::OffsetDateTime; -use tracing::{debug, error, info, warn}; -use windows::{ - core::PCSTR, - Win32::{ - Foundation::*, - Graphics::{ - Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}, - Gdi::ClientToScreen, - }, - System::{ - Com::CoTaskMemFree, - Memory::*, - Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}, - }, - UI::{ - HiDpi::{GetDpiForWindow, GetProcessDpiAwareness}, - Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath}, - WindowsAndMessaging::*, - }, - }, -}; - -/// This source will be the used to abstract the linux/windows way of getting MumbleLink -/// on windows, this represents the shared memory pointer to mumblelink, and as long as one of gw2 or a client like us is alive, the shared memory will stay alive -/// on linux, this will be a File in /dev/shm that will only exist if jokolink created it at some point in time. this lives in ram, so reading from it is pretty much free. -#[derive(Debug)] -pub struct MumbleWinImpl { - /// This is the pointer to shared memory which we mapped into our address space - /// This is NEVER null. Because we consider failing to create MumbleLink as a hard error. - /// ## Unsafe: - /// Must unmap this pointer when we are dropping - link_ptr: *const CMumbleLink, - /// This is the handle to shared memory. We must close the handle when we are quitting - /// This also never invalid. Because we consider failing to create MumbleLink as a hard error. - /// ## Unsafe: - /// Must close this handle when we are dropping - mumble_handle: HANDLE, - /// this is the previous ui_tick. We use this to check if there has been any change in mumble link memory - /// If there is a change, then we check if the new pid is the same as old pid - previous_ui_tick: u32, - /// This is the previous pid of the mumble link - /// If the current pid has changed, then it means we are dealing with a new gw2 process. - previous_pid: u32, - /// This is the process handle for gw2. - /// when we see a change in pid, we will close the handle (if its valid) and open a new handle to the new gw2 process - /// - /// This handle is very important, because its validity shows that the gw2 process is "alive". - /// If ui_tick has not changed for more than a second, then we will check using windows api if the process is still alive. - /// If not, we will reset everything in our struct except for last_pid and last_ui_tick. - process_handle: HANDLE, - /// if ui_tick updates, we set this to now. - /// If ui_tick doesn't update for more than 1 second AND we are alive, we will check if gw2 is still alive and reset the timestamp. - last_ui_tick_update: Instant, - /// if ui_tick changes this frame and we are alive, we get window size/pos of gw2 and reset this. - /// if we are not alive, then we simply skip this check. - last_pos_size_check: Instant, - - /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. - client_pos: [i32; 2], - client_size: [u32; 2], - /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - /// 0 for false - /// 1 for true - /// -1 for no idea. maybe because we couldn't find the config or read it or whatever. - /// I recommend just assuming that it is true when in doubt. Because the text is too small to read when dpi scaling is turned off. - dpi_scaling: i32, - /// DPI of the gw2 window - /// We get this via win32 api - dpi: i32, - /// This is the window handle of gw2. - /// This is automatically set when we try to get window size/pos. and will be reset if gw2 process dies or if we find a new gw2 process. - window_handle: isize, - /// X11 window id. This is only useful for jokolink when it is run as dll on wine - /// When the struct is initialized, we also try to get xid. and keep it here. On windows, we will just keep it at zero. - xid: u32, - /// This is the $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - /// But we get this programmatically via ShGetKnownFolderPath - _gw2_config_watcher: notify::RecommendedWatcher, - gw2_config_changed: std::sync::Arc, - gw2_config_path: PathBuf, /* - /// This is the position and size of gw2 window. This also includes a few hidden pixels around gw2 which serve as the border - /// Every time we check if the process is alive - window_pos_size: [i32; 4], - /// same as above. But we use DwmGetWindowAttribute, to exclude the drop shadow borders from the window rect - window_pos_size_without_borders: [i32; 4], - */ -} - -unsafe impl Send for MumbleWinImpl {} - -impl MumbleWinImpl { - pub fn new(key: &str) -> Result { - unsafe { - let (handle, link_ptr) = - create_link_shared_mem(key).wrap_err("failed to create mumblelink shm ")?; - let gw2_config_changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let gw2_config_path = { - let roaming_appdata_pwstr = SHGetKnownFolderPath( - &FOLDERID_RoamingAppData as *const _, - Default::default(), - HANDLE::default(), - ) - .into_diagnostic() - .wrap_err("failed to get known folder roaming app data path")?; - - let mut roaming_str = roaming_appdata_pwstr - .to_string() - .into_diagnostic() - .wrap_err("appdata/roaming is not a utf-8 path")?; - info!(roaming_str, "RoamingAppData path"); - CoTaskMemFree(Some(roaming_appdata_pwstr.0 as _)); - if !roaming_str.ends_with('\\') { - roaming_str.push('\\'); - } - roaming_str.push_str("Guild Wars 2\\GFXSettings.Gw2-64.exe.xml"); - info!(roaming_str, "gw2 config path"); - roaming_str - }; - let gw2_config_path = std::path::PathBuf::from_str(&gw2_config_path) - .into_diagnostic() - .wrap_err("failed to create pathbuf from gw2 config path in roaming appdata")?; - std::fs::create_dir_all(gw2_config_path.parent().unwrap()) - .into_diagnostic() - .wrap_err("failed to create gw2 config dir in appdata roaming ")?; - if !gw2_config_path.exists() { - std::fs::File::create(&gw2_config_path) - .into_diagnostic() - .wrap_err("failed to create empty gw2 config file ")?; - } - let dpi_scaling = check_dpi_scaling_enabled(&gw2_config_path); - - info!( - ?dpi_scaling, - ?gw2_config_path, - "dpi scaling when we are starting out" - ); - // lets just assume that the scaling is true by default - let dpi_scaling = dpi_scaling.unwrap_or(1); - gw2_config_changed.store(false, std::sync::atomic::Ordering::Relaxed); - let gw2_config_changed_2 = gw2_config_changed.clone(); - let mut gw2_config_watcher = notify::recommended_watcher(move |ev| { - debug!(?ev, "gw2 config changed"); - gw2_config_changed_2.store(true, std::sync::atomic::Ordering::Relaxed); - }) - .into_diagnostic() - .wrap_err("failed to create gw2 config directory watcher")?; - gw2_config_watcher - .watch(&gw2_config_path, notify::RecursiveMode::NonRecursive) - .into_diagnostic() - .wrap_err("faield to watch gw2 config dir")?; - - Ok(Self { - link_ptr, - mumble_handle: handle, - window_handle: 0, - last_ui_tick_update: Instant::now(), - previous_ui_tick: CMumbleLink::get_ui_tick(link_ptr), - // window_pos_size: [0; 4], - process_handle: HANDLE::default(), - previous_pid: 0, - xid: 0, - last_pos_size_check: Instant::now(), - // window_pos_size_without_borders: [0; 4], - dpi_scaling, - client_pos: [0; 2], - client_size: [0; 2], - dpi: 0, - _gw2_config_watcher: gw2_config_watcher, - gw2_config_changed, - gw2_config_path, - }) - } - } - pub fn is_alive(&self) -> bool { - !self.process_handle.is_invalid() - } - pub fn get_cmumble_link(&mut self) -> CMumbleLink { - let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; - link.context.timestamp = OffsetDateTime::now_utc() - .unix_timestamp_nanos() - .to_le_bytes(); - // link.context.window_pos_size = self.window_pos_size; - // link.context.window_pos_size_without_borders = self.window_pos_size_without_borders; - link.context.dpi_scaling = self.dpi_scaling; - link.context.dpi = self.dpi; - link.context.xid = self.xid; - link.context.client_pos = self.client_pos; - link.context.client_size = self.client_size; - link - } - /// This is the most important function which will be called every frame - /// 1. it gets the ui_tick from the link pointer - /// 2. checks if it has changed compared to previous ui_tick. If it didn't change, then we have nothing to do and we return. - /// 3. If it changed, we check if it is less than previous_ui_tick OR if the pid is differnet from previous_pid or if our process handle is invalid - /// 4. If any of the above conditions are true, we reset and reinitialize the gw2 process handle + window handle + window size etc.. - /// 5. If ui_tick simply increased and nothing else changed, then we proceed with the usual stuf which is check the timer and get updated window pos/size - pub fn tick(&mut self) -> Result<()> { - unsafe { - // if ui_tick is zero, we return - if !CMumbleLink::is_valid(self.link_ptr) { - // if we alive, that means ui_tick turned zero this frame for whatever reason, so we reset. - if self.is_alive() { - self.reset(); - } - return Ok(()); - } - let ui_tick = CMumbleLink::get_ui_tick(self.link_ptr); - let pid = CMumbleLink::get_pid(self.link_ptr); - let previous_ui_tick = self.previous_ui_tick; - // if ui tick didn't change. Then it means either we are in loading scree / character select screen or gw2 was closed (or crashed) - if ui_tick == previous_ui_tick { - // if we are not alive, then we just return because it just means mumble is not being updated. - // but if we are alive, then we need to check whehter gw2 is still alive (in loading screen) or dead - if self.is_alive() { - // we don't want to check every frame. Instead, we check in intervals of 3 seconds until gw2 finally loads into a map or it closes (so we can reset) - if self.last_ui_tick_update.elapsed() > Duration::from_secs(3) { - self.last_ui_tick_update = Instant::now(); - match check_process_alive(self.process_handle) { - Ok(alive) => { - if !alive { - self.reset(); - } - } - Err(e) => { - error!(?e, "failed to get GetExitCodeProcess"); - self.reset(); - } - } - } - } - return Ok(()); - } - // if ui_tick has changed, then we have some stuff to do. - if ui_tick < previous_ui_tick // only happens if process changes - || pid != self.previous_pid // gw2 process changed. need to get new handles/sizes etc.. - || !self.is_alive() - // if we are in reset status, then its our chance to reinitialize because mumble just updated. - { - info!(ui_tick, notify = 2u64, "found new gw2 process"); - self.reinitialize(); - } - // if reinitialization failed, then we can try again next frame. - // if we are alive, that means everything is working as expected. - // we update the previous ui_tick and check if we need to update window pos/size - if self.is_alive() { - self.last_ui_tick_update = Instant::now(); - self.previous_ui_tick = ui_tick; - // check in 2 seconds intervals because it rarely changes - if self.last_pos_size_check.elapsed() > Duration::from_secs(2) { - self.last_pos_size_check = Instant::now(); - - // self.window_pos_size = match get_window_pos_size(self.window_handle) { - // Ok(window_pos_size) => { - // if self.window_pos_size != window_pos_size { - // info!( - // ?self.window_pos_size, ?window_pos_size, - // "window position size changed" - // ); - // } - // window_pos_size - // } - // Err(e) => { - // error!(?e, "failed to get window position size"); - // self.reset(); // go back to being dead because it shouldn't usually fail - // return Ok(()); - // } - // }; - // let dpi_awareness = match GetProcessDpiAwareness(self.process_handle) { - // Ok(dpi) => dpi.0, - // Err(e) => { - // error!(?e, "failed to get dpi awareness"); - // 0 - // } - // }; - // if self.dpi_scaling != dpi_awareness { - // info!(dpi_awareness, self.dpi_scaling, "dpi scaling changed"); - // } - // self.dpi_scaling = dpi_awareness; - - let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; - if dpi != self.dpi { - info!(dpi, self.dpi, "dpi changed for gw2 window"); - } - if dpi == 0 { - error!(dpi, "invalid dpi value for guild wars 2"); - } - self.dpi = dpi; - // if the config changed, we will attempt to read dpi scaling. - // if we fail, we will just ignore it, and try again during next check of window pos (2 secs?) - // if we succeed, we will store false in the atomic bool - if self - .gw2_config_changed - .load(std::sync::atomic::Ordering::Relaxed) - { - match check_dpi_scaling_enabled(&self.gw2_config_path) { - Ok(dpi_scaling) => { - if self.dpi_scaling != dpi_scaling { - info!(self.dpi_scaling, dpi_scaling, "dpi scaling changed"); - } - self.dpi_scaling = dpi_scaling; - self.gw2_config_changed - .store(false, std::sync::atomic::Ordering::Relaxed); - } - Err(e) => { - error!(notify = 0.0f64, ?e, "failed to open gw2 config file to check for dpi scaling changes"); - } - } - } - // self.window_pos_size_without_borders = - // match get_window_pos_size_without_borders(HWND(self.window_handle)) { - // Ok(window_pos_size_without_borders) => { - // if self.window_pos_size_without_borders - // != window_pos_size_without_borders - // { - // info!( - // ?self.window_pos_size_without_borders, - // ?window_pos_size_without_borders, - // "window position size changed" - // ); - // } - // window_pos_size_without_borders - // } - // Err(e) => { - // error!(?e, "failed to get window position size"); - // self.reset(); // go back to being dead because it shouldn't usually fail - // return Ok(()); - // } - // }; - match get_client_rect_in_screen_coords(HWND(self.window_handle)) { - Ok((client_pos, client_size)) => { - if self.client_pos != client_pos || self.client_size != client_size { - info!( - ?self.client_pos, - ?client_pos, - ?self.client_size, - ?client_size, - "window position or size changed" - ); - } - self.client_pos = client_pos; - self.client_size = client_size; - } - Err(e) => { - error!(?e, "failed to get client position size"); - self.reset(); // go back to being dead because it shouldn't usually fail - return Ok(()); - } - }; - } - } - } - Ok(()) - } - /// A function which clears all the gw2 related resources like process/window handles - unsafe fn reset(&mut self) { - warn!("resetting mumble data"); - self.window_handle = 0; - if !self.process_handle.is_invalid() { - if let Err(e) = CloseHandle(self.process_handle) { - error!(?e, "failed to close process handle of old gw2"); - } - } - self.process_handle = HANDLE::default(); - // self.window_pos_size = [0; 4]; - // self.window_pos_size_without_borders = [0; 4]; - self.dpi = 0; - self.client_pos = [0; 2]; - self.client_size = [0; 2]; - self.previous_pid = 0; - self.xid = 0; - } - unsafe fn reinitialize(&mut self) { - warn!("we are reinitializing our mumble data"); - info!( - "printing cmumblelink as it might be useful for debugging. {:?}", - self.get_cmumble_link() - ); - assert!( - CMumbleLink::is_valid(self.link_ptr), - "attempting to reinitialize when mumble is still unintialized" - ); - let pid = CMumbleLink::get_pid(self.link_ptr); - assert!(pid != 0, "attempting to initialize with pid == 0"); - self.reset(); - info!( - "ui_tick: {}. pid: {pid}", - CMumbleLink::get_ui_tick(self.link_ptr) - ); - match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { - Ok(process_handle) => { - info!("got process handle: {process_handle:?}"); - // get pid from mumble link - let mut window_handle = pid as isize; - - // enumerate windows and get the handle and assign it to the pid variable if the process id of the handle actually matches the pid - let _ = EnumWindows( - Some(get_handle_by_pid), - LPARAM(((&mut window_handle) as *mut isize) as isize), - ); - // if lparam_pid is still the same as pid, then we couldn't find the relevant window handle - if window_handle == pid as isize { - if let Err(e) = CloseHandle(process_handle) { - error!( - ?e, - "failed to close process handle when we couldn't get window handle." - ); - } - error!( - "failed to initialize mumble data because we couldn't find window handle" - ); - return; - } - info!("found window handle too. yay"); - // now we have both process_handle and window_handle. We just need the window size to initialize our struct - // this function only gets the suface/viewport pos/size without any borders/decoraitons. - match get_client_rect_in_screen_coords(HWND(window_handle)) { - Ok((client_pos, client_size)) => { - // this block is purely for logging purposes only to verify that all sizes are working properly. - { - // GetWindowRect includes drop shadow borders and titlebar - match get_window_pos_size(window_handle) { - Ok(pos_size) => { - info!( - ?pos_size, - "get window position and size using GetWindowRect" - ); - } - Err(e) => { - error!(?e, "failed to initialize mumble data because we coudln't get window position and size"); - } - } - // DwmGetWindowAttribute doesn't include drop shadow borders, but includes titlebar - match get_window_pos_size_without_borders(HWND(window_handle)) { - Ok(window_pos_size_without_borders) => { - info!(?window_pos_size_without_borders, "got window pos/size without borders using DwmGetWindowAttribute"); - } - Err(e) => { - error!( - ?e, - "failed to get window position size without borders" - ); - } - }; - } - // only useful in wine - match std::ffi::CString::new("__wine_x11_whole_window") { - Ok(atom_string) => { - let xid = - GetPropA(HWND(window_handle), PCSTR(atom_string.as_ptr() as _)); - // check if the xid is actually null - if xid.is_invalid() { - // will happen on windows. But this is harmless - info!(?xid, "xid is invalid. This is completely fine on windows. This is only for linux users"); - } else { - info!("found xid too <3. {xid:?}"); - self.xid = xid - .0 - .try_into() - .map_err(|e| { - error!( - ?e, - ?xid, - "failed to fit x11 window id into u32" - ); - }) - .unwrap_or_default(); - } - } - Err(e) => { - error!(?e, notify = 0u64, "impossible. But __wine_x11_whole_window apparently not a valid cstring."); - } - } - // again, just for logging purposes and verify against lutris settings of dpi - let dpi_awareness = match GetProcessDpiAwareness(process_handle) { - Ok(dpi) => dpi.0, - Err(e) => { - error!(?e, "failed to get dpi awareness"); - 0 - } - }; - let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; - if dpi != self.dpi { - info!(dpi, self.dpi, "dpi changed for gw2 window"); - } - info!( - ?client_pos, - ?client_size, - dpi_awareness, - dpi, - pid, - ?process_handle, - ?window_handle, - "reinitialization complete " - ); - self.process_handle = process_handle; - self.window_handle = window_handle; - self.dpi = dpi; - self.client_pos = client_pos; - self.client_size = client_size; - self.last_ui_tick_update = Instant::now(); - self.previous_pid = pid; - } - Err(e) => { - error!(?e, "failed to get client rect"); - } - } - } - Err(e) => { - error!(?e, pid, "failed to open process handle"); - } - } - } -} - -fn check_dpi_scaling_enabled(path: &std::path::Path) -> Result { - // from $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - // life is too short to parse an xml out of this file. just find the following strings - const DPI_SCALING_TRUE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="true"#; - const DPI_SCALING_FALSE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="false"#; - let contents = std::fs::read_to_string(path) - .into_diagnostic() - .wrap_err("failed to read gw2 file")?; - - if contents.contains(DPI_SCALING_FALSE) { - return Ok(0); - }; - if contents.contains(DPI_SCALING_TRUE) { - return Ok(1); - }; - error!(contents, "failed to read dpi scaling from gw2 config file"); - Ok(-1) -} -/// This function creates/opens the shared memory with the key as the name. -/// Then, it maps the shared memory into the address space of our process. -/// Finally, we are provided the Handle of shared memory and the pointer to the starting address of the mapped memory. -/// can fail if -/// 1. key is not a valid cstring -/// 2. creating shared memory fails -/// 3. mapping shared memory into our addres space fails and we get a null pointer instead -unsafe fn create_link_shared_mem(key: &str) -> Result<(HANDLE, *mut CMumbleLink)> { - info!("creating MumbleLink shared memory: {key}"); - // prepare the key as a cstr to pass to windows functions - let key_cstr = std::ffi::CString::new(key) - .into_diagnostic() - .wrap_err(miette::miette!("invalid mumble link name {key}"))?; - unsafe { - // create a Mumble Link shared memory file - // the file handle will need not be stored because when process exits, the handle will be dropped by windows - let file_handle = CreateFileMappingA( - INVALID_HANDLE_VALUE, - None, - PAGE_READWRITE, - 0, - C_MUMBLE_LINK_SIZE_FULL as u32 + 4096, // we add the size of description field here. - PCSTR(key_cstr.as_ptr() as _), - ) - .into_diagnostic() - .wrap_err("failed to create file mapping for MumbleLink")?; - // map the shared memory into the address space of our process using the handle we got from creating the shm - let cml_ptr = MapViewOfFile( - file_handle, - FILE_MAP_ALL_ACCESS, - 0, - 0, - C_MUMBLE_LINK_SIZE_FULL + 4096, // adding the description field size here - ) - .Value; - // check if we were successful - if cml_ptr.is_null() { - bail!( - "could not map view of file, error code: {:#?}", - GetLastError() - ) - } - Ok((file_handle, cml_ptr.cast())) - } -} - -unsafe fn check_process_alive(process_handle: HANDLE) -> Result { - let mut exit_code = 0u32; - GetExitCodeProcess(process_handle, &mut exit_code as *mut u32) - .into_diagnostic() - .wrap_err("failed to get exit code of process ")?; - Ok(exit_code == STATUS_PENDING.0 as u32) - - // this is slightly faster than using the GetExitCodeProcess method. - // GetExitCodeProcess takes around 3 us on average with lowest being 2.5 us. - // WaitForSingleObject takes around 2 us on average withe lowest being 1.5 us. - // let result = unsafe { WaitForSingleObject(process_handle, 0) }; - - // if result == WAIT_ABANDONED || result == WAIT_OBJECT_0 { - // Ok(false) - // } else if result == WAIT_TIMEOUT.0 { - // Ok(true) - // } else { - // bail!("WaitForSingleObject returned code: {:#?}", result) - // } -} -/// This function gets called by EnumWindows as a lambda function. it will be given a handle to all windows one by one, -/// and the pid of the process we want to match against that handle's pid. if handle's pid is matched against our pid, we will -/// assign the handle to our pid pointer so that the they can use it after EnumWindows returns -unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LPARAM) -> BOOL { - // gw2_pid is a long pointer TO a HWND. we cast gw2_pid from isize to a * mut isize. - let local_gw2_pid = *(gw2_pid_ptr.0 as *mut isize); - - // make a varible to hold the process id of a window handle given to us. - let mut window_handle_pid: u32 = 0; - // get the process id of the handle and then store it in the handle_pid variable. - GetWindowThreadProcessId(window_handle, Some((&mut window_handle_pid) as *mut u32)); - // if handle_pid is null, it means we failed to get the pid. so, we return true so that enumWindows can call us again with the handle to the next window. - if window_handle_pid == 0 { - info!("failed to get process id of window handle {window_handle:?}"); - return BOOL(1); - } - - info!("window handle {window_handle:?} has pid {window_handle_pid}"); - - // we check if the pid which gw2_pid references is equal to handle_pid - if local_gw2_pid == window_handle_pid as isize { - info!( - "successfully found the handle: {window_handle:?} of our gw2 with pid {local_gw2_pid}" - ); - // we now assign the window_handle to the memory pointed by gw2_pid pointer. - *(gw2_pid_ptr.0 as *mut isize) = window_handle.0; - return BOOL(0); - } - BOOL(1) -} -/// Quirk: GetWindowRect also includes the invisible "borders" which windows uses for resizing or whatever -/// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. -/// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). -/// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. -fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { - bail!("GetWindowRect call failed {e:#?}"); - } - let pos = [rect.left, rect.top]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = DwmGetWindowAttribute( - window_handle, - DWMWA_EXTENDED_FRAME_BOUNDS, - &mut rect as *mut RECT as _, - std::mem::size_of::() as _, - ) { - bail!("DwmGetWindowAttribute call failed {e:#?}"); - } - let pos = [rect.left, rect.top]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = GetClientRect(window_handle, &mut rect as *mut RECT) { - bail!("GetClientRect call failed {e:#?}"); - } - let mut point: POINT = POINT { - x: rect.left, - y: rect.top, - }; - if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { - bail!("ClientToScreen call failed"); - } - let pos = [point.x, point.y]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -impl Drop for MumbleWinImpl { - fn drop(&mut self) { - unsafe { - warn!("dropping mumble link windows impl"); - if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: self.link_ptr as _, - }) { - error!(?e, "failed to unmap view of mumble file"); - } - if let Err(e) = CloseHandle(self.mumble_handle) { - error!(?e, "failed to close handle of mumble link ") - } - if !self.process_handle.is_invalid() { - if let Err(e) = CloseHandle(self.process_handle) { - error!(?e, "failed to close handle of mumble link ") - } - } - } - } -} diff --git a/crates/joko_link_models/src/mumble/mod.rs b/crates/joko_link_models/src/mumble/mod.rs index 16a38d3..510dc33 100644 --- a/crates/joko_link_models/src/mumble/mod.rs +++ b/crates/joko_link_models/src/mumble/mod.rs @@ -160,7 +160,7 @@ pub enum UISize { #[bitflags] #[repr(u32)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] /// The Uistate enum to represent the status of the UI in game pub enum UIState { IsMapOpen = 0b00000001, diff --git a/crates/joko_package/Cargo.toml b/crates/joko_package/Cargo.toml deleted file mode 100644 index 564eb2f..0000000 --- a/crates/joko_package/Cargo.toml +++ /dev/null @@ -1,57 +0,0 @@ -[package] -name = "joko_package" -version = "0.2.1" -edition = "2021" - -[dependencies] -# jmf deps -# for marker packs -base64 = "0.21.2" -bincode = { workspace = true } -bytemuck = { workspace = true } -cap-std = { workspace = true } -cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings -data-encoding = "2.4.0" -egui = { workspace = true } -enumflags2 = { workspace = true } -glam = { workspace = true } -image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. -indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip -itertools = { workspace = true } -joko_core = { path = "../joko_core" } -joko_components = { path = "../joko_components" } -joko_render_models = { path = "../joko_render_models" } -joko_package_models = { path = "../joko_package_models" } -jokoapi = { path = "../jokoapi" } -joko_link = { path = "../joko_link" } -miette = { workspace = true } -once = "0.3.4" -ordered_hash_map = { workspace = true } -paste = { workspace = true } -phf = { version = "*", features = ["macros"] } -rayon = { workspace = true } -rfd = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -smol_str = { workspace = true } -time = { workspace = true , features = ["serde"]} -tokio = { workspace = true } -tracing = { workspace = true } -url = { workspace = true } -uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } -xot = { version = "0.16.0" } -zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) -walkdir = "2.5.0" - - - -[dev-dependencies] -# jmf deps -rstest = { version = "0", default-features = false } -# rstest_reuse = "0.3.0" -similar-asserts = "1" - - -[build-dependencies] -# for rapidxml -cxx-build = { version = "1" } diff --git a/crates/joko_package/README.md b/crates/joko_package/README.md deleted file mode 100644 index cbbf8d6..0000000 --- a/crates/joko_package/README.md +++ /dev/null @@ -1,87 +0,0 @@ - -## Status -still in early stages of development - - - - -### RapidXML Integration -Taco uses RapidXML, which is very very lenient in its parsing. -this led to marker packs not caring about their xml being valid xml. -Blish instead created a custom parsing library to deal with this and have workarounds for known issues. - -rapidxml does fix these issues itself when we roundtrip xml through it. so, we have a function called `rapid_filter` which takes in xml string and returns a "filtered" xml string that fixes a bunch of issues like escaping special characters like -ampersand, gt, lt etc.. with proper xml formatting i.e `&`, `>` etc.. - -Sources of rapidxml are in the vendor folder. it is a custom fork from https://github.com/timniederhausen/rapidxml which -added some fixes / enhancements. its stil a mess with compiler warnings, but whatever. - -we use cxxbridge crate. -`rapid.hpp` is our header with declaration for `rapid_filter` inside `rapid` namespace. (includes `joko_marker_format/src/lib.rs.h`) -`lib.rs` has extern declaration which has the same signature but in rust. (includes `joko_marker_format/vendor/rapid/rapid.hpp`) -`build.rs` has the compilation instructions. it uses `lib.rs` extern declaration, `rapid.cpp` as compilation unit as it - contains the definition of `rapid_filter` and finally outputs a `librapid.a` for linking. - -with this, we now filter the xml with `rapid_filter` before deserializing it in rust. if we still have errors we just -complain about it. - - - -### XML Marker Format -Marker Pack - -1. Textures - 1. identified by the relative path. case sensitive. But to accommodate case-insensitive MS windows packs, we will convert all paths to lowercase when importing. - 2. png format. - 3. need to convert to a srgba texture and upload to gpu to use it - 4. mostly tiny images. here's the composition of tekkit's pack textures - -| count | dimensions | -|-------|---------------| -| 630 | 100x100 | -| 7 | 150x150 | -| 89 | 200x200 | -| 683 | 250x250 | -| 42 | 256x256 | -| 435 | 500x500 | - -2. Tbins - 1. binary data of a series of vec3 positions. + mapid + a version (just ver 2 for now) - 2. need to generate a mesh to be usable to upload on gpu. different mesh for 2d map / minimap. trail_scale an affect width of the generated mesh - 3. anim_speed attr needs dynamic texture coords (probably based on time delta offset) - 4. color attribute requires blending. - 5. uses texture - 6. can be statically or dynamically filtered (culled). but no cooldowns. - -3. MarkerCategories - 1. create a tree structure of menu to be displayed. - 2. identified by their name (and parents in the hierarchy) as a unique path. - 3. can be enabled or disabled. need to persist this data in activation data or somewhere else. - 4. enabled / disabled categories act as dynamic filters for markers / trails. - 5. attributes get inherited by children unless overrided. and also inehrited by the markers / trails. - 6. can be enabled / disabled by a marker action (toggle_category attribute) -4. Markers - 1. render a quad. either billbaord or static rotation. - 2. needs texture + alpha attribute + color attribute for blending. - 3. alpha is also affected by fadenear and fadefar attributes. - 4. static filters like ingamevisibility or map visibility or minimap visibility. - 5. can display text via info / tip-description. - 6. dynamic filters like behavior + race + profession + specialization + mount + map type + category + festival + achievement. - 7. size is determined by texture + minSize / maxSize + scale. map quad rendering affected by scale on map and mapdisplaysize attribute - 8. triggers actions of behavior + copy-message (copy clipboard) + bounce?? + toggling category based on player proximity and pressing of a special action key (usually F) -5. Trails - 1. render the tbin mesh. - 2. same filters as marker - 3. no triggering / activation / cooldowns though. - - -3D: -1. can match blish -2. need to ignore certain attributes like minSize and maxSize. - - -2D: -1. can match taco -2. more performance because 2d? - - diff --git a/crates/joko_package/build.rs b/crates/joko_package/build.rs deleted file mode 100644 index 062e89b..0000000 --- a/crates/joko_package/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() { - cxx_build::bridge("src/lib.rs") // our extern declaration in rust for rapid_filter - .file("vendor/rapid/rapid.cpp") // our compilation unit containing definition - .warnings(false) - .extra_warnings(false) - .compile("rapid"); // name of library = librapid.a - - println!("cargo:rerun-if-changed=src/lib.rs"); - println!("cargo:rerun-if-changed=vendor/rapid/rapid.cpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapid.hpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapidxml.hpp"); - println!("cargo:rerun-if-changed=vendor/rapid/rapidxml_print.hpp"); - // shadow_rs::new().expect("failed to run shadow"); -} diff --git a/crates/joko_package/images/marker.png b/crates/joko_package/images/marker.png deleted file mode 100644 index 294a322e8475b221dbee3297868b719baa40f656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173015 zcmd3MRZ|>H(r6+1RAkYRiIAb7pwQ&yq|~9Hp#Oo;P>AsVI(?T4%l|M}by*3hnn{wQe+ZnF zxUx7D)UO1TR};WL9LY)Uiz^fqdf$HyddRWD913a;RbEP5)64MGAEAI$`tBk1TT8~m z%b4OtT4?^X;$bJfax@T2N>fVgJ(wObvgnpo?tDkpx^mLrnn9UnS(Tudl9{8_uYVL5_sBbkAk@AFs8=&JorjlDM6&YF1sXM@(R9 zHLd?gL=5C?4A3j7Y`r%M+lh_={%PxMeNOAqS~X~Mby9M6$J{Xa-(0BV0bxwrtJ}wc zXlW~R%4WJ_Eq;j?%nr|^oBTUysgx18ItW*!g9R{;=_Y5t?b#w1RqCBL6%_>tjqBh-is^Ny5=LW-dXOZ_XdBmKCn7Gx71DHmWCL_e8AfOK{h^y)_ULmzVR6``jIn?Pc|c#840(=pE9)SbCX}H z0#HXZq*5a(x~(;i|8TyB#-MXx;sXy+yq0pZt3LTzX!v5fEmoPi27UX3y4ePh=-%Ld zSnp^>cv!U%8-~a@{9@w#=b*?`Lb`3UpYh+u41S!#Onwno&)dM|eOrM*UxjB4B8DN|F0_Vb!lkBh;{x^ez-*kz!&3S5m#+KpLx%@a=iDtx5PrSZGB9K==ZC$UHG z?>Aum21QL$6RmytY0lyv*E0~gbHcmarSXEI^*kPyc)T0P8_2>PoeT?BtkZOL2yp)M zq4PH(p+DGtFzX8b+sEN_m8Zr8Lv5>F4&?Jss86P7%it01gR_4J0$-^2;T9JCcE>JL z6+bvX(dcljAKJP~w*pXLY`O{N=`tE{?IO~`ZD`%Zt%)(yq@aZ}4DsKNV>e;5nDz2p ziwKYc25da@;vAGNPtbg)d&SPW*XKpNd6tVhUnZNx#H}vXl3|^(SsIW8O|%|{CKswv zFQqG^Z7{Iwqg;pBmcy;v8<{%LlU-|p6(d3~{t|YV)zWMjt=@kCl)21yikhDH^dmJ2 zG{4*Vi@L5f)4pQ2K6Xr)4v$Y?@KfpVOexdEtfFd5y&FR88Gl1uMQ&bY== zbb+m@#F6|(mj*8`LtktUitLRBDi-N|j=SF#|U~y8WhV43yweF@i`p<+TyV z*i9o$W`v?Ur9|J>_0Ypqq8nG&`VJ=oR8qbdsFX z8c}0!nCjKuO2{*A&t7D4{ODEu`FA@5H$|nN^FoH{c#>ITQ*DzQ&B_nY@N+zmxq{(c zfq||35Lskmw7B^v3^Z+`KI%OmkvSVrT-6uUGL^h4OXuTX`l3%>#%BAD|1=nv`wW*J zW9aV_sN%C4Au={=I4BLwC;+Hl)#**cKEFX`&YhadA3$9PS{%$?8layIlsyxLa z)48Y3E)S~cSrZeQSBG&C@$uOgjq?ms5IAuqFO0`x>GGIr@BSwz1;i^t9L$ZTwU+7z^2St z4zyMnYTl{9Z_o{0N9DH~2oDKR0+~!3>@n1hnt>weX?0nLeT9 zWdM0UWI6m~{NRNymU<;+DOsNZ+d82?mIc$P_cV9=i!@Q9JJac|>ADdSUiO7WV?^u6lmOD?{$`7&sg@YS$^5Ob`5E zk~t&XSfAMO#O?m0R?*&9rQLyd$!d+;n*V8x^r$8+)G+kW@e z$-eL+Ufl#!P+4BEprH3va?;4aLXC6|ar-+C0apXF9I9{NiWWu-=eFD=sC<9N6)@!Z z7-G&sFp*@LuA&wB2!++ykY*uBXd*<;c;?zWlbtj=h@G&otFiu~ZJ4*wQtx;(y;jeW zBp+IMvk5Dr_*z0*O+|hgMfr~WEyQ8JBiZKeTTW784>s65<$F#DwQ%BBXhZT>d0;4H zKrGke>BoxXpt|TILFt?{a5MT8OlBm~!poHK-Q8nvtCID$!VK9wbeJ-R z?AWi$sW1?>_@3b>O!%#2)g@P*BqNFVEJ?9wG+(#F8+DZCRh{@N%0DmP2;sy3QD_xa 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%0D>~L^060CsHx=NV^<)QR_ycX_mn5bPDsp8-9 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�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 E19PVjH2?qr diff --git a/crates/joko_package/images/trail_black.png b/crates/joko_package/images/trail_black.png deleted file mode 100644 index d4326e68a6bdc79ef11a27f2deee592e3dbc4382..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2293 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#QHWK~#8N?VU%6 zWknQ*$DA?eoE_=%sJa1|knwU2G`c6$D4`bAp<*yx2@UBEAsK zlRt`uVrQ|kuqqLQ|A6>Sj2iS|ZWDV6s{%3Y^W-!5Wci~8e3)MZdBTZeDPa{V627O! zOw;}-z7o%iqXjua^{OceWS@V4xKNPrZ6IbVasa#nzLyWg&*BeZWiGaH3^@Dz$y>d- zaEsVQkW5;cYj5$8U>i4TFvkI__+D{@urk)U;tBDC7&VY{;d}9%z${{ksZOQm0H=ww z#J+;HyKAi|FpD-7?~89lRc2AC0l=ESrMOHSC~)&aiQT~Ff*rz#g2eDIVI>p;e!pPv zK5FU?#jRp@VI|ZF0+WaBSzD8*D=?NG75pEnP-#oR1>ziWm{?83cI-$5*=}wpJ{KQ} ze?+k=B>-FL41W^Ennfi907eXD+-t>=Vm*;sD+sEGpAPs@ z;2T&0#25Xb=+flr3a0CqT;sIIsbS=Vr`LY ziI(MnCR2O~3P|_`#rOm{1^{dRd~vB@@I#tBU5UhSBf(6CS%g2~_OdZVw$FgG=JREa znsRf8D&lT}-^H4x$nqZ+JUzLt`7^)Kw`L-z$~JyoMldsIsmBZ;p$y2uy<2Bd|iuD@($uN z@v$JMu~{^vtp!y**3D7tX$_Fe+$k1_?38i_pqYbu{;7h!bgiw}{9d~X@&bAZ-WCK# z7D^QWv~gkd(9A($rPjf3gtvj2#f(Psx0B~sI27$lN)6i$J^s!6$U62zbQ>*~s zxE3dNjuA9c^j+A*6pt{A=rrbddXkwSMgUB8KELnE;(mgV0YK)vvA9CeFfqNZX30*)(|VAVdIMCD`V8&7Zy49yj+Ba8U)bK~RNw zI$slb-`3p3Hjz?tP`PcX=W+n_Tn>Pq%K?y|u!K2k7R4HN0OTj^;8zBp!4)C^e6?CZ z3K0NLiPtCoXpJErAP&ax8C)TZAZpyWT7V5q)yr<+%=$#UDxMYGdPZF!cpeaHV7Rvr zH;_Xf)VxF09HtW;Nc85@f6(Ulq!@$qmTLKWS#TvCByAGuLVV%C62su9U<<)i+V-~a z+Xa1!qyDZK@LhSP>`2`B%~kQUxmSSGJ6r&DoaoI!5P7xf6hYtNp@PCn+rgwcgQHwr z3du+Bs5p*iPI>oz+D=L8uDlZOUMwt6^d!_#-yFz7c6`t@)( zt;AyRN%&&k7EW%_TmWSy6w^LWK7-#?!q*n+^jrW{%{zxn%K=FESo66!nsqt0W5@JQ z>o&)nGr|~qaWw$n+Q@&9`^Z^NQbB8lw2<` zi>iE~R4&>{knnYB@^l4H{#^p=xk%-{1W?$)+vlud+q!;Cu{3eImkXfiCtyh+Fp5#m z0f4hd!pGA^fhM=XZ`520$sLEytL2KRBIXzXtfw3;zg$q#9jB@nTUgNS47eJWAfSrq zOCiS;88&Nvskeo52^M)v5z1`_I9yyKnv0}zo%W~GeuAwc)x>xBLC(r408STN9(j}~ z`SM7*p~+b|-Ah*ySHG}($x^ul0Ok(2hw@2AUAmY7a4NYc$!5_sA^=RAPd2+>^0odY^5p#!N&1-%eac~x+i%TGqnnke!0F#G(KIiUydE`XB zgQgaiL-GOY2T9DL7y*F2i^InZz#3>xhyoIhC%FN$0OjQ9W>LrhAj)yW@bizC~KvM?`e{ P00000NkvXXu0mjf7Ck~> diff --git a/crates/joko_package/images/trail_rainbow.png b/crates/joko_package/images/trail_rainbow.png deleted file mode 100644 index ea3ff6d305a60bb6a05c6418b6417c832ac14c16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16987 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 diff --git a/crates/joko_package/src/io/deserialize.rs b/crates/joko_package/src/io/deserialize.rs deleted file mode 100644 index ba0401e..0000000 --- a/crates/joko_package/src/io/deserialize.rs +++ /dev/null @@ -1,1610 +0,0 @@ -use crate::BASE64_ENGINE; -use base64::Engine; -use cap_std::fs_utf8::{Dir, DirEntry}; -use joko_core::{serde_glam::Vec3, RelativePath}; -use joko_package_models::{ - attributes::{CommonAttributes, XotAttributeNameIDs}, - category::{prefix_parent, Category, RawCategory}, - marker::Marker, - package::{PackCore, PackageImportReport}, - route::Route, - trail::{TBin, TBinStatus, Trail}, -}; -use ordered_hash_map::OrderedHashMap; -use std::{collections::VecDeque, io::Read, str::FromStr}; -use tracing::{debug, error, info, info_span, instrument, trace, warn}; -use uuid::Uuid; -use xot::{Element, Node, Xot}; - -const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; - -pub(crate) fn load_pack_core_from_normalized_folder( - core_dir: &Dir, - import_report: Option, -) -> Result { - //called from already parsed data - let mut core_pack = PackCore::new(); - if let Some(mut import_report) = import_report { - import_report.reset_counters(); - import_report.uuid = core_pack.uuid; - core_pack.report = import_report; - } - // walks the directory and loads all files into the hashmap - let start = std::time::SystemTime::now(); - recursive_walk_dir_and_read_images_and_tbins( - core_dir, - &mut core_pack, - &RelativePath::default(), - ) - .or(Err("failed to walk dir when loading a markerpack"))?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of core package textures from disk took {} ms", - elaspsed.as_millis() - ); - - //categories are required to register other objects - let cats_xml = core_dir - .read_to_string("categories.xml") - .or(Err("failed to read categories.xml"))?; - let categories_file = String::from("categories.xml"); - let parse_categories_file_start = std::time::SystemTime::now(); - parse_categories_from_normalized_file(&categories_file, &cats_xml, &mut core_pack) - .or(Err("failed to parse category file"))?; - let elapsed = parse_categories_file_start.elapsed().unwrap_or_default(); - info!("parse_categories_file took {} ms", elapsed.as_millis()); - - // parse map data of the pack - for entry in core_dir - .entries() - .or(Err("failed to read entries of pack dir"))? - { - let dir_entry = entry.or(Err("entry error whiel reading xml files"))?; - - let name = dir_entry - .file_name() - .or(Err("map data entry name not utf-8"))? - .to_string(); - - if name.ends_with(".xml") { - if let Some(name_as_str) = name.strip_suffix(".xml") { - match name_as_str { - "categories" => { - //already done - } - file_name => { - // parse map file - let span_guard = info_span!("load file", file_name).entered(); - //let mut partial_pack = PackCore::partial(&core_pack.all_categories); - load_xml_from_normalized_file(file_name, &dir_entry, &mut core_pack)?; - //core_pack.merge_partial(partial_pack); - std::mem::drop(span_guard); - } - } - } - } else { - trace!("file ignored: {name}") - } - } - info!( - "Entities registered (category + markers): {}", - core_pack.entities_parents.len() - ); - info!("Categories registered: {}", core_pack.all_categories.len()); - info!( - "Markers registered: {}", - core_pack.entities_parents.len() - core_pack.all_categories.len() - ); - info!("Maps registered: {}", core_pack.maps.len()); - info!("Textures registered: {}", core_pack.textures.len()); - info!("Trail binaries registered: {}", core_pack.tbins.len()); - Ok(core_pack) -} - -fn recursive_walk_dir_and_read_images_and_tbins( - dir: &Dir, - pack: &mut PackCore, - parent_path: &RelativePath, -) -> Result<(), String> { - for entry in dir.entries().or(Err("failed to get directory entries"))? { - let entry = entry.or(Err("dir entry error when iterating dir entries"))?; - let name = entry.file_name().or(Err("No file name found"))?; - let path = parent_path.join_str(&name); - - if entry - .file_type() - .or(Err("failed to get file type"))? - .is_file() - { - if path.ends_with(".png") || path.ends_with(".trl") { - let mut bytes = vec![]; - entry - .open() - .or(Err("failed to open file"))? - .read_to_end(&mut bytes) - .or(Err("failed to read file contents"))?; - if name.ends_with(".png") { - pack.register_texture(name, &path, bytes); - } else if name.ends_with(".trl") { - if let Some(tbs) = parse_tbin_from_slice(&bytes) { - /*let is_closed: bool = tbs.closed; - if is_closed { - if tbs.iso_x {} - if tbs.iso_y {} - if tbs.iso_z {} - }*/ - pack.tbins.insert(path, tbs.tbin); - } else { - info!("invalid tbin: {path}"); - } - } - } - } else { - recursive_walk_dir_and_read_images_and_tbins( - &entry.open_dir().or(Err("Could not open directory"))?, - pack, - &path, - )?; - } - } - Ok(()) -} -fn parse_tbin_from_slice(bytes: &[u8]) -> Option { - let content_length = bytes.len(); - // content_length must be atleast 8 to contain version + map_id - if content_length < 8 { - info!("failed to parse tbin because the len is less than 8"); - return None; - } - - let mut version_bytes = [0_u8; 4]; - version_bytes.copy_from_slice(&bytes[4..8]); - let version = u32::from_ne_bytes(version_bytes); - let mut map_id_bytes = [0_u8; 4]; - map_id_bytes.copy_from_slice(&bytes[4..8]); - let map_id = u32::from_ne_bytes(map_id_bytes); - - let zero = glam::Vec3 { - x: 0.0, - y: 0.0, - z: 0.0, - }; - - // this will either be empty vec or series of vec3s. - let nodes: VecDeque = bytes[8..] - .chunks_exact(12) - .map(|float_bytes| { - // make [f32 ;3] out of those 12 bytes - let arr = [ - f32::from_le_bytes([ - // first float - float_bytes[0], - float_bytes[1], - float_bytes[2], - float_bytes[3], - ]), - f32::from_le_bytes([ - // second float - float_bytes[4], - float_bytes[5], - float_bytes[6], - float_bytes[7], - ]), - f32::from_le_bytes([ - // third float - float_bytes[8], - float_bytes[9], - float_bytes[10], - float_bytes[11], - ]), - ]; - - glam::Vec3::from_array(arr) - }) - .collect(); - - //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. - let mut iso_x = false; - let mut iso_y = false; - let mut iso_z = false; - let mut closed = false; - let mut resulting_nodes: Vec = Vec::new(); - if !nodes.is_empty() { - //at least the first exist and can be accessed - let ref_node = nodes[0]; - let mut c_iso_x = true; - let mut c_iso_y = true; - let mut c_iso_z = true; - // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts - resulting_nodes.push(Vec3(ref_node)); - for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { - //ignore zeroes since they would be separators - if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { - let distance_to_next_point = a.distance_squared(*b); - let mut current_cursor = distance_to_next_point; - while current_cursor > MAX_TRAIL_CHUNK_LENGTH { - let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); - resulting_nodes.push(Vec3(c)); - current_cursor -= MAX_TRAIL_CHUNK_LENGTH; - } - } - resulting_nodes.push(Vec3(*b)); - } - for node in &nodes { - if resulting_nodes.len() > 1 { - //TODO: load epsilon from a configuration somewhere, with a default value - if (node.x - ref_node.x).abs() < 0.1 { - c_iso_x = false; - } - if (node.y - ref_node.y).abs() < 0.1 { - c_iso_y = false; - } - if (node.z - ref_node.z).abs() < 0.1 { - c_iso_z = false; - } - } - } - iso_x = c_iso_x; - iso_y = c_iso_y; - iso_z = c_iso_z; - if nodes.len() > 1 { - // TODO: get this threshold from configuration - closed = nodes - .front() - .unwrap() - .distance(*nodes.back().unwrap()) - .abs() - < 0.1 - } - } - Some(TBinStatus { - tbin: TBin { - map_id, - version, - nodes: resulting_nodes, - }, - iso_x, - iso_y, - iso_z, - closed, - }) -} - -fn parse_categories( - pack: &mut PackCore, - tree: &Xot, - tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, - names: &XotAttributeNameIDs, - source_file_uuid: &Uuid, -) { - //called once per file - parse_categories_recursive( - pack, - tree, - tags, - first_pass_categories, - names, - None, - source_file_uuid, - ) -} - -// a recursive function to parse the marker category tree. -fn parse_categories_recursive( - pack: &mut PackCore, - tree: &Xot, - tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, - names: &XotAttributeNameIDs, - parent_name: Option, - source_file_uuid: &Uuid, -) { - for tag in tags { - let ele = match tree.element(tag) { - Some(ele) => ele, - None => continue, - }; - if ele.name() != names.marker_category { - continue; - } - - let name = ele - .get_attribute(names.name) - .or(ele.get_attribute(names.capital_name)) - .unwrap_or_default() - .to_lowercase(); - if name.is_empty() { - continue; - } - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(ele, names); - let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); - - let separator = ele - .get_attribute(names.separator) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or_default(); - - let default_enabled = ele - .get_attribute(names.default_enabled) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or(true); - let full_category_name: String = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, name) - } else { - name.to_string() - }; - let guid = parse_guid(names, ele); - trace!( - "recursive_marker_category_parser {} {} {:?}", - name, - guid, - parent_name - ); - if !first_pass_categories.contains_key(&full_category_name) { - let mut sources: OrderedHashMap = OrderedHashMap::new(); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_inherited_texture( - icon_file.as_str().to_string(), - full_category_name.clone(), - source_file_uuid, - ); - } - } - - sources.insert(guid, *source_file_uuid); - first_pass_categories.insert( - full_category_name.clone(), - RawCategory { - guid, - parent_name: parent_name.clone(), - display_name: display_name.to_string(), - relative_category_name: name.to_string(), - full_category_name: full_category_name.clone(), - separator, - default_enabled, - props: common_attributes, - sources, - }, - ); - } - parse_categories_recursive( - pack, - tree, - tree.children(tag), - first_pass_categories, - names, - Some(full_category_name), - source_file_uuid, - ); - } -} - -fn parse_categories_from_normalized_file( - file_name: &String, - cats_xml_str: &str, - pack: &mut PackCore, -) -> Result<(), String> { - let mut tree = xot::Xot::new(); - let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); - let root_node = tree.parse(cats_xml_str).or(Err("invalid xml"))?; - - let overlay_data_node = tree.document_element(root_node).or(Err("no doc element"))?; - - if let Some(od) = tree.element(overlay_data_node) { - let mut categories: OrderedHashMap = Default::default(); - if od.name() == xot_names.overlay_data { - parse_category_categories_xml_recursive( - file_name, - &tree, - tree.children(overlay_data_node), - &mut categories, - &xot_names, - None, - None, - )?; - trace!("loaded categories: {:?}", categories); - pack.categories = categories; - pack.register_categories(); - } else { - return Err("root tag is not OverlayData".to_string()); - } - } else { - return Err("doc element is not element???".to_string()); - } - Ok(()) -} - -fn load_xml_from_normalized_file( - file_name: &str, - dir_entry: &DirEntry, - target: &mut PackCore, -) -> Result<(), String> { - let mut xml_str = String::new(); - dir_entry - .open() - .or(Err("failed to open xml file"))? - .read_to_string(&mut xml_str) - .or(Err("failed to read xml string"))?; - //TODO: launch an async load of the file + make a priority queue to have current map first - parse_map_xml_string(file_name, &xml_str, target) - .or(Err(format!("error parsing file: {file_name}"))) -} - -fn parse_map_xml_string( - file_name: &str, - map_xml_str: &str, - target: &mut PackCore, -) -> Result<(), String> { - let mut tree = Xot::new(); - let root_node = tree.parse(map_xml_str).or(Err("invalid xml"))?; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let overlay_data_node = tree - .document_element(root_node) - .or(Err("missing doc element"))?; - - let overlay_data_element = tree.element(overlay_data_node).ok_or("no doc ele")?; - - if overlay_data_element.name() != names.overlay_data { - return Err("root tag is not OverlayData".to_string()); - } - let pois = tree - .children(overlay_data_node) - .find(|node| match tree.element(*node) { - Some(ele) => ele.name() == names.pois, - None => false, - }) - .ok_or("missing pois node")?; - - for poi_node in tree.children(pois) { - if let Some(child_element) = tree.element(poi_node) { - let full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - - let span_guard = info_span!("category", full_category_name).entered(); - - let opt_source_file_uuid = Uuid::from_str( - child_element - .get_attribute(names._source_file_name) - .unwrap_or_default(), - ); - let source_file_uuid = if let Ok(uuid) = opt_source_file_uuid { - uuid - } else { - error!("Package corrupted, invalid source file uuid"); - //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); - Uuid::new_v4() - }; - - if let Some(source_file_name) = - target.report.source_file_uuid_to_name(&source_file_uuid) - { - let source_file_name = source_file_name.clone(); // this is to bypass borrow checker which has no idea this cannot be changed - target.register_source_file(&source_file_name); - } else { - println!("{:?}", source_file_uuid); - } - - //There is no file name, only an uuid to register - target.active_source_files.insert(source_file_uuid, true); - - if child_element.name() == names.route { - debug!("Found a route in core pack {:?}", child_element); - let route = parse_route( - &names, - &tree, - &poi_node, - child_element, - &full_category_name, - source_file_uuid, - ); - if let Some(route) = route { - target.register_route(route)?; - } else { - info!("Could not parse route {:?}", child_element); - } - } else { - if full_category_name.is_empty() { - panic!( - "full_category_name is empty {:?} {:?}", - map_xml_str, child_element - ); - } - let raw_uid = child_element.get_attribute(names.guid); - if raw_uid.is_none() { - info!( - "This POI is either invalid or inside a Route {:?}", - child_element - ); - span_guard.exit(); - continue; - } - //FIXME: this needs to be changed for partial load - let opt_cat_uuid = target.get_category_uuid(&full_category_name); - if opt_cat_uuid.is_none() { - error!( - "Mandatory category missing, packge is corrupted {:?} {:?}", - file_name, child_element - ); - return Err(format!( - "Mandatory category missing, packge is corrupted {:?} {:?}", - map_xml_str, child_element - )); - } - let category_uuid = opt_cat_uuid.unwrap(); //categories MUST exist, they have already been parsed - let guid = raw_uid - .and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - }) - .ok_or(format!("invalid guid {:?}", raw_uid))?; - - if child_element.name() == names.poi { - debug!("Found a POI in core pack {:?}", child_element); - let map_id = child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or("invalid mapid")?; - - let xpos = child_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .or(Err("invalid x position"))?; - let ypos = child_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .or(Err("invalid y position"))?; - let zpos = child_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .or(Err("invalid z position"))?; - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - let marker = Marker { - position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), - map_id, - category: full_category_name.clone(), - parent: *category_uuid, - attrs: ca, - guid, - source_file_uuid, - }; - target.register_marker(full_category_name, marker)?; - } else if child_element.name() == names.trail { - debug!("Found a trail in core pack {:?}", child_element); - let map_id = child_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or("invalid mapid")?; - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child_element, &names); - - let trail = Trail { - category: full_category_name.clone(), - parent: *category_uuid, - map_id, - props: ca, - guid, - dynamic: false, - source_file_uuid, - }; - target.register_trail(full_category_name, trail)?; - } - } - span_guard.exit(); - } - } - Ok(()) -} - -// a temporary recursive function to parse the marker category tree. -fn parse_category_categories_xml_recursive( - _file_name: &String, //meant for future implementation of source file definition for categories - tree: &Xot, - tags: impl Iterator, - cats: &mut OrderedHashMap, - names: &XotAttributeNameIDs, - parent_uuid: Option, - parent_name: Option, -) -> Result<(), String> { - for tag in tags { - if let Some(ele) = tree.element(tag) { - if ele.name() != names.marker_category { - continue; - } - - let relative_category_name = ele - .get_attribute(names.name) - .or(ele - .get_attribute(names.display_name) - .or(ele.get_attribute(names.capital_name))) - .unwrap_or_default() - .to_lowercase(); - if relative_category_name.is_empty() { - info!("category doesn't have a name attribute: {ele:#?}"); - continue; - } - let span_guard = info_span!("category", relative_category_name).entered(); - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); - - let separator = match ele.get_attribute(names.separator).unwrap_or("0") { - "0" => false, - "1" => true, - ors => { - info!("separator attribute has invalid value: {ors}"); - false - } - }; - - let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { - "0" => false, - "1" => true, - ors => { - info!("default_enabled attribute has invalid value: {ors}"); - true - } - }; - let full_category_name: String = if let Some(parent_name) = &parent_name { - format!("{}.{}", parent_name, relative_category_name) - } else { - relative_category_name.to_string() - }; - let guid = parse_guid(names, ele); - trace!( - "recursive_marker_category_parser_categories_xml {} {} {:?}", - full_category_name, - guid, - parent_uuid - ); - if display_name.is_empty() { - if parent_name.is_some() { - return Err( - "Package is corrupted, please import it again with current version" - .to_string(), - ); - } - parse_category_categories_xml_recursive( - _file_name, - tree, - tree.children(tag), - cats, - names, - Some(guid), - Some(full_category_name), - )?; - } else { - let current_category = if let Some(c) = cats.get_mut(&guid) { - c - } else { - let c = Category { - guid, - parent: parent_uuid, - display_name: display_name.to_string(), - relative_category_name: relative_category_name.to_string(), - full_category_name: full_category_name.clone(), - separator, - default_enabled, - props: ca, - children: Default::default(), - }; - cats.insert(guid, c); - cats.back_mut().unwrap() - }; - parse_category_categories_xml_recursive( - _file_name, - tree, - tree.children(tag), - &mut current_category.children, - names, - Some(guid), - Some(full_category_name), - )?; - }; - - std::mem::drop(span_guard); - } else { - //it may be a comment, a space, anything - //info!("In file {}, ignore node {:?}", file_name, tag); - } - } - Ok(()) -} - -pub(crate) fn get_pack_from_taco_zip( - input_path: std::path::PathBuf, - extract_temporary_path: &std::path::PathBuf, -) -> Result { - let mut taco_zip = vec![]; - std::fs::File::open(input_path) - .or(Err("Could not open target folder"))? - .read_to_end(&mut taco_zip) - .or(Err("Could not read target folder"))?; - - let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) - .or(Err("failed to read zip archive"))?; - if extract_temporary_path.exists() { - std::fs::remove_dir_all(extract_temporary_path).or(Err("Could not purge target folder"))?; - } - zip_archive - .extract(extract_temporary_path) - .or(Err("Could not extract archive into target folder"))?; - - _get_pack_from_taco_folder(extract_temporary_path) -} - -/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. -/// will return error if there's an issue with zipfile. -/// -/// but any other errors like invalid attributes or missing markers etc.. will just be logged. -/// the intention is "best effort" parsing and not "validating" xml marker packs. -/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. - -#[instrument(skip_all)] -fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { - let mut pack = PackCore::new(); - - // file paths of different file types - let mut images = vec![]; - let mut tbins = vec![]; - let mut xmls = vec![]; - // we collect the names first, because reading a file from zip is a mutating operation. - // So, we can't iterate AND read the file at the same time - for entry in walkdir::WalkDir::new(package_path).into_iter() { - let entry = entry.or(Err("Could not walk directory"))?; - let path_as_string = entry - .path() - .strip_prefix(package_path) - .unwrap() - .to_str() - .unwrap() - .to_string(); - if path_as_string.ends_with(".png") { - images.push(path_as_string); - } else if path_as_string.ends_with(".trl") { - tbins.push(path_as_string); - } else if path_as_string.ends_with(".xml") { - xmls.push(path_as_string); - } else if path_as_string.replace('\\', "/").ends_with('/') { - // directory. so, we can silently ignore this. - } else { - //info!("ignoring file: {name}"); - } - } - xmls.sort(); //build back the intended order in folder, since zip_archive may not give the files in order. - let start_texture_loading = std::time::SystemTime::now(); - for file_path in images { - let span = info_span!("load image", file_path).entered(); - let relative_file_path: RelativePath = file_path.parse().unwrap(); - if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { - match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { - Ok(_) => { - pack.register_texture(file_path, &relative_file_path, bytes); - } - Err(e) => { - info!(?e, "failed to parse image file"); - } - } - } - std::mem::drop(span); - } - - for file_path in tbins { - let span = info_span!("load tbin", file_path).entered(); - let relative_path: RelativePath = file_path.parse().unwrap(); - if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { - if let Some(tbs) = parse_tbin_from_slice(&bytes) { - /*let is_closed: bool = tbs.closed; - if is_closed { - if tbs.iso_x {} - if tbs.iso_y {} - if tbs.iso_z {} - }*/ - assert!( - pack.tbins.insert(relative_path, tbs.tbin).is_none(), - "duplicate tbin file {file_path}" - ); - } else { - info!("failed to parse tbin from slice: {relative_path}"); - } - } else { - info!(file_path, "failed to read tbin from zipfile"); - } - std::mem::drop(span); - } - let elapsed_texture_loading = start_texture_loading.elapsed().unwrap_or_default(); - pack.report.telemetry.texture_loading = elapsed_texture_loading.as_millis(); - tracing::info!( - "Loading of taco package textures from disk took {} ms", - elapsed_texture_loading.as_millis() - ); - - let span_guard_categories = info_span!("deserialize xml: categories").entered(); - let start_categories_loading = std::time::SystemTime::now(); - //first pass: categories only - let span_guard_first_pass = - info_span!("deserialize xml first pass: load MarkerCategory").entered(); - let mut first_pass_categories: OrderedHashMap = Default::default(); - for source_file_name in xmls.iter() { - let source_file_name = source_file_name.to_string(); - let span_guard = - info_span!("deserialize xml first pass: load file", source_file_name).entered(); - let r = std::fs::read_to_string(package_path.join(&source_file_name)); - let xml_str = if r.is_ok() { - r.unwrap() - } else { - info!("failed to read file from zip"); - continue; - }; - let source_file_uuid = pack.register_source_file(&source_file_name); - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - - parse_categories( - &mut pack, - &tree, - tree.children(od), - &mut first_pass_categories, - &names, - &source_file_uuid, - ); - drop(span_guard); - } - span_guard_first_pass.exit(); - let elaspsed_first_pass = start_categories_loading.elapsed().unwrap_or_default(); - pack.report.telemetry.categories_first_pass = elaspsed_first_pass.as_millis(); - - //second pass: orphan categories - let span_guard_second_pass = - info_span!("deserialize xml second pass: orphan categories").entered(); - let start_categories_loading_second_pass = std::time::SystemTime::now(); - for source_file_name in xmls.iter() { - let source_file_name = source_file_name.to_string(); - let span_guard = - info_span!("deserialize xml second pass: load file", source_file_name).entered(); - let r = std::fs::read_to_string(package_path.join(&source_file_name)); - let xml_str = if r.is_ok() { - r.unwrap() - } else { - info!("failed to read file from zip"); - continue; - }; - let source_file_uuid = pack.register_source_file(&source_file_name); - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - debug!("missing overlay data tag"); - continue; - } - }; - let pois = match tree.children(od).find(|node| { - tree.element(*node) - .map(|ele: &xot::Element| ele.name() == names.pois) - .unwrap_or_default() - }) { - Some(pois) => pois, - None => { - debug!("missing pois tag"); - continue; - } - }; - - for child_node in tree.children(pois) { - let child_element = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - let mut full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - if full_category_name.is_empty() { - if child_element.name() == names.route { - // If route, take the first element inside - if let Some(category) = - parse_route_category(&names, &tree, &child_node, child_element) - { - if category.is_empty() { - continue; - } - full_category_name = category; - } else { - continue; - } - } else { - continue; - } - } - let guid = parse_guid(&names, child_element); - if !pack.category_exists(&full_category_name) - && !first_pass_categories.contains_key(&full_category_name) - { - let category_uuid = Uuid::new_v4(); - let mut sources: OrderedHashMap = OrderedHashMap::new(); - sources.insert(guid, source_file_uuid); - first_pass_categories.insert( - full_category_name.clone(), - RawCategory { - default_enabled: true, - guid: category_uuid, - parent_name: prefix_parent(&full_category_name, '.'), - display_name: full_category_name.clone(), - full_category_name: full_category_name.clone(), - relative_category_name: full_category_name.clone(), - props: Default::default(), - separator: false, - sources, - }, - ); - debug!( - "There is an orphan missing category '{}' which was created", - full_category_name - ); - } else { - let cat = first_pass_categories.get_mut(&full_category_name); - cat.unwrap().sources.insert(guid, source_file_uuid); - } - } - drop(span_guard); - } - span_guard_second_pass.exit(); - - let elaspsed_second_pass = start_categories_loading_second_pass - .elapsed() - .unwrap_or_default(); - pack.report.telemetry.categories_second_pass = elaspsed_second_pass.as_millis(); - - let start_categories_reassemble = std::time::SystemTime::now(); - pack.categories = Category::reassemble(&first_pass_categories, &mut pack.report); - let elaspsed_reassemble = start_categories_reassemble.elapsed().unwrap_or_default(); - pack.report.telemetry.categories_reassemble.total = elaspsed_reassemble.as_millis(); - - let start_categories_registering = std::time::SystemTime::now(); - pack.register_categories(); - let elaspsed_categories_registering = - start_categories_registering.elapsed().unwrap_or_default(); - pack.report.telemetry.categories_registering = elaspsed_categories_registering.as_millis(); - - let elaspsed = start_categories_loading.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of taco package categories from disk took {} ms, {} + {} + {}", - elaspsed.as_millis(), - elaspsed_first_pass.as_millis(), - elaspsed_second_pass.as_millis(), - elaspsed_reassemble.as_millis(), - ); - - //third and last pass: elements - let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); - let start_elements_registering = std::time::SystemTime::now(); - for source_file_name in xmls.iter() { - let source_file_name = source_file_name.to_string(); - let span_guard = - info_span!("deserialize xml third pass load file ", source_file_name).entered(); - let r = std::fs::read_to_string(package_path.join(&source_file_name)); - let xml_str = if r.is_ok() { - r.unwrap() - } else { - info!("failed to read file from zip"); - continue; - }; - let source_file_uuid = pack.register_source_file(&source_file_name); - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - - let pois = match tree.children(od).find(|node| { - tree.element(*node) - .map(|ele: &xot::Element| ele.name() == names.pois) - .unwrap_or_default() - }) { - Some(pois) => pois, - None => { - debug!("missing POIs tag"); - continue; - } - }; - - for child_node in tree.children(pois) { - let child_element = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - let full_category_name = child_element - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - - debug!("import element: {:?}", child_element); - if child_element.name() == names.route { - let route = parse_route( - &names, - &tree, - &child_node, - child_element, - &full_category_name, - source_file_uuid, - ); - if let Some(mut route) = route { - //one must not create category anymore - route.parent = *pack.get_category_uuid(&route.category).unwrap(); - pack.register_route(route)?; - } else { - info!("Could not parse route {:?}", child_element); - } - } else { - if full_category_name.is_empty() { - info!("full_category_name is empty {:?}", child_element); - continue; - } - if !pack.category_exists(&full_category_name) { - panic!( - "Missing category {}, previous pass should have taken care of this", - full_category_name - ); - } - let guid = parse_guid(&names, child_element); - let category_uuid = - pack.get_or_create_category_uuid(&full_category_name, guid, &source_file_uuid); - if child_element.name() == names.poi { - if let Some(marker) = parse_marker( - &mut pack, - &names, - child_element, - guid, - &full_category_name, - &category_uuid, - source_file_uuid, - ) { - pack.register_marker(full_category_name, marker)?; - } else { - debug!("Could not parse POI"); - } - } else if child_element.name() == names.trail { - if let Some(trail) = parse_trail( - &mut pack, - &names, - child_element, - guid, - &full_category_name, - &category_uuid, - source_file_uuid, - ) { - pack.register_trail(full_category_name, trail)?; - } else { - debug!("Could not parse Trail"); - } - } else { - info!("unknown element: {:?}", child_element); - } - } - } - - drop(span_guard); - } - span_guard_third_pass.exit(); - span_guard_categories.exit(); - let elaspsed_elements_registering = start_elements_registering.elapsed().unwrap_or_default(); - pack.report.telemetry.elements_registering = elaspsed_elements_registering.as_millis(); - - let elapsed_import = start_texture_loading.elapsed().unwrap_or_default(); - pack.report.telemetry.total = elapsed_import.as_millis(); - Ok(pack) -} - -fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { - child.get_attribute(names.guid).and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - .or_else(|| { - info!(guid, "failed to deserialize guid"); - None - }) - }) -} -fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid { - parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) -} - -fn parse_marker( - pack: &mut PackCore, - names: &XotAttributeNameIDs, - poi_element: &Element, - guid: Uuid, - category_name: &str, - category_uuid: &Uuid, - source_file_uuid: Uuid, -) -> Option { - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(poi_element, names); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - debug!(%icon_file, "failed to find this texture in this pack"); - pack.found_missing_element_texture( - icon_file.as_str().to_string(), - guid, - &source_file_uuid, - ); - } - } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { - debug!(icf, "marker's icon file attribute failed to parse"); - pack.found_missing_element_texture(icf.to_string(), guid, &source_file_uuid); - } - - if let Some(map_id) = poi_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - { - let xpos = poi_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let ypos = poi_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let zpos = poi_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - Some(Marker { - position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), - map_id, - category: category_name.to_owned(), - parent: *category_uuid, - attrs: common_attributes, - guid, - source_file_uuid, - }) - } else { - debug!("missing map id"); - None - } -} - -fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { - let x = poi_element - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let y = poi_element - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let z = poi_element - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - Vec3(glam::Vec3 { x, y, z }) -} - -fn parse_route_category( - names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, -) -> Option { - for child_node in tree.children(*route_node) { - let child = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - if child.name() == names.poi { - if let Some(cat) = child.get_attribute(names.category) { - return Some(cat.to_string()); - } - } - } - info!("Could not find a category for route element: {route_element:?}"); - None -} - -fn parse_route( - names: &XotAttributeNameIDs, - tree: &Xot, - route_node: &Node, - route_element: &Element, - category_name: &str, - source_file_uuid: Uuid, -) -> Option { - let mut path: Vec = Vec::new(); - let resetposx = route_element - .get_attribute(names.resetposx) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let resetposy = route_element - .get_attribute(names.resetposy) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let resetposz = route_element - .get_attribute(names.resetposz) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let reset_position = glam::Vec3::new(resetposx, resetposy, resetposz); - let reset_range = route_element - .get_attribute(names.reset_range) - .and_then(|map_id| map_id.parse::().ok()); - let name = route_element - .get_attribute(names.name) - .or(route_element.get_attribute(names.capital_name)); - - if name.is_none() { - info!("route element is missing name: {route_element:?}"); - return None; - } - let mut category: String = category_name.to_owned(); - let mut category_uuid: Option = parse_optional_guid(names, route_element); - let mut map_id: Option = route_element - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()); - for child_node in tree.children(*route_node) { - let child = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - if child.name() == names.poi { - let marker = parse_position(names, child); - path.push(marker); - if category.is_empty() { - if let Some(cat) = child.get_attribute(names.category) { - category = cat.to_string(); - } - } - if category_uuid.is_none() { - category_uuid = parse_optional_guid(names, child) - } - if map_id.is_none() { - if let Some(node_map_id) = child - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - { - map_id = Some(node_map_id); - } - } - } - } - if category.is_empty() { - info!("Could not find a category for route element: {route_element:?}"); - return None; - } - if map_id.is_none() { - info!("Could not find a map_id for route element: {route_element:?}"); - return None; - } - if category_uuid.is_none() { - info!("Could not find a uuid for route element: {route_element:?}"); - return None; - } - debug!( - "found route with {:?} elements {route_element:?}", - path.len() - ); - - Some(Route { - category, - parent: category_uuid.unwrap(), - path, - reset_position: Vec3(reset_position), - reset_range: reset_range.unwrap_or(0.0), - map_id: map_id.unwrap(), - name: name.unwrap().into(), - guid: parse_guid(names, route_element), - source_file_uuid, - }) -} - -fn parse_trail( - pack: &mut PackCore, - names: &XotAttributeNameIDs, - trail_element: &Element, - guid: Uuid, - category_name: &str, - category_uuid: &Uuid, - source_file_uuid: Uuid, -) -> Option { - //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html - - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(trail_element, names); - - if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) { - info!(%tex, "failed to find this texture in this pack"); - pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_uuid); - } - } - - #[allow(clippy::manual_map)] - // This is not exactly a manual map, we register something more in pack on some condition: a missing trail. - if let Some(map_id) = trail_element - .get_attribute(names.trail_data) - .and_then(|trail_data| { - //fix the path which may be a mix of windows and linux path - let file_path: RelativePath = trail_data.parse().unwrap(); - if let Some(tb) = pack.tbins.get(&file_path) { - Some(tb.map_id) - } else { - pack.found_missing_trail(&file_path, guid, &source_file_uuid); - None - } - }) - { - Some(Trail { - category: category_name.to_owned(), - parent: *category_uuid, - map_id, - props: common_attributes, - guid, - dynamic: false, - source_file_uuid, - }) - } else { - /*let td = trail_element.get_attribute(names.trail_data); - let file_path: RelativePath = td.unwrap_or_default().parse().unwrap(); - //pack.report.found_orphan_trail(&file_path, guid, &source_file_name); - let tbin = pack.tbins.get(&file_path).map(|tbin| (tbin.map_id, tbin.version)); - info!("missing map_id: {td:?} {file_path} {tbin:?}"); - */ - None - } -} - -#[instrument(skip(zip_archive))] -fn read_file_bytes_from_zip_by_name( - name: &str, - zip_archive: &mut zip::ZipArchive, -) -> Option> { - let mut bytes = vec![]; - match zip_archive.by_name(name) { - Ok(mut file) => match file.read_to_end(&mut bytes) { - Ok(size) => { - if size == 0 { - info!("empty file {name}"); - } else { - return Some(bytes); - } - } - Err(e) => { - info!(?e, "failed to read file"); - } - }, - Err(e) => { - info!(?e, "failed to get file from zip"); - } - } - None -} - -// #[cfg(test)] -// mod test { - -// use indexmap::IndexMap; -// use rstest::*; - -// use semver::Version; -// use similar_asserts::assert_eq; -// use std::io::Write; -// use std::sync::Arc; - -// use zip::write::FileOptions; -// use zip::ZipWriter; - -// use crate::{ -// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, -// INCHES_PER_METER, -// }; - -// const TEST_XML: &str = include_str!("test.xml"); -// const TEST_MARKER_PNG_NAME: &str = "marker.png"; -// const TEST_TRL_NAME: &str = "basic.trl"; - -// #[fixture] -// #[once] -// fn test_zip() -> Vec { -// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); -// // category.xml -// writer -// .start_file("category.xml", FileOptions::default()) -// .expect("failed to create category.xml"); -// writer -// .write_all(TEST_XML.as_bytes()) -// .expect("failed to write category.xml"); -// // marker.png -// writer -// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) -// .expect("failed to create marker.png"); -// writer -// .write_all(MARKER_PNG) -// .expect("failed to write marker.png"); -// // basic.trl -// writer -// .start_file(TEST_TRL_NAME, FileOptions::default()) -// .expect("failed to create basic trail"); -// writer -// .write_all(&0u32.to_ne_bytes()) -// .expect("failed to write version"); -// writer -// .write_all(&15u32.to_ne_bytes()) -// .expect("failed to write mapid "); -// writer -// .write_all(bytemuck::cast_slice(&[0f32; 3])) -// .expect("failed to write first node"); -// // done -// writer -// .finish() -// .expect("failed to finalize zip") -// .into_inner() -// } - -// #[fixture] -// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { -// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); -// assert_eq!(file_entries.len(), 3); -// let test_xml = std::str::from_utf8( -// file_entries -// .get(String::new("category.xml")) -// .expect("failed to get category.xml"), -// ) -// .expect("failed to get str from category.xml contents"); -// assert_eq!(test_xml, TEST_XML); -// let test_marker_png = file_entries -// .get(String::new("marker.png")) -// .expect("failed to get marker.png"); -// assert_eq!(test_marker_png, MARKER_PNG); -// file_entries -// } -// #[fixture] -// #[once] -// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { -// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); -// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); -// assert_eq!(pack.tbins.len(), 1); -// assert_eq!(pack.textures.len(), 1); -// assert_eq!( -// pack.textures -// .get(String::new(TEST_MARKER_PNG_NAME)) -// .expect("failed to get marker.png from textures"), -// MARKER_PNG -// ); - -// let tbin = pack -// .tbins -// .get(String::new(TEST_TRL_NAME)) -// .expect("failed to get basic trail") -// .clone(); - -// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); -// pack -// } - -// // #[rstest] -// // fn test_tag(test_pack: &Pack) { -// // let mut test_category_menu = CategoryMenu::default(); -// // let parent_path = String::new("parent"); -// // let child1_path = String::new("parent/child1"); -// // let subchild_path = String::new("parent/child1/subchild"); -// // let child2_path = String::new("parent/child2"); -// // test_category_menu.create_category(subchild_path); -// // test_category_menu.create_category(child2_path); -// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); -// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); -// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); -// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); - -// // assert_eq!(test_category_menu, test_pack.category_menu) -// // } - -// #[rstest] -// fn test_markers(test_pack: &Pack) { -// let marker = test_pack -// .markers -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// marker.props.texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); -// } -// #[rstest] -// fn test_trails(test_pack: &Pack) { -// let trail = test_pack -// .trails -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// trail.props.tbin.as_ref().unwrap(), -// String::new(TEST_TRL_NAME) -// ); -// assert_eq!( -// trail.props.trail_texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// } -// } diff --git a/crates/joko_package/src/io/error.rs b/crates/joko_package/src/io/error.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/joko_package/src/io/error.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/joko_package/src/io/export.rs b/crates/joko_package/src/io/export.rs deleted file mode 100644 index 73fc2f2..0000000 --- a/crates/joko_package/src/io/export.rs +++ /dev/null @@ -1,264 +0,0 @@ -use crate::{ - manager::{LoadedPackData, LoadedPackTexture}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::Dir; -use joko_package_models::{ - attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, - route::Route, trail::Trail, -}; -use miette::{Context, IntoDiagnostic, Result}; -use ordered_hash_map::OrderedHashMap; -use std::io::Write; -use tracing::info; -use uuid::Uuid; -use xot::{Element, Node, SerializeOptions, Xot}; - -pub(crate) fn export_package_v2( - pack: &PackCore, - writing_directory: &Dir, - name: String, -) -> Result<()> { - Ok(()) -} - -/// Save the pack core as xml pack using the given directory as pack root path. -pub(crate) fn export_package_v1( - pack_data: &LoadedPackData, - pack_textures: &LoadedPackData, - writing_directory: &Dir, -) -> Result<()> { - // save categories - info!( - "Saving data pack {}, {} categories, {} maps", - pack_data.name, - pack_data.categories.len(), - pack_data.maps.len() - ); - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) - .wrap_err("failed to serialize cats")?; - let cats = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to convert cats xot to string")?; - writing_directory - .create("categories.xml") - .into_diagnostic() - .wrap_err("failed to create categories.xml")? - .write_all(cats.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write to categories.xml")?; - // save maps - for (map_id, map_data) in pack_data.maps.iter() { - if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node: Node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create root wiht overlay data for pois")?; - let pois = tree.new_element(names.pois); - tree.append(od, pois) - .into_diagnostic() - .wrap_err("faild to append pois to od node")?; - for marker in map_data.markers.values() { - let poi = tree.new_element(names.poi); - tree.append(pois, poi) - .into_diagnostic() - .wrap_err("failed to append poi (marker) to pois")?; - let ele = tree.element_mut(poi).unwrap(); - serialize_marker_to_element(marker, ele, &names); - } - for route_path in map_data.routes.values() { - serialize_route_to_element(&mut tree, route_path, &pois, &names)?; - } - for trail in map_data.trails.values() { - if trail.dynamic { - continue; - } - let trail_node = tree.new_element(names.trail); - tree.append(pois, trail_node) - .into_diagnostic() - .wrap_err("failed to append a trail node to pois")?; - let ele = tree.element_mut(trail_node).unwrap(); - serialize_trail_to_element(trail, ele, &names); - } - let map_xml = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to serialize map data to string")?; - writing_directory - .create(format!("{map_id}.xml")) - .into_diagnostic() - .wrap_err("failed to create map xml file")? - .write_all(map_xml.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write map data to file")?; - } - Ok(()) -} -pub(crate) fn save_pack_texture_to_dir( - pack_texture: &LoadedPackTexture, - writing_directory: &Dir, -) -> Result<()> { - info!( - "Saving texture pack {}, {} textures, {} tbins", - pack_texture.name, - pack_texture.textures.len(), - pack_texture.tbins.len() - ); - // save images - for (img_path, img) in pack_texture.textures.iter() { - if let Some(parent) = img_path.parent() { - writing_directory - .create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir for an image: {img_path}") - })?; - } - writing_directory - .create(img_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(img) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; - } - // save tbins - for (tbin_path, tbin) in pack_texture.tbins.iter() { - if let Some(parent) = tbin_path.parent() { - writing_directory - .create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir of tbin: {tbin_path}") - })?; - } - let mut bytes: Vec = vec![]; - bytes.reserve(8 + tbin.nodes.len() * 12); - bytes.extend_from_slice(&tbin.version.to_ne_bytes()); - bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); - for node in &tbin.nodes { - bytes.extend_from_slice(&node[0].to_ne_bytes()); - bytes.extend_from_slice(&node[1].to_ne_bytes()); - bytes.extend_from_slice(&node[2].to_ne_bytes()); - } - writing_directory - .create(tbin_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? - .write_all(&bytes) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; - } - Ok(()) -} - -fn recursive_cat_serializer( - tree: &mut Xot, - names: &XotAttributeNameIDs, - cats: &OrderedHashMap, - parent: Node, -) -> Result<()> { - for (_, cat) in cats { - let cat_node = tree.new_element(names.marker_category); - tree.append(parent, cat_node).into_diagnostic()?; - { - let ele = tree.element_mut(cat_node).unwrap(); - ele.set_attribute(names.display_name, &cat.display_name); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); - // let cat_name = tree.add_name(cat_name); - ele.set_attribute(names.name, &cat.relative_category_name); - // no point in serializing default values - if !cat.default_enabled { - ele.set_attribute(names.default_enabled, "0"); - } - if cat.separator { - ele.set_attribute(names.separator, "1"); - } - cat.props.serialize_to_element(ele, names); - } - recursive_cat_serializer(tree, names, &cat.children, cat_node)?; - } - Ok(()) -} -fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); - ele.set_attribute(names.category, &trail.category); - ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute( - names._source_file_name, - format!("{}", trail.source_file_uuid), - ); - trail.props.serialize_to_element(ele, names); -} - -fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.xpos, format!("{}", marker.position[0])); - ele.set_attribute(names.ypos, format!("{}", marker.position[1])); - ele.set_attribute(names.zpos, format!("{}", marker.position[2])); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); - ele.set_attribute(names.map_id, format!("{}", marker.map_id)); - ele.set_attribute(names.category, &marker.category); - ele.set_attribute( - names._source_file_name, - format!("{}", marker.source_file_uuid), - ); - marker.attrs.serialize_to_element(ele, names); -} - -fn serialize_route_to_element( - tree: &mut Xot, - route: &Route, - parent: &Node, - names: &XotAttributeNameIDs, -) -> Result<()> { - let route_node = tree.new_element(names.route); - tree.append(*parent, route_node) - .into_diagnostic() - .wrap_err("failed to append route to pois")?; - let ele = tree.element_mut(route_node).unwrap(); - - ele.set_attribute(names.category, route.category.clone()); - ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); - ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); - ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); - ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); - ele.set_attribute(names.name, route.name.clone()); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); - ele.set_attribute(names.map_id, format!("{}", route.map_id)); - ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute( - names._source_file_name, - format!("{}", route.source_file_uuid), - ); - for pos in &route.path { - let child = tree.new_element(names.poi); - tree.append(route_node, child); - let child_elt = tree.element_mut(child).unwrap(); - child_elt.set_attribute(names.xpos, format!("{}", pos.x)); - child_elt.set_attribute(names.ypos, format!("{}", pos.y)); - child_elt.set_attribute(names.zpos, format!("{}", pos.z)); - //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); - } - Ok(()) -} diff --git a/crates/joko_package/src/io/mod.rs b/crates/joko_package/src/io/mod.rs deleted file mode 100644 index 310a6bb..0000000 --- a/crates/joko_package/src/io/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! This modules primarily deals with serializing and deserializing xml data from marker packs -//! - -mod deserialize; -mod error; -mod serialize; - -pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_normalized_folder}; -pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; diff --git a/crates/joko_package/src/io/serialize.rs b/crates/joko_package/src/io/serialize.rs deleted file mode 100644 index bcf3098..0000000 --- a/crates/joko_package/src/io/serialize.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::{ - manager::{LoadedPackData, LoadedPackTexture}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::Dir; -use glam::Vec3; -use joko_package_models::{ - attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, -}; -use miette::{Context, IntoDiagnostic, Result}; -use ordered_hash_map::OrderedHashMap; -use std::{fmt::format, io::Write}; -use tracing::info; -use uuid::Uuid; -use xot::{Element, Node, SerializeOptions, Xot}; - -/// Save the pack core as xml pack using the given directory as pack root path. -pub(crate) fn save_pack_data_to_dir( - pack_data: &LoadedPackData, - writing_directory: &Dir, -) -> Result<(), String> { - // save categories - info!( - "Saving data pack {}, {} categories, {} maps", - pack_data.name, - pack_data.categories.len(), - pack_data.maps.len() - ); - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node = tree - .new_root(od) - .or(Err("failed to create new root with overlay data node"))?; - recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od)?; - let cats = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .or(Err("failed to convert cats xot to string"))?; - writing_directory - .create("categories.xml") - .or(Err("failed to create categories.xml"))? - .write_all(cats.as_bytes()) - .or(Err("failed to write to categories.xml"))?; - // save maps - for (map_id, map_data) in pack_data.maps.iter() { - if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node: Node = tree - .new_root(od) - .or(Err("failed to create root wiht overlay data for pois"))?; - let pois = tree.new_element(names.pois); - tree.append(od, pois) - .or(Err("faild to append pois to od node"))?; - for marker in map_data.markers.values() { - let poi = tree.new_element(names.poi); - tree.append(pois, poi) - .or(Err("failed to append poi (marker) to pois"))?; - let ele = tree.element_mut(poi).unwrap(); - serialize_marker_to_element(marker, ele, &names); - } - for route_path in map_data.routes.values() { - serialize_route_to_element(&mut tree, route_path, &pois, &names)?; - } - for trail in map_data.trails.values() { - if trail.dynamic { - continue; - } - let trail_node = tree.new_element(names.trail); - tree.append(pois, trail_node) - .or(Err("failed to append a trail node to pois"))?; - let ele = tree.element_mut(trail_node).unwrap(); - serialize_trail_to_element(trail, ele, &names); - } - let map_xml = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .or(Err("failed to serialize map data to string"))?; - writing_directory - .create(format!("{map_id}.xml")) - .or(Err("failed to create map xml file"))? - .write_all(map_xml.as_bytes()) - .or(Err("failed to write map data to file"))?; - } - Ok(()) -} -pub(crate) fn save_pack_texture_to_dir( - pack_texture: &LoadedPackTexture, - writing_directory: &Dir, -) -> Result<(), String> { - info!( - "Saving texture pack {}, {} textures, {} tbins", - pack_texture.name, - pack_texture.textures.len(), - pack_texture.tbins.len() - ); - // save images - for (img_path, img) in pack_texture.textures.iter() { - if let Some(parent) = img_path.parent() { - writing_directory.create_dir_all(parent).or(Err(format!( - "failed to create parent dir for an image: {img_path}" - )))?; - } - writing_directory - .create(img_path.as_str()) - .or(Err(format!("failed to create file for image: {img_path}")))? - .write(img) - .or(Err(format!( - "failed to write image bytes to file: {img_path}" - )))?; - } - // save tbins - for (tbin_path, tbin) in pack_texture.tbins.iter() { - if let Some(parent) = tbin_path.parent() { - writing_directory.create_dir_all(parent).or(Err(format!( - "failed to create parent dir of tbin: {tbin_path}" - )))?; - } - let mut bytes: Vec = - Vec::with_capacity(8 + tbin.nodes.len() * std::mem::size_of::()); - bytes.extend_from_slice(&tbin.version.to_ne_bytes()); - bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); - for node in &tbin.nodes { - let node = &node.0; - bytes.extend_from_slice(&node[0].to_ne_bytes()); - bytes.extend_from_slice(&node[1].to_ne_bytes()); - bytes.extend_from_slice(&node[2].to_ne_bytes()); - } - writing_directory - .create(tbin_path.as_str()) - .or(Err(format!("failed to create tbin file: {tbin_path}")))? - .write_all(&bytes) - .or(Err(format!("failed to write tbin to path: {tbin_path}")))?; - } - Ok(()) -} - -fn recursive_cat_serializer( - tree: &mut Xot, - names: &XotAttributeNameIDs, - cats: &OrderedHashMap, - parent: Node, -) -> Result<(), String> { - for (_, cat) in cats { - let cat_node = tree.new_element(names.marker_category); - tree.append(parent, cat_node) - .or(Err("Could not insert category node"))?; - { - let ele = tree.element_mut(cat_node).unwrap(); - ele.set_attribute(names.display_name, &cat.display_name); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(cat.guid)); - // let cat_name = tree.add_name(cat_name); - ele.set_attribute(names.name, &cat.relative_category_name); - // no point in serializing default values - if !cat.default_enabled { - ele.set_attribute(names.default_enabled, "0"); - } - if cat.separator { - ele.set_attribute(names.separator, "1"); - } - cat.props.serialize_to_element(ele, names); - } - recursive_cat_serializer(tree, names, &cat.children, cat_node)?; - } - Ok(()) -} -fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); - ele.set_attribute(names.category, &trail.category); - ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - ele.set_attribute( - names._source_file_name, - format!("{}", trail.source_file_uuid), - ); - trail.props.serialize_to_element(ele, names); -} - -fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { - let position = &marker.position.0; - ele.set_attribute(names.xpos, format!("{}", position[0])); - ele.set_attribute(names.ypos, format!("{}", position[1])); - ele.set_attribute(names.zpos, format!("{}", position[2])); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); - ele.set_attribute(names.map_id, format!("{}", marker.map_id)); - ele.set_attribute(names.category, &marker.category); - ele.set_attribute( - names._source_file_name, - format!("{}", marker.source_file_uuid), - ); - marker.attrs.serialize_to_element(ele, names); -} - -fn serialize_route_to_element( - tree: &mut Xot, - route: &Route, - parent: &Node, - names: &XotAttributeNameIDs, -) -> Result<(), String> { - let route_node = tree.new_element(names.route); - tree.append(*parent, route_node) - .or(Err("failed to append route to pois"))?; - let ele = tree.element_mut(route_node).unwrap(); - - let reset_position = &route.reset_position.0; - ele.set_attribute(names.category, route.category.clone()); - ele.set_attribute(names.resetposx, format!("{}", reset_position[0])); - ele.set_attribute(names.resetposy, format!("{}", reset_position[1])); - ele.set_attribute(names.resetposz, format!("{}", reset_position[2])); - ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); - ele.set_attribute(names.name, route.name.clone()); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); - ele.set_attribute(names.map_id, format!("{}", route.map_id)); - ele.set_attribute(names.texture, "default_trail_texture.png"); - ele.set_attribute( - names._source_file_name, - format!("{}", route.source_file_uuid), - ); - for pos in &route.path { - let pos = &pos.0; - let child = tree.new_element(names.poi); - tree.append(route_node, child) - .or(Err("Could not inser child node"))?; - let child_elt = tree.element_mut(child).unwrap(); - child_elt.set_attribute(names.xpos, format!("{}", pos.x)); - child_elt.set_attribute(names.ypos, format!("{}", pos.y)); - child_elt.set_attribute(names.zpos, format!("{}", pos.z)); - //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); - } - Ok(()) -} diff --git a/crates/joko_package/src/io/test.xml b/crates/joko_package/src/io/test.xml deleted file mode 100644 index 3b50657..0000000 --- a/crates/joko_package/src/io/test.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/crates/joko_package/src/io/xmlfile_schema.xsd b/crates/joko_package/src/io/xmlfile_schema.xsd deleted file mode 100644 index 895a0ac..0000000 --- a/crates/joko_package/src/io/xmlfile_schema.xsd +++ /dev/null @@ -1,394 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/crates/joko_package/src/lib.rs b/crates/joko_package/src/lib.rs deleted file mode 100644 index d10d8a8..0000000 --- a/crates/joko_package/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! ReadOnly XML marker packs support for Jokolay -//! -//! - -pub(crate) mod io; -pub(crate) mod manager; -pub mod message; - -pub use manager::{ - build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, - jokolay_to_extract_path, load_all_from_dir, ImportStatus, LoadedPackData, LoadedPackTexture, - PackageDataManager, PackageUIManager, -}; - -// for compile time build info like pkg version or build timestamp or git hash etc.. -// shadow_rs::shadow!(build); - -// to filter the xml with rapidxml first -#[cxx::bridge(namespace = "rapid")] -mod ffi { - unsafe extern "C++" { - include!("joko_package/vendor/rapid/rapid.hpp"); - pub fn rapid_filter(src_xml: String) -> String; - - } -} - -pub fn rapid_filter_rust(src_xml: String) -> String { - ffi::rapid_filter(src_xml) -} - -pub const INCHES_PER_METER: f32 = 39.37; - -pub fn is_default(t: &T) -> bool { - t == &T::default() -} - -pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( - &base64::alphabet::STANDARD, - base64::engine::GeneralPurposeConfig::new(), -); diff --git a/crates/joko_package/src/manager/mod.rs b/crates/joko_package/src/manager/mod.rs deleted file mode 100644 index 8063da8..0000000 --- a/crates/joko_package/src/manager/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! How should the pack be stored by jokolay? -//! 1. Inside a directory called packs, we will have a separate directory for each pack. -//! 2. the name of the directory will serve as an ID for each pack. -//! 3. Inside the directory, we will have -//! 1. categories.xml -> The xml file which contains the whole category tree -//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. -//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. - -/* -expensive: -categories being a tree with order among siblings (better to use a tree crate?) -markers/trails referring to a category via full path. -editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. - -We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. - -*/ - -mod pack; -mod package_data; -mod package_ui; - -pub use pack::import::{import_pack_from_zip_file_path, ImportStatus}; -pub use pack::loaded::{ - build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, load_all_from_dir, - LoadedPackData, LoadedPackTexture, -}; -pub use package_data::PackageDataManager; -pub use package_ui::PackageUIManager; diff --git a/crates/joko_package/src/manager/pack/activation.rs b/crates/joko_package/src/manager/pack/activation.rs deleted file mode 100644 index 71f76ff..0000000 --- a/crates/joko_package/src/manager/pack/activation.rs +++ /dev/null @@ -1,20 +0,0 @@ -use indexmap::IndexMap; -use uuid::Uuid; - -/// This is the activation data per pack -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -pub struct ActivationData { - /// this is for markers which are global and only activate once regardless of account - pub global: IndexMap, - /// this is the activation data per character - /// for markers which trigger once per character - pub character: IndexMap>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ActivationType { - /// clean these up when the map is changed - ReappearOnMapChange, - /// clean these up when the timestamp is reached - TimeStamp(time::OffsetDateTime), - Instance(std::net::IpAddr), -} diff --git a/crates/joko_package/src/manager/pack/active.rs b/crates/joko_package/src/manager/pack/active.rs deleted file mode 100644 index 02120e7..0000000 --- a/crates/joko_package/src/manager/pack/active.rs +++ /dev/null @@ -1,303 +0,0 @@ -use joko_package_models::attributes::CommonAttributes; -use jokoapi::end_point::mounts::Mount; -use ordered_hash_map::OrderedHashMap; - -use egui::TextureHandle; -use indexmap::IndexMap; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::INCHES_PER_METER; -use joko_core::{ - serde_glam::{Vec2, Vec3}, - RelativePath, -}; -use joko_render_models::{ - marker::{MarkerObject, MarkerVertex}, - trail::TrailObject, -}; -use joko_link::MumbleLink; - -/* -- activation data with uuids and track the latest timestamp that will be activated -- category activation data -> track and changes to propagate to markers of this map -- current active markers, which will keep track of their original marker, so as to propagate any changes easily -*/ -#[derive(Clone)] -pub struct ActiveTrail { - pub trail_object: TrailObject, - pub texture_handle: TextureHandle, -} -/// This is an active marker. -/// It stores all the info that we need to scan every frame -#[derive(Clone)] -pub(crate) struct ActiveMarker { - /// texture id from managed textures - pub texture_id: u64, - /// owned texture handle to keep it alive - pub _texture: TextureHandle, - /// position - pub pos: Vec3, - /// billboard must not be bigger than this size in pixels - pub max_pixel_size: f32, - /// billboard must not be smaller than this size in pixels - pub min_pixel_size: f32, - pub common_attributes: CommonAttributes, -} - -pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0; // in game metric, for GW2, inches - -impl ActiveMarker { - pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { - let Self { - texture_id, - pos, - common_attributes: attrs, - _texture, - max_pixel_size, - min_pixel_size, - .. - } = self; - // let width = *width; - // let height = *height; - let texture_id = *texture_id; - let pos = *pos; - // filters - if let Some(mounts) = attrs.get_mount() { - if let Some(current) = Mount::try_from_mumble_link(link.mount) { - if !mounts.contains(current) { - return None; - } - } else { - return None; - } - } - let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs - .get_fade_far() - .copied() - .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) - / INCHES_PER_METER; - let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); - let player_distance = pos.0.distance(link.player_pos.0); - let camera_distance = pos.0.distance(link.cam_pos.0); - let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); - - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let color = attrs.get_color().copied().unwrap_or_default(); - /* - 1. we need to filter the markers - 1. statically - mapid, character, map_type, race, profession - 2. dynamically - achievement, behavior, mount, fade_far, cull - 3. force hide/show by user discretion - 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior - 3. store the state for these markers activation data, and temporary data like bounce - */ - /* - skip if: - alpha is 0.0 - achievement id/bit is done (maybe this should be at map filter level?) - behavior (activation) - cull - distance > fade_far - visibility (ingame/map/minimap) - mount - specialization - */ - if fade_far > 0.0 && player_distance > fade_far { - return None; - } - // markers are 1 meter in width/height by default - let mut pos = pos.0; - pos.y += height_offset; - let direction_to_marker = link.cam_pos.0 - pos; - let direction_to_side = direction_to_marker.normalize().cross(glam::Vec3::Y); - - let far_offset = { - let dpi = if link.dpi_scaling <= 0 { - 96.0 - } else { - link.dpi as f32 - } / 96.0; - let gw2_width = link.client_size.0.as_vec2().x / dpi; - - // offset (half width i.e. distance from center of the marker to the side of the marker) - const SIDE_OFFSET_FAR: f32 = 1.0; - // the size of the projected on to the near plane - let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); - // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width - let near_offset_in_pixels = near_offset * gw2_width; - - // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width - let near_offset_in_pixels = near_offset_in_pixels - .clamp(*min_pixel_size, *max_pixel_size) - .min(gw2_width / 2.0); - - let near_offset_of_marker = near_offset_in_pixels / gw2_width; - near_offset_of_marker * camera_distance / z_near - }; - // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; - // we want to map 100 pixels to one meter in game - // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard - // But, i will ignore that as that makes markers too small - let x_offset = far_offset; - let y_offset = x_offset; // seems all markers are squares - let bottom_left = MarkerVertex { - position: Vec3(pos - (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), - texture_coordinates: Vec2(glam::vec2(0.0, 1.0)), - alpha, - color, - fade_near_far, - }; - - let top_left = MarkerVertex { - position: Vec3(pos - (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), - texture_coordinates: Vec2(glam::vec2(0.0, 0.0)), - alpha, - color, - fade_near_far, - }; - let top_right = MarkerVertex { - position: Vec3(pos + (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), - texture_coordinates: Vec2(glam::vec2(1.0, 0.0)), - alpha, - color, - fade_near_far, - }; - let bottom_right = MarkerVertex { - position: Vec3(pos + (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), - texture_coordinates: Vec2(glam::vec2(1.0, 1.0)), - alpha, - color, - fade_near_far, - }; - let vertices = [ - top_left, - bottom_left, - bottom_right, - bottom_right, - top_right, - top_left, - ]; - Some(MarkerObject { - vertices, - texture: texture_id, - distance: player_distance, - }) - } -} - -impl ActiveTrail { - pub fn get_vertices_and_texture( - attrs: &CommonAttributes, - positions: &[Vec3], - texture: TextureHandle, - ) -> Option { - // can't have a trail without atleast two nodes - if positions.len() < 2 { - return None; - } - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs - .get_fade_far() - .copied() - .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) - / INCHES_PER_METER; - let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); - let color = attrs.get_color().copied().unwrap_or([0u8; 4]); - // default taco width - let horizontal_offset = 20.0 / INCHES_PER_METER; - // scale it trail scale - let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); - let height = horizontal_offset * 2.0; - - let mut vertices = vec![]; - // trail mesh is split by separating different parts with a [0, 0, 0] - // we will call each separate trail mesh as a "strip" of trail. - // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. - for strip in positions.split(|&v| v.0 == glam::Vec3::ZERO) { - let mut y_offset = 1.0; - for two_positions in strip.windows(2) { - let first = two_positions[0].0; - let second = two_positions[1].0; - // right side of the vector from first to second - let right_side = (second - first) - .normalize() - .cross(glam::Vec3::Y) - .normalize(); - - let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; - let first_left = MarkerVertex { - position: Vec3(first - (right_side * horizontal_offset)), - texture_coordinates: Vec2(glam::vec2(0.0, y_offset)), - alpha, - color, - fade_near_far, - }; - let first_right = MarkerVertex { - position: Vec3(first + (right_side * horizontal_offset)), - texture_coordinates: Vec2(glam::vec2(1.0, y_offset)), - alpha, - color, - fade_near_far, - }; - let second_left = MarkerVertex { - position: Vec3(second - (right_side * horizontal_offset)), - texture_coordinates: Vec2(glam::vec2(0.0, new_offset)), - alpha, - color, - fade_near_far, - }; - let second_right = MarkerVertex { - position: Vec3(second + (right_side * horizontal_offset)), - texture_coordinates: Vec2(glam::vec2(1.0, new_offset)), - alpha, - color, - fade_near_far, - }; - y_offset = if new_offset.is_sign_positive() { - new_offset - } else { - 1.0 - new_offset.fract().abs() - }; - vertices.extend([ - second_left, - first_left, - first_right, - first_right, - second_right, - second_left, - ]); - } - } - - Some(ActiveTrail { - trail_object: TrailObject { - vertices: vertices.into(), - texture: match texture.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }, - }, - texture_handle: texture, - }) - } -} - -#[derive(Default, Clone)] -pub(crate) struct CurrentMapData { - /// the map to which the current map data belongs to - //pub map_id: u32, - //pub active_elements: HashSet, - /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: OrderedHashMap, - /// The key is the index of the marker in the map markers - /// Their position in the map markers serves as their "id" as uuids can be duplicates. - pub active_markers: IndexMap, - pub wip_markers: IndexMap, - /// The key is the position/index of this trail in the map trails. same as markers - pub active_trails: IndexMap, - pub wip_trails: IndexMap, -} diff --git a/crates/joko_package/src/manager/pack/category_selection.rs b/crates/joko_package/src/manager/pack/category_selection.rs deleted file mode 100644 index 3ad1a3a..0000000 --- a/crates/joko_package/src/manager/pack/category_selection.rs +++ /dev/null @@ -1,308 +0,0 @@ -use joko_components::ComponentDataExchange; -use joko_package_models::{ - attributes::CommonAttributes, - category::Category, - package::{PackCore, PackageImportReport}, -}; -use joko_render_models::messages::UIToUIMessage; -use miette::{IntoDiagnostic, Result}; -use ordered_hash_map::OrderedHashMap; -use std::collections::{HashMap, HashSet}; - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::message::MessageToPackageBack; - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct CategorySelection { - //#[serde(skip)] - pub uuid: Uuid, //FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information - #[serde(skip)] - pub parent: Option, - pub is_selected: bool, //has it been selected in configuration to be displayed - pub is_active: bool, //currently being displayed (i.e.: active) - pub separator: bool, - pub display_name: String, - pub children: OrderedHashMap, -} - -pub struct SelectedCategoryManager { - data: OrderedHashMap, -} -impl<'a> SelectedCategoryManager { - pub fn new( - selected_categories: &OrderedHashMap, - categories: &OrderedHashMap, - ) -> Self { - let mut list_of_enabled_categories = Default::default(); - CategorySelection::get_list_of_enabled_categories( - selected_categories, - categories, - &mut list_of_enabled_categories, - &Default::default(), - ); - - Self { - data: list_of_enabled_categories, - } - } - #[allow(dead_code)] - pub fn cloned_data(&self) -> OrderedHashMap { - self.data.clone() - } - pub fn is_selected(&self, category: &Uuid) -> bool { - self.data.contains_key(category) - } - pub fn get(&self, key: &Uuid) -> &CommonAttributes { - self.data.get(key).unwrap() - } - #[allow(dead_code)] - pub fn len(&self) -> usize { - self.data.len() - } - pub fn keys(&'a self) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { - self.data.keys() - } -} - -impl CategorySelection { - pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { - let mut selectable_categories = OrderedHashMap::new(); - Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); - selectable_categories - } - fn get_list_of_enabled_categories( - selection: &OrderedHashMap, - categories: &OrderedHashMap, - list_of_enabled_categories: &mut OrderedHashMap, - parent_common_attributes: &CommonAttributes, - ) { - for (_, cat) in categories { - if let Some(selectable_category) = selection.get(&cat.relative_category_name) { - if !selectable_category.is_selected { - continue; - } - let mut common_attributes = cat.props.clone(); - common_attributes.inherit_if_attr_none(parent_common_attributes); - Self::get_list_of_enabled_categories( - &selectable_category.children, - &cat.children, - list_of_enabled_categories, - &common_attributes, - ); - list_of_enabled_categories.insert(cat.guid, common_attributes); - } - } - } - pub fn get( - selection: &mut OrderedHashMap, - uuid: Uuid, - ) -> Option<&mut CategorySelection> { - if selection.is_empty() { - None - } else { - for cat in selection.values_mut() { - if cat.uuid == uuid { - return Some(cat); - } - if let Some(res) = Self::get(&mut cat.children, uuid) { - return Some(res); - } - } - None - } - } - #[allow(dead_code)] - pub fn recursive_populate_guids( - selection: &mut OrderedHashMap, - entities_parents: &mut HashMap, - parent_uuid: Option, - ) { - for cat in selection.values_mut() { - if cat.uuid.is_nil() { - cat.uuid = Uuid::new_v4(); - } - cat.parent = parent_uuid; - Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); - if let Some(parent_uuid) = parent_uuid { - entities_parents.insert(cat.uuid, parent_uuid); - } - //assert!(cat.guid.len() > 0); - } - } - fn recursive_create_selectable_categories( - selectable_categories: &mut OrderedHashMap, - cats: &OrderedHashMap, - ) { - for (_, cat) in cats.iter() { - if !selectable_categories.contains_key(&cat.relative_category_name) { - let to_insert = CategorySelection { - uuid: cat.guid, - parent: cat.parent, - is_selected: cat.default_enabled, - is_active: !cat.separator, //by default separators are not considered active since they contain nothing - separator: cat.separator, - display_name: cat.display_name.clone(), - children: Default::default(), - }; - //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); - selectable_categories.insert(cat.relative_category_name.clone(), to_insert); - } - let s = selectable_categories - .get_mut(&cat.relative_category_name) - .unwrap(); - Self::recursive_create_selectable_categories(&mut s.children, &cat.children); - } - } - - pub fn recursive_set( - selection: &mut OrderedHashMap, - uuid: Uuid, - status: bool, - ) -> bool { - if selection.is_empty() { - false - } else { - for cat in selection.values_mut() { - if cat.separator { - continue; - } - if cat.uuid == uuid { - cat.is_selected = status; - return true; - } - if Self::recursive_set(&mut cat.children, uuid, status) { - return true; - } - } - false - } - } - pub fn recursive_set_all( - selection: &mut OrderedHashMap, - status: bool, - ) { - if selection.is_empty() { - return; - } - for cat in selection.values_mut() { - if cat.separator { - continue; - } - cat.is_selected = status; - Self::recursive_set_all(&mut cat.children, status); - } - } - - pub fn recursive_update_active_categories( - selection: &mut OrderedHashMap, - active_elements: &HashSet, - ) -> bool { - let mut is_active = false; - if selection.is_empty() { - //println!("recursive_update_active_categories is_empty"); - return is_active; - } - for cat in selection.values_mut() { - cat.is_active = active_elements.contains(&cat.uuid) - || Self::recursive_update_active_categories(&mut cat.children, active_elements); - if cat.is_active { - is_active = true; - } - } - is_active - } - - fn context_menu( - u2b_sender: &tokio::sync::mpsc::Sender, - cs: &mut CategorySelection, - ui: &mut egui::Ui, - ) { - if ui.button("Activate branch").clicked() { - cs.is_selected = true; - CategorySelection::recursive_set_all(&mut cs.children, true); - let msg = bincode::serialize( - &MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true), - ) - .unwrap(); //shall crash if wrong serialization of messages - let _ = u2b_sender.send(msg); - ui.close_menu(); - } - if ui.button("Deactivate branch").clicked() { - CategorySelection::recursive_set_all(&mut cs.children, false); - cs.is_selected = false; - let msg = bincode::serialize( - &MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false), - ) - .unwrap(); //shall crash if wrong serialization of messages - let _ = u2b_sender.send(msg); - ui.close_menu(); - } - } - - pub fn recursive_selection_ui( - u2b_sender: &tokio::sync::mpsc::Sender, - _u2u_sender: &tokio::sync::mpsc::Sender, - selection: &mut OrderedHashMap, - ui: &mut egui::Ui, - is_dirty: &mut bool, - show_only_active: bool, - import_quality_report: &PackageImportReport, - ) { - if selection.is_empty() { - return; - } - egui::ScrollArea::vertical().show(ui, |ui| { - for cat in selection.values_mut() { - if !cat.is_active && show_only_active && !cat.separator { - continue; - } - ui.horizontal(|ui| { - if cat.separator { - ui.add_space(3.0); - } else { - let cb = ui.checkbox(&mut cat.is_selected, ""); - if cb.changed() { - let msg = bincode::serialize( - &MessageToPackageBack::CategoryActivationElementStatusChange( - cat.uuid, - cat.is_selected, - ), - ) - .unwrap(); //shall crash if wrong serialization of messages - let _ = u2b_sender.send(msg); - *is_dirty = true; - } - } - //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); - let color = if import_quality_report.is_category_discovered_late(cat.uuid) { - egui::Color32::LIGHT_RED - } else if cat.is_active { - egui::Color32::LIGHT_GREEN - } else { - egui::Color32::GRAY - }; - let label = egui::RichText::new(&cat.display_name).color(color); - if cat.children.is_empty() { - ui.label(label); - } else { - ui.menu_button(label, |ui: &mut egui::Ui| { - Self::recursive_selection_ui( - u2b_sender, - _u2u_sender, - &mut cat.children, - ui, - is_dirty, - show_only_active, - import_quality_report, - ); - }) - .response - .context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); - } - }); - } - }); - } -} diff --git a/crates/joko_package/src/manager/pack/dirty.rs b/crates/joko_package/src/manager/pack/dirty.rs deleted file mode 100644 index f7fd509..0000000 --- a/crates/joko_package/src/manager/pack/dirty.rs +++ /dev/null @@ -1,28 +0,0 @@ -use ordered_hash_map::OrderedHashSet; - -use joko_core::RelativePath; - -#[derive(Debug, Default, Clone)] -pub(crate) struct DirtyMarker { - pub all: bool, - /// whether categories need to be saved - pub categories: bool, - /// whether selected categories needs to be saved - pub selected_categories: bool, - /// Whether any mapdata needs saving - pub map: OrderedHashSet, - /// whether any texture needs saving - pub texture: OrderedHashSet, - /// whether any tbin needs saving - pub tbin: OrderedHashSet, -} - -impl DirtyMarker { - pub fn is_dirty(&self) -> bool { - self.categories - || self.selected_categories - || !self.map.is_empty() - || !self.texture.is_empty() - || !self.tbin.is_empty() - } -} diff --git a/crates/joko_package/src/manager/pack/entry.rs b/crates/joko_package/src/manager/pack/entry.rs deleted file mode 100644 index ad78681..0000000 --- a/crates/joko_package/src/manager/pack/entry.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Debug)] -pub struct PackEntry { - pub url: url::Url, - pub description: String, -} - diff --git a/crates/joko_package/src/manager/pack/file_selection.rs b/crates/joko_package/src/manager/pack/file_selection.rs deleted file mode 100644 index c3ddc03..0000000 --- a/crates/joko_package/src/manager/pack/file_selection.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::collections::BTreeMap; - -use uuid::Uuid; - -pub struct SelectedFileManager { - data: BTreeMap, -} -impl SelectedFileManager { - pub fn new( - selected_files: &BTreeMap, - pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, - ) -> Self { - let mut list_of_enabled_files: BTreeMap = Default::default(); - SelectedFileManager::recursive_get_full_names( - selected_files, - pack_source_files, - currently_used_files, - &mut list_of_enabled_files, - ); - Self { - data: list_of_enabled_files, - } - } - fn recursive_get_full_names( - _selected_files: &BTreeMap, - _pack_source_files: &BTreeMap, - currently_used_files: &BTreeMap, - list_of_enabled_files: &mut BTreeMap, - ) { - for (key, v) in currently_used_files.iter() { - list_of_enabled_files.insert(*key, *v); - } - } - #[allow(dead_code)] - pub fn cloned_data(&self) -> BTreeMap { - self.data.clone() - } - pub fn is_selected(&self, source_file_uuid: &Uuid) -> bool { - let default = false; - self.data.is_empty() || *self.data.get(source_file_uuid).unwrap_or(&default) - } - #[allow(dead_code)] - pub fn len(&self) -> usize { - self.data.len() - } -} diff --git a/crates/joko_package/src/manager/pack/import.rs b/crates/joko_package/src/manager/pack/import.rs deleted file mode 100644 index 0234a46..0000000 --- a/crates/joko_package/src/manager/pack/import.rs +++ /dev/null @@ -1,29 +0,0 @@ -use joko_package_models::package::PackCore; -use tracing::info; - -#[derive(Debug, Default)] -pub enum ImportStatus { - #[default] - UnInitialized, - WaitingForFileChooser, - LoadingPack(std::path::PathBuf), - WaitingLoading(std::path::PathBuf), - PackDone(String, PackCore, bool), - PackError(miette::Report), -} - -pub fn import_pack_from_zip_file_path( - file_path: std::path::PathBuf, - extract_temporary_path: &std::path::PathBuf, -) -> Result<(String, PackCore), String> { - info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(file_path.clone(), extract_temporary_path).map(|pack| { - ( - file_path - .file_name() - .map(|ostr| ostr.to_string_lossy().to_string()) - .unwrap_or_default(), - pack, - ) - }) -} diff --git a/crates/joko_package/src/manager/pack/list.rs b/crates/joko_package/src/manager/pack/list.rs deleted file mode 100644 index 499fe2f..0000000 --- a/crates/joko_package/src/manager/pack/list.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Debug, Default)] -pub struct PackList { - pub packs: BTreeMap, -} - - diff --git a/crates/joko_package/src/manager/pack/loaded.rs b/crates/joko_package/src/manager/pack/loaded.rs deleted file mode 100644 index 223b475..0000000 --- a/crates/joko_package/src/manager/pack/loaded.rs +++ /dev/null @@ -1,1021 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - path::PathBuf, - sync::Arc, -}; - -use joko_components::ComponentDataExchange; -use joko_package_models::{ - attributes::{Behavior, CommonAttributes}, - category::Category, - map::MapData, - package::{PackCore, PackageImportReport}, - trail::TBin, -}; -use ordered_hash_map::OrderedHashMap; - -use cap_std::fs_utf8::Dir; -use egui::{ColorImage, TextureHandle}; -use image::EncodableLayout; -use serde::{Deserialize, Serialize}; -use tracing::{debug, error, info, info_span, trace}; -use uuid::Uuid; - -use crate::message::MessageToPackageUI; -use crate::{ - io::{load_pack_core_from_normalized_folder, save_pack_data_to_dir, save_pack_texture_to_dir}, - manager::{ - pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, - package_data::EXTRACT_DIRECTORY_NAME, - }, - message::MessageToPackageBack, -}; -use joko_core::{ - serde_glam::Vec3, - task::{AsyncTask, AsyncTaskGuard}, - RelativePath, -}; -use joko_render_models::{messages::UIToUIMessage, trail::TrailObject}; -use joko_link::MumbleLink; -use miette::{Context, IntoDiagnostic, Result}; - -use super::activation::{ActivationData, ActivationType}; -use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; -use crate::manager::pack::category_selection::CategorySelection; -use crate::manager::package_data::{ - EDITABLE_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME, PACKAGES_DIRECTORY_NAME, - PACKAGE_MANAGER_DIRECTORY_NAME, -}; - -type ImportAllTriplet = ( - BTreeMap, - BTreeMap, - BTreeMap, -); -type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); - -//TODO: separate in front and back tasks -pub(crate) struct PackTasks { - //an object that can handle such tasks should be passed as argument of any function that may required an async action - save_texture_task: AsyncTask>, - save_data_task: AsyncTask>, - save_report_task: AsyncTask<(Arc

, PackageImportReport), Result<(), String>>, - load_all_packs_task: - AsyncTask<(Arc, std::path::PathBuf), Result>, -} - -//TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? -#[derive(Clone)] -pub struct LoadedPackData { - pub name: String, - pub uuid: Uuid, - pub dir: Arc, - /// The actual xml pack. - //pub core: PackCore, - pub categories: OrderedHashMap, - pub all_categories: HashMap, - pub source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package - pub maps: HashMap, - selected_files: BTreeMap, - _is_dirty: bool, //there was an edition in the package itself - - // loca copy in the data side of what is exposed in UI - selectable_categories: OrderedHashMap, - pub entities_parents: HashMap, - activation_data: ActivationData, - active_elements: HashSet, //keep track of which elements are active -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct LoadedPackTexture { - //TODO: there is a need for a late loading of texture to avoid transmitting them (serialize) - pub name: String, - pub uuid: Uuid, - /// The directory inside which the pack data is stored - /// There should be a subdirectory called `core` which stores the pack core - /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. - /// eg: Active categories, activation data etc.. - //pub dir: Arc, - pub path: std::path::PathBuf, - pub source_files: BTreeMap, - pub tbins: HashMap, - pub textures: HashMap>, - - /// The selection of categories which are "enabled" and markers belonging to these may be rendered - selectable_categories: OrderedHashMap, - #[serde(skip)] - current_map_data: CurrentMapData, - activation_data: ActivationData, - //active_elements: HashSet, //which are the active elements (loaded) - _is_dirty: bool, -} - -impl PackTasks { - pub fn new() -> Self { - Self { - save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), - save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), - save_report_task: AsyncTaskGuard::new(PackTasks::async_save_report), - load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), - } - } - pub fn is_running(&self) -> bool { - self.save_texture_task.lock().unwrap().is_running() - || self.save_data_task.lock().unwrap().is_running() - } - pub fn count(&self) -> i32 { - self.save_texture_task.lock().unwrap().count() - + self.save_data_task.lock().unwrap().count() - + self.load_all_packs_task.lock().unwrap().count() - } - - pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { - //saved on load, or change of list of what to display - if status { - std::mem::take(&mut texture_pack._is_dirty); - let _ = self - .save_texture_task - .lock() - .unwrap() - .send(texture_pack.clone()); - } - } - - pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { - if status { - std::mem::take(&mut data_pack._is_dirty); - let _ = self.save_data_task.lock().unwrap().send(data_pack.clone()); - } - } - pub fn save_report(&self, target_dir: Arc, report: PackageImportReport, status: bool) { - if status { - let _ = self - .save_report_task - .lock() - .unwrap() - .send((target_dir, report)); - } - } - pub fn load_all_packs(&self, jokolay_dir: Arc, root_path: std::path::PathBuf) { - let _ = self - .load_all_packs_task - .lock() - .unwrap() - .send((jokolay_dir, root_path)); - } - pub fn wait_for_load_all_packs(&self) -> Result { - self.load_all_packs_task.lock().unwrap().recv().unwrap() - } - - #[allow(dead_code, unused)] - fn change_map( - &self, - pack: &mut LoadedPackData, - b2u_sender: &std::sync::mpsc::Sender, - link: &MumbleLink, - currently_used_files: &BTreeMap, - ) { - //TODO - unimplemented!("PackTask::change_map is not implemented"); - } - - fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<(), String> { - trace!("Save texture package {:?}", pack_texture.path); - let std_file = std::fs::OpenOptions::new() - .open(&pack_texture.path) - .or(Err("Could not open file"))?; - let dir = cap_std::fs_utf8::Dir::from_std_file(std_file); - match serde_json::to_string_pretty(&pack_texture.selectable_categories) { - Ok(cs_json) => match dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - match serde_json::to_string_pretty(&pack_texture.activation_data) { - Ok(ad_json) => match dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { - Ok(_) => { - debug!("wrote activation to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write activation data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize activation"); - } - } - let writing_directory = dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - save_pack_texture_to_dir(&pack_texture, &writing_directory)?; - Ok(()) - } - - fn async_save_data(pack_data: LoadedPackData) -> Result<(), String> { - trace!("Save data package {:?}", pack_data.dir); - pack_data - .dir - .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to create xmlpack directory"))?; - let writing_directory = pack_data - .dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - save_pack_data_to_dir(&pack_data, &writing_directory)?; - Ok(()) - } - - fn async_save_report(input: (Arc, PackageImportReport)) -> Result<(), String> { - let (writing_directory, report) = input; - trace!("Save report package {:?}", writing_directory); - match serde_json::to_string_pretty(&report) { - Ok(cs_json) => { - match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote import quality report to disk"); - } - Err(e) => { - debug!(?e, "failed to write import quality report to disk"); - } - } - } - Err(e) => { - error!(?e, "failed to serialize import quality report"); - } - } - Ok(()) - } -} - -impl LoadedPackData { - const CORE_PACK_DIR_NAME: &'static str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; - - fn load_selectable_categories( - pack_dir: &Arc, - pack: &PackCore, - ) -> OrderedHashMap { - //FIXME: we need to patch those categories from the one in the files - (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { - match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize category data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(pack); - match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - cs - }) - } - - fn load_import_report(pack_dir: &Arc) -> Option { - //FIXME: we need to patch those categories from the one in the files - (if pack_dir.is_file(PackageImportReport::REPORT_FILE_NAME) { - match pack_dir.read_to_string(PackageImportReport::REPORT_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize import report"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of import report"); - None - } - } - } else { - None - }) - .flatten() - } - pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { - if !pack_dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .or(Err("failed to check if pack core exists"))? - { - return Err("pack core doesn't exist in this pack".to_string()); - } - let core_dir = pack_dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - let start = std::time::SystemTime::now(); - let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_normalized_folder(&core_dir, import_report) - .or(Err("failed to load pack from dir"))?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of package from disk {} took {} ms", - name, - elaspsed.as_millis() - ); - - //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. - //let selectable_categories = CategorySelection::default_from_pack_core(&core); - let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); - - Ok(LoadedPackData { - name, - uuid: core.uuid, - dir: pack_dir, - selected_files: Default::default(), - all_categories: core.all_categories, - categories: core.categories, - maps: core.maps, - source_files: core.active_source_files, - _is_dirty: false, - active_elements: Default::default(), - activation_data: Default::default(), - selectable_categories, - entities_parents: core.entities_parents, - }) - } - - pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { - if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { - self._is_dirty = true; - true - } else { - false - } - } - pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { - if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { - cs.is_selected = status; - self._is_dirty = true; - if CategorySelection::recursive_set(&mut cs.children, uuid, status) { - return true; - } - } - false - } - pub fn category_set_all(&mut self, status: bool) { - CategorySelection::recursive_set_all(&mut self.selectable_categories, status); - self._is_dirty = true; - } - - pub fn is_dirty(&self) -> bool { - self._is_dirty - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn tick( - &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, - _loop_index: u128, - link: &MumbleLink, - currently_used_files: &BTreeMap, - list_of_active_or_selected_elements_changed: bool, - map_changed: bool, - _tasks: &PackTasks, - next_loaded: &mut HashSet, - ) { - //since the loading of texture is lazy, there is no problem when calling this regularly - if map_changed || list_of_active_or_selected_elements_changed { - //tasks.change_map(self, b2u_sender, link, currently_used_files); - let mut active_elements: HashSet = Default::default(); - self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - let _ = b2u_sender.send( - MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()) - .into(), - ); - self.active_elements = active_elements.clone(); - next_loaded.extend(active_elements); - } - } - - fn on_map_changed( - &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, - link: &MumbleLink, - currently_used_files: &BTreeMap, - active_elements: &mut HashSet, - ) { - info!(link.map_id, "current map data is updated. {}", self.name); - if link.map_id == 0 { - info!("No map do not do anything"); - return; - } - debug!( - "Start building SelectedCategoryManager {}", - self.selectable_categories.len() - ); - let selected_categories_manager = - SelectedCategoryManager::new(&self.selectable_categories, &self.categories); - - debug!("Start building SelectedFileManager"); - let selected_files_manager = SelectedFileManager::new( - &self.selected_files, - &self.source_files, - currently_used_files, - ); - - debug!("Start loading markers"); - let mut nb_markers_attempt = 0; - let mut nb_markers_loaded = 0; - for marker in self - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .markers - .values() - { - nb_markers_attempt += 1; - if selected_files_manager.is_selected(&marker.source_file_uuid) { - active_elements.insert(marker.guid); - active_elements.insert(marker.parent); - if selected_categories_manager.is_selected(&marker.parent) { - let category_attributes = selected_categories_manager.get(&marker.parent); - let mut common_attributes = marker.attrs.clone(); // why a clone ? - common_attributes.inherit_if_attr_none(category_attributes); - let key = &marker.guid; - if let Some(behavior) = common_attributes.get_behavior() { - if match behavior { - Behavior::AlwaysVisible => false, - Behavior::ReappearOnMapChange - | Behavior::ReappearOnDailyReset - | Behavior::OnlyVisibleBeforeActivation - | Behavior::ReappearAfterTimer - | Behavior::ReappearOnMapReset - | Behavior::WeeklyReset => { - self.activation_data.global.contains_key(key) - } - Behavior::OncePerInstance => self - .activation_data - .global - .get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default(), - Behavior::DailyPerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| a.contains_key(key)) - .unwrap_or_default(), - Behavior::OncePerInstancePerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| { - a.get(key) - .map(|a| match a { - ActivationType::Instance(a) => { - a == &link.server_address - } - _ => false, - }) - .unwrap_or_default() - }) - .unwrap_or_default(), - Behavior::WvWObjective => { - false // ??? - } - } { - continue; - } - } - if let Some(tex_path) = common_attributes.get_icon_file() { - let _ = b2u_sender.send( - MessageToPackageUI::MarkerTexture( - self.uuid, - tex_path.clone(), - marker.guid, - marker.position, - common_attributes, - ) - .into(), - ); - } else { - debug!("no texture attribute on this marker"); - } - - nb_markers_loaded += 1; - } else { - debug!( - "category {} = {} is not enabled", - marker.category, marker.parent - ); - } - } - } - - debug!("Start loading trails"); - let mut nb_trails_attempt = 0; - let mut nb_trails_loaded = 0; - for trail in self - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .trails - .values() - { - nb_trails_attempt += 1; - if selected_files_manager.is_selected(&trail.source_file_uuid) { - active_elements.insert(trail.guid); - active_elements.insert(trail.parent); - if selected_categories_manager.is_selected(&trail.parent) { - let category_attributes = selected_categories_manager.get(&trail.parent); - let mut common_attributes = trail.props.clone(); - common_attributes.inherit_if_attr_none(category_attributes); - if let Some(tex_path) = common_attributes.get_texture() { - let _ = b2u_sender.send( - MessageToPackageUI::TrailTexture( - self.uuid, - tex_path.clone(), - trail.guid, - common_attributes, - ) - .into(), - ); - } else { - debug!("no texture attribute on this trail"); - } - nb_trails_loaded += 1; - } else { - debug!( - "category {} = {} is not enabled", - trail.category, trail.parent - ); - } - } - } - info!( - "Load notifications for {} on map {}: {}/{} markers and {}/{} trails", - self.name, - link.map_id, - nb_markers_loaded, - nb_markers_attempt, - nb_trails_loaded, - nb_trails_attempt - ); - debug!( - "active categories: {:?}", - selected_categories_manager.keys() - ); - } -} - -impl LoadedPackTexture { - const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; - - pub fn category_set_all(&mut self, status: bool) { - CategorySelection::recursive_set_all(&mut self.selectable_categories, status); - self._is_dirty = true; - } - - pub fn update_active_categories(&mut self, active_elements: &HashSet) { - CategorySelection::recursive_update_active_categories( - &mut self.selectable_categories, - active_elements, - ); - } - pub fn category_sub_menu( - &mut self, - u2b_sender: &tokio::sync::mpsc::Sender, - u2u_sender: &tokio::sync::mpsc::Sender, - ui: &mut egui::Ui, - show_only_active: bool, - import_quality_report: &PackageImportReport, - ) { - //it is important to generate a new id each time to avoid collision - ui.push_id(ui.next_auto_id(), |ui| { - CategorySelection::recursive_selection_ui( - u2b_sender, - u2u_sender, - &mut self.selectable_categories, - ui, - &mut self._is_dirty, - show_only_active, - import_quality_report, - ); - }); - if self._is_dirty { - let msg = - bincode::serialize(&MessageToPackageBack::CategoryActivationStatusChanged).unwrap(); //shall crash if wrong serialization of messages - let _ = u2b_sender.send(msg); - } - } - - pub fn is_dirty(&self) -> bool { - self._is_dirty - } - pub(crate) fn tick( - &mut self, - u2u_sender: &tokio::sync::mpsc::Sender, - _timestamp: f64, - link: &MumbleLink, - //next_on_screen: &mut HashSet, - z_near: f32, - _tasks: &PackTasks, - ) -> Result<()> { - tracing::trace!( - "LoadedPackTexture.tick: {} {}-{} {}-{}", - self.name, - self.current_map_data.active_markers.len(), - self.current_map_data.wip_markers.len(), - self.current_map_data.active_trails.len(), - self.current_map_data.wip_trails.len(), - ); - let mut marker_objects = Vec::new(); - for marker in self.current_map_data.active_markers.values() { - if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { - marker_objects.push(mo); - } - } - tracing::trace!( - "LoadedPackTexture.tick: {}, markers {}", - self.name, - marker_objects.len() - ); - let msg = bincode::serialize(&UIToUIMessage::BulkMarkerObject(marker_objects)) - .into_diagnostic()?; - let _ = u2u_sender.send(msg); - let mut trail_objects = Vec::new(); - for trail in self.current_map_data.active_trails.values() { - trail_objects.push(TrailObject { - vertices: trail.trail_object.vertices.clone(), - texture: trail.trail_object.texture, - }); - //next_on_screen.insert(*uuid); - } - tracing::trace!( - "LoadedPackTexture.tick: {}, trails {}", - self.name, - trail_objects.len() - ); - let msg = - bincode::serialize(&UIToUIMessage::BulkTrailObject(trail_objects)).into_diagnostic()?; - let _ = u2u_sender.send(msg); - Ok(()) - } - - pub fn swap(&mut self) { - info!( - "swap {} to display {} textures, {} markers, {} trails", - self.name, - self.current_map_data.active_textures.len(), - self.current_map_data.wip_markers.len(), - self.current_map_data.wip_trails.len() - ); - self.current_map_data.active_markers = - std::mem::take(&mut self.current_map_data.wip_markers); - self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); - } - - pub fn load_marker_texture( - &mut self, - egui_context: &egui::Context, - default_tex_id: &TextureHandle, - tex_path: &RelativePath, - marker_uuid: Uuid, - position: Vec3, - common_attributes: CommonAttributes, - ) { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - - //TODO: insertion must happen inside the UI => egui_context should never be transmitted on a tick() - self.current_map_data.active_textures.insert( - tex_path.clone(), - egui_context.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - error!(%tex_path, "failed to find this icon texture"); - } - } - let th = self - .current_map_data - .active_textures - .get(tex_path) - .unwrap_or(default_tex_id); - let texture_id = match th.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }; - - let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size - let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size - let am = ActiveMarker { - texture_id, - _texture: th.clone(), - common_attributes, - pos: position, - max_pixel_size, - min_pixel_size, - }; - self.current_map_data.wip_markers.insert(marker_uuid, am); - } - - pub fn load_trail_texture( - &mut self, - egui_context: &egui::Context, - default_tex_id: &TextureHandle, - tex_path: &RelativePath, - trail_uuid: Uuid, - common_attributes: CommonAttributes, - ) { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - egui_context.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - error!(%tex_path, "failed to find this trail texture"); - } - } else { - trace!("Trail texture already loaded {:?}", tex_path); - } - let texture_path = common_attributes.get_texture(); - let th = texture_path - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - - let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { - debug!(?texture_path, "tbin path"); - tbin - } else { - info!(?trail_uuid, "missing tbin path"); - return; - }; - let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { - tbin - } else { - info!(%tbin_path, "failed to find tbin"); - return; - }; - if let Some(active_trail) = - ActiveTrail::get_vertices_and_texture(&common_attributes, &tbin.nodes, th.clone()) - { - self.current_map_data - .wip_trails - .insert(trail_uuid, active_trail); - } else { - info!("Cannot display {texture_path:?}") - } - } -} - -pub fn jokolay_to_editable_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { - let marker_manager_path = jokolay_to_marker_path(jokolay_path); - marker_manager_path.join(EDITABLE_PACKAGE_NAME) -} - -pub fn jokolay_to_extract_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { - jokolay_path.join(EXTRACT_DIRECTORY_NAME) -} - -pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { - jokolay_path - .join(PACKAGE_MANAGER_DIRECTORY_NAME) - .join(PACKAGES_DIRECTORY_NAME) -} - -pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { - jokolay_dir - .create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!( - "failed to create marker manager directory {}", - PACKAGE_MANAGER_DIRECTORY_NAME - ))?; - let marker_manager_dir = jokolay_dir - .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!( - "failed to open marker manager directory {}", - PACKAGE_MANAGER_DIRECTORY_NAME - ))?; - - marker_manager_dir - .create_dir_all(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!( - "failed to create marker packs directory {}", - PACKAGES_DIRECTORY_NAME - ))?; - let marker_packs_dir = marker_manager_dir - .open_dir(PACKAGES_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err(format!( - "failed to open marker packs dir {}", - PACKAGES_DIRECTORY_NAME - ))?; - - marker_packs_dir - .create_dir_all(EDITABLE_PACKAGE_NAME) - .into_diagnostic() - .wrap_err("failed to create editable package directory")?; - let editable_package = marker_packs_dir - .open_dir(EDITABLE_PACKAGE_NAME) - .into_diagnostic() - .wrap_err("failed to create editable package directory")?; - - editable_package - .create_dir_all("data") - .into_diagnostic() - .wrap_err("failed to create data folder for editable package")?; - - Ok(marker_packs_dir) -} - -pub fn load_all_from_dir( - input: (Arc, std::path::PathBuf), -) -> Result { - let (jokolay_dir, root_path) = input; - let marker_packs_dir = - jokolay_to_marker_dir(&jokolay_dir).or(Err("Failed to open packages directory"))?; - let marker_packs_path = jokolay_to_marker_path(&root_path); - let mut data_packs: BTreeMap = Default::default(); - let mut texture_packs: BTreeMap = Default::default(); - let mut report_packs: BTreeMap = Default::default(); - - for entry in marker_packs_dir - .entries() - .or(Err("failed to get entries of marker packs dir"))? - { - let entry = entry.or(Err("Failed to read packages directory"))?; - if entry - .metadata() - .or(Err("Could not read folder metadata"))? - .is_file() - { - continue; - } - if let Ok(name) = entry.file_name() { - let pack_path = marker_packs_path.join(&name); - let pack_dir = entry.open_dir().or(Err(format!( - "failed to open pack entry as directory: {}", - name - )))?; - { - if name == EDITABLE_PACKAGE_NAME { - //TODO: have a version of loading that does not involve already ingested packages - if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { - let lp = - build_from_core(name.clone(), pack_dir.into(), pack_path, pack_core); - let (data, tex, report) = lp; - data_packs.insert(data.uuid, data); - texture_packs.insert(tex.uuid, tex); - report_packs.insert(report.uuid, report); - } - } else if name == LOCAL_EXPANDED_PACKAGE_NAME { - //ignore this package, it'll be overwriten - } else { - let span_guard = info_span!("loading pack from dir", name).entered(); - - match build_from_dir(name.clone(), pack_dir.into(), pack_path) { - Ok(lp) => { - let (data, tex, report) = lp; - data_packs.insert(data.uuid, data); - texture_packs.insert(tex.uuid, tex); - report_packs.insert(report.uuid, report); - } - Err(e) => { - error!(?e, "failed to load pack from directory: {}", name); - } - } - drop(span_guard); - } - } - } - } - Ok((data_packs, texture_packs, report_packs)) -} - -fn build_from_dir( - name: String, - pack_dir: Arc, - pack_path: PathBuf, -) -> Result { - if !pack_dir - .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to check if pack core exists"))? - { - return Err("pack core doesn't exist in this pack".to_string()); - } - let core_dir = pack_dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - let start = std::time::SystemTime::now(); - let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_normalized_folder(&core_dir, import_report) - .or(Err("failed to load pack from dir"))?; - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of package from disk {} took {} ms", - name, - elaspsed.as_millis() - ); - let res = build_from_core(name.clone(), pack_dir, pack_path, core); - Ok(res) -} - -pub fn build_from_core( - name: String, - pack_dir: Arc, - path: PathBuf, - core: PackCore, -) -> ImportTriplet { - let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); - let data = LoadedPackData { - name: name.clone(), - uuid: core.uuid, - dir: Arc::clone(&pack_dir), - selected_files: Default::default(), - all_categories: core.all_categories, - categories: core.categories, - maps: core.maps, - source_files: core.active_source_files.clone(), - _is_dirty: false, - activation_data: Default::default(), - active_elements: Default::default(), - selectable_categories: selectable_categories.clone(), - entities_parents: core.entities_parents, - }; - let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); - let tex = LoadedPackTexture { - uuid: core.uuid, - selectable_categories, - textures: core.textures, - current_map_data: Default::default(), - _is_dirty: false, - activation_data, - path, - name, - tbins: core.tbins, - //active_elements: Default::default(), - source_files: core.active_source_files, - }; - let report = core.report; - (data, tex, report) -} diff --git a/crates/joko_package/src/manager/pack/mod.rs b/crates/joko_package/src/manager/pack/mod.rs deleted file mode 100644 index 908a692..0000000 --- a/crates/joko_package/src/manager/pack/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod activation; -pub mod active; -pub mod category_selection; -pub mod file_selection; -pub mod import; -pub mod loaded; diff --git a/crates/joko_package/src/manager/package_data.rs b/crates/joko_package/src/manager/package_data.rs deleted file mode 100644 index 4d92339..0000000 --- a/crates/joko_package/src/manager/package_data.rs +++ /dev/null @@ -1,516 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - sync::Arc, -}; - -use cap_std::fs_utf8::Dir; -use joko_components::ComponentDataExchange; -use joko_package_models::package::PackageImportReport; - -use tracing::{error, info, info_span, trace}; - -use crate::{ - build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, - jokolay_to_extract_path, message::MessageToPackageBack, -}; -use joko_link::{MumbleLink, MumbleLinkSharedState}; -use miette::{IntoDiagnostic, Result}; -use uuid::Uuid; - -use crate::manager::pack::loaded::{LoadedPackData, PackTasks}; -use crate::message::MessageToPackageUI; - -use super::pack::loaded::{jokolay_to_marker_dir, jokolay_to_marker_path}; - -pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose -pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose -pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading -pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite -pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package - // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; - -#[derive(Clone)] -pub struct PackageBackSharedState { - choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI - pub root_dir: Arc, - pub root_path: std::path::PathBuf, - #[allow(dead_code)] - pub editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration - extract_path: std::path::PathBuf, -} - -/// It manage everything that has to do with marker packs. -/// 1. imports, loads, saves and exports marker packs. -/// 2. maintains the categories selection data for every pack -/// 3. contains activation data globally and per character -/// 4. When we load into a map, it filters the markers and runs the logic every frame -/// 1. If a marker needs to be activated (based on player position or whatever) -/// 2. marker needs to be drawn -/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture -/// 4. render that marker use joko_render - -#[must_use] -pub struct PackageDataManager { - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - //_marker_manager_dir: Arc, - /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. - /// The name of the child directory is the name of the pack - //pub marker_packs_dir: Arc, - pub marker_packs_path: std::path::PathBuf, - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - pub packs: BTreeMap, - tasks: PackTasks, - current_map_id: u32, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, - - pub currently_used_files: BTreeMap, - parents: HashMap, - loaded_elements: HashSet, - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - pub state: PackageBackSharedState, -} - -impl PackageDataManager { - /// Creates a new instance of [MarkerManager]. - /// 1. It opens the marker manager directory - /// 2. loads its configuration - /// 3. opens the packs directory - /// 4. loads all the packs - /// 5. loads all the activation data - /// 6. returns self - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - ) -> Result { - let marker_packs_dir = jokolay_to_marker_dir(&root_dir)?; - let marker_packs_path = jokolay_to_marker_path(root_path); - //TODO: load configuration from disk (ui.toml) - let editable_path = jokolay_to_editable_path(root_path.clone()) - .to_str() - .unwrap() - .to_string(); - let state = PackageBackSharedState { - choice_of_category_changed: false, - root_dir, - root_path: root_path.to_owned(), - editable_path: std::path::PathBuf::from(editable_path), - extract_path: jokolay_to_extract_path(root_path), - }; - Ok(Self { - packs: Default::default(), - tasks: PackTasks::new(), - //marker_packs_dir: Arc::new(marker_packs_dir), - marker_packs_path, - current_map_id: 0, - save_interval: 0.0, - currently_used_files: Default::default(), - parents: Default::default(), - loaded_elements: Default::default(), - channel_sender, - channel_receiver, - state, - }) - } - - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { - self.currently_used_files = currently_used_files; - } - - pub fn category_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_set(uuid, status) { - break; - } - } - } - - pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { - for pack in self.packs.values_mut() { - if pack.category_branch_set(uuid, status) { - break; - } - } - } - - pub fn category_set_all(&mut self, status: bool) { - for pack in self.packs.values_mut() { - pack.category_set_all(status); - } - } - - pub fn register(&mut self, element: Uuid, parent: Uuid) { - self.parents.insert(element, parent); - } - pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { - self.parents.get(element) - } - pub fn get_parents<'a, I>(&self, input: I) -> HashSet - where - I: Iterator, - { - let iter = input.into_iter(); - let mut result: HashSet = HashSet::new(); - let mut current_generation: Vec = Vec::new(); - for elt in iter { - current_generation.push(*elt) - } - //info!("starts with {}", current_generation.len()); - loop { - if current_generation.is_empty() { - //info!("ends with {}", result.len()); - return result; - } - let mut next_gen: Vec = Vec::new(); - for elt in current_generation.iter() { - if let Some(p) = self.get_parent(elt) { - if result.contains(p) { - //avoid duplicate, redundancy or loop - continue; - } - next_gen.push(*p); - } - } - let to_insert = std::mem::replace(&mut current_generation, next_gen); - result.extend(to_insert); - } - #[allow(unreachable_code)] // sillyness of some tools - { - unreachable!("The loop should always return") - } - } - - pub fn get_active_elements_parents( - &mut self, - categories_and_elements_to_be_loaded: HashSet, - ) { - trace!( - "There are {} active elements", - categories_and_elements_to_be_loaded.len() - ); - - //first merge the parents to iterate overit - let mut parents: HashMap = Default::default(); - for pack in self.packs.values_mut() { - parents.extend(pack.entities_parents.clone()); - } - self.parents = parents; - //then climb up the tree of parent's categories - self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); - } - - fn handle_message(&mut self, msg: MessageToPackageBack) { - //let (b2u_sender, _) = package_manager.channels(); - match msg { - MessageToPackageBack::ActiveFiles(currently_used_files) => { - tracing::trace!("Handling of MessageToPackageBack::ActiveFiles"); - self.set_currently_used_files(currently_used_files); - self.state.choice_of_category_changed = true; - } - MessageToPackageBack::CategoryActivationElementStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationElementStatusChange" - ); - self.category_set(category_uuid, status); - } - MessageToPackageBack::CategoryActivationBranchStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationBranchStatusChange" - ); - self.category_branch_set(category_uuid, status); - } - MessageToPackageBack::CategoryActivationStatusChanged => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationStatusChanged" - ); - self.state.choice_of_category_changed = true; - } - MessageToPackageBack::CategorySetAll(status) => { - tracing::trace!("Handling of MessageToPackageBack::CategorySetAll"); - self.category_set_all(status); - self.state.choice_of_category_changed = true; - } - MessageToPackageBack::DeletePacks(to_delete) => { - tracing::trace!("Handling of MessageToPackageBack::DeletePacks"); - let std_file = std::fs::OpenOptions::new() - .open(&self.marker_packs_path) - .or(Err("Could not open file")) - .unwrap(); - let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); - let mut deleted = Vec::new(); - - for pack_uuid in to_delete { - if let Some(pack) = self.packs.remove(&pack_uuid) { - if let Err(e) = marker_packs_dir.remove_dir_all(&pack.name) { - error!(?e, pack.name, "failed to remove pack"); - } else { - info!("deleted marker pack: {}", pack.name); - deleted.push(pack_uuid); - } - } - } - let _ = self - .channel_sender - .send(MessageToPackageUI::DeletedPacks(deleted).into()); - } - MessageToPackageBack::ImportPack(file_path) => { - tracing::trace!("Handling of MessageToPackageBack::ImportPack"); - let _ = self - .channel_sender - .send(MessageToPackageUI::NbTasksRunning(1).into()); - let start = std::time::SystemTime::now(); - let result = import_pack_from_zip_file_path(file_path, &self.state.extract_path); - let elaspsed = start.elapsed().unwrap_or_default(); - tracing::info!( - "Loading of taco package from disk took {} ms", - elaspsed.as_millis() - ); - match result { - Ok((file_name, pack)) => { - let _ = self - .channel_sender - .send(MessageToPackageUI::ImportedPack(file_name, pack).into()); - } - Err(e) => { - let _ = self - .channel_sender - .send(MessageToPackageUI::ImportFailure(e).into()); - } - } - let _ = self - .channel_sender - .send(MessageToPackageUI::NbTasksRunning(0).into()); - } - MessageToPackageBack::ReloadPack => { - unimplemented!( - "Handling of MessageToPackageBack::ReloadPack has not been implemented yet" - ); - } - MessageToPackageBack::SavePack(name, pack) => { - tracing::trace!("Handling of MessageToPackageBack::SavePack"); - let std_file = std::fs::OpenOptions::new() - .open(&self.marker_packs_path) - .or(Err("Could not open file")) - .unwrap(); - let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); - let name = name.as_str(); - if marker_packs_dir.exists(name) { - match marker_packs_dir.remove_dir_all(name).into_diagnostic() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to delete already existing marker pack"); - } - } - } - if let Err(e) = marker_packs_dir.create_dir_all(name) { - error!(?e, "failed to create directory for pack"); - } - match marker_packs_dir.open_dir(name) { - Ok(dir) => { - let pack_path = self.marker_packs_path.join(name); - let (data_pack, mut texture_pack, mut report) = - build_from_core(name.to_string(), dir.into(), pack_path, pack); - tracing::trace!("Package loaded into data and texture"); - let uuid_of_insertion = self.save(data_pack, report.clone()); - report.uuid = uuid_of_insertion; - texture_pack.uuid = uuid_of_insertion; - let _ = self - .channel_sender - .send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); - } - Err(e) => { - error!( - ?e, - "failed to open marker pack directory to save pack {:?} {}", - self.marker_packs_path, - name - ); - } - }; - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling MessageToPackageBack has not been implemented yet"); - } - } - } - - pub fn flush_all_messages(&mut self) -> PackageBackSharedState { - tracing::trace!( - "choice_of_category_changed: {}", - self.state.choice_of_category_changed - ); - - let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - let msg = bincode::deserialize(&msg).unwrap(); - messages.push(msg); - } - for msg in messages { - self.handle_message(msg); - } - self.state.clone() - } - - pub fn tick( - &mut self, - loop_index: u128, - ms: &MumbleLinkSharedState, - link: Option<&MumbleLink>, - ) { - let mut currently_used_files: BTreeMap = Default::default(); - let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - - let link = if ms.read_ui_link { - ms.copy_of_ui_link.as_ref() - } else { - link - }; - - if let Some(link) = link { - //TODO: how to save/load the active files ? - let mut have_used_files_list_changed = false; - let map_changed = self.current_map_id != link.map_id; - self.current_map_id = link.map_id; - for pack in self.packs.values_mut() { - if let Some(current_map) = pack.maps.get(&link.map_id) { - for marker in current_map.markers.values() { - if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { - currently_used_files.insert( - marker.source_file_uuid, - *self - .currently_used_files - .get(&marker.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } - } - for trail in current_map.trails.values() { - if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { - currently_used_files.insert( - trail.source_file_uuid, - *self - .currently_used_files - .get(&trail.source_file_uuid) - .unwrap_or_else(|| { - have_used_files_list_changed = true; - is_active - }), - ); - } - } - } - } - let tasks = &self.tasks; - for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); - let _ = self - .channel_sender - .send(MessageToPackageUI::NbTasksRunning(tasks.count()).into()); - tasks.save_data(pack, pack.is_dirty()); - pack.tick( - &self.channel_sender, - loop_index, - link, - ¤tly_used_files, - have_used_files_list_changed || self.state.choice_of_category_changed, - map_changed, - tasks, - &mut categories_and_elements_to_be_loaded, - ); - std::mem::drop(span_guard); - } - if map_changed { - self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let _ = self - .channel_sender - .send(MessageToPackageUI::ActiveElements(self.loaded_elements.clone()).into()); - } - if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed - { - //there is no point in sending a new list if nothing changed - let _ = self.channel_sender.send( - MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()).into(), - ); - self.currently_used_files = currently_used_files; - let _ = self - .channel_sender - .send(MessageToPackageUI::TextureSwapChain.into()); - } - } - self.state.choice_of_category_changed = false; - } - - fn delete_packs(&mut self, to_delete: Vec) { - for uuid in to_delete { - self.packs.remove(&uuid); - } - } - pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { - let mut to_delete: Vec = Vec::new(); - for (uuid, pack) in self.packs.iter() { - if pack.name == data_pack.name { - to_delete.push(*uuid); - } - } - self.delete_packs(to_delete); - self.tasks - .save_report(Arc::clone(&data_pack.dir), report, true); - self.tasks.save_data(&mut data_pack, true); - let mut uuid_to_insert = data_pack.uuid; - while self.packs.contains_key(&uuid_to_insert) { - //collision avoidance - trace!( - "Uuid collision detected for {} for package {}", - uuid_to_insert, - data_pack.name - ); - uuid_to_insert = Uuid::new_v4(); - } - data_pack.uuid = uuid_to_insert; - self.packs.insert(uuid_to_insert, data_pack); - uuid_to_insert - } - - pub fn load_all(&mut self) { - once::assert_has_not_been_called!("Early load must happen only once"); - // Called only once at application start. - let _ = self - .channel_sender - .send(MessageToPackageUI::NbTasksRunning(1).into()); - self.tasks.load_all_packs( - Arc::clone(&self.state.root_dir), - self.state.root_path.clone(), - ); - if let Ok((data_packages, texture_packages, report_packages)) = - self.tasks.wait_for_load_all_packs() - { - for (uuid, data_pack) in data_packages { - self.packs.insert(uuid, data_pack); - } - for ((_, texture_pack), (_, report)) in - std::iter::zip(texture_packages, report_packages) - { - let _ = self - .channel_sender - .send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); - } - - let _ = self - .channel_sender - .send(MessageToPackageUI::NbTasksRunning(0).into()); - } - let _ = self - .channel_sender - .send(MessageToPackageUI::FirstLoadDone.into()); - } -} diff --git a/crates/joko_package/src/manager/package_ui.rs b/crates/joko_package/src/manager/package_ui.rs deleted file mode 100644 index 4c07332..0000000 --- a/crates/joko_package/src/manager/package_ui.rs +++ /dev/null @@ -1,771 +0,0 @@ -use std::{ - collections::{BTreeMap, HashSet}, - sync::{Arc, Mutex}, -}; - -use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; -use image::EncodableLayout; -use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; - -use joko_render_models::messages::UIToUIMessage; -use tracing::{info_span, trace}; - -use crate::message::MessageToPackageBack; -use joko_components::{ComponentDataExchange, JokolayUIComponent, PeerComponentChannel}; -use joko_core::{serde_glam::Vec3, RelativePath}; -use joko_link::{MumbleChanges, MumbleLink}; -use miette::Result; -use uuid::Uuid; - -use crate::manager::pack::import::ImportStatus; -use crate::manager::pack::loaded::{LoadedPackTexture, PackTasks}; -use crate::message::MessageToPackageUI; - -//FIXME: there is an interest to merge the PackageUIManager and the render -#[derive(Clone)] -pub struct PackageUISharedState { - list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display - first_load_done: bool, - nb_running_tasks_on_back: i32, // store the number of running tasks in background thread - import_status: Arc>, -} - -#[must_use] -pub struct PackageUIManager { - default_marker_texture: Option, - default_trail_texture: Option, - packs: BTreeMap, - reports: BTreeMap, - tasks: PackTasks, - - currently_used_files: BTreeMap, - all_files_activation_status: bool, // this consume a change of display event - show_only_active: bool, - pack_details: Option, // if filled, display the details of the package - - delayed_marker_texture: Vec<(Uuid, RelativePath, Uuid, Vec3, CommonAttributes)>, - delayed_trail_texture: Vec<(Uuid, RelativePath, Uuid, CommonAttributes)>, - - //egui_context: &'l egui::Context, //TODO: remove, this is not the proper place to be, or if it is, badly used - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - sender_u2u: Option>, - receiver_mumblelink: Option>, - receiver_near_scene: Option>, - state: PackageUISharedState, -} - -impl PackageUIManager { - pub fn new( - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - ) -> Self { - let state = PackageUISharedState { - list_of_textures_changed: false, - first_load_done: false, - nb_running_tasks_on_back: 0, - import_status: Default::default(), - }; - Self { - packs: Default::default(), - tasks: PackTasks::new(), - reports: Default::default(), - default_marker_texture: None, - default_trail_texture: None, - - all_files_activation_status: false, - show_only_active: true, - currently_used_files: Default::default(), // UI copy to (de-)activate files - pack_details: None, - - delayed_marker_texture: Default::default(), - delayed_trail_texture: Default::default(), - channel_sender, - channel_receiver, - sender_u2u: None, - receiver_mumblelink: None, - receiver_near_scene: None, - state, - } - } - - fn handle_message(&mut self, msg: MessageToPackageUI) { - match msg { - MessageToPackageUI::ActiveElements(active_elements) => { - tracing::trace!("Handling of MessageToPackageUI::ActiveElements"); - self.update_active_categories(&active_elements); - } - MessageToPackageUI::CurrentlyUsedFiles(currently_used_files) => { - tracing::trace!("Handling of MessageToPackageUI::CurrentlyUsedFiles"); - self.set_currently_used_files(currently_used_files); - } - MessageToPackageUI::DeletedPacks(to_delete) => { - tracing::trace!("Handling of MessageToPackageUI::DeletedPacks"); - self.delete_packs(to_delete); - } - MessageToPackageUI::FirstLoadDone => { - self.state.first_load_done = true; - } - MessageToPackageUI::ImportedPack(file_name, pack) => { - tracing::trace!("Handling of MessageToPackageUI::ImportedPack"); - *self.state.import_status.lock().unwrap() = - ImportStatus::PackDone(file_name, pack, false); - } - MessageToPackageUI::ImportFailure(message) => { - tracing::trace!("Handling of MessageToPackageUI::ImportFailure"); - *self.state.import_status.lock().unwrap() = - ImportStatus::PackError(miette::Report::msg(message)); - } - MessageToPackageUI::LoadedPack(pack_texture, report) => { - tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); - self.save(pack_texture, report); - self.state.import_status = Default::default(); - let _ = self - .channel_sender - .send(MessageToPackageBack::CategoryActivationStatusChanged.into()); - } - MessageToPackageUI::MarkerTexture( - pack_uuid, - tex_path, - marker_uuid, - position, - common_attributes, - ) => { - tracing::trace!("Handling of MessageToPackageUI::MarkerTexture"); - //FIXME: make it a TODO on tick() - self.delayed_marker_texture.push(( - pack_uuid, - tex_path, - marker_uuid, - position, - common_attributes, - )); - } - MessageToPackageUI::NbTasksRunning(nb_tasks) => { - tracing::trace!("Handling of MessageToPackageUI::NbTasksRunning"); - self.state.nb_running_tasks_on_back = nb_tasks; - } - MessageToPackageUI::PackageActiveElements(pack_uuid, active_elements) => { - tracing::trace!("Handling of MessageToPackageUI::PackageActiveElements"); - self.update_pack_active_categories(pack_uuid, &active_elements); - } - MessageToPackageUI::TextureSwapChain => { - tracing::debug!("Handling of MessageToPackageUI::TextureSwapChain"); - self.swap(); - self.state.list_of_textures_changed = true; - } - MessageToPackageUI::TrailTexture( - pack_uuid, - tex_path, - trail_uuid, - common_attributes, - ) => { - tracing::trace!("Handling of MessageToPackageUI::TrailTexture"); - self.delayed_trail_texture.push(( - pack_uuid, - tex_path, - trail_uuid, - common_attributes, - )); - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling MessageToPackageUI has not been implemented yet"); - } - } - } - - pub fn flush_all_messages(&mut self) -> PackageUISharedState { - if let Ok(mut import_status) = self.state.import_status.lock() { - if let ImportStatus::LoadingPack(file_path) = &mut *import_status { - let _ = self - .channel_sender - .send(MessageToPackageBack::ImportPack(file_path.clone()).into()); - *import_status = ImportStatus::WaitingLoading(file_path.clone()); - } - } - let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - let msg = bincode::deserialize(&msg).unwrap(); - messages.push(msg); - } - for msg in messages { - self.handle_message(msg); - } - self.state.clone() - } - - pub fn late_init(&mut self, egui_context: &egui::Context) { - //TODO: make it even later, at another place - if self.default_marker_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.default_marker_texture = Some(egui_context.load_texture( - "default marker", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - if self.default_trail_texture.is_none() { - let img = - image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.default_trail_texture = Some(egui_context.load_texture( - "default trail", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - }, - )); - } - } - - pub fn delete_packs(&mut self, to_delete: Vec) { - for uuid in to_delete { - self.packs.remove(&uuid); - self.reports.remove(&uuid); - } - } - pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { - self.currently_used_files = currently_used_files; - } - - pub fn update_active_categories(&mut self, active_elements: &HashSet) { - trace!("There are {} active elements", active_elements.len()); - for pack in self.packs.values_mut() { - pack.update_active_categories(active_elements); - } - } - - pub fn update_pack_active_categories( - &mut self, - pack_uuid: Uuid, - active_elements: &HashSet, - ) { - trace!("There are {} active elements", active_elements.len()); - for (uuid, pack) in self.packs.iter_mut() { - if uuid == &pack_uuid { - pack.update_active_categories(active_elements); - break; - } - } - } - pub fn swap(&mut self) { - for pack in self.packs.values_mut() { - pack.swap(); - } - } - - pub fn load_marker_texture( - &mut self, - pack_uuid: Uuid, - egui_context: &egui::Context, - tex_path: RelativePath, - marker_uuid: Uuid, - position: Vec3, - common_attributes: CommonAttributes, - ) { - if let Some(pack) = self.packs.get_mut(&pack_uuid) { - pack.load_marker_texture( - egui_context, - self.default_marker_texture.as_ref().unwrap(), - &tex_path, - marker_uuid, - position, - common_attributes, - ); - }; - } - pub fn load_trail_texture( - &mut self, - pack_uuid: Uuid, - egui_context: &egui::Context, - tex_path: RelativePath, - trail_uuid: Uuid, - common_attributes: CommonAttributes, - ) { - if let Some(pack) = self.packs.get_mut(&pack_uuid) { - pack.load_trail_texture( - egui_context, - self.default_trail_texture.as_ref().unwrap(), - &tex_path, - trail_uuid, - common_attributes, - ); - }; - } - - fn pack_importer(import_status: Arc>) { - //called when a new pack is imported - rayon::spawn(move || { - *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; - - if let Some(file_path) = rfd::FileDialog::new() - .add_filter("taco", &["zip", "taco"]) - .pick_file() - { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); - } else { - *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); - } - }); - } - - fn category_set_all(&mut self, status: bool) { - for pack in self.packs.values_mut() { - pack.category_set_all(status); - } - } - - pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { - let tasks = &self.tasks; - let sender_u2u = self.sender_u2u.as_ref().unwrap(); - for pack in self.packs.values_mut() { - tasks.save_texture(pack, pack.is_dirty()); - } - if link.changes.contains(MumbleChanges::Position) - || link.changes.contains(MumbleChanges::Map) - { - for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); - pack.tick(&sender_u2u, timestamp, link, z_near, tasks); - std::mem::drop(span_guard); - } - let _ = sender_u2u.send(UIToUIMessage::RenderSwapChain.into()); - } - Ok(()) - } - - pub fn menu_ui( - &mut self, - ui: &mut egui::Ui, - nb_running_tasks_on_back: i32, - nb_running_tasks_on_network: i32, - ) { - ui.menu_button("Markers", |ui| { - if self.show_only_active { - if ui.button("Show everything").clicked() { - self.show_only_active = false; - } - } else if ui.button("Show only active").clicked() { - self.show_only_active = true; - } - if ui.button("Activate all elements").clicked() { - self.category_set_all(true); - let _ = self - .channel_sender - .send(MessageToPackageBack::CategorySetAll(true).into()); - } - if ui.button("Deactivate all elements").clicked() { - self.category_set_all(false); - let _ = self - .channel_sender - .send(MessageToPackageBack::CategorySetAll(false).into()); - } - - for (pack, import_quality_report) in - std::iter::zip(self.packs.values_mut(), self.reports.values()) - { - //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; - //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - let u2u_sender = self.sender_u2u.as_ref().unwrap(); - pack.category_sub_menu( - &self.channel_sender, - &u2u_sender, - ui, - self.show_only_active, - import_quality_report, - ); - } - }); - if self.tasks.is_running() - || nb_running_tasks_on_back > 0 - || nb_running_tasks_on_network > 0 - { - let sp = egui::Spinner::new() - .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); - ui.add(sp); - } - } - pub fn status_as_color( - &self, - nb_running_tasks_on_back: i32, - nb_running_tasks_on_network: i32, - ) -> egui::Color32 { - //we can choose whatever color code we want to focus on load, save, network queries, anything. - let nb_running_tasks_on_ui = self.tasks.count(); - //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 - let color_ui = if nb_running_tasks_on_ui > 0 { - let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; - let res = nb_ui_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - let color_back = if nb_running_tasks_on_back > 0 { - let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; - let res = nb_bask_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - let color_network = if nb_running_tasks_on_network > 0 { - let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; - let res = nb_network_tasks * 0x80; - res + 0x7f - } else { - 0 - }; - - egui::Color32::from_rgb(color_ui, color_back, color_network) - } - - fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool) { - let mut files_changed = false; - Window::new("File Manager") - .open(open) - .show(etx, |ui| -> Result<()> { - egui::ScrollArea::vertical().show(ui, |ui| { - egui::Grid::new("link grid") - .num_columns(4) - .striped(true) - .show(ui, |ui| { - let mut all_files_toggle = false; - ui.horizontal(|ui| { - if ui.button("activate all").clicked() { - self.all_files_activation_status = true; - all_files_toggle = true; - files_changed = true; - } - if ui.button("deactivate all").clicked() { - self.all_files_activation_status = false; - all_files_toggle = true; - files_changed = true; - } - }); - //ui.label("Trails"); - //ui.label("Markers"); - ui.end_row(); - - for pack in self.packs.values_mut() { - //TODO: first loop to list what is active per pack, to not display all packs - let report = self.reports.get(&pack.uuid).unwrap(); - let mut pack_files_toggle = false; - let mut pack_files_activation_status = true; - ui.horizontal(|ui| { - ui.label(&pack.name); - if ui.button("activate all").clicked() { - pack_files_activation_status = true; - pack_files_toggle = true; - files_changed = true; - } - if ui.button("deactivate all").clicked() { - pack_files_activation_status = false; - pack_files_toggle = true; - files_changed = true; - } - }); - ui.end_row(); - for source_file_uuid in pack.source_files.keys() { - if let Some(is_selected) = - self.currently_used_files.get_mut(source_file_uuid) - { - if all_files_toggle { - *is_selected = self.all_files_activation_status; - } - if pack_files_toggle { - *is_selected = pack_files_activation_status; - } - ui.add_space(3.0); - //reports may be corrupted or not loaded, files are there - if let Some(source_file_name) = - report.source_file_uuid_to_name(source_file_uuid) - { - //format the file from reports and packages + prefix with the package name - let cb = ui.checkbox( - is_selected, - format!("{}: {}", pack.name, source_file_name), - ); - if cb.changed() { - files_changed = true; - } - } else { - // Import report is corrupted, only print reference - let cb = ui.checkbox( - is_selected, - format!("{}: {}", pack.name, source_file_uuid), - ); - if cb.changed() { - files_changed = true; - } - } - ui.end_row(); - } - } - } - ui.end_row(); - }) - }); - Ok(()) - }); - if files_changed { - let _ = self - .channel_sender - .send(MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()).into()); - } - } - - fn gui_package_details(&mut self, ui: &mut Ui, uuid: Uuid) { - // protection against deletion while displaying details - if let Some(pack) = self.packs.get(&uuid) { - if let Some(report) = self.reports.get(&uuid) { - let collapsing = - CollapsingHeader::new(format!("Last load details of package {}", pack.name)); - let header_response = collapsing - .open(Some(true)) - .show(ui, |ui| { - egui::Grid::new("packs details") - .striped(true) - .show(ui, |ui| { - let number_of = &report.number_of; - ui.label("categories"); - ui.label(format!("{}", number_of.categories)); - ui.end_row(); - ui.label("missing_categories"); - ui.label(format!("{}", number_of.missing_categories)); - ui.end_row(); - ui.label("textures"); - ui.label(format!("{}", number_of.textures)); - ui.end_row(); - ui.label("missing_textures"); - ui.label(format!("{}", number_of.missing_textures)); - ui.end_row(); - ui.label("entities"); - ui.label(format!("{}", number_of.entities)); - ui.end_row(); - ui.label("markers"); - ui.label(format!("{}", number_of.markers)); - ui.end_row(); - ui.label("trails"); - ui.label(format!("{}", number_of.trails)); - ui.end_row(); - ui.label("routes"); - ui.label(format!("{}", number_of.routes)); - ui.end_row(); - ui.label("maps"); - ui.label(format!("{}", number_of.maps)); - ui.end_row(); - ui.label("source_files"); - ui.label(format!("{}", number_of.source_files)); - ui.end_row(); - }) - }) - .header_response; - if header_response.clicked() { - self.pack_details = None; - } - } else { - self.pack_details = None; - } - } else { - self.pack_details = None; - } - } - fn gui_package_list( - &mut self, - etx: &egui::Context, - import_status: &Arc>, - open: &mut bool, - first_load_done: bool, - ) { - Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { - CollapsingHeader::new("Loaded Packs").show(ui, |ui| { - egui::Grid::new("packs").striped(true).show(ui, |ui| { - if !first_load_done { - ui.label("Loading in progress..."); - } - let mut to_delete = vec![]; - for pack in self.packs.values() { - ui.label(pack.name.clone()); - if ui.button("delete").clicked() { - to_delete.push(pack.uuid); - } - if ui.button("Details").clicked() { - self.pack_details = Some(pack.uuid); - } - if ui.button("Export").clicked() { - //TODO - } - ui.end_row(); - } - if !to_delete.is_empty() { - let _ = self.channel_sender.send(MessageToPackageBack::DeletePacks(to_delete).into()); - } - }); - }); - if let Some(uuid) = self.pack_details { - self.gui_package_details(ui, uuid); - } else if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - Self::pack_importer(Arc::clone(import_status)); - } - //ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { - ui.label(format!("pack is being imported from {p:?}")); - } - ImportStatus::PackDone(name, pack, saved) => { - if *saved { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); - } else { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - if ui.button("save").clicked() { - let _ = self.channel_sender.send(MessageToPackageBack::SavePack(name.clone(), pack.clone()).into()); - } - } - } - ImportStatus::PackError(e) => { - let error_msg = format!("failed to import pack due to error: {e:#?}"); - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - *status = ImportStatus::UnInitialized; - } - ui.colored_label( - egui::Color32::RED, - error_msg, - ); - } - } - } - - Ok(()) - }); - } - pub fn gui( - &mut self, - etx: &egui::Context, - is_marker_open: &mut bool, - import_status: &Arc>, - is_file_open: &mut bool, - first_load_done: bool, - ) { - self.gui_package_list(etx, import_status, is_marker_open, first_load_done); - self.gui_file_manager(etx, is_file_open); - } - - pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { - /* - We save in a file with the name of the package, while we keep track of it from a uuid point of view. - It means we can have duplicates unless package with same name is deleted. - */ - let mut to_delete: Vec = Vec::new(); - for (uuid, pack) in self.packs.iter() { - if pack.name == texture_pack.name { - to_delete.push(*uuid); - } - } - self.delete_packs(to_delete); - self.tasks.save_texture(&mut texture_pack, true); - self.packs.insert(texture_pack.uuid, texture_pack); - self.reports.insert(report.uuid, report); - } -} - -//TODO: there is a need for a more complex input according to deps -impl JokolayUIComponent for PackageUIManager { - fn flush_all_messages(&mut self) -> PackageUISharedState { - let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - let msg = bincode::deserialize(&msg).unwrap(); - messages.push(msg); - } - for msg in messages { - self.handle_message(msg); - } - self.state.clone() - } - - fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> Option<&()> { - let raw_link = self - .receiver_mumblelink - .as_mut() - .unwrap() - .blocking_recv() - .unwrap(); - let link: &MumbleLink = &bincode::deserialize(&raw_link).unwrap(); - - for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in - std::mem::take(&mut self.delayed_marker_texture) - { - self.load_marker_texture( - pack_uuid, - egui_context, - tex_path, - marker_uuid, - position, - common_attributes, - ); - } - for (pack_uuid, tex_path, trail_uuid, common_attributes) in - std::mem::take(&mut self.delayed_trail_texture) - { - self.load_trail_texture( - pack_uuid, - egui_context, - tex_path, - trail_uuid, - common_attributes, - ); - } - - let raw_z_near = self - .receiver_near_scene - .as_mut() - .unwrap() - .blocking_recv() - .unwrap(); - let z_near: f32 = bincode::deserialize(&raw_z_near).unwrap(); - let _ = self._tick(timestamp, link, z_near); - None - } - fn bind( - &mut self, - mut deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - mut _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - mut _input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - mut notify: std::collections::HashMap< - u32, - tokio::sync::mpsc::Sender, - >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - self.sender_u2u = notify.remove(&0); - self.receiver_mumblelink = deps.remove(&0); - self.receiver_near_scene = deps.remove(&1); - unimplemented!("PackageUIManager component binding is not implemented") - } -} diff --git a/crates/joko_package/src/message.rs b/crates/joko_package/src/message.rs deleted file mode 100644 index dd3e117..0000000 --- a/crates/joko_package/src/message.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::{BTreeMap, HashSet}; - -use joko_components::ComponentDataExchange; -use joko_package_models::{ - attributes::CommonAttributes, - package::{PackCore, PackageImportReport}, -}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use joko_core::{serde_glam::Vec3, RelativePath}; -use joko_render_models::{marker::MarkerObject, trail::TrailObject}; - -use crate::LoadedPackTexture; - -#[derive(Serialize, Deserialize)] -pub enum MessageToPackageUI { - ActiveElements(HashSet), //list of all elements that are loaded for current map - CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display - LoadedPack(LoadedPackTexture, PackageImportReport), //push a loaded pack to UI - DeletedPacks(Vec), //push a deleted set of packs to UI - FirstLoadDone, - ImportedPack(String, PackCore), - ImportFailure(String), - MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), - //MumbleLink(Option), - //MumbleLinkChanged,//tell there is a need to resize - NbTasksRunning(i32), //tell the number of taks running in background - PackageActiveElements(Uuid, HashSet), // first is the package reference, second is the list of active elements in the package. - TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain - TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), -} - -impl From for ComponentDataExchange { - fn from(src: MessageToPackageUI) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} - -#[derive(Serialize, Deserialize)] -pub enum MessageToPackageBack { - ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. - CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. - CategoryActivationBranchStatusChange(Uuid, bool), //same, for a whole branch - CategoryActivationStatusChanged, //something happened that needs to reload the whole set - CategorySetAll(bool), //signal all categories should be now at this status - DeletePacks(Vec), //uuid of the pack to delete - ImportPack(std::path::PathBuf), - ReloadPack, - SavePack(String, PackCore), -} - -impl From for ComponentDataExchange { - fn from(src: MessageToPackageBack) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} diff --git a/crates/joko_package/vendor/rapid/license.txt b/crates/joko_package/vendor/rapid/license.txt deleted file mode 100644 index e5ecd23..0000000 --- a/crates/joko_package/vendor/rapid/license.txt +++ /dev/null @@ -1,52 +0,0 @@ -Use of this software is granted under one of the following two licenses, -to be chosen freely by the user. - -1. Boost Software License - Version 1.0 - August 17th, 2003 -=============================================================================== - -Copyright (c) 2006, 2007 Marcin Kalicinski - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -2. The MIT License -=============================================================================== - -Copyright (c) 2006, 2007 Marcin Kalicinski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/crates/joko_package/vendor/rapid/rapid.cpp b/crates/joko_package/vendor/rapid/rapid.cpp deleted file mode 100644 index 7c0aceb..0000000 --- a/crates/joko_package/vendor/rapid/rapid.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "joko_package/vendor/rapid/rapid.hpp" -#include "joko_package/vendor/rapid/rapidxml.hpp" -#include "joko_package/vendor/rapid/rapidxml_print.hpp" -#include "joko_package/src/lib.rs.h" -#include -#include -#include -#include -void remove_duplicate_nodes(rapidxml::xml_node *node) -{ - - std::set duplicates; - rapidxml::xml_attribute *attr = node->first_attribute(); - while (attr) - { - std::string name(attr->name(), attr->name_size()); - if (duplicates.count(name) == 1) - { - rapidxml::xml_attribute *prev = attr; - attr = attr->next_attribute(); - node->remove_attribute(prev); - } - else - { - duplicates.insert(name); - attr = attr->next_attribute(); - } - } - for (rapidxml::xml_node *child = node->first_node(); child; child = child->next_sibling()) - { - remove_duplicate_nodes(child); - } -} - -namespace rapid -{ - - rust::String rapid_filter(rust::String src_xml) - { - // return std::string(src_xml); - std::string src = static_cast(src_xml); - std::string dst; - using namespace rapidxml; - // create document - xml_document doc; - // rapid xml throws exception if there's a parsing error - try - { - // parse the xml text. if there's exceptions we go to catch block from here - doc.parse<0>((char *)src.c_str()); - // delete all the duplicate attributes, so that there's no obvious errors for rust deserializers - for (rapidxml::xml_node *child = doc.first_node(); child; child = child->next_sibling()) - { - remove_duplicate_nodes(child); - } - std::ostringstream oss; - oss << doc; - dst = oss.str(); - } - catch (const parse_error &e) - { - return ""; - } - return dst; - } -} diff --git a/crates/joko_package/vendor/rapid/rapid.hpp b/crates/joko_package/vendor/rapid/rapid.hpp deleted file mode 100644 index 3652609..0000000 --- a/crates/joko_package/vendor/rapid/rapid.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include "joko_package/src/lib.rs.h" -#include "rust/cxx.h" - -namespace rapid { - rust::String rapid_filter(rust::String src_xml); -} \ No newline at end of file diff --git a/crates/joko_package/vendor/rapid/rapidxml.hpp b/crates/joko_package/vendor/rapid/rapidxml.hpp deleted file mode 100644 index d025eef..0000000 --- a/crates/joko_package/vendor/rapid/rapidxml.hpp +++ /dev/null @@ -1,2645 +0,0 @@ -#ifndef RAPIDXML_HPP_INCLUDED -#define RAPIDXML_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/13 01:46:17 $ -//! \file rapidxml.hpp This file contains rapidxml parser and DOM implementation - -// If standard library is disabled, user must provide implementations of required functions and typedefs -#if !defined(RAPIDXML_NO_STDLIB) - #include // For std::size_t - #include // (Optional.) For std::strlen, ... - #include // (Optional.) For std::wcslen, ... - #include // For assert - #include // For placement new -#endif - -// RAPIDXML_NOEXCEPT: Expands to 'noexcept' on supported compilers. -#if !defined(RAPIDXML_NOEXCEPT) -# if !defined(RAPIDXML_DISABLE_NOEXCEPT) -# if defined(__clang__) -# if __has_feature(__cxx_noexcept__) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# elif defined(__GNUC__) -# if ((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4) -# if defined(__GXX_EXPERIMENTAL_CXX0X__) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# endif -# elif defined(_MSC_VER) && (_MSC_VER >= 1900) -# define RAPIDXML_NOEXCEPT noexcept(true) -# endif -# endif -# if !defined(RAPIDXML_NOEXCEPT) -# define RAPIDXML_NOEXCEPT -# endif -#endif - -// On MSVC, disable "conditional expression is constant" warning (level 4). -// This warning is almost impossible to avoid with certain types of templated code -#ifdef _MSC_VER - #pragma warning(push) - #pragma warning(disable:4127) // Conditional expression is constant -#endif - -/////////////////////////////////////////////////////////////////////////// -// RAPIDXML_PARSE_ERROR - -#if defined(RAPIDXML_NO_EXCEPTIONS) - -#define RAPIDXML_PARSE_ERROR(what, where) { parse_error_handler(what, where); assert(0); } - -namespace rapidxml -{ - //! When exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, - //! this function is called to notify user about the error. - //! It must be defined by the user. - //!

- //! This function cannot return. If it does, the results are undefined. - //!

- //! A very simple definition might look like that: - //!
-    //! void %rapidxml::%parse_error_handler(const char *what, void *where)
-    //! {
-    //!     std::cout << "Parse error: " << what << "\n";
-    //!     std::abort();
-    //! }
-    //! 
- //! \param what Human readable description of the error. - //! \param where Pointer to character data where error was detected. - void parse_error_handler(const char *what, void *where); -} - -#else - -#include // For std::exception - -#define RAPIDXML_PARSE_ERROR(what, where) throw parse_error(what, where) - -namespace rapidxml -{ - - //! Parse error exception. - //! This exception is thrown by the parser when an error occurs. - //! Use what() function to get human-readable error message. - //! Use where() function to get a pointer to position within source text where error was detected. - //!

- //! If throwing exceptions by the parser is undesirable, - //! it can be disabled by defining RAPIDXML_NO_EXCEPTIONS macro before rapidxml.hpp is included. - //! This will cause the parser to call rapidxml::parse_error_handler() function instead of throwing an exception. - //! This function must be defined by the user. - //!

- //! This class derives from std::exception class. - class parse_error: public std::exception - { - public: - //! Constructs parse error - parse_error(const char *what, void *where) - : m_what(what) - , m_where(where) - { - } - - //! Gets human readable description of error. - //! \return Pointer to null terminated description of the error. - virtual const char *what() const throw() - { - return m_what; - } - - //! Gets pointer to character data where error happened. - //! Ch should be the same as char type of xml_document that produced the error. - //! \return Pointer to location within the parsed string where error occured. - template - Ch *where() const - { - return reinterpret_cast(m_where); - } - - private: - const char *m_what; - void *m_where; - }; -} - -#endif - -/////////////////////////////////////////////////////////////////////////// -// Pool sizes - -#ifndef RAPIDXML_STATIC_POOL_SIZE - // Size of static memory block of memory_pool. - // Define RAPIDXML_STATIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. - // No dynamic memory allocations are performed by memory_pool until static memory is exhausted. - #define RAPIDXML_STATIC_POOL_SIZE (64 * 1024) -#endif - -#ifndef RAPIDXML_DYNAMIC_POOL_SIZE - // Size of dynamic memory block of memory_pool. - // Define RAPIDXML_DYNAMIC_POOL_SIZE before including rapidxml.hpp if you want to override the default value. - // After the static block is exhausted, dynamic blocks with approximately this size are allocated by memory_pool. - #define RAPIDXML_DYNAMIC_POOL_SIZE (64 * 1024) -#endif - -#ifndef RAPIDXML_ALIGNMENT - // Memory allocation alignment. - // Define RAPIDXML_ALIGNMENT before including rapidxml.hpp if you want to override the default value, which is the size of pointer. - // All memory allocations for nodes, attributes and strings will be aligned to this value. - // This must be a power of 2 and at least 1, otherwise memory_pool will not work. - #define RAPIDXML_ALIGNMENT sizeof(void *) -#endif - -namespace rapidxml -{ - // Forward declarations - template class xml_node; - template class xml_attribute; - template class xml_document; - - //! Enumeration listing all node types produced by the parser. - //! Use xml_node::type() function to query node type. - enum node_type - { - node_document, //!< A document node. Name and value are empty. - node_element, //!< An element node. Name contains element name. Value contains text of first data node. - node_data, //!< A data node. Name is empty. Value contains data text. - node_cdata, //!< A CDATA node. Name is empty. Value contains data text. - node_comment, //!< A comment node. Name is empty. Value contains comment text. - node_declaration, //!< A declaration node. Name and value are empty. Declaration parameters (version, encoding and standalone) are in node attributes. - node_doctype, //!< A DOCTYPE node. Name is empty. Value contains DOCTYPE text. - node_pi //!< A PI node. Name contains target. Value contains instructions. - }; - - /////////////////////////////////////////////////////////////////////// - // Parsing flags - - //! Parse flag instructing the parser to not create data nodes. - //! Text of first data node will still be placed in value of parent element, unless rapidxml::parse_no_element_values flag is also specified. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_data_nodes = 0x1; - - //! Parse flag instructing the parser to not use text of first data node as a value of parent element. - //! Can be combined with other flags by use of | operator. - //! Note that child data nodes of element node take precendence over its value when printing. - //! That is, if element has one or more child data nodes and a value, the value will be ignored. - //! Use rapidxml::parse_no_data_nodes flag to prevent creation of data nodes if you want to manipulate data using values of elements. - //!

- //! See xml_document::parse() function. - const int parse_no_element_values = 0x2; - - //! Parse flag instructing the parser to not place zero terminators after strings in the source text. - //! By default zero terminators are placed, modifying source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_string_terminators = 0x4; - - //! Parse flag instructing the parser to not translate entities in the source text. - //! By default entities are translated, modifying source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_entity_translation = 0x8; - - //! Parse flag instructing the parser to disable UTF-8 handling and assume plain 8 bit characters. - //! By default, UTF-8 handling is enabled. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_no_utf8 = 0x10; - - //! Parse flag instructing the parser to create XML declaration node. - //! By default, declaration node is not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_declaration_node = 0x20; - - //! Parse flag instructing the parser to create comments nodes. - //! By default, comment nodes are not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_comment_nodes = 0x40; - - //! Parse flag instructing the parser to create DOCTYPE node. - //! By default, doctype node is not created. - //! Although W3C specification allows at most one DOCTYPE node, RapidXml will silently accept documents with more than one. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_doctype_node = 0x80; - - //! Parse flag instructing the parser to create PI nodes. - //! By default, PI nodes are not created. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_pi_nodes = 0x100; - - //! Parse flag instructing the parser to validate closing tag names. - //! If not set, name inside closing tag is irrelevant to the parser. - //! By default, closing tags are not validated. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_validate_closing_tags = 0x200; - - //! Parse flag instructing the parser to trim all leading and trailing whitespace of data nodes. - //! By default, whitespace is not trimmed. - //! This flag does not cause the parser to modify source text. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_trim_whitespace = 0x400; - - //! Parse flag instructing the parser to condense all whitespace runs of data nodes to a single space character. - //! Trimming of leading and trailing whitespace of data is controlled by rapidxml::parse_trim_whitespace flag. - //! By default, whitespace is not normalized. - //! If this flag is specified, source text will be modified. - //! Can be combined with other flags by use of | operator. - //!

- //! See xml_document::parse() function. - const int parse_normalize_whitespace = 0x800; - - // Compound flags - - //! Parse flags which represent default behaviour of the parser. - //! This is always equal to 0, so that all other flags can be simply ored together. - //! Normally there is no need to inconveniently disable flags by anding with their negated (~) values. - //! This also means that meaning of each flag is a negation of the default setting. - //! For example, if flag name is rapidxml::parse_no_utf8, it means that utf-8 is enabled by default, - //! and using the flag will disable it. - //!

- //! See xml_document::parse() function. - const int parse_default = 0; - - //! A combination of parse flags that forbids any modifications of the source text. - //! This also results in faster parsing. However, note that the following will occur: - //!
    - //!
  • names and values of nodes will not be zero terminated, you have to use xml_base::name_size() and xml_base::value_size() functions to determine where name and value ends
  • - //!
  • entities will not be translated
  • - //!
  • whitespace will not be normalized
  • - //!
- //! See xml_document::parse() function. - const int parse_non_destructive = parse_no_string_terminators | parse_no_entity_translation; - - //! A combination of parse flags resulting in fastest possible parsing, without sacrificing important data. - //!

- //! See xml_document::parse() function. - const int parse_fastest = parse_non_destructive | parse_no_data_nodes; - - //! A combination of parse flags resulting in largest amount of data being extracted. - //! This usually results in slowest parsing. - //!

- //! See xml_document::parse() function. - const int parse_full = parse_declaration_node | parse_comment_nodes | parse_doctype_node | parse_pi_nodes | parse_validate_closing_tags; - - /////////////////////////////////////////////////////////////////////// - // Internals - - //! \cond internal - namespace internal - { - - // Struct that contains lookup tables for the parser - // It must be a template to allow correct linking (because it has static data members, which are defined in a header file). - template - struct lookup_tables - { - static const unsigned char lookup_whitespace[256]; // Whitespace table - static const unsigned char lookup_node_name[256]; // Node name table - static const unsigned char lookup_text[256]; // Text table - static const unsigned char lookup_text_pure_no_ws[256]; // Text table - static const unsigned char lookup_text_pure_with_ws[256]; // Text table - static const unsigned char lookup_attribute_name[256]; // Attribute name table - static const unsigned char lookup_attribute_data_1[256]; // Attribute data table with single quote - static const unsigned char lookup_attribute_data_1_pure[256]; // Attribute data table with single quote - static const unsigned char lookup_attribute_data_2[256]; // Attribute data table with double quotes - static const unsigned char lookup_attribute_data_2_pure[256]; // Attribute data table with double quotes - static const unsigned char lookup_digits[256]; // Digits - static const unsigned char lookup_upcase[256]; // To uppercase conversion table for ASCII characters - }; - - // Find length of the string - template - inline std::size_t measure(const Ch *p) RAPIDXML_NOEXCEPT - { - const Ch *tmp = p; - while (*tmp) - ++tmp; - return tmp - p; - } - -#if !defined(RAPIDXML_NO_STDLIB) - inline std::size_t measure(const char* p) RAPIDXML_NOEXCEPT - { return std::strlen(p); } - - inline std::size_t measure(const wchar_t* p) RAPIDXML_NOEXCEPT - { return std::wcslen(p); } -#endif - - // Compare strings for equality - template - inline bool compare(const Ch *p1, std::size_t size1, const Ch *p2, - std::size_t size2, bool case_sensitive) RAPIDXML_NOEXCEPT - { - if (size1 != size2) - return false; - if (case_sensitive) - { - for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) - if (*p1 != *p2) - return false; - } - else - { - for (const Ch *end = p1 + size1; p1 < end; ++p1, ++p2) - if (lookup_tables<0>::lookup_upcase[static_cast(*p1)] != lookup_tables<0>::lookup_upcase[static_cast(*p2)]) - return false; - } - return true; - } - } - //! \endcond - - /////////////////////////////////////////////////////////////////////// - // Memory pool - - //! This class is used by the parser to create new nodes and attributes, without overheads of dynamic memory allocation. - //! In most cases, you will not need to use this class directly. - //! However, if you need to create nodes manually or modify names/values of nodes, - //! you are encouraged to use memory_pool of relevant xml_document to allocate the memory. - //! Not only is this faster than allocating them by using new operator, - //! but also their lifetime will be tied to the lifetime of document, - //! possibly simplyfing memory management. - //!

- //! Call allocate_node() or allocate_attribute() functions to obtain new nodes or attributes from the pool. - //! You can also call allocate_string() function to allocate strings. - //! Such strings can then be used as names or values of nodes without worrying about their lifetime. - //! Note that there is no free() function -- all allocations are freed at once when clear() function is called, - //! or when the pool is destroyed. - //!

- //! It is also possible to create a standalone memory_pool, and use it - //! to allocate nodes, whose lifetime will not be tied to any document. - //!

- //! Pool maintains RAPIDXML_STATIC_POOL_SIZE bytes of statically allocated memory. - //! Until static memory is exhausted, no dynamic memory allocations are done. - //! When static memory is exhausted, pool allocates additional blocks of memory of size RAPIDXML_DYNAMIC_POOL_SIZE each, - //! by using global new[] and delete[] operators. - //! This behaviour can be changed by setting custom allocation routines. - //! Use set_allocator() function to set them. - //!

- //! Allocations for nodes, attributes and strings are aligned at RAPIDXML_ALIGNMENT bytes. - //! This value defaults to the size of pointer on target architecture. - //!

- //! To obtain absolutely top performance from the parser, - //! it is important that all nodes are allocated from a single, contiguous block of memory. - //! Otherwise, cache misses when jumping between two (or more) disjoint blocks of memory can slow down parsing quite considerably. - //! If required, you can tweak RAPIDXML_STATIC_POOL_SIZE, RAPIDXML_DYNAMIC_POOL_SIZE and RAPIDXML_ALIGNMENT - //! to obtain best wasted memory to performance compromise. - //! To do it, define their values before rapidxml.hpp file is included. - //! \param Ch Character type of created nodes. - template - class memory_pool - { - - public: - - //! \cond internal - typedef void *(alloc_func)(std::size_t); // Type of user-defined function used to allocate memory - typedef void (free_func)(void *); // Type of user-defined function used to free memory - //! \endcond - - //! Constructs empty pool with default allocator functions. - memory_pool() RAPIDXML_NOEXCEPT - : m_alloc_func(0) - , m_free_func(0) - { - init(); - } - - //! Destroys pool and frees all the memory. - //! This causes memory occupied by nodes allocated by the pool to be freed. - //! Nodes allocated from the pool are no longer valid. - ~memory_pool() - { - clear(); - } - - //! Allocates a new node from the pool, and optionally assigns name and value to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param type Type of node to create. - //! \param name Name to assign to the node, or 0 to assign no name. - //! \param value Value to assign to the node, or 0 to assign no value. - //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. - //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. - //! \return Pointer to allocated node. This pointer will never be NULL. - xml_node *allocate_node(node_type type, - const Ch *name = 0, const Ch *value = 0, - std::size_t name_size = 0, std::size_t value_size = 0) - { - void *memory = allocate_aligned(sizeof(xml_node)); - xml_node *node = new(memory) xml_node(type); - if (name) - { - if (name_size > 0) - node->name(name, name_size); - else - node->name(name); - } - if (value) - { - if (value_size > 0) - node->value(value, value_size); - else - node->value(value); - } - return node; - } - - //! Allocates a new attribute from the pool, and optionally assigns name and value to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param name Name to assign to the attribute, or 0 to assign no name. - //! \param value Value to assign to the attribute, or 0 to assign no value. - //! \param name_size Size of name to assign, or 0 to automatically calculate size from name string. - //! \param value_size Size of value to assign, or 0 to automatically calculate size from value string. - //! \return Pointer to allocated attribute. This pointer will never be NULL. - xml_attribute *allocate_attribute(const Ch *name = 0, const Ch *value = 0, - std::size_t name_size = 0, std::size_t value_size = 0) - { - void *memory = allocate_aligned(sizeof(xml_attribute)); - xml_attribute *attribute = new(memory) xml_attribute; - if (name) - { - if (name_size > 0) - attribute->name(name, name_size); - else - attribute->name(name); - } - if (value) - { - if (value_size > 0) - attribute->value(value, value_size); - else - attribute->value(value); - } - return attribute; - } - - //! Allocates a char array of given size from the pool, and optionally copies a given string to it. - //! If the allocation request cannot be accomodated, this function will throw std::bad_alloc. - //! If exceptions are disabled by defining RAPIDXML_NO_EXCEPTIONS, this function - //! will call rapidxml::parse_error_handler() function. - //! \param source String to initialize the allocated memory with, or 0 to not initialize it. - //! \param size Number of characters to allocate, or zero to calculate it automatically from source string length; if size is 0, source string must be specified and null terminated. - //! \return Pointer to allocated char array. This pointer will never be NULL. - Ch *allocate_string(const Ch *source = 0, std::size_t size = 0) - { - assert(source || size); // Either source or size (or both) must be specified - if (size == 0) - size = internal::measure(source) + 1; - Ch *result = static_cast(allocate_aligned(size * sizeof(Ch))); - if (source) - for (std::size_t i = 0; i < size; ++i) - result[i] = source[i]; - return result; - } - - //! Clones an xml_node and its hierarchy of child nodes and attributes. - //! Nodes and attributes are allocated from this memory pool. - //! Names and values are not cloned, they are shared between the clone and the source. - //! Result node can be optionally specified as a second parameter, - //! in which case its contents will be replaced with cloned source node. - //! This is useful when you want to clone entire document. - //! \param source Node to clone. - //! \param result Node to put results in, or 0 to automatically allocate result node - //! \return Pointer to cloned node. This pointer will never be NULL. - xml_node *clone_node(const xml_node *source, xml_node *result = 0) - { - // Prepare result node - if (result) - { - result->remove_all_attributes(); - result->remove_all_nodes(); - result->type(source->type()); - } - else - result = allocate_node(source->type()); - - // Clone name and value - result->name(source->name(), source->name_size()); - result->value(source->value(), source->value_size()); - - // Clone child nodes and attributes - for (xml_node *child = source->first_node(); child; child = child->next_sibling()) - result->append_node(clone_node(child)); - for (xml_attribute *attr = source->first_attribute(); attr; attr = attr->next_attribute()) - result->append_attribute(allocate_attribute(attr->name(), attr->value(), attr->name_size(), attr->value_size())); - - return result; - } - - //! Clears the pool. - //! This causes memory occupied by nodes allocated by the pool to be freed. - //! Any nodes or strings allocated from the pool will no longer be valid. - void clear() RAPIDXML_NOEXCEPT - { - while (m_begin != m_static_memory) - { - char *previous_begin = reinterpret_cast
(align(m_begin))->previous_begin; - if (m_free_func) - m_free_func(m_begin); - else - delete[] m_begin; - m_begin = previous_begin; - } - init(); - } - - //! Sets or resets the user-defined memory allocation functions for the pool. - //! This can only be called when no memory is allocated from the pool yet, otherwise results are undefined. - //! Allocation function must not return invalid pointer on failure. It should either throw, - //! stop the program, or use longjmp() function to pass control to other place of program. - //! If it returns invalid pointer, results are undefined. - //!

- //! User defined allocation functions must have the following forms: - //!
- //!
void *allocate(std::size_t size); - //!
void free(void *pointer); - //!

- //! \param af Allocation function, or 0 to restore default function - //! \param ff Free function, or 0 to restore default function - void set_allocator(alloc_func *af, free_func *ff) - { - assert(m_begin == m_static_memory && m_ptr == align(m_begin)); // Verify that no memory is allocated yet - m_alloc_func = af; - m_free_func = ff; - } - - private: - - struct header - { - char *previous_begin; - }; - - void init() RAPIDXML_NOEXCEPT - { - m_begin = m_static_memory; - m_ptr = align(m_begin); - m_end = m_static_memory + sizeof(m_static_memory); - } - - char *align(char *ptr) const RAPIDXML_NOEXCEPT - { - std::size_t alignment = ((RAPIDXML_ALIGNMENT - (std::size_t(ptr) & (RAPIDXML_ALIGNMENT - 1))) & (RAPIDXML_ALIGNMENT - 1)); - return ptr + alignment; - } - - char *allocate_raw(std::size_t size) - { - // Allocate - void *memory; - if (m_alloc_func) // Allocate memory using either user-specified allocation function or global operator new[] - { - memory = m_alloc_func(size); - assert(memory); // Allocator is not allowed to return 0, on failure it must either throw, stop the program or use longjmp - } - else - { - memory = new char[size]; -#ifdef RAPIDXML_NO_EXCEPTIONS - if (!memory) // If exceptions are disabled, verify memory allocation, because new will not be able to throw bad_alloc - RAPIDXML_PARSE_ERROR("out of memory", 0); -#endif - } - return static_cast(memory); - } - - void *allocate_aligned(std::size_t size) - { - // Calculate aligned pointer - char *result = align(m_ptr); - - // If not enough memory left in current pool, allocate a new pool - if (result + size > m_end) - { - // Calculate required pool size (may be bigger than RAPIDXML_DYNAMIC_POOL_SIZE) - std::size_t pool_size = RAPIDXML_DYNAMIC_POOL_SIZE; - if (pool_size < size) - pool_size = size; - - // Allocate - std::size_t alloc_size = sizeof(header) + (2 * RAPIDXML_ALIGNMENT - 2) + pool_size; // 2 alignments required in worst case: one for header, one for actual allocation - char *raw_memory = allocate_raw(alloc_size); - - // Setup new pool in allocated memory - char *pool = align(raw_memory); - header *new_header = reinterpret_cast
(pool); - new_header->previous_begin = m_begin; - m_begin = raw_memory; - m_ptr = pool + sizeof(header); - m_end = raw_memory + alloc_size; - - // Calculate aligned pointer again using new pool - result = align(m_ptr); - } - - // Update pool and return aligned pointer - m_ptr = result + size; - return result; - } - - char *m_begin; // Start of raw memory making up current pool - char *m_ptr; // First free byte in current pool - char *m_end; // One past last available byte in current pool - char m_static_memory[RAPIDXML_STATIC_POOL_SIZE]; // Static raw memory - alloc_func *m_alloc_func; // Allocator function, or 0 if default is to be used - free_func *m_free_func; // Free function, or 0 if default is to be used - }; - - /////////////////////////////////////////////////////////////////////////// - // XML base - - //! Base class for xml_node and xml_attribute implementing common functions: - //! name(), name_size(), value(), value_size() and parent(). - //! \param Ch Character type to use - template - class xml_base - { - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - // Construct a base with empty name, value and parent - xml_base() RAPIDXML_NOEXCEPT - : m_name(0) - , m_value(0) - , m_parent(0) - , m_offset(0) - { - } - - /////////////////////////////////////////////////////////////////////////// - // Node data access - - //! Gets name of the node. - //! Interpretation of name depends on type of node. - //! Note that name will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. - //!

- //! Use name_size() function to determine length of the name. - //! \return Name of node, or empty string if node has no name. - Ch *name() const RAPIDXML_NOEXCEPT - { - return m_name ? m_name : nullstr(); - } - - //! Gets size of node name, not including terminator character. - //! This function works correctly irrespective of whether name is or is not zero terminated. - //! \return Size of node name, in characters. - std::size_t name_size() const RAPIDXML_NOEXCEPT - { - return m_name ? m_name_size : 0; - } - - //! Gets value of node. - //! Interpretation of value depends on type of node. - //! Note that value will not be zero-terminated if rapidxml::parse_no_string_terminators option was selected during parse. - //!

- //! Use value_size() function to determine length of the value. - //! \return Value of node, or empty string if node has no value. - Ch *value() const RAPIDXML_NOEXCEPT - { - return m_value ? m_value : nullstr(); - } - - //! Gets size of node value, not including terminator character. - //! This function works correctly irrespective of whether value is or is not zero terminated. - //! \return Size of node value, in characters. - std::size_t value_size() const RAPIDXML_NOEXCEPT - { - return m_value ? m_value_size : 0; - } - - //! Get the start offset of this node inside the source string. - Ch *offset() const RAPIDXML_NOEXCEPT - { - return m_offset; - } - - /////////////////////////////////////////////////////////////////////////// - // Node modification - - //! Sets name of node to a non zero-terminated string. - //! See \ref ownership_of_strings. - //!

- //! Note that node does not own its name or value, it only stores a pointer to it. - //! It will not delete or otherwise free the pointer on destruction. - //! It is reponsibility of the user to properly manage lifetime of the string. - //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - - //! on destruction of the document the string will be automatically freed. - //!

- //! Size of name must be specified separately, because name does not have to be zero terminated. - //! Use name(const Ch *) function to have the length automatically calculated (string must be zero terminated). - //! \param name Name of node to set. Does not have to be zero terminated. - //! \param size Size of name, in characters. This does not include zero terminator, if one is present. - void name(const Ch *name, std::size_t size) RAPIDXML_NOEXCEPT - { - m_name = const_cast(name); - m_name_size = size; - } - - //! Sets name of node to a zero-terminated string. - //! See also \ref ownership_of_strings and xml_node::name(const Ch *, std::size_t). - //! \param name Name of node to set. Must be zero terminated. - void name(const Ch *name) RAPIDXML_NOEXCEPT - { - this->name(name, internal::measure(name)); - } - - //! Sets value of node to a non zero-terminated string. - //! See \ref ownership_of_strings. - //!

- //! Note that node does not own its name or value, it only stores a pointer to it. - //! It will not delete or otherwise free the pointer on destruction. - //! It is reponsibility of the user to properly manage lifetime of the string. - //! The easiest way to achieve it is to use memory_pool of the document to allocate the string - - //! on destruction of the document the string will be automatically freed. - //!

- //! Size of value must be specified separately, because it does not have to be zero terminated. - //! Use value(const Ch *) function to have the length automatically calculated (string must be zero terminated). - //!

- //! If an element has a child node of type node_data, it will take precedence over element value when printing. - //! If you want to manipulate data of elements using values, use parser flag rapidxml::parse_no_data_nodes to prevent creation of data nodes by the parser. - //! \param value value of node to set. Does not have to be zero terminated. - //! \param size Size of value, in characters. This does not include zero terminator, if one is present. - void value(const Ch *value, std::size_t size) RAPIDXML_NOEXCEPT - { - m_value = const_cast(value); - m_value_size = size; - } - - //! Sets value of node to a zero-terminated string. - //! See also \ref ownership_of_strings and xml_node::value(const Ch *, std::size_t). - //! \param value Vame of node to set. Must be zero terminated. - void value(const Ch *value) RAPIDXML_NOEXCEPT - { - this->value(value, internal::measure(value)); - } - - //! Sets the offset inside the source string. - //! This is only intended for debugging purposes. - void offset(Ch *offset) RAPIDXML_NOEXCEPT - { - m_offset = offset; - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets node parent. - //! \return Pointer to parent node, or 0 if there is no parent. - xml_node *parent() const RAPIDXML_NOEXCEPT - { - return m_parent; - } - - protected: - - // Return empty string - static Ch *nullstr() RAPIDXML_NOEXCEPT - { - static Ch zero = Ch('\0'); - return &zero; - } - - Ch *m_name; // Name of node, or 0 if no name - Ch *m_value; // Value of node, or 0 if no value - std::size_t m_name_size; // Length of node name, or undefined of no name - std::size_t m_value_size; // Length of node value, or undefined if no value - xml_node *m_parent; // Pointer to parent node, or 0 if none - Ch *m_offset; // Start offset of this node inside the string - }; - - //! Class representing attribute node of XML document. - //! Each attribute has name and value strings, which are available through name() and value() functions (inherited from xml_base). - //! Note that after parse, both name and value of attribute will point to interior of source text used for parsing. - //! Thus, this text must persist in memory for the lifetime of attribute. - //! \param Ch Character type to use. - template - class xml_attribute: public xml_base - { - - friend class xml_node; - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - //! Constructs an empty attribute with the specified type. - //! Consider using memory_pool of appropriate xml_document if allocating attributes manually. - xml_attribute() - { - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets document of which attribute is a child. - //! \return Pointer to document that contains this attribute, or 0 if there is no parent document. - xml_document *document() const - { - if (xml_node *node = this->parent()) - { - while (node->parent()) - node = node->parent(); - return node->type() == node_document ? static_cast *>(node) : 0; - } - else - return 0; - } - - //! Gets previous attribute, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return previous attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *previous_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_prev_attribute; attribute; attribute = attribute->m_prev_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return this->m_parent ? m_prev_attribute : 0; - } - - //! Gets next attribute, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return next attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *next_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_next_attribute; attribute; attribute = attribute->m_next_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return this->m_parent ? m_next_attribute : 0; - } - - private: - - xml_attribute *m_prev_attribute; // Pointer to previous sibling of attribute, or 0 if none; only valid if parent is non-zero - xml_attribute *m_next_attribute; // Pointer to next sibling of attribute, or 0 if none; only valid if parent is non-zero - - }; - - /////////////////////////////////////////////////////////////////////////// - // XML node - - //! Class representing a node of XML document. - //! Each node may have associated name and value strings, which are available through name() and value() functions. - //! Interpretation of name and value depends on type of the node. - //! Type of node can be determined by using type() function. - //!

- //! Note that after parse, both name and value of node, if any, will point interior of source text used for parsing. - //! Thus, this text must persist in the memory for the lifetime of node. - //! \param Ch Character type to use. - template - class xml_node: public xml_base - { - - public: - - /////////////////////////////////////////////////////////////////////////// - // Construction & destruction - - //! Constructs an empty node with the specified type. - //! Consider using memory_pool of appropriate document to allocate nodes manually. - //! \param type Type of node to construct. - xml_node(node_type type) - : m_type(type) - , m_first_node(0) - , m_first_attribute(0) - { - } - - /////////////////////////////////////////////////////////////////////////// - // Node data access - - //! Gets type of node. - //! \return Type of node. - node_type type() const - { - return m_type; - } - - /////////////////////////////////////////////////////////////////////////// - // Related nodes access - - //! Gets document of which node is a child. - //! \return Pointer to document that contains this node, or 0 if there is no parent document. - xml_document *document() const - { - xml_node *node = const_cast *>(this); - while (node->parent()) - node = node->parent(); - return node->type() == node_document ? static_cast *>(node) : 0; - } - - //! Gets first child node, optionally matching node name. - //! \param name Name of child to find, or 0 to return first child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found child, or 0 if not found. - xml_node *first_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *child = m_first_node; child; child = child->next_sibling()) - if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) - return child; - return 0; - } - else - return m_first_node; - } - - //! Gets last child node, optionally matching node name. - //! Behaviour is undefined if node has no children. - //! Use first_node() to test if node has children. - //! \param name Name of child to find, or 0 to return last child regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found child, or 0 if not found. - xml_node *last_node(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(m_first_node); // Cannot query for last child if node has no children - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *child = m_last_node; child; child = child->previous_sibling()) - if (internal::compare(child->name(), child->name_size(), name, name_size, case_sensitive)) - return child; - return 0; - } - else - return m_last_node; - } - - //! Gets previous sibling node, optionally matching node name. - //! Behaviour is undefined if node has no parent. - //! Use parent() to test if node has a parent. - //! \param name Name of sibling to find, or 0 to return previous sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found sibling, or 0 if not found. - xml_node *previous_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(this->m_parent); // Cannot query for siblings if node has no parent - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *sibling = m_prev_sibling; sibling; sibling = sibling->m_prev_sibling) - if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) - return sibling; - return 0; - } - else - return m_prev_sibling; - } - - //! Gets next sibling node, optionally matching node name. - //! Behaviour is undefined if node has no parent. - //! Use parent() to test if node has a parent. - //! \param name Name of sibling to find, or 0 to return next sibling regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found sibling, or 0 if not found. - xml_node *next_sibling(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - assert(this->m_parent); // Cannot query for siblings if node has no parent - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_node *sibling = m_next_sibling; sibling; sibling = sibling->m_next_sibling) - if (internal::compare(sibling->name(), sibling->name_size(), name, name_size, case_sensitive)) - return sibling; - return 0; - } - else - return m_next_sibling; - } - - //! Gets first attribute of node, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return first attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *first_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_first_attribute; attribute; attribute = attribute->m_next_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return m_first_attribute; - } - - //! Gets last attribute of node, optionally matching attribute name. - //! \param name Name of attribute to find, or 0 to return last attribute regardless of its name; this string doesn't have to be zero-terminated if name_size is non-zero - //! \param name_size Size of name, in characters, or 0 to have size calculated automatically from string - //! \param case_sensitive Should name comparison be case-sensitive; non case-sensitive comparison works properly only for ASCII characters - //! \return Pointer to found attribute, or 0 if not found. - xml_attribute *last_attribute(const Ch *name = 0, std::size_t name_size = 0, bool case_sensitive = true) const - { - if (name) - { - if (name_size == 0) - name_size = internal::measure(name); - for (xml_attribute *attribute = m_last_attribute; attribute; attribute = attribute->m_prev_attribute) - if (internal::compare(attribute->name(), attribute->name_size(), name, name_size, case_sensitive)) - return attribute; - return 0; - } - else - return m_first_attribute ? m_last_attribute : 0; - } - - /////////////////////////////////////////////////////////////////////////// - // Node modification - - //! Sets type of node. - //! \param type Type of node to set. - void type(node_type type) - { - m_type = type; - } - - /////////////////////////////////////////////////////////////////////////// - // Node manipulation - - //! Prepends a new child node. - //! The prepended child becomes the first child, and all existing children are moved one position back. - //! \param child Node to prepend. - void prepend_node(xml_node *child) - { - assert(child && !child->parent() && child->type() != node_document); - if (first_node()) - { - child->m_next_sibling = m_first_node; - m_first_node->m_prev_sibling = child; - } - else - { - child->m_next_sibling = 0; - m_last_node = child; - } - m_first_node = child; - child->m_parent = this; - child->m_prev_sibling = 0; - } - - //! Appends a new child node. - //! The appended child becomes the last child. - //! \param child Node to append. - void append_node(xml_node *child) - { - assert(child && !child->parent() && child->type() != node_document); - if (first_node()) - { - child->m_prev_sibling = m_last_node; - m_last_node->m_next_sibling = child; - } - else - { - child->m_prev_sibling = 0; - m_first_node = child; - } - m_last_node = child; - child->m_parent = this; - child->m_next_sibling = 0; - } - - //! Inserts a new child node at specified place inside the node. - //! All children after and including the specified node are moved one position back. - //! \param where Place where to insert the child, or 0 to insert at the back. - //! \param child Node to insert. - void insert_node(xml_node *where, xml_node *child) - { - assert(!where || where->parent() == this); - assert(child && !child->parent() && child->type() != node_document); - if (where == m_first_node) - prepend_node(child); - else if (where == 0) - append_node(child); - else - { - child->m_prev_sibling = where->m_prev_sibling; - child->m_next_sibling = where; - where->m_prev_sibling->m_next_sibling = child; - where->m_prev_sibling = child; - child->m_parent = this; - } - } - - //! Removes first child node. - //! If node has no children, behaviour is undefined. - //! Use first_node() to test if node has children. - void remove_first_node() - { - assert(first_node()); - xml_node *child = m_first_node; - m_first_node = child->m_next_sibling; - if (child->m_next_sibling) - child->m_next_sibling->m_prev_sibling = 0; - else - m_last_node = 0; - child->m_parent = 0; - } - - //! Removes last child of the node. - //! If node has no children, behaviour is undefined. - //! Use first_node() to test if node has children. - void remove_last_node() - { - assert(first_node()); - xml_node *child = m_last_node; - if (child->m_prev_sibling) - { - m_last_node = child->m_prev_sibling; - child->m_prev_sibling->m_next_sibling = 0; - } - else - m_first_node = 0; - child->m_parent = 0; - } - - //! Removes specified child from the node - // \param where Pointer to child to be removed. - void remove_node(xml_node *where) - { - assert(where && where->parent() == this); - assert(first_node()); - if (where == m_first_node) - remove_first_node(); - else if (where == m_last_node) - remove_last_node(); - else - { - where->m_prev_sibling->m_next_sibling = where->m_next_sibling; - where->m_next_sibling->m_prev_sibling = where->m_prev_sibling; - where->m_parent = 0; - } - } - - //! Removes all child nodes (but not attributes). - void remove_all_nodes() - { - for (xml_node *node = first_node(); node; node = node->m_next_sibling) - node->m_parent = 0; - m_first_node = 0; - } - - //! Prepends a new attribute to the node. - //! \param attribute Attribute to prepend. - void prepend_attribute(xml_attribute *attribute) - { - assert(attribute && !attribute->parent()); - if (first_attribute()) - { - attribute->m_next_attribute = m_first_attribute; - m_first_attribute->m_prev_attribute = attribute; - } - else - { - attribute->m_next_attribute = 0; - m_last_attribute = attribute; - } - m_first_attribute = attribute; - attribute->m_parent = this; - attribute->m_prev_attribute = 0; - } - - //! Appends a new attribute to the node. - //! \param attribute Attribute to append. - void append_attribute(xml_attribute *attribute) - { - assert(attribute && !attribute->parent()); - if (first_attribute()) - { - attribute->m_prev_attribute = m_last_attribute; - m_last_attribute->m_next_attribute = attribute; - } - else - { - attribute->m_prev_attribute = 0; - m_first_attribute = attribute; - } - m_last_attribute = attribute; - attribute->m_parent = this; - attribute->m_next_attribute = 0; - } - - //! Inserts a new attribute at specified place inside the node. - //! All attributes after and including the specified attribute are moved one position back. - //! \param where Place where to insert the attribute, or 0 to insert at the back. - //! \param attribute Attribute to insert. - void insert_attribute(xml_attribute *where, xml_attribute *attribute) - { - assert(!where || where->parent() == this); - assert(attribute && !attribute->parent()); - if (where == m_first_attribute) - prepend_attribute(attribute); - else if (where == 0) - append_attribute(attribute); - else - { - attribute->m_prev_attribute = where->m_prev_attribute; - attribute->m_next_attribute = where; - where->m_prev_attribute->m_next_attribute = attribute; - where->m_prev_attribute = attribute; - attribute->m_parent = this; - } - } - - //! Removes first attribute of the node. - //! If node has no attributes, behaviour is undefined. - //! Use first_attribute() to test if node has attributes. - void remove_first_attribute() - { - assert(first_attribute()); - xml_attribute *attribute = m_first_attribute; - if (attribute->m_next_attribute) - { - attribute->m_next_attribute->m_prev_attribute = 0; - } - else - m_last_attribute = 0; - attribute->m_parent = 0; - m_first_attribute = attribute->m_next_attribute; - } - - //! Removes last attribute of the node. - //! If node has no attributes, behaviour is undefined. - //! Use first_attribute() to test if node has attributes. - void remove_last_attribute() - { - assert(first_attribute()); - xml_attribute *attribute = m_last_attribute; - if (attribute->m_prev_attribute) - { - attribute->m_prev_attribute->m_next_attribute = 0; - m_last_attribute = attribute->m_prev_attribute; - } - else - m_first_attribute = 0; - attribute->m_parent = 0; - } - - //! Removes specified attribute from node. - //! \param where Pointer to attribute to be removed. - void remove_attribute(xml_attribute *where) - { - assert(first_attribute() && where->parent() == this); - if (where == m_first_attribute) - remove_first_attribute(); - else if (where == m_last_attribute) - remove_last_attribute(); - else - { - where->m_prev_attribute->m_next_attribute = where->m_next_attribute; - where->m_next_attribute->m_prev_attribute = where->m_prev_attribute; - where->m_parent = 0; - } - } - - //! Removes all attributes of node. - void remove_all_attributes() - { - for (xml_attribute *attribute = first_attribute(); attribute; attribute = attribute->m_next_attribute) - attribute->m_parent = 0; - m_first_attribute = 0; - } - - private: - - /////////////////////////////////////////////////////////////////////////// - // Restrictions - - // No copying - xml_node(const xml_node &); - void operator =(const xml_node &); - - /////////////////////////////////////////////////////////////////////////// - // Data members - - // Note that some of the pointers below have UNDEFINED values if certain other pointers are 0. - // This is required for maximum performance, as it allows the parser to omit initialization of - // unneded/redundant values. - // - // The rules are as follows: - // 1. first_node and first_attribute contain valid pointers, or 0 if node has no children/attributes respectively - // 2. last_node and last_attribute are valid only if node has at least one child/attribute respectively, otherwise they contain garbage - // 3. prev_sibling and next_sibling are valid only if node has a parent, otherwise they contain garbage - - node_type m_type; // Type of node; always valid - xml_node *m_first_node; // Pointer to first child node, or 0 if none; always valid - xml_node *m_last_node; // Pointer to last child node, or 0 if none; this value is only valid if m_first_node is non-zero - xml_attribute *m_first_attribute; // Pointer to first attribute of node, or 0 if none; always valid - xml_attribute *m_last_attribute; // Pointer to last attribute of node, or 0 if none; this value is only valid if m_first_attribute is non-zero - xml_node *m_prev_sibling; // Pointer to previous sibling of node, or 0 if none; this value is only valid if m_parent is non-zero - xml_node *m_next_sibling; // Pointer to next sibling of node, or 0 if none; this value is only valid if m_parent is non-zero - - }; - - /////////////////////////////////////////////////////////////////////////// - // XML document - - //! This class represents root of the DOM hierarchy. - //! It is also an xml_node and a memory_pool through public inheritance. - //! Use parse() function to build a DOM tree from a zero-terminated XML text string. - //! parse() function allocates memory for nodes and attributes by using functions of xml_document, - //! which are inherited from memory_pool. - //! To access root node of the document, use the document itself, as if it was an xml_node. - //! \param Ch Character type to use. - template - class xml_document: public xml_node, public memory_pool - { - - public: - - //! Constructs empty XML document - xml_document() - : xml_node(node_document) - { - } - - //! Parses zero-terminated XML string according to given flags. - //! Passed string will be modified by the parser, unless rapidxml::parse_non_destructive flag is used. - //! The string must persist for the lifetime of the document. - //! In case of error, rapidxml::parse_error exception will be thrown. - //!

- //! If you want to parse contents of a file, you must first load the file into the memory, and pass pointer to its beginning. - //! Make sure that data is zero-terminated. - //!

- //! Document can be parsed into multiple times. - //! Each new call to parse removes previous nodes and attributes (if any), but does not clear memory pool. - //! \param text XML data to parse; pointer is non-const to denote fact that this data may be modified by the parser. - template - void parse(Ch *text) - { - assert(text); - - // Remove current contents - this->remove_all_nodes(); - this->remove_all_attributes(); - - // Parse BOM, if any - parse_bom(text); - - // Parse children - while (1) - { - // Skip whitespace before node - skip(text); - if (*text == 0) - break; - - // Parse and append new child - if (*text == Ch('<')) - { - ++text; // Skip '<' - if (xml_node *node = parse_node(text)) - this->append_node(node); - } - else - RAPIDXML_PARSE_ERROR("expected <", text); - } - - } - - //! Clears the document by deleting all nodes and clearing the memory pool. - //! All nodes owned by document pool are destroyed. - void clear() - { - this->remove_all_nodes(); - this->remove_all_attributes(); - memory_pool::clear(); - } - - private: - - /////////////////////////////////////////////////////////////////////// - // Internal character utility functions - - // Detect whitespace character - struct whitespace_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_whitespace[static_cast(ch)]; - } - }; - - // Detect node name character - struct node_name_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_node_name[static_cast(ch)]; - } - }; - - // Detect attribute name character - struct attribute_name_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_attribute_name[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) - struct text_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) that does not require processing - struct text_pure_no_ws_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text_pure_no_ws[static_cast(ch)]; - } - }; - - // Detect text character (PCDATA) that does not require processing - struct text_pure_with_ws_pred - { - static unsigned char test(Ch ch) - { - return internal::lookup_tables<0>::lookup_text_pure_with_ws[static_cast(ch)]; - } - }; - - // Detect attribute value character - template - struct attribute_value_pred - { - static unsigned char test(Ch ch) - { - if (Quote == Ch('\'')) - return internal::lookup_tables<0>::lookup_attribute_data_1[static_cast(ch)]; - if (Quote == Ch('\"')) - return internal::lookup_tables<0>::lookup_attribute_data_2[static_cast(ch)]; - return 0; // Should never be executed, to avoid warnings on Comeau - } - }; - - // Detect attribute value character - template - struct attribute_value_pure_pred - { - static unsigned char test(Ch ch) - { - if (Quote == Ch('\'')) - return internal::lookup_tables<0>::lookup_attribute_data_1_pure[static_cast(ch)]; - if (Quote == Ch('\"')) - return internal::lookup_tables<0>::lookup_attribute_data_2_pure[static_cast(ch)]; - return 0; // Should never be executed, to avoid warnings on Comeau - } - }; - - // Insert coded character, using UTF8 or 8-bit ASCII - template - static void insert_coded_character(Ch *&text, unsigned long code) - { - if (Flags & parse_no_utf8) - { - // Insert 8-bit ASCII character - // Todo: possibly verify that code is less than 256 and use replacement char otherwise? - text[0] = static_cast(code); - text += 1; - } - else - { - // Insert UTF8 sequence - if (code < 0x80) // 1 byte sequence - { - text[0] = static_cast(code); - text += 1; - } - else if (code < 0x800) // 2 byte sequence - { - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xC0); - text += 2; - } - else if (code < 0x10000) // 3 byte sequence - { - text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xE0); - text += 3; - } - else if (code < 0x110000) // 4 byte sequence - { - text[3] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[2] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[1] = static_cast((code | 0x80) & 0xBF); code >>= 6; - text[0] = static_cast(code | 0xF0); - text += 4; - } - else // Invalid, only codes up to 0x10FFFF are allowed in Unicode - { - RAPIDXML_PARSE_ERROR("invalid numeric character entity", text); - } - } - } - - // Skip characters until predicate evaluates to true - template - static void skip(Ch *&text) - { - Ch *tmp = text; - while (StopPred::test(*tmp)) - ++tmp; - text = tmp; - } - - // Skip characters until predicate evaluates to true while doing the following: - // - replacing XML character entity references with proper characters (' & " < > &#...;) - // - condensing whitespace sequences to single space character - template - static Ch *skip_and_expand_character_refs(Ch *&text) - { - // If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip - if (Flags & parse_no_entity_translation && - !(Flags & parse_normalize_whitespace) && - !(Flags & parse_trim_whitespace)) - { - skip(text); - return text; - } - - // Use simple skip until first modification is detected - skip(text); - - // Use translation skip - Ch *src = text; - Ch *dest = src; - while (StopPred::test(*src)) - { - // If entity translation is enabled - if (!(Flags & parse_no_entity_translation)) - { - // Test if replacement is needed - if (src[0] == Ch('&')) - { - switch (src[1]) - { - - // & ' - case Ch('a'): - if (src[2] == Ch('m') && src[3] == Ch('p') && src[4] == Ch(';')) - { - *dest = Ch('&'); - ++dest; - src += 5; - continue; - } - if (src[2] == Ch('p') && src[3] == Ch('o') && src[4] == Ch('s') && src[5] == Ch(';')) - { - *dest = Ch('\''); - ++dest; - src += 6; - continue; - } - break; - - // " - case Ch('q'): - if (src[2] == Ch('u') && src[3] == Ch('o') && src[4] == Ch('t') && src[5] == Ch(';')) - { - *dest = Ch('"'); - ++dest; - src += 6; - continue; - } - break; - - // > - case Ch('g'): - if (src[2] == Ch('t') && src[3] == Ch(';')) - { - *dest = Ch('>'); - ++dest; - src += 4; - continue; - } - break; - - // < - case Ch('l'): - if (src[2] == Ch('t') && src[3] == Ch(';')) - { - *dest = Ch('<'); - ++dest; - src += 4; - continue; - } - break; - - // &#...; - assumes ASCII - case Ch('#'): - if (src[2] == Ch('x')) - { - unsigned long code = 0; - src += 3; // Skip &#x - while (1) - { - unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; - if (digit == 0xFF) - break; - code = code * 16 + digit; - ++src; - } - insert_coded_character(dest, code); // Put character in output - } - else - { - unsigned long code = 0; - src += 2; // Skip &# - while (1) - { - unsigned char digit = internal::lookup_tables<0>::lookup_digits[static_cast(*src)]; - if (digit == 0xFF) - break; - code = code * 10 + digit; - ++src; - } - insert_coded_character(dest, code); // Put character in output - } - if (*src == Ch(';')) - ++src; - else - RAPIDXML_PARSE_ERROR("expected ;", src); - continue; - - // Something else - default: - // Ignore, just copy '&' verbatim - break; - - } - } - } - - // If whitespace condensing is enabled - if (Flags & parse_normalize_whitespace) - { - // Test if condensing is needed - if (whitespace_pred::test(*src)) - { - *dest = Ch(' '); ++dest; // Put single space in dest - ++src; // Skip first whitespace char - // Skip remaining whitespace chars - while (whitespace_pred::test(*src)) - ++src; - continue; - } - } - - // No replacement, only copy character - *dest++ = *src++; - - } - - // Return new end - text = src; - return dest; - - } - - /////////////////////////////////////////////////////////////////////// - // Internal parsing functions - - // Parse BOM, if any - template - void parse_bom(Ch *&text) - { - // UTF-8? - if (static_cast(text[0]) == 0xEF && - static_cast(text[1]) == 0xBB && - static_cast(text[2]) == 0xBF) - { - text += 3; // Skup utf-8 bom - } - } - - // Parse XML declaration ( - xml_node *parse_xml_declaration(Ch *&text) - { - // If parsing of declaration is disabled - if (!(Flags & parse_declaration_node)) - { - // Skip until end of declaration - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 2; // Skip '?>' - return 0; - } - - // Create declaration - xml_node *declaration = this->allocate_node(node_declaration); - declaration->offset(text); - - // Skip whitespace before attributes or ?> - skip(text); - - // Parse declaration attributes - parse_node_attributes(text, declaration); - - // Skip ?> - if (text[0] != Ch('?') || text[1] != Ch('>')) - RAPIDXML_PARSE_ERROR("expected ?>", text); - text += 2; - - return declaration; - } - - // Parse XML comment (' - return 0; // Do not produce comment node - } - - // Remember value start - Ch *value = text; - - // Skip until end of comment - while (text[0] != Ch('-') || text[1] != Ch('-') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Create comment node - xml_node *comment = this->allocate_node(node_comment); - comment->offset(value); - comment->value(value, text - value); - - // Place zero terminator after comment value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 3; // Skip '-->' - return comment; - } - - // Parse DOCTYPE - template - xml_node *parse_doctype(Ch *&text) - { - // Remember value start - Ch *value = text; - - // Skip to > - while (*text != Ch('>')) - { - // Determine character type - switch (*text) - { - - // If '[' encountered, scan for matching ending ']' using naive algorithm with depth - // This works for all W3C test files except for 2 most wicked - case Ch('['): - { - ++text; // Skip '[' - int depth = 1; - while (depth > 0) - { - switch (*text) - { - case Ch('['): ++depth; break; - case Ch(']'): --depth; break; - case 0: RAPIDXML_PARSE_ERROR("unexpected end of data", text); - } - ++text; - } - break; - } - - // Error on end of text - case Ch('\0'): - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - - // Other character, skip it - default: - ++text; - - } - } - - // If DOCTYPE nodes enabled - if (Flags & parse_doctype_node) - { - // Create a new doctype node - xml_node *doctype = this->allocate_node(node_doctype); - doctype->offset(value); - doctype->value(value, text - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 1; // skip '>' - return doctype; - } - else - { - text += 1; // skip '>' - return 0; - } - - } - - // Parse PI - template - xml_node *parse_pi(Ch *&text) - { - // If creation of PI nodes is enabled - if (Flags & parse_pi_nodes) - { - // Create pi node - xml_node *pi = this->allocate_node(node_pi); - pi->offset(text); - - // Extract PI target name - Ch *name = text; - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected PI target", text); - pi->name(name, text - name); - - // Skip whitespace between pi target and pi - skip(text); - - // Remember start of pi - Ch *value = text; - - // Skip to '?>' - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (*text == Ch('\0')) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Set pi value (verbatim, no entity expansion or whitespace normalization) - pi->value(value, text - value); - - // Place zero terminator after name and value - if (!(Flags & parse_no_string_terminators)) - { - pi->name()[pi->name_size()] = Ch('\0'); - pi->value()[pi->value_size()] = Ch('\0'); - } - - text += 2; // Skip '?>' - return pi; - } - else - { - // Skip to '?>' - while (text[0] != Ch('?') || text[1] != Ch('>')) - { - if (*text == Ch('\0')) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 2; // Skip '?>' - return 0; - } - } - - // Parse and append data - // Return character that ends data. - // This is necessary because this character might have been overwritten by a terminating 0 - template - Ch parse_and_append_data(xml_node *node, Ch *&text, Ch *contents_start) - { - // Backup to contents start if whitespace trimming is disabled - if (!(Flags & parse_trim_whitespace)) - text = contents_start; - - // Skip until end of data - Ch *value = text, *end; - if (Flags & parse_normalize_whitespace) - end = skip_and_expand_character_refs(text); - else - end = skip_and_expand_character_refs(text); - - // Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after > - if (Flags & parse_trim_whitespace) - { - if (Flags & parse_normalize_whitespace) - { - // Whitespace is already condensed to single space characters by skipping function, so just trim 1 char off the end - if (*(end - 1) == Ch(' ')) - --end; - } - else - { - // Backup until non-whitespace character is found - while (whitespace_pred::test(*(end - 1))) - --end; - } - } - - // If characters are still left between end and value (this test is only necessary if normalization is enabled) - // Create new data node - if (!(Flags & parse_no_data_nodes)) - { - xml_node *data = this->allocate_node(node_data); - data->value(value, end - value); - node->append_node(data); - } - - // Add data to parent node if no data exists yet - if (!(Flags & parse_no_element_values)) - if (*node->value() == Ch('\0')) - node->value(value, end - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - { - Ch ch = *text; - *end = Ch('\0'); - return ch; // Return character that ends data; this is required because zero terminator overwritten it - } - - // Return character that ends data - return *text; - } - - // Parse CDATA - template - xml_node *parse_cdata(Ch *&text) - { - // If CDATA is disabled - if (Flags & parse_no_data_nodes) - { - // Skip until end of cdata - while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - text += 3; // Skip ]]> - return 0; // Do not produce CDATA node - } - - // Skip until end of cdata - Ch *value = text; - while (text[0] != Ch(']') || text[1] != Ch(']') || text[2] != Ch('>')) - { - if (!text[0]) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - - // Create new cdata node - xml_node *cdata = this->allocate_node(node_cdata); - cdata->offset(value); - cdata->value(value, text - value); - - // Place zero terminator after value - if (!(Flags & parse_no_string_terminators)) - *text = Ch('\0'); - - text += 3; // Skip ]]> - return cdata; - } - - // Parse element node - template - xml_node *parse_element(Ch *&text) - { - // Create element node - xml_node *element = this->allocate_node(node_element); - element->offset(text); - - // Extract element name - Ch *name = text; - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected element name", text); - element->name(name, text - name); - - // Skip whitespace between element name and attributes or > - skip(text); - - // Parse attributes, if any - parse_node_attributes(text, element); - - // Determine ending type - if (*text == Ch('>')) - { - ++text; - parse_node_contents(text, element); - } - else if (*text == Ch('/')) - { - ++text; - if (*text != Ch('>')) - RAPIDXML_PARSE_ERROR("expected >", text); - ++text; - } - else - RAPIDXML_PARSE_ERROR("expected >", text); - - // Place zero terminator after name - if (!(Flags & parse_no_string_terminators)) - element->name()[element->name_size()] = Ch('\0'); - - // Return parsed element - return element; - } - - // Determine node type, and parse it - template - xml_node *parse_node(Ch *&text) - { - // Parse proper node type - switch (text[0]) - { - - // <... - default: - // Parse and append element node - return parse_element(text); - - // (text); - } - else - { - // Parse PI - return parse_pi(text); - } - - // (text); - } - break; - - // (text); - } - break; - - // (text); - } - - } // switch - - // Attempt to skip other, unrecognized node types starting with ')) - { - if (*text == 0) - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - ++text; - } - ++text; // Skip '>' - return 0; // No node recognized - - } - } - - // Parse contents of the node - children, data etc. - template - void parse_node_contents(Ch *&text, xml_node *node) - { - // For all children and text - while (1) - { - // Skip whitespace between > and node contents - Ch *contents_start = text; // Store start of node contents before whitespace is skipped - skip(text); - Ch next_char = *text; - - // After data nodes, instead of continuing the loop, control jumps here. - // This is because zero termination inside parse_and_append_data() function - // would wreak havoc with the above code. - // Also, skipping whitespace after data nodes is unnecessary. - after_data_node: - - // Determine what comes next: node closing, child node, data node, or 0? - switch (next_char) - { - - // Node closing or child node - case Ch('<'): - if (text[1] == Ch('/')) - { - // Node closing - text += 2; // Skip '(text); - if (!internal::compare(node->name(), node->name_size(), closing_name, text - closing_name, true)) - RAPIDXML_PARSE_ERROR("invalid closing tag name", text); - } - else - { - // No validation, just skip name - skip(text); - } - // Skip remaining whitespace after node name - skip(text); - if (*text != Ch('>')) - RAPIDXML_PARSE_ERROR("expected >", text); - ++text; // Skip '>' - return; // Node closed, finished parsing contents - } - else - { - // Child node - ++text; // Skip '<' - if (xml_node *child = parse_node(text)) - node->append_node(child); - } - break; - - // End of data - error - case Ch('\0'): - RAPIDXML_PARSE_ERROR("unexpected end of data", text); - - // Data node - default: - next_char = parse_and_append_data(node, text, contents_start); - goto after_data_node; // Bypass regular processing after data nodes - - } - } - } - - // Parse XML attributes of the node - template - void parse_node_attributes(Ch *&text, xml_node *node) - { - // For all attributes - while (attribute_name_pred::test(*text)) - { - // Extract attribute name - Ch *name = text; - ++text; // Skip first character of attribute name - skip(text); - if (text == name) - RAPIDXML_PARSE_ERROR("expected attribute name", name); - - // Create new attribute - xml_attribute *attribute = this->allocate_attribute(); - attribute->name(name, text - name); - node->append_attribute(attribute); - - // Skip whitespace after attribute name - skip(text); - - // Skip = - if (*text != Ch('=')) - RAPIDXML_PARSE_ERROR("expected =", text); - ++text; - - // Add terminating zero after name - if (!(Flags & parse_no_string_terminators)) - attribute->name()[attribute->name_size()] = 0; - - // Skip whitespace after = - skip(text); - - // Skip quote and remember if it was ' or " - Ch quote = *text; - if (quote != Ch('\'') && quote != Ch('"')) - RAPIDXML_PARSE_ERROR("expected ' or \"", text); - ++text; - - // Extract attribute value and expand char refs in it - Ch *value = text, *end; - const int AttFlags = Flags & ~parse_normalize_whitespace; // No whitespace normalization in attributes - if (quote == Ch('\'')) - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); - else - end = skip_and_expand_character_refs, attribute_value_pure_pred, AttFlags>(text); - - // Set attribute value - attribute->value(value, end - value); - - // Make sure that end quote is present - if (*text != quote) - RAPIDXML_PARSE_ERROR("expected ' or \"", text); - ++text; // Skip quote - - // Add terminating zero after value - if (!(Flags & parse_no_string_terminators)) - attribute->value()[attribute->value_size()] = 0; - - // Skip whitespace after attribute value - skip(text); - } - } - - }; - - //! \cond internal - namespace internal - { - - // Whitespace (space \n \r \t) - template - const unsigned char lookup_tables::lookup_whitespace[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, // 0 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1 - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // F - }; - - // Node name (anything but space \n \r \t / > ? \0) - template - const unsigned char lookup_tables::lookup_node_name[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) (anything but < \0) - template - const unsigned char lookup_tables::lookup_text[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) that does not require processing when ws normalization is disabled - // (anything but < \0 &) - template - const unsigned char lookup_tables::lookup_text_pure_no_ws[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Text (i.e. PCDATA) that does not require processing when ws normalizationis is enabled - // (anything but < \0 & space \n \r \t) - template - const unsigned char lookup_tables::lookup_text_pure_with_ws[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute name (anything but space \n \r \t / < > = ? ! \0) - template - const unsigned char lookup_tables::lookup_attribute_name[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with single quote (anything but ' \0) - template - const unsigned char lookup_tables::lookup_attribute_data_1[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with single quote that does not require processing (anything but ' \0 &) - template - const unsigned char lookup_tables::lookup_attribute_data_1_pure[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with double quote (anything but " \0) - template - const unsigned char lookup_tables::lookup_attribute_data_2[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Attribute data with double quote that does not require processing (anything but " \0 &) - template - const unsigned char lookup_tables::lookup_attribute_data_2_pure[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 8 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 9 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // A - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // B - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // D - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // F - }; - - // Digits (dec and hex, 255 denotes end of numeric character reference) - template - const unsigned char lookup_tables::lookup_digits[256] = - { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 0 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 1 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 2 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,255,255,255,255,255,255, // 3 - 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 4 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 5 - 255, 10, 11, 12, 13, 14, 15,255,255,255,255,255,255,255,255,255, // 6 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 7 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 8 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // 9 - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // A - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // B - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // C - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // D - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, // E - 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 // F - }; - - // Upper case conversion - template - const unsigned char lookup_tables::lookup_upcase[256] = - { - // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A B C D E F - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, // 0 - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, // 1 - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, // 2 - 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // 3 - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 4 - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, // 5 - 96, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // 6 - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 123,124,125,126,127, // 7 - 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, // 8 - 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, // 9 - 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175, // A - 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, // B - 192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207, // C - 208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223, // D - 224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239, // E - 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255 // F - }; - } - //! \endcond - -} - -// Undefine internal macros -#undef RAPIDXML_PARSE_ERROR - -// On MSVC, restore warnings state -#ifdef _MSC_VER - #pragma warning(pop) -#endif - -#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp b/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp deleted file mode 100644 index 68cf57f..0000000 --- a/crates/joko_package/vendor/rapid/rapidxml_iterators.hpp +++ /dev/null @@ -1,295 +0,0 @@ -#ifndef RAPIDXML_ITERATORS_HPP_INCLUDED -#define RAPIDXML_ITERATORS_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/15 23:02:39 $ -//! \file rapidxml_iterators.hpp This file contains rapidxml iterators - -#include "rapidxml.hpp" - -namespace rapidxml -{ - const unsigned int iterate_check_name = 1 << 0; - const unsigned int iterate_case_sensitive = 1 << 1; - - //! Iterator of child nodes of xml_node - template - class node_iterator - { - public: - typedef xml_node *value_type; - typedef const value_type& reference; - typedef xml_node *pointer; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - - node_iterator() - : m_cur(0) - , m_prev(0) - , m_flags(0) - { - } - - node_iterator(xml_node* node, xml_node* prev, - unsigned char flags) - : m_cur(node) - , m_prev(prev) - , m_flags(flags) - { - } - - reference operator*() const - { - assert(m_cur); - return m_cur; - } - - pointer operator->() const - { - assert(m_cur); - return m_cur; - } - - node_iterator& operator++() - { - increment(); - return *this; - } - - node_iterator operator++(int) - { - node_iterator tmp = *this; - increment(); - return tmp; - } - - node_iterator& operator--() - { - decrement(); - return *this; - } - - node_iterator operator--(int) - { - node_iterator tmp = *this; - decrement(); - return tmp; - } - - bool operator==(const node_iterator &rhs) const - { - return m_cur == rhs.m_cur; - } - - bool operator!=(const node_iterator &rhs) const - { - return m_cur != rhs.m_cur; - } - - private: - void increment() - { - assert(m_cur && "Attempted to increment end iterator"); - m_prev = m_cur; - - if (m_flags & iterate_check_name) - m_cur = m_cur->next_sibling( - m_cur->name(), m_cur->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_cur = m_cur->next_sibling(); - } - - void decrement() - { - assert(m_prev && "Attempted to decrement begin iterator"); - m_cur = m_prev; - - if (m_flags & iterate_check_name) - m_prev = m_prev->previous_sibling( - m_prev->name(), m_prev->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_prev = m_prev->previous_sibling(); - } - - xml_node *m_cur; - xml_node *m_prev; - unsigned char m_flags; - }; - - //! Iterator of child attributes of xml_node - template - class attribute_iterator - { - public: - typedef xml_attribute *value_type; - typedef const value_type& reference; - typedef xml_attribute *pointer; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - - attribute_iterator() - : m_cur(0) - , m_prev(0) - , m_flags(0) - { - } - - attribute_iterator(xml_attribute* attr, xml_attribute* prev, - unsigned char flags) - : m_cur(attr) - , m_prev(prev) - , m_flags(flags) - { - } - - reference operator*() const - { - assert(m_cur); - return m_cur; - } - - pointer operator->() const - { - assert(m_cur); - return m_cur; - } - - attribute_iterator& operator++() - { - increment(); - return *this; - } - - attribute_iterator operator++(int) - { - attribute_iterator tmp = *this; - increment(); - return tmp; - } - - attribute_iterator& operator--() - { - decrement(); - return *this; - } - - attribute_iterator operator--(int) - { - attribute_iterator tmp = *this; - decrement(); - return tmp; - } - - bool operator==(const attribute_iterator &rhs) const - { - return m_cur == rhs.m_cur; - } - - bool operator!=(const attribute_iterator &rhs) const - { - return m_cur != rhs.m_cur; - } - - private: - void increment() - { - assert(m_cur && "Attempted to increment end iterator"); - m_prev = m_cur; - - if (m_flags & iterate_check_name) - m_cur = m_cur->next_attribute( - m_cur->name(), m_cur->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_cur = m_cur->next_attribute(); - } - - void decrement() - { - assert(m_prev && "Attempted to decrement begin iterator"); - m_cur = m_prev; - - if (m_flags & iterate_check_name) - m_prev = m_prev->previous_attribute( - m_prev->name(), m_prev->name_size(), - !!(m_flags & iterate_case_sensitive)); - else - m_prev = m_prev->previous_attribute(); - } - - xml_attribute* m_cur; - xml_attribute* m_prev; - unsigned char m_flags; - }; - - // Range-based for loop support - template - class iterator_range - { - public: - typedef Iterator const_iterator; - typedef Iterator iterator; - - iterator_range(Iterator first, Iterator last) - : m_first(first) - , m_last(last) - { - } - - Iterator begin() const { return m_first; } - Iterator end() const { return m_last; } - - private: - Iterator m_first; - Iterator m_last; - }; - - template - iterator_range> nodes(const xml_node* node, - const Ch* name = 0, - std::size_t name_size = 0, - bool case_sensitive = true) - { - unsigned char flags = 0; - if (name) - flags |= iterate_check_name; - if (case_sensitive) - flags |= iterate_case_sensitive; - - xml_node* first = - node->first_node(name, name_size, case_sensitive); - xml_node* last = first ? - node->last_node(name, name_size, case_sensitive) : nullptr; - - node_iterator begin(first, 0, flags); - node_iterator end(0, last, flags); - return iterator_range>(begin, end); - } - - template - iterator_range> attributes(const xml_node* node, - const Ch *name = 0, - std::size_t name_size = 0, - bool case_sensitive = true) - { - unsigned char flags = 0; - if (name) - flags |= iterate_check_name; - if (case_sensitive) - flags |= iterate_case_sensitive; - - xml_attribute* first = - node->first_attribute(name, name_size, case_sensitive); - xml_attribute* last = - node->last_attribute(name, name_size, case_sensitive); - - attribute_iterator begin(first, 0, flags); - attribute_iterator end(0, last, flags); - return iterator_range>(begin, end); - } -} - -#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_print.hpp b/crates/joko_package/vendor/rapid/rapidxml_print.hpp deleted file mode 100644 index ae80e1f..0000000 --- a/crates/joko_package/vendor/rapid/rapidxml_print.hpp +++ /dev/null @@ -1,422 +0,0 @@ -#ifndef RAPIDXML_PRINT_HPP_INCLUDED -#define RAPIDXML_PRINT_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/13 01:46:17 $ -//! \file rapidxml_print.hpp This file contains rapidxml printer implementation - -#include "rapidxml.hpp" - -// Only include streams if not disabled -#ifndef RAPIDXML_NO_STREAMS - #include - #include -#endif - -namespace rapidxml -{ - - /////////////////////////////////////////////////////////////////////// - // Printing flags - - const int print_no_indenting = 0x1; //!< Printer flag instructing the printer to suppress indenting of XML. See print() function. - - /////////////////////////////////////////////////////////////////////// - // Internal - - //! \cond internal - namespace internal - { - - /////////////////////////////////////////////////////////////////////////// - // Internal character operations - - // Copy characters from given range to given output iterator - template - inline OutIt copy_chars(const Ch *begin, const Ch *end, OutIt out) - { - while (begin != end) - *out++ = *begin++; - return out; - } - - // Copy characters from given range to given output iterator and expand - // characters into references (< > ' " &) - template - inline OutIt copy_and_expand_chars(const Ch *begin, const Ch *end, Ch noexpand, OutIt out) - { - while (begin != end) - { - if (*begin == noexpand) - { - *out++ = *begin; // No expansion, copy character - } - else - { - switch (*begin) - { - case Ch('<'): - *out++ = Ch('&'); *out++ = Ch('l'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('>'): - *out++ = Ch('&'); *out++ = Ch('g'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('\''): - *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('p'); *out++ = Ch('o'); *out++ = Ch('s'); *out++ = Ch(';'); - break; - case Ch('"'): - *out++ = Ch('&'); *out++ = Ch('q'); *out++ = Ch('u'); *out++ = Ch('o'); *out++ = Ch('t'); *out++ = Ch(';'); - break; - case Ch('&'): - *out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('m'); *out++ = Ch('p'); *out++ = Ch(';'); - break; - default: - *out++ = *begin; // No expansion, copy character - } - } - ++begin; // Step to next character - } - return out; - } - - // Fill given output iterator with repetitions of the same character - template - inline OutIt fill_chars(OutIt out, int n, Ch ch) - { - for (int i = 0; i < n; ++i) - *out++ = ch; - return out; - } - - // Find character - template - inline bool find_char(const Ch *begin, const Ch *end) - { - while (begin != end) - if (*begin++ == ch) - return true; - return false; - } - - /////////////////////////////////////////////////////////////////////////// - // Internal printing operations - - template - OutIt print_node(OutIt out, const xml_node *node, int flags, int indent); - - // Print children of the node - template - inline OutIt print_children(OutIt out, const xml_node *node, int flags, int indent) - { - for (xml_node *child = node->first_node(); child; child = child->next_sibling()) - out = print_node(out, child, flags, indent); - return out; - } - - // Print attributes of the node - template - inline OutIt print_attributes(OutIt out, const xml_node *node, int flags) - { - for (xml_attribute *attribute = node->first_attribute(); attribute; attribute = attribute->next_attribute()) - { - if (attribute->name() && attribute->value()) - { - // Print attribute name - *out = Ch(' '), ++out; - out = copy_chars(attribute->name(), attribute->name() + attribute->name_size(), out); - *out = Ch('='), ++out; - // Print attribute value using appropriate quote type - if (find_char(attribute->value(), attribute->value() + attribute->value_size())) - { - *out = Ch('\''), ++out; - out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('"'), out); - *out = Ch('\''), ++out; - } - else - { - *out = Ch('"'), ++out; - out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('\''), out); - *out = Ch('"'), ++out; - } - } - } - return out; - } - - // Print data node - template - inline OutIt print_data_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_data); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); - return out; - } - - // Print data node - template - inline OutIt print_cdata_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_cdata); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'); ++out; - *out = Ch('!'); ++out; - *out = Ch('['); ++out; - *out = Ch('C'); ++out; - *out = Ch('D'); ++out; - *out = Ch('A'); ++out; - *out = Ch('T'); ++out; - *out = Ch('A'); ++out; - *out = Ch('['); ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch(']'); ++out; - *out = Ch(']'); ++out; - *out = Ch('>'); ++out; - return out; - } - - // Print element node - template - inline OutIt print_element_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_element); - - // Print element name and attributes, if any - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - out = print_attributes(out, node, flags); - - // If node is childless - if (node->value_size() == 0 && !node->first_node()) - { - // Print childless node tag ending - *out = Ch('/'), ++out; - *out = Ch('>'), ++out; - } - else - { - // Print normal node tag ending - *out = Ch('>'), ++out; - - // Test if node contains a single data node only (and no other nodes) - xml_node *child = node->first_node(); - if (!child) - { - // If node has no children, only print its value without indenting - out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out); - } - else if (child->next_sibling() == 0 && child->type() == node_data) - { - // If node has a sole data child, only print its value without indenting - out = copy_and_expand_chars(child->value(), child->value() + child->value_size(), Ch(0), out); - } - else - { - // Print all children with full indenting - if (!(flags & print_no_indenting)) - *out = Ch('\n'), ++out; - out = print_children(out, node, flags, indent + 1); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - } - - // Print node end - *out = Ch('<'), ++out; - *out = Ch('/'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - *out = Ch('>'), ++out; - } - return out; - } - - // Print declaration node - template - inline OutIt print_declaration_node(OutIt out, const xml_node *node, int flags, int indent) - { - // Print declaration start - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('?'), ++out; - *out = Ch('x'), ++out; - *out = Ch('m'), ++out; - *out = Ch('l'), ++out; - - // Print attributes - out = print_attributes(out, node, flags); - - // Print declaration end - *out = Ch('?'), ++out; - *out = Ch('>'), ++out; - - return out; - } - - // Print comment node - template - inline OutIt print_comment_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_comment); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('!'), ++out; - *out = Ch('-'), ++out; - *out = Ch('-'), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('-'), ++out; - *out = Ch('-'), ++out; - *out = Ch('>'), ++out; - return out; - } - - // Print doctype node - template - inline OutIt print_doctype_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_doctype); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('!'), ++out; - *out = Ch('D'), ++out; - *out = Ch('O'), ++out; - *out = Ch('C'), ++out; - *out = Ch('T'), ++out; - *out = Ch('Y'), ++out; - *out = Ch('P'), ++out; - *out = Ch('E'), ++out; - *out = Ch(' '), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('>'), ++out; - return out; - } - - // Print pi node - template - inline OutIt print_pi_node(OutIt out, const xml_node *node, int flags, int indent) - { - assert(node->type() == node_pi); - if (!(flags & print_no_indenting)) - out = fill_chars(out, indent, Ch('\t')); - *out = Ch('<'), ++out; - *out = Ch('?'), ++out; - out = copy_chars(node->name(), node->name() + node->name_size(), out); - *out = Ch(' '), ++out; - out = copy_chars(node->value(), node->value() + node->value_size(), out); - *out = Ch('?'), ++out; - *out = Ch('>'), ++out; - return out; - } - - // Print node - template - inline OutIt print_node(OutIt out, const xml_node *node, int flags, int indent) - { - // Print proper node type - switch (node->type()) - { - // Document - case node_document: - out = print_children(out, node, flags, indent); - break; - - // Element - case node_element: - out = print_element_node(out, node, flags, indent); - break; - - // Data - case node_data: - out = print_data_node(out, node, flags, indent); - break; - - // CDATA - case node_cdata: - out = print_cdata_node(out, node, flags, indent); - break; - - // Declaration - case node_declaration: - out = print_declaration_node(out, node, flags, indent); - break; - - // Comment - case node_comment: - out = print_comment_node(out, node, flags, indent); - break; - - // Doctype - case node_doctype: - out = print_doctype_node(out, node, flags, indent); - break; - - // Pi - case node_pi: - out = print_pi_node(out, node, flags, indent); - break; - - // Unknown - default: - assert(0); - break; - } - - // If indenting not disabled, add line break after node - if (!(flags & print_no_indenting)) - *out = Ch('\n'), ++out; - - // Return modified iterator - return out; - } - } - //! \endcond - - /////////////////////////////////////////////////////////////////////////// - // Printing - - //! Prints XML to given output iterator. - //! \param out Output iterator to print to. - //! \param node Node to be printed. Pass xml_document to print entire document. - //! \param flags Flags controlling how XML is printed. - //! \return Output iterator pointing to position immediately after last character of printed text. - template - inline OutIt print(OutIt out, const xml_node &node, int flags = 0) - { - return internal::print_node(out, &node, flags, 0); - } - -#ifndef RAPIDXML_NO_STREAMS - - //! Prints XML to given output stream. - //! \param out Output stream to print to. - //! \param node Node to be printed. Pass xml_document to print entire document. - //! \param flags Flags controlling how XML is printed. - //! \return Output stream. - template - inline std::basic_ostream &print(std::basic_ostream &out, const xml_node &node, int flags = 0) - { - print(std::ostream_iterator(out), node, flags); - return out; - } - - //! Prints formatted XML to given output stream. Uses default printing flags. Use print() function to customize printing process. - //! \param out Output stream to print to. - //! \param node Node to be printed. - //! \return Output stream. - template - inline std::basic_ostream &operator <<(std::basic_ostream &out, const xml_node &node) - { - return print(out, node); - } - -#endif - -} - -#endif diff --git a/crates/joko_package/vendor/rapid/rapidxml_utils.hpp b/crates/joko_package/vendor/rapid/rapidxml_utils.hpp deleted file mode 100644 index 91cf83e..0000000 --- a/crates/joko_package/vendor/rapid/rapidxml_utils.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef RAPIDXML_UTILS_HPP_INCLUDED -#define RAPIDXML_UTILS_HPP_INCLUDED - -// Copyright (C) 2006, 2009 Marcin Kalicinski -// Version 1.13 -// Revision $DateTime: 2009/05/15 23:02:39 $ -//! \file rapidxml_utils.hpp This file contains high-level rapidxml utilities that can be useful -//! in certain simple scenarios. They should probably not be used if maximizing performance is the main objective. - -#include "rapidxml.hpp" -#include - -namespace rapidxml -{ - //! Counts children of node. Time complexity is O(n). - //! \return Number of children of node - template - inline std::size_t count_children(const xml_node *node, - const Ch* name = 0, - std::size_t name_size = 0) - { - if (name && name_size == 0) - name_size = internal::measure(name); - - xml_node *child = node->first_node(name, name_size); - std::size_t count = 0; - while (child) - { - ++count; - child = child->next_sibling(name, name_size); - } - return count; - } - - //! Counts attributes of node. Time complexity is O(n). - //! \return Number of attributes of node - template - inline std::size_t count_attributes(const xml_node *node, - const Ch* name = 0, - std::size_t name_size = 0) - { - if (name && name_size == 0) - name_size = internal::measure(name); - - xml_attribute *attr = node->first_attribute(name, name_size); - std::size_t count = 0; - while (attr) - { - ++count; - attr = attr->next_attribute(name, name_size); - } - return count; - } -} - -#endif diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index 0daa425..f2e7353 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -1155,7 +1155,7 @@ impl ToString for Cull { /// Filter for which festivals will the marker be active for #[bitflags] #[repr(u8)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Festival { DragonBash = 1 << 0, #[allow(clippy::enum_variant_names)] diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs index e612d7b..a0c7ff2 100644 --- a/crates/joko_plugin_manager/src/lib.rs +++ b/crates/joko_plugin_manager/src/lib.rs @@ -26,4 +26,8 @@ impl JokolayComponent<(), ()> for JokolayPlugin { ) { } } -impl JokolayComponentDeps for JokolayPlugin {} +impl JokolayComponentDeps for JokolayPlugin { + fn requires(&self) -> Vec<&str> { + vec!["mumble_link_back"] + } +} diff --git a/crates/joko_plugins/Cargo.toml b/crates/joko_plugins/Cargo.toml deleted file mode 100644 index 98b1e9d..0000000 --- a/crates/joko_plugins/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "joko_plugins" -version = "0.2.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -joko_components = { path = "../joko_components" } -scopeguard = "1.2.0" -smol_str = { workspace = true } -tokio = { workspace = true } diff --git a/crates/joko_plugins/src/lib.rs b/crates/joko_plugins/src/lib.rs deleted file mode 100644 index b4c801c..0000000 --- a/crates/joko_plugins/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -use joko_components::{ - ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, -}; - -pub struct JokolayPlugin {} - -pub struct JokolayPluginManager {} - -impl JokolayComponent<(), ()> for JokolayPlugin { - fn flush_all_messages(&mut self) -> () {} - fn tick(&mut self, timestamp: f64) -> Option<&()> { - None - } - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - _input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - } -} -impl JokolayComponentDeps for JokolayPlugin {} diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml deleted file mode 100644 index 40a2af4..0000000 --- a/crates/joko_render/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -# Define all structures that can be sent through asynchronous messages - -[package] -name = "joko_render" -version = "0.2.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bincode = { workspace = true } -bytemuck = { workspace = true } -glam = { workspace = true, features = ["bytemuck"] } -tracing = { workspace = true } -egui = { workspace = true } -egui_render_three_d = { version = "*" } -egui_window_glfw_passthrough = { version = "0.8" } -tokio = { workspace = true } - - -joko_components = { path = "../joko_components" } -joko_link = { path = "../joko_link" } -joko_render_models = { path = "../joko_render_models" } - diff --git a/crates/joko_render/shaders/marker.fs b/crates/joko_render/shaders/marker.fs deleted file mode 100644 index 90dad16..0000000 --- a/crates/joko_render/shaders/marker.fs +++ /dev/null @@ -1,18 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 vtex_coord; -layout(location = 1) in float valpha; -layout(location = 2) in vec4 vcolor; - -layout(location = 0) out vec4 ocolor; - -layout(location = 1) uniform sampler2D sam; - -void main() { - vec4 color = texture(sam, vtex_coord, -2.0); - color.a = color.a * valpha; - if (color.a < 0.01) { - discard; - } - ocolor = color; -} diff --git a/crates/joko_render/shaders/marker.vs b/crates/joko_render/shaders/marker.vs deleted file mode 100644 index adbb641..0000000 --- a/crates/joko_render/shaders/marker.vs +++ /dev/null @@ -1,39 +0,0 @@ -#version 450 - -layout(location = 0) in vec4 position; -layout(location = 1) in float alpha; -layout(location = 2) in vec2 tex_coord; -layout(location = 3) in vec2 fade_near_far; -layout(location = 4) in vec4 color; - -layout(location = 0) out vec2 vtex_coord; -layout(location = 1) out float valpha; -layout(location = 2) out vec4 vcolor; - - -layout(location = 0) uniform vec3 camera_pos; -// location 1 is for sampler in frag shader -layout(location = 2) uniform mat4 transform; - - -void main( -) { - valpha = alpha; - vtex_coord = tex_coord; - gl_Position = transform * position; - vcolor = color; - - float dist = distance(camera_pos, position.xyz); - if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { - // if distance is exactly fade_near, we will multiply with 1.0 - // if its more, then we will multiply with how far we are in between fade_near and fade_far - float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); - // The actual alpha - valpha *= ratio; - } - if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { - valpha = 0.0; - } -} - - diff --git a/crates/joko_render/shaders/marker.wgsl b/crates/joko_render/shaders/marker.wgsl deleted file mode 100644 index e69de29..0000000 diff --git a/crates/joko_render/shaders/player_visibility.wgsl b/crates/joko_render/shaders/player_visibility.wgsl deleted file mode 100644 index ba3c564..0000000 --- a/crates/joko_render/shaders/player_visibility.wgsl +++ /dev/null @@ -1,24 +0,0 @@ - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) ndc_pos: vec2 -}; - -@vertex -fn vs_main( - @location(0) position: vec2, -) -> VertexOutput { - var result: VertexOutput; - - result.position = vec4(position.xy, 0.5, 1.0); - result.ndc_pos = position; - return result; -} - - -@fragment -fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { - let alpha: f32 = distance(vertex.ndc_pos.xy, vec2(0.0, 0.0)); - return vec4(0.0, 0.0, 0.0, pow(alpha, 5.0) / 2.0); -} - diff --git a/crates/joko_render/shaders/trail.fs b/crates/joko_render/shaders/trail.fs deleted file mode 100644 index e8ca1d4..0000000 --- a/crates/joko_render/shaders/trail.fs +++ /dev/null @@ -1,22 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 vtex_coord; -layout(location = 1) in float valpha; -layout(location = 2) in vec4 vcolor; - -layout(location = 0) out vec4 ocolor; - -layout(location = 1) uniform sampler2D sam; // wrap_s = "REPEAT" wrap_t = "REPEAT" -layout(location = 3) uniform vec2 scroll_texture; - -void main() { - //vec4 color = texture(sam, vec2 (vtex_coord.x + scroll_texture.x, vtex_coord.y + scroll_texture.y), -2.0); - vec4 color = texture(sam, vec2 (vtex_coord.x + 0.0, vtex_coord.y + scroll_texture.y), -2.0); - //vec4 color = texture(sam, vtex_coord + scroll_texture); - //vec4 color = texture(sam, vtex_coord, -2.0);//original working - color.a = color.a * valpha; - if (color.a < 0.01) { - discard; - } - ocolor = color; -} diff --git a/crates/joko_render/shaders/trail.vs b/crates/joko_render/shaders/trail.vs deleted file mode 100644 index c8f04b6..0000000 --- a/crates/joko_render/shaders/trail.vs +++ /dev/null @@ -1,37 +0,0 @@ -#version 450 - -layout(location = 0) in vec4 position; -layout(location = 1) in float alpha; -layout(location = 2) in vec2 tex_coord; -layout(location = 3) in vec2 fade_near_far; -layout(location = 4) in vec4 color; - - -layout(location = 0) out vec2 vtex_coord; -layout(location = 1) out float valpha; -layout(location = 2) out vec4 vcolor; - -layout(location = 0) uniform vec3 camera_pos; -// location 1 is for sampler in frag shader -layout(location = 2) uniform mat4 transform; -// location 3 is for scroll_texture - -void main( -) { - valpha = alpha; - vtex_coord = tex_coord; - gl_Position = transform * position; - vcolor = color; - - float dist = distance(camera_pos, position.xyz); - if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { - // if distance is exactly fade_near, we will multiply with 1.0 - // if its more, then we will multiply with how far we are in between fade_near and fade_far - float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); - // The actual alpha - valpha *= ratio; - } - if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { - valpha = 0.0; - } -} diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render/src/billboard.rs deleted file mode 100644 index 96ee03a..0000000 --- a/crates/joko_render/src/billboard.rs +++ /dev/null @@ -1,360 +0,0 @@ -use egui::ahash::HashMap; -use egui_render_three_d::{ - three_d::{context::*, Context, HasContext}, - GpuTexture, -}; -use glam::Vec2; -use joko_render_models::{ - marker::{MarkerObject, MarkerVertex}, - trail::TrailObject, -}; -use tracing::{error, info, trace, warn}; - -use crate::gl_error; - -const MARKER_VERTEX_STRIDE: i32 = std::mem::size_of::() as _; -pub struct BillBoardRenderer { - pub markers: Vec, - pub trails: Vec, - pub markers_wip: Vec, //work in progress: this is where the markers are inserted - pub trails_wip: Vec, //work in progress: this is where the markers are inserted - marker_program: NativeProgram, - marker_vertex_buffer: NativeBuffer, - marker_vertex_array: NativeVertexArray, - - trail_program: NativeProgram, - trail_vertex_buffers: Vec, - trail_vertex_arrays: Vec, -} - -const MARKER_VERTEX_SHADER: &str = include_str!("../shaders/marker.vs"); -const MARKER_FRAGMENT_SHADER: &str = include_str!("../shaders/marker.fs"); -const TRAIL_VERTEX_SHADER: &str = include_str!("../shaders/trail.vs"); -const TRAIL_FRAGMENT_SHADER: &str = include_str!("../shaders/trail.fs"); - -impl BillBoardRenderer { - pub fn new(gl: &Context) -> Self { - unsafe { - let marker_program = - new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); - gl_error!(gl); - - let trail_shift_program = - new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); - gl_error!(gl); - - let marker_vertex_buffer = create_buffer(gl); - let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); - - Self { - markers: Vec::new(), - markers_wip: Vec::new(), - - marker_program, - marker_vertex_buffer, - marker_vertex_array, - - trails: Vec::new(), - trails_wip: Vec::new(), - - trail_program: trail_shift_program, - trail_vertex_buffers: Default::default(), - trail_vertex_arrays: Default::default(), - } - } - } - - pub fn swap(&mut self) { - trace!( - "swap UI to display {} markers, {} trails", - self.markers_wip.len(), - self.trails_wip.len() - ); - self.markers = std::mem::take(&mut self.markers_wip); - self.trails = std::mem::take(&mut self.trails_wip); - } - - pub fn prepare_render_data(&mut self, gl: &Context) { - /* - TODO: map view (view from above) - trim down the trails too far ? - fatten them ? - */ - unsafe { - gl_error!(gl); - } - // sort by depth - self.markers.sort_unstable_by(|first, second| { - first.distance.total_cmp(&second.distance).reverse() // we need the farther markers (more distance from camera) to be rendered first, for correct alpha blending - }); - - let mut required_size_in_bytes = - (self.markers.len() * 6 * std::mem::size_of::()) as u64; - for trail in self.trails.iter() { - let len = (trail.vertices.len() * std::mem::size_of::()) as u64; - required_size_in_bytes = required_size_in_bytes.max(len); - } - let mut vb: Vec = Vec::with_capacity(self.markers.len() * 6); - - for marker_object in self.markers.iter() { - vb.extend_from_slice(&marker_object.vertices); - } - unsafe { - gl_error!(gl); - gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); - gl.buffer_data_u8_slice(ARRAY_BUFFER, bytemuck::cast_slice(&vb), DYNAMIC_DRAW); - gl_error!(gl); - } - if self.trails.len() > self.trail_vertex_buffers.len() { - let needs = self.trails.len() - self.trail_vertex_buffers.len(); - for _ in 0..needs { - let vb = unsafe { create_buffer(gl) }; - self.trail_vertex_buffers.push(vb); - let trail_vertex_array = unsafe { create_trail_array(gl, vb, 1) }; - self.trail_vertex_arrays.push(trail_vertex_array); - } - } - for (trail, trail_buffer) in self.trails.iter().zip(self.trail_vertex_buffers.iter()) { - unsafe { - gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); - gl.buffer_data_u8_slice( - ARRAY_BUFFER, - bytemuck::cast_slice(trail.vertices.as_ref()), - DYNAMIC_DRAW, - ); - } - } - unsafe { - gl_error!(gl); - } - } - pub fn render( - &self, - gl: &Context, - cam_pos: glam::Vec3, - view_proj: &glam::Mat4, - textures: &HashMap, - latest_time: f64, - ) { - unsafe { - gl_error!(gl); - gl.disable(SCISSOR_TEST); - - gl.use_program(Some(self.trail_program)); - gl_error!(gl); - gl.active_texture(TEXTURE0); - gl_error!(gl); - let scroll_texture: Vec2 = Vec2 { - x: 0.0, - y: (latest_time as f32 % 2.0) - 1.0, - }; //TODO: manage speed in some configurations. per trail ? - - gl.uniform_2_f32_slice(Some(&NativeUniformLocation(3)), scroll_texture.as_ref()); - //https://stackoverflow.com/questions/27771902/opengl-changing-texture-coordinates-on-the-fly - //https://www.khronos.org/opengl/wiki/Uniform_(GLSL) - for ((trail, trail_buffer), trail_array) in self - .trails - .iter() - .zip(self.trail_vertex_buffers.iter()) - .zip(self.trail_vertex_arrays.iter()) - { - if let Some(texture) = textures.get(&trail.texture) { - gl.bind_vertex_array(Some(*trail_array)); - gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); - gl.uniform_matrix_4_f32_slice( - Some(&NativeUniformLocation(2)), - false, - view_proj.to_cols_array().as_ref(), - ); - gl_error!(gl); - - gl.bind_vertex_buffer(0, Some(*trail_buffer), 0, MARKER_VERTEX_STRIDE); - gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); - gl.bind_texture(TEXTURE_2D, Some(texture.handle)); - gl.bind_sampler(0, Some(texture.sampler)); - gl_error!(gl); - gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); - gl_error!(gl); - - /* - gl.polygon_mode(FRONT_AND_BACK, LINE); - gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); - gl.polygon_mode(FRONT_AND_BACK, FILL); - gl_error!(gl); - */ - } - } - gl.use_program(Some(self.marker_program)); - gl_error!(gl); - gl.bind_vertex_array(Some(self.marker_vertex_array)); - gl_error!(gl); - gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); - gl.uniform_matrix_4_f32_slice( - Some(&NativeUniformLocation(2)), - false, - view_proj.to_cols_array().as_ref(), - ); - gl_error!(gl); - gl.bind_vertex_buffer(0, Some(self.marker_vertex_buffer), 0, MARKER_VERTEX_STRIDE); - gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); - for (index, mo) in self.markers.iter().enumerate() { - let index: u32 = index.try_into().unwrap(); - if let Some(texture) = textures.get(&mo.texture) { - gl.bind_texture(TEXTURE_2D, Some(texture.handle)); - gl.bind_sampler(0, Some(texture.sampler)); - gl.draw_arrays(TRIANGLES, index as i32 * 6, 6); - } - } - gl_error!(gl); - gl.bind_vertex_array(None); - } - } -} - -/// takes in strings containing vertex/fragment shaders and returns a Shaderprogram with them attached -#[tracing::instrument(skip(gl))] -pub fn new_program( - gl: &Context, - vertex_shader_source: &str, - fragment_shader_source: &str, - _geometry_shader_source: Option<&str>, -) -> NativeProgram { - //https://www.khronos.org/opengl/wiki/Shader_Compilation#Program_setup - unsafe { - gl_error!(gl); - - let program = gl.create_program().unwrap(); - let vertex_shader = gl.create_shader(VERTEX_SHADER).unwrap(); - gl.shader_source(vertex_shader, vertex_shader_source); - gl.compile_shader(vertex_shader); - if !gl.get_shader_compile_status(vertex_shader) { - let e = gl.get_shader_info_log(vertex_shader); - error!("{}", &e); - panic!("vertex shader compilation error: {}", &e); - } - let frag_shader = gl.create_shader(FRAGMENT_SHADER).unwrap(); - gl.shader_source(frag_shader, fragment_shader_source); - gl.compile_shader(frag_shader); - if !gl.get_shader_compile_status(frag_shader) { - let e = gl.get_shader_info_log(frag_shader); - error!("frag shader compilation error:{}", &e); - panic!("frag shader compilation error: {}", &e); - } - gl.attach_shader(program, vertex_shader); - gl.attach_shader(program, frag_shader); - // let geometry_shader; - // geometry_shader = gl.create_shader(glow::GEOMETRY_SHADER).unwrap(); - // if let Some(gss) = geometry_shader_source { - // gl.shader_source(geometry_shader, gss); - // gl.compile_shader(geometry_shader); - // if !gl.get_shader_compile_status(geometry_shader) { - // let e = gl.get_shader_info_log(geometry_shader); - // error!("frag shader compilation error:{}", &e); - // panic!("geometry shader compilation error: {}", &e); - // } - // gl.attach_shader(shader_program, geometry_shader); - // } - gl.link_program(program); - if !gl.get_program_link_status(program) { - let e = gl.get_program_info_log(program); - error!("shader program link error: {}", &e); - panic!("shader program link error: {}", &e); - } - gl.delete_shader(vertex_shader); - // if geometry_shader_source.is_some() { - // gl.delete_shader(geometry_shader); - // } - gl.delete_shader(frag_shader); - gl_error!(gl); - let active_attribute_count = gl.get_active_attributes(program); - let mut shader_info = format!("Shader Info:\nvertex attributes: {active_attribute_count}"); - for index in 0..active_attribute_count { - if let Some(attr) = gl.get_active_attribute(program, index) { - let location = gl.get_attrib_location(program, &attr.name); - shader_info = format!("{shader_info}\n{} @ {}", attr.name, location.unwrap()); - } else { - warn!("attribute with index {index} doesn't exist"); - } - } - let active_uniform_count = gl.get_active_uniforms(program); - shader_info = format!("{shader_info}\nuniform locations:{active_uniform_count}"); - for index in 0..active_uniform_count { - if let Some(attr) = gl.get_active_uniform(program, index) { - let location = gl.get_uniform_location(program, &attr.name); - shader_info = format!("{shader_info}\n{} @ {}", attr.name, location.unwrap().0); - } else { - warn!("uniform with index {index} doesn't exist"); - } - } - info!("{shader_info}"); - program - } -} -unsafe fn create_buffer(gl: &Context) -> NativeBuffer { - gl_error!(gl); - let vb = gl.create_buffer().expect("failed to create vb for markers"); - gl_error!(gl); - - gl.bind_vertex_array(None); - gl.bind_buffer(ARRAY_BUFFER, Some(vb)); - gl_error!(gl); - - gl.bind_buffer(ARRAY_BUFFER, None); - gl_error!(gl); - vb -} - -unsafe fn create_marker_array(gl: &Context, vertex_buffer: NativeBuffer) -> NativeVertexArray { - create_array(gl, vertex_buffer, 1) -} - -unsafe fn create_array( - gl: &Context, - vertex_buffer: NativeBuffer, - binding_index: u32, -) -> NativeVertexArray { - let marker_vertex_array = gl.create_vertex_array().expect("failed to create egui vao"); - gl.bind_vertex_array(Some(marker_vertex_array)); - gl.bind_vertex_buffer(binding_index, Some(vertex_buffer), 0, MARKER_VERTEX_STRIDE); - gl_error!(gl); - - gl.enable_vertex_array_attrib(marker_vertex_array, 0); - gl.vertex_array_attrib_format_f32(marker_vertex_array, 0, 3, FLOAT, false, 0); - gl.vertex_array_attrib_binding_f32(marker_vertex_array, 0, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(marker_vertex_array, 1); - gl.vertex_array_attrib_format_f32(marker_vertex_array, 1, 1, FLOAT, false, 12); - gl.vertex_array_attrib_binding_f32(marker_vertex_array, 1, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(marker_vertex_array, 2); - gl.vertex_array_attrib_format_f32(marker_vertex_array, 2, 2, FLOAT, false, 16); - gl.vertex_array_attrib_binding_f32(marker_vertex_array, 2, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(marker_vertex_array, 3); - gl.vertex_array_attrib_format_f32(marker_vertex_array, 3, 2, FLOAT, false, 24); - gl.vertex_array_attrib_binding_f32(marker_vertex_array, 3, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(marker_vertex_array, 4); - gl.vertex_array_attrib_format_f32(marker_vertex_array, 4, 4, UNSIGNED_BYTE, true, 32); - gl.vertex_array_attrib_binding_f32(marker_vertex_array, 4, 0); - gl_error!(gl); - marker_vertex_array -} - -unsafe fn create_trail_array( - gl: &Context, - vertex_buffer: NativeBuffer, - binding_index: u32, -) -> NativeVertexArray { - let trail_vertex_array = create_array(gl, vertex_buffer, binding_index); - gl.enable_vertex_array_attrib(trail_vertex_array, 5); - gl.vertex_array_attrib_format_f32(trail_vertex_array, 5, 2, FLOAT, false, 36); - gl.vertex_array_attrib_binding_f32(trail_vertex_array, 5, 0); - gl_error!(gl); - - trail_vertex_array -} diff --git a/crates/joko_render/src/gl.rs b/crates/joko_render/src/gl.rs deleted file mode 100644 index 8ac9626..0000000 --- a/crates/joko_render/src/gl.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[macro_export] -macro_rules! gl_error { - ($gl:expr) => {{ - let e = $gl.get_error(); - if e != egui_render_three_d::three_d::context::NO_ERROR { - tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); - } - }}; -} diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs deleted file mode 100644 index 9050354..0000000 --- a/crates/joko_render/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod billboard; -pub mod gl; -pub mod renderer; diff --git a/crates/joko_render/src/renderer.rs b/crates/joko_render/src/renderer.rs deleted file mode 100644 index 9c49027..0000000 --- a/crates/joko_render/src/renderer.rs +++ /dev/null @@ -1,357 +0,0 @@ -use crate::billboard::BillBoardRenderer; -use crate::gl_error; -use egui_render_three_d::three_d; -use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; -use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; -use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; -use egui_render_three_d::three_d::Camera; -use egui_render_three_d::three_d::HasContext; -use egui_render_three_d::three_d::ScissorBox; -use egui_render_three_d::three_d::Viewport; -use egui_render_three_d::ThreeDBackend; -use egui_render_three_d::ThreeDConfig; -use egui_window_glfw_passthrough::GlfwBackend; -use glam::Mat4; -use joko_components::JokolayComponent; -use joko_components::JokolayComponentDeps; -use joko_render_models::messages::UIToUIMessage; -use joko_link::MumbleLink; -use joko_link::UIState; -use three_d::prelude::*; - -use joko_render_models::{marker::MarkerObject, trail::TrailObject}; - -pub struct JokoRenderer { - pub view_proj: Mat4, - pub cam_pos: glam::Vec3, - pub camera: Camera, - pub viewport: Viewport, - pub has_link: bool, - pub is_map_open: bool, - pub billboard_renderer: BillBoardRenderer, - pub gl: egui_render_three_d::ThreeDBackend, - channel_receiver: Option>, -} - -impl JokoRenderer { - pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { - let glfw = glfw_backend.glfw.clone(); - let backend = ThreeDBackend::new( - ThreeDConfig { - glow_config: Default::default(), - }, - |s| glfw.get_proc_address_raw(s), - //glfw_backend.window.raw_window_handle(), - glfw_backend.framebuffer_size_physical, - ); - let viewport = Viewport { - x: 0, - y: 0, - width: glfw_backend.framebuffer_size_physical[0], - height: glfw_backend.framebuffer_size_physical[1], - }; - let gl = &backend.context; - unsafe { gl_error!(gl) }; - let billboard_renderer = BillBoardRenderer::new(gl); - unsafe { gl_error!(gl) }; - Self { - viewport, - view_proj: Default::default(), - camera: Camera::new_perspective( - viewport, - [0.0, 0.0, 0.0].into(), - [0.0, 0.0, 0.0].into(), - Vector3::unit_y(), - Deg(90.0), - 1.0, - 5000.0, - ), - has_link: false, - is_map_open: false, - gl: backend, - billboard_renderer, - cam_pos: Default::default(), - channel_receiver: None, - } - } - - /* - CRect GetMinimapRectangle() - { - int w = mumbleLink.miniMap.compassWidth; - int h = mumbleLink.miniMap.compassHeight; - - CRect pos; - CRect size = App->GetRoot()->GetClientRect(); - float scale = GetWindowTooSmallScale(); - - pos.x1 = int( size.Width() - w * scale ); - pos.x2 = size.Width(); - - - if ( mumbleLink.isMinimapTopRight ) - { - pos.y1 = 1; - pos.y2 = int( h * scale + 1 ); - } - else - { - int delta = 37; - if ( mumbleLink.uiSize == 0 ) - delta = 33; - if ( mumbleLink.uiSize == 2 ) - delta = 41; - if ( mumbleLink.uiSize == 3 ) - delta = 45; - - pos.y1 = int( size.Height() - h * scale - delta * scale ); - pos.y2 = int( size.Height() - delta * scale ); - } - - return pos; - } - */ - pub fn get_z_near() -> f32 { - 1.0 - } - pub fn get_z_far() -> f32 { - 1000.0 - } - pub fn swap(&mut self) { - self.billboard_renderer.swap(); - } - /* - //https://wiki.guildwars2.com/wiki/API:1/event_details#Coordinate_recalculation - fn _scale_coords(continent_rect, map_rect, coords){ - continent_width = continent_rect[1].x - continent_rect[0].x; - continent_height = continent_rect[1].y - continent_rect[0].y; - map_width = map_rect[1].x - map_rect[0].x; - map_height = map_rect[1].y - map_rect[0].y; - position_on_map_x = coords.x - map_rect[0].x; - position_on_map_y = coords.y - map_rect[1].y; - return [ - Math.round( continent_rect[0].x + ( 1 * position_on_map_x / map_width * continent_width ) ), - Math.round( continent_rect[0].y + (-1 * position_on_map_y / map_height * continent_height ) ) - ]; - } - */ - fn handle_u2u_message(&mut self, msg: UIToUIMessage) { - match msg { - UIToUIMessage::BulkMarkerObject(marker_objects) => { - tracing::debug!( - "Handling of UIToUIMessage::BulkMarkerObject {}", - marker_objects.len() - ); - self.extend_markers(marker_objects); - } - UIToUIMessage::BulkTrailObject(trail_objects) => { - tracing::debug!( - "Handling of UIToUIMessage::BulkTrailObject {}", - trail_objects.len() - ); - self.extend_trails(trail_objects); - } - UIToUIMessage::MarkerObject(mo) => { - tracing::trace!("Handling of UIToUIMessage::MarkerObject"); - self.add_billboard(*mo); - } - UIToUIMessage::TrailObject(to) => { - tracing::trace!("Handling of UIToUIMessage::TrailObject"); - self.add_trail(*to); - } - UIToUIMessage::RenderSwapChain => { - tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); - self.swap(); - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling UIToUIMessage has not been implemented yet"); - } - } - } - - pub fn extend_markers(&mut self, marker_objects: Vec) { - self.billboard_renderer.markers_wip.extend(marker_objects); - } - pub fn add_billboard(&mut self, marker_object: MarkerObject) { - self.billboard_renderer.markers_wip.push(marker_object); - } - - pub fn extend_trails(&mut self, trail_objects: Vec) { - self.billboard_renderer.trails_wip.extend(trail_objects); - } - pub fn add_trail(&mut self, trail_object: TrailObject) { - self.billboard_renderer.trails_wip.push(trail_object); - } - - pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - self.gl.prepare_frame(latest_framebuffer_size_getter); - unsafe { - let gl = self.gl.context.clone(); - gl_error!(gl); - // self.gl.context.set_viewport(self.viewport); - self.gl.context.set_scissor(ScissorBox::new_at_origo( - self.viewport.width, - self.viewport.height, - )); - self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); - self.gl - .context - .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); - gl_error!(gl); - } - } - - pub fn render_egui( - &mut self, - meshes: Vec, - textures_delta: egui::TexturesDelta, - logical_screen_size: [f32; 2], - latest_time: f64, - ) { - if self.has_link && !self.is_map_open { - self.billboard_renderer - .prepare_render_data(&self.gl.context); - self.billboard_renderer.render( - &self.gl.context, - self.cam_pos, - &self.view_proj, - &self.gl.glow_backend.painter.managed_textures, - latest_time, - ); - } - self.gl - .render_egui(meshes, textures_delta, logical_screen_size); - } - - pub fn present(&mut self) {} - - pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { - tracing::info!(?latest_size, "resizing framebuffer"); - - self.viewport = Viewport { - x: 0, - y: 0, - width: latest_size[0], - height: latest_size[1], - }; - self.gl.resize_framebuffer(latest_size); - } -} - -impl JokolayComponentDeps for JokoRenderer {} -impl JokolayComponent<(), ()> for JokoRenderer { - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - mut input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap< - u32, - tokio::sync::mpsc::Sender, - >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - self.channel_receiver = input_notification.remove(&0); - } - fn flush_all_messages(&mut self) -> () { - let channel_receiver = self.channel_receiver.as_mut().unwrap(); - - //two steps reading due to self mutability required by channel - let mut messages = Vec::new(); - while let Ok(msg) = channel_receiver.try_recv() { - let msg: UIToUIMessage = bincode::deserialize(&msg).unwrap(); - messages.push(msg); - } - for msg in messages { - self.handle_u2u_message(msg); - } - () - } - fn tick(&mut self, _latest_time: f64) -> Option<&()> { - let link: Option<&MumbleLink> = None; - if let Some(link) = link { - //x positive => east - //y positive => ascention - //z positive => north - self.is_map_open = if let Some(ui_state) = link.ui_state { - ui_state.contains(UIState::IsMapOpen) - } else { - false - }; - - //TODO: change perspective is map is open - let center = link.cam_pos.0 + link.f_camera_front.0; - let cam_pos = link.cam_pos; - /* - let map_pos_x = (link.player_x - link.map_center_x) / 1.64; - let map_pos_y = (link.map_center_y - link.player_y) / 1.64; - let center = if self.is_map_open { - glam::Vec3{ - x: link.player_pos.x - map_pos_x, - y: link.player_pos.y + 100.0, - z: link.player_pos.z - map_pos_y, - } - } else { - link.cam_pos + link.f_camera_front //default old one - }; - - let client_width = (link.client_size.x) as f32; - let client_height = (link.client_size.y) as f32; - - let cam_pos = if self.is_map_open { - //TODO: validate values - glam::Vec3{ - x: link.player_pos.x - map_pos_x, - y: link.player_pos.y + 101.0, - z: link.player_pos.z - map_pos_y, - } - }else { - link.cam_pos //default old one - };*/ - let camera = Camera::new_perspective( - self.viewport, - cam_pos.0.to_array().into(), - center.to_array().into(), - Vector3::unit_y(), - Rad(link.fov), - Self::get_z_near(), - Self::get_z_far(), - ); - self.camera = camera; - /* - is_map_open: - target camera direction: 0 -20 1 - have trails seen from further - have trails fatter drawing - - println!("client: {} {} {} {}", client_width, client_height, client_width.div(client_height), client_height.div(client_width)); - println!("map scale: {}", link.map_scale); - println!("map position: {} {}", map_pos_x, map_pos_y); - println!("cam: {} {} {}", cam_pos.x, cam_pos.y, cam_pos.z); - println!("center: {} {} {}", center.x, center.y, center.z); - println!("H: {}", cam_pos.y - center.y); - println!("player: {} {} {}", link.player_pos.x, link.player_pos.y, link.player_pos.z); - */ - - let view = Mat4::look_at_lh(cam_pos.0, center, glam::Vec3::Y); - let proj = Mat4::perspective_lh( - link.fov, - self.viewport.aspect(), - Self::get_z_near(), - Self::get_z_far(), - ); - self.view_proj = proj * view; - self.cam_pos = cam_pos.0; - self.has_link = true; - } else { - self.has_link = false; - } - None - } -} diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index b55837b..7e3b402 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -4,7 +4,7 @@ use crate::prelude::*; #[bitflags] #[repr(u8)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Race { Unknown = 1 << 1, Asura = 1 << 2, diff --git a/crates/jokoapi/src/lib.rs b/crates/jokoapi/src/lib.rs index a57a903..8567c08 100644 --- a/crates/jokoapi/src/lib.rs +++ b/crates/jokoapi/src/lib.rs @@ -23,7 +23,7 @@ pub(crate) mod prelude { pub type HttpClient = ureq::Agent; pub use crate::end_point::EndPoint; pub use enumflags2::bitflags; - pub use miette::{IntoDiagnostic, Result, WrapErr}; + pub use miette::{IntoDiagnostic, Result}; pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; pub use std::fmt::Display; const API_BASE_URL: &str = "https://api.guildwars2.com"; diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index ab7d3cf..42b86f5 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -85,9 +85,8 @@ impl Jokolay { MumbleManager::new("MumbleLink", true).wrap_err("failed to create mumble manager")?; let dummy_plugin = Box::new(JokolayPlugin {}); - component_manager.register("dummy_plugin", dummy_plugin); component_manager.register( - "mumble_link_ui", + "mumble_link_back", Box::new( MumbleManager::new("MumbleLink", true) .wrap_err("failed to create mumble manager")?, @@ -100,6 +99,7 @@ impl Jokolay { .wrap_err("failed to create mumble manager")?, ), ); + component_manager.register("dummy_plugin", dummy_plugin); match component_manager.build_routes() { Ok(_) => {} diff --git a/crates/jokolink/Cargo.toml b/crates/jokolink/Cargo.toml deleted file mode 100644 index 5959ae3..0000000 --- a/crates/jokolink/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "jokolink" -version = "0.2.1" -edition = "2021" -[lib] -crate-type = ["cdylib", "lib"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] - - -[dependencies] -widestring = { version = "1", default-features = false, features = ["std"] } -num-derive = { version = "0", default-features = false } -num-traits = { version = "0", default-features = false } -enumflags2 = { workspace = true } -time = { workspace = true } -miette = { workspace = true } -tracing = { workspace = true } -serde = { workspace = true } -glam = { workspace = true } -serde_json = { workspace = true } - -[target.'cfg(unix)'.dependencies] -x11rb = { version = "0.12", default-features = false, features = [] } - -[target.'cfg(windows)'.dependencies] -windows = { version = "0.51.1", features = [ - "Win32_System_Memory", - "Win32_Foundation", - "Win32_Security", - "Win32_UI_WindowsAndMessaging", - "Win32_System_Threading", - "Win32_System_LibraryLoader", - "Win32_System_SystemInformation", - "Win32_Graphics_Dwm", - "Win32_UI_HiDpi", - "Win32_Graphics_Gdi", - "Win32_UI_Shell", - "Win32_System_Com", -] } -arcdps = { version = "*", default-features = false } -notify = {version = "*" } -tracing-appender = {version = "*" } -tracing-subscriber = {version = "*" } diff --git a/crates/jokolink/README.md b/crates/jokolink/README.md deleted file mode 100644 index 5962a47..0000000 --- a/crates/jokolink/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# jokolink -A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). - -it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. - -## Precaution -This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. - -> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` - -## Installation -1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) -2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` -3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts -4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. - - -## Configuration -Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. - - * loglevel: - default: "info" - type: string - possible_values: ["trace", "debug", "info", "warn", "error"] - help: the log level of the application. - - * logdir: - default: "." // current working directory - type: directory path - help: a path to a directory, where jokolink will create jokolink.log file - - * mumble_link_name: - default: "MumbleLink" - type: string - help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name - - * interval - default: 5 - type: unsigned integer (positive integer) - help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. - - * copy_dest_dir: - default: "z:\\dev\\shm" - type: directory path - help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). - - -## Verification : -1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. -2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. -3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. -4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. - - - -## Cross Compilation -To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. -`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs deleted file mode 100644 index cc8b3a7..0000000 --- a/crates/jokolink/src/lib.rs +++ /dev/null @@ -1,176 +0,0 @@ -//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory - -//! Joko link is designed to primarily get the MumbleLink or the window size -//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). -//! on windows, you can use it to create/open shared memory. -//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. -//! then, you can easily read the /dev/shm file from a any number of linux native applications. -//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. -//! - -mod mumble; -use enumflags2::BitFlags; -use glam::{IVec2, UVec2}; -//use jokoapi::end_point::{mounts::Mount, races::Race}; -use miette::{IntoDiagnostic, Result, WrapErr}; -pub use mumble::*; -use serde_json::from_str; -use tracing::error; - -/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing -pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; -#[cfg(target_os = "linux")] -pub mod linux; -#[cfg(target_os = "windows")] -pub mod win; - -#[cfg(target_os = "linux")] -use linux::MumbleLinuxImpl as MumblePlatformImpl; -#[cfg(target_os = "windows")] -use win::MumbleWinImpl as MumblePlatformImpl; -// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there -// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; -/// This primarily manages the mumble backend. -/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. -/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. -/// and every frame gets the latest mumble link data, and compares with the previous frame. -/// if any of the changed this frame, it will set the relevant changed flags so that plugins -/// or other parts of program which care can run the relevant code. -pub struct MumbleManager { - /// This abstracts over the windows and linux impl of mumble link functionality. - /// we use this to get the latest mumble link and latest window dimensions of the current mumble link - backend: MumblePlatformImpl, - /// latest mumble link - link: MumbleLink, -} -impl MumbleManager { - pub fn new(name: &str, _jokolay_window_id: Option) -> Result { - let backend = MumblePlatformImpl::new(name)?; - Ok(Self { - backend, - link: Default::default(), - }) - } - pub fn is_alive(&self) -> bool { - self.backend.is_alive() - } - - pub fn tick(&mut self) -> Result> { - if let Err(e) = self.backend.tick() { - error!(?e, "mumble backend tick error"); - return Ok(None); - } - - if !self.backend.is_alive() { - self.link.client_size.x = 0; - self.link.client_size.y = 0; - self.link.changes = BitFlags::all(); - return Ok(Some(&self.link)); - } - // backend is alive and tick is successful. time to get link - let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); - let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { - Default::default() - } else { - self.link.clone() - }; - - if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { - return Ok(None); - } - let mut changes: BitFlags = Default::default(); - // safety. as the link is valid, we can use as_ref - let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) - .into_diagnostic() - .wrap_err("failed to get widestring out of cml identity")? - .to_string() - .into_diagnostic() - .wrap_err("failed to convert widestring to cstring")?; - - let identity: ctypes::CIdentity = from_str(&json_string) - .into_diagnostic() - .wrap_err("failed to deserialize identity from json string")?; - let uisz = identity - .get_uisz() - .ok_or(miette::miette!("uisz is invalid"))?; - let server_address = if cml.context.server_address[0] == 2 { - let addr = cml.context.server_address; - std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() - } else { - std::net::Ipv4Addr::UNSPECIFIED.into() - }; - if new_link.ui_tick != cml.ui_tick { - changes.insert(MumbleChanges::UiTick); - } - if new_link.name != identity.name { - changes.insert(MumbleChanges::Character); - } - if new_link.map_id != cml.context.map_id { - changes.insert(MumbleChanges::Map); - } - let client_pos = IVec2::new(cml.context.client_pos[0], cml.context.client_pos[1]); - let client_size = UVec2::new(cml.context.client_size[0], cml.context.client_size[1]); - - if new_link.client_pos != client_pos { - changes.insert(MumbleChanges::WindowPosition); - } - if new_link.client_size != client_size { - changes.insert(MumbleChanges::WindowSize); - } - let cam_pos = cml.f_camera_position.into(); - if new_link.cam_pos != cam_pos { - changes.insert(MumbleChanges::Camera); - } - - let player_pos = cml.f_avatar_position.into(); - if new_link.player_pos != player_pos { - changes.insert(MumbleChanges::Position); - } - //let player_race = Self::get_race(identity.race); - - new_link = MumbleLink { - ui_tick: cml.ui_tick, - player_pos, - f_avatar_front: cml.f_avatar_front.into(), - cam_pos, - f_camera_front: cml.f_camera_front.into(), - name: identity.name, - map_id: cml.context.map_id, - fov: identity.fov, - uisz, - // window_pos, - // window_size, - changes, - // window_pos_without_borders, - // window_size_without_borders, - dpi_scaling: cml.context.dpi_scaling, - dpi: cml.context.dpi, - client_pos, - client_size, - map_type: cml.context.map_type, - server_address, - shard_id: cml.context.shard_id, - instance: cml.context.instance, - build_id: cml.context.build_id, - ui_state: cml.context.get_ui_state(), - compass_width: cml.context.compass_width, - compass_height: cml.context.compass_height, - compass_rotation: cml.context.compass_rotation, - player_x: cml.context.player_x, - player_y: cml.context.player_y, - map_center_x: cml.context.map_center_x, - map_center_y: cml.context.map_center_y, - map_scale: cml.context.map_scale, - process_id: cml.context.process_id, - mount: cml.context.mount_index, - race: identity.race, - }; - self.link = new_link; - - Ok(if self.link.ui_tick == 0 { - None - } else { - Some(&self.link) - }) - } -} diff --git a/crates/jokolink/src/linux/mod.rs b/crates/jokolink/src/linux/mod.rs deleted file mode 100644 index f0adab4..0000000 --- a/crates/jokolink/src/linux/mod.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; -use miette::{Context, IntoDiagnostic, Result}; -use std::fs::File; -use std::io::{Read, Seek}; -use time::OffsetDateTime; -use tracing::info; -// use x11rb::protocol::xproto::{change_property, intern_atom, AtomEnum, GetGeometryReply, PropMode}; -// use x11rb::rust_connection::ConnectError; - -pub use x11rb::rust_connection::RustConnection; - -/// This is the bak -pub struct MumbleLinuxImpl { - mfile: File, - link_buffer: LinkBuffer, - /// we basically use this as the ui_tick of mumblelink - /// If this changed recently, it means jokolink is running (i.e. gw2 is running) - previous_jokolink_timestamp: i128, -} - -type LinkBuffer = Box<[u8; C_MUMBLE_LINK_SIZE_FULL]>; - -impl MumbleLinuxImpl { - pub fn new(link_name: &str) -> Result { - let mumble_file_name = format!("/dev/shm/{link_name}"); - info!("creating mumble file at {mumble_file_name}"); - #[allow(clippy::suspicious_open_options)] - let mut mfile = File::options() - .read(true) - .write(true) // write/append is needed for the create flag - .create(true) - .open(&mumble_file_name) - .into_diagnostic() - .wrap_err("failed to create mumble file")?; - let mut link_buffer = LinkBuffer::new([0u8; C_MUMBLE_LINK_SIZE_FULL]); - mfile.rewind().into_diagnostic()?; - mfile - .read(link_buffer.as_mut()) - .into_diagnostic() - .wrap_err("failed to get link buffer from mfile")?; - let previous_jokolink_timestamp = - unsafe { CMumbleLink::get_timestamp(link_buffer.as_ptr() as _) }; - Ok(MumbleLinuxImpl { - mfile, - link_buffer, - previous_jokolink_timestamp, - }) - } - pub fn tick(&mut self) -> Result<()> { - self.mfile.rewind().into_diagnostic()?; - self.mfile - .read(self.link_buffer.as_mut()) - .into_diagnostic() - .wrap_err("failed to get link buffer")?; - self.previous_jokolink_timestamp = - unsafe { CMumbleLink::get_timestamp(self.link_buffer.as_ptr() as _) }; - Ok(()) - } - pub fn is_alive(&self) -> bool { - OffsetDateTime::now_utc().unix_timestamp_nanos() - self.previous_jokolink_timestamp - < std::time::Duration::from_secs(1).as_nanos() as i128 - } - pub fn get_cmumble_link(&self) -> CMumbleLink { - if self.is_alive() { - unsafe { std::ptr::read(self.link_buffer.as_ptr() as _) } - } else { - Default::default() - } - } - // pub fn set_transient_for(&self) -> Result<()> { - // Ok(()) - // Ok(self - // .xc - // .set_transient_for(xid_from_buffer(&self.link_buffer))?) - // } -} - -// struct X11Connection { -// jokolay_window_id: u32, -// transient_for_atom: u32, -// // net_wm_pid_atom: u32, -// xc: RustConnection, -// } -// impl X11Connection { -// pub const WM_TRANSIENT_FOR: &'static str = "WM_TRANSIENT_FOR"; -// // pub const NET_WM_PID: &'static str = "_NET_WM_PID"; -// fn new(jokolay_window_id: u32) -> Result { -// let (xc, _) = RustConnection::connect(None).expect("failed to create x11 connection"); -// let transient_for_atom = intern_atom(&xc, true, Self::WM_TRANSIENT_FOR.as_bytes()) -// .map_err(|e| X11Error::AtomQueryError { -// source: e, -// atom_str: Self::WM_TRANSIENT_FOR, -// })? -// .reply() -// .map_err(|e| X11Error::AtomReplyError { -// source: e, -// atom_str: Self::WM_TRANSIENT_FOR, -// })? -// .atom; -// // let net_wm_pid_atom = intern_atom(&xc, true, Self::NET_WM_PID.as_bytes()) -// // .map_err(|e| X11Error::AtomQueryError { -// // source: e, -// // atom_str: Self::NET_WM_PID, -// // })? -// // .reply() -// // .map_err(|e| X11Error::AtomReplyError { -// // source: e, -// // atom_str: Self::NET_WM_PID, -// // })? -// // .atom; - -// Ok(Self { -// jokolay_window_id, -// transient_for_atom, -// xc, -// // net_wm_pid_atom, -// }) -// } -// pub fn set_transient_for(&self, parent_window: u32) -> Result<(), X11Error> { -// if let Ok(xst) = std::env::var("XDG_SESSION_TYPE") { -// if xst == "wayland" { -// tracing::warn!("skipping transient_for because we are on wayland"); -// return Ok(()); -// } -// if xst != "x11" { -// tracing::warn!("xdg session type is neither wayland not x11: {xst}"); -// } -// } -// assert_ne!(parent_window, 0); -// change_property( -// &self.xc, -// PropMode::REPLACE, -// self.jokolay_window_id, -// self.transient_for_atom, -// AtomEnum::WINDOW, -// 32, -// 1, -// &parent_window.to_ne_bytes(), -// ) -// .map_err(|e| X11Error::TransientForError { -// source: e, -// parent: parent_window, -// child: self.jokolay_window_id, -// })? -// .check() -// .map_err(|e| X11Error::TransientForReplyError { -// source: e, -// parent: parent_window, -// child: self.jokolay_window_id, -// })?; -// Ok(()) -// } - -// pub fn get_window_dimensions(&self, xid: u32) -> Result<[i32; 4]> { -// assert_ne!(xid, 0); -// let geometry = x11rb::protocol::xproto::get_geometry(&self.xc, xid) -// .into_diagnostic() -// .wrap_err("get geometry fn failed")? -// .reply() -// .into_diagnostic() -// .wrap_err("geometry reply is wrong")?; -// let translated_coordinates = x11rb::protocol::xproto::translate_coordinates( -// &self.xc, -// xid, -// geometry.root, -// geometry.x, -// geometry.y, -// ) -// .into_diagnostic() -// .wrap_err("failed to translate coords")? -// .reply() -// .into_diagnostic() -// .wrap_err("translate coords reply error")?; -// let x_outer = translated_coordinates.dst_x as i32; -// let y_outer = translated_coordinates.dst_y as i32; -// let width = geometry.width; -// let height = geometry.height; - -// tracing::debug!( -// "translated_x: {}, translated_y: {}, width: {}, height: {}, geo_x: {}, geo_y: {}", -// x_outer, -// y_outer, -// width, -// height, -// geometry.x, -// geometry.y -// ); -// Ok([x_outer, y_outer, width as _, height as _]) -// } -// // pub fn get_pid_from_xid(&self, xid: u32) -> Result { -// // assert_ne!(xid, 0); - -// // let pid_prop = get_property( -// // &self.xc, -// // false, -// // xid, -// // self.net_wm_pid_atom, -// // AtomEnum::CARDINAL, -// // 0, -// // 1, -// // ) -// // .expect("coudn't get _NET_WM_PID property gw2") -// // .reply() -// // .expect("reply for _NET_WM_PID property gw2 "); - -// // if pid_prop.bytes_after != 0 -// // && pid_prop.format != 32 -// // && pid_prop.value_len != 1 -// // && pid_prop.value.len() != 4 -// // { -// // panic!("invalid pid property {:#?}", pid_prop); -// // } -// // Ok(u32::from_ne_bytes(pid_prop.value.try_into().expect( -// // "pid property value has a bytes length of less than 4", -// // ))) -// // } -// } -// pub fn get_frame_extents(xc: &RustConnection, xid: u32) -> Result<(u32, u32, u32, u32)> { -// assert_ne!(xid, 0); -// let net_frame_extents_atom = intern_atom(&self.xc, true, b"_NET_FRAME_EXTENTS") -// .expect("coudn't intern atom for _NET_FRAME_EXTENTS ")? -// .reply() -// .expect("reply for intern atom for _NET_FRAME_EXTENTS")? -// .atom; -// let frame_prop = get_property( -// &self.xc, -// false, -// xid, -// net_frame_extents_atom, -// AtomEnum::ANY, -// 0, -// 100, -// ) -// .expect("coudn't get frame property gw2")? -// .reply() -// .expect("reply for frame property gw2")?; - -// if frame_prop.bytes_after != 0 { -// bail!( -// "bytes after in frame property is {}", -// frame_prop.bytes_after -// ); -// } -// if frame_prop.format != 32 { -// bail!("frame_prop format is {}", frame_prop.format); -// } -// if frame_prop.value_len != 4 { -// bail!("frame_prop value_len is {}", frame_prop.value_len); -// } -// if frame_prop.value.len() != 16 { -// bail!("frame_prop.value.len() is {}", frame_prop.value.len()); -// } -// // avoid bytemuck dependency and just do this raw. -// let mut arr = [0u8; 4]; -// arr.copy_from_slice(&frame_prop.value[0..4]); -// let left_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[4..8]); -// let right_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[8..12]); -// let top_border = u32::from_ne_bytes(arr); -// arr.copy_from_slice(&frame_prop.value[12..16]); -// let bottom_border = u32::from_ne_bytes(arr); -// Ok((left_border, right_border, top_border, bottom_border)) -// } - -// pub fn get_gw2_pid(&mut self) -> Result { -// assert_ne!(self.gw2_window_handle, 0); -// let pid_atom = x11rb::protocol::xproto::intern_atom(&self.&self.xc, true, b"_NET_WM_PID") -// .expect("could not intern atom '_NET_WM_PID'")? -// .reply() -// .expect("reply error while interning '_NET_WM_PID'.")? -// .atom; -// let reply = x11rb::protocol::xproto::get_property( -// &self.&self.xc, -// false, -// self.gw2_window_handle, -// pid_atom, -// x11rb::protocol::xproto::AtomEnum::CARDINAL, -// 0, -// 1, -// ) -// .expect("could not request '_NET_WM_PID' for gw2 window handle ")? -// .reply() -// .expect("the reply for '_NET_WM_PID' of gw2 handle ")?; - -// let pid_format = 32; -// if pid_format != reply.format { -// bail!("pid_format is not 32. so, type is wrong"); -// } -// let pid_buffer_size = 4; -// if pid_buffer_size != reply.value.len() { -// bail!("pid_buffer is not 4 bytes"); -// } -// let value_len = 1; -// if value_len != reply.value_len { -// bail!("pid reply's value_len is not 1"); -// } -// let remaining_bytes_len = 0; -// if remaining_bytes_len != reply.bytes_after { -// bail!("we still have too many bytes remaining after reading '_NET_WM_PID'"); -// } -// let mut buffer = [0u8; 4]; -// buffer.copy_from_slice(&reply.value); -// Ok(u32::from_ne_bytes(buffer)) -// } diff --git a/crates/jokolink/src/mumble/ctypes.rs b/crates/jokolink/src/mumble/ctypes.rs deleted file mode 100644 index 72dd4ac..0000000 --- a/crates/jokolink/src/mumble/ctypes.rs +++ /dev/null @@ -1,288 +0,0 @@ -use enumflags2::BitFlags; -use miette::bail; -use serde::{Deserialize, Serialize}; - -use crate::{UISize, UIState}; - -/// The total size of the CMumbleLink struct. used to know the amount of memory to give to win32 call that creates the shared memory -pub const C_MUMBLE_LINK_SIZE_FULL: usize = std::mem::size_of::(); -/// This is how much of the CMumbleLink memory that is actually useful and updated. the rest is just zeroed out. -pub const USEFUL_C_MUMBLE_LINK_SIZE: usize = 1196; - -/// The CMumblelink is how it is represented in the memory. But we rarely use it as it is and instead convert it into MumbleLink before using it for convenience -/// Many of the fields are documentad in the actual MumbleLink struct -#[derive(Debug, Clone, Copy)] -#[repr(C)] -pub struct CMumbleLink { - //// The ui_version will always be same as mumble doesn't change. we will come back to change it IF there's a new version. - pub ui_version: u32, - //// This tick represents the update count of the link (which is usually the frame count ) since mumble was initialized. not from the start of game, but the start of mumble - pub ui_tick: u32, - //// position of the character - pub f_avatar_position: [f32; 3], - //// direction towards which the character is facing - pub f_avatar_front: [f32; 3], - //// the up direction vector of the character. - pub f_avatar_top: [f32; 3], - //// The name of the character currently logged in - pub name: [u16; 256], - //// The position of the camera - pub f_camera_position: [f32; 3], - //// The direction towards which the camera is facing - pub f_camera_front: [f32; 3], - //// The up direction for the camera - pub f_camera_top: [f32; 3], - //// This is a widestring of json containing the serialized data of [CIdentity] - pub identity: [u16; 256], - //// The [Self::context] field is 256 bytes, but the game only uses the first few bytes. - //// The first 48 bytes are used by mumble to uniquely identify the map/instance/room of the player - //// So, this field is always set to 48 bytes. - //// But gw2 writes even more data for the sake of addon functionality like minimap position etc.. - //// So, adding another 37 bytes which gw2 writes to. The total length of context is roughly 88 bytes if we consider the alignment. - pub context_len: u32, - //// 88 bytes are useful context written by gw2. Jokolink writes some more additional data beyond the 88 bytes like - //// X11 ID or window size or the timestamp when it last wrote data to this link etc.. which is useful for linux native clients like jokolay - pub context: CMumbleContext, - // Useless for now. Nothing is ever written here. - // we will just remove this field and add the size when creating shared memory. - // no point in copying more than 5kb when we only care about the first 1kb. - // pub description: [u16; 2048], -} -impl Default for CMumbleLink { - fn default() -> Self { - Self { - ui_version: Default::default(), - ui_tick: Default::default(), - f_avatar_position: Default::default(), - f_avatar_front: Default::default(), - f_avatar_top: Default::default(), - name: [0; 256], - f_camera_position: Default::default(), - f_camera_front: Default::default(), - f_camera_top: Default::default(), - identity: [0; 256], - context_len: Default::default(), - context: Default::default(), - // description: [0; 2048], - } - } -} - -impl CMumbleLink { - /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read - pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { - unsafe { std::ptr::read_volatile(link_ptr) } - } - - /// Checks if the MumbleLink memory is actually initialized by checking if [CMumbleLink::ui_tick] is non-zero. - /// Even if it returns true because [`CMumbleLink::ui_tick`] is non-zero, it could be a remnant from an older gw2 process. - /// The only way to verify that gw2 is active (with a character logged into a map), is to check if the tick changed from last frame to current frame. - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - pub unsafe fn is_valid(link_ptr: *const CMumbleLink) -> bool { - unsafe { (*link_ptr).ui_tick > 0 } - } - - /// gets uitick if we want to know the frame number since initialization of CMumbleLink - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then return value will be zero - /// 3. Even if it is not zero, the ui_tick maybe a stale because the game is dead (or in map loading screen / character select screen / cutscene) - pub unsafe fn get_ui_tick(link_ptr: *const CMumbleLink) -> u32 { - (*link_ptr).ui_tick - } - /// gets the pid from [CMumbleLink::context] field - /// # Safety - /// 1. `link_ptr` must point to valid memory atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If MumbleLink (i.e. memory referenced by link_ptr) is unintialized, then pid will be zero - /// 3. Even if it is initialized, the process could be dead and the pid may be reused for a different process now - pub unsafe fn get_pid(link_ptr: *const CMumbleLink) -> u32 { - (*link_ptr).context.process_id - } - // #[cfg(unix)] - // pub unsafe fn get_xid(link_ptr: *const CMumbleLink) -> u32 { - // (*link_ptr).context.xid - // } - // #[cfg(unix)] - // pub unsafe fn get_pos_size(link_ptr: *const CMumbleLink) -> [i32; 4] { - // (*link_ptr).context.client_pos_size - // } - /// This gets the timestamp written by `jokolink` - /// The return value is nanoseconds since unix_epoch. - /// This is an easy way to check that jokolink (and by extension gw2) is still alive even if ui_tick doesn't change. - /// This happens when gw2 is in character select screen or cutscene etc.. when ui_tick stops updating. - /// # Safety - /// 1. `link_ptr` must be valid and point to memory of atleast [USEFUL_C_MUMBLE_LINK_SIZE] bytes in size - /// 2. If it is uninitialized, the return value could be zero - #[cfg(unix)] - pub unsafe fn get_timestamp(link_ptr: *const CMumbleLink) -> i128 { - let bytes = (*link_ptr).context.timestamp; - i128::from_le_bytes(bytes) - } -} - -#[derive(Debug, Clone, Copy)] -#[repr(C)] -/// The mumble context as stored inside the context field of CMumbleLink. -/// the first 48 bytes Mumble uses for identification is upto `build_id` field -/// the rest of the fields after `build_id` are provided by gw2 for addon devs. -pub struct CMumbleContext { - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - pub server_address: [u8; 28], // contains sockaddr_in or sockaddr_in6 - /// Map ID - pub map_id: u32, - pub map_type: u32, - pub shard_id: u32, - pub instance: u32, - pub build_id: u32, - /// The fields until now are provided for mumble. - /// The rest of the data from here is what gw2 provides for the benefit of addons. - /// This is the current UI state of the game. refer to [UIState] - /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: u32, - pub compass_width: u16, // pixels - pub compass_height: u16, // pixels - pub compass_rotation: f32, // radians - pub player_x: f32, // continentCoords - pub player_y: f32, // continentCoords - pub map_center_x: f32, // continentCoords - pub map_center_y: f32, // continentCoords - pub map_scale: f32, - /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. - /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid - /// on windows, we use this to get window handle which can give us a window size. - /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid - /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 - pub process_id: u32, - /// refers to [Mount] - /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match api - pub mount_index: u8, - /// This is where the context fields provided by gw2 end. - /// From here on, these are custom fields set by jokolink.dll for the use of jokolay - /// These fields will be set before writing the link data to the `/dev/shm/MumbleLink` file from which jokolay can pick it up - /// - /// timestamp when jokolink wrote this data. unix nanoseconds - /// This timestamp will be written every frame by jokolink even if mumble link is uninitialized. - /// This is [i128] in little endian byte order. We use a byte array instead of [i128] directly because context is aligned to 4 by default. And - /// [i64]/[i128] will change that alignment to 8. This will lead to 4 bytes padding between [CMumbleLink::context_len] and [CMumbleLink::context] - /// - /// If jokolink doesn't write for more than 1 or 2 seconds, it can be safely assumed that gw2 was closed/crashed. - /// This is in nanoseconds since unix epoch in UTC timezone. - pub timestamp: [u8; 16], - /// This represents the x11 window id of the gw2 window. AFAIK, wine uses x11 only (no wayland), so this could be useful to set transient for - pub xid: u32, - /* - pub window_pos_size_without_borders: [i32; 4], - /// x, y, width, height of guild wars 2 window relative to top left corner of the screen. - /// This is populated with `GetWindowRect` fn - /// DPI aware. In screen coordinate. But includes drop shadow too :(. - pub window_pos_size: [i32; 4], - */ - /// dpi awareness of the gw2 process. Most probably will be `2` and below we have the relevant MS docs - /// DPI_AWARENESS_PER_MONITOR_AWARE - /// Value: 2 - /// Per monitor DPI aware. This process checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes. These processes are not automatically scaled by the system. - pub dpi_scaling: i32, - /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. - pub dpi: i32, - /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. - pub client_pos: [i32; 2], - pub client_size: [u32; 2], - /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. - pub padding: [u8; 96], -} -impl Default for CMumbleContext { - fn default() -> Self { - assert_eq!(std::mem::size_of::(), 256); - Self { - server_address: Default::default(), - map_id: Default::default(), - map_type: Default::default(), - shard_id: Default::default(), - instance: Default::default(), - build_id: Default::default(), - ui_state: Default::default(), - compass_width: Default::default(), - compass_height: Default::default(), - compass_rotation: Default::default(), - player_x: Default::default(), - player_y: Default::default(), - map_center_x: Default::default(), - map_center_y: Default::default(), - map_scale: Default::default(), - process_id: Default::default(), - mount_index: Default::default(), - timestamp: Default::default(), - // window_pos_size: Default::default(), - padding: [0; 96], - xid: Default::default(), - // window_pos_size_without_borders: Default::default(), - dpi_scaling: Default::default(), - dpi: Default::default(), - client_pos: Default::default(), - client_size: Default::default(), - } - } -} -impl CMumbleContext { - pub fn get_ui_state(&self) -> Option> { - BitFlags::from_bits(self.ui_state).ok() - } - - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - /// contains sockaddr_in or sockaddr_in6 - pub fn get_map_ip(&self) -> miette::Result { - if self.server_address[0] != 2 { - // add ipv6 support when gw2 servers add ipv6 support. - bail!("ipaddr parsing failed for CMumble Context"); - } - let ip = std::net::Ipv4Addr::from([ - self.server_address[4], - self.server_address[5], - self.server_address[6], - self.server_address[7], - ]); - Ok(ip) - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] -#[serde(crate = "serde")] -/// The json structure of the Identity field inside Cmumblelink. -/// the json string is null terminated and utf-16 encoded. so, need to use -/// Widestring crate's U16Cstring to first parse the bytes and then, convert to -/// String before deserializing to CIdentity -pub struct CIdentity { - /// The name of the character - pub name: String, - /// The core profession id of the character. matches the ids of v2/professions endpoint - pub profession: u32, - /// Character's third specialization, or 0 if no specialization is present. See /v2/specializations for valid IDs. - pub spec: u32, - /// The race of the character. does not match api - pub race: u32, - /// API:2/maps - pub map_id: u32, - /// useless field from pre-megaserver days. is just shard_id from context struct - pub world_id: u32, - /// Team color per API:2/colors (0 = white) - pub team_color_id: u32, - /// Whether the character has a commander tag active - pub commander: bool, - /// Vertical field-of-view - pub fov: f32, - /// A value corresponding to the user's current UI scaling. - pub uisz: u32, -} - -impl CIdentity { - pub fn get_uisz(&self) -> Option { - Some(match self.uisz { - 0 => UISize::Small, - 1 => UISize::Normal, - 2 => UISize::Large, - 3 => UISize::Larger, - _ => return None, - }) - } -} diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/jokolink/src/mumble/mod.rs deleted file mode 100644 index 560ef18..0000000 --- a/crates/jokolink/src/mumble/mod.rs +++ /dev/null @@ -1,174 +0,0 @@ -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -pub mod ctypes; -use std::net::IpAddr; - -use enumflags2::{bitflags, BitFlags}; -use glam::UVec2; -use glam::{IVec2, Vec3}; -use num_derive::FromPrimitive; -use num_derive::ToPrimitive; -use serde::Deserialize; -use serde::Serialize; - -/// As the CMumbleLink has all the fields multiple -#[derive(Clone, Debug)] -pub struct MumbleLink { - /// ui tick. (more or less represents the frame number of gw2) - pub ui_tick: u32, - /// character position - pub player_pos: Vec3, - /// direction char is facing - pub f_avatar_front: Vec3, - /// camera position - pub cam_pos: Vec3, - /// direction camera is facing - pub f_camera_front: Vec3, - /// The name of the character - pub name: String, - /// API:2/maps - pub map_id: u32, - pub map_type: u32, - /// first byte is `2` if ipv4. and `[4..7]` bytes contain the ipv4 octets. - pub server_address: IpAddr, // contains sockaddr_in or sockaddr_in6 - pub shard_id: u32, - pub instance: u32, - pub build_id: u32, - /// The fields until now are provided for mumble. - /// The rest of the data from here is what gw2 provides for the benefit of addons. - /// This is the current UI state of the game. refer to [UIState] - /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: Option>, - pub compass_width: u16, // pixels - pub compass_height: u16, // pixels - pub compass_rotation: f32, // radians - pub player_x: f32, // continentCoords - pub player_y: f32, // continentCoords - pub map_center_x: f32, // continentCoords - pub map_center_y: f32, // continentCoords - pub map_scale: f32, - /// The ID of the process that last updated the MumbleLink data. If working with multiple instances, this could be used to serve the correct MumbleLink data. - /// but jokolink doesn't care, it just updates from whatever data. so, it is upto the user to deal with the change of pid - /// on windows, we use this to get window handle which can give us a window size. - /// On linux, this is useless because this is the process ID inside wine, and not the actual linux pid - /// But, the jokolink binary uses this to get the window handle and then the X Window ID of gw2 - pub process_id: u32, - /// refers to [Mount] - /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api - //pub mount: Option, - //pub race: Race, - pub mount: u8, - pub race: u32, - - /// Vertical field-of-view - pub fov: f32, - /// A value corresponding to the user's current UI scaling. - pub uisz: UISize, - // pub window_pos: IVec2, - // pub window_size: IVec2, - // pub window_pos_without_borders: IVec2, - // pub window_size_without_borders: IVec2, - /// This is the dpi of gw2 window. 96dpi is the default for a non-hidpi monitor with scaling 1.0 - /// for a scaling of 2.0, it becomes 192 and so on. - pub dpi: i32, - /// This is whether gw2 is scaling its UI elements to match the dpi. So, if the dpi is bigger than 96, gw2 will make text/ui bigger. - /// -1 means we couldn't get the setting from gw2's config file in appdata/roaming - /// 0 means scaling is disabled (false) - /// 1 means scaling is enabled (true). - pub dpi_scaling: i32, - /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* - pub client_pos: IVec2, - /// This is the size of gw2's viewport (width/height) in screen coordinates - pub client_size: UVec2, - /// changes since last mumble link update - pub changes: BitFlags, -} -impl Default for MumbleLink { - fn default() -> Self { - Self { - ui_tick: Default::default(), - player_pos: Default::default(), - f_avatar_front: Default::default(), - cam_pos: Default::default(), - f_camera_front: Default::default(), - name: String::from("This Is Jokolay Dummy"), - map_id: Default::default(), - map_type: Default::default(), - server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), - shard_id: Default::default(), - instance: Default::default(), - build_id: Default::default(), - ui_state: Default::default(), - compass_width: Default::default(), - compass_height: Default::default(), - compass_rotation: Default::default(), - player_x: Default::default(), - player_y: Default::default(), - map_center_x: Default::default(), - map_center_y: Default::default(), - map_scale: Default::default(), - process_id: Default::default(), - mount: Default::default(), - race: u32::MAX, - fov: 2.0, - uisz: Default::default(), - dpi: Default::default(), - dpi_scaling: 96, - client_pos: Default::default(), - client_size: UVec2 { x: 1024, y: 768 }, - changes: Default::default(), - } - } -} -/// These flags represent the changes in mumble link compared to previous values -#[bitflags] -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum MumbleChanges { - UiTick = 1, - Map = 1 << 1, - Character = 1 << 2, - WindowPosition = 1 << 3, - WindowSize = 1 << 4, - Camera = 1 << 5, - Position = 1 << 6, -} - -/// represents the ui scale set in settings -> graphics options -> interface size -#[derive( - Debug, - Clone, - Default, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - FromPrimitive, - ToPrimitive, -)] -#[serde(crate = "serde")] -pub enum UISize { - Small = 0, - #[default] - Normal = 1, - Large = 2, - Larger = 3, -} - -#[bitflags] -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -/// The Uistate enum to represent the status of the UI in game -pub enum UIState { - IsMapOpen = 0b00000001, - IsCompassTopRight = 0b00000010, - DoesCompassHaveRotationEnabled = 0b00000100, - GameHasFocus = 0b00001000, - InCompetitiveGamemode = 0b00010000, - TextboxFocus = 0b00100000, - IsInCombat = 0b01000000, -} diff --git a/crates/jokolink/src/win/dll.rs b/crates/jokolink/src/win/dll.rs deleted file mode 100644 index 721b5fe..0000000 --- a/crates/jokolink/src/win/dll.rs +++ /dev/null @@ -1,490 +0,0 @@ -#![allow(non_snake_case)] - -arcdps::arcdps_export! { - name: "jokolink", - // This is just "joko" as hex bytes - sig: 0x6a6f6b6f, - init: init, - release: release, -} - -fn init() -> ::core::result::Result<(), Box> { - println!("jokolink init called by arcdps. spawning background thread for jokolink"); - unsafe { spawn_jokolink_thread() }; - Ok(()) -} -/// If no other thread has been spawned, this will spawn a new thread where jokolink will run -unsafe fn spawn_jokolink_thread() { - if d3d11::JOKOLINK_THREAD_HANDLE.is_none() { - let (quit_request_sender, quit_request_receiver) = std::sync::mpsc::sync_channel(0); - let (quit_response_sender, quit_response_receiver) = std::sync::mpsc::sync_channel(1); - - d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); - d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); - - let th = std::thread::Builder::new() - .name("jokolink thread".to_string()) - .spawn(move || { - d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); - "jokolink thread quit" - }); - match th { - Ok(handle) => { - println!("spawned jokolink thread. handle: {handle:?}"); - d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); - } - Err(e) => { - eprintln!("failed to spawn jokolink thread due to error {e:#?}"); - } - } - } else { - println!("jokolink thread has already been initialized, so skipping initialization."); - } -} -/// This is really unsafe, so we have to be careful -/// We cannot directly terminate thread because it might lead to some syncronization issues and cause a crash/deadlock -/// we HAVE to terminate the thread because otherwise, it will crash gw2 too. -/// So, we use channels to send a signal to jokolink thread to quit. -/// Then, we use another channel to wait and receive a signal that will be sent by jokolink thread when it terminates. -/// -/// We can't call `join` on the thread handle because.. like i said, it can lead to a deadlock/crash. -/// This applies whether we are loaded by game as d3d11.dll or by arcdps as an addon. -unsafe fn terminate_jokolink_thread() { - if let Some(sender) = d3d11::JOKOLINK_QUIT_REQUESTER.take() { - if let Err(e) = sender.send(()) { - eprintln!("failed to send quit signal due to error {e:#?}"); - } else { - println!("successfully sent the quit signal to the jokolink thread"); - } - } - if let Some(receiver) = d3d11::JOKOLINK_QUIT_RESPONDER.take() { - match receiver.recv() { - Ok(_) => { - println!("received quit response from jokolink thread"); - } - Err(e) => { - eprintln!("failed to receive quit response from jokolink thread. {e:#?}"); - } - } - } - if let Some(handle) = d3d11::JOKOLINK_THREAD_HANDLE.take() { - if handle.is_finished() { - println!("jokolink thread is finished"); - } else { - println!("jokolink thread is not yet finished, so waiting for it by joining the handle :(((("); - match handle.join() { - Ok(o) => { - println!("joined jokolink thread with return value: {o}"); - } - Err(e) => { - eprintln!("jokolink thread panic: {e:?}"); - } - } - } - } else { - println!("jokolink thread was never started. So, nothing to terminate"); - } -} -fn release() { - println!("jokolink release called by arcdps."); - unsafe { - terminate_jokolink_thread(); - } -} - -pub mod d3d11 { - use std::{ - sync::mpsc::{Receiver, SyncSender}, - thread::JoinHandle, - }; - - use windows::{ - core::*, - Win32::Foundation::*, - Win32::System::{ - LibraryLoader::{GetProcAddress, LoadLibraryA}, - SystemInformation::GetSystemDirectoryA, - // Threading::{CreateThread, TerminateThread, THREAD_CREATION_FLAGS}, - }, - }; - - /// Dll injection basics: - /// 1. You write a custom dll library exposing functions that match the names/signatures of the actual winapi functions - /// 2. Then, you place your custom dll library in gw2's executable directory. - /// 3. gw2 loads your dll and calls your functions thinking it is calling winapi functions. - /// 4. You will use this chance to do whatever you want, before forwarding the calls to the actual winapi functions - /// 5. So, we will load the dll from `system32` directory once. store it in [DLL_PTR] - /// 6. When a function is called, we check if the fn pointer is already loaded. If it is not, we get it from the dll pointer - static mut DLL_PTR: HMODULE = HMODULE(0); - static mut CREATE_DEVICE_FNPTR: Option< - unsafe extern "system" fn( - padapter: *mut ::core::ffi::c_void, - drivertype: i32, - software: HMODULE, - flags: u32, - pfeaturelevels: *const i32, - featurelevels: u32, - sdkversion: u32, - ppdevice: *mut *mut ::core::ffi::c_void, - pfeaturelevel: *mut i32, - ppimmediatecontext: *mut *mut ::core::ffi::c_void, - ) -> HRESULT, - > = None; - pub static mut JOKOLINK_THREAD_HANDLE: Option> = None; - - /// This is used to tell wine_main fn thread to quit. - pub static mut JOKOLINK_QUIT_REQUESTER: Option> = None; - /// This is used to wait for wine_main fn thread to quit and send us a signal - pub static mut JOKOLINK_QUIT_RESPONDER: Option> = None; - /// This function is called whenever the dll is loaded into process or thread, and whenever the dll is unloaded out of process/thread. - /// # Safety - /// Don't do *anything* complicated at all. It can easily lead to a deadlock - /// https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices - /// Improper synchronization within DllMain can cause an application to deadlock or access data or code in an uninitialized DLL. - #[no_mangle] - pub unsafe extern "system" fn DllMain( - _dll_module: HINSTANCE, - call_reason: u32, - _: *mut (), - ) -> bool { - match call_reason { - // process detach - 0 => { - // unlike attach - println!("jokolink dll is being detached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}."); - super::terminate_jokolink_thread(); - } - // process attach - 1 => { - // Sometimes, our dll might be attached/detached multiple times. And we don't want to start jokolink_thread everything time - // Instead, we only launch our jokolink thread when the D3D11CreateDevice is called - println!("jokolink dll has been attached. WINE_MAIN_THREAD_HANDLE is {JOKOLINK_THREAD_HANDLE:?}"); - } - // thread attach and detach - 2 | 3 => { - // no need to do anything for thread attach and thread detach - } - // invalid values - rest => { - eprintln!("unrecognized dll main call reason: {rest}"); - } - } - true - } - /// This is the function we will "hook" into. - /// GW2 will call this function right after the "login window" when creating the main window - /// This is where we initialize our jokolink thread. - /// # Safety - /// Just need to load d3d11.dll from windows/system32 equivalent directory and call that function for gw2 - #[no_mangle] - pub unsafe extern "system" fn D3D11CreateDevice( - padapter: *mut ::core::ffi::c_void, - drivertype: i32, - software: HMODULE, - flags: u32, - pfeaturelevels: *const i32, - featurelevels: u32, - sdkversion: u32, - ppdevice: *mut *mut ::core::ffi::c_void, - pfeaturelevel: *mut i32, - ppimmediatecontext: *mut *mut ::core::ffi::c_void, - ) -> HRESULT { - if DLL_PTR.is_invalid() { - let mut path = [0u8; MAX_PATH as _]; - let len = GetSystemDirectoryA(Some(&mut path)) as usize; - // we make sure that len is not zero. It means that GetSystemDirectoryA fn didn't fail. - // we also check if length is above 200, because then we might be reaching the limit of maximum path length supported by windows. - if len == 0 || len > 200 { - eprintln!("the system directory path size is: {len}. So, i am quitting"); - return HRESULT::default(); - } - const D3D11_DLL_PATH: &str = "\\d3d11.dll\0"; - path[len..(len + D3D11_DLL_PATH.len())].copy_from_slice(D3D11_DLL_PATH.as_bytes()); - - match LoadLibraryA(PCSTR::from_raw(path.as_ptr())) { - Ok(p) => { - println!("successfully loaded library d3d11.dll "); - DLL_PTR = p; - } - Err(e) => { - eprintln!("could not load d3d11.dll from system path due to error: {e:#?}"); - return HRESULT::default(); - } - } - } else { - println!("d3d11.dll library is already loaded. So, skipping that"); - } - if CREATE_DEVICE_FNPTR.is_none() { - if let Some(p) = GetProcAddress(DLL_PTR, PCSTR("D3D11CreateDevice\0".as_ptr())) { - println!("successfully got proc address of D3D11CreateDevice"); - let _ = CREATE_DEVICE_FNPTR.insert(std::mem::transmute(p)); - } else { - eprintln!("could not load address of D3D11CreateDevice"); - } - } else { - println!("D3D11CreateDevice fn ptr is already loaded, so skipped that"); - } - if JOKOLINK_THREAD_HANDLE.is_none() { - println!("starting jokolink's wine_main on another thrad"); - - super::spawn_jokolink_thread(); - } - println!("calling D3D11CreateDevice fn"); - if let Some(p) = CREATE_DEVICE_FNPTR { - p( - padapter, - drivertype, - software, - flags, - pfeaturelevels, - featurelevels, - sdkversion, - ppdevice, - pfeaturelevel, - ppimmediatecontext, - ) - } else { - HRESULT::default() - } - } - - // unsafe extern "system" fn wine_main(_: *mut ::core::ffi::c_void) -> u32 { - // super::spawn_jokolink_thread(); - // 0 - // } - pub mod wine { - use crate::mumble::ctypes::*; - use crate::win::MumbleWinImpl; - use crate::DEFAULT_MUMBLELINK_NAME; - use miette::{Context, IntoDiagnostic, Result}; - use serde::{Deserialize, Serialize}; - use std::io::Write; - use std::io::{Seek, SeekFrom}; - use std::path::{Path, PathBuf}; - use std::str::FromStr; - use std::sync::mpsc::{Receiver, SyncSender}; - use std::time::Duration; - use tracing::{error, info}; - use tracing_subscriber::filter::LevelFilter; - #[derive(Debug, Clone, Serialize, Deserialize)] - #[serde(default)] - pub struct JokolinkConfig { - pub loglevel: String, - pub logdir: PathBuf, - pub mumble_link_name: String, - pub interval: u32, - pub copy_dest_dir: PathBuf, - } - - impl Default for JokolinkConfig { - fn default() -> Self { - Self { - loglevel: "info".to_string(), - logdir: PathBuf::from("."), - mumble_link_name: DEFAULT_MUMBLELINK_NAME.to_string(), - interval: 5, - copy_dest_dir: PathBuf::from("z:\\dev\\shm"), - } - } - } - - pub fn wine_main( - quit_request_receiver: Receiver<()>, - quit_response_sender: SyncSender<()>, - ) { - if let Err(e) = std::panic::catch_unwind(move || { - let config = "./jokolink_config.json".to_string(); - let config = std::path::PathBuf::from(config); - if !config.exists() { - match std::fs::File::create(&config) { - Ok(mut f) => match serde_json::to_string_pretty(&JokolinkConfig::default()) - { - Ok(config_string) => { - if let Err(e) = f.write_all(config_string.as_bytes()) { - eprintln!( - "failed to write default config file due to error {e:#?}" - ); - } - } - Err(e) => { - eprintln!("failed to serialize default config due to error {e:#?}"); - } - }, - Err(e) => eprintln!("failed to create config.json due to error {e:#?}"), - } - } - let config: JokolinkConfig = match std::fs::File::open(&config) { - Ok(f) => match serde_json::from_reader(std::io::BufReader::new(f)) { - Ok(config) => config, - Err(e) => { - eprintln!("failed to deserialize config file due to error {e:#?}"); - return; - } - }, - Err(e) => { - eprintln!("failed to open config file due to error {e:#?}"); - return; - } - }; - println!("successfully loaded configuration file"); - match miette::set_hook(Box::new(|_| { - Box::new( - miette::MietteHandlerOpts::new() - .unicode(true) - .context_lines(4) - .with_cause_chain() - .build(), - ) - })) { - Ok(_) => { - println!("miette hook set"); - } - Err(e) => { - eprintln!("failed to set miette hook due to {e:#?}"); - } - } - let guard = match log_init( - LevelFilter::from_str(&config.loglevel).unwrap_or(LevelFilter::INFO), - &config.logdir, - Path::new("jokolink.log"), - ) { - Ok(g) => g, - Err(e) => { - eprintln!("failed to initiailize logging due to error {e:#?}"); - return; - } - }; - if let Err(e) = fake_main(config, quit_request_receiver) { - eprintln!("fake main exited due to error: {e:#?}"); - } - std::mem::drop(guard); - println!("dropped logfile guard"); - }) { - eprintln!("There was a panic in jokolink thread: {e:?}"); - } - println!("exiting wine_main function"); - match quit_response_sender.send(()) { - Ok(_) => { - println!("successfully sent quit response"); - } - Err(e) => { - eprintln!("failed to send quit response due to: {e:#?}"); - } - } - } - - fn fake_main(config: JokolinkConfig, quit_signal: Receiver<()>) -> Result<()> { - let refresh_inverval = Duration::from_millis(config.interval as u64); - - info!("Application Name: {}", env!("CARGO_PKG_NAME")); - info!("Application Version: {}", env!("CARGO_PKG_VERSION")); - info!("Application Authors: {}", env!("CARGO_PKG_AUTHORS")); - info!( - "Application Repository Link: {}", - env!("CARGO_PKG_REPOSITORY") - ); - info!("Application License: {}", env!("CARGO_PKG_LICENSE")); - - // info!("git version details: {}", git_version::git_version!()); - - info!( - "the file log lvl: {:?}, the logfile directory: {:?}", - &config.loglevel, &config.logdir - ); - info!("created app and initialized logging"); - info!("the mumble link names: {:#?}", &config.mumble_link_name); - info!( - "the mumble refresh interval in milliseconds: {:#?}", - refresh_inverval - ); - - info!( - "the path to which we write mumble data: {:#?}", - &config.copy_dest_dir - ); - let mumble_key = config.mumble_link_name.clone(); - - let dest_path = config.copy_dest_dir.join(&mumble_key); - - // create a shared memory file in /dev/shm/mumble_link_key_name so that jokolay can mumble stuff from there. - info!( - "creating the path to destination shm file: {:?}", - &dest_path - ); - - #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] - let mut mfile = std::fs::File::options() - .write(true) - .create(true) - .open(&dest_path) - .into_diagnostic() - .wrap_err_with(|| { - format!("failed to create shm file with path {:#?}", &dest_path) - })?; - // create shared memory using the mumble link key - let mut source = MumbleWinImpl::new(&mumble_key)?; - - loop { - if let Err(e) = source.tick() { - error!(?e, "mumble tick error"); - } - let link = source.get_cmumble_link(); - - let buffer: [u8; C_MUMBLE_LINK_SIZE_FULL] = - unsafe { std::ptr::read_volatile(&link as *const CMumbleLink as *const _) }; - mfile - .seek(SeekFrom::Start(0)) - .into_diagnostic() - .wrap_err("could not seek to start of shared memory file due to error")?; - - // write buffer to the file - mfile - .write(&buffer) - .into_diagnostic() - .wrap_err("could not write to shared memory file due to error")?; - match quit_signal.try_recv() { - Ok(_) => { - println!("received quit signal. returning from wine_main()"); - error!("received quit signal. returning from wine_main()"); - return Ok(()); - } - Err(e) => match e { - std::sync::mpsc::TryRecvError::Empty => {} - std::sync::mpsc::TryRecvError::Disconnected => { - eprintln!("why is the quit signaller sender disconnected????"); - } - }, - } - // we sleep for a few milliseconds to avoid reading mumblelink too many times. we will read it around 100 to 200 times per second - std::thread::sleep(refresh_inverval); - } - } - - /// initializes global logging backend that is used by log macros - /// Takes in a filter for stdout/stderr, a filter for logfile and finally the path to logfile - pub fn log_init( - file_filter: LevelFilter, - log_directory: &Path, - log_file_name: &Path, - ) -> Result { - // let file_appender = tracing_appender::rolling::never(log_directory, log_file_name); - let file_path = log_directory.join(log_file_name); - let writer = std::io::BufWriter::new( - std::fs::File::create(&file_path) - .into_diagnostic() - .wrap_err_with(|| { - format!("failed to create logfile at path: {:#?}", &file_path) - })?, - ); - let (nb, guard) = tracing_appender::non_blocking(writer); - tracing_subscriber::fmt() - .with_writer(nb) - .with_max_level(file_filter) - .pretty() - .with_ansi(false) - .init(); - - Ok(guard) - } - } -} diff --git a/crates/jokolink/src/win/mod.rs b/crates/jokolink/src/win/mod.rs deleted file mode 100644 index 21ebc75..0000000 --- a/crates/jokolink/src/win/mod.rs +++ /dev/null @@ -1,735 +0,0 @@ -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -pub mod dll; -//putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration - -use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; -use miette::{bail, Context, IntoDiagnostic, Result}; -use notify::Watcher; -use std::{ - path::PathBuf, - str::FromStr, - time::{Duration, Instant}, -}; -use time::OffsetDateTime; -use tracing::{debug, error, info, warn}; -use windows::{ - core::PCSTR, - Win32::{ - Foundation::*, - Graphics::{ - Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}, - Gdi::ClientToScreen, - }, - System::{ - Com::CoTaskMemFree, - Memory::*, - Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}, - }, - UI::{ - HiDpi::{GetDpiForWindow, GetProcessDpiAwareness}, - Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath}, - WindowsAndMessaging::*, - }, - }, -}; - -/// This source will be the used to abstract the linux/windows way of getting MumbleLink -/// on windows, this represents the shared memory pointer to mumblelink, and as long as one of gw2 or a client like us is alive, the shared memory will stay alive -/// on linux, this will be a File in /dev/shm that will only exist if jokolink created it at some point in time. this lives in ram, so reading from it is pretty much free. -#[derive(Debug)] -pub struct MumbleWinImpl { - /// This is the pointer to shared memory which we mapped into our address space - /// This is NEVER null. Because we consider failing to create MumbleLink as a hard error. - /// ## Unsafe: - /// Must unmap this pointer when we are dropping - link_ptr: *const CMumbleLink, - /// This is the handle to shared memory. We must close the handle when we are quitting - /// This also never invalid. Because we consider failing to create MumbleLink as a hard error. - /// ## Unsafe: - /// Must close this handle when we are dropping - mumble_handle: HANDLE, - /// this is the previous ui_tick. We use this to check if there has been any change in mumble link memory - /// If there is a change, then we check if the new pid is the same as old pid - previous_ui_tick: u32, - /// This is the previous pid of the mumble link - /// If the current pid has changed, then it means we are dealing with a new gw2 process. - previous_pid: u32, - /// This is the process handle for gw2. - /// when we see a change in pid, we will close the handle (if its valid) and open a new handle to the new gw2 process - /// - /// This handle is very important, because its validity shows that the gw2 process is "alive". - /// If ui_tick has not changed for more than a second, then we will check using windows api if the process is still alive. - /// If not, we will reset everything in our struct except for last_pid and last_ui_tick. - process_handle: HANDLE, - /// if ui_tick updates, we set this to now. - /// If ui_tick doesn't update for more than 1 second AND we are alive, we will check if gw2 is still alive and reset the timestamp. - last_ui_tick_update: Instant, - /// if ui_tick changes this frame and we are alive, we get window size/pos of gw2 and reset this. - /// if we are not alive, then we simply skip this check. - last_pos_size_check: Instant, - - /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. - client_pos: [i32; 2], - client_size: [u32; 2], - /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - /// 0 for false - /// 1 for true - /// -1 for no idea. maybe because we couldn't find the config or read it or whatever. - /// I recommend just assuming that it is true when in doubt. Because the text is too small to read when dpi scaling is turned off. - dpi_scaling: i32, - /// DPI of the gw2 window - /// We get this via win32 api - dpi: i32, - /// This is the window handle of gw2. - /// This is automatically set when we try to get window size/pos. and will be reset if gw2 process dies or if we find a new gw2 process. - window_handle: isize, - /// X11 window id. This is only useful for jokolink when it is run as dll on wine - /// When the struct is initialized, we also try to get xid. and keep it here. On windows, we will just keep it at zero. - xid: u32, - /// This is the $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - /// But we get this programmatically via ShGetKnownFolderPath - _gw2_config_watcher: notify::RecommendedWatcher, - gw2_config_changed: std::sync::Arc, - gw2_config_path: PathBuf, /* - /// This is the position and size of gw2 window. This also includes a few hidden pixels around gw2 which serve as the border - /// Every time we check if the process is alive - window_pos_size: [i32; 4], - /// same as above. But we use DwmGetWindowAttribute, to exclude the drop shadow borders from the window rect - window_pos_size_without_borders: [i32; 4], - */ -} - -unsafe impl Send for MumbleWinImpl {} - -impl MumbleWinImpl { - pub fn new(key: &str) -> Result { - unsafe { - let (handle, link_ptr) = - create_link_shared_mem(key).wrap_err("failed to create mumblelink shm ")?; - let gw2_config_changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let gw2_config_path = { - let roaming_appdata_pwstr = SHGetKnownFolderPath( - &FOLDERID_RoamingAppData as *const _, - Default::default(), - HANDLE::default(), - ) - .into_diagnostic() - .wrap_err("failed to get known folder roaming app data path")?; - - let mut roaming_str = roaming_appdata_pwstr - .to_string() - .into_diagnostic() - .wrap_err("appdata/roaming is not a utf-8 path")?; - info!(roaming_str, "RoamingAppData path"); - CoTaskMemFree(Some(roaming_appdata_pwstr.0 as _)); - if !roaming_str.ends_with('\\') { - roaming_str.push('\\'); - } - roaming_str.push_str("Guild Wars 2\\GFXSettings.Gw2-64.exe.xml"); - info!(roaming_str, "gw2 config path"); - roaming_str - }; - let gw2_config_path = std::path::PathBuf::from_str(&gw2_config_path) - .into_diagnostic() - .wrap_err("failed to create pathbuf from gw2 config path in roaming appdata")?; - std::fs::create_dir_all(gw2_config_path.parent().unwrap()) - .into_diagnostic() - .wrap_err("failed to create gw2 config dir in appdata roaming ")?; - if !gw2_config_path.exists() { - std::fs::File::create(&gw2_config_path) - .into_diagnostic() - .wrap_err("failed to create empty gw2 config file ")?; - } - let dpi_scaling = check_dpi_scaling_enabled(&gw2_config_path); - - info!( - ?dpi_scaling, - ?gw2_config_path, - "dpi scaling when we are starting out" - ); - // lets just assume that the scaling is true by default - let dpi_scaling = dpi_scaling.unwrap_or(1); - gw2_config_changed.store(false, std::sync::atomic::Ordering::Relaxed); - let gw2_config_changed_2 = gw2_config_changed.clone(); - let mut gw2_config_watcher = notify::recommended_watcher(move |ev| { - debug!(?ev, "gw2 config changed"); - gw2_config_changed_2.store(true, std::sync::atomic::Ordering::Relaxed); - }) - .into_diagnostic() - .wrap_err("failed to create gw2 config directory watcher")?; - gw2_config_watcher - .watch(&gw2_config_path, notify::RecursiveMode::NonRecursive) - .into_diagnostic() - .wrap_err("faield to watch gw2 config dir")?; - - Ok(Self { - link_ptr, - mumble_handle: handle, - window_handle: 0, - last_ui_tick_update: Instant::now(), - previous_ui_tick: CMumbleLink::get_ui_tick(link_ptr), - // window_pos_size: [0; 4], - process_handle: HANDLE::default(), - previous_pid: 0, - xid: 0, - last_pos_size_check: Instant::now(), - // window_pos_size_without_borders: [0; 4], - dpi_scaling, - client_pos: [0; 2], - client_size: [0; 2], - dpi: 0, - _gw2_config_watcher: gw2_config_watcher, - gw2_config_changed, - gw2_config_path, - }) - } - } - pub fn is_alive(&self) -> bool { - !self.process_handle.is_invalid() - } - pub fn get_cmumble_link(&mut self) -> CMumbleLink { - let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; - link.context.timestamp = OffsetDateTime::now_utc() - .unix_timestamp_nanos() - .to_le_bytes(); - // link.context.window_pos_size = self.window_pos_size; - // link.context.window_pos_size_without_borders = self.window_pos_size_without_borders; - link.context.dpi_scaling = self.dpi_scaling; - link.context.dpi = self.dpi; - link.context.xid = self.xid; - link.context.client_pos = self.client_pos; - link.context.client_size = self.client_size; - link - } - /// This is the most important function which will be called every frame - /// 1. it gets the ui_tick from the link pointer - /// 2. checks if it has changed compared to previous ui_tick. If it didn't change, then we have nothing to do and we return. - /// 3. If it changed, we check if it is less than previous_ui_tick OR if the pid is differnet from previous_pid or if our process handle is invalid - /// 4. If any of the above conditions are true, we reset and reinitialize the gw2 process handle + window handle + window size etc.. - /// 5. If ui_tick simply increased and nothing else changed, then we proceed with the usual stuf which is check the timer and get updated window pos/size - pub fn tick(&mut self) -> Result<()> { - unsafe { - // if ui_tick is zero, we return - if !CMumbleLink::is_valid(self.link_ptr) { - // if we alive, that means ui_tick turned zero this frame for whatever reason, so we reset. - if self.is_alive() { - self.reset(); - } - return Ok(()); - } - let ui_tick = CMumbleLink::get_ui_tick(self.link_ptr); - let pid = CMumbleLink::get_pid(self.link_ptr); - let previous_ui_tick = self.previous_ui_tick; - // if ui tick didn't change. Then it means either we are in loading scree / character select screen or gw2 was closed (or crashed) - if ui_tick == previous_ui_tick { - // if we are not alive, then we just return because it just means mumble is not being updated. - // but if we are alive, then we need to check whehter gw2 is still alive (in loading screen) or dead - if self.is_alive() { - // we don't want to check every frame. Instead, we check in intervals of 3 seconds until gw2 finally loads into a map or it closes (so we can reset) - if self.last_ui_tick_update.elapsed() > Duration::from_secs(3) { - self.last_ui_tick_update = Instant::now(); - match check_process_alive(self.process_handle) { - Ok(alive) => { - if !alive { - self.reset(); - } - } - Err(e) => { - error!(?e, "failed to get GetExitCodeProcess"); - self.reset(); - } - } - } - } - return Ok(()); - } - // if ui_tick has changed, then we have some stuff to do. - if ui_tick < previous_ui_tick // only happens if process changes - || pid != self.previous_pid // gw2 process changed. need to get new handles/sizes etc.. - || !self.is_alive() - // if we are in reset status, then its our chance to reinitialize because mumble just updated. - { - info!(ui_tick, notify = 2u64, "found new gw2 process"); - self.reinitialize(); - } - // if reinitialization failed, then we can try again next frame. - // if we are alive, that means everything is working as expected. - // we update the previous ui_tick and check if we need to update window pos/size - if self.is_alive() { - self.last_ui_tick_update = Instant::now(); - self.previous_ui_tick = ui_tick; - // check in 2 seconds intervals because it rarely changes - if self.last_pos_size_check.elapsed() > Duration::from_secs(2) { - self.last_pos_size_check = Instant::now(); - - // self.window_pos_size = match get_window_pos_size(self.window_handle) { - // Ok(window_pos_size) => { - // if self.window_pos_size != window_pos_size { - // info!( - // ?self.window_pos_size, ?window_pos_size, - // "window position size changed" - // ); - // } - // window_pos_size - // } - // Err(e) => { - // error!(?e, "failed to get window position size"); - // self.reset(); // go back to being dead because it shouldn't usually fail - // return Ok(()); - // } - // }; - // let dpi_awareness = match GetProcessDpiAwareness(self.process_handle) { - // Ok(dpi) => dpi.0, - // Err(e) => { - // error!(?e, "failed to get dpi awareness"); - // 0 - // } - // }; - // if self.dpi_scaling != dpi_awareness { - // info!(dpi_awareness, self.dpi_scaling, "dpi scaling changed"); - // } - // self.dpi_scaling = dpi_awareness; - - let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; - if dpi != self.dpi { - info!(dpi, self.dpi, "dpi changed for gw2 window"); - } - if dpi == 0 { - error!(dpi, "invalid dpi value for guild wars 2"); - } - self.dpi = dpi; - // if the config changed, we will attempt to read dpi scaling. - // if we fail, we will just ignore it, and try again during next check of window pos (2 secs?) - // if we succeed, we will store false in the atomic bool - if self - .gw2_config_changed - .load(std::sync::atomic::Ordering::Relaxed) - { - match check_dpi_scaling_enabled(&self.gw2_config_path) { - Ok(dpi_scaling) => { - if self.dpi_scaling != dpi_scaling { - info!(self.dpi_scaling, dpi_scaling, "dpi scaling changed"); - } - self.dpi_scaling = dpi_scaling; - self.gw2_config_changed - .store(false, std::sync::atomic::Ordering::Relaxed); - } - Err(e) => { - error!(notify = 0.0f64, ?e, "failed to open gw2 config file to check for dpi scaling changes"); - } - } - } - // self.window_pos_size_without_borders = - // match get_window_pos_size_without_borders(HWND(self.window_handle)) { - // Ok(window_pos_size_without_borders) => { - // if self.window_pos_size_without_borders - // != window_pos_size_without_borders - // { - // info!( - // ?self.window_pos_size_without_borders, - // ?window_pos_size_without_borders, - // "window position size changed" - // ); - // } - // window_pos_size_without_borders - // } - // Err(e) => { - // error!(?e, "failed to get window position size"); - // self.reset(); // go back to being dead because it shouldn't usually fail - // return Ok(()); - // } - // }; - match get_client_rect_in_screen_coords(HWND(self.window_handle)) { - Ok((client_pos, client_size)) => { - if self.client_pos != client_pos || self.client_size != client_size { - info!( - ?self.client_pos, - ?client_pos, - ?self.client_size, - ?client_size, - "window position or size changed" - ); - } - self.client_pos = client_pos; - self.client_size = client_size; - } - Err(e) => { - error!(?e, "failed to get client position size"); - self.reset(); // go back to being dead because it shouldn't usually fail - return Ok(()); - } - }; - } - } - } - Ok(()) - } - /// A function which clears all the gw2 related resources like process/window handles - unsafe fn reset(&mut self) { - warn!("resetting mumble data"); - self.window_handle = 0; - if !self.process_handle.is_invalid() { - if let Err(e) = CloseHandle(self.process_handle) { - error!(?e, "failed to close process handle of old gw2"); - } - } - self.process_handle = HANDLE::default(); - // self.window_pos_size = [0; 4]; - // self.window_pos_size_without_borders = [0; 4]; - self.dpi = 0; - self.client_pos = [0; 2]; - self.client_size = [0; 2]; - self.previous_pid = 0; - self.xid = 0; - } - unsafe fn reinitialize(&mut self) { - warn!("we are reinitializing our mumble data"); - info!( - "printing cmumblelink as it might be useful for debugging. {:?}", - self.get_cmumble_link() - ); - assert!( - CMumbleLink::is_valid(self.link_ptr), - "attempting to reinitialize when mumble is still unintialized" - ); - let pid = CMumbleLink::get_pid(self.link_ptr); - assert!(pid != 0, "attempting to initialize with pid == 0"); - self.reset(); - info!( - "ui_tick: {}. pid: {pid}", - CMumbleLink::get_ui_tick(self.link_ptr) - ); - match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { - Ok(process_handle) => { - info!("got process handle: {process_handle:?}"); - // get pid from mumble link - let mut window_handle = pid as isize; - - // enumerate windows and get the handle and assign it to the pid variable if the process id of the handle actually matches the pid - let _ = EnumWindows( - Some(get_handle_by_pid), - LPARAM(((&mut window_handle) as *mut isize) as isize), - ); - // if lparam_pid is still the same as pid, then we couldn't find the relevant window handle - if window_handle == pid as isize { - if let Err(e) = CloseHandle(process_handle) { - error!( - ?e, - "failed to close process handle when we couldn't get window handle." - ); - } - error!( - "failed to initialize mumble data because we couldn't find window handle" - ); - return; - } - info!("found window handle too. yay"); - // now we have both process_handle and window_handle. We just need the window size to initialize our struct - // this function only gets the suface/viewport pos/size without any borders/decoraitons. - match get_client_rect_in_screen_coords(HWND(window_handle)) { - Ok((client_pos, client_size)) => { - // this block is purely for logging purposes only to verify that all sizes are working properly. - { - // GetWindowRect includes drop shadow borders and titlebar - match get_window_pos_size(window_handle) { - Ok(pos_size) => { - info!( - ?pos_size, - "get window position and size using GetWindowRect" - ); - } - Err(e) => { - error!(?e, "failed to initialize mumble data because we coudln't get window position and size"); - } - } - // DwmGetWindowAttribute doesn't include drop shadow borders, but includes titlebar - match get_window_pos_size_without_borders(HWND(window_handle)) { - Ok(window_pos_size_without_borders) => { - info!(?window_pos_size_without_borders, "got window pos/size without borders using DwmGetWindowAttribute"); - } - Err(e) => { - error!( - ?e, - "failed to get window position size without borders" - ); - } - }; - } - // only useful in wine - match std::ffi::CString::new("__wine_x11_whole_window") { - Ok(atom_string) => { - let xid = - GetPropA(HWND(window_handle), PCSTR(atom_string.as_ptr() as _)); - // check if the xid is actually null - if xid.is_invalid() { - // will happen on windows. But this is harmless - info!(?xid, "xid is invalid. This is completely fine on windows. This is only for linux users"); - } else { - info!("found xid too <3. {xid:?}"); - self.xid = xid - .0 - .try_into() - .map_err(|e| { - error!( - ?e, - ?xid, - "failed to fit x11 window id into u32" - ); - }) - .unwrap_or_default(); - } - } - Err(e) => { - error!(?e, notify = 0u64, "impossible. But __wine_x11_whole_window apparently not a valid cstring."); - } - } - // again, just for logging purposes and verify against lutris settings of dpi - let dpi_awareness = match GetProcessDpiAwareness(process_handle) { - Ok(dpi) => dpi.0, - Err(e) => { - error!(?e, "failed to get dpi awareness"); - 0 - } - }; - let dpi = GetDpiForWindow(HWND(self.window_handle)) as i32; - if dpi != self.dpi { - info!(dpi, self.dpi, "dpi changed for gw2 window"); - } - info!( - ?client_pos, - ?client_size, - dpi_awareness, - dpi, - pid, - ?process_handle, - ?window_handle, - "reinitialization complete " - ); - self.process_handle = process_handle; - self.window_handle = window_handle; - self.dpi = dpi; - self.client_pos = client_pos; - self.client_size = client_size; - self.last_ui_tick_update = Instant::now(); - self.previous_pid = pid; - } - Err(e) => { - error!(?e, "failed to get client rect"); - } - } - } - Err(e) => { - error!(?e, pid, "failed to open process handle"); - } - } - } -} - -fn check_dpi_scaling_enabled(path: &std::path::Path) -> Result { - // from $USER/AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml - // life is too short to parse an xml out of this file. just find the following strings - const DPI_SCALING_TRUE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="true"#; - const DPI_SCALING_FALSE: &str = r#"dpiScaling" Registered="True" Type="Bool" Value="false"#; - let contents = std::fs::read_to_string(path) - .into_diagnostic() - .wrap_err("failed to read gw2 file")?; - - if contents.contains(DPI_SCALING_FALSE) { - return Ok(0); - }; - if contents.contains(DPI_SCALING_TRUE) { - return Ok(1); - }; - error!(contents, "failed to read dpi scaling from gw2 config file"); - Ok(-1) -} -/// This function creates/opens the shared memory with the key as the name. -/// Then, it maps the shared memory into the address space of our process. -/// Finally, we are provided the Handle of shared memory and the pointer to the starting address of the mapped memory. -/// can fail if -/// 1. key is not a valid cstring -/// 2. creating shared memory fails -/// 3. mapping shared memory into our addres space fails and we get a null pointer instead -unsafe fn create_link_shared_mem(key: &str) -> Result<(HANDLE, *mut CMumbleLink)> { - info!("creating MumbleLink shared memory: {key}"); - // prepare the key as a cstr to pass to windows functions - let key_cstr = std::ffi::CString::new(key) - .into_diagnostic() - .wrap_err(miette::miette!("invalid mumble link name {key}"))?; - unsafe { - // create a Mumble Link shared memory file - // the file handle will need not be stored because when process exits, the handle will be dropped by windows - let file_handle = CreateFileMappingA( - INVALID_HANDLE_VALUE, - None, - PAGE_READWRITE, - 0, - C_MUMBLE_LINK_SIZE_FULL as u32 + 4096, // we add the size of description field here. - PCSTR(key_cstr.as_ptr() as _), - ) - .into_diagnostic() - .wrap_err("failed to create file mapping for MumbleLink")?; - // map the shared memory into the address space of our process using the handle we got from creating the shm - let cml_ptr = MapViewOfFile( - file_handle, - FILE_MAP_ALL_ACCESS, - 0, - 0, - C_MUMBLE_LINK_SIZE_FULL + 4096, // adding the description field size here - ) - .Value; - // check if we were successful - if cml_ptr.is_null() { - bail!( - "could not map view of file, error code: {:#?}", - GetLastError() - ) - } - Ok((file_handle, cml_ptr.cast())) - } -} - -unsafe fn check_process_alive(process_handle: HANDLE) -> Result { - let mut exit_code = 0u32; - GetExitCodeProcess(process_handle, &mut exit_code as *mut u32) - .into_diagnostic() - .wrap_err("failed to get exit code of process ")?; - Ok(exit_code == STATUS_PENDING.0 as u32) - - // this is slightly faster than using the GetExitCodeProcess method. - // GetExitCodeProcess takes around 3 us on average with lowest being 2.5 us. - // WaitForSingleObject takes around 2 us on average withe lowest being 1.5 us. - // let result = unsafe { WaitForSingleObject(process_handle, 0) }; - - // if result == WAIT_ABANDONED || result == WAIT_OBJECT_0 { - // Ok(false) - // } else if result == WAIT_TIMEOUT.0 { - // Ok(true) - // } else { - // bail!("WaitForSingleObject returned code: {:#?}", result) - // } -} -/// This function gets called by EnumWindows as a lambda function. it will be given a handle to all windows one by one, -/// and the pid of the process we want to match against that handle's pid. if handle's pid is matched against our pid, we will -/// assign the handle to our pid pointer so that the they can use it after EnumWindows returns -unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LPARAM) -> BOOL { - // gw2_pid is a long pointer TO a HWND. we cast gw2_pid from isize to a * mut isize. - let local_gw2_pid = *(gw2_pid_ptr.0 as *mut isize); - - // make a varible to hold the process id of a window handle given to us. - let mut window_handle_pid: u32 = 0; - // get the process id of the handle and then store it in the handle_pid variable. - GetWindowThreadProcessId(window_handle, Some((&mut window_handle_pid) as *mut u32)); - // if handle_pid is null, it means we failed to get the pid. so, we return true so that enumWindows can call us again with the handle to the next window. - if window_handle_pid == 0 { - info!("failed to get process id of window handle {window_handle:?}"); - return BOOL(1); - } - - info!("window handle {window_handle:?} has pid {window_handle_pid}"); - - // we check if the pid which gw2_pid references is equal to handle_pid - if local_gw2_pid == window_handle_pid as isize { - info!( - "successfully found the handle: {window_handle:?} of our gw2 with pid {local_gw2_pid}" - ); - // we now assign the window_handle to the memory pointed by gw2_pid pointer. - *(gw2_pid_ptr.0 as *mut isize) = window_handle.0; - return BOOL(0); - } - BOOL(1) -} -/// Quirk: GetWindowRect also includes the invisible "borders" which windows uses for resizing or whatever -/// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. -/// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). -/// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. -fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { - bail!("GetWindowRect call failed {e:#?}"); - } - let pos = [rect.left, rect.top]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = DwmGetWindowAttribute( - window_handle, - DWMWA_EXTENDED_FRAME_BOUNDS, - &mut rect as *mut RECT as _, - std::mem::size_of::() as _, - ) { - bail!("DwmGetWindowAttribute call failed {e:#?}"); - } - let pos = [rect.left, rect.top]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { - unsafe { - let mut rect: RECT = RECT { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if let Err(e) = GetClientRect(window_handle, &mut rect as *mut RECT) { - bail!("GetClientRect call failed {e:#?}"); - } - let mut point: POINT = POINT { - x: rect.left, - y: rect.top, - }; - if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { - bail!("ClientToScreen call failed"); - } - let pos = [point.x, point.y]; - let size = [ - (rect.right - rect.left) as u32, - (rect.bottom - rect.top) as u32, - ]; - Ok((pos, size)) - } -} -impl Drop for MumbleWinImpl { - fn drop(&mut self) { - unsafe { - warn!("dropping mumble link windows impl"); - if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: self.link_ptr as _, - }) { - error!(?e, "failed to unmap view of mumble file"); - } - if let Err(e) = CloseHandle(self.mumble_handle) { - error!(?e, "failed to close handle of mumble link ") - } - if !self.process_handle.is_invalid() { - if let Err(e) = CloseHandle(self.process_handle) { - error!(?e, "failed to close handle of mumble link ") - } - } - } - } -} From 876a5ac7e5d483ea3cc8a9c3458e995fe15a6ae9 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 28 Apr 2024 04:03:51 +0200 Subject: [PATCH 46/54] ignore all tests on macro in packages attributes doc comments --- crates/joko_component_manager/src/lib.rs | 217 ++++++++++--------- crates/joko_package_models/src/attributes.rs | 95 +++++--- 2 files changed, 172 insertions(+), 140 deletions(-) diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index 39643a7..49920c7 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -1,39 +1,40 @@ use std::collections::HashMap; use joko_component_models::JokolayComponentDeps; -use petgraph::{csr::IndexType, graph::NodeIndex, stable_graph::StableDiGraph, visit::IntoNodeIdentifiers, Direction}; +use petgraph::{ + csr::IndexType, graph::NodeIndex, stable_graph::StableDiGraph, visit::IntoNodeIdentifiers, + Direction, +}; use tracing::trace; pub struct ComponentManager { data: HashMap>, } - -fn get_invocation_order(my_graph: &mut StableDiGraph) -> Vec +fn get_invocation_order(my_graph: &mut StableDiGraph) -> Vec where N: std::cmp::Ord, - Ix: IndexType + Ix: IndexType, { let mut invocation_order = Vec::new(); //peel nodes one by one while my_graph.externals(Direction::Outgoing).count() > 0 { let mut to_delete = Vec::new(); - for external_node in my_graph.externals(Direction::Outgoing) { + for external_node in my_graph.externals(Direction::Outgoing) { to_delete.push(external_node); } let mut current_level_invocation_order = Vec::new(); for external_node in to_delete { current_level_invocation_order.push(my_graph.remove_node(external_node).unwrap()); } - current_level_invocation_order.sort();//This grant a deterministic order regardless of circumstances + current_level_invocation_order.sort(); //This grant a deterministic order regardless of circumstances invocation_order.extend(current_level_invocation_order); } //if there is a cycle, there are remaining nodes invocation_order } - impl ComponentManager { pub fn new() -> Self { Self { @@ -73,7 +74,7 @@ impl ComponentManager { type G = petgraph::stable_graph::StableDiGraph; - let mut known_services: HashMap > = Default::default(); + let mut known_services: HashMap> = Default::default(); let mut depgraph: G = G::default(); let mut translation: HashMap, NodeIndex> = Default::default(); let mut service_id = 0; @@ -145,7 +146,10 @@ impl ComponentManager { let invocation_order = get_invocation_order(&mut depgraph); if depgraph.node_count() > 0 { - return Err(format!("Found a cyclic dependancy between {:?}", depgraph.node_identifiers())); + return Err(format!( + "Found a cyclic dependancy between {:?}", + depgraph.node_identifiers() + )); } trace!("services: {:?}", known_services); trace!("invocation_order: {:?}", invocation_order); @@ -168,107 +172,104 @@ impl Default for ComponentManager { } } +#[cfg(test)] +mod test { + #[test] + fn test_invocation_order_1() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + let d = my_graph.add_node("d".to_string()); + let _e = my_graph.add_node("e".to_string()); + + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + my_graph.add_edge(c, d, 1); + my_graph.add_edge(a, d, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); + } + #[test] + fn test_invocation_order_2() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } -#[test] -fn test_invocation_order_1() { - type G = petgraph::stable_graph::StableDiGraph; - let mut my_graph = G::default(); - let a = my_graph.add_node("a".to_string()); - let b = my_graph.add_node("b".to_string()); - let c = my_graph.add_node("c".to_string()); - let d = my_graph.add_node("d".to_string()); - let _e = my_graph.add_node("e".to_string()); - - my_graph.add_edge(b, c, 1); - my_graph.add_edge(a, c, 1); - my_graph.add_edge(c, d, 1); - my_graph.add_edge(a, d, 1); - - - println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = get_invocation_order(&mut my_graph); - println!("nb nodes: {}", my_graph.node_count()); - println!("invocation order: {:?}", invocation_order); - assert!(my_graph.node_count() == 0); -} - - -#[test] -fn test_invocation_order_2() { - type G = petgraph::stable_graph::StableDiGraph; - let mut my_graph = G::default(); - let a = my_graph.add_node("a".to_string()); - let b = my_graph.add_node("b".to_string()); - let c = my_graph.add_node("c".to_string()); - - my_graph.add_edge(a, b, 1); - my_graph.add_edge(b, a, 1); - my_graph.add_edge(b, c, 1); - - println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = get_invocation_order(&mut my_graph); - println!("nb nodes: {}", my_graph.node_count()); - println!("invocation order: {:?}", invocation_order); - assert!(my_graph.node_count() == 2); -} - - -#[test] -fn test_invocation_order_3() { - type GG = petgraph::stable_graph::StableDiGraph; - let mut my_graph = GG::default(); - let a = my_graph.add_node(1); - let b = my_graph.add_node(2); - let c = my_graph.add_node(3); - - my_graph.add_edge(a, b, 1); - my_graph.add_edge(b, a, 1); - my_graph.add_edge(b, c, 1); - - println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = get_invocation_order(&mut my_graph); - println!("nb nodes: {}", my_graph.node_count()); - println!("invocation order: {:?}", invocation_order); - assert!(my_graph.node_count() == 2); -} - -#[test] -fn test_invocation_order_4() { - type GG = petgraph::stable_graph::StableDiGraph; - let mut my_graph = GG::default(); - let a = my_graph.add_node(1); - let b = my_graph.add_node(2); - let c = my_graph.add_node(3); - - my_graph.add_edge(a, b, 1); - my_graph.add_edge(b, c, 1); - my_graph.add_edge(a, c, 1); - - println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = get_invocation_order(&mut my_graph); - println!("nb nodes: {}", my_graph.node_count()); - println!("invocation order: {:?}", invocation_order); - assert!(my_graph.node_count() == 0); -} + #[test] + fn test_invocation_order_3() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } -#[test] -fn test_duplicate_node_value() { - type GG = petgraph::stable_graph::StableDiGraph; - let mut my_graph = GG::default(); - let a = my_graph.add_node(1); - let b = my_graph.add_node(2); - let c = my_graph.add_node(3); - let _doublon = my_graph.add_node(3);// same value, considered as a separate node + #[test] + fn test_invocation_order_4() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); + } - my_graph.add_edge(a, b, 1); - my_graph.add_edge(b, a, 1); - my_graph.add_edge(a, c, 1); - - println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = get_invocation_order(&mut my_graph); - println!("nb nodes: {}", my_graph.node_count()); - println!("invocation order: {:?}", invocation_order); - assert!(my_graph.node_count() == 2); + #[test] + fn test_duplicate_node_value() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + let _doublon = my_graph.add_node(3); // same value, considered as a separate node + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } } - diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index f2e7353..3236c03 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -98,6 +98,7 @@ pub struct XotAttributeNameIDs { pub resetposz: NameId, pub _source_file_name: NameId, } + impl XotAttributeNameIDs { pub fn register_with_xot(tree: &mut Xot) -> Self { Self { @@ -242,6 +243,7 @@ macro_rules! common_attributes_struct_macro { } } } + /// uses the [ToString] impl of attributes to serialize them (only if the relevant active attribute flag is set) /// /// #### Args: @@ -249,12 +251,12 @@ macro_rules! common_attributes_struct_macro { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute(names.field1, ca.field1.to_string()); /// } @@ -266,6 +268,7 @@ macro_rules! set_attribute_to_ele { })+ }; } + /// true -> 1 and 0 -> false. (only if the relevant active attribute flag is set) /// /// #### Args: @@ -273,12 +276,12 @@ macro_rules! set_attribute_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_bool_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute(names.field1, /// ca @@ -304,6 +307,7 @@ macro_rules! set_attribute_bool_to_ele { })+ }; } + /// iterates over a bitflags field and joins the enabled flags (as str) with comma. (only if the relevant active attribute flag is set) /// /// #### Args: @@ -311,12 +315,12 @@ macro_rules! set_attribute_bool_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_bitflags_as_array_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute( /// names.field1, @@ -334,6 +338,7 @@ macro_rules! set_attribute_bitflags_as_array_to_ele { })+ }; } + /// uses the [FromStr] impl of attributes to deserialize them (and set the relevant active attribute flag if successful) /// /// #### Args: @@ -341,12 +346,12 @@ macro_rules! set_attribute_bitflags_as_array_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// update_attribute_from_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(value) = ele.get_attribute(names.field1) { /// match value.trim().parse() { /// Ok(value) => { @@ -379,6 +384,24 @@ macro_rules! update_attribute_from_ele { }; } +fn parse_boolean(raw_value: &str) -> Option { + let trimmed = raw_value.trim().to_lowercase(); + match trimmed.as_ref() { + "true" => Some(true), + "false" => Some(false), + _ => { + match trimmed.parse::() { + //might entirely get rid of parsing + Ok(parsed_value) => match parsed_value { + 0 | 1 => Some(parsed_value == 1), + _ => None, + }, + Err(_e) => None, + } + } + } +} + /// deserializes an [i8] and matches that as 1 -> true and 0 -> false. /// On success, set the relevant active attribute flag. /// @@ -387,12 +410,12 @@ macro_rules! update_attribute_from_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// update_attribute_bool_from_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(value) = ele.get_attribute(names.field1) { /// match value.trim().parse::() { /// Ok(value) => { @@ -417,24 +440,6 @@ macro_rules! update_attribute_from_ele { /// } /// } /// ``` - -fn parse_boolean(raw_value: &str) -> Option { - let trimmed = raw_value.trim().to_lowercase(); - match trimmed.as_ref() { - "true" => Some(true), - "false" => Some(false), - _ => { - match trimmed.parse::() { - //might entirely get rid of parsing - Ok(parsed_value) => match parsed_value { - 0 | 1 => Some(parsed_value == 1), - _ => None, - }, - Err(_e) => None, - } - } - } -} macro_rules! update_attribute_bool_from_ele { ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { $(if let Some(value) = $ele.get_attribute($names.$field) { @@ -452,6 +457,7 @@ macro_rules! update_attribute_bool_from_ele { })+ }; } + /// deserializes an [i8] and matches that as 1 -> true and 0 -> false. /// On success, set the relevant active attribute flag. /// @@ -460,12 +466,12 @@ macro_rules! update_attribute_bool_from_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1,t1; f2,t2;...]: an array of field identifiers which will be serialized and their enum type. -/// ```rust +/// ```rust,ignore /// update_attribute_bitflags_array_from_ele!(ca, ele, names, [f1, t1; f2, t2]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(field1_str) = ele.get_attribute(names.field1) { /// for value in field1_str.split(',') { /// match value.trim().parse::() { @@ -499,8 +505,9 @@ macro_rules! update_attribute_bitflags_array_from_ele { })+ }; } + /// generates getters for bool attributes -/// ```rust +/// ```rust,ignore /// getters_for_bool_attributes!([field1, field2, field3]); /// ``` /// @@ -522,8 +529,9 @@ macro_rules! getters_for_bool_attributes { } }; } + /// generates setters for bool attributes -/// ```rust +/// ```rust,ignore /// setters_for_bool_attributes!([field1, field2, field3]); /// ``` /// @@ -548,6 +556,7 @@ macro_rules! setters_for_bool_attributes { } }; } + common_attributes_struct_macro!( /// the struct we use for inheritance from category/other markers. #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -956,6 +965,7 @@ pub enum BoolAttributes { /// scaling of marker on 2d map (or minimap) scale_on_map_with_zoom = 1 << 9, } + #[allow(non_camel_case_types)] #[bitflags] #[repr(u64)] @@ -1020,6 +1030,7 @@ pub enum ActiveAttributes { trail_scale = 1 << 56, trigger_range = 1 << 57, } + #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] pub enum Behavior { #[default] @@ -1044,6 +1055,7 @@ pub enum Behavior { WvWObjective, WeeklyReset = 101, } + impl FromStr for Behavior { type Err = &'static str; @@ -1064,6 +1076,7 @@ impl FromStr for Behavior { }) } } + /// Filter which professions the marker should be active for. if its null, its available for all professions #[bitflags] #[repr(u16)] @@ -1079,6 +1092,7 @@ pub enum Profession { Thief = 1 << 7, Warrior = 1 << 8, } + impl FromStr for Profession { type Err = &'static str; @@ -1097,6 +1111,7 @@ impl FromStr for Profession { }) } } + impl AsRef for Profession { fn as_ref(&self) -> &str { match self { @@ -1112,11 +1127,13 @@ impl AsRef for Profession { } } } + impl ToString for Profession { fn to_string(&self) -> String { self.as_ref().to_string() } } + #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub enum Cull { #[default] @@ -1124,6 +1141,7 @@ pub enum Cull { ClockWise, CounterClockWise, } + impl FromStr for Cull { type Err = &'static str; @@ -1138,6 +1156,7 @@ impl FromStr for Cull { }) } } + impl AsRef for Cull { fn as_ref(&self) -> &'static str { match self { @@ -1147,11 +1166,13 @@ impl AsRef for Cull { } } } + impl ToString for Cull { fn to_string(&self) -> String { self.as_ref().to_string() } } + /// Filter for which festivals will the marker be active for #[bitflags] #[repr(u8)] @@ -1165,6 +1186,7 @@ pub enum Festival { SuperAdventureBox = 1 << 4, Wintersday = 1 << 5, } + impl FromStr for Festival { type Err = &'static str; @@ -1180,6 +1202,7 @@ impl FromStr for Festival { }) } } + impl AsRef for Festival { fn as_ref(&self) -> &'static str { match self { @@ -1192,11 +1215,13 @@ impl AsRef for Festival { } } } + impl ToString for Festival { fn to_string(&self) -> String { self.as_ref().to_string() } } + /// Filter for which specializations (the third traitline) will the marker be active for #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[repr(u8)] @@ -1356,6 +1381,7 @@ impl FromStr for Specialization { }) } } + impl AsRef for Specialization { fn as_ref(&self) -> &str { match self { @@ -1440,6 +1466,7 @@ impl ToString for Specialization { self.as_ref().to_string() } } + /// Most of this data is stolen from BlishHUD. #[bitflags] #[repr(u32)] @@ -1484,22 +1511,26 @@ pub enum MapType { /// WvW lounge map type, e.g. Armistice Bastion. WvwLounge = 1 << 18, } + impl FromStr for MapType { type Err = &'static str; fn from_str(_s: &str) -> Result { unimplemented!("needs research to verify the map type values") } } + impl AsRef for MapType { fn as_ref(&self) -> &str { unimplemented!("needs research to verify the maptype values") } } + impl ToString for MapType { fn to_string(&self) -> String { self.as_ref().to_string() } } + /// made it using multi cursor (ctrl + shift + L) by copy-pasting json from api #[allow(unused)] pub static MAP_ID_TO_NAME: phf::OrderedMap = phf::phf_ordered_map! { From 32baf1f0c922780abe20b0d2997aceee37ab2d89 Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 28 Apr 2024 23:49:48 +0200 Subject: [PATCH 47/54] start to add features on how serialization should happen + start to have functions to encapsulate the transfer --- Cargo.lock | 24 +++ Cargo.toml | 1 + crates/joko_component_manager/Cargo.toml | 6 + crates/joko_component_manager/src/lib.rs | 125 ++++++++++-- crates/joko_component_models/Cargo.toml | 9 + crates/joko_component_models/src/lib.rs | 95 ++++----- .../joko_component_models/src/messages_any.rs | 28 +++ .../src/messages_bincode.rs | 19 ++ crates/joko_core/Cargo.toml | 8 + crates/joko_core/src/lib.rs | 2 + crates/joko_link_manager/Cargo.toml | 5 + crates/joko_link_manager/src/lib.rs | 52 +++-- crates/joko_link_models/Cargo.toml | 9 + crates/joko_link_models/src/lib.rs | 30 ++- crates/joko_link_models/src/messages.rs | 28 +++ crates/joko_package_manager/Cargo.toml | 10 + .../src/manager/pack/category_selection.rs | 29 ++- .../src/manager/pack/loaded.rs | 48 ++--- .../src/manager/package_data.rs | 192 +++++++++++------- .../src/manager/package_ui.rs | 163 ++++++++------- crates/joko_package_manager/src/message.rs | 20 +- crates/joko_package_models/Cargo.toml | 12 +- crates/joko_plugin_manager/Cargo.toml | 8 + crates/joko_plugin_manager/src/lib.rs | 11 +- crates/joko_render_manager/Cargo.toml | 8 + crates/joko_render_manager/src/renderer.rs | 42 ++-- crates/joko_render_models/Cargo.toml | 9 + crates/joko_render_models/src/marker.rs | 2 +- crates/joko_render_models/src/messages.rs | 20 +- crates/jokolay/Cargo.toml | 7 + crates/jokolay/src/app/messages.rs | 1 + crates/jokolay/src/app/mod.rs | 84 ++++---- 32 files changed, 733 insertions(+), 374 deletions(-) create mode 100644 crates/joko_component_models/src/messages_any.rs create mode 100644 crates/joko_component_models/src/messages_bincode.rs create mode 100644 crates/joko_link_models/src/messages.rs diff --git a/Cargo.lock b/Cargo.lock index d54ab15..22233f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,6 +785,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "ecolor" version = "0.26.2" @@ -1460,8 +1466,10 @@ name = "joko_component_models" version = "0.2.1" dependencies = [ "bincode", + "downcast-rs", "egui", "scopeguard", + "serde", "smol_str", "tokio", ] @@ -1472,6 +1480,7 @@ version = "0.2.1" dependencies = [ "bytemuck", "glam", + "mutually_exclusive_features", "scopeguard", "serde", "smol_str", @@ -1512,10 +1521,13 @@ name = "joko_link_models" version = "0.2.1" dependencies = [ "arcdps", + "bincode", "enumflags2", "glam", + "joko_component_models", "joko_core", "miette", + "mutually_exclusive_features", "notify", "num-derive", "num-traits", @@ -1555,6 +1567,7 @@ dependencies = [ "joko_render_models", "jokoapi", "miette", + "mutually_exclusive_features", "once", "ordered_hash_map", "paste", @@ -1582,6 +1595,7 @@ version = "0.2.1" dependencies = [ "base64", "bimap", + "bincode", "bytemuck", "cxx-build", "data-encoding", @@ -1589,9 +1603,11 @@ dependencies = [ "glam", "indexmap", "itertools", + "joko_component_models", "joko_core", "jokoapi", "miette", + "mutually_exclusive_features", "ordered_hash_map", "paste", "phf", @@ -1642,6 +1658,7 @@ dependencies = [ "glam", "joko_component_models", "joko_core", + "mutually_exclusive_features", "serde", ] @@ -1677,6 +1694,7 @@ dependencies = [ "joko_plugin_manager", "joko_render_manager", "miette", + "mutually_exclusive_features", "rayon", "rfd", "ringbuffer", @@ -1898,6 +1916,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + [[package]] name = "next-gen" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 57d6033..286ae64 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "time",] } # f ureq = { version = "*" } url = { version = "*", features = ["serde"] } uuid = { version = "*" } +mutually_exclusive_features = "0.1.0" #https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml index 589a975..207d2ec 100644 --- a/crates/joko_component_manager/Cargo.toml +++ b/crates/joko_component_manager/Cargo.toml @@ -5,6 +5,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + [dependencies] bincode = { workspace = true } egui = { workspace = true } diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index 49920c7..cdf0a1a 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -1,6 +1,9 @@ -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, +}; -use joko_component_models::JokolayComponentDeps; +use joko_component_models::{ComponentDataExchange, JokolayComponent, JokolayComponentDeps}; use petgraph::{ csr::IndexType, graph::NodeIndex, stable_graph::StableDiGraph, visit::IntoNodeIdentifiers, Direction, @@ -8,9 +11,17 @@ use petgraph::{ use tracing::trace; pub struct ComponentManager { + //TODO: make it a component too ? data: HashMap>, } +struct ComponentHandle { + component: Box, +} +pub struct ComponentExecutor { + components: Vec, //FIXME: how to type erase result ? +} + fn get_invocation_order(my_graph: &mut StableDiGraph) -> Vec where N: std::cmp::Ord, @@ -35,8 +46,17 @@ where invocation_order } +fn has_unique_elements(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + Hash, +{ + let mut uniq = HashSet::new(); + iter.into_iter().all(move |x| uniq.insert(x)) +} impl ComponentManager { pub fn new() -> Self { + //clone itself on a world basis ? which would follow a component thread Self { data: Default::default(), } @@ -46,16 +66,21 @@ impl ComponentManager { self.data.insert(service_name.to_owned(), co); } + pub fn executor(&self, world: &str) -> ComponentExecutor { + /* + TODO: + extract the list of components of this world + bind them + insert them into the executor + */ + ComponentExecutor { + components: Default::default(), + } + } pub fn build_routes(&mut self) -> Result<(), String> { /* + TODO: split in worlds - fn bind( - &mut self, - deps: HashMap, - bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap - notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) https://docs.rs/dep-graph/latest/dep_graph/ https://lib.rs/crates/petgraph https://docs.rs/solvent/latest/solvent/ @@ -74,15 +99,28 @@ impl ComponentManager { type G = petgraph::stable_graph::StableDiGraph; + let mut hosted_services: HashMap> = Default::default(); let mut known_services: HashMap> = Default::default(); let mut depgraph: G = G::default(); let mut translation: HashMap, NodeIndex> = Default::default(); let mut service_id = 0; for (service_name, co) in self.data.iter() { + if !has_unique_elements( + co.peer() + .iter() + .chain(co.notify().iter()) + .chain(co.requires().iter()), + ) { + return Err(format!( + "Service {} has duplicate elements. Each name can only appear at one place", + service_name + )); + } let service_name = service_name.clone(); - if !known_services.contains_key(&service_name) { + if !hosted_services.contains_key(&service_name) { let node_id = depgraph.add_node(service_id); service_id += 1; + hosted_services.insert(service_name.clone(), node_id); known_services.insert(service_name.clone(), node_id); } trace!("node: {}, peers: {:?}", service_name, co.peer()); @@ -107,6 +145,22 @@ impl ComponentManager { translation.insert(peer_id, merged_id); } } + for required_service_name in co.requires() { + let required_service_name = required_service_name.to_string(); + if !known_services.contains_key(&required_service_name) { + let node_id = depgraph.add_node(service_id); + service_id += 1; + known_services.insert(required_service_name.clone(), node_id); + } + } + for notified_service_name in co.notify() { + let notified_service_name = notified_service_name.to_string(); + if !known_services.contains_key(¬ified_service_name) { + let node_id = depgraph.add_node(service_id); + service_id += 1; + known_services.insert(notified_service_name.clone(), node_id); + } + } } //If we reached here, it means all peers agree @@ -115,18 +169,17 @@ impl ComponentManager { for (service_name, co) in self.data.iter() { let node_id = *known_services.get(service_name).unwrap(); - let service_id = *translation.get(&node_id).or(Some(&node_id)).unwrap(); + let node_id = *translation.get(&node_id).unwrap_or(&node_id); trace!("node: {}, requires: {:?}", service_name, co.requires()); for required_service_name in co.requires() { let required_service_id = *known_services.get(required_service_name).unwrap(); let required_service_id = *translation .get(&required_service_id) - .or(Some(&required_service_id)) - .unwrap(); - if service_id != required_service_id { - depgraph.add_edge(service_id, required_service_id, 1); + .unwrap_or(&required_service_id); + if node_id != required_service_id { + depgraph.add_edge(node_id, required_service_id, 1); //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. - requirements_graph.add_edge(service_id, required_service_id, 1); + requirements_graph.add_edge(node_id, required_service_id, 1); } } trace!("node: {}, notify: {:?}", service_name, co.notify()); @@ -134,15 +187,26 @@ impl ComponentManager { let notified_service_id = *known_services.get(notified_service_name).unwrap(); let notified_service_id = *translation .get(¬ified_service_id) - .or(Some(¬ified_service_id)) - .unwrap(); - if service_id != notified_service_id { - depgraph.add_edge(notified_service_id, service_id, 1); + .unwrap_or(¬ified_service_id); + if node_id != notified_service_id { + //there is no dep on the graph, the only worth of the notified service is it needs to exist //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. - notification_graph.add_edge(notified_service_id, service_id, 1); + notification_graph.add_edge(notified_service_id, node_id, 1); } } } + // Before anything find diff between keys of known_services vs hosted_services + let hosted_keys: HashSet = hosted_services.keys().cloned().collect(); + let known_keys: HashSet = known_services.keys().cloned().collect(); + trace!("hosted_keys: {:?}", hosted_keys); + trace!("known_keys: {:?}", known_keys); + if known_keys.difference(&hosted_keys).count() > 0 { + //TODO: have error!() with details of which component asked for it + return Err(format!( + "Some relationship could not be satisfied. Missing: {:?}", + known_keys.difference(&hosted_keys) + )); + } let invocation_order = get_invocation_order(&mut depgraph); if depgraph.node_count() > 0 { @@ -155,7 +219,8 @@ impl ComponentManager { trace!("invocation_order: {:?}", invocation_order); /* TODO: make use of: - requirements graph + requirements graph => components subscribe to it. There should be at most one element in it, eaten at each step of the loop. + => how to make sure ui does subscribe to ui only and back to back ? => introduce "worlds" "myworld:component" notification graph invocation order */ @@ -172,6 +237,22 @@ impl Default for ComponentManager { } } +impl ComponentHandle { + fn broadcast(&mut self, data: ComponentDataExchange) { + println!("{:?}", data); + unimplemented!("The broadcast of data is not done"); + } +} + +impl ComponentExecutor { + fn tick(&mut self, latest_time: f64) -> () { + for handle in self.components.iter_mut() { + let res = handle.component.tick(latest_time); + handle.broadcast(res); + } + } +} + #[cfg(test)] mod test { #[test] diff --git a/crates/joko_component_models/Cargo.toml b/crates/joko_component_models/Cargo.toml index 7ae5c56..257d73c 100644 --- a/crates/joko_component_models/Cargo.toml +++ b/crates/joko_component_models/Cargo.toml @@ -5,9 +5,18 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] bincode = { workspace = true } +downcast-rs = "1.2.1" egui = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } tokio = { workspace = true } +serde = { workspace = true } diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs index 5728d29..c596ac5 100644 --- a/crates/joko_component_models/src/lib.rs +++ b/crates/joko_component_models/src/lib.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; -//could become a "dyn Message". -//std::any::Any is a trait -//TODO: It would have a wrap and unwrap ? -pub type ComponentDataExchange = Vec; -//pub type ComponentDataExchange = Box<[u8]>; -//pub type ComponentDataExchange = [u8; 1024]; +#[cfg(feature = "messages_any")] +mod messages_any; +#[cfg(feature = "messages_any")] +pub use messages_any::*; + +#[cfg(feature = "messages_bincode")] +mod messages_bincode; +#[cfg(feature = "messages_bincode")] +pub use messages_bincode::*; + pub type PeerComponentChannel = ( tokio::sync::mpsc::Receiver, tokio::sync::mpsc::Sender, @@ -32,75 +36,44 @@ pub trait JokolayComponentDeps { } } -pub trait JokolayComponent -where - SharedStatus: Clone, -{ - fn flush_all_messages(&mut self) -> SharedStatus; - fn tick(&mut self, latest_time: f64) -> Option<&ComponentResult>; +pub trait JokolayComponent { + /* + This make sense only when components are very similar. It make no sense to ask for a uniform way to build components. + type T; + type E; + fn new( + root_path: &std::path::Path, + ) -> Result;*/ + + fn flush_all_messages(&mut self); + fn tick(&mut self, latest_time: f64) -> ComponentDataExchange; fn bind( &mut self, deps: HashMap>, - bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + bound: HashMap, // Private channel only two bounded modules can use between each others. input_notification: HashMap>, notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. ); //By default, there is no third party component, thus we can implement it as a noop - - /* - - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - ) -> Result; - */ + /* + TODO: there could be an optional trait: Chain. + If there is a strong connection between two elements, passing values by channels and copy could be inefficient, calling a function with arguments could be better => + it's almost a macro with an unset number of arguments and unknown types. + It could be possible on plugins, not other kind of components + */ } -pub trait JokolayUIComponent +pub trait JokolayUIComponent where - SharedStatus: Clone, + ComponentResult: Clone, { - fn flush_all_messages(&mut self) -> SharedStatus; - fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> Option<&ComponentResult>; + fn flush_all_messages(&mut self); + //the only reason there is another Component trait is because of the egui_context + fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> ComponentResult; fn bind( &mut self, deps: HashMap>, - bound: HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + bound: HashMap, // Private channel only two bounded modules can use between each others. input_notification: HashMap>, notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. ); //By default, there is no third party component, thus we can implement it as a noop - - /* - - // any extra information should come from configuration, which can be loaded from those two arguments. - Those roots are specific to the component, it cannot shared it with another component - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - ) -> Result; - - fn bind( - &mut self, - deps: HashMap, - bound: HashMap,// ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - input_notification: HashMap - notify: HashMap, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) - https://docs.rs/dep-graph/latest/dep_graph/ - https://lib.rs/crates/petgraph - https://docs.rs/solvent/latest/solvent/ - https://lib.rs/crates/cargo-depgraph - => check "peer" is always mutual - => graph with the "peer" elements replaced by some merged id - => check there is no loop (there could be surprises) - => if there is no problem, then: - - build again the graph with UI plugins only and save one traversal (memory + file) - - build again the graph with back plugins only and save one traversal (memory + file) - => if there is a problem, do not save anything - - - - fn tick( - &mut self, - ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() - */ } diff --git a/crates/joko_component_models/src/messages_any.rs b/crates/joko_component_models/src/messages_any.rs new file mode 100644 index 0000000..7de4bb7 --- /dev/null +++ b/crates/joko_component_models/src/messages_any.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +pub type ComponentDataExchange = Arc>; + +pub fn default_data_exchange() -> ComponentDataExchange { + Arc::new(Box::new(0)) +} + +pub fn to_data<'a, T>(value: T) -> ComponentDataExchange +where + T: Serialize + Clone + Send + Sync + 'static, +{ + Arc::new(Box::new(T::from(value))) +} + +pub fn from_data<'a, T>(value: ComponentDataExchange) -> T +where + T: Deserialize<'a> + Clone + Send + Sync + 'static, +{ + use downcast_rs::Downcast; + + let a = value.as_any(); + let d = a.downcast_ref::(); + let res = d.unwrap().to_owned(); + res +} diff --git a/crates/joko_component_models/src/messages_bincode.rs b/crates/joko_component_models/src/messages_bincode.rs new file mode 100644 index 0000000..a20cd96 --- /dev/null +++ b/crates/joko_component_models/src/messages_bincode.rs @@ -0,0 +1,19 @@ +pub type ComponentDataExchange = Vec; + +pub fn default_data_exchange() -> ComponentDataExchange { + ComponentDataExchange::default() +} + +pub fn to_data(value: T) -> ComponentDataExchange +where + T: Serialize, +{ + bincode::serialize(&value).unwrap() +} + +pub fn from_data<'a, T>(value: &'a ComponentDataExchange) -> T +where + T: Deserialize<'a>, +{ + bincode::deserialize(&value).unwrap() +} diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index 19c231c..7d21884 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -11,3 +11,11 @@ glam = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } serde = { workspace = true } +mutually_exclusive_features = { workspace = true } + + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index f7864f2..5a29d12 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,3 +1,5 @@ +mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); + use std::str::FromStr; use serde::{Deserialize, Serialize}; diff --git a/crates/joko_link_manager/Cargo.toml b/crates/joko_link_manager/Cargo.toml index eabd405..753da2d 100644 --- a/crates/joko_link_manager/Cargo.toml +++ b/crates/joko_link_manager/Cargo.toml @@ -5,7 +5,12 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] [dependencies] diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs index 766f8b5..2c38a9d 100644 --- a/crates/joko_link_manager/src/lib.rs +++ b/crates/joko_link_manager/src/lib.rs @@ -12,11 +12,12 @@ use std::vec; use enumflags2::BitFlags; use joko_component_models::{ - ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, + from_data, to_data, ComponentDataExchange, JokolayComponent, JokolayComponentDeps, + PeerComponentChannel, }; use joko_core::serde_glam::{IVec2, UVec2, Vec3}; use joko_link_models::{ - ctypes, MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkSharedState, + ctypes, MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult, }; //use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::{IntoDiagnostic, Result, WrapErr}; @@ -35,6 +36,9 @@ use linux::MumbleLinuxImpl as MumblePlatformImpl; #[cfg(target_os = "windows")] use win::MumbleWinImpl as MumblePlatformImpl; +struct MumbleChannels { + notification_receiver: tokio::sync::mpsc::Receiver, +} // Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there // pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; /// This primarily manages the mumble backend. @@ -50,22 +54,23 @@ pub struct MumbleManager { is_ui: bool, /// latest mumble link link: MumbleLink, - channel_receiver: std::sync::mpsc::Receiver, - state: MumbleLinkSharedState, + + channels: Option, + state: MumbleLinkResult, } impl MumbleManager { pub fn new(name: &str, is_ui: bool) -> Result { let backend = MumblePlatformImpl::new(name)?; - let (_, receiver) = std::sync::mpsc::channel(); Ok(Self { backend, link: Default::default(), - channel_receiver: receiver, + channels: None, is_ui, - state: MumbleLinkSharedState { + state: MumbleLinkResult { read_ui_link: true, - copy_of_ui_link: None, + link: None, + ui_link: None, }, }) } @@ -85,7 +90,7 @@ impl MumbleManager { } MessageToMumbleLinkBack::Value(link) => { tracing::trace!("Handling of UIToBackMessage::MumbleLink"); - self.state.copy_of_ui_link = link; + self.state.ui_link = link; } #[allow(unreachable_patterns)] _ => { @@ -219,16 +224,22 @@ impl MumbleManager { } } -impl JokolayComponent for MumbleManager { - fn flush_all_messages(&mut self) -> MumbleLinkSharedState { - while let Ok(msg) = self.channel_receiver.try_recv() { +impl JokolayComponent for MumbleManager { + fn flush_all_messages(&mut self) { + let channels = self.channels.as_mut().unwrap(); + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); + } + for msg in messages { self.handle_message(msg); } - self.state.clone() } - fn tick(&mut self, _latest_time: f64) -> Option<&MumbleLink> { - self._tick().unwrap_or(None) + fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + let link = self._tick().unwrap_or(None); + self.state.link = link.cloned(); + to_data(self.state.clone()) } fn bind( &mut self, @@ -236,13 +247,18 @@ impl JokolayComponent for MumbleManager { u32, tokio::sync::broadcast::Receiver, >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. _input_notification: std::collections::HashMap< u32, tokio::sync::mpsc::Receiver, >, _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. ) { + let (notification_receiver, _) = bound.remove(&0).unwrap(); + let channels = MumbleChannels { + notification_receiver, + }; + self.channels = Some(channels); } } @@ -250,9 +266,9 @@ impl JokolayComponentDeps for MumbleManager { //default is enough fn peer(&self) -> Vec<&str> { if self.is_ui { - vec!["mumble_link_back"] + vec!["back:mumble_link"] } else { - vec!["mumble_link_ui"] + vec!["ui:mumble_link"] } } } diff --git a/crates/joko_link_models/Cargo.toml b/crates/joko_link_models/Cargo.toml index ca50e28..ade6983 100644 --- a/crates/joko_link_models/Cargo.toml +++ b/crates/joko_link_models/Cargo.toml @@ -5,11 +5,19 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + [features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] [dependencies] +bincode = { workspace = true } joko_core = { path = "../joko_core" } +joko_component_models = { path = "../joko_component_models" } widestring = { version = "1", default-features = false, features = ["std"] } num-derive = { version = "0", default-features = false } num-traits = { version = "0", default-features = false } @@ -21,6 +29,7 @@ serde = { workspace = true } glam = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } +mutually_exclusive_features = { workspace = true } [target.'cfg(unix)'.dependencies] x11rb = { version = "0.12", default-features = false, features = [] } diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs index 5df2625..b939521 100644 --- a/crates/joko_link_models/src/lib.rs +++ b/crates/joko_link_models/src/lib.rs @@ -8,18 +8,32 @@ //! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. //! +mod messages; mod mumble; +use joko_component_models::ComponentDataExchange; +pub use messages::*; pub use mumble::*; +use serde::{Deserialize, Serialize}; -pub enum MessageToMumbleLinkBack { - BindedOnUI, - Autonomous, - Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. +#[derive(Clone, Serialize, Deserialize)] +pub struct MumbleLinkResult { + pub read_ui_link: bool, + pub link: Option, + pub ui_link: Option, } -#[derive(Clone)] -pub struct MumbleLinkSharedState { - pub read_ui_link: bool, - pub copy_of_ui_link: Option, +/* +impl From for ComponentDataExchange { + fn from(src: MumbleLinkResult) -> ComponentDataExchange { + bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages + } +} + +#[allow(clippy::from_over_into)] +impl Into for ComponentDataExchange { + fn into(self) -> MumbleLinkResult { + bincode::deserialize(&self).unwrap() + } } +*/ diff --git a/crates/joko_link_models/src/messages.rs b/crates/joko_link_models/src/messages.rs new file mode 100644 index 0000000..a0e4501 --- /dev/null +++ b/crates/joko_link_models/src/messages.rs @@ -0,0 +1,28 @@ +mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); + +use joko_component_models::{to_data, ComponentDataExchange}; +use serde::{Deserialize, Serialize}; + +use crate::MumbleLink; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToMumbleLinkBack { + BindedOnUI, + Autonomous, + Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. +} + +/* +impl From for ComponentDataExchange { + fn from(src: MessageToMumbleLinkBack) -> ComponentDataExchange { + to_data(src) + } +} + +#[allow(clippy::from_over_into)] +impl Into for ComponentDataExchange { + fn into(self) -> MessageToMumbleLinkBack { + bincode::deserialize(&self).unwrap() + } +} +*/ diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml index acbac60..e1a2c4d 100644 --- a/crates/joko_package_manager/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -3,6 +3,14 @@ name = "joko_package_manager" version = "0.2.1" edition = "2021" + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] # jmf deps # for marker packs @@ -42,6 +50,8 @@ uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serd xot = { version = "0.16.0" } zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) walkdir = "2.5.0" +mutually_exclusive_features = { workspace = true } + diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs index b2d141e..6eb38e5 100644 --- a/crates/joko_package_manager/src/manager/pack/category_selection.rs +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -1,4 +1,4 @@ -use joko_component_models::ComponentDataExchange; +use joko_component_models::{to_data, ComponentDataExchange}; use joko_package_models::{ attributes::CommonAttributes, category::Category, @@ -220,24 +220,23 @@ impl CategorySelection { if ui.button("Activate branch").clicked() { cs.is_selected = true; CategorySelection::recursive_set_all(&mut cs.children, true); - let _ = u2b_sender.blocking_send( - MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true).into(), - ); + let _ = u2b_sender.blocking_send(to_data( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true), + )); ui.close_menu(); } if ui.button("Deactivate branch").clicked() { CategorySelection::recursive_set_all(&mut cs.children, false); cs.is_selected = false; - let _ = u2b_sender.blocking_send( - MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false).into(), - ); + let _ = u2b_sender.blocking_send(to_data( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false), + )); ui.close_menu(); } } pub fn recursive_selection_ui( - u2b_sender: &tokio::sync::mpsc::Sender, - _u2u_sender: &tokio::sync::mpsc::Sender, + back_end_notifier: &tokio::sync::mpsc::Sender, selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, @@ -258,13 +257,12 @@ impl CategorySelection { } else { let cb = ui.checkbox(&mut cat.is_selected, ""); if cb.changed() { - let _ = u2b_sender.blocking_send( + let _ = back_end_notifier.blocking_send(to_data( MessageToPackageBack::CategoryActivationElementStatusChange( cat.uuid, cat.is_selected, - ) - .into(), - ); + ), + )); *is_dirty = true; } } @@ -282,8 +280,7 @@ impl CategorySelection { } else { ui.menu_button(label, |ui: &mut egui::Ui| { Self::recursive_selection_ui( - u2b_sender, - _u2u_sender, + back_end_notifier, &mut cat.children, ui, is_dirty, @@ -292,7 +289,7 @@ impl CategorySelection { ); }) .response - .context_menu(|ui| Self::context_menu(u2b_sender, cat, ui)); + .context_menu(|ui| Self::context_menu(back_end_notifier, cat, ui)); } }); } diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index 33b2ee9..e3b4bd7 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -4,7 +4,7 @@ use std::{ sync::Arc, }; -use joko_component_models::ComponentDataExchange; +use joko_component_models::{to_data, ComponentDataExchange}; use joko_package_models::{ attributes::{Behavior, CommonAttributes}, category::Category, @@ -36,7 +36,7 @@ use joko_core::{ RelativePath, }; use joko_link_models::MumbleLink; -use joko_render_models::{messages::UIToUIMessage, trail::TrailObject}; +use joko_render_models::{messages::MessageToRenderer, trail::TrailObject}; use miette::{Context, IntoDiagnostic, Result}; use super::activation::{ActivationData, ActivationType}; @@ -394,7 +394,6 @@ impl LoadedPackData { pub(crate) fn tick( &mut self, b2u_sender: &tokio::sync::mpsc::Sender, - _loop_index: u128, link: &MumbleLink, currently_used_files: &BTreeMap, list_of_active_or_selected_elements_changed: bool, @@ -407,10 +406,10 @@ impl LoadedPackData { //tasks.change_map(self, b2u_sender, link, currently_used_files); let mut active_elements: HashSet = Default::default(); self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - let _ = b2u_sender.blocking_send( - MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()) - .into(), - ); + let _ = b2u_sender.blocking_send(to_data(MessageToPackageUI::PackageActiveElements( + self.uuid, + active_elements.clone(), + ))); self.active_elements = active_elements.clone(); next_loaded.extend(active_elements); } @@ -510,16 +509,14 @@ impl LoadedPackData { } } if let Some(tex_path) = common_attributes.get_icon_file() { - let _ = b2u_sender.blocking_send( - MessageToPackageUI::MarkerTexture( + let _ = + b2u_sender.blocking_send(to_data(MessageToPackageUI::MarkerTexture( self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes, - ) - .into(), - ); + ))); } else { debug!("no texture attribute on this marker"); } @@ -553,15 +550,13 @@ impl LoadedPackData { let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - let _ = b2u_sender.blocking_send( - MessageToPackageUI::TrailTexture( + let _ = + b2u_sender.blocking_send(to_data(MessageToPackageUI::TrailTexture( self.uuid, tex_path.clone(), trail.guid, common_attributes, - ) - .into(), - ); + ))); } else { debug!("no texture attribute on this trail"); } @@ -606,8 +601,7 @@ impl LoadedPackTexture { } pub fn category_sub_menu( &mut self, - u2b_sender: &tokio::sync::mpsc::Sender, - u2u_sender: &tokio::sync::mpsc::Sender, + back_end_notifier: &tokio::sync::mpsc::Sender, ui: &mut egui::Ui, show_only_active: bool, import_quality_report: &PackageImportReport, @@ -615,8 +609,7 @@ impl LoadedPackTexture { //it is important to generate a new id each time to avoid collision ui.push_id(ui.next_auto_id(), |ui| { CategorySelection::recursive_selection_ui( - u2b_sender, - u2u_sender, + back_end_notifier, &mut self.selectable_categories, ui, &mut self._is_dirty, @@ -625,8 +618,9 @@ impl LoadedPackTexture { ); }); if self._is_dirty { - let _ = u2b_sender - .blocking_send(MessageToPackageBack::CategoryActivationStatusChanged.into()); + let _ = back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationStatusChanged, + )); } } @@ -635,7 +629,7 @@ impl LoadedPackTexture { } pub(crate) fn tick( &mut self, - u2u_sender: &tokio::sync::mpsc::Sender, + renderer_notifier: &tokio::sync::mpsc::Sender, _timestamp: f64, link: &MumbleLink, //next_on_screen: &mut HashSet, @@ -661,7 +655,8 @@ impl LoadedPackTexture { self.name, marker_objects.len() ); - let _ = u2u_sender.blocking_send(UIToUIMessage::BulkMarkerObject(marker_objects).into()); + let _ = renderer_notifier + .blocking_send(to_data(MessageToRenderer::BulkMarkerObject(marker_objects))); let mut trail_objects = Vec::new(); for trail in self.current_map_data.active_trails.values() { trail_objects.push(TrailObject { @@ -675,7 +670,8 @@ impl LoadedPackTexture { self.name, trail_objects.len() ); - let _ = u2u_sender.blocking_send(UIToUIMessage::BulkTrailObject(trail_objects).into()); + let _ = renderer_notifier + .blocking_send(to_data(MessageToRenderer::BulkTrailObject(trail_objects))); Ok(()) } diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs index 3f5a8bc..9787ede 100644 --- a/crates/joko_package_manager/src/manager/package_data.rs +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -4,21 +4,24 @@ use std::{ }; use cap_std::fs_utf8::Dir; -use joko_component_models::ComponentDataExchange; +use joko_component_models::{ + default_data_exchange, from_data, to_data, ComponentDataExchange, JokolayComponent, + JokolayComponentDeps, +}; use joko_package_models::package::PackageImportReport; use tracing::{error, info, info_span, trace}; use crate::{ build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, - jokolay_to_extract_path, message::MessageToPackageBack, + jokolay_to_extract_path, + message::{MessageToPackageBack, MessageToPackageUI}, }; -use joko_link_models::{MumbleLink, MumbleLinkSharedState}; +use joko_link_models::MumbleLinkResult; use miette::{IntoDiagnostic, Result}; use uuid::Uuid; use crate::manager::pack::loaded::{LoadedPackData, PackTasks}; -use crate::message::MessageToPackageUI; use super::pack::loaded::jokolay_to_marker_path; @@ -39,6 +42,13 @@ pub struct PackageBackSharedState { extract_path: std::path::PathBuf, } +struct PackageDataChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, + + notification_receiver: tokio::sync::mpsc::Receiver, + front_end_notifier: tokio::sync::mpsc::Sender, +} + /// It manage everything that has to do with marker packs. /// 1. imports, loads, saves and exports marker packs. /// 2. maintains the categories selection data for every pack @@ -70,8 +80,8 @@ pub struct PackageDataManager { pub currently_used_files: BTreeMap, parents: HashMap, loaded_elements: HashSet, - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, + channels: Option, + pub state: PackageBackSharedState, } @@ -83,12 +93,7 @@ impl PackageDataManager { /// 4. loads all the packs /// 5. loads all the activation data /// 6. returns self - pub fn new( - root_dir: Arc, - root_path: &std::path::Path, - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - ) -> Result { + pub fn new(root_dir: Arc, root_path: &std::path::Path) -> Result { let marker_packs_path = jokolay_to_marker_path(root_path); //TODO: load configuration from disk (ui.toml) let editable_path = jokolay_to_editable_path(root_path) @@ -112,8 +117,7 @@ impl PackageDataManager { currently_used_files: Default::default(), parents: Default::default(), loaded_elements: Default::default(), - channel_sender, - channel_receiver, + channels: None, state, }) } @@ -254,15 +258,17 @@ impl PackageDataManager { } } } - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::DeletedPacks(deleted).into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::DeletedPacks(deleted))); } MessageToPackageBack::ImportPack(file_path) => { tracing::trace!("Handling of MessageToPackageBack::ImportPack"); - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::NbTasksRunning(1).into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(1))); let start = std::time::SystemTime::now(); let result = import_pack_from_zip_file_path(file_path, &self.state.extract_path); let elaspsed = start.elapsed().unwrap_or_default(); @@ -272,19 +278,19 @@ impl PackageDataManager { ); match result { Ok((file_name, pack)) => { - let _ = self.channel_sender.blocking_send( - MessageToPackageUI::ImportedPack(file_name, pack).into(), - ); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::ImportedPack(file_name, pack), + )); } Err(e) => { - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::ImportFailure(e).into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::ImportFailure(e))); } } - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::NbTasksRunning(0).into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(0))); } MessageToPackageBack::ReloadPack => { unimplemented!( @@ -319,9 +325,10 @@ impl PackageDataManager { let uuid_of_insertion = self.save(data_pack, report.clone()); report.uuid = uuid_of_insertion; texture_pack.uuid = uuid_of_insertion; - let _ = self.channel_sender.blocking_send( - MessageToPackageUI::LoadedPack(texture_pack, report).into(), - ); + let channels = self.channels.as_mut().unwrap(); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::LoadedPack(texture_pack, report), + )); } Err(e) => { error!( @@ -347,9 +354,9 @@ impl PackageDataManager { ); let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - let msg = bincode::deserialize(&msg).unwrap(); - messages.push(msg); + let channels = self.channels.as_mut().unwrap(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); } for msg in messages { self.handle_message(msg); @@ -357,19 +364,14 @@ impl PackageDataManager { self.state.clone() } - pub fn tick( - &mut self, - loop_index: u128, - ms: &MumbleLinkSharedState, - link: Option<&MumbleLink>, - ) { + pub fn _tick(&mut self, mumble_link_result: &MumbleLinkResult) { let mut currently_used_files: BTreeMap = Default::default(); let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - let link = if ms.read_ui_link { - ms.copy_of_ui_link.as_ref() + let link = if mumble_link_result.read_ui_link { + mumble_link_result.ui_link.as_ref() } else { - link + mumble_link_result.link.as_ref() }; if let Some(link) = link { @@ -412,13 +414,13 @@ impl PackageDataManager { let tasks = &self.tasks; for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::NbTasksRunning(tasks.count()).into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(tasks.count()))); tasks.save_data(pack, pack.is_dirty()); pack.tick( - &self.channel_sender, - loop_index, + &channels.front_end_notifier, link, ¤tly_used_files, have_used_files_list_changed || self.state.choice_of_category_changed, @@ -430,20 +432,22 @@ impl PackageDataManager { } if map_changed { self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let _ = self.channel_sender.blocking_send( - MessageToPackageUI::ActiveElements(self.loaded_elements.clone()).into(), - ); + let channels = self.channels.as_mut().unwrap(); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::ActiveElements(self.loaded_elements.clone()), + )); } if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed { + let channels = self.channels.as_mut().unwrap(); //there is no point in sending a new list if nothing changed - let _ = self.channel_sender.blocking_send( - MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()).into(), - ); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()), + )); self.currently_used_files = currently_used_files; - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::TextureSwapChain.into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::TextureSwapChain)); } } self.state.choice_of_category_changed = false; @@ -482,10 +486,11 @@ impl PackageDataManager { pub fn load_all(&mut self) { once::assert_has_not_been_called!("Early load must happen only once"); + let channels = self.channels.as_mut().unwrap(); // Called only once at application start. - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::NbTasksRunning(1).into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(1))); self.tasks.load_all_packs( Arc::clone(&self.state.root_dir), self.state.root_path.clone(), @@ -499,17 +504,64 @@ impl PackageDataManager { for ((_, texture_pack), (_, report)) in std::iter::zip(texture_packages, report_packages) { - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::LoadedPack(texture_pack, report).into()); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::LoadedPack(texture_pack, report), + )); } - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::NbTasksRunning(0).into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(0))); } - let _ = self - .channel_sender - .blocking_send(MessageToPackageUI::FirstLoadDone.into()); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::FirstLoadDone)); + } +} + +impl JokolayComponent for PackageDataManager { + fn flush_all_messages(&mut self) { + let channels = self.channels.as_mut().unwrap(); + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + fn bind( + &mut self, + mut deps: HashMap>, + mut bound: HashMap, // Private channel only two bounded modules can use between each others. + mut input_notification: HashMap>, + _notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + ) { + let (_, front_end_notifier) = bound.remove(&0).unwrap(); + let channels = PackageDataChannels { + subscription_mumblelink: deps.remove(&0).unwrap(), + front_end_notifier, + notification_receiver: input_notification.remove(&0).unwrap(), + }; + self.channels = Some(channels); + } + fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + let channels = self.channels.as_mut().unwrap(); + let raw_mlr = channels.subscription_mumblelink.blocking_recv().unwrap(); + let mumble_link_result: MumbleLinkResult = from_data(raw_mlr); + self._tick(&mumble_link_result); + default_data_exchange() + } +} + +impl JokolayComponentDeps for PackageDataManager { + fn notify(&self) -> Vec<&str> { + vec![] + } + fn peer(&self) -> Vec<&str> { + vec!["ui:jokolay_package_manager"] + } + fn requires(&self) -> Vec<&str> { + vec!["back:mumble_link"] } } diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs index 6693838..5a65f0a 100644 --- a/crates/joko_package_manager/src/manager/package_ui.rs +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -7,13 +7,16 @@ use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; use image::EncodableLayout; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; -use joko_render_models::messages::UIToUIMessage; +use joko_render_models::messages::MessageToRenderer; use tracing::{info_span, trace}; use crate::message::MessageToPackageBack; -use joko_component_models::{ComponentDataExchange, JokolayUIComponent, PeerComponentChannel}; +use joko_component_models::{ + from_data, to_data, ComponentDataExchange, JokolayComponentDeps, JokolayUIComponent, + PeerComponentChannel, +}; use joko_core::{serde_glam::Vec3, RelativePath}; -use joko_link_models::{MumbleChanges, MumbleLink}; +use joko_link_models::{MumbleChanges, MumbleLink, MumbleLinkResult}; use miette::Result; use uuid::Uuid; @@ -30,6 +33,16 @@ pub struct PackageUISharedState { import_status: Arc>, } +struct PackageUIChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, + subscription_near_scene: tokio::sync::broadcast::Receiver, + + notification_receiver: tokio::sync::mpsc::Receiver, + + back_end_notifier: tokio::sync::mpsc::Sender, + renderer_notifier: tokio::sync::mpsc::Sender, +} + #[must_use] pub struct PackageUIManager { default_marker_texture: Option, @@ -46,20 +59,12 @@ pub struct PackageUIManager { delayed_marker_texture: Vec<(Uuid, RelativePath, Uuid, Vec3, CommonAttributes)>, delayed_trail_texture: Vec<(Uuid, RelativePath, Uuid, CommonAttributes)>, - //egui_context: &'l egui::Context, //TODO: remove, this is not the proper place to be, or if it is, badly used - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - sender_u2u: Option>, - receiver_mumblelink: Option>, - receiver_near_scene: Option>, + channels: Option, state: PackageUISharedState, } impl PackageUIManager { - pub fn new( - channel_receiver: tokio::sync::mpsc::Receiver, - channel_sender: tokio::sync::mpsc::Sender, - ) -> Self { + pub fn new() -> Self { let state = PackageUISharedState { list_of_textures_changed: false, first_load_done: false, @@ -80,11 +85,7 @@ impl PackageUIManager { delayed_marker_texture: Default::default(), delayed_trail_texture: Default::default(), - channel_sender, - channel_receiver, - sender_u2u: None, - receiver_mumblelink: None, - receiver_near_scene: None, + channels: None, state, } } @@ -120,9 +121,10 @@ impl PackageUIManager { tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); self.save(pack_texture, report); self.state.import_status = Default::default(); - let _ = self - .channel_sender - .blocking_send(MessageToPackageBack::CategoryActivationStatusChanged.into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels.back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationStatusChanged, + )); } MessageToPackageUI::MarkerTexture( pack_uuid, @@ -176,18 +178,18 @@ impl PackageUIManager { } pub fn flush_all_messages(&mut self) -> PackageUISharedState { + let channels = self.channels.as_mut().unwrap(); if let Ok(mut import_status) = self.state.import_status.lock() { if let ImportStatus::LoadingPack(file_path) = &mut *import_status { - let _ = self - .channel_sender - .blocking_send(MessageToPackageBack::ImportPack(file_path.clone()).into()); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::ImportPack(file_path.clone()))); *import_status = ImportStatus::WaitingLoading(file_path.clone()); } } let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - let msg = bincode::deserialize(&msg).unwrap(); - messages.push(msg); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); } for msg in messages { self.handle_message(msg); @@ -326,7 +328,8 @@ impl PackageUIManager { pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { let tasks = &self.tasks; - let sender_u2u = self.sender_u2u.as_ref().unwrap(); + let channels = self.channels.as_ref().unwrap(); + let renderer_notifier = &channels.renderer_notifier; for pack in self.packs.values_mut() { tasks.save_texture(pack, pack.is_dirty()); } @@ -335,10 +338,10 @@ impl PackageUIManager { { for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); - pack.tick(sender_u2u, timestamp, link, z_near, tasks)?; + pack.tick(renderer_notifier, timestamp, link, z_near, tasks)?; std::mem::drop(span_guard); } - let _ = sender_u2u.blocking_send(UIToUIMessage::RenderSwapChain.into()); + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); } Ok(()) } @@ -359,26 +362,27 @@ impl PackageUIManager { } if ui.button("Activate all elements").clicked() { self.category_set_all(true); - let _ = self - .channel_sender - .blocking_send(MessageToPackageBack::CategorySetAll(true).into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(true))); } if ui.button("Deactivate all elements").clicked() { self.category_set_all(false); - let _ = self - .channel_sender - .blocking_send(MessageToPackageBack::CategorySetAll(false).into()); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(false))); } + let channels = self.channels.as_mut().unwrap(); for (pack, import_quality_report) in std::iter::zip(self.packs.values_mut(), self.reports.values()) { //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - let u2u_sender = self.sender_u2u.as_ref().unwrap(); pack.category_sub_menu( - &self.channel_sender, - u2u_sender, + &channels.back_end_notifier, ui, self.show_only_active, import_quality_report, @@ -430,6 +434,7 @@ impl PackageUIManager { } fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool) { + let channels = self.channels.as_mut().unwrap(); let mut files_changed = false; Window::new("File Manager") .open(open) @@ -518,9 +523,9 @@ impl PackageUIManager { Ok(()) }); if files_changed { - let _ = self.channel_sender.blocking_send( - MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()).into(), - ); + let _ = channels.back_end_notifier.blocking_send(to_data( + MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()), + )); } } @@ -588,6 +593,7 @@ impl PackageUIManager { first_load_done: bool, ) { Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { + let channels = self.channels.as_mut().unwrap(); CollapsingHeader::new("Loaded Packs").show(ui, |ui| { egui::Grid::new("packs").striped(true).show(ui, |ui| { if !first_load_done { @@ -608,7 +614,7 @@ impl PackageUIManager { ui.end_row(); } if !to_delete.is_empty() { - let _ = self.channel_sender.blocking_send(MessageToPackageBack::DeletePacks(to_delete).into()); + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::DeletePacks(to_delete))); } }); }); @@ -639,7 +645,7 @@ impl PackageUIManager { ui.text_edit_singleline(name); }); if ui.button("save").clicked() { - let _ = self.channel_sender.blocking_send(MessageToPackageBack::SavePack(name.clone(), pack.clone()).into()); + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::SavePack(name.clone(), pack.clone()))); } } } @@ -690,27 +696,24 @@ impl PackageUIManager { } } -//TODO: there is a need for a more complex input according to deps -impl JokolayUIComponent for PackageUIManager { - fn flush_all_messages(&mut self) -> PackageUISharedState { +impl JokolayUIComponent for PackageUIManager { + fn flush_all_messages(&mut self) { + let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); - while let Ok(msg) = self.channel_receiver.try_recv() { - messages.push(msg.into()); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); } for msg in messages { self.handle_message(msg); } - self.state.clone() } - fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> Option<&()> { - let raw_link = self - .receiver_mumblelink - .as_mut() - .unwrap() - .blocking_recv() - .unwrap(); - let link: &MumbleLink = &bincode::deserialize(&raw_link).unwrap(); + fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> PackageUISharedState { + let raw_link = { + let channels = self.channels.as_mut().unwrap(); + channels.subscription_mumblelink.blocking_recv().unwrap() + }; + let link_result: MumbleLinkResult = from_data(raw_link); for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in std::mem::take(&mut self.delayed_marker_texture) @@ -736,15 +739,11 @@ impl JokolayUIComponent for PackageUIManager { ); } - let raw_z_near = self - .receiver_near_scene - .as_mut() - .unwrap() - .blocking_recv() - .unwrap(); - let z_near: f32 = bincode::deserialize(&raw_z_near).unwrap(); - let _ = self._tick(timestamp, link, z_near); - None + let channels = self.channels.as_mut().unwrap(); + let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); + let z_near: f32 = from_data(raw_z_near); + let _ = self._tick(timestamp, link_result.link.as_ref().unwrap(), z_near); + self.state.clone() } fn bind( &mut self, @@ -752,8 +751,8 @@ impl JokolayUIComponent for PackageUIManager { u32, tokio::sync::broadcast::Receiver, >, - mut _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - mut _input_notification: std::collections::HashMap< + mut bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + mut input_notification: std::collections::HashMap< u32, tokio::sync::mpsc::Receiver, >, @@ -762,9 +761,27 @@ impl JokolayUIComponent for PackageUIManager { tokio::sync::mpsc::Sender, >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. ) { - self.sender_u2u = notify.remove(&0); - self.receiver_mumblelink = deps.remove(&0); - self.receiver_near_scene = deps.remove(&1); - unimplemented!("PackageUIManager component binding is not implemented") + let (_, back_end_notifier) = bound.remove(&0).unwrap(); + let channels = PackageUIChannels { + subscription_mumblelink: deps.remove(&0).unwrap(), + subscription_near_scene: deps.remove(&1).unwrap(), + notification_receiver: input_notification.remove(&0).unwrap(), + back_end_notifier, + renderer_notifier: notify.remove(&0).unwrap(), + }; + + self.channels = Some(channels); + } +} + +impl JokolayComponentDeps for PackageUIManager { + fn notify(&self) -> Vec<&str> { + vec!["ui:jokolay_renderer"] + } + fn peer(&self) -> Vec<&str> { + vec!["back:jokolay_package_manager"] + } + fn requires(&self) -> Vec<&str> { + vec!["ui:mumble_link", "ui:jokolay_near_scene"] } } diff --git a/crates/joko_package_manager/src/message.rs b/crates/joko_package_manager/src/message.rs index 949aad7..eb7475e 100644 --- a/crates/joko_package_manager/src/message.rs +++ b/crates/joko_package_manager/src/message.rs @@ -1,3 +1,5 @@ +mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); + use std::collections::{BTreeMap, HashSet}; use joko_component_models::ComponentDataExchange; @@ -12,7 +14,7 @@ use joko_core::{serde_glam::Vec3, RelativePath}; use crate::LoadedPackTexture; -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub enum MessageToPackageUI { ActiveElements(HashSet), //list of all elements that are loaded for current map CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display @@ -27,7 +29,7 @@ pub enum MessageToPackageUI { TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), } - +/* impl From for ComponentDataExchange { fn from(src: MessageToPackageUI) -> ComponentDataExchange { bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages @@ -39,9 +41,9 @@ impl Into for ComponentDataExchange { fn into(self) -> MessageToPackageUI { bincode::deserialize(&self).unwrap() } -} +}*/ -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub enum MessageToPackageBack { ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. @@ -53,9 +55,17 @@ pub enum MessageToPackageBack { ReloadPack, SavePack(String, PackCore), } - +/* impl From for ComponentDataExchange { fn from(src: MessageToPackageBack) -> ComponentDataExchange { bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages } } + +#[allow(clippy::from_over_into)] +impl Into for ComponentDataExchange { + fn into(self) -> MessageToPackageBack { + bincode::deserialize(&self).unwrap() + } +} +*/ diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml index af29fc6..55bb78b 100644 --- a/crates/joko_package_models/Cargo.toml +++ b/crates/joko_package_models/Cargo.toml @@ -3,10 +3,19 @@ name = "joko_package_models" version = "0.2.1" edition = "2021" + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] # jmf deps # for marker packs base64 = "0.21.2" +bincode = { workspace = true } bimap = { version = "0.6.3", features = ["serde"] } bytemuck = { workspace = true } data-encoding = "2.4.0" @@ -15,6 +24,7 @@ glam = { workspace = true } indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip itertools = { workspace = true } joko_core = { path = "../joko_core" } +joko_component_models = { path = "../joko_component_models" } jokoapi = { path = "../jokoapi" } miette = { workspace = true } ordered_hash_map = { workspace = true } @@ -27,7 +37,7 @@ tracing = { workspace = true } url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } xot = { version = "0.16.0" } - +mutually_exclusive_features = { workspace = true } [dev-dependencies] diff --git a/crates/joko_plugin_manager/Cargo.toml b/crates/joko_plugin_manager/Cargo.toml index 79043dc..41b68d8 100644 --- a/crates/joko_plugin_manager/Cargo.toml +++ b/crates/joko_plugin_manager/Cargo.toml @@ -5,6 +5,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] joko_component_models = { path = "../joko_component_models" } scopeguard = "1.2.0" diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs index a0c7ff2..018adf8 100644 --- a/crates/joko_plugin_manager/src/lib.rs +++ b/crates/joko_plugin_manager/src/lib.rs @@ -1,15 +1,16 @@ use joko_component_models::{ - ComponentDataExchange, JokolayComponent, JokolayComponentDeps, PeerComponentChannel, + default_data_exchange, ComponentDataExchange, JokolayComponent, JokolayComponentDeps, + PeerComponentChannel, }; pub struct JokolayPlugin {} pub struct JokolayPluginManager {} -impl JokolayComponent<(), ()> for JokolayPlugin { +impl JokolayComponent for JokolayPlugin { fn flush_all_messages(&mut self) {} - fn tick(&mut self, _timestamp: f64) -> Option<&()> { - None + fn tick(&mut self, _timestamp: f64) -> ComponentDataExchange { + default_data_exchange() } fn bind( &mut self, @@ -28,6 +29,6 @@ impl JokolayComponent<(), ()> for JokolayPlugin { } impl JokolayComponentDeps for JokolayPlugin { fn requires(&self) -> Vec<&str> { - vec!["mumble_link_back"] + vec!["back:mumble_link"] } } diff --git a/crates/joko_render_manager/Cargo.toml b/crates/joko_render_manager/Cargo.toml index de8b2fa..01181c0 100644 --- a/crates/joko_render_manager/Cargo.toml +++ b/crates/joko_render_manager/Cargo.toml @@ -7,6 +7,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] bincode = { workspace = true } bytemuck = { workspace = true } diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs index 5dfebdf..0bf18ba 100644 --- a/crates/joko_render_manager/src/renderer.rs +++ b/crates/joko_render_manager/src/renderer.rs @@ -12,17 +12,22 @@ use egui_render_three_d::ThreeDBackend; use egui_render_three_d::ThreeDConfig; use egui_window_glfw_passthrough::GlfwBackend; use glam::Mat4; +use joko_component_models::default_data_exchange; +use joko_component_models::from_data; use joko_component_models::ComponentDataExchange; use joko_component_models::JokolayComponent; use joko_component_models::JokolayComponentDeps; use joko_component_models::PeerComponentChannel; use joko_link_models::MumbleLink; use joko_link_models::UIState; -use joko_render_models::messages::UIToUIMessage; +use joko_render_models::messages::MessageToRenderer; use three_d::prelude::*; use joko_render_models::{marker::MarkerObject, trail::TrailObject}; +struct JokoRendererChannels { + notification_receiver: tokio::sync::mpsc::Receiver, +} pub struct JokoRenderer { pub view_proj: Mat4, pub cam_pos: glam::Vec3, @@ -32,11 +37,11 @@ pub struct JokoRenderer { pub is_map_open: bool, pub billboard_renderer: BillBoardRenderer, pub gl: egui_render_three_d::ThreeDBackend, - channel_receiver: Option>, + channels: Option, } impl JokoRenderer { - pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { + pub fn new(glfw_backend: &mut GlfwBackend) -> Self { let glfw = glfw_backend.glfw.clone(); let backend = ThreeDBackend::new( ThreeDConfig { @@ -73,7 +78,7 @@ impl JokoRenderer { gl: backend, billboard_renderer, cam_pos: Default::default(), - channel_receiver: None, + channels: None, } } @@ -137,31 +142,31 @@ impl JokoRenderer { ]; } */ - fn handle_u2u_message(&mut self, msg: UIToUIMessage) { + fn handle_u2u_message(&mut self, msg: MessageToRenderer) { match msg { - UIToUIMessage::BulkMarkerObject(marker_objects) => { + MessageToRenderer::BulkMarkerObject(marker_objects) => { tracing::debug!( "Handling of UIToUIMessage::BulkMarkerObject {}", marker_objects.len() ); self.extend_markers(marker_objects); } - UIToUIMessage::BulkTrailObject(trail_objects) => { + MessageToRenderer::BulkTrailObject(trail_objects) => { tracing::debug!( "Handling of UIToUIMessage::BulkTrailObject {}", trail_objects.len() ); self.extend_trails(trail_objects); } - UIToUIMessage::MarkerObject(mo) => { + MessageToRenderer::MarkerObject(mo) => { tracing::trace!("Handling of UIToUIMessage::MarkerObject"); self.add_billboard(*mo); } - UIToUIMessage::TrailObject(to) => { + MessageToRenderer::TrailObject(to) => { tracing::trace!("Handling of UIToUIMessage::TrailObject"); self.add_trail(*to); } - UIToUIMessage::RenderSwapChain => { + MessageToRenderer::RenderSwapChain => { tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); self.swap(); } @@ -242,7 +247,7 @@ impl JokoRenderer { } impl JokolayComponentDeps for JokoRenderer {} -impl JokolayComponent<(), ()> for JokoRenderer { +impl JokolayComponent for JokoRenderer { fn bind( &mut self, _deps: std::collections::HashMap< @@ -256,21 +261,24 @@ impl JokolayComponent<(), ()> for JokoRenderer { >, _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. ) { - self.channel_receiver = input_notification.remove(&0); + let channels = JokoRendererChannels { + notification_receiver: input_notification.remove(&0).unwrap(), + }; + self.channels = Some(channels); } fn flush_all_messages(&mut self) { - let channel_receiver = self.channel_receiver.as_mut().unwrap(); + let channels = self.channels.as_mut().unwrap(); //two steps reading due to self mutability required by channel let mut messages = Vec::new(); - while let Ok(msg) = channel_receiver.try_recv() { - messages.push(msg.into()); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(msg)); } for msg in messages { self.handle_u2u_message(msg); } } - fn tick(&mut self, _latest_time: f64) -> Option<&()> { + fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { let link: Option<&MumbleLink> = None; if let Some(link) = link { //x positive => east @@ -349,6 +357,6 @@ impl JokolayComponent<(), ()> for JokoRenderer { } else { self.has_link = false; } - None + default_data_exchange() } } diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml index 9449c18..c2d0cd5 100644 --- a/crates/joko_render_models/Cargo.toml +++ b/crates/joko_render_models/Cargo.toml @@ -7,6 +7,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + + [dependencies] bincode = { workspace = true } bytemuck = { workspace = true } @@ -15,4 +22,6 @@ serde = { workspace = true } joko_core = { path = "../joko_core" } joko_component_models = { path = "../joko_component_models" } +mutually_exclusive_features = { workspace = true } + diff --git a/crates/joko_render_models/src/marker.rs b/crates/joko_render_models/src/marker.rs index 7fae322..cc630d4 100644 --- a/crates/joko_render_models/src/marker.rs +++ b/crates/joko_render_models/src/marker.rs @@ -12,7 +12,7 @@ pub struct MarkerVertex { pub color: [u8; 4], } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MarkerObject { /// The six vertices that make up the marker quad pub vertices: [MarkerVertex; 6], diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs index 50b4443..d6a7ec8 100644 --- a/crates/joko_render_models/src/messages.rs +++ b/crates/joko_render_models/src/messages.rs @@ -1,10 +1,16 @@ +mutually_exclusive_features::exactly_one_of!( + "messages_any", + "messages_bincode", + "messages_downcast" +); + use joko_component_models::ComponentDataExchange; use serde::{Deserialize, Serialize}; use crate::{marker::MarkerObject, trail::TrailObject}; -#[derive(Serialize, Deserialize)] -pub enum UIToUIMessage { +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToRenderer { BulkMarkerObject(Vec), BulkTrailObject(Vec), //Present,// a render loop is finished and we can present it @@ -13,15 +19,17 @@ pub enum UIToUIMessage { TrailObject(Box), } -impl From for ComponentDataExchange { - fn from(src: UIToUIMessage) -> ComponentDataExchange { +/* +impl From for ComponentDataExchange { + fn from(src: MessageToRenderer) -> ComponentDataExchange { bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages } } #[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> UIToUIMessage { +impl Into for ComponentDataExchange { + fn into(self) -> MessageToRenderer { bincode::deserialize(&self).unwrap() } } +*/ diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index cfee5ed..9aca08d 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -8,9 +8,15 @@ default-run = "jokolay" name = "jokolay" path = "src/main.rs" + [features] +default = ["messages_any"] # will not work because wayland won't allow us to get global cursor position wayland = ["egui_window_glfw_passthrough/wayland"] +messages_any = [] +messages_downcast = [] +messages_bincode = [] + [dependencies] enumflags2 = { workspace = true } @@ -34,6 +40,7 @@ serde_json = { workspace = true } tokio = { workspace = true } indexmap = { workspace = true } ringbuffer = { workspace = true } +mutually_exclusive_features = { workspace = true } rayon = { workspace = true } diff --git a/crates/jokolay/src/app/messages.rs b/crates/jokolay/src/app/messages.rs index 5d14d68..6b0af2c 100644 --- a/crates/jokolay/src/app/messages.rs +++ b/crates/jokolay/src/app/messages.rs @@ -1,3 +1,4 @@ +mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); pub enum MessageToApplicationBack { SaveUIConfiguration(String), } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 42b86f5..a3dc90d 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, io::Write, ops::DerefMut, sync::{Arc, Mutex}, @@ -17,11 +16,13 @@ use crate::app::mumble::mumble_gui; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use init::{get_jokolay_dir, get_jokolay_path}; use joko_component_manager::ComponentManager; -use joko_component_models::{ComponentDataExchange, JokolayComponent, JokolayUIComponent}; +use joko_component_models::{from_data, JokolayComponent, JokolayUIComponent}; use joko_package_manager::{PackageDataManager, PackageUIManager}; use joko_link_manager::MumbleManager; -use joko_link_models::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, UISize}; +use joko_link_models::{ + MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult, UISize, +}; use joko_package_manager::jokolay_to_editable_path; use joko_package_manager::ImportStatus; use joko_render_manager::renderer::JokoRenderer; @@ -86,14 +87,14 @@ impl Jokolay { let dummy_plugin = Box::new(JokolayPlugin {}); component_manager.register( - "mumble_link_back", + "ui:mumble_link", Box::new( MumbleManager::new("MumbleLink", true) .wrap_err("failed to create mumble manager")?, ), ); component_manager.register( - "mumble_link_back", + "back:mumble_link", Box::new( MumbleManager::new("MumbleLink", false) .wrap_err("failed to create mumble manager")?, @@ -101,15 +102,6 @@ impl Jokolay { ); component_manager.register("dummy_plugin", dummy_plugin); - match component_manager.build_routes() { - Ok(_) => {} - Err(e) => { - error!(?e, "Could not build component routes"); - } - } - - let (b2u_sender, b2u_receiver) = tokio::sync::mpsc::channel(10); - let (u2b_sender, u2b_receiver) = tokio::sync::mpsc::channel(10); /* components can be migrated to plugins root_path/ @@ -129,11 +121,17 @@ impl Jokolay { ... */ + component_manager.register( + "back:jokolay_package_manager", + Box::new(PackageDataManager::new( + Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay + &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay + )?), + ); + let package_data_manager = PackageDataManager::new( Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay - u2b_receiver, // to be removed since dynamically inserted on tick (once Plugin is implemented) - b2u_sender, //TODO: list of bounded & notify )?; let mut theme_manager = ThemeManager::new(Arc::clone(&root_dir)).wrap_err("failed to create theme manager")?; @@ -168,11 +166,21 @@ impl Jokolay { }); let maximal_window_width = video_mode.unwrap().width; let maximal_window_height = video_mode.unwrap().height; - let mut package_ui_manager = PackageUIManager::new(b2u_receiver, u2b_sender); + + component_manager.register( + "ui:jokolay_package_manager", + Box::new(PackageUIManager::new()), + ); + let mut package_ui_manager = PackageUIManager::new(); glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); - let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); + + component_manager.register( + "ui:jokolay_renderer", + Box::new(JokoRenderer::new(&mut glfw_backend)), + ); + let joko_renderer = JokoRenderer::new(&mut glfw_backend); let editable_path = jokolay_to_editable_path(&root_path) .to_str() @@ -183,6 +191,13 @@ impl Jokolay { editable_path.clone(), ); + match component_manager.build_routes() { + Ok(_) => {} + Err(e) => { + panic!("Could not build component routes. {}", e); + } + } + let menu_panel = MenuPanel::default(); package_ui_manager.late_init(&egui_context); @@ -294,11 +309,12 @@ impl Jokolay { Self::handle_app_message(Arc::clone(&package_manager.state.root_dir), msg); } - let ms = mumble_manager.flush_all_messages(); + mumble_manager.flush_all_messages(); - let link = mumble_manager.tick(start.elapsed().into_diagnostic()?.as_secs_f64()); + let latest_time = start.elapsed().into_diagnostic()?.as_secs_f64(); + let mumble_link_result = mumble_manager.tick(latest_time); //TODO: in Component manager, make use of this value package_manager.flush_all_messages(); - package_manager.tick(loop_index, &ms, link); + package_manager.tick(latest_time); thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; @@ -332,7 +348,6 @@ impl Jokolay { let (u2gb_sender, u2gb_receiver) = std::sync::mpsc::channel(); let (u2mb_sender, u2mb_receiver) = tokio::sync::mpsc::channel(1); //FIXME: route the data to the consumers. - let (u2u_sender, u2u_receiver) = tokio::sync::mpsc::channel(1); Self::start_background_loop(Arc::clone(&self.app), u2gb_receiver); @@ -341,28 +356,6 @@ impl Jokolay { let mut gui = *self.gui; let mut local_state = self.state_ui; - //TODO: in "deps", broadcast link and z_near at each loop: link, JokoRenderer::get_z_near() - let mut input_notification: HashMap< - u32, - tokio::sync::mpsc::Receiver, - > = Default::default(); //for renderer - input_notification.insert(0, u2u_receiver); - gui.joko_renderer.bind( - Default::default(), - Default::default(), - input_notification, - Default::default(), - ); - - let mut notifier: HashMap> = - Default::default(); //for package manager - notifier.insert(0, u2u_sender); - gui.package_manager.bind( - Default::default(), - Default::default(), - Default::default(), - notifier, - ); loop { //TODO: one could wrap the egui_context into a plugin result so that it can be used from other plugins //TODO: same for the UI as a notified element. @@ -445,7 +438,8 @@ impl Jokolay { let _ = u2mb_sender.send(MessageToMumbleLinkBack::Value(local_state.link.clone())); } else { let is_mumble_alive = mumble_manager.is_alive(); - match mumble_manager.tick(latest_time) { + let res: MumbleLinkResult = from_data(mumble_manager.tick(latest_time)); + match &res.link { Some(link) => { if link.changes.contains(MumbleChanges::WindowPosition) || link.changes.contains(MumbleChanges::WindowSize) From f6d81b2f32da3facc5a82bf2f3fce6f82066e128 Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 1 May 2024 15:55:48 +0200 Subject: [PATCH 48/54] communication channels between components --- Cargo.lock | 37 +- Cargo.toml | 1 + crates/joko_component_manager/Cargo.toml | 7 +- crates/joko_component_manager/src/lib.rs | 403 +++++++++++++----- crates/joko_component_models/Cargo.toml | 5 +- crates/joko_component_models/src/lib.rs | 76 ++-- .../joko_component_models/src/messages_any.rs | 21 +- crates/joko_core/Cargo.toml | 8 - crates/joko_core/src/lib.rs | 2 - crates/joko_link_manager/Cargo.toml | 6 - crates/joko_link_manager/src/lib.rs | 32 +- crates/joko_link_models/Cargo.toml | 16 - crates/joko_link_models/src/lib.rs | 16 - crates/joko_link_models/src/messages.rs | 18 - crates/joko_package_manager/Cargo.toml | 9 - .../src/manager/pack/import.rs | 5 +- .../src/manager/pack/loaded.rs | 9 +- .../src/manager/package_data.rs | 40 +- .../src/manager/package_ui.rs | 78 ++-- crates/joko_package_manager/src/message.rs | 30 -- crates/joko_package_models/Cargo.toml | 12 +- crates/joko_plugin_manager/Cargo.toml | 8 - crates/joko_plugin_manager/src/lib.rs | 22 +- crates/joko_render_manager/Cargo.toml | 8 - crates/joko_render_manager/src/billboard.rs | 1 + crates/joko_render_manager/src/renderer.rs | 41 +- crates/joko_render_models/Cargo.toml | 9 - crates/joko_render_models/src/messages.rs | 22 - crates/jokolay/Cargo.toml | 6 +- crates/jokolay/src/app/messages.rs | 1 - crates/jokolay/src/app/mod.rs | 37 +- 31 files changed, 462 insertions(+), 524 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22233f1..499bcbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1450,13 +1450,12 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" name = "joko_component_manager" version = "0.2.1" dependencies = [ + "bimap", "bincode", "egui", "joko_component_models", "petgraph", "scopeguard", - "smol_str", - "solvent", "tokio", "tracing", ] @@ -1470,7 +1469,6 @@ dependencies = [ "egui", "scopeguard", "serde", - "smol_str", "tokio", ] @@ -1480,7 +1478,6 @@ version = "0.2.1" dependencies = [ "bytemuck", "glam", - "mutually_exclusive_features", "scopeguard", "serde", "smol_str", @@ -1521,26 +1518,17 @@ name = "joko_link_models" version = "0.2.1" dependencies = [ "arcdps", - "bincode", "enumflags2", "glam", - "joko_component_models", "joko_core", "miette", - "mutually_exclusive_features", "notify", "num-derive", "num-traits", "serde", - "serde_json", - "time", - "tokio", - "tracing", "tracing-appender", "tracing-subscriber", - "widestring", "windows", - "x11rb", ] [[package]] @@ -1548,7 +1536,6 @@ name = "joko_package_manager" version = "0.2.1" dependencies = [ "base64", - "bincode", "bytemuck", "cap-std", "cxx", @@ -1567,7 +1554,6 @@ dependencies = [ "joko_render_models", "jokoapi", "miette", - "mutually_exclusive_features", "once", "ordered_hash_map", "paste", @@ -1595,7 +1581,6 @@ version = "0.2.1" dependencies = [ "base64", "bimap", - "bincode", "bytemuck", "cxx-build", "data-encoding", @@ -1603,11 +1588,9 @@ dependencies = [ "glam", "indexmap", "itertools", - "joko_component_models", "joko_core", "jokoapi", "miette", - "mutually_exclusive_features", "ordered_hash_map", "paste", "phf", @@ -1628,7 +1611,6 @@ version = "0.2.1" dependencies = [ "joko_component_models", "scopeguard", - "smol_str", "tokio", ] @@ -1636,7 +1618,6 @@ dependencies = [ name = "joko_render_manager" version = "0.2.1" dependencies = [ - "bincode", "bytemuck", "egui", "egui_render_three_d", @@ -1653,12 +1634,9 @@ dependencies = [ name = "joko_render_models" version = "0.2.1" dependencies = [ - "bincode", "bytemuck", "glam", - "joko_component_models", "joko_core", - "mutually_exclusive_features", "serde", ] @@ -1694,7 +1672,6 @@ dependencies = [ "joko_plugin_manager", "joko_render_manager", "miette", - "mutually_exclusive_features", "rayon", "rfd", "ringbuffer", @@ -1916,12 +1893,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mutually_exclusive_features" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" - [[package]] name = "next-gen" version = "0.1.1" @@ -2758,12 +2729,6 @@ dependencies = [ "serde", ] -[[package]] -name = "solvent" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" - [[package]] name = "spin" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index 286ae64..d00343f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ resolver = "2" [workspace.dependencies] #https://docs.rs/tracing/latest/tracing/level_filters/index.html +bimap = { version = "0.6.3", features = ["serde"] } bincode = "1.3.3" bytemuck = { version = "1", features = ["derive"] } cap-directories = { version = "2" } diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml index 207d2ec..a276f1a 100644 --- a/crates/joko_component_manager/Cargo.toml +++ b/crates/joko_component_manager/Cargo.toml @@ -9,15 +9,14 @@ edition = "2021" default = ["messages_any"] messages_any = [] messages_downcast = [] -messages_bincode = [] +messages_bincode = ["dep:bincode"] [dependencies] -bincode = { workspace = true } +bimap = { workspace = true } +bincode = { workspace = true, optional = true } egui = { workspace = true } scopeguard = "1.2.0" -smol_str = { workspace = true } tokio = { workspace = true } joko_component_models = { path = "../joko_component_models" } -solvent = "0.8.3" petgraph = "0.6.4" tracing.workspace = true diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index cdf0a1a..9eec99f 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -3,45 +3,80 @@ use std::{ hash::Hash, }; -use joko_component_models::{ComponentDataExchange, JokolayComponent, JokolayComponentDeps}; +use joko_component_models::{ComponentChannels, ComponentDataExchange, JokolayComponent}; use petgraph::{ - csr::IndexType, graph::NodeIndex, stable_graph::StableDiGraph, visit::IntoNodeIdentifiers, - Direction, + csr::IndexType, + graph::NodeIndex, + stable_graph::{EdgeReference, StableDiGraph}, + visit::{EdgeRef, IntoNodeIdentifiers}, }; use tracing::trace; +type BroadcastChannels = ( + tokio::sync::broadcast::Sender, + tokio::sync::broadcast::Receiver, +); pub struct ComponentManager { //TODO: make it a component too ? - data: HashMap>, + known_components: HashMap, + broadcasters: HashMap, //a receiver is kept idle in order to not close the channels. https://docs.rs/tokio/latest/tokio/sync/broadcast/#closing + notifications: HashMap>, } struct ComponentHandle { + name: String, component: Box, + channels: ComponentChannels, + relations_to_ids: HashMap, } pub struct ComponentExecutor { components: Vec, //FIXME: how to type erase result ? } -fn get_invocation_order(my_graph: &mut StableDiGraph) -> Vec +#[derive(Clone)] +enum RelationShip { + Requires, + Peer, + Notify, +} +fn get_invocation_order( + graph: &mut StableDiGraph, + filter: F, +) -> Vec> where N: std::cmp::Ord, Ix: IndexType, + F: Fn(EdgeReference) -> bool, { let mut invocation_order = Vec::new(); //peel nodes one by one - while my_graph.externals(Direction::Outgoing).count() > 0 { + let mut modified = true; + while modified { + modified = false; let mut to_delete = Vec::new(); - for external_node in my_graph.externals(Direction::Outgoing) { - to_delete.push(external_node); + for node_id in graph.node_indices() { + let mut is_pointed = false; + for edge in graph.edges(node_id) { + if filter(edge) { + is_pointed = true; + break; + } + } + if !is_pointed { + to_delete.push(node_id); + } } let mut current_level_invocation_order = Vec::new(); for external_node in to_delete { - current_level_invocation_order.push(my_graph.remove_node(external_node).unwrap()); + graph.remove_node(external_node); + modified = true; + current_level_invocation_order.push(external_node); } current_level_invocation_order.sort(); //This grant a deterministic order regardless of circumstances invocation_order.extend(current_level_invocation_order); } + //if there is a cycle, there are remaining nodes invocation_order } @@ -58,12 +93,54 @@ impl ComponentManager { pub fn new() -> Self { //clone itself on a world basis ? which would follow a component thread Self { - data: Default::default(), + known_components: Default::default(), + broadcasters: Default::default(), + notifications: Default::default(), } } - pub fn register(&mut self, service_name: &str, co: Box) { - self.data.insert(service_name.to_owned(), co); + /// Register a component. + /// On its relationship, each component reference (names) shall be assigned an id. + /// That id is 0 based and goes in following order: peers, notify, requirements + /// A component, when binding must retrieve the with the proper id. + pub fn register( + &mut self, + component_name: &str, + component: Box, + ) -> Result<(), String> { + if !has_unique_elements( + component + .peers() + .iter() + .chain(component.notify().iter()) + .chain(component.requirements().iter()), + ) { + return Err(format!( + "Service {} has duplicate elements. Each name can only appear at one place", + component_name + )); + } + let mut relations_to_ids: HashMap = Default::default(); + for (idx, name) in component + .peers() + .iter() + .chain(component.notify().iter()) + .chain(component.requirements().iter()) + .enumerate() + { + relations_to_ids.insert(name.to_string(), idx); + } + + let handle = ComponentHandle { + name: component_name.to_string(), + component, + channels: ComponentChannels::default(), + relations_to_ids, + }; + self.known_components + .insert(component_name.to_string(), handle); + + Ok(()) } pub fn executor(&self, world: &str) -> ComponentExecutor { @@ -77,127 +154,125 @@ impl ComponentManager { components: Default::default(), } } + + /// Check, create and bind the relationships and communication channels between the components. + /// A world define what is accessible. Mostly for execution separation purpose (another thread, server, anything). + /// "requirements" must be a DAG. It must always be in the same "world". + /// "peers" must be mutual. + /// pub fn build_routes(&mut self) -> Result<(), String> { - /* - TODO: split in worlds - - https://docs.rs/dep-graph/latest/dep_graph/ - https://lib.rs/crates/petgraph - https://docs.rs/solvent/latest/solvent/ - => check "peer" is always mutual - => graph with the "peer" elements replaced by some merged id - => check there is no loop (there could be surprises) - => if there is no problem, then: - - build again the graph with UI plugins only and save one traversal (memory + file) - - build again the graph with back plugins only and save one traversal (memory + file) - => if there is a problem, do not save anything - - fn tick( - &mut self, - ) -> Option<&PluginResult>; where u32 is the position in bind() + requires() - */ + //TODO: check worlds + + type G = petgraph::stable_graph::StableDiGraph; - type G = petgraph::stable_graph::StableDiGraph; + //TODO: those are temporary channels, one should work between existing and new channels => we need to save the work of a previous build + let mut notifications: HashMap> = + Default::default(); + let mut broadcasters: HashMap = Default::default(); - let mut hosted_services: HashMap> = Default::default(); - let mut known_services: HashMap> = Default::default(); + let mut known_services: bimap::BiHashMap> = Default::default(); let mut depgraph: G = G::default(); - let mut translation: HashMap, NodeIndex> = Default::default(); - let mut service_id = 0; - for (service_name, co) in self.data.iter() { - if !has_unique_elements( - co.peer() - .iter() - .chain(co.notify().iter()) - .chain(co.requires().iter()), - ) { - return Err(format!( - "Service {} has duplicate elements. Each name can only appear at one place", - service_name - )); - } - let service_name = service_name.clone(); - if !hosted_services.contains_key(&service_name) { - let node_id = depgraph.add_node(service_id); - service_id += 1; - hosted_services.insert(service_name.clone(), node_id); - known_services.insert(service_name.clone(), node_id); + + // initialize the basic channels + for (component_name, handle) in self.known_components.iter_mut() { + let node_id = depgraph.add_node(component_name.clone()); + known_services.insert(component_name.clone(), node_id); + if handle.component.accept_notifications() { + let (sender, receiver) = tokio::sync::mpsc::channel(1000); + handle.channels.input_notification = Some(receiver); + notifications.insert(component_name.clone(), sender); } - trace!("node: {}, peers: {:?}", service_name, co.peer()); - for peer_name in co.peer() { + broadcasters.insert(component_name.clone(), tokio::sync::broadcast::channel(1)); + } + + // register nodes + for handle in self.known_components.values() { + let component = &handle.component; + for peer_name in component.peers() { let peer_name = peer_name.to_string(); - if !known_services.contains_key(&peer_name) { - let node_id = depgraph.add_node(service_id); - service_id += 1; + if !known_services.contains_left(&peer_name) { + let node_id = depgraph.add_node(peer_name.clone()); known_services.insert(peer_name.clone(), node_id); } - if let Some(peer) = self.data.get(&peer_name) { - if !peer.peer().contains(&service_name.as_str()) { - return Err(format!( - "Missmatch in peers: '{}' asked for '{}' to be a peer, reverse is not true", - service_name, peer_name - )); - } - let parent_id = *known_services.get(&service_name).unwrap(); - let peer_id = *known_services.get(&peer_name).unwrap(); - let merged_id = parent_id.min(peer_id); - translation.insert(parent_id, merged_id); - translation.insert(peer_id, merged_id); - } } - for required_service_name in co.requires() { + for required_service_name in component.requirements() { let required_service_name = required_service_name.to_string(); - if !known_services.contains_key(&required_service_name) { - let node_id = depgraph.add_node(service_id); - service_id += 1; + if !known_services.contains_left(&required_service_name) { + let node_id = depgraph.add_node(required_service_name.clone()); known_services.insert(required_service_name.clone(), node_id); } } - for notified_service_name in co.notify() { + for notified_service_name in component.notify() { let notified_service_name = notified_service_name.to_string(); - if !known_services.contains_key(¬ified_service_name) { - let node_id = depgraph.add_node(service_id); - service_id += 1; + if !known_services.contains_left(¬ified_service_name) { + let node_id = depgraph.add_node(notified_service_name.clone()); known_services.insert(notified_service_name.clone(), node_id); } } } - //If we reached here, it means all peers agree - - let mut requirements_graph = depgraph.clone(); - let mut notification_graph = depgraph.clone(); - - for (service_name, co) in self.data.iter() { - let node_id = *known_services.get(service_name).unwrap(); - let node_id = *translation.get(&node_id).unwrap_or(&node_id); - trace!("node: {}, requires: {:?}", service_name, co.requires()); - for required_service_name in co.requires() { - let required_service_id = *known_services.get(required_service_name).unwrap(); - let required_service_id = *translation - .get(&required_service_id) - .unwrap_or(&required_service_id); + + // register relationships + for (component_name, handle) in self.known_components.iter() { + let component = &handle.component; + let node_id = *known_services.get_by_left(component_name).unwrap(); + + trace!("node: {}, peers: {:?}", component_name, component.peers()); + for peer_name in component.peers() { + let peer_name = peer_name.to_string(); + if let Some(peer_handle) = self.known_components.get(&peer_name) { + let peer = &peer_handle.component; + trace!("peer: {}, peers: {:?}", peer_name, peer.peers()); + if !peer.peers().contains(&component_name.as_str()) { + return Err(format!( + "Missmatch in peers: '{}' asked for '{}' to be a peer, reverse is not true", + component_name, peer_name + )); + } + let peer_id = *known_services.get_by_left(&peer_name).unwrap(); + let mut has_rel = false; + for e in depgraph.edges_connecting(node_id, peer_id) { + if let RelationShip::Peer = e.weight() { + has_rel = true; + break; + } + } + if !has_rel { + depgraph.add_edge(node_id, peer_id, RelationShip::Peer); + depgraph.add_edge(peer_id, node_id, RelationShip::Peer); + } + } + } + trace!( + "node: {}, requires: {:?}", + component_name, + component.requirements() + ); + for required_service_name in component.requirements() { + let required_service_id = + *known_services.get_by_left(required_service_name).unwrap(); + //let required_service_id = *translation.get(&required_service_id).unwrap_or(&required_service_id); if node_id != required_service_id { - depgraph.add_edge(node_id, required_service_id, 1); + depgraph.add_edge(node_id, required_service_id, RelationShip::Requires); //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. - requirements_graph.add_edge(node_id, required_service_id, 1); } } - trace!("node: {}, notify: {:?}", service_name, co.notify()); - for notified_service_name in co.notify() { - let notified_service_id = *known_services.get(notified_service_name).unwrap(); - let notified_service_id = *translation - .get(¬ified_service_id) - .unwrap_or(¬ified_service_id); + trace!("node: {}, notify: {:?}", component_name, component.notify()); + for notified_service_name in component.notify() { + let notified_service_id = + *known_services.get_by_left(notified_service_name).unwrap(); + //let notified_service_id = *translation.get(¬ified_service_id).unwrap_or(¬ified_service_id); if node_id != notified_service_id { //there is no dep on the graph, the only worth of the notified service is it needs to exist //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. - notification_graph.add_edge(notified_service_id, node_id, 1); + depgraph.add_edge(node_id, notified_service_id, RelationShip::Notify); } } } - // Before anything find diff between keys of known_services vs hosted_services - let hosted_keys: HashSet = hosted_services.keys().cloned().collect(); - let known_keys: HashSet = known_services.keys().cloned().collect(); + //If we reached here, it means all peers agree. + + //Is there a difference between keys of known_services vs hosted_services. + let hosted_keys: HashSet = self.known_components.keys().cloned().collect(); + let known_keys: HashSet = depgraph.node_weights().cloned().collect(); trace!("hosted_keys: {:?}", hosted_keys); trace!("known_keys: {:?}", known_keys); if known_keys.difference(&hosted_keys).count() > 0 { @@ -207,16 +282,24 @@ impl ComponentManager { known_keys.difference(&hosted_keys) )); } + // no missing component - let invocation_order = get_invocation_order(&mut depgraph); - if depgraph.node_count() > 0 { + // check for cycles + let mut graph_copy = depgraph.clone(); + let invocation_order = get_invocation_order(&mut graph_copy, |e| matches!(e.weight(), RelationShip::Requires)); + if graph_copy.node_count() > 0 { return Err(format!( "Found a cyclic dependancy between {:?}", - depgraph.node_identifiers() + graph_copy.node_identifiers() )); } + // no cycle + trace!("services: {:?}", known_services); trace!("invocation_order: {:?}", invocation_order); + + // At this point, every relationship is sane, none missing, no cycle. We can now build the communication channels. + /* TODO: make use of: requirements graph => components subscribe to it. There should be at most one element in it, eaten at each step of the loop. @@ -225,9 +308,92 @@ impl ComponentManager { invocation order */ - unimplemented!( - "The algorithm to build and check dependancies between components is not implemented" - ) + //TODO: channels are part of each component handle, all that remains is insert them + /*for (node_id, _) in translation.iter() { + peers_channels.insert(node_id.clone(), tokio::sync::mpsc::channel(1000)); + }*/ + for node_id in depgraph.node_indices() { + let notify_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Notify)); + for rel in notify_rel { + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + if let Some(sender) = notifications.get(dst_component_name) { + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + trace!( + "{} wants to notify {}", + src_component_name, + dst_component_name + ); + trace!("source map: {:?}", src_handle.relations_to_ids); + let dst_relative_id = + *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + if let Some(src_component) = self.known_components.get_mut(src_component_name) { + src_component + .channels + .notify + .insert(dst_relative_id, sender.clone()); + } + } + } + let peer_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Peer)); + for rel in peer_rel { + // we shall overwrite the channels, but this is ok since we are not using them yet. + // TODO: if in the future there is dynamic loading, there shall be a need to dynamically rebuilt and thus get and reuse the existing channels. + let (local, remote) = { + let (sender_1, receiver_1) = tokio::sync::mpsc::channel(1000); + let (sender_2, receiver_2) = tokio::sync::mpsc::channel(1000); + ((sender_1, receiver_2), (sender_2, receiver_1)) + }; + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + src_handle.channels.peers.insert(dst_relative_id, local); + + let dst_handle = self.known_components.get_mut(dst_component_name).unwrap(); + let src_relative_id = *dst_handle.relations_to_ids.get(src_component_name).unwrap(); + dst_handle.channels.peers.insert(src_relative_id, remote); + } + + let requirement_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Requires)); + for rel in requirement_rel { + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + let (sender, _) = broadcasters.get(src_component_name).unwrap(); + src_handle + .channels + .requirements + .insert(dst_relative_id, sender.subscribe()); + } + } + + for (service_name, handle) in self.known_components.iter_mut() { + trace!( + "bind {} with, notified: {}, notify: {}, requirements: {}, peers: {}", + service_name, + handle.channels.input_notification.is_some(), + handle.channels.notify.len(), + handle.channels.requirements.len(), + handle.channels.peers.len(), + ); + trace!("Component ids: {:?}", handle.relations_to_ids); + handle.component.bind(std::mem::take(&mut handle.channels)); + } + + //unimplemented!("The algorithm to build and check dependancies between components is not implemented"); + self.broadcasters = broadcasters; + self.notifications = notifications; + Ok(()) } } @@ -240,16 +406,17 @@ impl Default for ComponentManager { impl ComponentHandle { fn broadcast(&mut self, data: ComponentDataExchange) { println!("{:?}", data); - unimplemented!("The broadcast of data is not done"); + unimplemented!("The broadcast of data is not implemented"); } } impl ComponentExecutor { - fn tick(&mut self, latest_time: f64) -> () { + fn tick(&mut self, latest_time: f64) { for handle in self.components.iter_mut() { let res = handle.component.tick(latest_time); handle.broadcast(res); } + unimplemented!("The component executor tick is not implemented"); } } @@ -271,7 +438,7 @@ mod test { my_graph.add_edge(a, d, 1); println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = crate::get_invocation_order(&mut my_graph); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); println!("nb nodes: {}", my_graph.node_count()); println!("invocation order: {:?}", invocation_order); assert!(my_graph.node_count() == 0); @@ -290,7 +457,7 @@ mod test { my_graph.add_edge(b, c, 1); println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = crate::get_invocation_order(&mut my_graph); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); println!("nb nodes: {}", my_graph.node_count()); println!("invocation order: {:?}", invocation_order); assert!(my_graph.node_count() == 2); @@ -309,7 +476,7 @@ mod test { my_graph.add_edge(b, c, 1); println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = crate::get_invocation_order(&mut my_graph); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); println!("nb nodes: {}", my_graph.node_count()); println!("invocation order: {:?}", invocation_order); assert!(my_graph.node_count() == 2); @@ -328,7 +495,7 @@ mod test { my_graph.add_edge(a, c, 1); println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = crate::get_invocation_order(&mut my_graph); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); println!("nb nodes: {}", my_graph.node_count()); println!("invocation order: {:?}", invocation_order); assert!(my_graph.node_count() == 0); @@ -348,7 +515,7 @@ mod test { my_graph.add_edge(a, c, 1); println!("nb nodes: {}", my_graph.node_count()); - let invocation_order = crate::get_invocation_order(&mut my_graph); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); println!("nb nodes: {}", my_graph.node_count()); println!("invocation order: {:?}", invocation_order); assert!(my_graph.node_count() == 2); diff --git a/crates/joko_component_models/Cargo.toml b/crates/joko_component_models/Cargo.toml index 257d73c..743a683 100644 --- a/crates/joko_component_models/Cargo.toml +++ b/crates/joko_component_models/Cargo.toml @@ -9,14 +9,13 @@ edition = "2021" default = ["messages_any"] messages_any = [] messages_downcast = [] -messages_bincode = [] +messages_bincode = ["dep:bincode"] [dependencies] -bincode = { workspace = true } +bincode = { workspace = true, optional = true } downcast-rs = "1.2.1" egui = { workspace = true } scopeguard = "1.2.0" -smol_str = { workspace = true } tokio = { workspace = true } serde = { workspace = true } diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs index c596ac5..7cdcc36 100644 --- a/crates/joko_component_models/src/lib.rs +++ b/crates/joko_component_models/src/lib.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - #[cfg(feature = "messages_any")] mod messages_any; #[cfg(feature = "messages_any")] @@ -11,69 +9,61 @@ mod messages_bincode; pub use messages_bincode::*; pub type PeerComponentChannel = ( - tokio::sync::mpsc::Receiver, tokio::sync::mpsc::Sender, + tokio::sync::mpsc::Receiver, ); -pub trait JokolayComponentDeps { +pub trait JokolayComponent { /** Names are external to traits and implementation. That way it is easy to change it without change in binary. In case of first class components, name is hardcoded. In case of plugins, name is part of a manifest and can be changed at will. */ + //TODO: fn watch(&self) -> Vec<&str> {} // elements in peer(), requires() and notify() are mutually exclusives - fn peer(&self) -> Vec<&str> { + fn peers(&self) -> Vec<&str> { //by default, no other plugin bound vec![] } - fn requires(&self) -> Vec<&str> { - //by default, no requirement + /// Shall eat a new value produced by the required components at each tick + /// By default, no requirement + fn requirements(&self) -> Vec<&str> { vec![] } fn notify(&self) -> Vec<&str> { //by default, no third party plugin vec![] } -} - -pub trait JokolayComponent { + fn accept_notifications(&self) -> bool { + false + } /* - This make sense only when components are very similar. It make no sense to ask for a uniform way to build components. - type T; - type E; - fn new( - root_path: &std::path::Path, - ) -> Result;*/ + TODO: + for global values that does not need a specific new value at each frame (such as configuration), watch over the values. + fn watch(&self) -> Vec<&str> + https://docs.rs/tokio/latest/tokio/sync/watch/index.html + */ + /// Drain every notifications sent by any other component fn flush_all_messages(&mut self); + fn tick(&mut self, latest_time: f64) -> ComponentDataExchange; - fn bind( - &mut self, - deps: HashMap>, - bound: HashMap, // Private channel only two bounded modules can use between each others. - input_notification: HashMap>, - notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ); //By default, there is no third party component, thus we can implement it as a noop - /* - TODO: there could be an optional trait: Chain. - If there is a strong connection between two elements, passing values by channels and copy could be inefficient, calling a function with arguments could be better => - it's almost a macro with an unset number of arguments and unknown types. - It could be possible on plugins, not other kind of components - */ + + /// when reasing the channels, the id of channels are set by their appearance order in "peers", then "requirements", then "notify" + fn bind(&mut self, channels: ComponentChannels); + /* + TODO: there could be an optional trait: Chain. + If there is a strong connection between two elements, passing values by channels and copy could be inefficient, calling a function with arguments could be better => + it's almost a macro with an unset number of arguments and unknown types. + It could be possible on plugins, not other kind of components + */ } -pub trait JokolayUIComponent -where - ComponentResult: Clone, -{ - fn flush_all_messages(&mut self); - //the only reason there is another Component trait is because of the egui_context - fn tick(&mut self, latest_time: f64, egui_context: &egui::Context) -> ComponentResult; - fn bind( - &mut self, - deps: HashMap>, - bound: HashMap, // Private channel only two bounded modules can use between each others. - input_notification: HashMap>, - notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ); //By default, there is no third party component, thus we can implement it as a noop +#[derive(Default)] +pub struct ComponentChannels { + pub requirements: + std::collections::HashMap>, + pub peers: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + pub input_notification: Option>, + pub notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. } diff --git a/crates/joko_component_models/src/messages_any.rs b/crates/joko_component_models/src/messages_any.rs index 7de4bb7..3a77214 100644 --- a/crates/joko_component_models/src/messages_any.rs +++ b/crates/joko_component_models/src/messages_any.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{any::TypeId, sync::Arc}; use serde::{Deserialize, Serialize}; @@ -8,21 +8,24 @@ pub fn default_data_exchange() -> ComponentDataExchange { Arc::new(Box::new(0)) } -pub fn to_data<'a, T>(value: T) -> ComponentDataExchange +pub fn to_data(value: T) -> ComponentDataExchange where T: Serialize + Clone + Send + Sync + 'static, { - Arc::new(Box::new(T::from(value))) + Arc::new(Box::new(value)) } pub fn from_data<'a, T>(value: ComponentDataExchange) -> T where T: Deserialize<'a> + Clone + Send + Sync + 'static, { - use downcast_rs::Downcast; - - let a = value.as_any(); - let d = a.downcast_ref::(); - let res = d.unwrap().to_owned(); - res + if let Some(d) = value.downcast_ref::() { + d.to_owned() + } else { + panic!( + "Bad routing of elements, expected {:?} {:?}", + TypeId::of::(), + TypeId::of::() + ); + } } diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index 7d21884..19c231c 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -11,11 +11,3 @@ glam = { workspace = true } scopeguard = "1.2.0" smol_str = { workspace = true } serde = { workspace = true } -mutually_exclusive_features = { workspace = true } - - -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 5a29d12..f7864f2 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,5 +1,3 @@ -mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); - use std::str::FromStr; use serde::{Deserialize, Serialize}; diff --git a/crates/joko_link_manager/Cargo.toml b/crates/joko_link_manager/Cargo.toml index 753da2d..f3131ad 100644 --- a/crates/joko_link_manager/Cargo.toml +++ b/crates/joko_link_manager/Cargo.toml @@ -6,12 +6,6 @@ edition = "2021" crate-type = ["cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - [dependencies] joko_core = { path = "../joko_core" } diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs index 2c38a9d..9853ac1 100644 --- a/crates/joko_link_manager/src/lib.rs +++ b/crates/joko_link_manager/src/lib.rs @@ -12,8 +12,7 @@ use std::vec; use enumflags2::BitFlags; use joko_component_models::{ - from_data, to_data, ComponentDataExchange, JokolayComponent, JokolayComponentDeps, - PeerComponentChannel, + from_data, to_data, ComponentChannels, ComponentDataExchange, JokolayComponent, }; use joko_core::serde_glam::{IVec2, UVec2, Vec3}; use joko_link_models::{ @@ -226,6 +225,10 @@ impl MumbleManager { impl JokolayComponent for MumbleManager { fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); while let Ok(msg) = channels.notification_receiver.try_recv() { @@ -237,34 +240,23 @@ impl JokolayComponent for MumbleManager { } fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let link = self._tick().unwrap_or(None); self.state.link = link.cloned(); to_data(self.state.clone()) } - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - mut bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - _input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - let (notification_receiver, _) = bound.remove(&0).unwrap(); + fn bind(&mut self, mut channels: ComponentChannels) { + let (_, notification_receiver) = channels.peers.remove(&0).unwrap(); let channels = MumbleChannels { notification_receiver, }; self.channels = Some(channels); } -} - -impl JokolayComponentDeps for MumbleManager { //default is enough - fn peer(&self) -> Vec<&str> { + fn peers(&self) -> Vec<&str> { if self.is_ui { vec!["back:mumble_link"] } else { diff --git a/crates/joko_link_models/Cargo.toml b/crates/joko_link_models/Cargo.toml index ade6983..3f53d8e 100644 --- a/crates/joko_link_models/Cargo.toml +++ b/crates/joko_link_models/Cargo.toml @@ -7,32 +7,16 @@ crate-type = ["cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - - [dependencies] -bincode = { workspace = true } joko_core = { path = "../joko_core" } -joko_component_models = { path = "../joko_component_models" } -widestring = { version = "1", default-features = false, features = ["std"] } num-derive = { version = "0", default-features = false } num-traits = { version = "0", default-features = false } enumflags2 = { workspace = true } -time = { workspace = true } miette = { workspace = true } -tracing = { workspace = true } serde = { workspace = true } glam = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -mutually_exclusive_features = { workspace = true } [target.'cfg(unix)'.dependencies] -x11rb = { version = "0.12", default-features = false, features = [] } [target.'cfg(windows)'.dependencies] windows = { version = "0.51.1", features = [ diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs index b939521..48d7a50 100644 --- a/crates/joko_link_models/src/lib.rs +++ b/crates/joko_link_models/src/lib.rs @@ -11,7 +11,6 @@ mod messages; mod mumble; -use joko_component_models::ComponentDataExchange; pub use messages::*; pub use mumble::*; use serde::{Deserialize, Serialize}; @@ -22,18 +21,3 @@ pub struct MumbleLinkResult { pub link: Option, pub ui_link: Option, } - -/* -impl From for ComponentDataExchange { - fn from(src: MumbleLinkResult) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> MumbleLinkResult { - bincode::deserialize(&self).unwrap() - } -} -*/ diff --git a/crates/joko_link_models/src/messages.rs b/crates/joko_link_models/src/messages.rs index a0e4501..f112cda 100644 --- a/crates/joko_link_models/src/messages.rs +++ b/crates/joko_link_models/src/messages.rs @@ -1,6 +1,3 @@ -mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); - -use joko_component_models::{to_data, ComponentDataExchange}; use serde::{Deserialize, Serialize}; use crate::MumbleLink; @@ -11,18 +8,3 @@ pub enum MessageToMumbleLinkBack { Autonomous, Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. } - -/* -impl From for ComponentDataExchange { - fn from(src: MessageToMumbleLinkBack) -> ComponentDataExchange { - to_data(src) - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> MessageToMumbleLinkBack { - bincode::deserialize(&self).unwrap() - } -} -*/ diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml index e1a2c4d..0243030 100644 --- a/crates/joko_package_manager/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -4,18 +4,10 @@ version = "0.2.1" edition = "2021" -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - - [dependencies] # jmf deps # for marker packs base64 = "0.21.2" -bincode = { workspace = true } bytemuck = { workspace = true } cap-std = { workspace = true } cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings @@ -50,7 +42,6 @@ uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serd xot = { version = "0.16.0" } zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) walkdir = "2.5.0" -mutually_exclusive_features = { workspace = true } diff --git a/crates/joko_package_manager/src/manager/pack/import.rs b/crates/joko_package_manager/src/manager/pack/import.rs index 0234a46..7c9a532 100644 --- a/crates/joko_package_manager/src/manager/pack/import.rs +++ b/crates/joko_package_manager/src/manager/pack/import.rs @@ -1,7 +1,8 @@ use joko_package_models::package::PackCore; +use serde::{Deserialize, Serialize}; use tracing::info; -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Deserialize)] pub enum ImportStatus { #[default] UnInitialized, @@ -9,7 +10,7 @@ pub enum ImportStatus { LoadingPack(std::path::PathBuf), WaitingLoading(std::path::PathBuf), PackDone(String, PackCore, bool), - PackError(miette::Report), + PackError(String), } pub fn import_pack_from_zip_file_path( diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index e3b4bd7..1ee29c2 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -799,12 +799,15 @@ impl LoadedPackTexture { } pub fn jokolay_to_editable_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { - let marker_manager_path = jokolay_to_marker_path(jokolay_path); - marker_manager_path.join(EDITABLE_PACKAGE_NAME) + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(EDITABLE_PACKAGE_NAME) } pub fn jokolay_to_extract_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { - jokolay_path.join(EXTRACT_DIRECTORY_NAME) + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(EXTRACT_DIRECTORY_NAME) } pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs index 9787ede..9e151f9 100644 --- a/crates/joko_package_manager/src/manager/package_data.rs +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -5,8 +5,8 @@ use std::{ use cap_std::fs_utf8::Dir; use joko_component_models::{ - default_data_exchange, from_data, to_data, ComponentDataExchange, JokolayComponent, - JokolayComponentDeps, + default_data_exchange, from_data, to_data, ComponentChannels, ComponentDataExchange, + JokolayComponent, }; use joko_package_models::package::PackageImportReport; @@ -485,6 +485,10 @@ impl PackageDataManager { } pub fn load_all(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); once::assert_has_not_been_called!("Early load must happen only once"); let channels = self.channels.as_mut().unwrap(); // Called only once at application start. @@ -521,6 +525,10 @@ impl PackageDataManager { impl JokolayComponent for PackageDataManager { fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); while let Ok(msg) = channels.notification_receiver.try_recv() { @@ -530,38 +538,36 @@ impl JokolayComponent for PackageDataManager { self.handle_message(msg); } } - fn bind( - &mut self, - mut deps: HashMap>, - mut bound: HashMap, // Private channel only two bounded modules can use between each others. - mut input_notification: HashMap>, - _notify: HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - let (_, front_end_notifier) = bound.remove(&0).unwrap(); + fn bind(&mut self, mut channels: ComponentChannels) { + let (front_end_notifier, _) = channels.peers.remove(&0).unwrap(); let channels = PackageDataChannels { - subscription_mumblelink: deps.remove(&0).unwrap(), + subscription_mumblelink: channels.requirements.remove(&1).unwrap(), front_end_notifier, - notification_receiver: input_notification.remove(&0).unwrap(), + notification_receiver: channels.input_notification.unwrap(), }; self.channels = Some(channels); } fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); let raw_mlr = channels.subscription_mumblelink.blocking_recv().unwrap(); let mumble_link_result: MumbleLinkResult = from_data(raw_mlr); self._tick(&mumble_link_result); default_data_exchange() } -} - -impl JokolayComponentDeps for PackageDataManager { fn notify(&self) -> Vec<&str> { vec![] } - fn peer(&self) -> Vec<&str> { + fn peers(&self) -> Vec<&str> { vec!["ui:jokolay_package_manager"] } - fn requires(&self) -> Vec<&str> { + fn requirements(&self) -> Vec<&str> { vec!["back:mumble_link"] } + fn accept_notifications(&self) -> bool { + true + } } diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs index 5a65f0a..bc29a4d 100644 --- a/crates/joko_package_manager/src/manager/package_ui.rs +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -8,12 +8,12 @@ use image::EncodableLayout; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use joko_render_models::messages::MessageToRenderer; +use serde::{Deserialize, Serialize}; use tracing::{info_span, trace}; use crate::message::MessageToPackageBack; use joko_component_models::{ - from_data, to_data, ComponentDataExchange, JokolayComponentDeps, JokolayUIComponent, - PeerComponentChannel, + from_data, to_data, ComponentChannels, ComponentDataExchange, JokolayComponent, }; use joko_core::{serde_glam::Vec3, RelativePath}; use joko_link_models::{MumbleChanges, MumbleLink, MumbleLinkResult}; @@ -25,7 +25,7 @@ use crate::manager::pack::loaded::{LoadedPackTexture, PackTasks}; use crate::message::MessageToPackageUI; //FIXME: there is an interest to merge the PackageUIManager and the render -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct PackageUISharedState { list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display first_load_done: bool, @@ -35,8 +35,6 @@ pub struct PackageUISharedState { struct PackageUIChannels { subscription_mumblelink: tokio::sync::broadcast::Receiver, - subscription_near_scene: tokio::sync::broadcast::Receiver, - notification_receiver: tokio::sync::mpsc::Receiver, back_end_notifier: tokio::sync::mpsc::Sender, @@ -51,6 +49,8 @@ pub struct PackageUIManager { reports: BTreeMap, tasks: PackTasks, + egui_context: Arc, + z_near: f32, currently_used_files: BTreeMap, all_files_activation_status: bool, // this consume a change of display event show_only_active: bool, @@ -64,7 +64,8 @@ pub struct PackageUIManager { } impl PackageUIManager { - pub fn new() -> Self { + pub fn new(egui_context: Arc, z_near: f32) -> Self { + //z_near is a constant, make it a https://docs.rs/tokio/latest/tokio/sync/watch/index.html if required to be dynamic let state = PackageUISharedState { list_of_textures_changed: false, first_load_done: false, @@ -78,6 +79,8 @@ impl PackageUIManager { default_marker_texture: None, default_trail_texture: None, + egui_context, + z_near, all_files_activation_status: false, show_only_active: true, currently_used_files: Default::default(), // UI copy to (de-)activate files @@ -114,8 +117,7 @@ impl PackageUIManager { } MessageToPackageUI::ImportFailure(message) => { tracing::trace!("Handling of MessageToPackageUI::ImportFailure"); - *self.state.import_status.lock().unwrap() = - ImportStatus::PackError(miette::Report::msg(message)); + *self.state.import_status.lock().unwrap() = ImportStatus::PackError(message); } MessageToPackageUI::LoadedPack(pack_texture, report) => { tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); @@ -315,7 +317,7 @@ impl PackageUIManager { *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); } else { *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); + ImportStatus::PackError("file chooser was cancelled".to_string()); } }); } @@ -696,8 +698,9 @@ impl PackageUIManager { } } -impl JokolayUIComponent for PackageUIManager { +impl JokolayComponent for PackageUIManager { fn flush_all_messages(&mut self) { + assert!(self.channels.is_some()); let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); while let Ok(msg) = channels.notification_receiver.try_recv() { @@ -708,7 +711,8 @@ impl JokolayUIComponent for PackageUIManager { } } - fn tick(&mut self, timestamp: f64, egui_context: &egui::Context) -> PackageUISharedState { + fn tick(&mut self, timestamp: f64) -> ComponentDataExchange { + assert!(self.channels.is_some()); let raw_link = { let channels = self.channels.as_mut().unwrap(); channels.subscription_mumblelink.blocking_recv().unwrap() @@ -720,7 +724,7 @@ impl JokolayUIComponent for PackageUIManager { { self.load_marker_texture( pack_uuid, - egui_context, + &Arc::clone(&self.egui_context), tex_path, marker_uuid, position, @@ -732,56 +736,40 @@ impl JokolayUIComponent for PackageUIManager { { self.load_trail_texture( pack_uuid, - egui_context, + &Arc::clone(&self.egui_context), tex_path, trail_uuid, common_attributes, ); } - let channels = self.channels.as_mut().unwrap(); - let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); - let z_near: f32 = from_data(raw_z_near); - let _ = self._tick(timestamp, link_result.link.as_ref().unwrap(), z_near); - self.state.clone() + //let channels = self.channels.as_mut().unwrap(); + //let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); + //let z_near: f32 = from_data(raw_z_near); + let _ = self._tick(timestamp, link_result.link.as_ref().unwrap(), self.z_near); + to_data(self.state.clone()) } - fn bind( - &mut self, - mut deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - mut bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - mut input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - mut notify: std::collections::HashMap< - u32, - tokio::sync::mpsc::Sender, - >, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - let (_, back_end_notifier) = bound.remove(&0).unwrap(); + fn bind(&mut self, mut channels: ComponentChannels) { + let (back_end_notifier, _) = channels.peers.remove(&0).unwrap(); let channels = PackageUIChannels { - subscription_mumblelink: deps.remove(&0).unwrap(), - subscription_near_scene: deps.remove(&1).unwrap(), - notification_receiver: input_notification.remove(&0).unwrap(), + subscription_mumblelink: channels.requirements.remove(&2).unwrap(), + notification_receiver: channels.input_notification.unwrap(), back_end_notifier, - renderer_notifier: notify.remove(&0).unwrap(), + renderer_notifier: channels.notify.remove(&1).unwrap(), }; self.channels = Some(channels); } -} - -impl JokolayComponentDeps for PackageUIManager { fn notify(&self) -> Vec<&str> { vec!["ui:jokolay_renderer"] } - fn peer(&self) -> Vec<&str> { + fn peers(&self) -> Vec<&str> { vec!["back:jokolay_package_manager"] } - fn requires(&self) -> Vec<&str> { - vec!["ui:mumble_link", "ui:jokolay_near_scene"] + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn accept_notifications(&self) -> bool { + true } } diff --git a/crates/joko_package_manager/src/message.rs b/crates/joko_package_manager/src/message.rs index eb7475e..90b0b77 100644 --- a/crates/joko_package_manager/src/message.rs +++ b/crates/joko_package_manager/src/message.rs @@ -1,8 +1,5 @@ -mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); - use std::collections::{BTreeMap, HashSet}; -use joko_component_models::ComponentDataExchange; use joko_package_models::{ attributes::CommonAttributes, package::{PackCore, PackageImportReport}, @@ -29,19 +26,6 @@ pub enum MessageToPackageUI { TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), } -/* -impl From for ComponentDataExchange { - fn from(src: MessageToPackageUI) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> MessageToPackageUI { - bincode::deserialize(&self).unwrap() - } -}*/ #[derive(Clone, Serialize, Deserialize)] pub enum MessageToPackageBack { @@ -55,17 +39,3 @@ pub enum MessageToPackageBack { ReloadPack, SavePack(String, PackCore), } -/* -impl From for ComponentDataExchange { - fn from(src: MessageToPackageBack) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> MessageToPackageBack { - bincode::deserialize(&self).unwrap() - } -} -*/ diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml index 55bb78b..24940a6 100644 --- a/crates/joko_package_models/Cargo.toml +++ b/crates/joko_package_models/Cargo.toml @@ -4,19 +4,11 @@ version = "0.2.1" edition = "2021" -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - - [dependencies] # jmf deps # for marker packs base64 = "0.21.2" -bincode = { workspace = true } -bimap = { version = "0.6.3", features = ["serde"] } +bimap = { workspace = true } bytemuck = { workspace = true } data-encoding = "2.4.0" enumflags2 = { workspace = true } @@ -24,7 +16,6 @@ glam = { workspace = true } indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip itertools = { workspace = true } joko_core = { path = "../joko_core" } -joko_component_models = { path = "../joko_component_models" } jokoapi = { path = "../jokoapi" } miette = { workspace = true } ordered_hash_map = { workspace = true } @@ -37,7 +28,6 @@ tracing = { workspace = true } url = { workspace = true } uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } xot = { version = "0.16.0" } -mutually_exclusive_features = { workspace = true } [dev-dependencies] diff --git a/crates/joko_plugin_manager/Cargo.toml b/crates/joko_plugin_manager/Cargo.toml index 41b68d8..9bc73ee 100644 --- a/crates/joko_plugin_manager/Cargo.toml +++ b/crates/joko_plugin_manager/Cargo.toml @@ -6,15 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - - [dependencies] joko_component_models = { path = "../joko_component_models" } scopeguard = "1.2.0" -smol_str = { workspace = true } tokio = { workspace = true } diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs index 018adf8..2da8b3f 100644 --- a/crates/joko_plugin_manager/src/lib.rs +++ b/crates/joko_plugin_manager/src/lib.rs @@ -1,6 +1,5 @@ use joko_component_models::{ - default_data_exchange, ComponentDataExchange, JokolayComponent, JokolayComponentDeps, - PeerComponentChannel, + default_data_exchange, ComponentChannels, ComponentDataExchange, JokolayComponent, }; pub struct JokolayPlugin {} @@ -12,23 +11,8 @@ impl JokolayComponent for JokolayPlugin { fn tick(&mut self, _timestamp: f64) -> ComponentDataExchange { default_data_exchange() } - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - _input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { - } -} -impl JokolayComponentDeps for JokolayPlugin { - fn requires(&self) -> Vec<&str> { + fn bind(&mut self, _channels: ComponentChannels) {} + fn requirements(&self) -> Vec<&str> { vec!["back:mumble_link"] } } diff --git a/crates/joko_render_manager/Cargo.toml b/crates/joko_render_manager/Cargo.toml index 01181c0..93e2463 100644 --- a/crates/joko_render_manager/Cargo.toml +++ b/crates/joko_render_manager/Cargo.toml @@ -8,15 +8,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - - [dependencies] -bincode = { workspace = true } bytemuck = { workspace = true } glam = { workspace = true, features = ["bytemuck"] } tracing = { workspace = true } diff --git a/crates/joko_render_manager/src/billboard.rs b/crates/joko_render_manager/src/billboard.rs index 96ee03a..f64a8f9 100644 --- a/crates/joko_render_manager/src/billboard.rs +++ b/crates/joko_render_manager/src/billboard.rs @@ -45,6 +45,7 @@ impl BillBoardRenderer { let marker_vertex_buffer = create_buffer(gl); let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); + gl_error!(gl); Self { markers: Vec::new(), diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs index 0bf18ba..4304000 100644 --- a/crates/joko_render_manager/src/renderer.rs +++ b/crates/joko_render_manager/src/renderer.rs @@ -14,10 +14,9 @@ use egui_window_glfw_passthrough::GlfwBackend; use glam::Mat4; use joko_component_models::default_data_exchange; use joko_component_models::from_data; +use joko_component_models::ComponentChannels; use joko_component_models::ComponentDataExchange; use joko_component_models::JokolayComponent; -use joko_component_models::JokolayComponentDeps; -use joko_component_models::PeerComponentChannel; use joko_link_models::MumbleLink; use joko_link_models::UIState; use joko_render_models::messages::MessageToRenderer; @@ -41,13 +40,17 @@ pub struct JokoRenderer { } impl JokoRenderer { - pub fn new(glfw_backend: &mut GlfwBackend) -> Self { - let glfw = glfw_backend.glfw.clone(); + pub fn new(glfw_backend: &GlfwBackend) -> Self { + /* + FIXME: Box + JokoRenderer => segfault when panic + Arc vs Box: no change + */ + //let glfw = glfw_backend.glfw.clone(); let backend = ThreeDBackend::new( ThreeDConfig { glow_config: Default::default(), }, - |s| glfw.get_proc_address_raw(s), + |s| glfw_backend.glfw.get_proc_address_raw(s), //glfw_backend.window.raw_window_handle(), glfw_backend.framebuffer_size_physical, ); @@ -246,27 +249,21 @@ impl JokoRenderer { } } -impl JokolayComponentDeps for JokoRenderer {} impl JokolayComponent for JokoRenderer { - fn bind( - &mut self, - _deps: std::collections::HashMap< - u32, - tokio::sync::broadcast::Receiver, - >, - _bound: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - mut input_notification: std::collections::HashMap< - u32, - tokio::sync::mpsc::Receiver, - >, - _notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. - ) { + fn bind(&mut self, channels: ComponentChannels) { let channels = JokoRendererChannels { - notification_receiver: input_notification.remove(&0).unwrap(), + notification_receiver: channels.input_notification.unwrap(), }; self.channels = Some(channels); } + fn accept_notifications(&self) -> bool { + true + } fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); //two steps reading due to self mutability required by channel @@ -279,6 +276,10 @@ impl JokolayComponent for JokoRenderer { } } fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let link: Option<&MumbleLink> = None; if let Some(link) = link { //x positive => east diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml index c2d0cd5..d94e66c 100644 --- a/crates/joko_render_models/Cargo.toml +++ b/crates/joko_render_models/Cargo.toml @@ -7,21 +7,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["messages_any"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] - [dependencies] -bincode = { workspace = true } bytemuck = { workspace = true } glam = { workspace = true, features = ["bytemuck"] } serde = { workspace = true } joko_core = { path = "../joko_core" } -joko_component_models = { path = "../joko_component_models" } -mutually_exclusive_features = { workspace = true } diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs index d6a7ec8..3c34a0a 100644 --- a/crates/joko_render_models/src/messages.rs +++ b/crates/joko_render_models/src/messages.rs @@ -1,10 +1,3 @@ -mutually_exclusive_features::exactly_one_of!( - "messages_any", - "messages_bincode", - "messages_downcast" -); - -use joko_component_models::ComponentDataExchange; use serde::{Deserialize, Serialize}; use crate::{marker::MarkerObject, trail::TrailObject}; @@ -18,18 +11,3 @@ pub enum MessageToRenderer { RenderSwapChain, // The list of elements to display was changed TrailObject(Box), } - -/* -impl From for ComponentDataExchange { - fn from(src: MessageToRenderer) -> ComponentDataExchange { - bincode::serialize(&src).unwrap() //shall crash if wrong serialization of messages - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComponentDataExchange { - fn into(self) -> MessageToRenderer { - bincode::deserialize(&self).unwrap() - } -} -*/ diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index 9aca08d..d65bc18 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -10,12 +10,9 @@ path = "src/main.rs" [features] -default = ["messages_any"] # will not work because wayland won't allow us to get global cursor position wayland = ["egui_window_glfw_passthrough/wayland"] -messages_any = [] -messages_downcast = [] -messages_bincode = [] + [dependencies] @@ -40,7 +37,6 @@ serde_json = { workspace = true } tokio = { workspace = true } indexmap = { workspace = true } ringbuffer = { workspace = true } -mutually_exclusive_features = { workspace = true } rayon = { workspace = true } diff --git a/crates/jokolay/src/app/messages.rs b/crates/jokolay/src/app/messages.rs index 6b0af2c..5d14d68 100644 --- a/crates/jokolay/src/app/messages.rs +++ b/crates/jokolay/src/app/messages.rs @@ -1,4 +1,3 @@ -mutually_exclusive_features::exactly_one_of!("messages_any", "messages_bincode"); pub enum MessageToApplicationBack { SaveUIConfiguration(String), } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index a3dc90d..11815c5 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -16,7 +16,7 @@ use crate::app::mumble::mumble_gui; use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use init::{get_jokolay_dir, get_jokolay_path}; use joko_component_manager::ComponentManager; -use joko_component_models::{from_data, JokolayComponent, JokolayUIComponent}; +use joko_component_models::{from_data, JokolayComponent}; use joko_package_manager::{PackageDataManager, PackageUIManager}; use joko_link_manager::MumbleManager; @@ -57,7 +57,7 @@ struct JokolayGui { ui_configuration: ui_parameters::JokolayUIConfiguration, menu_panel: MenuPanel, joko_renderer: JokoRenderer, - egui_context: egui::Context, + egui_context: Arc, glfw_backend: GlfwBackend, theme_manager: ThemeManager, mumble_manager: MumbleManager, @@ -86,21 +86,21 @@ impl Jokolay { MumbleManager::new("MumbleLink", true).wrap_err("failed to create mumble manager")?; let dummy_plugin = Box::new(JokolayPlugin {}); - component_manager.register( + let _ = component_manager.register( "ui:mumble_link", Box::new( MumbleManager::new("MumbleLink", true) .wrap_err("failed to create mumble manager")?, ), ); - component_manager.register( + let _ = component_manager.register( "back:mumble_link", Box::new( MumbleManager::new("MumbleLink", false) .wrap_err("failed to create mumble manager")?, ), ); - component_manager.register("dummy_plugin", dummy_plugin); + let _ = component_manager.register("dummy_plugin", dummy_plugin); /* components can be migrated to plugins @@ -121,7 +121,8 @@ impl Jokolay { ... */ - component_manager.register( + let egui_context = Arc::new(egui::Context::default()); + let _ = component_manager.register( "back:jokolay_package_manager", Box::new(PackageDataManager::new( Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay @@ -136,7 +137,6 @@ impl Jokolay { let mut theme_manager = ThemeManager::new(Arc::clone(&root_dir)).wrap_err("failed to create theme manager")?; - let egui_context = egui::Context::default(); theme_manager.init_egui(&egui_context); let mut glfw_backend = GlfwBackend::new(GlfwConfig { glfw_callback: Box::new(|glfw_context| { @@ -167,20 +167,24 @@ impl Jokolay { let maximal_window_width = video_mode.unwrap().width; let maximal_window_height = video_mode.unwrap().height; - component_manager.register( + let _ = component_manager.register( "ui:jokolay_package_manager", - Box::new(PackageUIManager::new()), + Box::new(PackageUIManager::new( + Arc::clone(&egui_context), + JokoRenderer::get_z_near(), + )), ); - let mut package_ui_manager = PackageUIManager::new(); + let mut package_ui_manager = + PackageUIManager::new(Arc::clone(&egui_context), JokoRenderer::get_z_near()); glfw_backend.window.set_floating(true); glfw_backend.window.set_decorated(false); - component_manager.register( + let joko_renderer = JokoRenderer::new(&glfw_backend); + let _ = component_manager.register( "ui:jokolay_renderer", - Box::new(JokoRenderer::new(&mut glfw_backend)), + Box::new(JokoRenderer::new(&glfw_backend)), ); - let joko_renderer = JokoRenderer::new(&mut glfw_backend); let editable_path = jokolay_to_editable_path(&root_path) .to_str() @@ -372,7 +376,7 @@ impl Jokolay { } = &mut gui; let latest_time = glfw_backend.glfw.get_time(); - let etx = egui_context.clone(); + let etx = Arc::clone(&egui_context); /* if etx.input(|i| { @@ -438,7 +442,8 @@ impl Jokolay { let _ = u2mb_sender.send(MessageToMumbleLinkBack::Value(local_state.link.clone())); } else { let is_mumble_alive = mumble_manager.is_alive(); - let res: MumbleLinkResult = from_data(mumble_manager.tick(latest_time)); + let data = mumble_manager.tick(latest_time); + let res: MumbleLinkResult = from_data(data); match &res.link { Some(link) => { if link.changes.contains(MumbleChanges::WindowPosition) @@ -478,7 +483,7 @@ impl Jokolay { .window .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); } - package_manager.tick(latest_time, egui_context); + package_manager.tick(latest_time); local_state.window_changed = false; } From 7153d0ebea243d2ec90eda8aebbffba6c079fc1f Mon Sep 17 00:00:00 2001 From: moi Date: Sun, 5 May 2024 15:16:21 +0200 Subject: [PATCH 49/54] working interaction between components (few bugs to tackle, see TODO & FIXME) --- Cargo.lock | 40 +- Cargo.toml | 5 + crates/joko_component_manager/Cargo.toml | 4 +- crates/joko_component_manager/src/lib.rs | 203 +++-- crates/joko_component_models/Cargo.toml | 4 +- crates/joko_component_models/src/lib.rs | 18 +- .../joko_component_models/src/messages_any.rs | 52 +- .../src/messages_bincode.rs | 40 +- crates/joko_link_manager/Cargo.toml | 1 + crates/joko_link_manager/src/lib.rs | 23 +- crates/joko_link_manager/src/win/dll.rs | 2 +- crates/joko_link_manager/src/win/mod.rs | 3 +- crates/joko_link_ui_manager/Cargo.toml | 23 + crates/joko_link_ui_manager/README.md | 58 ++ crates/joko_link_ui_manager/src/lib.rs | 370 +++++++++ crates/joko_package_manager/Cargo.toml | 3 +- .../src/io/deserialize.rs | 58 +- .../joko_package_manager/src/io/serialize.rs | 66 +- .../src/manager/pack/category_selection.rs | 6 +- .../src/manager/pack/import.rs | 1 + .../src/manager/pack/loaded.rs | 222 +++--- .../src/manager/package_data.rs | 118 ++- .../src/manager/package_ui.rs | 480 +++++------ crates/joko_plugin_manager/Cargo.toml | 1 - crates/joko_plugin_manager/src/lib.rs | 9 +- crates/joko_render_manager/src/renderer.rs | 123 ++- crates/joko_ui_models/Cargo.toml | 11 + crates/joko_ui_models/README.md | 58 ++ crates/joko_ui_models/src/lib.rs | 13 + crates/jokolay/Cargo.toml | 2 + crates/jokolay/src/app/menu.rs | 290 +++++++ crates/jokolay/src/app/messages.rs | 3 + crates/jokolay/src/app/mod.rs | 744 +++--------------- crates/jokolay/src/app/ui_parameters.rs | 179 ++++- crates/jokolay/src/app/window.rs | 147 ++++ crates/jokolay/src/manager/theme/mod.rs | 36 +- 36 files changed, 2214 insertions(+), 1202 deletions(-) create mode 100644 crates/joko_link_ui_manager/Cargo.toml create mode 100644 crates/joko_link_ui_manager/README.md create mode 100644 crates/joko_link_ui_manager/src/lib.rs create mode 100644 crates/joko_ui_models/Cargo.toml create mode 100644 crates/joko_ui_models/README.md create mode 100644 crates/joko_ui_models/src/lib.rs create mode 100644 crates/jokolay/src/app/menu.rs create mode 100644 crates/jokolay/src/app/window.rs diff --git a/Cargo.lock b/Cargo.lock index 499bcbb..056331a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,12 +785,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "ecolor" version = "0.26.2" @@ -1452,10 +1446,8 @@ version = "0.2.1" dependencies = [ "bimap", "bincode", - "egui", "joko_component_models", "petgraph", - "scopeguard", "tokio", "tracing", ] @@ -1465,9 +1457,6 @@ name = "joko_component_models" version = "0.2.1" dependencies = [ "bincode", - "downcast-rs", - "egui", - "scopeguard", "serde", "tokio", ] @@ -1531,6 +1520,24 @@ dependencies = [ "windows", ] +[[package]] +name = "joko_link_ui_manager" +version = "0.2.1" +dependencies = [ + "egui", + "enumflags2", + "glam", + "joko_component_models", + "joko_core", + "joko_link_models", + "joko_ui_models", + "miette", + "num-derive", + "num-traits", + "serde", + "tokio", +] + [[package]] name = "joko_package_manager" version = "0.2.1" @@ -1552,6 +1559,7 @@ dependencies = [ "joko_link_models", "joko_package_models", "joko_render_models", + "joko_ui_models", "jokoapi", "miette", "once", @@ -1610,7 +1618,6 @@ name = "joko_plugin_manager" version = "0.2.1" dependencies = [ "joko_component_models", - "scopeguard", "tokio", ] @@ -1640,6 +1647,13 @@ dependencies = [ "serde", ] +[[package]] +name = "joko_ui_models" +version = "0.2.1" +dependencies = [ + "egui", +] + [[package]] name = "jokoapi" version = "0.2.1" @@ -1668,9 +1682,11 @@ dependencies = [ "joko_component_models", "joko_link_manager", "joko_link_models", + "joko_link_ui_manager", "joko_package_manager", "joko_plugin_manager", "joko_render_manager", + "joko_ui_models", "miette", "rayon", "rfd", diff --git a/Cargo.toml b/Cargo.toml index d00343f..f78b23a 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,12 @@ members = [ "crates/joko_package_manager", "crates/joko_package_models", "crates/joko_link_manager", + "crates/joko_link_ui_manager", "crates/joko_link_models", "crates/jokoapi", "crates/jokolay", "crates/joko_core", + "crates/joko_ui_models", "crates/joko_component_manager", "crates/joko_component_models", "crates/joko_plugin_manager", @@ -34,6 +36,7 @@ itertools = { version = "*" } miette = { version = "*", features = ["fancy"] } ordered_hash_map = { version = "*", features= ["serde"] } paste = { version = "*" } +once = "0.3.4" rayon = { version = "*" } rfd = { version = "*" } ringbuffer = { version = "0.14" } @@ -49,6 +52,8 @@ ureq = { version = "*" } url = { version = "*", features = ["serde"] } uuid = { version = "*" } mutually_exclusive_features = "0.1.0" +ractor = "0.9.8" + #https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml index a276f1a..2bbf132 100644 --- a/crates/joko_component_manager/Cargo.toml +++ b/crates/joko_component_manager/Cargo.toml @@ -14,9 +14,7 @@ messages_bincode = ["dep:bincode"] [dependencies] bimap = { workspace = true } bincode = { workspace = true, optional = true } -egui = { workspace = true } -scopeguard = "1.2.0" tokio = { workspace = true } joko_component_models = { path = "../joko_component_models" } petgraph = "0.6.4" -tracing.workspace = true +tracing = {workspace = true} diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index 9eec99f..2163cec 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -1,44 +1,63 @@ +use core::fmt; use std::{ collections::{HashMap, HashSet}, hash::Hash, + sync::{Arc, RwLock}, }; -use joko_component_models::{ComponentChannels, ComponentDataExchange, JokolayComponent}; +use joko_component_models::{Component, ComponentChannels, ComponentMessage, ComponentResult}; use petgraph::{ csr::IndexType, + dot::Dot, graph::NodeIndex, stable_graph::{EdgeReference, StableDiGraph}, visit::{EdgeRef, IntoNodeIdentifiers}, }; -use tracing::trace; +use tracing::{info_span, trace}; type BroadcastChannels = ( - tokio::sync::broadcast::Sender, - tokio::sync::broadcast::Receiver, + tokio::sync::broadcast::Sender, + tokio::sync::broadcast::Receiver, ); pub struct ComponentManager { //TODO: make it a component too ? known_components: HashMap, broadcasters: HashMap, //a receiver is kept idle in order to not close the channels. https://docs.rs/tokio/latest/tokio/sync/broadcast/#closing - notifications: HashMap>, + notifications: HashMap>, + invocation_order: Vec, } struct ComponentHandle { name: String, - component: Box, + component: Arc>, channels: ComponentChannels, relations_to_ids: HashMap, } + pub struct ComponentExecutor { + world: String, + broadcasters: HashMap>, components: Vec, //FIXME: how to type erase result ? + has_been_initialized: bool, } -#[derive(Clone)] +#[derive(Clone, Debug)] enum RelationShip { Requires, Peer, Notify, } + +impl fmt::Display for RelationShip { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + RelationShip::Requires => write!(f, "Requires"), + RelationShip::Peer => write!(f, "Peer"), + RelationShip::Notify => write!(f, "Notify"), + } + } +} + fn get_invocation_order( graph: &mut StableDiGraph, filter: F, @@ -96,39 +115,43 @@ impl ComponentManager { known_components: Default::default(), broadcasters: Default::default(), notifications: Default::default(), + invocation_order: Default::default(), } } /// Register a component. /// On its relationship, each component reference (names) shall be assigned an id. - /// That id is 0 based and goes in following order: peers, notify, requirements + /// That id is 0 based and goes in following order: peers, requirements, notify /// A component, when binding must retrieve the with the proper id. pub fn register( &mut self, component_name: &str, - component: Box, + component: Arc>, ) -> Result<(), String> { - if !has_unique_elements( - component + let mut relations_to_ids: HashMap = Default::default(); + { + let component = component.as_ref().read().unwrap(); + if !has_unique_elements( + component + .peers() + .iter() + .chain(component.requirements().iter()) + .chain(component.notify().iter()), + ) { + return Err(format!( + "Service {} has duplicate elements. Each name can only appear at one place", + component_name + )); + } + for (idx, name) in component .peers() .iter() + .chain(component.requirements().iter()) .chain(component.notify().iter()) - .chain(component.requirements().iter()), - ) { - return Err(format!( - "Service {} has duplicate elements. Each name can only appear at one place", - component_name - )); - } - let mut relations_to_ids: HashMap = Default::default(); - for (idx, name) in component - .peers() - .iter() - .chain(component.notify().iter()) - .chain(component.requirements().iter()) - .enumerate() - { - relations_to_ids.insert(name.to_string(), idx); + .enumerate() + { + relations_to_ids.insert(name.to_string(), idx); + } } let handle = ComponentHandle { @@ -143,15 +166,37 @@ impl ComponentManager { Ok(()) } - pub fn executor(&self, world: &str) -> ComponentExecutor { + pub fn executor(&mut self, world: &str) -> ComponentExecutor { /* TODO: extract the list of components of this world bind them insert them into the executor */ + let mut component_names: Vec = Default::default(); + for name in self.invocation_order.iter() { + if name.starts_with(world) { + component_names.push(name.clone()); + } + } + trace!( + "executor of world: {} shall be built from: {:?}", + world, + component_names + ); + let mut components: Vec = Default::default(); + let mut broadcasters: HashMap> = + Default::default(); + for name in component_names { + components.push(self.known_components.remove(&name).unwrap()); + let channels = self.broadcasters.get(&name).unwrap(); + broadcasters.insert(name.clone(), channels.0.clone()); + } ComponentExecutor { - components: Default::default(), + world: world.to_owned(), + components, + broadcasters, + has_been_initialized: false, } } @@ -166,7 +211,7 @@ impl ComponentManager { type G = petgraph::stable_graph::StableDiGraph; //TODO: those are temporary channels, one should work between existing and new channels => we need to save the work of a previous build - let mut notifications: HashMap> = + let mut notifications: HashMap> = Default::default(); let mut broadcasters: HashMap = Default::default(); @@ -175,9 +220,10 @@ impl ComponentManager { // initialize the basic channels for (component_name, handle) in self.known_components.iter_mut() { + let component = handle.component.read().unwrap(); let node_id = depgraph.add_node(component_name.clone()); known_services.insert(component_name.clone(), node_id); - if handle.component.accept_notifications() { + if component.accept_notifications() { let (sender, receiver) = tokio::sync::mpsc::channel(1000); handle.channels.input_notification = Some(receiver); notifications.insert(component_name.clone(), sender); @@ -187,7 +233,7 @@ impl ComponentManager { // register nodes for handle in self.known_components.values() { - let component = &handle.component; + let component = handle.component.read().unwrap(); for peer_name in component.peers() { let peer_name = peer_name.to_string(); if !known_services.contains_left(&peer_name) { @@ -213,14 +259,14 @@ impl ComponentManager { // register relationships for (component_name, handle) in self.known_components.iter() { - let component = &handle.component; + let component = handle.component.read().unwrap(); let node_id = *known_services.get_by_left(component_name).unwrap(); trace!("node: {}, peers: {:?}", component_name, component.peers()); for peer_name in component.peers() { let peer_name = peer_name.to_string(); if let Some(peer_handle) = self.known_components.get(&peer_name) { - let peer = &peer_handle.component; + let peer = &peer_handle.component.read().unwrap(); trace!("peer: {}, peers: {:?}", peer_name, peer.peers()); if !peer.peers().contains(&component_name.as_str()) { return Err(format!( @@ -270,6 +316,9 @@ impl ComponentManager { } //If we reached here, it means all peers agree. + //println!("{}", Dot::with_config(&depgraph, &[Config::EdgeNoLabel])); + println!("{}", Dot::with_config(&depgraph, &[])); + //Is there a difference between keys of known_services vs hosted_services. let hosted_keys: HashSet = self.known_components.keys().cloned().collect(); let known_keys: HashSet = depgraph.node_weights().cloned().collect(); @@ -286,7 +335,9 @@ impl ComponentManager { // check for cycles let mut graph_copy = depgraph.clone(); - let invocation_order = get_invocation_order(&mut graph_copy, |e| matches!(e.weight(), RelationShip::Requires)); + let invocation_order = get_invocation_order(&mut graph_copy, |e| { + matches!(e.weight(), RelationShip::Requires) + }); if graph_copy.node_count() > 0 { return Err(format!( "Found a cyclic dependancy between {:?}", @@ -313,7 +364,9 @@ impl ComponentManager { peers_channels.insert(node_id.clone(), tokio::sync::mpsc::channel(1000)); }*/ for node_id in depgraph.node_indices() { - let notify_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Notify)); + let notify_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Notify)); for rel in notify_rel { let dst_node_id = rel.target(); let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); @@ -337,7 +390,9 @@ impl ComponentManager { } } } - let peer_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Peer)); + let peer_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Peer)); for rel in peer_rel { // we shall overwrite the channels, but this is ok since we are not using them yet. // TODO: if in the future there is dynamic loading, there shall be a need to dynamically rebuilt and thus get and reuse the existing channels. @@ -350,6 +405,7 @@ impl ComponentManager { let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); let dst_node_id = rel.target(); let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + trace!("{} is a peer of {}", src_component_name, dst_component_name); let src_handle = self.known_components.get_mut(src_component_name).unwrap(); let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); @@ -360,7 +416,9 @@ impl ComponentManager { dst_handle.channels.peers.insert(src_relative_id, remote); } - let requirement_rel = depgraph.edges(node_id).filter(|e| matches!(e.weight(), RelationShip::Requires)); + let requirement_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Requires)); for rel in requirement_rel { let src_node_id = rel.source(); let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); @@ -369,15 +427,31 @@ impl ComponentManager { let src_handle = self.known_components.get_mut(src_component_name).unwrap(); let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); - let (sender, _) = broadcasters.get(src_component_name).unwrap(); + let (sender, _) = broadcasters.get(dst_component_name).unwrap(); + trace!( + "{} requires a value from {}", + src_component_name, + dst_component_name + ); + trace!( + "build broadcast of {}. Before subscribe: {}", + dst_component_name, + sender.receiver_count() + ); src_handle .channels .requirements .insert(dst_relative_id, sender.subscribe()); + trace!( + "build broadcast of {}. After subscribe: {}", + dst_component_name, + sender.receiver_count() + ); } } for (service_name, handle) in self.known_components.iter_mut() { + let mut component = handle.component.write().unwrap(); trace!( "bind {} with, notified: {}, notify: {}, requirements: {}, peers: {}", service_name, @@ -387,12 +461,16 @@ impl ComponentManager { handle.channels.peers.len(), ); trace!("Component ids: {:?}", handle.relations_to_ids); - handle.component.bind(std::mem::take(&mut handle.channels)); + component.bind(std::mem::take(&mut handle.channels)); } //unimplemented!("The algorithm to build and check dependancies between components is not implemented"); self.broadcasters = broadcasters; self.notifications = notifications; + self.invocation_order = invocation_order + .iter() + .map(|node_id| known_services.get_by_right(node_id).unwrap().to_string()) + .collect(); Ok(()) } } @@ -403,20 +481,43 @@ impl Default for ComponentManager { } } -impl ComponentHandle { - fn broadcast(&mut self, data: ComponentDataExchange) { - println!("{:?}", data); - unimplemented!("The broadcast of data is not implemented"); - } -} - impl ComponentExecutor { - fn tick(&mut self, latest_time: f64) { + pub fn init(&mut self) { + assert!( + !self.has_been_initialized, + "An executor can only initialize once the components" + ); + trace!( + "ComponentExecutor::init() {} {}", + self.world, + self.components.len() + ); + for handle in self.components.iter_mut() { + let mut component = handle.component.write().unwrap(); + component.init(); + } + self.has_been_initialized = true; + //unimplemented!("The component executor init is not implemented"); + } + pub fn tick(&mut self, latest_time: f64) { + let span_guard = info_span!("ComponentExecutor::tick()", self.world).entered(); + //trace!("start {}", latest_time); for handle in self.components.iter_mut() { - let res = handle.component.tick(latest_time); - handle.broadcast(res); + let mut component = handle.component.write().unwrap(); + //trace!("flush_all_messages of {}", handle.name); + component.flush_all_messages(); + + //trace!("tick for {}", handle.name); + let res = component.tick(latest_time); + + //trace!("broadcast result for {}", handle.name); + let b = self.broadcasters.get_mut(&handle.name).unwrap(); + //trace!("broadcast size for {} before {}, {}", handle.name, b.len(), b.receiver_count()); + let _ = b.send(res); + //trace!("broadcast size for {} after {}", handle.name, b.len()); } - unimplemented!("The component executor tick is not implemented"); + //trace!("end"); + drop(span_guard); } } diff --git a/crates/joko_component_models/Cargo.toml b/crates/joko_component_models/Cargo.toml index 743a683..2a7b830 100644 --- a/crates/joko_component_models/Cargo.toml +++ b/crates/joko_component_models/Cargo.toml @@ -14,8 +14,6 @@ messages_bincode = ["dep:bincode"] [dependencies] bincode = { workspace = true, optional = true } -downcast-rs = "1.2.1" -egui = { workspace = true } -scopeguard = "1.2.0" +#downcast-rs = "1.2.1" # TODO: implement messages_downcast feature tokio = { workspace = true } serde = { workspace = true } diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs index 7cdcc36..8ec3d9a 100644 --- a/crates/joko_component_models/src/lib.rs +++ b/crates/joko_component_models/src/lib.rs @@ -9,11 +9,11 @@ mod messages_bincode; pub use messages_bincode::*; pub type PeerComponentChannel = ( - tokio::sync::mpsc::Sender, - tokio::sync::mpsc::Receiver, + tokio::sync::mpsc::Sender, + tokio::sync::mpsc::Receiver, ); -pub trait JokolayComponent { +pub trait Component: Send + Sync { /** Names are external to traits and implementation. That way it is easy to change it without change in binary. In case of first class components, name is hardcoded. @@ -44,10 +44,13 @@ pub trait JokolayComponent { https://docs.rs/tokio/latest/tokio/sync/watch/index.html */ + /// called once after building relationships + fn init(&mut self); + /// Drain every notifications sent by any other component fn flush_all_messages(&mut self); - fn tick(&mut self, latest_time: f64) -> ComponentDataExchange; + fn tick(&mut self, latest_time: f64) -> ComponentResult; /// when reasing the channels, the id of channels are set by their appearance order in "peers", then "requirements", then "notify" fn bind(&mut self, channels: ComponentChannels); @@ -59,11 +62,12 @@ pub trait JokolayComponent { */ } +/// when reasing the channels, the id of channels are set by their appearance order in "peers", then "requirements", then "notify" #[derive(Default)] pub struct ComponentChannels { pub requirements: - std::collections::HashMap>, + std::collections::HashMap>, pub peers: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. - pub input_notification: Option>, - pub notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. + pub input_notification: Option>, + pub notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. } diff --git a/crates/joko_component_models/src/messages_any.rs b/crates/joko_component_models/src/messages_any.rs index 3a77214..4bf83af 100644 --- a/crates/joko_component_models/src/messages_any.rs +++ b/crates/joko_component_models/src/messages_any.rs @@ -2,29 +2,65 @@ use std::{any::TypeId, sync::Arc}; use serde::{Deserialize, Serialize}; -pub type ComponentDataExchange = Arc>; +#[derive(Clone)] +pub struct ComponentMessage { + data: Arc>, +} -pub fn default_data_exchange() -> ComponentDataExchange { - Arc::new(Box::new(0)) +#[derive(Clone)] +pub struct ComponentResult { + //TODO: remove + Send + Sync + data: Arc>, } -pub fn to_data(value: T) -> ComponentDataExchange +pub fn default_component_result() -> ComponentResult { + ComponentResult { + data: Arc::new(Box::new(0)), + } +} + +pub fn to_data(value: T) -> ComponentMessage where T: Serialize + Clone + Send + Sync + 'static, { - Arc::new(Box::new(value)) + ComponentMessage { + data: Arc::new(Box::new(value)), + } +} +pub fn to_broadcast(value: T) -> ComponentResult +where + T: Serialize + Clone + Send + Sync + 'static, //TODO: remove + Send + Sync +{ + ComponentResult { + data: Arc::new(Box::new(value)), + } } -pub fn from_data<'a, T>(value: ComponentDataExchange) -> T +pub fn from_data<'a, T>(value: &'a ComponentMessage) -> T where T: Deserialize<'a> + Clone + Send + Sync + 'static, { - if let Some(d) = value.downcast_ref::() { + if let Some(d) = value.data.downcast_ref::() { + d.to_owned() + } else { + panic!( + "Bad routing of elements, expected {:?} {:?}", + TypeId::of::(), + TypeId::of::() + ); + } +} + +pub fn from_broadcast<'a, T>(value: &'a ComponentResult) -> T +where + T: Deserialize<'a> + Clone + Send + Sync + 'static, //TODO: remove + Send + Sync +{ + if let Some(d) = value.data.downcast_ref::() { d.to_owned() } else { panic!( "Bad routing of elements, expected {:?} {:?}", - TypeId::of::(), + TypeId::of::(), TypeId::of::() ); } diff --git a/crates/joko_component_models/src/messages_bincode.rs b/crates/joko_component_models/src/messages_bincode.rs index a20cd96..1e706b1 100644 --- a/crates/joko_component_models/src/messages_bincode.rs +++ b/crates/joko_component_models/src/messages_bincode.rs @@ -1,19 +1,45 @@ -pub type ComponentDataExchange = Vec; +use serde::{Deserialize, Serialize}; -pub fn default_data_exchange() -> ComponentDataExchange { - ComponentDataExchange::default() +#[derive(Clone)] +pub struct ComponentMessage { + data: Vec, +} +#[derive(Clone, Default)] +pub struct ComponentResult { + data: Vec, +} + +pub fn default_component_result() -> ComponentResult { + ComponentResult::default() } -pub fn to_data(value: T) -> ComponentDataExchange +pub fn to_data(value: T) -> ComponentMessage +where + T: Serialize, +{ + ComponentMessage { + data: bincode::serialize(&value).unwrap(), + } +} +pub fn to_broadcast(value: T) -> ComponentResult where T: Serialize, { - bincode::serialize(&value).unwrap() + ComponentResult { + data: bincode::serialize(&value).unwrap(), + } +} + +pub fn from_data<'a, T>(value: &'a ComponentMessage) -> T +where + T: Deserialize<'a>, +{ + bincode::deserialize(&value.data).unwrap() } -pub fn from_data<'a, T>(value: &'a ComponentDataExchange) -> T +pub fn from_broadcast<'a, T>(value: &'a ComponentResult) -> T where T: Deserialize<'a>, { - bincode::deserialize(&value).unwrap() + bincode::deserialize(&value.data).unwrap() } diff --git a/crates/joko_link_manager/Cargo.toml b/crates/joko_link_manager/Cargo.toml index f3131ad..15d56a7 100644 --- a/crates/joko_link_manager/Cargo.toml +++ b/crates/joko_link_manager/Cargo.toml @@ -45,3 +45,4 @@ arcdps = { version = "*", default-features = false } notify = {version = "*" } tracing-appender = {version = "*" } tracing-subscriber = {version = "*" } + diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs index 9853ac1..761c364 100644 --- a/crates/joko_link_manager/src/lib.rs +++ b/crates/joko_link_manager/src/lib.rs @@ -12,7 +12,7 @@ use std::vec; use enumflags2::BitFlags; use joko_component_models::{ - from_data, to_data, ComponentChannels, ComponentDataExchange, JokolayComponent, + from_data, to_broadcast, Component, ComponentChannels, ComponentMessage, ComponentResult, }; use joko_core::serde_glam::{IVec2, UVec2, Vec3}; use joko_link_models::{ @@ -36,7 +36,7 @@ use linux::MumbleLinuxImpl as MumblePlatformImpl; use win::MumbleWinImpl as MumblePlatformImpl; struct MumbleChannels { - notification_receiver: tokio::sync::mpsc::Receiver, + notification_receiver: tokio::sync::mpsc::Receiver, } // Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there // pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; @@ -67,7 +67,7 @@ impl MumbleManager { channels: None, is_ui, state: MumbleLinkResult { - read_ui_link: true, + read_ui_link: false, link: None, ui_link: None, }, @@ -103,6 +103,7 @@ impl MumbleManager { return Ok(None); } + //println!("mumble_link {} map found {}", self.is_ui, self.link.map_id); if !self.backend.is_alive() { self.link.client_size.0.x = 0; self.link.client_size.0.y = 0; @@ -223,7 +224,14 @@ impl MumbleManager { } } -impl JokolayComponent for MumbleManager { +impl Component for MumbleManager { + fn init(&mut self) {} + + fn accept_notifications(&self) -> bool { + // we may want to receive data from a manually edited form + !self.is_ui + } + fn flush_all_messages(&mut self) { assert!( self.channels.is_some(), @@ -232,21 +240,22 @@ impl JokolayComponent for MumbleManager { let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); + messages.push(from_data(&msg)); } for msg in messages { self.handle_message(msg); } } - fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + fn tick(&mut self, _latest_time: f64) -> ComponentResult { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); let link = self._tick().unwrap_or(None); self.state.link = link.cloned(); - to_data(self.state.clone()) + //println!("mumble_link result {} has link: {}", self.is_ui, self.state.link.is_some()); + to_broadcast(self.state.clone()) } fn bind(&mut self, mut channels: ComponentChannels) { let (_, notification_receiver) = channels.peers.remove(&0).unwrap(); diff --git a/crates/joko_link_manager/src/win/dll.rs b/crates/joko_link_manager/src/win/dll.rs index 721b5fe..09cca0a 100644 --- a/crates/joko_link_manager/src/win/dll.rs +++ b/crates/joko_link_manager/src/win/dll.rs @@ -253,7 +253,7 @@ pub mod d3d11 { // 0 // } pub mod wine { - use crate::mumble::ctypes::*; + use crate::ctypes::*; use crate::win::MumbleWinImpl; use crate::DEFAULT_MUMBLELINK_NAME; use miette::{Context, IntoDiagnostic, Result}; diff --git a/crates/joko_link_manager/src/win/mod.rs b/crates/joko_link_manager/src/win/mod.rs index 21ebc75..e786586 100644 --- a/crates/joko_link_manager/src/win/mod.rs +++ b/crates/joko_link_manager/src/win/mod.rs @@ -3,7 +3,7 @@ pub mod dll; //putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration -use crate::mumble::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; +use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; use miette::{bail, Context, IntoDiagnostic, Result}; use notify::Watcher; use std::{ @@ -101,6 +101,7 @@ pub struct MumbleWinImpl { } unsafe impl Send for MumbleWinImpl {} +unsafe impl Sync for MumbleWinImpl {} impl MumbleWinImpl { pub fn new(key: &str) -> Result { diff --git a/crates/joko_link_ui_manager/Cargo.toml b/crates/joko_link_ui_manager/Cargo.toml new file mode 100644 index 0000000..f8a43a7 --- /dev/null +++ b/crates/joko_link_ui_manager/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "joko_link_ui_manager" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +joko_core = { path = "../joko_core" } +joko_link_models = { path = "../joko_link_models" } +joko_ui_models = { path = "../joko_ui_models" } +joko_component_models = { path = "../joko_component_models" } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +miette = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } +tokio = { workspace = true } +egui = { workspace = true } + diff --git a/crates/joko_link_ui_manager/README.md b/crates/joko_link_ui_manager/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link_ui_manager/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link_ui_manager/src/lib.rs b/crates/joko_link_ui_manager/src/lib.rs new file mode 100644 index 0000000..0364d9e --- /dev/null +++ b/crates/joko_link_ui_manager/src/lib.rs @@ -0,0 +1,370 @@ +use std::{borrow::BorrowMut, sync::Arc}; + +use egui::DragValue; +use joko_component_models::{ + default_component_result, from_broadcast, to_data, Component, ComponentMessage, ComponentResult, +}; +use joko_link_models::{MessageToMumbleLinkBack, MumbleLink, MumbleLinkResult}; +use joko_ui_models::{UIArea, UIPanel}; + +struct MumbleUIManagerChannels { + subscription_mumble_link: tokio::sync::broadcast::Receiver, + back_end_notifier: tokio::sync::mpsc::Sender, +} + +pub struct MumbleUIManager { + egui_context: Arc, + editable_mumble: bool, + last_known_link: MumbleLink, + channels: Option, +} + +impl MumbleUIManager { + pub fn new(egui_context: Arc) -> Self { + Self { + egui_context, + editable_mumble: false, + last_known_link: Default::default(), + channels: None, + } + } + fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.compass_height)); + ui.add(DragValue::new(&mut link.compass_width)); + ui.add(DragValue::new(&mut link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.horizontal(|ui| { + ui.label(&link.name); + ui.label(format!("{:?}", link.race)); + }); + ui.end_row(); + + ui.label("map id"); + ui.add(DragValue::new(&mut link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut link.map_type)); + ui.end_row(); + ui.label("world position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.map_center_x)); + ui.add(DragValue::new(&mut link.map_center_y)); + ui.add(DragValue::new(&mut link.map_scale)); + }); + ui.end_row(); + + ui.label("address"); + ui.label(format!("{}", link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut link.dpi)); + ui.end_row(); + }); + } + + fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut dummy_link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut dummy_link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut dummy_link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut dummy_link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut dummy_link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + + ui.label("ui state"); + if let Some(ui_state) = dummy_link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.compass_height)); + ui.add(DragValue::new(&mut dummy_link.compass_width)); + ui.add(DragValue::new(&mut dummy_link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut dummy_link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = dummy_link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.label(&dummy_link.name); + ui.end_row(); + ui.label("map id"); + ui.add(DragValue::new(&mut dummy_link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut dummy_link.map_type)); + ui.end_row(); + ui.label("address"); + ui.label(format!("{}", dummy_link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut dummy_link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut dummy_link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", dummy_link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut dummy_link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut dummy_link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut dummy_link.dpi)); + ui.end_row(); + + // ui.label("position"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos.x)); + // ui.add(DragValue::new(&mut link.window_pos.y)); + // }); + // ui.end_row(); + // ui.label("size"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size.x)); + // ui.add(DragValue::new(&mut link.window_size.y)); + // }); + // ui.end_row(); + // ui.label("position_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); + // }); + // ui.end_row(); + // ui.label("size_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); + // }); + // ui.end_row(); + }); + } +} + +impl Component for MumbleUIManager { + fn bind(&mut self, mut channels: joko_component_models::ComponentChannels) { + let channels = MumbleUIManagerChannels { + subscription_mumble_link: channels.requirements.remove(&0).unwrap(), + back_end_notifier: channels.notify.remove(&1).unwrap(), + }; + self.channels = Some(channels); + } + fn flush_all_messages(&mut self) {} + fn accept_notifications(&self) -> bool { + false + } + fn init(&mut self) {} + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn notify(&self) -> Vec<&str> { + vec!["back:mumble_link"] + } + fn peers(&self) -> Vec<&str> { + vec![] + } + fn tick(&mut self, _latest_time: f64) -> joko_component_models::ComponentResult { + let channels = self.channels.as_mut().unwrap(); + + if let Ok(link) = channels.subscription_mumble_link.try_recv() { + let data: MumbleLinkResult = from_broadcast(&link); + if data.read_ui_link || self.editable_mumble { + } else if let Some(link) = data.link { + self.last_known_link = link; + } + } + default_component_result() + } +} + +impl UIPanel for MumbleUIManager { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Mumble Manager".to_string(), + id: "mumble_ui".to_string(), + }] + } + fn init(&mut self) {} + fn gui(&mut self, is_open: &mut bool, _area_id: &str) { + //FIXME: cannot edit anymore => why ? + //UI seems laggy when clicking + let channels = self.channels.as_mut().unwrap(); + let u2mb_sender = channels.back_end_notifier.borrow_mut(); + let egui_context = &self.egui_context; + + egui::Window::new("Mumble Manager") + .open(is_open) + .show(egui_context, |ui| { + ui.horizontal(|ui| { + if ui.selectable_label(!self.editable_mumble, "live").clicked() { + self.editable_mumble = false; + let _ = + u2mb_sender.blocking_send(to_data(MessageToMumbleLinkBack::Autonomous)); + } + if ui + .selectable_label(self.editable_mumble, "editable") + .clicked() + { + self.editable_mumble = true; + let _ = + u2mb_sender.blocking_send(to_data(MessageToMumbleLinkBack::BindedOnUI)); + } + }); + if self.editable_mumble { + ui.label( + egui::RichText::new( + "Mumble is not live, values need to be manually updated.", + ) + .color(egui::Color32::RED), + ); + Self::editable_mumble_ui(ui, &mut self.last_known_link); + } else { + let link: MumbleLink = self.last_known_link.clone(); + Self::live_mumble_ui(ui, link); + } + }); + } +} diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml index 0243030..e1e4272 100644 --- a/crates/joko_package_manager/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -22,10 +22,11 @@ joko_core = { path = "../joko_core" } joko_component_models = { path = "../joko_component_models" } joko_render_models = { path = "../joko_render_models" } joko_package_models = { path = "../joko_package_models" } +joko_ui_models = { path = "../joko_ui_models" } jokoapi = { path = "../jokoapi" } joko_link_models = { path = "../joko_link_models" } miette = { workspace = true } -once = "0.3.4" +once = {workspace = true} ordered_hash_map = { workspace = true } paste = { workspace = true } phf = { version = "*", features = ["macros"] } diff --git a/crates/joko_package_manager/src/io/deserialize.rs b/crates/joko_package_manager/src/io/deserialize.rs index ba0401e..2716e29 100644 --- a/crates/joko_package_manager/src/io/deserialize.rs +++ b/crates/joko_package_manager/src/io/deserialize.rs @@ -11,10 +11,16 @@ use joko_package_models::{ trail::{TBin, TBinStatus, Trail}, }; use ordered_hash_map::OrderedHashMap; -use std::{collections::VecDeque, io::Read, str::FromStr}; +use std::{ + collections::VecDeque, + io::{Cursor, Read}, + path::Path, + str::FromStr, +}; use tracing::{debug, error, info, info_span, instrument, trace, warn}; use uuid::Uuid; use xot::{Element, Node, Xot}; +use zip::result::{ZipError, ZipResult}; const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; @@ -729,6 +735,53 @@ fn parse_category_categories_xml_recursive( Ok(()) } +//copy of zip::ZipArchive extract, but handling the bad windows path +fn extract>( + zip_archive: &mut zip::ZipArchive>>, + directory: P, +) -> ZipResult<()> { + use std::fs; + use std::io; + + for i in 0..zip_archive.len() { + let mut file = zip_archive.by_index(i)?; + let filepath = file + .enclosed_name() + .ok_or(ZipError::InvalidArchive("Invalid file path"))?; + + let filepath = filepath + .to_owned() + .as_mut_os_str() + .to_str() + .unwrap() + .replace('\\', "/") + .trim_start_matches('/') + .to_lowercase(); + let filepath = std::path::Path::new(&filepath); + let outpath = directory.as_ref().join(filepath); + + if file.name().replace('\\', "/").ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut outfile = fs::File::create(&outpath)?; + io::copy(&mut file, &mut outfile)?; + } + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } + Ok(()) +} pub(crate) fn get_pack_from_taco_zip( input_path: std::path::PathBuf, extract_temporary_path: &std::path::PathBuf, @@ -744,8 +797,7 @@ pub(crate) fn get_pack_from_taco_zip( if extract_temporary_path.exists() { std::fs::remove_dir_all(extract_temporary_path).or(Err("Could not purge target folder"))?; } - zip_archive - .extract(extract_temporary_path) + extract(&mut zip_archive, extract_temporary_path) .or(Err("Could not extract archive into target folder"))?; _get_pack_from_taco_folder(extract_temporary_path) diff --git a/crates/joko_package_manager/src/io/serialize.rs b/crates/joko_package_manager/src/io/serialize.rs index edd838a..687933b 100644 --- a/crates/joko_package_manager/src/io/serialize.rs +++ b/crates/joko_package_manager/src/io/serialize.rs @@ -3,14 +3,13 @@ use crate::{ BASE64_ENGINE, }; use base64::Engine; -use cap_std::fs_utf8::Dir; use glam::Vec3; use joko_package_models::{ attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, }; use miette::Result; use ordered_hash_map::OrderedHashMap; -use std::io::Write; +use std::{io::Write, path::Path}; use tracing::info; use uuid::Uuid; use xot::{Element, Node, SerializeOptions, Xot}; @@ -18,15 +17,17 @@ use xot::{Element, Node, SerializeOptions, Xot}; /// Save the pack core as xml pack using the given directory as pack root path. pub(crate) fn save_pack_data_to_dir( pack_data: &LoadedPackData, - writing_directory: &Dir, + writing_directory: &Path, ) -> Result<(), String> { // save categories info!( - "Saving data pack {}, {} categories, {} maps", + "Saving data pack {}, {} topmost categories, {} maps into {:?}", pack_data.name, pack_data.categories.len(), - pack_data.maps.len() + pack_data.maps.len(), + writing_directory ); + std::fs::create_dir_all(writing_directory).or(Err("failed to create core pack directory"))?; let mut tree = Xot::new(); let names = XotAttributeNameIDs::register_with_xot(&mut tree); let od = tree.new_element(names.overlay_data); @@ -38,15 +39,22 @@ pub(crate) fn save_pack_data_to_dir( .with_serialize_options(SerializeOptions { pretty: true }) .to_string(root_node) .or(Err("failed to convert cats xot to string"))?; - writing_directory - .create("categories.xml") + + let target = writing_directory.join("categories.xml"); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) .or(Err("failed to create categories.xml"))? .write_all(cats.as_bytes()) .or(Err("failed to write to categories.xml"))?; + // save maps for (map_id, map_data) in pack_data.maps.iter() { if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + let map_file_name = writing_directory.join(format!("{map_id}.xml")); + if let Err(e) = std::fs::remove_file(map_file_name) { info!( ?e, map_id, "failed to remove xml file that had nothing to write to" @@ -86,8 +94,12 @@ pub(crate) fn save_pack_data_to_dir( .with_serialize_options(SerializeOptions { pretty: true }) .to_string(root_node) .or(Err("failed to serialize map data to string"))?; - writing_directory - .create(format!("{map_id}.xml")) + let target = writing_directory.join(format!("{map_id}.xml")); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) .or(Err("failed to create map xml file"))? .write_all(map_xml.as_bytes()) .or(Err("failed to write map data to file"))?; @@ -96,7 +108,7 @@ pub(crate) fn save_pack_data_to_dir( } pub(crate) fn save_pack_texture_to_dir( pack_texture: &LoadedPackTexture, - writing_directory: &Dir, + writing_directory: &Path, ) -> Result<(), String> { info!( "Saving texture pack {}, {} textures, {} tbins", @@ -104,25 +116,30 @@ pub(crate) fn save_pack_texture_to_dir( pack_texture.textures.len(), pack_texture.tbins.len() ); + std::fs::create_dir_all(writing_directory).or(Err("failed to create core pack directory"))?; // save images for (img_path, img) in pack_texture.textures.iter() { if let Some(parent) = img_path.parent() { - writing_directory.create_dir_all(parent).or(Err(format!( + std::fs::create_dir_all(writing_directory.join(parent)).or(Err(format!( "failed to create parent dir for an image: {img_path}" )))?; } - let amount = writing_directory - .create(img_path.as_str()) + let target = writing_directory.join(img_path.as_str()); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) .or(Err(format!("failed to create file for image: {img_path}")))? - .write(img); - if amount.is_err() { - return Err(format!("failed to write image bytes to file: {img_path}")); - } + .write_all(img) + .or(Err(format!( + "failed to write image bytes to file: {img_path}" + )))?; } // save tbins for (tbin_path, tbin) in pack_texture.tbins.iter() { if let Some(parent) = tbin_path.parent() { - writing_directory.create_dir_all(parent).or(Err(format!( + std::fs::create_dir_all(writing_directory.join(parent)).or(Err(format!( "failed to create parent dir of tbin: {tbin_path}" )))?; } @@ -136,11 +153,14 @@ pub(crate) fn save_pack_texture_to_dir( bytes.extend_from_slice(&node[1].to_ne_bytes()); bytes.extend_from_slice(&node[2].to_ne_bytes()); } - writing_directory - .create(tbin_path.as_str()) + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(writing_directory.join(tbin_path.as_str())) .or(Err(format!("failed to create tbin file: {tbin_path}")))? - .write_all(&bytes) - .or(Err(format!("failed to write tbin to path: {tbin_path}")))?; + .write_all(bytes.as_slice()) + .or(Err(format!("failed to write tbin to file: {tbin_path}")))?; } Ok(()) } diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs index 6eb38e5..9532238 100644 --- a/crates/joko_package_manager/src/manager/pack/category_selection.rs +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -1,4 +1,4 @@ -use joko_component_models::{to_data, ComponentDataExchange}; +use joko_component_models::{to_data, ComponentMessage}; use joko_package_models::{ attributes::CommonAttributes, category::Category, @@ -213,7 +213,7 @@ impl CategorySelection { } fn context_menu( - u2b_sender: &tokio::sync::mpsc::Sender, + u2b_sender: &tokio::sync::mpsc::Sender, cs: &mut CategorySelection, ui: &mut egui::Ui, ) { @@ -236,7 +236,7 @@ impl CategorySelection { } pub fn recursive_selection_ui( - back_end_notifier: &tokio::sync::mpsc::Sender, + back_end_notifier: &tokio::sync::mpsc::Sender, selection: &mut OrderedHashMap, ui: &mut egui::Ui, is_dirty: &mut bool, diff --git a/crates/joko_package_manager/src/manager/pack/import.rs b/crates/joko_package_manager/src/manager/pack/import.rs index 7c9a532..d8843b2 100644 --- a/crates/joko_package_manager/src/manager/pack/import.rs +++ b/crates/joko_package_manager/src/manager/pack/import.rs @@ -10,6 +10,7 @@ pub enum ImportStatus { LoadingPack(std::path::PathBuf), WaitingLoading(std::path::PathBuf), PackDone(String, PackCore, bool), + WaitingForSave, PackError(String), } diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index 1ee29c2..bc1da2e 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -1,10 +1,11 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, - path::PathBuf, + io::{Read, Write}, + path::{Path, PathBuf}, sync::Arc, }; -use joko_component_models::{to_data, ComponentDataExchange}; +use joko_component_models::{to_data, ComponentMessage}; use joko_package_models::{ attributes::{Behavior, CommonAttributes}, category::Category, @@ -59,7 +60,7 @@ pub(crate) struct PackTasks { //an object that can handle such tasks should be passed as argument of any function that may required an async action save_texture_task: AsyncTask>, save_data_task: AsyncTask>, - save_report_task: AsyncTask<(Arc, PackageImportReport), Result<(), String>>, + save_report_task: AsyncTask<(PathBuf, PackageImportReport), Result<(), String>>, load_all_packs_task: AsyncTask<(Arc, std::path::PathBuf), Result>, } @@ -69,7 +70,7 @@ pub(crate) struct PackTasks { pub struct LoadedPackData { pub name: String, pub uuid: Uuid, - pub dir: Arc, + pub path: PathBuf, /// The actual xml pack. //pub core: PackCore, pub categories: OrderedHashMap, @@ -133,11 +134,9 @@ impl PackTasks { //saved on load, or change of list of what to display if status { std::mem::take(&mut texture_pack._is_dirty); - let _ = self - .save_texture_task - .lock() - .unwrap() - .send(texture_pack.clone()); + let t = self.save_texture_task.lock().unwrap(); + let _ = t.send(texture_pack.clone()); + t.recv().unwrap().unwrap(); //expose errors of the save function call. If it had an error, we shall crash. } } @@ -147,7 +146,7 @@ impl PackTasks { let _ = self.save_data_task.lock().unwrap().send(data_pack.clone()); } } - pub fn save_report(&self, target_dir: Arc, report: PackageImportReport, status: bool) { + pub fn save_report(&self, target_dir: PathBuf, report: PackageImportReport, status: bool) { if status { let _ = self .save_report_task @@ -157,11 +156,15 @@ impl PackTasks { } } pub fn load_all_packs(&self, jokolay_dir: Arc, root_path: std::path::PathBuf) { - let _ = self + match self .load_all_packs_task .lock() .unwrap() - .send((jokolay_dir, root_path)); + .send((jokolay_dir, root_path)) + { + Ok(_) => {} + Err(e) => error!(?e), + } } pub fn wait_for_load_all_packs(&self) -> Result { self.load_all_packs_task.lock().unwrap().recv().unwrap() @@ -181,70 +184,68 @@ impl PackTasks { fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<(), String> { trace!("Save texture package {:?}", pack_texture.path); - let std_file = std::fs::OpenOptions::new() - .open(&pack_texture.path) - .or(Err("Could not open file"))?; - let dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + match serde_json::to_string_pretty(&pack_texture.selectable_categories) { - Ok(cs_json) => match dir.write(LoadedPackData::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, + Ok(cs_json) => { + let target = pack_texture + .path + .join(LoadedPackData::CATEGORY_SELECTION_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open category selection data file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write category selection data to disk"); + } Err(e) => { error!(?e, "failed to serialize cat selection"); } } match serde_json::to_string_pretty(&pack_texture.activation_data) { - Ok(ad_json) => match dir.write(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME, ad_json) { - Ok(_) => { - debug!("wrote activation to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write activation data to disk"); - } - }, + Ok(ad_json) => { + let target = pack_texture + .path + .join(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open activation data file on disk") + .write_all(ad_json.as_bytes()) + .expect("failed to write activation data to disk"); + } Err(e) => { error!(?e, "failed to serialize activation"); } } - let writing_directory = dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - save_pack_texture_to_dir(&pack_texture, &writing_directory)?; - Ok(()) + let target = pack_texture.path.join(LoadedPackData::CORE_PACK_DIR_NAME); + save_pack_texture_to_dir(&pack_texture, &target) } fn async_save_data(pack_data: LoadedPackData) -> Result<(), String> { - trace!("Save data package {:?}", pack_data.dir); - pack_data - .dir - .create_dir_all(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to create xmlpack directory"))?; - let writing_directory = pack_data - .dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; - save_pack_data_to_dir(&pack_data, &writing_directory)?; + trace!("Save data package {:?}", pack_data.path); + let target = pack_data.path.join(LoadedPackData::CORE_PACK_DIR_NAME); + save_pack_data_to_dir(&pack_data, &target)?; Ok(()) } - fn async_save_report(input: (Arc, PackageImportReport)) -> Result<(), String> { + fn async_save_report(input: (PathBuf, PackageImportReport)) -> Result<(), String> { let (writing_directory, report) = input; trace!("Save report package {:?}", writing_directory); match serde_json::to_string_pretty(&report) { Ok(cs_json) => { - match writing_directory.write(PackageImportReport::REPORT_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote import quality report to disk"); - } - Err(e) => { - debug!(?e, "failed to write import quality report to disk"); - } - } + let target = writing_directory.join(PackageImportReport::REPORT_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open import quality report file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write import quality report to disk"); } Err(e) => { error!(?e, "failed to serialize import quality report"); @@ -259,13 +260,16 @@ impl LoadedPackData { const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; fn load_selectable_categories( - pack_dir: &Arc, + path: &Path, pack: &PackCore, ) -> OrderedHashMap { //FIXME: we need to patch those categories from the one in the files - (if pack_dir.is_file(Self::CATEGORY_SELECTION_FILE_NAME) { - match pack_dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { + let target = path.join(Self::CATEGORY_SELECTION_FILE_NAME); + trace!("load_selectable_categories open {:?}", target); + let mut cd_json = String::new(); + (if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(&target) { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { Ok(cd) => Some(cd), Err(e) => { error!(?e, "failed to deserialize category data"); @@ -284,14 +288,17 @@ impl LoadedPackData { .unwrap_or_else(|| { let cs = CategorySelection::default_from_pack_core(pack); match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match pack_dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, + Ok(cs_json) => { + let target = path.join(Self::CATEGORY_SELECTION_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open category file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write category data to disk"); + } Err(e) => { error!(?e, "failed to serialize cat selection"); } @@ -321,7 +328,7 @@ impl LoadedPackData { }) .flatten() } - pub fn load_from_dir(name: String, pack_dir: Arc) -> Result { + pub fn load_from_dir(name: String, pack_dir: Arc, path: PathBuf) -> Result { if !pack_dir .try_exists(Self::CORE_PACK_DIR_NAME) .or(Err("failed to check if pack core exists"))? @@ -344,12 +351,12 @@ impl LoadedPackData { //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. //let selectable_categories = CategorySelection::default_from_pack_core(&core); - let selectable_categories = Self::load_selectable_categories(&pack_dir, &core); + let selectable_categories = Self::load_selectable_categories(&path, &core); Ok(LoadedPackData { name, uuid: core.uuid, - dir: pack_dir, + path, selected_files: Default::default(), all_categories: core.all_categories, categories: core.categories, @@ -393,7 +400,7 @@ impl LoadedPackData { #[allow(clippy::too_many_arguments)] pub(crate) fn tick( &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, + b2u_sender: &tokio::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, list_of_active_or_selected_elements_changed: bool, @@ -417,7 +424,7 @@ impl LoadedPackData { fn on_map_changed( &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, + b2u_sender: &tokio::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, active_elements: &mut HashSet, @@ -601,7 +608,7 @@ impl LoadedPackTexture { } pub fn category_sub_menu( &mut self, - back_end_notifier: &tokio::sync::mpsc::Sender, + back_end_notifier: &tokio::sync::mpsc::Sender, ui: &mut egui::Ui, show_only_active: bool, import_quality_report: &PackageImportReport, @@ -629,7 +636,7 @@ impl LoadedPackTexture { } pub(crate) fn tick( &mut self, - renderer_notifier: &tokio::sync::mpsc::Sender, + renderer_notifier: &tokio::sync::mpsc::Sender, _timestamp: f64, link: &MumbleLink, //next_on_screen: &mut HashSet, @@ -847,14 +854,14 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { PACKAGES_DIRECTORY_NAME ))?; - marker_packs_dir + marker_manager_dir .create_dir_all(EDITABLE_PACKAGE_NAME) .into_diagnostic() .wrap_err("failed to create editable package directory")?; - let editable_package = marker_packs_dir + let editable_package = marker_manager_dir .open_dir(EDITABLE_PACKAGE_NAME) .into_diagnostic() - .wrap_err("failed to create editable package directory")?; + .wrap_err("failed to open editable package directory")?; editable_package .create_dir_all("data") @@ -868,8 +875,14 @@ pub fn load_all_from_dir( input: (Arc, std::path::PathBuf), ) -> Result { let (jokolay_dir, root_path) = input; - let marker_packs_dir = - jokolay_to_marker_dir(&jokolay_dir).or(Err("Failed to open packages directory"))?; + trace!("load_all_from_dir {:?}", root_path); + let marker_packs_dir = match jokolay_to_marker_dir(&jokolay_dir) { + Ok(marker_packs_dir) => marker_packs_dir, + Err(e) => { + error!("Failed to open packages directory {:?}", e); + return Err("Failed to open packages directory".to_string()); + } + }; let marker_packs_path = jokolay_to_marker_path(&root_path); let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); @@ -897,8 +910,7 @@ pub fn load_all_from_dir( if name == EDITABLE_PACKAGE_NAME { //TODO: have a version of loading that does not involve already ingested packages if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { - let lp = - build_from_core(name.clone(), pack_dir.into(), pack_path, pack_core); + let lp = build_from_core(name.clone(), pack_path, pack_core); let (data, tex, report) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); @@ -952,21 +964,16 @@ fn build_from_dir( name, elaspsed.as_millis() ); - let res = build_from_core(name.clone(), pack_dir, pack_path, core); + let res = build_from_core(name.clone(), pack_path, core); Ok(res) } -pub fn build_from_core( - name: String, - pack_dir: Arc, - path: PathBuf, - core: PackCore, -) -> ImportTriplet { - let selectable_categories = LoadedPackData::load_selectable_categories(&pack_dir, &core); +pub fn build_from_core(name: String, path: PathBuf, core: PackCore) -> ImportTriplet { + let selectable_categories = LoadedPackData::load_selectable_categories(&path, &core); let data = LoadedPackData { name: name.clone(), uuid: core.uuid, - dir: Arc::clone(&pack_dir), + path: path.clone(), selected_files: Default::default(), all_categories: core.all_categories, categories: core.categories, @@ -978,25 +985,28 @@ pub fn build_from_core( selectable_categories: selectable_categories.clone(), entities_parents: core.entities_parents, }; - let activation_data = (if pack_dir.is_file(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - match pack_dir.read_to_string(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), + let target = path.join(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME); + let mut cd_json = String::new(); + let activation_data = + (if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(target) { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, Err(e) => { - error!(?e, "failed to deserialize activation data"); + error!(?e, "failed to read string of category data"); None } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); + } else { + None + }) + .flatten() + .unwrap_or_default(); let tex = LoadedPackTexture { uuid: core.uuid, selectable_categories, diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs index 9e151f9..122766c 100644 --- a/crates/joko_package_manager/src/manager/package_data.rs +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -5,8 +5,8 @@ use std::{ use cap_std::fs_utf8::Dir; use joko_component_models::{ - default_data_exchange, from_data, to_data, ComponentChannels, ComponentDataExchange, - JokolayComponent, + default_component_result, from_broadcast, from_data, to_data, Component, ComponentChannels, + ComponentMessage, ComponentResult, }; use joko_package_models::package::PackageImportReport; @@ -43,10 +43,10 @@ pub struct PackageBackSharedState { } struct PackageDataChannels { - subscription_mumblelink: tokio::sync::broadcast::Receiver, + subscription_mumblelink: tokio::sync::broadcast::Receiver, - notification_receiver: tokio::sync::mpsc::Receiver, - front_end_notifier: tokio::sync::mpsc::Sender, + front_end_notifier: tokio::sync::mpsc::Sender, + front_end_receiver: tokio::sync::mpsc::Receiver, } /// It manage everything that has to do with marker packs. @@ -299,46 +299,37 @@ impl PackageDataManager { } MessageToPackageBack::SavePack(name, pack) => { tracing::trace!("Handling of MessageToPackageBack::SavePack"); - let std_file = std::fs::OpenOptions::new() + println!("save in {:?}", self.marker_packs_path); + + /*let std_file = std::fs::OpenOptions::new() .open(&self.marker_packs_path) - .or(Err("Could not open file")) .unwrap(); - let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file);*/ let name = name.as_str(); - if marker_packs_dir.exists(name) { - match marker_packs_dir.remove_dir_all(name).into_diagnostic() { + let pack_path = self.marker_packs_path.join(name); + + if pack_path.exists() { + match std::fs::remove_dir_all(pack_path.clone()).into_diagnostic() { Ok(_) => {} Err(e) => { error!(?e, "failed to delete already existing marker pack"); } } } - if let Err(e) = marker_packs_dir.create_dir_all(name) { + if let Err(e) = std::fs::create_dir_all(pack_path.clone()) { error!(?e, "failed to create directory for pack"); } - match marker_packs_dir.open_dir(name) { - Ok(dir) => { - let pack_path = self.marker_packs_path.join(name); - let (data_pack, mut texture_pack, mut report) = - build_from_core(name.to_string(), dir.into(), pack_path, pack); - tracing::trace!("Package loaded into data and texture"); - let uuid_of_insertion = self.save(data_pack, report.clone()); - report.uuid = uuid_of_insertion; - texture_pack.uuid = uuid_of_insertion; - let channels = self.channels.as_mut().unwrap(); - let _ = channels.front_end_notifier.blocking_send(to_data( - MessageToPackageUI::LoadedPack(texture_pack, report), - )); - } - Err(e) => { - error!( - ?e, - "failed to open marker pack directory to save pack {:?} {}", - self.marker_packs_path, - name - ); - } - }; + + let (data_pack, mut texture_pack, mut report) = + build_from_core(name.to_string(), pack_path, pack); + tracing::trace!("Package loaded into data and texture"); + let uuid_of_insertion = self.save(data_pack, report.clone()); + report.uuid = uuid_of_insertion; + texture_pack.uuid = uuid_of_insertion; + let channels = self.channels.as_mut().unwrap(); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::LoadedPack(texture_pack, report), + )); } #[allow(unreachable_patterns)] _ => { @@ -347,23 +338,6 @@ impl PackageDataManager { } } - pub fn flush_all_messages(&mut self) -> PackageBackSharedState { - tracing::trace!( - "choice_of_category_changed: {}", - self.state.choice_of_category_changed - ); - - let mut messages = Vec::new(); - let channels = self.channels.as_mut().unwrap(); - while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); - } - for msg in messages { - self.handle_message(msg); - } - self.state.clone() - } - pub fn _tick(&mut self, mumble_link_result: &MumbleLinkResult) { let mut currently_used_files: BTreeMap = Default::default(); let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); @@ -379,6 +353,10 @@ impl PackageDataManager { let mut have_used_files_list_changed = false; let map_changed = self.current_map_id != link.map_id; self.current_map_id = link.map_id; + trace!( + "PackageDataManager::tick map id is: {}", + self.current_map_id + ); for pack in self.packs.values_mut() { if let Some(current_map) = pack.maps.get(&link.map_id) { for marker in current_map.markers.values() { @@ -449,6 +427,8 @@ impl PackageDataManager { .front_end_notifier .blocking_send(to_data(MessageToPackageUI::TextureSwapChain)); } + } else { + trace!("PackageDataManager::tick no link") } self.state.choice_of_category_changed = false; } @@ -466,8 +446,7 @@ impl PackageDataManager { } } self.delete_packs(to_delete); - self.tasks - .save_report(Arc::clone(&data_pack.dir), report, true); + self.tasks.save_report(data_pack.path.clone(), report, true); self.tasks.save_data(&mut data_pack, true); let mut uuid_to_insert = data_pack.uuid; while self.packs.contains_key(&uuid_to_insert) { @@ -490,6 +469,7 @@ impl PackageDataManager { "channels must be initialized before interacting with component." ); once::assert_has_not_been_called!("Early load must happen only once"); + trace!("Load all packages"); let channels = self.channels.as_mut().unwrap(); // Called only once at application start. let _ = channels @@ -508,6 +488,7 @@ impl PackageDataManager { for ((_, texture_pack), (_, report)) in std::iter::zip(texture_packages, report_packages) { + trace!("load_all notify front of a valid loaded package"); let _ = channels.front_end_notifier.blocking_send(to_data( MessageToPackageUI::LoadedPack(texture_pack, report), )); @@ -523,40 +504,51 @@ impl PackageDataManager { } } -impl JokolayComponent for PackageDataManager { +impl Component for PackageDataManager { + fn init(&mut self) { + self.load_all(); + } + fn flush_all_messages(&mut self) { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); + tracing::trace!( + "choice_of_category_changed: {}", + self.state.choice_of_category_changed + ); + let channels = self.channels.as_mut().unwrap(); let mut messages = Vec::new(); - while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); + while let Ok(msg) = channels.front_end_receiver.try_recv() { + messages.push(from_data(&msg)); } for msg in messages { self.handle_message(msg); } } fn bind(&mut self, mut channels: ComponentChannels) { - let (front_end_notifier, _) = channels.peers.remove(&0).unwrap(); + let (front_end_notifier, front_end_receiver) = channels.peers.remove(&0).unwrap(); let channels = PackageDataChannels { subscription_mumblelink: channels.requirements.remove(&1).unwrap(), front_end_notifier, - notification_receiver: channels.input_notification.unwrap(), + front_end_receiver, }; self.channels = Some(channels); } - fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + fn tick(&mut self, _latest_time: f64) -> ComponentResult { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); let channels = self.channels.as_mut().unwrap(); - let raw_mlr = channels.subscription_mumblelink.blocking_recv().unwrap(); - let mumble_link_result: MumbleLinkResult = from_data(raw_mlr); + //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); + let raw_mlr = channels.subscription_mumblelink.try_recv().unwrap(); + let mumble_link_result: MumbleLinkResult = from_broadcast(&raw_mlr); + //trace!("subscription_mumblelink provided data"); self._tick(&mumble_link_result); - default_data_exchange() + default_component_result() } fn notify(&self) -> Vec<&str> { vec![] @@ -568,6 +560,6 @@ impl JokolayComponent for PackageDataManager { vec!["back:mumble_link"] } fn accept_notifications(&self) -> bool { - true + false } } diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs index bc29a4d..2e2ee93 100644 --- a/crates/joko_package_manager/src/manager/package_ui.rs +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -1,4 +1,5 @@ use std::{ + borrow::BorrowMut, collections::{BTreeMap, HashSet}, sync::{Arc, Mutex}, }; @@ -8,12 +9,14 @@ use image::EncodableLayout; use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; use joko_render_models::messages::MessageToRenderer; +use joko_ui_models::{UIArea, UIPanel}; use serde::{Deserialize, Serialize}; use tracing::{info_span, trace}; use crate::message::MessageToPackageBack; use joko_component_models::{ - from_data, to_data, ComponentChannels, ComponentDataExchange, JokolayComponent, + from_broadcast, from_data, to_broadcast, to_data, Component, ComponentChannels, + ComponentMessage, ComponentResult, }; use joko_core::{serde_glam::Vec3, RelativePath}; use joko_link_models::{MumbleChanges, MumbleLink, MumbleLinkResult}; @@ -34,11 +37,11 @@ pub struct PackageUISharedState { } struct PackageUIChannels { - subscription_mumblelink: tokio::sync::broadcast::Receiver, - notification_receiver: tokio::sync::mpsc::Receiver, + subscription_mumblelink: tokio::sync::broadcast::Receiver, - back_end_notifier: tokio::sync::mpsc::Sender, - renderer_notifier: tokio::sync::mpsc::Sender, + back_end_notifier: tokio::sync::mpsc::Sender, + back_end_receiver: tokio::sync::mpsc::Receiver, + renderer_notifier: tokio::sync::mpsc::Sender, } #[must_use] @@ -72,7 +75,7 @@ impl PackageUIManager { nb_running_tasks_on_back: 0, import_status: Default::default(), }; - Self { + let mut res = Self { packs: Default::default(), tasks: PackTasks::new(), reports: Default::default(), @@ -90,7 +93,9 @@ impl PackageUIManager { delayed_trail_texture: Default::default(), channels: None, state, - } + }; + res._init(); + res } fn handle_message(&mut self, msg: MessageToPackageUI) { @@ -108,6 +113,10 @@ impl PackageUIManager { self.delete_packs(to_delete); } MessageToPackageUI::FirstLoadDone => { + let channels = self.channels.as_ref().unwrap(); + let renderer_notifier = &channels.renderer_notifier; + let _ = + renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); self.state.first_load_done = true; } MessageToPackageUI::ImportedPack(file_name, pack) => { @@ -127,6 +136,9 @@ impl PackageUIManager { let _ = channels.back_end_notifier.blocking_send(to_data( MessageToPackageBack::CategoryActivationStatusChanged, )); + let renderer_notifier = &channels.renderer_notifier; + let _ = + renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); } MessageToPackageUI::MarkerTexture( pack_uuid, @@ -179,27 +191,8 @@ impl PackageUIManager { } } - pub fn flush_all_messages(&mut self) -> PackageUISharedState { - let channels = self.channels.as_mut().unwrap(); - if let Ok(mut import_status) = self.state.import_status.lock() { - if let ImportStatus::LoadingPack(file_path) = &mut *import_status { - let _ = channels - .back_end_notifier - .blocking_send(to_data(MessageToPackageBack::ImportPack(file_path.clone()))); - *import_status = ImportStatus::WaitingLoading(file_path.clone()); - } - } - let mut messages = Vec::new(); - while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); - } - for msg in messages { - self.handle_message(msg); - } - self.state.clone() - } - - pub fn late_init(&mut self, egui_context: &egui::Context) { + fn _init(&mut self) { + let egui_context: &egui::Context = &self.egui_context; //TODO: make it even later, at another place if self.default_marker_texture.is_none() { let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); @@ -329,6 +322,7 @@ impl PackageUIManager { } pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { + trace!("PackageUIManager::_tick for {} packages", self.packs.len()); let tasks = &self.tasks; let channels = self.channels.as_ref().unwrap(); let renderer_notifier = &channels.renderer_notifier; @@ -337,6 +331,7 @@ impl PackageUIManager { } if link.changes.contains(MumbleChanges::Position) || link.changes.contains(MumbleChanges::Map) + || self.state.list_of_textures_changed { for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); @@ -344,62 +339,11 @@ impl PackageUIManager { std::mem::drop(span_guard); } let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); + self.state.list_of_textures_changed = false; } Ok(()) } - pub fn menu_ui( - &mut self, - ui: &mut egui::Ui, - nb_running_tasks_on_back: i32, - nb_running_tasks_on_network: i32, - ) { - ui.menu_button("Markers", |ui| { - if self.show_only_active { - if ui.button("Show everything").clicked() { - self.show_only_active = false; - } - } else if ui.button("Show only active").clicked() { - self.show_only_active = true; - } - if ui.button("Activate all elements").clicked() { - self.category_set_all(true); - let channels = self.channels.as_mut().unwrap(); - let _ = channels - .back_end_notifier - .blocking_send(to_data(MessageToPackageBack::CategorySetAll(true))); - } - if ui.button("Deactivate all elements").clicked() { - self.category_set_all(false); - let channels = self.channels.as_mut().unwrap(); - let _ = channels - .back_end_notifier - .blocking_send(to_data(MessageToPackageBack::CategorySetAll(false))); - } - - let channels = self.channels.as_mut().unwrap(); - for (pack, import_quality_report) in - std::iter::zip(self.packs.values_mut(), self.reports.values()) - { - //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; - //category_sub_menu is for display only, it's a bad idea to use it to manipulate status - pack.category_sub_menu( - &channels.back_end_notifier, - ui, - self.show_only_active, - import_quality_report, - ); - } - }); - if self.tasks.is_running() - || nb_running_tasks_on_back > 0 - || nb_running_tasks_on_network > 0 - { - let sp = egui::Spinner::new() - .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); - ui.add(sp); - } - } pub fn status_as_color( &self, nb_running_tasks_on_back: i32, @@ -435,12 +379,14 @@ impl PackageUIManager { egui::Color32::from_rgb(color_ui, color_back, color_network) } - fn gui_file_manager(&mut self, etx: &egui::Context, open: &mut bool) { + fn gui_file_manager(&mut self, is_open: &mut bool) { + //FIXME: the deactivate all for all files, seems to toggle only the next one not in target state + let egui_context = self.egui_context.borrow_mut(); let channels = self.channels.as_mut().unwrap(); let mut files_changed = false; Window::new("File Manager") - .open(open) - .show(etx, |ui| -> Result<()> { + .open(is_open) + .show(egui_context, |ui| -> Result<()> { egui::ScrollArea::vertical().show(ui, |ui| { egui::Grid::new("link grid") .num_columns(4) @@ -531,136 +477,147 @@ impl PackageUIManager { } } - fn gui_package_details(&mut self, ui: &mut Ui, uuid: Uuid) { + fn gui_package_details(ui: &mut Ui, data: (&LoadedPackTexture, &PackageImportReport)) { // protection against deletion while displaying details - if let Some(pack) = self.packs.get(&uuid) { - if let Some(report) = self.reports.get(&uuid) { - let collapsing = - CollapsingHeader::new(format!("Last load details of package {}", pack.name)); - let header_response = collapsing - .open(Some(true)) + let (pack, report) = data; + + let collapsing = + CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + //FIXME: clear the pack details + let _header_response = collapsing + .open(Some(true)) + .show(ui, |ui| { + egui::Grid::new("packs details") + .striped(true) .show(ui, |ui| { - egui::Grid::new("packs details") - .striped(true) - .show(ui, |ui| { - let number_of = &report.number_of; - ui.label("categories"); - ui.label(format!("{}", number_of.categories)); - ui.end_row(); - ui.label("missing_categories"); - ui.label(format!("{}", number_of.missing_categories)); - ui.end_row(); - ui.label("textures"); - ui.label(format!("{}", number_of.textures)); - ui.end_row(); - ui.label("missing_textures"); - ui.label(format!("{}", number_of.missing_textures)); - ui.end_row(); - ui.label("entities"); - ui.label(format!("{}", number_of.entities)); - ui.end_row(); - ui.label("markers"); - ui.label(format!("{}", number_of.markers)); - ui.end_row(); - ui.label("trails"); - ui.label(format!("{}", number_of.trails)); - ui.end_row(); - ui.label("routes"); - ui.label(format!("{}", number_of.routes)); - ui.end_row(); - ui.label("maps"); - ui.label(format!("{}", number_of.maps)); - ui.end_row(); - ui.label("source_files"); - ui.label(format!("{}", number_of.source_files)); - ui.end_row(); - }) + let number_of = &report.number_of; + ui.label("categories"); + ui.label(format!("{}", number_of.categories)); + ui.end_row(); + ui.label("missing_categories"); + ui.label(format!("{}", number_of.missing_categories)); + ui.end_row(); + ui.label("textures"); + ui.label(format!("{}", number_of.textures)); + ui.end_row(); + ui.label("missing_textures"); + ui.label(format!("{}", number_of.missing_textures)); + ui.end_row(); + ui.label("entities"); + ui.label(format!("{}", number_of.entities)); + ui.end_row(); + ui.label("markers"); + ui.label(format!("{}", number_of.markers)); + ui.end_row(); + ui.label("trails"); + ui.label(format!("{}", number_of.trails)); + ui.end_row(); + ui.label("routes"); + ui.label(format!("{}", number_of.routes)); + ui.end_row(); + ui.label("maps"); + ui.label(format!("{}", number_of.maps)); + ui.end_row(); + ui.label("source_files"); + ui.label(format!("{}", number_of.source_files)); + ui.end_row(); }) - .header_response; - if header_response.clicked() { + }) + .header_response; + /*if header_response.clicked() { + self.pack_details = None; + }*/ + } + fn gui_package_list(&mut self, is_open: &mut bool) { + let egui_context = self.egui_context.borrow_mut(); + let import_status = self.state.import_status.clone(); + let details = if let Some(uuid) = self.pack_details { + if let Some(pack) = self.packs.get(&uuid) { + if let Some(report) = self.reports.get(&uuid) { + Some((pack, report)) + } else { self.pack_details = None; + None } } else { self.pack_details = None; + None } } else { - self.pack_details = None; - } - } - fn gui_package_list( - &mut self, - etx: &egui::Context, - import_status: &Arc>, - open: &mut bool, - first_load_done: bool, - ) { - Window::new("Package Loader").open(open).show(etx, |ui| -> Result<()> { + None + }; + Window::new("Package Loader").open(is_open).show(egui_context, |ui| -> Result<()> { let channels = self.channels.as_mut().unwrap(); - CollapsingHeader::new("Loaded Packs").show(ui, |ui| { - egui::Grid::new("packs").striped(true).show(ui, |ui| { - if !first_load_done { - ui.label("Loading in progress..."); - } - let mut to_delete = vec![]; - for pack in self.packs.values() { - ui.label(pack.name.clone()); - if ui.button("delete").clicked() { - to_delete.push(pack.uuid); - } - if ui.button("Details").clicked() { - self.pack_details = Some(pack.uuid); + if !self.state.first_load_done { + ui.label("Loading in progress..."); + } else { + CollapsingHeader::new("Loaded Packs").show(ui, |ui| { + egui::Grid::new("packs").striped(true).show(ui, |ui| { + let mut to_delete = vec![]; + for pack in self.packs.values() { + ui.label(pack.name.clone()); + if ui.button("delete").clicked() { + to_delete.push(pack.uuid); + } + if ui.button("Details").clicked() { + self.pack_details = Some(pack.uuid); + } + if ui.button("Export").clicked() { + //TODO + } + ui.end_row(); } - if ui.button("Export").clicked() { - //TODO + if !to_delete.is_empty() { + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::DeletePacks(to_delete))); } - ui.end_row(); - } - if !to_delete.is_empty() { - let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::DeletePacks(to_delete))); - } + }); }); - }); - if let Some(uuid) = self.pack_details { - self.gui_package_details(ui, uuid); - } else if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - Self::pack_importer(Arc::clone(import_status)); + if let Some(data) = details { + Self::gui_package_details(ui, data); + } else if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + Self::pack_importer(Arc::clone(&import_status)); + } + //ui.label("import not started yet"); } - //ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { - ui.label(format!("pack is being imported from {p:?}")); - } - ImportStatus::PackDone(name, pack, saved) => { - if *saved { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); - } else { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - if ui.button("save").clicked() { - let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::SavePack(name.clone(), pack.clone()))); + ImportStatus::WaitingForFileChooser => { + ui.label( + "waiting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + if *saved { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } else { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + if ui.button("save").clicked() { + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::SavePack(name.clone(), pack.clone()))); + *status = ImportStatus::WaitingForSave; + } } } - } - ImportStatus::PackError(e) => { - let error_msg = format!("failed to import pack due to error: {e:#?}"); - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - *status = ImportStatus::UnInitialized; + ImportStatus::WaitingForSave => { + ui.colored_label(egui::Color32::GREEN, "Waiting for pack to be saved."); + } + ImportStatus::PackError(e) => { + let error_msg = format!("failed to import pack due to error: {e:#?}"); + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + *status = ImportStatus::UnInitialized; + } + ui.colored_label( + egui::Color32::RED, + error_msg, + ); } - ui.colored_label( - egui::Color32::RED, - error_msg, - ); } } } @@ -668,17 +625,6 @@ impl PackageUIManager { Ok(()) }); } - pub fn gui( - &mut self, - etx: &egui::Context, - is_marker_open: &mut bool, - import_status: &Arc>, - is_file_open: &mut bool, - first_load_done: bool, - ) { - self.gui_package_list(etx, import_status, is_marker_open, first_load_done); - self.gui_file_manager(etx, is_file_open); - } pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { /* @@ -698,26 +644,40 @@ impl PackageUIManager { } } -impl JokolayComponent for PackageUIManager { +impl Component for PackageUIManager { + fn init(&mut self) {} + fn flush_all_messages(&mut self) { assert!(self.channels.is_some()); let channels = self.channels.as_mut().unwrap(); + + if let Ok(mut import_status) = self.state.import_status.lock() { + if let ImportStatus::LoadingPack(file_path) = &mut *import_status { + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::ImportPack(file_path.clone()))); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); + } + } let mut messages = Vec::new(); - while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); + while let Ok(msg) = channels.back_end_receiver.try_recv() { + messages.push(from_data(&msg)); } for msg in messages { self.handle_message(msg); } } - fn tick(&mut self, timestamp: f64) -> ComponentDataExchange { + fn tick(&mut self, timestamp: f64) -> ComponentResult { assert!(self.channels.is_some()); + let raw_link = { let channels = self.channels.as_mut().unwrap(); - channels.subscription_mumblelink.blocking_recv().unwrap() + //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); + channels.subscription_mumblelink.try_recv().unwrap() }; - let link_result: MumbleLinkResult = from_data(raw_link); + let link_result: MumbleLinkResult = from_broadcast(&raw_link); + //trace!("subscription_mumblelink provided data"); for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in std::mem::take(&mut self.delayed_marker_texture) @@ -746,16 +706,18 @@ impl JokolayComponent for PackageUIManager { //let channels = self.channels.as_mut().unwrap(); //let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); //let z_near: f32 = from_data(raw_z_near); - let _ = self._tick(timestamp, link_result.link.as_ref().unwrap(), self.z_near); - to_data(self.state.clone()) + if let Some(link) = link_result.link.as_ref() { + let _ = self._tick(timestamp, link, self.z_near); + } + to_broadcast(self.state.clone()) } fn bind(&mut self, mut channels: ComponentChannels) { - let (back_end_notifier, _) = channels.peers.remove(&0).unwrap(); + let (back_end_notifier, back_end_receiver) = channels.peers.remove(&0).unwrap(); let channels = PackageUIChannels { - subscription_mumblelink: channels.requirements.remove(&2).unwrap(), - notification_receiver: channels.input_notification.unwrap(), + subscription_mumblelink: channels.requirements.remove(&1).unwrap(), back_end_notifier, - renderer_notifier: channels.notify.remove(&1).unwrap(), + back_end_receiver, + renderer_notifier: channels.notify.remove(&2).unwrap(), }; self.channels = Some(channels); @@ -770,6 +732,84 @@ impl JokolayComponent for PackageUIManager { vec!["ui:mumble_link"] } fn accept_notifications(&self) -> bool { - true + false + } +} + +impl UIPanel for PackageUIManager { + fn areas(&self) -> Vec { + vec![ + UIArea { + is_open: false, + name: "Package Manager".to_string(), + id: "package_loading".to_string(), + }, + UIArea { + is_open: false, + name: "File Manager".to_string(), + id: "file_manager".to_string(), + }, + ] + } + fn init(&mut self) {} + fn gui(&mut self, is_open: &mut bool, area_id: &str) { + match area_id { + "package_loading" => { + self.gui_package_list(is_open); + } + "file_manager" => { + self.gui_file_manager(is_open); + } + _ => {} + } + } + fn menu_ui(&mut self, ui: &mut egui::Ui) { + let nb_running_tasks_on_back: i32 = 0; + let nb_running_tasks_on_network: i32 = 0; + ui.menu_button("Markers", |ui| { + if self.show_only_active { + if ui.button("Show everything").clicked() { + self.show_only_active = false; + } + } else if ui.button("Show only active").clicked() { + self.show_only_active = true; + } + if ui.button("Activate all elements").clicked() { + self.category_set_all(true); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(true))); + } + if ui.button("Deactivate all elements").clicked() { + self.category_set_all(false); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(false))); + } + + let channels = self.channels.as_mut().unwrap(); + for (pack, import_quality_report) in + std::iter::zip(self.packs.values_mut(), self.reports.values()) + { + //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; + //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + pack.category_sub_menu( + &channels.back_end_notifier, + ui, + self.show_only_active, + import_quality_report, + ); + } + }); + if self.tasks.is_running() + || nb_running_tasks_on_back > 0 + || nb_running_tasks_on_network > 0 + { + let sp = egui::Spinner::new() + .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); + ui.add(sp); + } } } diff --git a/crates/joko_plugin_manager/Cargo.toml b/crates/joko_plugin_manager/Cargo.toml index 9bc73ee..ba6563c 100644 --- a/crates/joko_plugin_manager/Cargo.toml +++ b/crates/joko_plugin_manager/Cargo.toml @@ -8,5 +8,4 @@ edition = "2021" [dependencies] joko_component_models = { path = "../joko_component_models" } -scopeguard = "1.2.0" tokio = { workspace = true } diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs index 2da8b3f..ac188a1 100644 --- a/crates/joko_plugin_manager/src/lib.rs +++ b/crates/joko_plugin_manager/src/lib.rs @@ -1,15 +1,16 @@ use joko_component_models::{ - default_data_exchange, ComponentChannels, ComponentDataExchange, JokolayComponent, + default_component_result, Component, ComponentChannels, ComponentResult, }; pub struct JokolayPlugin {} pub struct JokolayPluginManager {} -impl JokolayComponent for JokolayPlugin { +impl Component for JokolayPlugin { + fn init(&mut self) {} fn flush_all_messages(&mut self) {} - fn tick(&mut self, _timestamp: f64) -> ComponentDataExchange { - default_data_exchange() + fn tick(&mut self, _timestamp: f64) -> ComponentResult { + default_component_result() } fn bind(&mut self, _channels: ComponentChannels) {} fn requirements(&self) -> Vec<&str> { diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs index 4304000..e2956d7 100644 --- a/crates/joko_render_manager/src/renderer.rs +++ b/crates/joko_render_manager/src/renderer.rs @@ -1,3 +1,6 @@ +use std::sync::Arc; +use std::sync::RwLock; + use crate::billboard::BillBoardRenderer; use crate::gl_error; use egui_render_three_d::three_d; @@ -10,14 +13,17 @@ use egui_render_three_d::three_d::ScissorBox; use egui_render_three_d::three_d::Viewport; use egui_render_three_d::ThreeDBackend; use egui_render_three_d::ThreeDConfig; +use egui_window_glfw_passthrough::glfw::Context; use egui_window_glfw_passthrough::GlfwBackend; use glam::Mat4; -use joko_component_models::default_data_exchange; +use joko_component_models::default_component_result; +use joko_component_models::from_broadcast; use joko_component_models::from_data; +use joko_component_models::Component; use joko_component_models::ComponentChannels; -use joko_component_models::ComponentDataExchange; -use joko_component_models::JokolayComponent; -use joko_link_models::MumbleLink; +use joko_component_models::ComponentMessage; +use joko_component_models::ComponentResult; +use joko_link_models::MumbleLinkResult; use joko_link_models::UIState; use joko_render_models::messages::MessageToRenderer; use three_d::prelude::*; @@ -25,7 +31,8 @@ use three_d::prelude::*; use joko_render_models::{marker::MarkerObject, trail::TrailObject}; struct JokoRendererChannels { - notification_receiver: tokio::sync::mpsc::Receiver, + notification_receiver: tokio::sync::mpsc::Receiver, + subscription_mumble_link: tokio::sync::broadcast::Receiver, } pub struct JokoRenderer { pub view_proj: Mat4, @@ -35,30 +42,36 @@ pub struct JokoRenderer { pub has_link: bool, pub is_map_open: bool, pub billboard_renderer: BillBoardRenderer, + glfw_backend: Arc>, + egui_context: Arc, pub gl: egui_render_three_d::ThreeDBackend, channels: Option, } +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for JokoRenderer {} +unsafe impl Sync for JokoRenderer {} + impl JokoRenderer { - pub fn new(glfw_backend: &GlfwBackend) -> Self { + pub fn new(glfw_backend: Arc>, egui_context: Arc) -> Self { /* FIXME: Box + JokoRenderer => segfault when panic Arc vs Box: no change */ //let glfw = glfw_backend.glfw.clone(); + let framebuffer_size_physical = glfw_backend.read().unwrap().framebuffer_size_physical; let backend = ThreeDBackend::new( ThreeDConfig { glow_config: Default::default(), }, - |s| glfw_backend.glfw.get_proc_address_raw(s), - //glfw_backend.window.raw_window_handle(), - glfw_backend.framebuffer_size_physical, + |s| glfw_backend.read().unwrap().glfw.get_proc_address_raw(s), + framebuffer_size_physical, ); let viewport = Viewport { x: 0, y: 0, - width: glfw_backend.framebuffer_size_physical[0], - height: glfw_backend.framebuffer_size_physical[1], + width: framebuffer_size_physical[0], + height: framebuffer_size_physical[1], }; let gl = &backend.context; unsafe { gl_error!(gl) }; @@ -79,7 +92,9 @@ impl JokoRenderer { has_link: false, is_map_open: false, gl: backend, + egui_context, billboard_renderer, + glfw_backend, cam_pos: Default::default(), channels: None, } @@ -145,7 +160,7 @@ impl JokoRenderer { ]; } */ - fn handle_u2u_message(&mut self, msg: MessageToRenderer) { + fn handle_message(&mut self, msg: MessageToRenderer) { match msg { MessageToRenderer::BulkMarkerObject(marker_objects) => { tracing::debug!( @@ -194,7 +209,8 @@ impl JokoRenderer { self.billboard_renderer.trails_wip.push(trail_object); } - pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { + pub fn prepare_frame(&mut self) { + let latest_framebuffer_size_getter = || Self::frame_size(Arc::clone(&self.glfw_backend)); self.gl.prepare_frame(latest_framebuffer_size_getter); unsafe { let gl = self.gl.context.clone(); @@ -247,12 +263,38 @@ impl JokoRenderer { }; self.gl.resize_framebuffer(latest_size); } + + fn frame_size(glfw_backend: Arc>) -> [u32; 2] { + let mut glfw_backend = glfw_backend.write().unwrap(); + let latest_size = glfw_backend.window.get_framebuffer_size(); + + let latest_size = [latest_size.0 as _, latest_size.1 as _]; + + glfw_backend.framebuffer_size_physical = latest_size; + glfw_backend.window_size_logical = [ + latest_size[0] as f32 / glfw_backend.scale, + latest_size[1] as f32 / glfw_backend.scale, + ]; + glfw_backend.resized_event_pending = false; + latest_size + } + fn _window_tick(&mut self) { + let resized_event_pending = { self.glfw_backend.read().unwrap().resized_event_pending }; + if resized_event_pending { + let latest_size = Self::frame_size(Arc::clone(&self.glfw_backend)); + self.resize_framebuffer(latest_size); + } + + self.prepare_frame(); + } } -impl JokolayComponent for JokoRenderer { - fn bind(&mut self, channels: ComponentChannels) { +impl Component for JokoRenderer { + fn init(&mut self) {} + fn bind(&mut self, mut channels: ComponentChannels) { let channels = JokoRendererChannels { notification_receiver: channels.input_notification.unwrap(), + subscription_mumble_link: channels.requirements.remove(&0).unwrap(), }; self.channels = Some(channels); } @@ -269,19 +311,27 @@ impl JokolayComponent for JokoRenderer { //two steps reading due to self mutability required by channel let mut messages = Vec::new(); while let Ok(msg) = channels.notification_receiver.try_recv() { - messages.push(from_data(msg)); + messages.push(from_data(&msg)); } for msg in messages { - self.handle_u2u_message(msg); + self.handle_message(msg); } } - fn tick(&mut self, _latest_time: f64) -> ComponentDataExchange { + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn tick(&mut self, latest_time: f64) -> ComponentResult { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); - let link: Option<&MumbleLink> = None; - if let Some(link) = link { + + self._window_tick(); + let channels = self.channels.as_mut().unwrap(); + let raw_link = channels.subscription_mumble_link.blocking_recv().unwrap(); + let link: MumbleLinkResult = from_broadcast(&raw_link); + if let Some(link) = link.link { + //trace!("JokoRenderer {:?} {:?}", link.player_pos, link.cam_pos); //x positive => east //y positive => ascention //z positive => north @@ -358,6 +408,37 @@ impl JokolayComponent for JokoRenderer { } else { self.has_link = false; } - default_data_exchange() + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + .. + } = self.egui_context.end_frame(); + if !platform_output.copied_text.is_empty() { + self.glfw_backend + .write() + .unwrap() + .window + .set_clipboard_string(&platform_output.copied_text); + } + + // if it doesn't require either keyboard or pointer, set passthrough to true + self.glfw_backend + .write() + .unwrap() + .window + .set_mouse_passthrough( + !(self.egui_context.wants_keyboard_input() + || self.egui_context.wants_pointer_input()), + ); + + let meshes = self + .egui_context + .tessellate(shapes, self.egui_context.pixels_per_point()); + let window_size_logical = self.glfw_backend.read().unwrap().window_size_logical; + self.render_egui(meshes, textures_delta, window_size_logical, latest_time); + self.present(); + self.glfw_backend.write().unwrap().window.swap_buffers(); + default_component_result() } } diff --git a/crates/joko_ui_models/Cargo.toml b/crates/joko_ui_models/Cargo.toml new file mode 100644 index 0000000..369ebf9 --- /dev/null +++ b/crates/joko_ui_models/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "joko_ui_models" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +egui = {workspace = true} diff --git a/crates/joko_ui_models/README.md b/crates/joko_ui_models/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_ui_models/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_ui_models/src/lib.rs b/crates/joko_ui_models/src/lib.rs new file mode 100644 index 0000000..45b783b --- /dev/null +++ b/crates/joko_ui_models/src/lib.rs @@ -0,0 +1,13 @@ +use egui::Ui; + +pub struct UIArea { + pub is_open: bool, + pub name: String, + pub id: String, +} +pub trait UIPanel { + fn init(&mut self); + fn gui(&mut self, is_open: &mut bool, area_id: &str); + fn menu_ui(&mut self, _ui: &mut Ui) {} + fn areas(&self) -> Vec; +} diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index d65bc18..d61b300 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -19,10 +19,12 @@ wayland = ["egui_window_glfw_passthrough/wayland"] enumflags2 = { workspace = true } joko_component_manager = { path = "../joko_component_manager" } joko_component_models = { path = "../joko_component_models" } +joko_ui_models = { path = "../joko_ui_models" } joko_plugin_manager = { path = "../joko_plugin_manager" } joko_render_manager = { path = "../joko_render_manager" } joko_package_manager = { path = "../joko_package_manager" } joko_link_manager = { path = "../joko_link_manager" } +joko_link_ui_manager = { path = "../joko_link_ui_manager" } joko_link_models = { path = "../joko_link_models" } egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users diff --git a/crates/jokolay/src/app/menu.rs b/crates/jokolay/src/app/menu.rs new file mode 100644 index 0000000..6008a33 --- /dev/null +++ b/crates/jokolay/src/app/menu.rs @@ -0,0 +1,290 @@ +use std::sync::{Arc, RwLock}; + +use egui_window_glfw_passthrough::GlfwBackend; +use joko_component_models::{ + default_component_result, from_broadcast, Component, ComponentChannels, ComponentResult, +}; +use joko_link_models::{MumbleLinkResult, UISize}; +use joko_ui_models::{UIArea, UIPanel}; +use tracing::info; + +use super::window::{MINIMAL_WINDOW_HEIGHT, MINIMAL_WINDOW_WIDTH}; + +struct MenuPanel { + //TODO: area => so we can have a single element producing multiple windows. It'll have to be registered for each area + //Or, register the functions that handle the areas. + //rename gui() into area() + panel: Arc>, + areas: Vec, +} + +struct MenuPanelManagerChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, +} + +/// Guild Wars 2 has an array of menu icons on top left corner of the game. +/// Its size is affected by four different factors +/// 1. UISZ: +/// This is a setting in graphics options of gw2 and it comes in 4 variants +/// small, normal, large and larger. +/// This is something we can get from mumblelink's context. +/// 2. DPI scaling +/// This is a setting in graphics options too. When scaling is enabled, sizes of menu become bigger according to the dpi of gw2 window +/// This is something we get from gw2's config file in AppData/Roaming and store in mumble link as dpi scaling +/// We also get dpi of gw2 window and store it in mumble link. +/// 3. Dimensions of the gw2 window +/// This is something we get from mumble link and win32 api. We store this as client pos/size in mumble link +/// It is not just the width or height, but their ratio to the 1024x768 resolution +/// +/// 1. By default, with dpi 96 (scale 1.0), at resolution 1024x768 these are the sizes of menu at different uisz settings +/// UISZ -> WIDTH HEIGHT +/// small -> 288 27 +/// normal -> 319 31 +/// large -> 355 34 +/// larger -> 391 38 +/// all units are in raw pixels. +/// +/// If we think of small uisz as the default. Then, we can express the rest of the sizes as ratio to small. +/// small = 1.0 +/// normal = 1.1 +/// large = 1.23 +/// larger = 1.35 +/// +/// So, just multiply small (288) with these ratios to get the actual pixels of each uisz. +/// 2. When dpi doubles, so do the sizes. 288 -> 576, 319 -> 638 etc.. So, when dpi scaling is enabled, we must multiply the above uisz ratio with dpi scale ratio to get the combined scaling ratio. +/// 3. The dimensions thing is a little complicated. So, i will just list the actual steps here. +/// 1. take gw2's actual width in raw pixels. lets call this gw2_width. +/// 2. take 1024 as reference minimum width. If dpi scaling is enabled, multiply 1024 * dpi scaling ratio. lets call this reference_width. +/// 3. Now, get the smaller value out of the two. lets call this minimum_width. +/// 4. finally, do (minimum_width / reference_width) to get "width scaling ratio". +/// 5. repeat steps 1 - 4, but for height. use 768 as the reference width (with approapriate dpi scaling). +/// 6. now just take the minimum of "width scaling ratio" and "height scaling ratio". lets call this "aspect ratio scaling". +/// +/// Finally, just multiply the width 288 or height 27 with these three values. +/// eg: menu width = 288 * uisz_ratio * dpi_scaling_ratio * aspect_ratio_scaling; +/// do the same with 288 replaced by 27 for height. + +pub struct MenuPanelManager { + //TODO: turn the MenuPanel into a component which depends on MumbleLink manager + pub pos: egui::Pos2, + pub ui_scaling_factor: f32, + pub show_tracing_window: bool, + glfw_backend: Arc>, + egui_context: Arc, + menus: Vec, + channels: Option, +} + +unsafe impl Send for MenuPanelManager {} +unsafe impl Sync for MenuPanelManager {} + +impl MenuPanelManager { + pub const WIDTH: f32 = 288.0; + pub const HEIGHT: f32 = 27.0; + + pub fn new(glfw_backend: Arc>, egui_context: Arc) -> Self { + Self { + glfw_backend, + egui_context, + pos: Default::default(), + show_tracing_window: Default::default(), + ui_scaling_factor: Default::default(), + menus: Default::default(), + channels: None, + } + } + + pub fn register(&mut self, component: Arc>) { + self.menus.push(MenuPanel { + panel: component.clone(), + areas: component.read().unwrap().areas(), + }) + } + + pub fn gui(&mut self) { + //let mut glfw_backend = self.glfw_backend.(); + // do the gui stuff now + egui::Area::new("menu panel") + .fixed_pos(self.pos) + .interactable(true) + .order(egui::Order::Foreground) + .show(&self.egui_context, |ui| { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; + ui.horizontal(|ui| { + //TODO: if any displayed, show an additional "hide all" + ui.menu_button( + egui::RichText::new("JKL") + .size((MenuPanelManager::HEIGHT - 2.0) * self.ui_scaling_factor) + .background_color(egui::Color32::TRANSPARENT), + |ui: &mut egui::Ui| { + let mut any_open = false; + for panel in self.menus.iter_mut() { + for area in panel.areas.iter_mut() { + ui.checkbox(&mut area.is_open, &area.name); + any_open = any_open || area.is_open; + } + } + //ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); + if any_open && ui.button("Close all panels").clicked() { + for panel in self.menus.iter_mut() { + for area in panel.areas.iter_mut() { + area.is_open = false; + } + } + } + if ui.button("exit").clicked() { + info!("exiting jokolay"); + self.glfw_backend + .write() + .unwrap() + .window + .set_should_close(true); + } + }, + ); + for panel in self.menus.iter_mut() { + let handle = &mut panel.panel.write().unwrap(); + handle.menu_ui(ui); + } + }); + }); + for panel in self.menus.iter_mut() { + let handle = &mut panel.panel.write().unwrap(); + for area in panel.areas.iter_mut() { + handle.gui(&mut area.is_open, &area.id); + } + } + } +} + +fn convert_uisz_to_scale(uisize: UISize) -> f32 { + const SMALL: f32 = 288.0; + const NORMAL: f32 = 319.0; + const LARGE: f32 = 355.0; + const LARGER: f32 = 391.0; + const SMALL_SCALING_RATIO: f32 = 1.0; + const NORMAL_SCALING_RATIO: f32 = NORMAL / SMALL; + const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; + const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; + match uisize { + UISize::Small => SMALL_SCALING_RATIO, + UISize::Normal => NORMAL_SCALING_RATIO, + UISize::Large => LARGE_SCALING_RATIO, + UISize::Larger => LARGER_SCALING_RATIO, + } +} +/* +Just some random measurements to verify in the future (or write tests for :)) +with dpi enabled, there's some math involved it seems. +Linux -> +width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled +small -> 540 53 +normal -> 599 59 +large -> 667 65 +larger -> 734 72 + + +Windows -> +width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled. +small -> 540 53 +normal -> 599 59 +large -> 667 65 +larger -> 734 72 + +width 1914 pixels. height 2072 pixels. ratio 0.92. fov 1.01. scaling 3.0. dpi enabled. dpi 288 +small -> 538 52 +normal -> 598 58 +large -> 665 65 +larger -> 731 72 + +width 3840. height 2160. ratio 1.78. scaling 3. dpi true. dpi 288 (windowed fullscreen) +small -> 810 80 +normal -> 900 89 +large -> 1000 99 +larger -> 1100 109 + +width 1916 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 1.5. dpi enabled. dpi 144 +small -> 432 42 +normal -> 480 47 +large -> 533 52 +larger -> 586 57 + +width 1000 pixels. height 1000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. +small -> 281 26 +normal -> 312 29 +large -> 347 33 +larger -> 382 36 + +width 2000 pixels. height 1000 pixels. ratio 2. fov 1.01. scaling 2.0. dpi enabled. +small -> 375 36 +normal -> 416 40 +large -> 463 45 +larger -> 509 49 + +width 2000 pixels. height 2000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. +small -> 562 55 +normal -> 624 61 +large -> 694 68 +larger -> 764 75 + + +*/ + +impl Component for MenuPanelManager { + fn init(&mut self) {} + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = MenuPanelManagerChannels { + subscription_mumblelink: channels.requirements.remove(&0).unwrap(), + }; + self.channels = Some(channels); + } + fn accept_notifications(&self) -> bool { + false + } + fn flush_all_messages(&mut self) {} + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let egui_context = &self.egui_context; + let raw_link = { + let channels = self.channels.as_mut().unwrap(); + channels.subscription_mumblelink.try_recv().unwrap() + }; + let link_result: MumbleLinkResult = from_broadcast(&raw_link); + + let mut ui_scaling_factor = 1.0; + if let Some(link) = link_result.link.as_ref() { + let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { + (if link.dpi == 0 { 96.0 } else { link.dpi as f32 }) / 96.0 + } else { + 1.0 + }; + + ui_scaling_factor *= gw2_scale; + let uisz_scale = convert_uisz_to_scale(link.uisz); + ui_scaling_factor *= uisz_scale; + + let min_width = MINIMAL_WINDOW_WIDTH as f32 * gw2_scale; + let min_height = MINIMAL_WINDOW_HEIGHT as f32 * gw2_scale; + let gw2_width = link.client_size.0.x.max(MINIMAL_WINDOW_WIDTH) as f32; + let gw2_height = link.client_size.0.y.max(MINIMAL_WINDOW_HEIGHT) as f32; + let min_width_ratio = min_width.min(gw2_width) / min_width; + let min_height_ratio = min_height.min(gw2_height) / min_height; + + let min_ratio = min_height_ratio.min(min_width_ratio); + ui_scaling_factor *= min_ratio; + + let egui_scale = egui_context.pixels_per_point(); + ui_scaling_factor /= egui_scale; + } + + self.pos.x = ui_scaling_factor * (Self::WIDTH + 8.0); // add 8 pixels padding just for some space + self.ui_scaling_factor = ui_scaling_factor; + default_component_result() + } +} diff --git a/crates/jokolay/src/app/messages.rs b/crates/jokolay/src/app/messages.rs index 5d14d68..3a24aa1 100644 --- a/crates/jokolay/src/app/messages.rs +++ b/crates/jokolay/src/app/messages.rs @@ -1,3 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] pub enum MessageToApplicationBack { SaveUIConfiguration(String), } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 11815c5..9f7224c 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,73 +1,39 @@ use std::{ - io::Write, - ops::DerefMut, - sync::{Arc, Mutex}, + borrow::BorrowMut, + sync::{Arc, RwLock}, thread, }; use cap_std::fs_utf8::Dir; -use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; -use joko_plugin_manager::JokolayPlugin; +use egui_window_glfw_passthrough::{GlfwBackend, GlfwConfig}; +use joko_link_ui_manager::MumbleUIManager; mod init; -mod messages; -mod mumble; +mod menu; mod ui_parameters; -use crate::app::mumble::mumble_gui; +mod window; + use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; use init::{get_jokolay_dir, get_jokolay_path}; -use joko_component_manager::ComponentManager; -use joko_component_models::{from_data, JokolayComponent}; +use joko_component_manager::{ComponentExecutor, ComponentManager}; use joko_package_manager::{PackageDataManager, PackageUIManager}; use joko_link_manager::MumbleManager; -use joko_link_models::{ - MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult, UISize, -}; use joko_package_manager::jokolay_to_editable_path; -use joko_package_manager::ImportStatus; use joko_render_manager::renderer::JokoRenderer; use miette::{Context, IntoDiagnostic, Result}; -use tracing::{error, info, info_span}; - -use self::messages::MessageToApplicationBack; - -const MINIMAL_WINDOW_WIDTH: u32 = 640; -const MINIMAL_WINDOW_HEIGHT: u32 = 480; -const MINIMAL_WINDOW_POSITION_X: i32 = 0; -const MINIMAL_WINDOW_POSITION_Y: i32 = 0; - -pub struct JokolayUIState { - link: Option, - editable_mumble: bool, - window_changed: bool, - first_load_done: bool, - nb_running_tasks_on_back: i32, // store the number of running tasks in background thread - nb_running_tasks_on_network: i32, // store the number of running tasks (requests) in progress - import_status: Arc>, - maximal_window_width: u32, - maximal_window_height: u32, - root_path: std::path::PathBuf, -} +use tracing::{error, info_span}; + +use self::{menu::MenuPanelManager, window::WindowManager}; -struct JokolayApp { - mumble_manager: MumbleManager, - package_manager: PackageDataManager, -} struct JokolayGui { - ui_configuration: ui_parameters::JokolayUIConfiguration, - menu_panel: MenuPanel, - joko_renderer: JokoRenderer, + menu_panel: Arc>, egui_context: Arc, - glfw_backend: GlfwBackend, - theme_manager: ThemeManager, - mumble_manager: MumbleManager, - package_manager: PackageUIManager, + glfw_backend: Arc>, } #[allow(unused)] pub struct Jokolay { gui: Box, - app: Arc>>, - state_ui: JokolayUIState, + app: ComponentManager, } impl Jokolay { @@ -80,27 +46,24 @@ impl Jokolay { let mut component_manager = ComponentManager::new(); - let mumble_data_manager = - MumbleManager::new("MumbleLink", false).wrap_err("failed to create mumble manager")?; - let mumble_ui_manager = - MumbleManager::new("MumbleLink", true).wrap_err("failed to create mumble manager")?; - - let dummy_plugin = Box::new(JokolayPlugin {}); let _ = component_manager.register( "ui:mumble_link", - Box::new( + Arc::new(RwLock::new( MumbleManager::new("MumbleLink", true) .wrap_err("failed to create mumble manager")?, - ), + )), ); let _ = component_manager.register( "back:mumble_link", - Box::new( + Arc::new(RwLock::new( MumbleManager::new("MumbleLink", false) .wrap_err("failed to create mumble manager")?, - ), + )), ); - let _ = component_manager.register("dummy_plugin", dummy_plugin); + let egui_context = Arc::new(egui::Context::default()); + let mumble_ui = Arc::new(RwLock::new(MumbleUIManager::new(Arc::clone(&egui_context)))); + + let _ = component_manager.register("ui:mumbe_ui", mumble_ui.clone()); /* components can be migrated to plugins @@ -121,24 +84,21 @@ impl Jokolay { ... */ - let egui_context = Arc::new(egui::Context::default()); let _ = component_manager.register( "back:jokolay_package_manager", - Box::new(PackageDataManager::new( + Arc::new(RwLock::new(PackageDataManager::new( Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay - )?), + )?)), ); - let package_data_manager = PackageDataManager::new( - Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay - &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay - )?; - let mut theme_manager = - ThemeManager::new(Arc::clone(&root_dir)).wrap_err("failed to create theme manager")?; + let theme_manager = Arc::new(RwLock::new( + ThemeManager::new(Arc::clone(&root_dir), Arc::clone(&egui_context)) + .wrap_err("failed to create theme manager")?, + )); - theme_manager.init_egui(&egui_context); - let mut glfw_backend = GlfwBackend::new(GlfwConfig { + #[allow(clippy::arc_with_non_send_sync)] + let glfw_backend = Arc::new(RwLock::new(GlfwBackend::new(GlfwConfig { glfw_callback: Box::new(|glfw_context| { glfw_context.window_hint( egui_window_glfw_passthrough::glfw::WindowHint::SRgbCapable(true), @@ -154,47 +114,54 @@ impl Jokolay { transparent_window: Some(true), window_title: "Jokolay".to_string(), ..Default::default() - }); - - //retrieve current screen resolution - let video_mode = glfw_backend.glfw.with_primary_monitor(|_, m| { - if let Some(m) = m { - m.get_video_mode() - } else { - None - } - }); - let maximal_window_width = video_mode.unwrap().width; - let maximal_window_height = video_mode.unwrap().height; - + }))); let _ = component_manager.register( - "ui:jokolay_package_manager", - Box::new(PackageUIManager::new( - Arc::clone(&egui_context), - JokoRenderer::get_z_near(), - )), + "ui:window_manager", + Arc::new(RwLock::new(WindowManager::new(Arc::clone(&glfw_backend)))), ); - let mut package_ui_manager = - PackageUIManager::new(Arc::clone(&egui_context), JokoRenderer::get_z_near()); - glfw_backend.window.set_floating(true); - glfw_backend.window.set_decorated(false); + let package_manager_ui = Arc::new(RwLock::new(PackageUIManager::new( + Arc::clone(&egui_context), + JokoRenderer::get_z_near(), + ))); + let _ = + component_manager.register("ui:jokolay_package_manager", package_manager_ui.clone()); - let joko_renderer = JokoRenderer::new(&glfw_backend); let _ = component_manager.register( "ui:jokolay_renderer", - Box::new(JokoRenderer::new(&glfw_backend)), + Arc::new(RwLock::new(JokoRenderer::new( + Arc::clone(&glfw_backend), + Arc::clone(&egui_context), + ))), ); let editable_path = jokolay_to_editable_path(&root_path) .to_str() .unwrap() .to_string(); - let ui_configuration = ui_parameters::JokolayUIConfiguration::new( - glfw_backend.glfw.get_time() as _, + + let configuration_ui = Arc::new(RwLock::new(ui_parameters::JokolayUIConfiguration::new( + Arc::clone(&glfw_backend), + Arc::clone(&egui_context), editable_path.clone(), + root_path.to_str().unwrap().to_owned(), + ))); + let _ = component_manager.register("ui:configuration", configuration_ui.clone()); + + let _ = component_manager.register( + "back:configuration", + Arc::new(RwLock::new(ui_parameters::JokolayConfiguration::new( + Arc::clone(&root_dir), + ))), ); + let menu_panel = Arc::new(RwLock::new(MenuPanelManager::new( + Arc::clone(&glfw_backend), + Arc::clone(&egui_context), + ))); + + let _ = component_manager.register("ui:menu_panel", menu_panel.clone()); + match component_manager.build_routes() { Ok(_) => {} Err(e) => { @@ -202,125 +169,56 @@ impl Jokolay { } } - let menu_panel = MenuPanel::default(); + /* + Configuration + Themes + Mumble Manager + Package Manager + File Manader => where ? + + close all + exit + */ + if let Ok(mut menu_panel) = menu_panel.write() { + menu_panel.register(configuration_ui); + menu_panel.register(theme_manager); + menu_panel.register(mumble_ui); + menu_panel.register(package_manager_ui); + } - package_ui_manager.late_init(&egui_context); let gui = JokolayGui { - ui_configuration, - joko_renderer, glfw_backend, egui_context, menu_panel, - theme_manager, - mumble_manager: mumble_ui_manager, - package_manager: package_ui_manager, }; //let gui = Mutex::new(gui); //let gui = Arc::new(gui); let gui = Box::new(gui); - let state_ui = JokolayUIState { - link: Some(MumbleLink::default()), - editable_mumble: false, - window_changed: true, - first_load_done: false, - nb_running_tasks_on_back: 0, - nb_running_tasks_on_network: 0, - import_status: Default::default(), - maximal_window_width, //TODO: what happens if change of screen ? - maximal_window_height, - root_path, - }; Ok(Self { gui, - app: Arc::new(Mutex::new(Box::new(JokolayApp { - mumble_manager: mumble_data_manager, - package_manager: package_data_manager, - }))), - state_ui, + app: component_manager, }) } - fn start_background_loop( - app: Arc>>, - u2gb_receiver: std::sync::mpsc::Receiver, - ) { + fn start_background_loop(mut executor: ComponentExecutor) { let _background_thread = std::thread::spawn(move || { - // Load the directory with packages in the background process - { - //TODO: lazy loading to load maps only when on it - let mut app = app.lock().unwrap(); - let JokolayApp { - mumble_manager: _, - package_manager, - } = &mut app.deref_mut().as_mut(); - package_manager.load_all(); - } - let _ = Self::background_loop(Arc::clone(&app), u2gb_receiver); + tracing::info!("Initialize the background components"); + executor.init(); + let _ = Self::background_loop(executor); }); } - fn handle_app_message(root_dir: Arc, msg: MessageToApplicationBack) { - match msg { - MessageToApplicationBack::SaveUIConfiguration(serialized_string) => { - match root_dir.create(ui_parameters::UI_PARAMETERS_FILE_NAME) { - Ok(mut file) => { - match file.write(serialized_string.as_bytes()).into_diagnostic() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to save UI configuration"); - } - } - } - Err(e) => { - error!(?e, "failed to open UI configuration file"); - } - } - } - #[allow(unreachable_patterns)] - _ => { - unimplemented!("Handling BackToUIMessage has not been implemented yet"); - } - } - } - - fn background_loop( - app: Arc>>, - u2gb_receiver: std::sync::mpsc::Receiver, - ) -> Result<()> { + fn background_loop(mut executor: ComponentExecutor) -> Result<()> { tracing::info!("entering background event loop"); let _span_guard = info_span!("background event loop").entered(); let mut loop_index: u128 = 0; let start = std::time::SystemTime::now(); loop { tracing::trace!("background loop tick: {}", loop_index); - let mut app = app.lock().unwrap(); - let JokolayApp { - mumble_manager, - package_manager, - } = &mut app.deref_mut().as_mut(); - - /* - TODO: for each plugin, run it from the ones without any dep to those that require those values => depgraph of plugins - - back-end deps: - package_manager -requires-> link - - front-end deps: - render -requires-> package_manager - package_manager -requires-> link - */ - while let Ok(msg) = u2gb_receiver.try_recv() { - Self::handle_app_message(Arc::clone(&package_manager.state.root_dir), msg); - } - - mumble_manager.flush_all_messages(); - let latest_time = start.elapsed().into_diagnostic()?.as_secs_f64(); - let mumble_link_result = mumble_manager.tick(latest_time); //TODO: in Component manager, make use of this value - package_manager.flush_all_messages(); - package_manager.tick(latest_time); + executor.tick(latest_time); - thread::sleep(std::time::Duration::from_millis(10)); + thread::sleep(std::time::Duration::from_millis(100)); loop_index += 1; } #[allow(unreachable_code)] @@ -330,302 +228,64 @@ impl Jokolay { } } - pub fn enter_event_loop(self) { - //TODO: all .tick() functions should have the same interface - /* - TODO: proper routing of a package to another - when loading a plugin, there is a relationship defined with another: either "requires" or "bind" or "notify" - - In case of "bind" the other plugin has to agree with it. - - In case of "requires" then the output of both "flush_all_messages" and "tick" of said requirement shall be passed to the plugin. - - In case of "notify" then a channel to send message is open. - => no loop when registering - => check for missing dep - => when a value is pushed it should be a broadcast (an immutable ref for each consumer), trashed at end of the loop. - => https://docs.rs/tokio/latest/tokio/sync/broadcast/ - channels for notifications should carry the source. One cannot trust the source since they are third part. Or accept to not know the source. Hence in the contract, can be ignored. - in a flush_all_messages, input notification must be drained - Name of the plugin defines the feature/service it provides. If a replacement is wished, one need to overwrite the plugin with another provider. - - Once validated all works properly with the existing code, we can create a PluginManager and PluginInstance, with each instance of the later being a rust wrapper around some plugin definition. - It'll act as the interface between our code and plugin world. It is basically an overhead which could be optimized later. - */ - - let (u2gb_sender, u2gb_receiver) = std::sync::mpsc::channel(); - let (u2mb_sender, u2mb_receiver) = tokio::sync::mpsc::channel(1); //FIXME: route the data to the consumers. - - Self::start_background_loop(Arc::clone(&self.app), u2gb_receiver); + pub fn enter_event_loop(&mut self) { + // do all the non-gui stuff + Self::start_background_loop(self.app.executor("back")); tracing::info!("entering glfw event loop"); let span_guard = info_span!("glfw event loop").entered(); - let mut gui = *self.gui; - let mut local_state = self.state_ui; + let mut ui_executor = self.app.executor("ui"); + + ui_executor.init(); loop { //TODO: one could wrap the egui_context into a plugin result so that it can be used from other plugins //TODO: same for the UI as a notified element. let JokolayGui { - ui_configuration, menu_panel, - joko_renderer, egui_context, glfw_backend, - theme_manager, - mumble_manager, - package_manager, - } = &mut gui; - let latest_time = glfw_backend.glfw.get_time(); - - let etx = Arc::clone(&egui_context); - - /* - if etx.input(|i| { - TODO: - handle shortcuts - a module publish a list of shortcuts - At import, user need to accept those. - We can't have a module that is a keyboard listener. - - modifiers are not forwarded. - println!("{:?} {:?}", i.keys_down, i.modifiers); - false - }) { - } - */ - - // gather events - glfw_backend.glfw.poll_events(); - glfw_backend.tick(); + } = &mut self.gui.borrow_mut(); + + let latest_time = { + let mut glfw_backend = glfw_backend.write().unwrap(); + let latest_time = glfw_backend.glfw.get_time(); + + // gather events + glfw_backend.glfw.poll_events(); + glfw_backend.tick(); + if glfw_backend.window.should_close() { + tracing::warn!("should close is true. So, exiting event loop"); + break; + } + let mut input = glfw_backend.take_raw_input(); + input.time = Some(latest_time); - if glfw_backend.window.should_close() { - tracing::warn!("should close is true. So, exiting event loop"); - break; - } + egui_context.begin_frame(input); + latest_time + }; + ui_executor.tick(latest_time); - if glfw_backend.resized_event_pending { - let latest_size = glfw_backend.window.get_framebuffer_size(); - let latest_size = [latest_size.0 as _, latest_size.1 as _]; - - glfw_backend.framebuffer_size_physical = latest_size; - glfw_backend.window_size_logical = [ - latest_size[0] as f32 / glfw_backend.scale, - latest_size[1] as f32 / glfw_backend.scale, - ]; - joko_renderer.resize_framebuffer(latest_size); - glfw_backend.resized_event_pending = false; - } - joko_renderer.prepare_frame(|| { - let latest_size = glfw_backend.window.get_framebuffer_size(); - tracing::info!( - ?latest_size, - "failed to get surface texture, so calling latest framebuffer size" - ); - let latest_size = [latest_size.0 as _, latest_size.1 as _]; - glfw_backend.framebuffer_size_physical = latest_size; - glfw_backend.window_size_logical = [ - latest_size[0] as f32 / glfw_backend.scale, - latest_size[1] as f32 / glfw_backend.scale, - ]; - latest_size - }); - - let mut input = glfw_backend.take_raw_input(); - input.time = Some(latest_time); - - etx.begin_frame(input); - - // do all the non-gui stuff first - ui_configuration.tick(latest_time); - if local_state.editable_mumble { - local_state.window_changed = true; - local_state.link.as_mut().unwrap().changes = enumflags2::BitFlags::all(); - let _ = u2mb_sender.send(MessageToMumbleLinkBack::Value(local_state.link.clone())); + if let Ok(mut menu_panel) = menu_panel.write() { + menu_panel.gui(); + JokolayTracingLayer::gui(egui_context, &mut menu_panel.show_tracing_window); + //TODO: make it depend on window manager or menu_panel ? } else { - let is_mumble_alive = mumble_manager.is_alive(); - let data = mumble_manager.tick(latest_time); - let res: MumbleLinkResult = from_data(data); - match &res.link { - Some(link) => { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - local_state.window_changed = true; - } - if is_mumble_alive { - local_state.link = Some(link.clone()); - } - } - _ => { - error!("mumble manager tick error"); - } - } + println!("cannot update GUI due to lock issues"); } - - // check if we need to change window position or size. - if let Some(link) = local_state.link.as_ref() { - if local_state.window_changed { - let client_pos = &link.client_pos.0; - let client_size = &link.client_size.0; - glfw_backend.window.set_pos( - client_pos.x.max(MINIMAL_WINDOW_POSITION_X), - client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), - ); - // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. - // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) - // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - let client_size_x = MINIMAL_WINDOW_WIDTH - .max(client_size.x) - .min(local_state.maximal_window_width); - let client_size_y = MINIMAL_WINDOW_HEIGHT - .max(client_size.y) - .min(local_state.maximal_window_height); - glfw_backend - .window - .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); - } - package_manager.tick(latest_time); - local_state.window_changed = false; - } - - joko_renderer.tick(latest_time); - menu_panel.tick(&etx, local_state.link.as_ref()); - - // do the gui stuff now - egui::Area::new("menu panel") - .fixed_pos(menu_panel.pos) - .interactable(true) - .order(egui::Order::Foreground) - .show(&etx, |ui| { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = - egui::Color32::TRANSPARENT; - ui.horizontal(|ui| { - //TODO: if any displayed, show an additional "hide all" - ui.menu_button( - egui::RichText::new("JKL") - .size((MenuPanel::HEIGHT - 2.0) * menu_panel.ui_scaling_factor) - .background_color(egui::Color32::TRANSPARENT), - |ui| { - ui.checkbox( - &mut menu_panel.show_parameters_manager, - "Configuration", - ); - ui.checkbox(&mut menu_panel.show_theme_window, "Themes"); - ui.checkbox( - &mut menu_panel.show_package_manager_window, - "Package Manager", - ); - ui.checkbox( - &mut menu_panel.show_mumble_manager_window, - "Mumble Manager", - ); - ui.checkbox( - &mut menu_panel.show_file_manager_window, - "File Manager", - ); - //ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); - if (menu_panel.show_parameters_manager - || menu_panel.show_package_manager_window - || menu_panel.show_mumble_manager_window - || menu_panel.show_theme_window - || menu_panel.show_file_manager_window - || menu_panel.show_tracing_window) - && ui.button("Close all panels").clicked() - { - menu_panel.show_parameters_manager = false; - menu_panel.show_package_manager_window = false; - menu_panel.show_mumble_manager_window = false; - menu_panel.show_theme_window = false; - menu_panel.show_file_manager_window = false; - menu_panel.show_tracing_window = false; - } - if ui.button("exit").clicked() { - info!("exiting jokolay"); - glfw_backend.window.set_should_close(true); - } - }, - ); - package_manager.menu_ui( - ui, - local_state.nb_running_tasks_on_back, - local_state.nb_running_tasks_on_network, - ); - }); - }); - - if let Some(link) = local_state.link.as_mut() { - mumble_gui( - &u2mb_sender, - &etx, - &mut menu_panel.show_mumble_manager_window, - &mut local_state.editable_mumble, - link, - ); - }; - package_manager.gui( - &etx, - &mut menu_panel.show_package_manager_window, - &local_state.import_status, - &mut menu_panel.show_file_manager_window, - local_state.first_load_done, - ); - JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); - theme_manager.gui(&etx, &mut menu_panel.show_theme_window); - ui_configuration.gui( - &u2gb_sender, - &etx, - glfw_backend, - &mut menu_panel.show_parameters_manager, - &local_state.root_path, - ); // show notifications - JokolayTracingLayer::show_notifications(&etx); + JokolayTracingLayer::show_notifications(egui_context); // end gui stuff - etx.request_repaint(); - - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - .. - } = etx.end_frame(); - - if !platform_output.copied_text.is_empty() { - glfw_backend - .window - .set_clipboard_string(&platform_output.copied_text); - } + egui_context.request_repaint(); - // if it doesn't require either keyboard or pointer, set passthrough to true - glfw_backend - .window - .set_mouse_passthrough(!(etx.wants_keyboard_input() || etx.wants_pointer_input())); - //TODO: view from above when map is open /* - TODO: have a clean view when game is not focused. - let mut do_draw = local_state.editable_mumble; - if !do_draw { - if let Some(link) = local_state.link.as_ref() { - if let Some(ui_state) = link.ui_state { - do_draw = ui_state.contains(UIState::GameHasFocus) - } - }; - }*/ - let animation_time = if ui_configuration.display_parameters.animate { latest_time } else { 0.0 - }; - - joko_renderer.render_egui( - etx.tessellate(shapes, etx.pixels_per_point()), - textures_delta, - glfw_backend.window_size_logical, - animation_time, - ); - joko_renderer.present(); - glfw_backend.window.swap_buffers(); + };*/ } drop(span_guard); } @@ -662,7 +322,7 @@ pub fn start_jokolay() { } match Jokolay::new(jokolay_dir.into(), jokolay_path) { - Ok(jokolay) => { + Ok(mut jokolay) => { jokolay.enter_event_loop(); } Err(e) => { @@ -671,165 +331,3 @@ pub fn start_jokolay() { }; std::mem::drop(log_file_flush_guard); } - -/// Guild Wars 2 has an array of menu icons on top left corner of the game. -/// Its size is affected by four different factors -/// 1. UISZ: -/// This is a setting in graphics options of gw2 and it comes in 4 variants -/// small, normal, large and larger. -/// This is something we can get from mumblelink's context. -/// 2. DPI scaling -/// This is a setting in graphics options too. When scaling is enabled, sizes of menu become bigger according to the dpi of gw2 window -/// This is something we get from gw2's config file in AppData/Roaming and store in mumble link as dpi scaling -/// We also get dpi of gw2 window and store it in mumble link. -/// 3. Dimensions of the gw2 window -/// This is something we get from mumble link and win32 api. We store this as client pos/size in mumble link -/// It is not just the width or height, but their ratio to the 1024x768 resolution -/// -/// 1. By default, with dpi 96 (scale 1.0), at resolution 1024x768 these are the sizes of menu at different uisz settings -/// UISZ -> WIDTH HEIGHT -/// small -> 288 27 -/// normal -> 319 31 -/// large -> 355 34 -/// larger -> 391 38 -/// all units are in raw pixels. -/// -/// If we think of small uisz as the default. Then, we can express the rest of the sizes as ratio to small. -/// small = 1.0 -/// normal = 1.1 -/// large = 1.23 -/// larger = 1.35 -/// -/// So, just multiply small (288) with these ratios to get the actual pixels of each uisz. -/// 2. When dpi doubles, so do the sizes. 288 -> 576, 319 -> 638 etc.. So, when dpi scaling is enabled, we must multiply the above uisz ratio with dpi scale ratio to get the combined scaling ratio. -/// 3. The dimensions thing is a little complicated. So, i will just list the actual steps here. -/// 1. take gw2's actual width in raw pixels. lets call this gw2_width. -/// 2. take 1024 as reference minimum width. If dpi scaling is enabled, multiply 1024 * dpi scaling ratio. lets call this reference_width. -/// 3. Now, get the smaller value out of the two. lets call this minimum_width. -/// 4. finally, do (minimum_width / reference_width) to get "width scaling ratio". -/// 5. repeat steps 1 - 4, but for height. use 768 as the reference width (with approapriate dpi scaling). -/// 6. now just take the minimum of "width scaling ratio" and "height scaling ratio". lets call this "aspect ratio scaling". -/// -/// Finally, just multiply the width 288 or height 27 with these three values. -/// eg: menu width = 288 * uisz_ratio * dpi_scaling_ratio * aspect_ratio_scaling; -/// do the same with 288 replaced by 27 for height. -#[derive(Debug, Default)] -pub struct MenuPanel { - pub pos: egui::Pos2, - pub ui_scaling_factor: f32, - show_tracing_window: bool, - show_theme_window: bool, - // show_settings_window: bool, - show_package_manager_window: bool, - show_mumble_manager_window: bool, - show_parameters_manager: bool, - show_file_manager_window: bool, -} - -impl MenuPanel { - pub const WIDTH: f32 = 288.0; - pub const HEIGHT: f32 = 27.0; - pub fn tick(&mut self, etx: &egui::Context, link: Option<&MumbleLink>) { - let mut ui_scaling_factor = 1.0; - if let Some(link) = link.as_ref() { - let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { - (if link.dpi == 0 { 96.0 } else { link.dpi as f32 }) / 96.0 - } else { - 1.0 - }; - - ui_scaling_factor *= gw2_scale; - let uisz_scale = convert_uisz_to_scale(link.uisz); - ui_scaling_factor *= uisz_scale; - - let min_width = MINIMAL_WINDOW_WIDTH as f32 * gw2_scale; - let min_height = MINIMAL_WINDOW_HEIGHT as f32 * gw2_scale; - let gw2_width = link.client_size.0.x.max(MINIMAL_WINDOW_WIDTH) as f32; - let gw2_height = link.client_size.0.y.max(MINIMAL_WINDOW_HEIGHT) as f32; - let min_width_ratio = min_width.min(gw2_width) / min_width; - let min_height_ratio = min_height.min(gw2_height) / min_height; - - let min_ratio = min_height_ratio.min(min_width_ratio); - ui_scaling_factor *= min_ratio; - - let egui_scale = etx.pixels_per_point(); - ui_scaling_factor /= egui_scale; - } - - self.pos.x = ui_scaling_factor * (Self::WIDTH + 8.0); // add 8 pixels padding just for some space - self.ui_scaling_factor = ui_scaling_factor; - } -} - -fn convert_uisz_to_scale(uisize: UISize) -> f32 { - const SMALL: f32 = 288.0; - const NORMAL: f32 = 319.0; - const LARGE: f32 = 355.0; - const LARGER: f32 = 391.0; - const SMALL_SCALING_RATIO: f32 = 1.0; - const NORMAL_SCALING_RATIO: f32 = NORMAL / SMALL; - const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; - const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; - match uisize { - UISize::Small => SMALL_SCALING_RATIO, - UISize::Normal => NORMAL_SCALING_RATIO, - UISize::Large => LARGE_SCALING_RATIO, - UISize::Larger => LARGER_SCALING_RATIO, - } -} -/* -Just some random measurements to verify in the future (or write tests for :)) -with dpi enabled, there's some math involved it seems. -Linux -> -width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled -small -> 540 53 -normal -> 599 59 -large -> 667 65 -larger -> 734 72 - - -Windows -> -width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled. -small -> 540 53 -normal -> 599 59 -large -> 667 65 -larger -> 734 72 - -width 1914 pixels. height 2072 pixels. ratio 0.92. fov 1.01. scaling 3.0. dpi enabled. dpi 288 -small -> 538 52 -normal -> 598 58 -large -> 665 65 -larger -> 731 72 - -width 3840. height 2160. ratio 1.78. scaling 3. dpi true. dpi 288 (windowed fullscreen) -small -> 810 80 -normal -> 900 89 -large -> 1000 99 -larger -> 1100 109 - -width 1916 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 1.5. dpi enabled. dpi 144 -small -> 432 42 -normal -> 480 47 -large -> 533 52 -larger -> 586 57 - -width 1000 pixels. height 1000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. -small -> 281 26 -normal -> 312 29 -large -> 347 33 -larger -> 382 36 - -width 2000 pixels. height 1000 pixels. ratio 2. fov 1.01. scaling 2.0. dpi enabled. -small -> 375 36 -normal -> 416 40 -large -> 463 45 -larger -> 509 49 - -width 2000 pixels. height 2000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. -small -> 562 55 -normal -> 624 61 -large -> 694 68 -larger -> 764 75 - - -*/ diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs index 027fd3e..9477273 100644 --- a/crates/jokolay/src/app/ui_parameters.rs +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -1,32 +1,72 @@ +use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::GlfwBackend; +use std::{ + io::Write, + sync::{Arc, RwLock}, +}; +use joko_component_models::{ + default_component_result, from_data, to_data, Component, ComponentMessage, ComponentResult, +}; +use joko_ui_models::{UIArea, UIPanel}; +use miette::IntoDiagnostic; use serde::{Deserialize, Serialize}; - -use super::messages::MessageToApplicationBack; +use tracing::error; pub const UI_PARAMETERS_FILE_NAME: &str = "ui.toml"; +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToApplicationBack { + SaveUIConfiguration(String), +} + #[derive(Serialize, Deserialize)] pub struct JokolayUIParameters { pub visible_borders: bool, pub animate: bool, pub editable_path: String, + pub root_path: String, //TODO: folder path for custom work directory //save configuration into a file + make backups of configuration } +struct JokolayUIConfigurationChannels { + back_end_notifier: tokio::sync::mpsc::Sender, +} +struct JokolayConfigurationChannels { + notification_receiver: tokio::sync::mpsc::Receiver, +} + pub struct JokolayUIConfiguration { pub fps_last_reset: f64, pub frame_count: u32, pub total_frame_count: u32, pub average_fps: u32, pub display_parameters: JokolayUIParameters, + glfw_backend: Arc>, + egui_context: Arc, + channels: Option, +} + +pub struct JokolayConfiguration { + root_dir: Arc, + channels: Option, } +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for JokolayUIConfiguration {} +unsafe impl Sync for JokolayUIConfiguration {} + impl JokolayUIConfiguration { - pub fn new(current_time: f64, editable_path: String) -> Self { + pub fn new( + glfw_backend: Arc>, + egui_context: Arc, + editable_path: String, + root_path: String, + ) -> Self { + let fps_last_reset: f64 = { glfw_backend.read().unwrap().glfw.get_time() as _ }; Self { - fps_last_reset: current_time, + fps_last_reset, frame_count: 0, total_frame_count: 0, average_fps: 0, @@ -34,11 +74,29 @@ impl JokolayUIConfiguration { visible_borders: false, animate: true, editable_path, + root_path, }, + glfw_backend, + egui_context, + channels: None, } } +} - pub fn tick(&mut self, current_time: f64) { +impl Component for JokolayUIConfiguration { + fn accept_notifications(&self) -> bool { + true + } + fn bind(&mut self, mut channels: joko_component_models::ComponentChannels) { + let back_end_notifier = channels.notify.remove(&0).unwrap(); + let channels = JokolayUIConfigurationChannels { back_end_notifier }; + self.channels = Some(channels) + } + fn flush_all_messages(&mut self) {} + + fn init(&mut self) {} + + fn tick(&mut self, current_time: f64) -> ComponentResult { self.total_frame_count += 1; self.frame_count += 1; if current_time - self.fps_last_reset > 1.0 { @@ -46,20 +104,95 @@ impl JokolayUIConfiguration { self.frame_count = 0; self.fps_last_reset = current_time; } + default_component_result() + } + fn notify(&self) -> Vec<&str> { + vec!["back:configuration"] + } +} + +impl JokolayConfiguration { + pub fn new(root_dir: Arc) -> Self { + Self { + root_dir, + channels: None, + } + } + fn handle_message(&mut self, msg: MessageToApplicationBack) { + let root_dir = &self.root_dir; + match msg { + MessageToApplicationBack::SaveUIConfiguration(serialized_string) => { + match root_dir.create(UI_PARAMETERS_FILE_NAME) { + Ok(mut file) => { + match file.write(serialized_string.as_bytes()).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to save UI configuration"); + } + } + } + Err(e) => { + error!(?e, "failed to open UI configuration file"); + } + } + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling BackToUIMessage has not been implemented yet"); + } + } + } +} + +impl Component for JokolayConfiguration { + fn accept_notifications(&self) -> bool { + true + } + fn init(&mut self) {} + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + fn bind(&mut self, channels: joko_component_models::ComponentChannels) { + let channels = JokolayConfigurationChannels { + notification_receiver: channels.input_notification.unwrap(), + }; + self.channels = Some(channels); + } + fn tick(&mut self, _latest_time: f64) -> joko_component_models::ComponentResult { + default_component_result() + } +} + +impl UIPanel for JokolayUIConfiguration { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Configuration".to_string(), + id: "configuration_ui".to_string(), + }] } + fn init(&mut self) {} - pub fn gui( - &mut self, - u2b_sender: &std::sync::mpsc::Sender, - etx: &egui::Context, - wb: &mut GlfwBackend, - open: &mut bool, - root_path: &std::path::Path, - ) { + fn gui(&mut self, is_open: &mut bool, _area_id: &str) { + let channels = self.channels.as_mut().unwrap(); + let u2b_sender = &channels.back_end_notifier; + let glfw_backend = Arc::clone(&self.glfw_backend); + let mut glfw_backend = glfw_backend.as_ref().write().unwrap(); let mut need_to_save = false; egui::Window::new("Configuration") - .open(open) - .show(etx, |ui| { + .open(is_open) + .show(&self.egui_context, |ui| { egui::Grid::new("frame details") .num_columns(2) .show(ui, |ui| { @@ -72,28 +205,28 @@ impl JokolayUIConfiguration { ui.label("Overlay position"); ui.label(&format!( "x: {}; y: {}", - wb.window_position[0], wb.window_position[1] + glfw_backend.window_position[0], glfw_backend.window_position[1] )); ui.end_row(); ui.label("Overlay size"); ui.label(&format!( "width: {}, height: {}", - wb.framebuffer_size_physical[0], wb.framebuffer_size_physical[1] + glfw_backend.framebuffer_size_physical[0], glfw_backend.framebuffer_size_physical[1] )); ui.end_row(); ui.label("Decorations (borders)") .on_hover_text("Should the jokolay overlay window boreders be displayed"); - let is_decorated = wb.window.is_decorated(); + let is_decorated = glfw_backend.window.is_decorated(); ui.horizontal(|ui|{ let result = is_decorated; if ui.selectable_label(result, "Visible").clicked() { - wb.window.set_decorated(true); + glfw_backend.window.set_decorated(true); self.display_parameters.visible_borders = true; need_to_save = true; } if ui.selectable_label(!result, "Hidden").clicked() { - wb.window.set_decorated(false); + glfw_backend.window.set_decorated(false); self.display_parameters.visible_borders = false; need_to_save = true; } @@ -114,7 +247,7 @@ impl JokolayUIConfiguration { }); ui.end_row(); ui.label("All files and preferences are saved into:"); - ui.label(root_path.to_str().unwrap()); + ui.label(&self.display_parameters.root_path); ui.end_row(); ui.label("Editable package directory") @@ -125,8 +258,8 @@ impl JokolayUIConfiguration { if need_to_save { match toml::to_string(&self.display_parameters) { Ok(serialized_string) => { - let _ = u2b_sender.send(MessageToApplicationBack::SaveUIConfiguration( - serialized_string, + let _ = u2b_sender.blocking_send(to_data( + MessageToApplicationBack::SaveUIConfiguration(serialized_string), )); } Err(e) => { diff --git a/crates/jokolay/src/app/window.rs b/crates/jokolay/src/app/window.rs new file mode 100644 index 0000000..77eab99 --- /dev/null +++ b/crates/jokolay/src/app/window.rs @@ -0,0 +1,147 @@ +use std::sync::{Arc, RwLock}; + +use egui_window_glfw_passthrough::GlfwBackend; +use joko_component_models::{ + default_component_result, from_broadcast, to_data, Component, ComponentChannels, + ComponentMessage, ComponentResult, +}; +use joko_link_models::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult}; + +pub(crate) const MINIMAL_WINDOW_WIDTH: u32 = 640; +pub(crate) const MINIMAL_WINDOW_HEIGHT: u32 = 480; +pub(crate) const MINIMAL_WINDOW_POSITION_X: i32 = 0; +pub(crate) const MINIMAL_WINDOW_POSITION_Y: i32 = 0; + +struct WindowManagerChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, + mumble_link_back_notifier: tokio::sync::mpsc::Sender, +} +pub(crate) struct WindowManager { + glfw_backend: Arc>, + window_changed: bool, + maximal_window_width: u32, + maximal_window_height: u32, + editable_mumble: bool, + last_known_link: Option, + channels: Option, +} + +impl WindowManager { + pub fn new(glfw_backend: Arc>) -> Self { + //retrieve current screen resolution + let video_mode = glfw_backend + .write() + .unwrap() + .glfw + .with_primary_monitor(|_, m| { + if let Some(m) = m { + m.get_video_mode() + } else { + None + } + }); + let maximal_window_width = video_mode.unwrap().width; + let maximal_window_height = video_mode.unwrap().height; + + glfw_backend.write().unwrap().window.set_floating(true); + glfw_backend.write().unwrap().window.set_decorated(false); + + Self { + glfw_backend, + window_changed: true, + maximal_window_width, + maximal_window_height, + editable_mumble: false, + last_known_link: None, + channels: None, + } + } +} + +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for WindowManager {} +unsafe impl Sync for WindowManager {} + +impl Component for WindowManager { + fn accept_notifications(&self) -> bool { + true + } + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = WindowManagerChannels { + subscription_mumblelink: channels.requirements.remove(&0).unwrap(), + mumble_link_back_notifier: channels.notify.remove(&1).unwrap(), + }; + + self.channels = Some(channels); + } + fn flush_all_messages(&mut self) { + //unimplemented!() + } + fn init(&mut self) { + //unimplemented!() + } + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] // is it ? + } + fn notify(&self) -> Vec<&str> { + vec!["back:mumble_link"] + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + if self.editable_mumble { + if let Some(last_known_link) = &mut self.last_known_link { + self.window_changed = true; + last_known_link.changes = enumflags2::BitFlags::all(); + let _ = channels.mumble_link_back_notifier.blocking_send(to_data( + MessageToMumbleLinkBack::Value(Some(last_known_link.clone())), + )); + } + } else if let Ok(data) = channels.subscription_mumblelink.try_recv() { + let res: MumbleLinkResult = from_broadcast(&data); + match res.link { + Some(link) => { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + self.window_changed = true; + } + self.last_known_link = Some(link); + } + _ => { + //error!("WindowManager manager tick error, MumbleLink link data, nothing found"); + } + } + } else { + println!("WindowManager: No data from mumble"); + } + if let Some(last_known_link) = &mut self.last_known_link { + if self.window_changed { + let client_pos = &last_known_link.client_pos.0; + let client_size = &last_known_link.client_size.0; + let mut glfw_backend = self.glfw_backend.write().unwrap(); + glfw_backend.window.set_pos( + client_pos.x.max(MINIMAL_WINDOW_POSITION_X), + client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), + ); + // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. + // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) + // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + let client_size_x = MINIMAL_WINDOW_WIDTH + .max(client_size.x) + .min(self.maximal_window_width); + let client_size_y = MINIMAL_WINDOW_HEIGHT + .max(client_size.y) + .min(self.maximal_window_height); + glfw_backend + .window + .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); + } + self.window_changed = false; + } + default_component_result() + } +} diff --git a/crates/jokolay/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs index 0483e58..5933994 100644 --- a/crates/jokolay/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, io::Read, sync::Arc}; use cap_std::fs_utf8::Dir; use egui::Style; +use joko_ui_models::{UIArea, UIPanel}; use miette::{Context, IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; @@ -13,6 +14,7 @@ pub struct ThemeManager { fonts: BTreeMap>, config: ThemeManagerConfig, ui_data: ThemeUIData, + egui_context: Arc, } #[derive(Debug, Default)] @@ -52,7 +54,7 @@ impl ThemeManager { const DEFAULT_FONT_NAME: &'static str = "default"; const DEFAULT_THEME_NAME: &'static str = "default"; const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; - pub fn new(jokolay_dir: Arc) -> Result { + pub fn new(jokolay_dir: Arc, egui_context: Arc) -> Result { jokolay_dir .create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() @@ -197,9 +199,21 @@ impl ThemeManager { fonts, config, ui_data: Default::default(), + egui_context, }) } - pub fn init_egui(&mut self, etx: &egui::Context) { +} + +impl UIPanel for ThemeManager { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Themes".to_string(), + id: "themes_ui".to_string(), + }] + } + fn init(&mut self) { + let egui_context = &mut self.egui_context; let mut fonts = egui::FontDefinitions::default(); for (name, font_data) in self.fonts.iter() { fonts.font_data.insert( @@ -207,19 +221,19 @@ impl ThemeManager { egui::FontData::from_owned(font_data.to_owned()), ); } - etx.set_fonts(fonts); + egui_context.set_fonts(fonts); if let Some(theme) = self.themes.get(&self.config.default_theme) { - etx.set_style(theme.style.clone()); + egui_context.set_style(theme.style.clone()); } else { error!(%self.config.default_theme, "failed to find the default theme in the loaded themes :("); } } - - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { + fn gui(&mut self, is_open: &mut bool, _area_id: &str) { + let egui_context = &mut self.egui_context; egui::Window::new("Theme Manager") - .open(open) + .open(is_open) .scroll2([false, true]) - .show(etx, |ui| { + .show(egui_context, |ui| { ui.horizontal(|ui| { ui.selectable_value( &mut self.ui_data.tab, @@ -240,7 +254,7 @@ impl ThemeManager { .on_hover_text("save this theme with the above name") .clicked() { - let style = etx.style().as_ref().clone(); + let style = egui_context.style().as_ref().clone(); let theme = Theme { style }; let theme_name = self.ui_data.theme_name.clone(); match serde_json::to_string_pretty(&theme) { @@ -276,7 +290,7 @@ impl ThemeManager { } self.themes.insert(theme_name, theme); } - etx.style_ui(ui); + egui_context.style_ui(ui); }); } ThemeUITab::Config => { @@ -315,7 +329,7 @@ impl ThemeManager { .clicked() && !checked { - etx.set_style(theme.style.clone()); + egui_context.set_style(theme.style.clone()); } } }); From c21eb84c95366f9ef71edf8596aed889f7778e27 Mon Sep 17 00:00:00 2001 From: moi Date: Wed, 8 May 2024 20:34:06 +0200 Subject: [PATCH 50/54] remove useless Arc (egui::Context is already one) + start of documentation for components interaction --- Cargo.lock | 6 +- crates/joko_component_manager/src/lib.rs | 24 +- crates/joko_link_models/src/lib.rs | 2 +- crates/joko_link_ui_manager/src/lib.rs | 20 +- crates/joko_package_manager/Cargo.toml | 1 - .../src/io/deserialize.rs | 52 ++--- .../src/manager/pack/active.rs | 10 +- .../src/manager/pack/category_selection.rs | 6 +- .../src/manager/pack/loaded.rs | 208 ++++++++---------- .../src/manager/package_data.rs | 125 ++++++----- .../src/manager/package_ui.rs | 57 ++--- crates/joko_package_manager/src/message.rs | 1 + crates/joko_package_models/src/package.rs | 6 +- crates/joko_render_manager/Cargo.toml | 1 + crates/joko_render_manager/src/billboard.rs | 14 ++ crates/joko_render_manager/src/renderer.rs | 71 ++++-- crates/joko_render_models/src/messages.rs | 4 +- crates/joko_ui_models/src/lib.rs | 3 +- crates/jokolay/src/app/init.rs | 2 - crates/jokolay/src/app/menu.rs | 24 +- crates/jokolay/src/app/mod.rs | 45 ++-- crates/jokolay/src/app/ui_parameters.rs | 11 +- crates/jokolay/src/manager/theme/mod.rs | 6 +- documentation/diagrams/category_change.dotuml | 45 ++++ 24 files changed, 408 insertions(+), 336 deletions(-) create mode 100644 documentation/diagrams/category_change.dotuml diff --git a/Cargo.lock b/Cargo.lock index 056331a..51ecc55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1399,9 +1399,9 @@ dependencies = [ [[package]] name = "io-extras" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c301e73fb90e8a29e600a9f402d095765f74310d582916a952f618836a1bd1ed" +checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b" dependencies = [ "io-lifetimes", "windows-sys 0.52.0", @@ -1544,7 +1544,6 @@ version = "0.2.1" dependencies = [ "base64", "bytemuck", - "cap-std", "cxx", "cxx-build", "data-encoding", @@ -1633,6 +1632,7 @@ dependencies = [ "joko_component_models", "joko_link_models", "joko_render_models", + "joko_ui_models", "tokio", "tracing", ] diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index 2163cec..0a6eaf6 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -32,12 +32,14 @@ struct ComponentHandle { component: Arc>, channels: ComponentChannels, relations_to_ids: HashMap, + nb_call: u128, + execution_time: std::time::Duration, } pub struct ComponentExecutor { world: String, broadcasters: HashMap>, - components: Vec, //FIXME: how to type erase result ? + components: Vec, has_been_initialized: bool, } @@ -159,6 +161,8 @@ impl ComponentManager { component, channels: ComponentChannels::default(), relations_to_ids, + nb_call: 0, + execution_time: Default::default(), }; self.known_components .insert(component_name.to_string(), handle); @@ -168,7 +172,6 @@ impl ComponentManager { pub fn executor(&mut self, world: &str) -> ComponentExecutor { /* - TODO: extract the list of components of this world bind them insert them into the executor @@ -224,7 +227,7 @@ impl ComponentManager { let node_id = depgraph.add_node(component_name.clone()); known_services.insert(component_name.clone(), node_id); if component.accept_notifications() { - let (sender, receiver) = tokio::sync::mpsc::channel(1000); + let (sender, receiver) = tokio::sync::mpsc::channel(10000); handle.channels.input_notification = Some(receiver); notifications.insert(component_name.clone(), sender); } @@ -359,10 +362,6 @@ impl ComponentManager { invocation order */ - //TODO: channels are part of each component handle, all that remains is insert them - /*for (node_id, _) in translation.iter() { - peers_channels.insert(node_id.clone(), tokio::sync::mpsc::channel(1000)); - }*/ for node_id in depgraph.node_indices() { let notify_rel = depgraph .edges(node_id) @@ -397,8 +396,8 @@ impl ComponentManager { // we shall overwrite the channels, but this is ok since we are not using them yet. // TODO: if in the future there is dynamic loading, there shall be a need to dynamically rebuilt and thus get and reuse the existing channels. let (local, remote) = { - let (sender_1, receiver_1) = tokio::sync::mpsc::channel(1000); - let (sender_2, receiver_2) = tokio::sync::mpsc::channel(1000); + let (sender_1, receiver_1) = tokio::sync::mpsc::channel(10000); + let (sender_2, receiver_2) = tokio::sync::mpsc::channel(10000); ((sender_1, receiver_2), (sender_2, receiver_1)) }; let src_node_id = rel.source(); @@ -493,8 +492,11 @@ impl ComponentExecutor { self.components.len() ); for handle in self.components.iter_mut() { + let start = std::time::SystemTime::now(); let mut component = handle.component.write().unwrap(); component.init(); + handle.nb_call += 1; + handle.execution_time += start.elapsed().unwrap(); } self.has_been_initialized = true; //unimplemented!("The component executor init is not implemented"); @@ -504,6 +506,7 @@ impl ComponentExecutor { //trace!("start {}", latest_time); for handle in self.components.iter_mut() { let mut component = handle.component.write().unwrap(); + let start = std::time::SystemTime::now(); //trace!("flush_all_messages of {}", handle.name); component.flush_all_messages(); @@ -513,6 +516,9 @@ impl ComponentExecutor { //trace!("broadcast result for {}", handle.name); let b = self.broadcasters.get_mut(&handle.name).unwrap(); //trace!("broadcast size for {} before {}, {}", handle.name, b.len(), b.receiver_count()); + handle.nb_call += 1; + handle.execution_time += start.elapsed().unwrap(); + //println!("component execution statistics for {}: {} {}", handle.name, handle.execution_time.as_millis() / handle.nb_call, handle.execution_time.as_millis()); let _ = b.send(res); //trace!("broadcast size for {} after {}", handle.name, b.len()); } diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs index 48d7a50..9306c30 100644 --- a/crates/joko_link_models/src/lib.rs +++ b/crates/joko_link_models/src/lib.rs @@ -15,7 +15,7 @@ pub use messages::*; pub use mumble::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Default)] pub struct MumbleLinkResult { pub read_ui_link: bool, pub link: Option, diff --git a/crates/joko_link_ui_manager/src/lib.rs b/crates/joko_link_ui_manager/src/lib.rs index 0364d9e..05ccf24 100644 --- a/crates/joko_link_ui_manager/src/lib.rs +++ b/crates/joko_link_ui_manager/src/lib.rs @@ -1,4 +1,4 @@ -use std::{borrow::BorrowMut, sync::Arc}; +use std::borrow::BorrowMut; use egui::DragValue; use joko_component_models::{ @@ -13,14 +13,14 @@ struct MumbleUIManagerChannels { } pub struct MumbleUIManager { - egui_context: Arc, + egui_context: egui::Context, editable_mumble: bool, last_known_link: MumbleLink, channels: Option, } impl MumbleUIManager { - pub fn new(egui_context: Arc) -> Self { + pub fn new(egui_context: egui::Context) -> Self { Self { egui_context, editable_mumble: false, @@ -328,11 +328,9 @@ impl UIPanel for MumbleUIManager { }] } fn init(&mut self) {} - fn gui(&mut self, is_open: &mut bool, _area_id: &str) { - //FIXME: cannot edit anymore => why ? - //UI seems laggy when clicking + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { let channels = self.channels.as_mut().unwrap(); - let u2mb_sender = channels.back_end_notifier.borrow_mut(); + let back_end_notifier = channels.back_end_notifier.borrow_mut(); let egui_context = &self.egui_context; egui::Window::new("Mumble Manager") @@ -341,16 +339,16 @@ impl UIPanel for MumbleUIManager { ui.horizontal(|ui| { if ui.selectable_label(!self.editable_mumble, "live").clicked() { self.editable_mumble = false; - let _ = - u2mb_sender.blocking_send(to_data(MessageToMumbleLinkBack::Autonomous)); + let _ = back_end_notifier + .blocking_send(to_data(MessageToMumbleLinkBack::Autonomous)); } if ui .selectable_label(self.editable_mumble, "editable") .clicked() { self.editable_mumble = true; - let _ = - u2mb_sender.blocking_send(to_data(MessageToMumbleLinkBack::BindedOnUI)); + let _ = back_end_notifier + .blocking_send(to_data(MessageToMumbleLinkBack::BindedOnUI)); } }); if self.editable_mumble { diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml index e1e4272..fe351c4 100644 --- a/crates/joko_package_manager/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" # for marker packs base64 = "0.21.2" bytemuck = { workspace = true } -cap-std = { workspace = true } cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings data-encoding = "2.4.0" egui = { workspace = true } diff --git a/crates/joko_package_manager/src/io/deserialize.rs b/crates/joko_package_manager/src/io/deserialize.rs index 2716e29..92f4235 100644 --- a/crates/joko_package_manager/src/io/deserialize.rs +++ b/crates/joko_package_manager/src/io/deserialize.rs @@ -1,6 +1,5 @@ use crate::BASE64_ENGINE; use base64::Engine; -use cap_std::fs_utf8::{Dir, DirEntry}; use joko_core::{serde_glam::Vec3, RelativePath}; use joko_package_models::{ attributes::{CommonAttributes, XotAttributeNameIDs}, @@ -25,7 +24,7 @@ use zip::result::{ZipError, ZipResult}; const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; pub(crate) fn load_pack_core_from_normalized_folder( - core_dir: &Dir, + core_path: &Path, import_report: Option, ) -> Result { //called from already parsed data @@ -38,7 +37,7 @@ pub(crate) fn load_pack_core_from_normalized_folder( // walks the directory and loads all files into the hashmap let start = std::time::SystemTime::now(); recursive_walk_dir_and_read_images_and_tbins( - core_dir, + core_path, &mut core_pack, &RelativePath::default(), ) @@ -50,8 +49,7 @@ pub(crate) fn load_pack_core_from_normalized_folder( ); //categories are required to register other objects - let cats_xml = core_dir - .read_to_string("categories.xml") + let cats_xml = std::fs::read_to_string(core_path.join("categories.xml")) .or(Err("failed to read categories.xml"))?; let categories_file = String::from("categories.xml"); let parse_categories_file_start = std::time::SystemTime::now(); @@ -61,16 +59,13 @@ pub(crate) fn load_pack_core_from_normalized_folder( info!("parse_categories_file took {} ms", elapsed.as_millis()); // parse map data of the pack - for entry in core_dir - .entries() - .or(Err("failed to read entries of pack dir"))? - { + for entry in std::fs::read_dir(core_path).or(Err("failed to read entries of pack dir"))? { let dir_entry = entry.or(Err("entry error whiel reading xml files"))?; let name = dir_entry .file_name() - .or(Err("map data entry name not utf-8"))? - .to_string(); + .into_string() + .or(Err("map data entry name not utf-8"))?; if name.ends_with(".xml") { if let Some(name_as_str) = name.strip_suffix(".xml") { @@ -82,7 +77,11 @@ pub(crate) fn load_pack_core_from_normalized_folder( // parse map file let span_guard = info_span!("load file", file_name).entered(); //let mut partial_pack = PackCore::partial(&core_pack.all_categories); - load_xml_from_normalized_file(file_name, &dir_entry, &mut core_pack)?; + load_xml_from_normalized_file( + file_name, + &dir_entry.path(), + &mut core_pack, + )?; //core_pack.merge_partial(partial_pack); std::mem::drop(span_guard); } @@ -108,13 +107,16 @@ pub(crate) fn load_pack_core_from_normalized_folder( } fn recursive_walk_dir_and_read_images_and_tbins( - dir: &Dir, + core_path: &Path, pack: &mut PackCore, parent_path: &RelativePath, ) -> Result<(), String> { - for entry in dir.entries().or(Err("failed to get directory entries"))? { + for entry in std::fs::read_dir(core_path).or(Err("failed to get directory entries"))? { let entry = entry.or(Err("dir entry error when iterating dir entries"))?; - let name = entry.file_name().or(Err("No file name found"))?; + let name = entry + .file_name() + .into_string() + .or(Err("No file name found"))?; let path = parent_path.join_str(&name); if entry @@ -123,12 +125,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( .is_file() { if path.ends_with(".png") || path.ends_with(".trl") { - let mut bytes = vec![]; - entry - .open() - .or(Err("failed to open file"))? - .read_to_end(&mut bytes) - .or(Err("failed to read file contents"))?; + let bytes = std::fs::read(entry.path()).or(Err("failed to read file contents"))?; if name.ends_with(".png") { pack.register_texture(name, &path, bytes); } else if name.ends_with(".trl") { @@ -146,11 +143,7 @@ fn recursive_walk_dir_and_read_images_and_tbins( } } } else { - recursive_walk_dir_and_read_images_and_tbins( - &entry.open_dir().or(Err("Could not open directory"))?, - pack, - &path, - )?; + recursive_walk_dir_and_read_images_and_tbins(&entry.path(), pack, &path)?; } } Ok(()) @@ -430,12 +423,13 @@ fn parse_categories_from_normalized_file( fn load_xml_from_normalized_file( file_name: &str, - dir_entry: &DirEntry, + file_path: &Path, target: &mut PackCore, ) -> Result<(), String> { let mut xml_str = String::new(); - dir_entry - .open() + std::fs::OpenOptions::new() + .read(true) + .open(file_path) .or(Err("failed to open xml file"))? .read_to_string(&mut xml_str) .or(Err("failed to read xml string"))?; diff --git a/crates/joko_package_manager/src/manager/pack/active.rs b/crates/joko_package_manager/src/manager/pack/active.rs index 03ed04d..03e92bd 100644 --- a/crates/joko_package_manager/src/manager/pack/active.rs +++ b/crates/joko_package_manager/src/manager/pack/active.rs @@ -25,7 +25,7 @@ use joko_render_models::{ #[derive(Clone)] pub struct ActiveTrail { pub trail_object: TrailObject, - pub texture_handle: TextureHandle, + //pub texture_handle: TextureHandle, } /// This is an active marker. /// It stores all the info that we need to scan every frame @@ -34,7 +34,7 @@ pub(crate) struct ActiveMarker { /// texture id from managed textures pub texture_id: u64, /// owned texture handle to keep it alive - pub _texture: TextureHandle, + //pub _texture: TextureHandle, /// position pub pos: Vec3, /// billboard must not be bigger than this size in pixels @@ -52,7 +52,7 @@ impl ActiveMarker { texture_id, pos, common_attributes: attrs, - _texture, + //_texture, max_pixel_size, min_pixel_size, .. @@ -280,7 +280,7 @@ impl ActiveTrail { egui::TextureId::User(_) => todo!(), }, }, - texture_handle: texture, + //texture_handle: texture, }) } } @@ -295,8 +295,6 @@ pub(crate) struct CurrentMapData { /// The key is the index of the marker in the map markers /// Their position in the map markers serves as their "id" as uuids can be duplicates. pub active_markers: IndexMap, - pub wip_markers: IndexMap, /// The key is the position/index of this trail in the map trails. same as markers pub active_trails: IndexMap, - pub wip_trails: IndexMap, } diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs index 9532238..bd58f93 100644 --- a/crates/joko_package_manager/src/manager/pack/category_selection.rs +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -213,14 +213,14 @@ impl CategorySelection { } fn context_menu( - u2b_sender: &tokio::sync::mpsc::Sender, + back_end_notifier: &tokio::sync::mpsc::Sender, cs: &mut CategorySelection, ui: &mut egui::Ui, ) { if ui.button("Activate branch").clicked() { cs.is_selected = true; CategorySelection::recursive_set_all(&mut cs.children, true); - let _ = u2b_sender.blocking_send(to_data( + let _ = back_end_notifier.blocking_send(to_data( MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true), )); ui.close_menu(); @@ -228,7 +228,7 @@ impl CategorySelection { if ui.button("Deactivate branch").clicked() { CategorySelection::recursive_set_all(&mut cs.children, false); cs.is_selected = false; - let _ = u2b_sender.blocking_send(to_data( + let _ = back_end_notifier.blocking_send(to_data( MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false), )); ui.close_menu(); diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index bc1da2e..31de32c 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -2,7 +2,6 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, io::{Read, Write}, path::{Path, PathBuf}, - sync::Arc, }; use joko_component_models::{to_data, ComponentMessage}; @@ -15,7 +14,6 @@ use joko_package_models::{ }; use ordered_hash_map::OrderedHashMap; -use cap_std::fs_utf8::Dir; use egui::{ColorImage, TextureHandle}; use image::EncodableLayout; use serde::{Deserialize, Serialize}; @@ -29,7 +27,6 @@ use crate::{ pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, package_data::EXTRACT_DIRECTORY_NAME, }, - message::MessageToPackageBack, }; use joko_core::{ serde_glam::Vec3, @@ -38,7 +35,7 @@ use joko_core::{ }; use joko_link_models::MumbleLink; use joko_render_models::{messages::MessageToRenderer, trail::TrailObject}; -use miette::{Context, IntoDiagnostic, Result}; +use miette::Result; use super::activation::{ActivationData, ActivationType}; use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; @@ -61,8 +58,7 @@ pub(crate) struct PackTasks { save_texture_task: AsyncTask>, save_data_task: AsyncTask>, save_report_task: AsyncTask<(PathBuf, PackageImportReport), Result<(), String>>, - load_all_packs_task: - AsyncTask<(Arc, std::path::PathBuf), Result>, + load_all_packs_task: AsyncTask>, } //TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? @@ -155,13 +151,8 @@ impl PackTasks { .send((target_dir, report)); } } - pub fn load_all_packs(&self, jokolay_dir: Arc, root_path: std::path::PathBuf) { - match self - .load_all_packs_task - .lock() - .unwrap() - .send((jokolay_dir, root_path)) - { + pub fn load_all_packs(&self, root_path: std::path::PathBuf) { + match self.load_all_packs_task.lock().unwrap().send(root_path) { Ok(_) => {} Err(e) => error!(?e), } @@ -307,11 +298,15 @@ impl LoadedPackData { }) } - fn load_import_report(pack_dir: &Arc) -> Option { - //FIXME: we need to patch those categories from the one in the files - (if pack_dir.is_file(PackageImportReport::REPORT_FILE_NAME) { - match pack_dir.read_to_string(PackageImportReport::REPORT_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { + fn load_import_report(pack_path: &Path) -> Option { + let report_file_name = pack_path.join(PackageImportReport::REPORT_FILE_NAME); + let mut cd_json = String::new(); + (if let Ok(mut file) = std::fs::OpenOptions::new() + .read(true) + .open(report_file_name) + { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { Ok(cd) => Some(cd), Err(e) => { error!(?e, "failed to deserialize import report"); @@ -328,19 +323,14 @@ impl LoadedPackData { }) .flatten() } - pub fn load_from_dir(name: String, pack_dir: Arc, path: PathBuf) -> Result { - if !pack_dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .or(Err("failed to check if pack core exists"))? - { + pub fn load_from_dir(name: String, path: PathBuf) -> Result { + let core_path = path.join(Self::CORE_PACK_DIR_NAME); + if !core_path.exists() { return Err("pack core doesn't exist in this pack".to_string()); } - let core_dir = pack_dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; let start = std::time::SystemTime::now(); - let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + let import_report = LoadedPackData::load_import_report(&path); + let core = load_pack_core_from_normalized_folder(&core_path, import_report) .or(Err("failed to load pack from dir"))?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!( @@ -400,31 +390,31 @@ impl LoadedPackData { #[allow(clippy::too_many_arguments)] pub(crate) fn tick( &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, + front_end_notifier: &tokio::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, - list_of_active_or_selected_elements_changed: bool, - map_changed: bool, _tasks: &PackTasks, next_loaded: &mut HashSet, ) { //since the loading of texture is lazy, there is no problem when calling this regularly - if map_changed || list_of_active_or_selected_elements_changed { - //tasks.change_map(self, b2u_sender, link, currently_used_files); - let mut active_elements: HashSet = Default::default(); - self.on_map_changed(b2u_sender, link, currently_used_files, &mut active_elements); - let _ = b2u_sender.blocking_send(to_data(MessageToPackageUI::PackageActiveElements( - self.uuid, - active_elements.clone(), - ))); - self.active_elements = active_elements.clone(); - next_loaded.extend(active_elements); - } + //tasks.change_map(self, b2u_sender, link, currently_used_files); + let mut active_elements: HashSet = Default::default(); + self.on_map_changed( + front_end_notifier, + link, + currently_used_files, + &mut active_elements, + ); + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()), + )); + self.active_elements = active_elements.clone(); + next_loaded.extend(active_elements); } fn on_map_changed( &mut self, - b2u_sender: &tokio::sync::mpsc::Sender, + front_end_notifier: &tokio::sync::mpsc::Sender, link: &MumbleLink, currently_used_files: &BTreeMap, active_elements: &mut HashSet, @@ -516,14 +506,15 @@ impl LoadedPackData { } } if let Some(tex_path) = common_attributes.get_icon_file() { - let _ = - b2u_sender.blocking_send(to_data(MessageToPackageUI::MarkerTexture( + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::MarkerTexture( self.uuid, tex_path.clone(), marker.guid, marker.position, common_attributes, - ))); + ), + )); } else { debug!("no texture attribute on this marker"); } @@ -557,13 +548,14 @@ impl LoadedPackData { let mut common_attributes = trail.props.clone(); common_attributes.inherit_if_attr_none(category_attributes); if let Some(tex_path) = common_attributes.get_texture() { - let _ = - b2u_sender.blocking_send(to_data(MessageToPackageUI::TrailTexture( + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::TrailTexture( self.uuid, tex_path.clone(), trail.guid, common_attributes, - ))); + ), + )); } else { debug!("no texture attribute on this trail"); } @@ -624,11 +616,6 @@ impl LoadedPackTexture { import_quality_report, ); }); - if self._is_dirty { - let _ = back_end_notifier.blocking_send(to_data( - MessageToPackageBack::CategoryActivationStatusChanged, - )); - } } pub fn is_dirty(&self) -> bool { @@ -644,12 +631,10 @@ impl LoadedPackTexture { _tasks: &PackTasks, ) -> Result<()> { tracing::trace!( - "LoadedPackTexture.tick: {} {}-{} {}-{}", + "LoadedPackTexture.tick: {} {} {}", self.name, self.current_map_data.active_markers.len(), - self.current_map_data.wip_markers.len(), self.current_map_data.active_trails.len(), - self.current_map_data.wip_trails.len(), ); let mut marker_objects = Vec::new(); for marker in self.current_map_data.active_markers.values() { @@ -682,7 +667,18 @@ impl LoadedPackTexture { Ok(()) } - pub fn swap(&mut self) { + pub fn clear(&mut self) { + info!( + "clear {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.active_markers.len(), + self.current_map_data.active_trails.len() + ); + self.current_map_data.active_markers.clear(); + self.current_map_data.active_trails.clear(); + } + /*pub fn swap(&mut self) { info!( "swap {} to display {} textures, {} markers, {} trails", self.name, @@ -693,7 +689,7 @@ impl LoadedPackTexture { self.current_map_data.active_markers = std::mem::take(&mut self.current_map_data.wip_markers); self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); - } + }*/ pub fn load_marker_texture( &mut self, @@ -738,13 +734,13 @@ impl LoadedPackTexture { let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size let am = ActiveMarker { texture_id, - _texture: th.clone(), + //_texture: th.clone(), common_attributes, pos: position, max_pixel_size, min_pixel_size, }; - self.current_map_data.wip_markers.insert(marker_uuid, am); + self.current_map_data.active_markers.insert(marker_uuid, am); } pub fn load_trail_texture( @@ -797,7 +793,7 @@ impl LoadedPackTexture { ActiveTrail::get_vertices_and_texture(&common_attributes, &tbin.nodes, th.clone()) { self.current_map_data - .wip_trails + .active_trails .insert(trail_uuid, active_trail); } else { info!("Cannot display {texture_path:?}") @@ -823,6 +819,7 @@ pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::Path .join(PACKAGES_DIRECTORY_NAME) } +/* pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { jokolay_dir .create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) @@ -869,28 +866,17 @@ pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { .wrap_err("failed to create data folder for editable package")?; Ok(marker_packs_dir) -} +}*/ -pub fn load_all_from_dir( - input: (Arc, std::path::PathBuf), -) -> Result { - let (jokolay_dir, root_path) = input; +pub fn load_all_from_dir(root_path: std::path::PathBuf) -> Result { trace!("load_all_from_dir {:?}", root_path); - let marker_packs_dir = match jokolay_to_marker_dir(&jokolay_dir) { - Ok(marker_packs_dir) => marker_packs_dir, - Err(e) => { - error!("Failed to open packages directory {:?}", e); - return Err("Failed to open packages directory".to_string()); - } - }; let marker_packs_path = jokolay_to_marker_path(&root_path); let mut data_packs: BTreeMap = Default::default(); let mut texture_packs: BTreeMap = Default::default(); let mut report_packs: BTreeMap = Default::default(); - for entry in marker_packs_dir - .entries() - .or(Err("failed to get entries of marker packs dir"))? + for entry in + std::fs::read_dir(marker_packs_path).or(Err("failed to get entries of marker packs dir"))? { let entry = entry.or(Err("Failed to read packages directory"))?; if entry @@ -900,63 +886,49 @@ pub fn load_all_from_dir( { continue; } - if let Ok(name) = entry.file_name() { - let pack_path = marker_packs_path.join(&name); - let pack_dir = entry.open_dir().or(Err(format!( - "failed to open pack entry as directory: {}", - name - )))?; - { - if name == EDITABLE_PACKAGE_NAME { - //TODO: have a version of loading that does not involve already ingested packages - if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_dir, None) { - let lp = build_from_core(name.clone(), pack_path, pack_core); + let name = entry.file_name().into_string().unwrap(); + let pack_path = entry.path(); + { + if name == EDITABLE_PACKAGE_NAME { + //TODO: have a version of loading that does not involve already ingested packages + if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_path, None) { + let lp = build_from_core(name.clone(), pack_path, pack_core); + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + } else if name == LOCAL_EXPANDED_PACKAGE_NAME { + //ignore this package, it'll be overwriten + } else { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name.clone(), pack_path) { + Ok(lp) => { let (data, tex, report) = lp; data_packs.insert(data.uuid, data); texture_packs.insert(tex.uuid, tex); report_packs.insert(report.uuid, report); } - } else if name == LOCAL_EXPANDED_PACKAGE_NAME { - //ignore this package, it'll be overwriten - } else { - let span_guard = info_span!("loading pack from dir", name).entered(); - - match build_from_dir(name.clone(), pack_dir.into(), pack_path) { - Ok(lp) => { - let (data, tex, report) = lp; - data_packs.insert(data.uuid, data); - texture_packs.insert(tex.uuid, tex); - report_packs.insert(report.uuid, report); - } - Err(e) => { - error!(?e, "failed to load pack from directory: {}", name); - } + Err(e) => { + error!(?e, "failed to load pack from directory: {}", name); } - drop(span_guard); } + drop(span_guard); } } } Ok((data_packs, texture_packs, report_packs)) } -fn build_from_dir( - name: String, - pack_dir: Arc, - pack_path: PathBuf, -) -> Result { - if !pack_dir - .try_exists(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to check if pack core exists"))? - { +fn build_from_dir(name: String, pack_path: PathBuf) -> Result { + let core_path = pack_path.join(LoadedPackData::CORE_PACK_DIR_NAME); + if !core_path.exists() { return Err("pack core doesn't exist in this pack".to_string()); } - let core_dir = pack_dir - .open_dir(LoadedPackData::CORE_PACK_DIR_NAME) - .or(Err("failed to open core pack directory"))?; let start = std::time::SystemTime::now(); - let import_report = LoadedPackData::load_import_report(&pack_dir); - let core = load_pack_core_from_normalized_folder(&core_dir, import_report) + let import_report = LoadedPackData::load_import_report(&pack_path); + let core = load_pack_core_from_normalized_folder(&core_path, import_report) .or(Err("failed to load pack from dir"))?; let elaspsed = start.elapsed().unwrap_or_default(); tracing::info!( diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs index 122766c..172e990 100644 --- a/crates/joko_package_manager/src/manager/package_data.rs +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -1,9 +1,5 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - sync::Arc, -}; +use std::collections::{BTreeMap, HashMap, HashSet}; -use cap_std::fs_utf8::Dir; use joko_component_models::{ default_component_result, from_broadcast, from_data, to_data, Component, ComponentChannels, ComponentMessage, ComponentResult, @@ -35,7 +31,6 @@ pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of imp #[derive(Clone)] pub struct PackageBackSharedState { choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI - pub root_dir: Arc, pub root_path: std::path::PathBuf, #[allow(dead_code)] pub editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration @@ -93,7 +88,7 @@ impl PackageDataManager { /// 4. loads all the packs /// 5. loads all the activation data /// 6. returns self - pub fn new(root_dir: Arc, root_path: &std::path::Path) -> Result { + pub fn new(root_path: &std::path::Path) -> Result { let marker_packs_path = jokolay_to_marker_path(root_path); //TODO: load configuration from disk (ui.toml) let editable_path = jokolay_to_editable_path(root_path) @@ -102,7 +97,6 @@ impl PackageDataManager { .to_string(); let state = PackageBackSharedState { choice_of_category_changed: false, - root_dir, root_path: root_path.to_owned(), editable_path: std::path::PathBuf::from(editable_path), extract_path: jokolay_to_extract_path(root_path), @@ -209,48 +203,50 @@ impl PackageDataManager { } fn handle_message(&mut self, msg: MessageToPackageBack) { - //let (b2u_sender, _) = package_manager.channels(); match msg { MessageToPackageBack::ActiveFiles(currently_used_files) => { - tracing::trace!("Handling of MessageToPackageBack::ActiveFiles"); + trace!( + "Handling of MessageToPackageBack::ActiveFiles {}", + currently_used_files.len() + ); + trace!( + "Handling of MessageToPackageBack::ActiveFiles {:?}", + currently_used_files + ); self.set_currently_used_files(currently_used_files); self.state.choice_of_category_changed = true; } MessageToPackageBack::CategoryActivationElementStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationElementStatusChange" - ); + trace!("Handling of MessageToPackageBack::CategoryActivationElementStatusChange"); self.category_set(category_uuid, status); + self.state.choice_of_category_changed = true; } MessageToPackageBack::CategoryActivationBranchStatusChange(category_uuid, status) => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationBranchStatusChange" - ); + trace!("Handling of MessageToPackageBack::CategoryActivationBranchStatusChange"); self.category_branch_set(category_uuid, status); + self.state.choice_of_category_changed = true; } MessageToPackageBack::CategoryActivationStatusChanged => { - tracing::trace!( - "Handling of MessageToPackageBack::CategoryActivationStatusChanged" - ); + trace!("Handling of MessageToPackageBack::CategoryActivationStatusChanged"); self.state.choice_of_category_changed = true; } MessageToPackageBack::CategorySetAll(status) => { - tracing::trace!("Handling of MessageToPackageBack::CategorySetAll"); + trace!( + "Handling of MessageToPackageBack::CategorySetAll {}", + status + ); self.category_set_all(status); self.state.choice_of_category_changed = true; } MessageToPackageBack::DeletePacks(to_delete) => { tracing::trace!("Handling of MessageToPackageBack::DeletePacks"); - let std_file = std::fs::OpenOptions::new() - .open(&self.marker_packs_path) - .or(Err("Could not open file")) - .unwrap(); - let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file); + let mut deleted = Vec::new(); for pack_uuid in to_delete { if let Some(pack) = self.packs.remove(&pack_uuid) { - if let Err(e) = marker_packs_dir.remove_dir_all(&pack.name) { + let target = self.marker_packs_path.join(&pack.name); + if let Err(e) = std::fs::remove_dir_all(target) { error!(?e, pack.name, "failed to remove pack"); } else { info!("deleted marker pack: {}", pack.name); @@ -299,7 +295,7 @@ impl PackageDataManager { } MessageToPackageBack::SavePack(name, pack) => { tracing::trace!("Handling of MessageToPackageBack::SavePack"); - println!("save in {:?}", self.marker_packs_path); + trace!("save in {:?}", self.marker_packs_path); /*let std_file = std::fs::OpenOptions::new() .open(&self.marker_packs_path) @@ -339,9 +335,6 @@ impl PackageDataManager { } pub fn _tick(&mut self, mumble_link_result: &MumbleLinkResult) { - let mut currently_used_files: BTreeMap = Default::default(); - let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); - let link = if mumble_link_result.read_ui_link { mumble_link_result.ui_link.as_ref() } else { @@ -357,6 +350,7 @@ impl PackageDataManager { "PackageDataManager::tick map id is: {}", self.current_map_id ); + let mut currently_used_files: BTreeMap = Default::default(); for pack in self.packs.values_mut() { if let Some(current_map) = pack.maps.get(&link.map_id) { for marker in current_map.markers.values() { @@ -389,48 +383,59 @@ impl PackageDataManager { } } } + trace!( + "currently_used_files: {} {:?}", + currently_used_files.len(), + currently_used_files + ); let tasks = &self.tasks; - for pack in self.packs.values_mut() { - let span_guard = info_span!("Updating package status").entered(); - let channels = self.channels.as_mut().unwrap(); - let _ = channels - .front_end_notifier - .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(tasks.count()))); - tasks.save_data(pack, pack.is_dirty()); - pack.tick( - &channels.front_end_notifier, - link, - ¤tly_used_files, - have_used_files_list_changed || self.state.choice_of_category_changed, - map_changed, - tasks, - &mut categories_and_elements_to_be_loaded, - ); - std::mem::drop(span_guard); - } - if map_changed { - self.get_active_elements_parents(categories_and_elements_to_be_loaded); - let channels = self.channels.as_mut().unwrap(); - let _ = channels.front_end_notifier.blocking_send(to_data( - MessageToPackageUI::ActiveElements(self.loaded_elements.clone()), - )); - } if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed { - let channels = self.channels.as_mut().unwrap(); + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + { + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::TextureBegin)); + } + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(tasks.count()))); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + &channels.front_end_notifier, + link, + ¤tly_used_files, + tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + //there is no point in sending a new list if nothing changed + + let channels = self.channels.as_mut().unwrap(); let _ = channels.front_end_notifier.blocking_send(to_data( MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()), )); self.currently_used_files = currently_used_files; + + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::ActiveElements(self.loaded_elements.clone()), + )); let _ = channels .front_end_notifier .blocking_send(to_data(MessageToPackageUI::TextureSwapChain)); } + self.state.choice_of_category_changed = false; } else { trace!("PackageDataManager::tick no link") } - self.state.choice_of_category_changed = false; } fn delete_packs(&mut self, to_delete: Vec) { @@ -475,10 +480,7 @@ impl PackageDataManager { let _ = channels .front_end_notifier .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(1))); - self.tasks.load_all_packs( - Arc::clone(&self.state.root_dir), - self.state.root_path.clone(), - ); + self.tasks.load_all_packs(self.state.root_path.clone()); if let Ok((data_packages, texture_packages, report_packages)) = self.tasks.wait_for_load_all_packs() { @@ -520,6 +522,7 @@ impl Component for PackageDataManager { ); let channels = self.channels.as_mut().unwrap(); + //println!("PackageDataManager: nb messages to read: {}", channels.front_end_receiver.len()); let mut messages = Vec::new(); while let Ok(msg) = channels.front_end_receiver.try_recv() { messages.push(from_data(&msg)); diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs index 2e2ee93..471bbe4 100644 --- a/crates/joko_package_manager/src/manager/package_ui.rs +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -49,10 +49,11 @@ pub struct PackageUIManager { default_marker_texture: Option, default_trail_texture: Option, packs: BTreeMap, + nb_swap: u128, reports: BTreeMap, tasks: PackTasks, - egui_context: Arc, + egui_context: egui::Context, z_near: f32, currently_used_files: BTreeMap, all_files_activation_status: bool, // this consume a change of display event @@ -67,7 +68,7 @@ pub struct PackageUIManager { } impl PackageUIManager { - pub fn new(egui_context: Arc, z_near: f32) -> Self { + pub fn new(egui_context: egui::Context, z_near: f32) -> Self { //z_near is a constant, make it a https://docs.rs/tokio/latest/tokio/sync/watch/index.html if required to be dynamic let state = PackageUISharedState { list_of_textures_changed: false, @@ -77,6 +78,7 @@ impl PackageUIManager { }; let mut res = Self { packs: Default::default(), + nb_swap: 0, tasks: PackTasks::new(), reports: Default::default(), default_marker_texture: None, @@ -113,11 +115,8 @@ impl PackageUIManager { self.delete_packs(to_delete); } MessageToPackageUI::FirstLoadDone => { - let channels = self.channels.as_ref().unwrap(); - let renderer_notifier = &channels.renderer_notifier; - let _ = - renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); self.state.first_load_done = true; + self.state.list_of_textures_changed = true; } MessageToPackageUI::ImportedPack(file_name, pack) => { tracing::trace!("Handling of MessageToPackageUI::ImportedPack"); @@ -133,12 +132,12 @@ impl PackageUIManager { self.save(pack_texture, report); self.state.import_status = Default::default(); let channels = self.channels.as_mut().unwrap(); - let _ = channels.back_end_notifier.blocking_send(to_data( + /*let _ = channels.back_end_notifier.blocking_send(to_data( MessageToPackageBack::CategoryActivationStatusChanged, )); + self.state.list_of_textures_changed = true;*/ let renderer_notifier = &channels.renderer_notifier; - let _ = - renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderFlush)); } MessageToPackageUI::MarkerTexture( pack_uuid, @@ -148,7 +147,6 @@ impl PackageUIManager { common_attributes, ) => { tracing::trace!("Handling of MessageToPackageUI::MarkerTexture"); - //FIXME: make it a TODO on tick() self.delayed_marker_texture.push(( pack_uuid, tex_path, @@ -165,9 +163,15 @@ impl PackageUIManager { tracing::trace!("Handling of MessageToPackageUI::PackageActiveElements"); self.update_pack_active_categories(pack_uuid, &active_elements); } + MessageToPackageUI::TextureBegin => { + tracing::trace!("Handling of MessageToPackageUI::TextureBegin"); + self.clear(); + } MessageToPackageUI::TextureSwapChain => { - tracing::debug!("Handling of MessageToPackageUI::TextureSwapChain"); - self.swap(); + tracing::trace!( + "Handling of MessageToPackageUI::TextureSwapChain {}", + self.nb_swap + ); self.state.list_of_textures_changed = true; } MessageToPackageUI::TrailTexture( @@ -253,16 +257,17 @@ impl PackageUIManager { } } } - pub fn swap(&mut self) { + pub fn clear(&mut self) { + self.nb_swap += 1; for pack in self.packs.values_mut() { - pack.swap(); + pack.clear(); } } pub fn load_marker_texture( &mut self, pack_uuid: Uuid, - egui_context: &egui::Context, + egui_context: egui::Context, tex_path: RelativePath, marker_uuid: Uuid, position: Vec3, @@ -270,7 +275,7 @@ impl PackageUIManager { ) { if let Some(pack) = self.packs.get_mut(&pack_uuid) { pack.load_marker_texture( - egui_context, + &egui_context, self.default_marker_texture.as_ref().unwrap(), &tex_path, marker_uuid, @@ -282,14 +287,14 @@ impl PackageUIManager { pub fn load_trail_texture( &mut self, pack_uuid: Uuid, - egui_context: &egui::Context, + egui_context: egui::Context, tex_path: RelativePath, trail_uuid: Uuid, common_attributes: CommonAttributes, ) { if let Some(pack) = self.packs.get_mut(&pack_uuid) { pack.load_trail_texture( - egui_context, + &egui_context, self.default_trail_texture.as_ref().unwrap(), &tex_path, trail_uuid, @@ -323,6 +328,9 @@ impl PackageUIManager { pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { trace!("PackageUIManager::_tick for {} packages", self.packs.len()); + if self.packs.is_empty() { + return Ok(()); + } let tasks = &self.tasks; let channels = self.channels.as_ref().unwrap(); let renderer_notifier = &channels.renderer_notifier; @@ -333,9 +341,11 @@ impl PackageUIManager { || link.changes.contains(MumbleChanges::Map) || self.state.list_of_textures_changed { + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderBegin)); + for pack in self.packs.values_mut() { let span_guard = info_span!("Updating package status").entered(); - pack.tick(renderer_notifier, timestamp, link, z_near, tasks)?; + pack.tick(renderer_notifier, timestamp, link, z_near, tasks)?; // compute the vertices: textures position, size, rotation and so on std::mem::drop(span_guard); } let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); @@ -380,7 +390,6 @@ impl PackageUIManager { } fn gui_file_manager(&mut self, is_open: &mut bool) { - //FIXME: the deactivate all for all files, seems to toggle only the next one not in target state let egui_context = self.egui_context.borrow_mut(); let channels = self.channels.as_mut().unwrap(); let mut files_changed = false; @@ -410,7 +419,6 @@ impl PackageUIManager { ui.end_row(); for pack in self.packs.values_mut() { - //TODO: first loop to list what is active per pack, to not display all packs let report = self.reports.get(&pack.uuid).unwrap(); let mut pack_files_toggle = false; let mut pack_files_activation_status = true; @@ -483,7 +491,6 @@ impl PackageUIManager { let collapsing = CollapsingHeader::new(format!("Last load details of package {}", pack.name)); - //FIXME: clear the pack details let _header_response = collapsing .open(Some(true)) .show(ui, |ui| { @@ -684,7 +691,7 @@ impl Component for PackageUIManager { { self.load_marker_texture( pack_uuid, - &Arc::clone(&self.egui_context), + self.egui_context.clone(), tex_path, marker_uuid, position, @@ -696,7 +703,7 @@ impl Component for PackageUIManager { { self.load_trail_texture( pack_uuid, - &Arc::clone(&self.egui_context), + self.egui_context.clone(), tex_path, trail_uuid, common_attributes, @@ -752,7 +759,7 @@ impl UIPanel for PackageUIManager { ] } fn init(&mut self) {} - fn gui(&mut self, is_open: &mut bool, area_id: &str) { + fn gui(&mut self, is_open: &mut bool, area_id: &str, _latest_time: f64) { match area_id { "package_loading" => { self.gui_package_list(is_open); diff --git a/crates/joko_package_manager/src/message.rs b/crates/joko_package_manager/src/message.rs index 90b0b77..734974e 100644 --- a/crates/joko_package_manager/src/message.rs +++ b/crates/joko_package_manager/src/message.rs @@ -23,6 +23,7 @@ pub enum MessageToPackageUI { MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), NbTasksRunning(i32), //tell the number of taks running in background PackageActiveElements(Uuid, HashSet), // first is the package reference, second is the list of active elements in the package. + TextureBegin, // start to produce new set of textures TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), } diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 45c6a6d..d55c8fb 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -161,7 +161,7 @@ pub struct PackCore { pub categories: OrderedHashMap, pub all_categories: HashMap, pub entities_parents: HashMap, - pub active_source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package + pub active_source_files: BTreeMap, pub maps: HashMap, pub report: PackageImportReport, } @@ -276,7 +276,7 @@ impl PackCore { if let Some(category_uuid) = self.all_categories.get(full_category_name) { *category_uuid } else { - //TODO: if import is "dirty", create missing category + // If imported package is "dirty", create missing category //TODO: default import mode is "strict" (get inspiration from HTML modes) debug!("There is no defined category for {}", full_category_name); @@ -363,7 +363,7 @@ impl PackCore { self.report.number_of.entities += 1; Ok(uuid_to_insert) } else { - //FIXME: this means a broken package, we could fix it by making usage of the relative category the node is in. + // Dirty package ! We could fix it by making usage of the relative category the node is in. Err(format!( "Can't register world entity {} {}, no associated category found.", full_category_name, uuid diff --git a/crates/joko_render_manager/Cargo.toml b/crates/joko_render_manager/Cargo.toml index 93e2463..d7ca262 100644 --- a/crates/joko_render_manager/Cargo.toml +++ b/crates/joko_render_manager/Cargo.toml @@ -19,6 +19,7 @@ tokio = { workspace = true } joko_component_models = { path = "../joko_component_models" } +joko_ui_models = { path = "../joko_ui_models" } joko_link_models = { path = "../joko_link_models" } joko_render_models = { path = "../joko_render_models" } diff --git a/crates/joko_render_manager/src/billboard.rs b/crates/joko_render_manager/src/billboard.rs index f64a8f9..c240787 100644 --- a/crates/joko_render_manager/src/billboard.rs +++ b/crates/joko_render_manager/src/billboard.rs @@ -65,6 +65,20 @@ impl BillBoardRenderer { } } + pub fn begin(&mut self) { + trace!("Begin with a fresh list of markers and trails"); + self.markers_wip.clear(); + self.trails_wip.clear(); + } + pub fn flush(&mut self) { + trace!( + "Flush UI to display {} markers, {} trails", + self.markers_wip.len(), + self.trails_wip.len() + ); + self.markers = self.markers_wip.clone(); + self.trails = self.trails_wip.clone(); + } pub fn swap(&mut self) { trace!( "swap UI to display {} markers, {} trails", diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs index e2956d7..162f34a 100644 --- a/crates/joko_render_manager/src/renderer.rs +++ b/crates/joko_render_manager/src/renderer.rs @@ -26,6 +26,8 @@ use joko_component_models::ComponentResult; use joko_link_models::MumbleLinkResult; use joko_link_models::UIState; use joko_render_models::messages::MessageToRenderer; +use joko_ui_models::UIArea; +use joko_ui_models::UIPanel; use three_d::prelude::*; use joko_render_models::{marker::MarkerObject, trail::TrailObject}; @@ -41,11 +43,13 @@ pub struct JokoRenderer { pub viewport: Viewport, pub has_link: bool, pub is_map_open: bool, + nb_swap: u128, pub billboard_renderer: BillBoardRenderer, glfw_backend: Arc>, - egui_context: Arc, + egui_context: egui::Context, pub gl: egui_render_three_d::ThreeDBackend, channels: Option, + link: MumbleLinkResult, } /// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation @@ -53,12 +57,7 @@ unsafe impl Send for JokoRenderer {} unsafe impl Sync for JokoRenderer {} impl JokoRenderer { - pub fn new(glfw_backend: Arc>, egui_context: Arc) -> Self { - /* - FIXME: Box + JokoRenderer => segfault when panic - Arc vs Box: no change - */ - //let glfw = glfw_backend.glfw.clone(); + pub fn new(glfw_backend: Arc>, egui_context: egui::Context) -> Self { let framebuffer_size_physical = glfw_backend.read().unwrap().framebuffer_size_physical; let backend = ThreeDBackend::new( ThreeDConfig { @@ -91,12 +90,14 @@ impl JokoRenderer { ), has_link: false, is_map_open: false, + nb_swap: 0, gl: backend, egui_context, billboard_renderer, glfw_backend, cam_pos: Default::default(), channels: None, + link: Default::default(), } } @@ -142,7 +143,15 @@ impl JokoRenderer { pub fn get_z_far() -> f32 { 1000.0 } + + pub fn begin(&mut self) { + self.billboard_renderer.begin(); + } + pub fn flush(&mut self) { + self.billboard_renderer.flush(); + } pub fn swap(&mut self) { + self.nb_swap += 1; self.billboard_renderer.swap(); } /* @@ -164,33 +173,44 @@ impl JokoRenderer { match msg { MessageToRenderer::BulkMarkerObject(marker_objects) => { tracing::debug!( - "Handling of UIToUIMessage::BulkMarkerObject {}", + "Handling of MessageToRenderer::BulkMarkerObject {}", marker_objects.len() ); self.extend_markers(marker_objects); } MessageToRenderer::BulkTrailObject(trail_objects) => { tracing::debug!( - "Handling of UIToUIMessage::BulkTrailObject {}", + "Handling of MessageToRenderer::BulkTrailObject {}", trail_objects.len() ); self.extend_trails(trail_objects); } MessageToRenderer::MarkerObject(mo) => { - tracing::trace!("Handling of UIToUIMessage::MarkerObject"); + tracing::trace!("Handling of MessageToRenderer::MarkerObject"); self.add_billboard(*mo); } MessageToRenderer::TrailObject(to) => { - tracing::trace!("Handling of UIToUIMessage::TrailObject"); + tracing::trace!("Handling of MessageToRenderer::TrailObject"); self.add_trail(*to); } + MessageToRenderer::RenderBegin => { + tracing::trace!("Handling of MessageToRenderer::RenderBegin"); + self.begin(); + } + MessageToRenderer::RenderFlush => { + tracing::trace!("Handling of MessageToRenderer::RenderFlush"); + self.flush(); + } MessageToRenderer::RenderSwapChain => { - tracing::debug!("Handling of UIToUIMessage::RenderSwapChain"); + tracing::trace!( + "Handling of MessageToRenderer::RenderSwapChain {}", + self.nb_swap + ); self.swap(); } #[allow(unreachable_patterns)] _ => { - unimplemented!("Handling UIToUIMessage has not been implemented yet"); + unimplemented!("Handling MessageToRenderer has not been implemented yet"); } } } @@ -320,17 +340,33 @@ impl Component for JokoRenderer { fn requirements(&self) -> Vec<&str> { vec!["ui:mumble_link"] } - fn tick(&mut self, latest_time: f64) -> ComponentResult { + fn tick(&mut self, _latest_time: f64) -> ComponentResult { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); - self._window_tick(); let channels = self.channels.as_mut().unwrap(); let raw_link = channels.subscription_mumble_link.blocking_recv().unwrap(); let link: MumbleLinkResult = from_broadcast(&raw_link); - if let Some(link) = link.link { + self.link = link; + default_component_result() + } +} + +impl UIPanel for JokoRenderer { + fn init(&mut self) {} + fn areas(&self) -> Vec { + vec![UIArea { + id: "overlay".to_string(), + name: String::new(), + is_open: true, // N/A + }] + } + + fn gui(&mut self, _is_open: &mut bool, _area_id: &str, latest_time: f64) { + self._window_tick(); + if let Some(link) = &self.link.link { //trace!("JokoRenderer {:?} {:?}", link.player_pos, link.cam_pos); //x positive => east //y positive => ascention @@ -408,6 +444,8 @@ impl Component for JokoRenderer { } else { self.has_link = false; } + + self.egui_context.request_repaint(); let egui::FullOutput { platform_output, textures_delta, @@ -439,6 +477,5 @@ impl Component for JokoRenderer { self.render_egui(meshes, textures_delta, window_size_logical, latest_time); self.present(); self.glfw_backend.write().unwrap().window.swap_buffers(); - default_component_result() } } diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs index 3c34a0a..5b20064 100644 --- a/crates/joko_render_models/src/messages.rs +++ b/crates/joko_render_models/src/messages.rs @@ -8,6 +8,8 @@ pub enum MessageToRenderer { BulkTrailObject(Vec), //Present,// a render loop is finished and we can present it MarkerObject(Box), - RenderSwapChain, // The list of elements to display was changed + RenderBegin, // There is a change in what to display, reset current build + RenderSwapChain, // The list of elements to display was changed. Or camera or position was changed. + RenderFlush, // Force whatever is being constructed to be kept and be what to display TrailObject(Box), } diff --git a/crates/joko_ui_models/src/lib.rs b/crates/joko_ui_models/src/lib.rs index 45b783b..0f816fa 100644 --- a/crates/joko_ui_models/src/lib.rs +++ b/crates/joko_ui_models/src/lib.rs @@ -3,11 +3,12 @@ use egui::Ui; pub struct UIArea { pub is_open: bool, pub name: String, + /// if empty, no option shall be displayed in the menu pub id: String, } pub trait UIPanel { fn init(&mut self); - fn gui(&mut self, is_open: &mut bool, area_id: &str); + fn gui(&mut self, is_open: &mut bool, area_id: &str, latest_time: f64); fn menu_ui(&mut self, _ui: &mut Ui) {} fn areas(&self) -> Vec; } diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index f6c4e1f..d26f821 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -4,8 +4,6 @@ use miette::{Context, IntoDiagnostic, Result}; /// Jokolay Configuration /// We will read a path from env `JOKOLAY_DATA_DIR` or create a folder at data_local_dir/jokolay, where data_local_dir is platform specific /// Inside this directory, we will store all of jokolay's data like configuration files, themes, logs etc.. - -//TODO: isn't directories-next better for introspection ? pub fn get_jokolay_path() -> Result { if let Some(project_dir) = directories_next::ProjectDirs::from("com.jokolay", "", "jokolay") { Ok(project_dir.data_local_dir().to_path_buf()) diff --git a/crates/jokolay/src/app/menu.rs b/crates/jokolay/src/app/menu.rs index 6008a33..c6eed64 100644 --- a/crates/jokolay/src/app/menu.rs +++ b/crates/jokolay/src/app/menu.rs @@ -11,11 +11,10 @@ use tracing::info; use super::window::{MINIMAL_WINDOW_HEIGHT, MINIMAL_WINDOW_WIDTH}; struct MenuPanel { - //TODO: area => so we can have a single element producing multiple windows. It'll have to be registered for each area - //Or, register the functions that handle the areas. - //rename gui() into area() panel: Arc>, areas: Vec, + nb_draw: u128, + draw_time: std::time::Duration, } struct MenuPanelManagerChannels { @@ -63,14 +62,12 @@ struct MenuPanelManagerChannels { /// Finally, just multiply the width 288 or height 27 with these three values. /// eg: menu width = 288 * uisz_ratio * dpi_scaling_ratio * aspect_ratio_scaling; /// do the same with 288 replaced by 27 for height. - pub struct MenuPanelManager { - //TODO: turn the MenuPanel into a component which depends on MumbleLink manager pub pos: egui::Pos2, pub ui_scaling_factor: f32, pub show_tracing_window: bool, glfw_backend: Arc>, - egui_context: Arc, + egui_context: egui::Context, menus: Vec, channels: Option, } @@ -82,7 +79,7 @@ impl MenuPanelManager { pub const WIDTH: f32 = 288.0; pub const HEIGHT: f32 = 27.0; - pub fn new(glfw_backend: Arc>, egui_context: Arc) -> Self { + pub fn new(glfw_backend: Arc>, egui_context: egui::Context) -> Self { Self { glfw_backend, egui_context, @@ -98,10 +95,12 @@ impl MenuPanelManager { self.menus.push(MenuPanel { panel: component.clone(), areas: component.read().unwrap().areas(), + nb_draw: 0, + draw_time: Default::default(), }) } - pub fn gui(&mut self) { + pub fn gui(&mut self, latest_time: f64) { //let mut glfw_backend = self.glfw_backend.(); // do the gui stuff now egui::Area::new("menu panel") @@ -111,7 +110,6 @@ impl MenuPanelManager { .show(&self.egui_context, |ui| { ui.style_mut().visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; ui.horizontal(|ui| { - //TODO: if any displayed, show an additional "hide all" ui.menu_button( egui::RichText::new("JKL") .size((MenuPanelManager::HEIGHT - 2.0) * self.ui_scaling_factor) @@ -120,6 +118,9 @@ impl MenuPanelManager { let mut any_open = false; for panel in self.menus.iter_mut() { for area in panel.areas.iter_mut() { + if area.name.is_empty() { + continue; + } ui.checkbox(&mut area.is_open, &area.name); any_open = any_open || area.is_open; } @@ -150,9 +151,12 @@ impl MenuPanelManager { }); for panel in self.menus.iter_mut() { let handle = &mut panel.panel.write().unwrap(); + let start = std::time::SystemTime::now(); for area in panel.areas.iter_mut() { - handle.gui(&mut area.is_open, &area.id); + handle.gui(&mut area.is_open, &area.id, latest_time); } + panel.nb_draw += 1; + panel.draw_time += start.elapsed().unwrap(); } } } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 9f7224c..ad24cc2 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,5 +1,4 @@ use std::{ - borrow::BorrowMut, sync::{Arc, RwLock}, thread, }; @@ -27,12 +26,12 @@ use self::{menu::MenuPanelManager, window::WindowManager}; struct JokolayGui { menu_panel: Arc>, - egui_context: Arc, + egui_context: egui::Context, glfw_backend: Arc>, } #[allow(unused)] pub struct Jokolay { - gui: Box, + gui: JokolayGui, app: ComponentManager, } @@ -60,8 +59,8 @@ impl Jokolay { .wrap_err("failed to create mumble manager")?, )), ); - let egui_context = Arc::new(egui::Context::default()); - let mumble_ui = Arc::new(RwLock::new(MumbleUIManager::new(Arc::clone(&egui_context)))); + let egui_context = egui::Context::default(); + let mumble_ui = Arc::new(RwLock::new(MumbleUIManager::new(egui_context.clone()))); let _ = component_manager.register("ui:mumbe_ui", mumble_ui.clone()); @@ -87,13 +86,12 @@ impl Jokolay { let _ = component_manager.register( "back:jokolay_package_manager", Arc::new(RwLock::new(PackageDataManager::new( - Arc::clone(&root_dir), //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay )?)), ); let theme_manager = Arc::new(RwLock::new( - ThemeManager::new(Arc::clone(&root_dir), Arc::clone(&egui_context)) + ThemeManager::new(Arc::clone(&root_dir), egui_context.clone()) .wrap_err("failed to create theme manager")?, )); @@ -121,19 +119,17 @@ impl Jokolay { ); let package_manager_ui = Arc::new(RwLock::new(PackageUIManager::new( - Arc::clone(&egui_context), + egui_context.clone(), JokoRenderer::get_z_near(), ))); let _ = component_manager.register("ui:jokolay_package_manager", package_manager_ui.clone()); - let _ = component_manager.register( - "ui:jokolay_renderer", - Arc::new(RwLock::new(JokoRenderer::new( - Arc::clone(&glfw_backend), - Arc::clone(&egui_context), - ))), - ); + let renderer_ui = Arc::new(RwLock::new(JokoRenderer::new( + Arc::clone(&glfw_backend), + egui_context.clone(), + ))); + let _ = component_manager.register("ui:jokolay_renderer", renderer_ui.clone()); let editable_path = jokolay_to_editable_path(&root_path) .to_str() @@ -142,7 +138,7 @@ impl Jokolay { let configuration_ui = Arc::new(RwLock::new(ui_parameters::JokolayUIConfiguration::new( Arc::clone(&glfw_backend), - Arc::clone(&egui_context), + egui_context.clone(), editable_path.clone(), root_path.to_str().unwrap().to_owned(), ))); @@ -157,7 +153,7 @@ impl Jokolay { let menu_panel = Arc::new(RwLock::new(MenuPanelManager::new( Arc::clone(&glfw_backend), - Arc::clone(&egui_context), + egui_context.clone(), ))); let _ = component_manager.register("ui:menu_panel", menu_panel.clone()); @@ -184,6 +180,7 @@ impl Jokolay { menu_panel.register(theme_manager); menu_panel.register(mumble_ui); menu_panel.register(package_manager_ui); + menu_panel.register(renderer_ui); } let gui = JokolayGui { @@ -193,7 +190,7 @@ impl Jokolay { }; //let gui = Mutex::new(gui); //let gui = Arc::new(gui); - let gui = Box::new(gui); + //let gui = Box::new(gui); Ok(Self { gui, app: component_manager, @@ -218,7 +215,7 @@ impl Jokolay { let latest_time = start.elapsed().into_diagnostic()?.as_secs_f64(); executor.tick(latest_time); - thread::sleep(std::time::Duration::from_millis(100)); + thread::sleep(std::time::Duration::from_millis(10)); loop_index += 1; } #[allow(unreachable_code)] @@ -239,14 +236,11 @@ impl Jokolay { ui_executor.init(); loop { - //TODO: one could wrap the egui_context into a plugin result so that it can be used from other plugins - //TODO: same for the UI as a notified element. - let JokolayGui { menu_panel, egui_context, glfw_backend, - } = &mut self.gui.borrow_mut(); + } = &mut self.gui; let latest_time = { let mut glfw_backend = glfw_backend.write().unwrap(); @@ -268,9 +262,8 @@ impl Jokolay { ui_executor.tick(latest_time); if let Ok(mut menu_panel) = menu_panel.write() { - menu_panel.gui(); + menu_panel.gui(latest_time); JokolayTracingLayer::gui(egui_context, &mut menu_panel.show_tracing_window); - //TODO: make it depend on window manager or menu_panel ? } else { println!("cannot update GUI due to lock issues"); } @@ -278,7 +271,7 @@ impl Jokolay { JokolayTracingLayer::show_notifications(egui_context); // end gui stuff - egui_context.request_repaint(); + //egui_context.request_repaint(); /* let animation_time = if ui_configuration.display_parameters.animate { diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs index 9477273..a86a585 100644 --- a/crates/jokolay/src/app/ui_parameters.rs +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -23,11 +23,10 @@ pub enum MessageToApplicationBack { #[derive(Serialize, Deserialize)] pub struct JokolayUIParameters { pub visible_borders: bool, - pub animate: bool, + pub animate: bool, //FIXME: not linked to animation anymore pub editable_path: String, pub root_path: String, - //TODO: folder path for custom work directory - //save configuration into a file + make backups of configuration + //TODO: save configuration into a file + make backups of configuration } struct JokolayUIConfigurationChannels { @@ -44,7 +43,7 @@ pub struct JokolayUIConfiguration { pub average_fps: u32, pub display_parameters: JokolayUIParameters, glfw_backend: Arc>, - egui_context: Arc, + egui_context: egui::Context, channels: Option, } @@ -60,7 +59,7 @@ unsafe impl Sync for JokolayUIConfiguration {} impl JokolayUIConfiguration { pub fn new( glfw_backend: Arc>, - egui_context: Arc, + egui_context: egui::Context, editable_path: String, root_path: String, ) -> Self { @@ -184,7 +183,7 @@ impl UIPanel for JokolayUIConfiguration { } fn init(&mut self) {} - fn gui(&mut self, is_open: &mut bool, _area_id: &str) { + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { let channels = self.channels.as_mut().unwrap(); let u2b_sender = &channels.back_end_notifier; let glfw_backend = Arc::clone(&self.glfw_backend); diff --git a/crates/jokolay/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs index 5933994..474a053 100644 --- a/crates/jokolay/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -14,7 +14,7 @@ pub struct ThemeManager { fonts: BTreeMap>, config: ThemeManagerConfig, ui_data: ThemeUIData, - egui_context: Arc, + egui_context: egui::Context, } #[derive(Debug, Default)] @@ -54,7 +54,7 @@ impl ThemeManager { const DEFAULT_FONT_NAME: &'static str = "default"; const DEFAULT_THEME_NAME: &'static str = "default"; const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; - pub fn new(jokolay_dir: Arc, egui_context: Arc) -> Result { + pub fn new(jokolay_dir: Arc, egui_context: egui::Context) -> Result { jokolay_dir .create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() @@ -228,7 +228,7 @@ impl UIPanel for ThemeManager { error!(%self.config.default_theme, "failed to find the default theme in the loaded themes :("); } } - fn gui(&mut self, is_open: &mut bool, _area_id: &str) { + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { let egui_context = &mut self.egui_context; egui::Window::new("Theme Manager") .open(is_open) diff --git a/documentation/diagrams/category_change.dotuml b/documentation/diagrams/category_change.dotuml new file mode 100644 index 0000000..f64b530 --- /dev/null +++ b/documentation/diagrams/category_change.dotuml @@ -0,0 +1,45 @@ +SequenceDiagram { + actor user + lifeline package_data + control choice_of_category_changed + lifeline package_ui + control list_of_textures_changed + collection categories + collection currently_used_files + collection active_markers + lifeline renderer + + user -a-> package_data "CategorySetAll" + package_data --> choice_of_category_changed "activate" + activate choice_of_category_changed + choice_of_category_changed --> package_data "trigger update" + + + activate package_data + package_data -a-> package_ui "TextureBegin" + package_ui --> active_markers "clear" + package_data -a-> package_ui "per package per marker: MarkerTexture" + package_data -a-> package_ui "per package per trail: TrailTexture" + package_data -a-> package_ui "per package: PackageActiveElements" + package_data -a-> package_ui "CurrentlyUsedFiles" + package_ui --> currently_used_files "replace" + package_data -a-> package_ui "ActiveElements" + package_ui --> categories "update each package" + package_data -a-> package_ui "TextureSwapChain" + package_ui --> list_of_textures_changed "activate" + activate list_of_textures_changed + package_data --> choice_of_category_changed "deactivate" + deactivate choice_of_category_changed + deactivate package_data + + activate package_ui + package_ui --> active_markers "insert" + list_of_textures_changed --> package_ui "trigger update" + package_ui -a-> renderer "RenderBegin" + package_ui -a-> renderer "per package: BulkMarkerObject" + package_ui -a-> renderer "per package: BulkTrailObject" + package_ui -a-> renderer "RenderSwapChain" + package_ui --> list_of_textures_changed "deactivate" + deactivate list_of_textures_changed + deactivate package_ui +} \ No newline at end of file From d2077d821b2b9a25b4943c202118a82c057fa8c9 Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 9 May 2024 17:22:24 +0200 Subject: [PATCH 51/54] fix the editable mumble link + comply with latest clippy warnings --- crates/joko_link_manager/src/lib.rs | 108 +++--- crates/joko_link_models/src/lib.rs | 8 - crates/joko_link_models/src/messages.rs | 4 +- crates/joko_link_ui_manager/src/lib.rs | 33 +- .../src/manager/pack/loaded.rs | 2 +- .../src/manager/package_data.rs | 12 +- .../src/manager/package_ui.rs | 6 +- crates/joko_package_models/src/attributes.rs | 322 +----------------- crates/joko_package_models/src/category.rs | 4 +- crates/joko_package_models/src/package.rs | 2 +- crates/joko_render_manager/src/billboard.rs | 4 +- crates/joko_render_manager/src/renderer.rs | 8 +- crates/jokoapi/src/end_point.rs | 2 + crates/jokoapi/src/end_point/mounts/mod.rs | 7 +- .../jokoapi/src/end_point/professions/mod.rs | 60 ++++ crates/jokoapi/src/end_point/races/mod.rs | 7 +- .../src/end_point/specializations/mod.rs | 248 ++++++++++++++ crates/jokolay/src/app/menu.rs | 6 +- crates/jokolay/src/app/mod.rs | 4 +- crates/jokolay/src/app/window.rs | 34 +- crates/jokolay/src/manager/theme/mod.rs | 2 +- .../diagrams/components_generated.dotuml | 22 ++ documentation/mumble_editable.dotuml | 39 +++ 23 files changed, 510 insertions(+), 434 deletions(-) create mode 100644 documentation/diagrams/components_generated.dotuml create mode 100644 documentation/mumble_editable.dotuml diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs index 761c364..acbbba2 100644 --- a/crates/joko_link_manager/src/lib.rs +++ b/crates/joko_link_manager/src/lib.rs @@ -12,16 +12,15 @@ use std::vec; use enumflags2::BitFlags; use joko_component_models::{ - from_data, to_broadcast, Component, ComponentChannels, ComponentMessage, ComponentResult, + from_data, to_broadcast, to_data, Component, ComponentChannels, ComponentMessage, + ComponentResult, }; use joko_core::serde_glam::{IVec2, UVec2, Vec3}; -use joko_link_models::{ - ctypes, MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult, -}; +use joko_link_models::{ctypes, MessageToMumbleLink, MumbleChanges, MumbleLink}; //use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::{IntoDiagnostic, Result, WrapErr}; use serde_json::from_str; -use tracing::error; +use tracing::{error, trace}; /// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; @@ -37,6 +36,7 @@ use win::MumbleWinImpl as MumblePlatformImpl; struct MumbleChannels { notification_receiver: tokio::sync::mpsc::Receiver, + front_end_notifier: Option>, } // Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there // pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; @@ -51,11 +51,11 @@ pub struct MumbleManager { /// we use this to get the latest mumble link and latest window dimensions of the current mumble link backend: MumblePlatformImpl, is_ui: bool, + repeat_last_value: bool, /// latest mumble link - link: MumbleLink, + link: Option, channels: Option, - state: MumbleLinkResult, } impl MumbleManager { @@ -66,38 +66,53 @@ impl MumbleManager { link: Default::default(), channels: None, is_ui, - state: MumbleLinkResult { - read_ui_link: false, - link: None, - ui_link: None, - }, + repeat_last_value: false, }) } pub fn is_alive(&self) -> bool { self.backend.is_alive() } - fn handle_message(&mut self, msg: MessageToMumbleLinkBack) { - //let (b2u_sender, _) = package_manager.channels(); + fn handle_message(&mut self, msg: MessageToMumbleLink) { match msg { - MessageToMumbleLinkBack::Autonomous => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkAutonomous"); - self.state.read_ui_link = false; + MessageToMumbleLink::Autonomous => { + trace!("Handling of MessageToMumbleLink::Autonomous {}", self.is_ui); + self.repeat_last_value = false; + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = + front_end_notifier.blocking_send(to_data(MessageToMumbleLink::Autonomous)); + } } - MessageToMumbleLinkBack::BindedOnUI => { - tracing::trace!("Handling of UIToBackMessage::MumbleLinkBindedOnUI"); - self.state.read_ui_link = true; + MessageToMumbleLink::BindedOnUI => { + trace!("Handling of MessageToMumbleLink::BindedOnUI {}", self.is_ui); + self.repeat_last_value = true; + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = + front_end_notifier.blocking_send(to_data(MessageToMumbleLink::BindedOnUI)); + } } - MessageToMumbleLinkBack::Value(link) => { - tracing::trace!("Handling of UIToBackMessage::MumbleLink"); - self.state.ui_link = link; + MessageToMumbleLink::Value(mut link) => { + trace!("Handling of MessageToMumbleLink::Value {}", self.is_ui); + link.changes = BitFlags::all(); + self.link = Some(link); + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = front_end_notifier.blocking_send(to_data(MessageToMumbleLink::Value( + self.link.clone().unwrap(), + ))); + } } #[allow(unreachable_patterns)] _ => { - unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + unimplemented!("Handling MessageToMumbleLink has not been implemented yet"); } } } - fn _tick(&mut self) -> Result> { + fn _tick(&mut self) -> Result> { if let Err(e) = self.backend.tick() { error!(?e, "mumble backend tick error"); return Ok(None); @@ -105,17 +120,19 @@ impl MumbleManager { //println!("mumble_link {} map found {}", self.is_ui, self.link.map_id); if !self.backend.is_alive() { - self.link.client_size.0.x = 0; - self.link.client_size.0.y = 0; - self.link.changes = BitFlags::all(); - return Ok(Some(&self.link)); + if let Some(link) = self.link.as_mut() { + link.client_size.0.x = 0; + link.client_size.0.y = 0; + link.changes = BitFlags::all(); + } + return Ok(self.link.clone()); } // backend is alive and tick is successful. time to get link let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); - let mut new_link = if cml.ui_tick == 0 && self.link.ui_tick != 0 { + let mut new_link = if cml.ui_tick == 0 && self.link.is_some() { Default::default() } else { - self.link.clone() + self.link.clone().unwrap_or_default() }; if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { @@ -214,13 +231,9 @@ impl MumbleManager { mount: cml.context.mount_index, race: identity.race, }; - self.link = new_link; + self.link = Some(new_link); - Ok(if self.link.ui_tick == 0 { - None - } else { - Some(&self.link) - }) + Ok(self.link.clone()) } } @@ -229,7 +242,7 @@ impl Component for MumbleManager { fn accept_notifications(&self) -> bool { // we may want to receive data from a manually edited form - !self.is_ui + true } fn flush_all_messages(&mut self) { @@ -252,22 +265,23 @@ impl Component for MumbleManager { self.channels.is_some(), "channels must be initialized before interacting with component." ); - let link = self._tick().unwrap_or(None); - self.state.link = link.cloned(); - //println!("mumble_link result {} has link: {}", self.is_ui, self.state.link.is_some()); - to_broadcast(self.state.clone()) + if self.repeat_last_value { + to_broadcast(self.link.clone()) + } else { + let link = self._tick().unwrap_or(None); + to_broadcast(link) + } } fn bind(&mut self, mut channels: ComponentChannels) { - let (_, notification_receiver) = channels.peers.remove(&0).unwrap(); let channels = MumbleChannels { - notification_receiver, + notification_receiver: channels.input_notification.unwrap(), + front_end_notifier: channels.notify.remove(&0), }; self.channels = Some(channels); } - //default is enough - fn peers(&self) -> Vec<&str> { + fn notify(&self) -> Vec<&str> { if self.is_ui { - vec!["back:mumble_link"] + vec![] } else { vec!["ui:mumble_link"] } diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs index 9306c30..3717129 100644 --- a/crates/joko_link_models/src/lib.rs +++ b/crates/joko_link_models/src/lib.rs @@ -13,11 +13,3 @@ mod mumble; pub use messages::*; pub use mumble::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Serialize, Deserialize, Default)] -pub struct MumbleLinkResult { - pub read_ui_link: bool, - pub link: Option, - pub ui_link: Option, -} diff --git a/crates/joko_link_models/src/messages.rs b/crates/joko_link_models/src/messages.rs index f112cda..8b5d85d 100644 --- a/crates/joko_link_models/src/messages.rs +++ b/crates/joko_link_models/src/messages.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::MumbleLink; #[derive(Clone, Serialize, Deserialize)] -pub enum MessageToMumbleLinkBack { +pub enum MessageToMumbleLink { BindedOnUI, Autonomous, - Value(Option), //pushed from a value imposed by UI. Either a form or a traveling for demo. + Value(MumbleLink), //pushed from a value imposed by UI. Either a form or a traveling for demo. } diff --git a/crates/joko_link_ui_manager/src/lib.rs b/crates/joko_link_ui_manager/src/lib.rs index 05ccf24..1c5f5e6 100644 --- a/crates/joko_link_ui_manager/src/lib.rs +++ b/crates/joko_link_ui_manager/src/lib.rs @@ -4,7 +4,7 @@ use egui::DragValue; use joko_component_models::{ default_component_result, from_broadcast, to_data, Component, ComponentMessage, ComponentResult, }; -use joko_link_models::{MessageToMumbleLinkBack, MumbleLink, MumbleLinkResult}; +use joko_link_models::{MessageToMumbleLink, MumbleLink}; use joko_ui_models::{UIArea, UIPanel}; struct MumbleUIManagerChannels { @@ -291,7 +291,12 @@ impl Component for MumbleUIManager { }; self.channels = Some(channels); } - fn flush_all_messages(&mut self) {} + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + } fn accept_notifications(&self) -> bool { false } @@ -306,12 +311,16 @@ impl Component for MumbleUIManager { vec![] } fn tick(&mut self, _latest_time: f64) -> joko_component_models::ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); if let Ok(link) = channels.subscription_mumble_link.try_recv() { - let data: MumbleLinkResult = from_broadcast(&link); - if data.read_ui_link || self.editable_mumble { - } else if let Some(link) = data.link { + let link: Option = from_broadcast(&link); + if self.editable_mumble { + } else if let Some(link) = link { self.last_known_link = link; } } @@ -329,6 +338,10 @@ impl UIPanel for MumbleUIManager { } fn init(&mut self) {} fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); let channels = self.channels.as_mut().unwrap(); let back_end_notifier = channels.back_end_notifier.borrow_mut(); let egui_context = &self.egui_context; @@ -340,7 +353,7 @@ impl UIPanel for MumbleUIManager { if ui.selectable_label(!self.editable_mumble, "live").clicked() { self.editable_mumble = false; let _ = back_end_notifier - .blocking_send(to_data(MessageToMumbleLinkBack::Autonomous)); + .blocking_send(to_data(MessageToMumbleLink::Autonomous)); } if ui .selectable_label(self.editable_mumble, "editable") @@ -348,7 +361,7 @@ impl UIPanel for MumbleUIManager { { self.editable_mumble = true; let _ = back_end_notifier - .blocking_send(to_data(MessageToMumbleLinkBack::BindedOnUI)); + .blocking_send(to_data(MessageToMumbleLink::BindedOnUI)); } }); if self.editable_mumble { @@ -358,11 +371,17 @@ impl UIPanel for MumbleUIManager { ) .color(egui::Color32::RED), ); + //TODO: how to detect there was a change in value, to only propagate changed values ? Self::editable_mumble_ui(ui, &mut self.last_known_link); } else { let link: MumbleLink = self.last_known_link.clone(); Self::live_mumble_ui(ui, link); } }); + if self.editable_mumble { + let _ = back_end_notifier.blocking_send(to_data(MessageToMumbleLink::Value( + self.last_known_link.clone(), + ))); + } } } diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index 31de32c..9af007c 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -408,7 +408,7 @@ impl LoadedPackData { let _ = front_end_notifier.blocking_send(to_data( MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()), )); - self.active_elements = active_elements.clone(); + self.active_elements.clone_from(&active_elements); next_loaded.extend(active_elements); } diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs index 172e990..8f0f8e4 100644 --- a/crates/joko_package_manager/src/manager/package_data.rs +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -4,6 +4,7 @@ use joko_component_models::{ default_component_result, from_broadcast, from_data, to_data, Component, ComponentChannels, ComponentMessage, ComponentResult, }; +use joko_link_models::MumbleLink; use joko_package_models::package::PackageImportReport; use tracing::{error, info, info_span, trace}; @@ -13,7 +14,6 @@ use crate::{ jokolay_to_extract_path, message::{MessageToPackageBack, MessageToPackageUI}, }; -use joko_link_models::MumbleLinkResult; use miette::{IntoDiagnostic, Result}; use uuid::Uuid; @@ -334,13 +334,7 @@ impl PackageDataManager { } } - pub fn _tick(&mut self, mumble_link_result: &MumbleLinkResult) { - let link = if mumble_link_result.read_ui_link { - mumble_link_result.ui_link.as_ref() - } else { - mumble_link_result.link.as_ref() - }; - + pub fn _tick(&mut self, link: &Option) { if let Some(link) = link { //TODO: how to save/load the active files ? let mut have_used_files_list_changed = false; @@ -548,7 +542,7 @@ impl Component for PackageDataManager { let channels = self.channels.as_mut().unwrap(); //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); let raw_mlr = channels.subscription_mumblelink.try_recv().unwrap(); - let mumble_link_result: MumbleLinkResult = from_broadcast(&raw_mlr); + let mumble_link_result: Option = from_broadcast(&raw_mlr); //trace!("subscription_mumblelink provided data"); self._tick(&mumble_link_result); default_component_result() diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs index 471bbe4..64f7687 100644 --- a/crates/joko_package_manager/src/manager/package_ui.rs +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -19,7 +19,7 @@ use joko_component_models::{ ComponentMessage, ComponentResult, }; use joko_core::{serde_glam::Vec3, RelativePath}; -use joko_link_models::{MumbleChanges, MumbleLink, MumbleLinkResult}; +use joko_link_models::{MumbleChanges, MumbleLink}; use miette::Result; use uuid::Uuid; @@ -683,7 +683,7 @@ impl Component for PackageUIManager { //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); channels.subscription_mumblelink.try_recv().unwrap() }; - let link_result: MumbleLinkResult = from_broadcast(&raw_link); + let link: Option = from_broadcast(&raw_link); //trace!("subscription_mumblelink provided data"); for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in @@ -713,7 +713,7 @@ impl Component for PackageUIManager { //let channels = self.channels.as_mut().unwrap(); //let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); //let z_near: f32 = from_data(raw_z_near); - if let Some(link) = link_result.link.as_ref() { + if let Some(link) = link.as_ref() { let _ = self._tick(timestamp, link, self.z_near); } to_broadcast(self.state.clone()) diff --git a/crates/joko_package_models/src/attributes.rs b/crates/joko_package_models/src/attributes.rs index 3236c03..099fc42 100644 --- a/crates/joko_package_models/src/attributes.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -3,6 +3,8 @@ use std::str::FromStr; use enumflags2::{bitflags, BitFlags}; use itertools::Itertools; use joko_core::serde_glam::Vec3; +use jokoapi::end_point::professions::Profession; +use jokoapi::end_point::specializations::Specialization; use serde::{Deserialize, Serialize}; use tracing::info; use xot::{Element, NameId, Xot}; @@ -1077,63 +1079,6 @@ impl FromStr for Behavior { } } -/// Filter which professions the marker should be active for. if its null, its available for all professions -#[bitflags] -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum Profession { - Elementalist = 1 << 0, - Engineer = 1 << 1, - Guardian = 1 << 2, - Mesmer = 1 << 3, - Necromancer = 1 << 4, - Ranger = 1 << 5, - Revenant = 1 << 6, - Thief = 1 << 7, - Warrior = 1 << 8, -} - -impl FromStr for Profession { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "guardian" => Profession::Guardian, - "warrior" => Profession::Warrior, - "engineer" => Profession::Engineer, - "ranger" => Profession::Ranger, - "thief" => Profession::Thief, - "elementalist" => Profession::Elementalist, - "mesmer" => Profession::Mesmer, - "necromancer" => Profession::Necromancer, - "revenant" => Profession::Revenant, - _ => return Err("invalid profession"), - }) - } -} - -impl AsRef for Profession { - fn as_ref(&self) -> &str { - match self { - Profession::Guardian => "guardian", - Profession::Warrior => "warrior", - Profession::Engineer => "engineer", - Profession::Ranger => "ranger", - Profession::Thief => "thief", - Profession::Elementalist => "elementalist", - Profession::Mesmer => "mesmer", - Profession::Necromancer => "necromancer", - Profession::Revenant => "revenant", - } - } -} - -impl ToString for Profession { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} - #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub enum Cull { #[default] @@ -1167,9 +1112,9 @@ impl AsRef for Cull { } } -impl ToString for Cull { - fn to_string(&self) -> String { - self.as_ref().to_string() +impl std::fmt::Display for Cull { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } @@ -1216,254 +1161,9 @@ impl AsRef for Festival { } } -impl ToString for Festival { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} - -/// Filter for which specializations (the third traitline) will the marker be active for -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[repr(u8)] -pub enum Specialization { - Dueling = 0, - DeathMagic = 1, - Invocation = 2, - Strength = 3, - Druid = 4, - Explosives = 5, - Daredevil = 6, - Marksmanship = 7, - Retribution = 8, - Domination = 9, - Tactics = 10, - Salvation = 11, - Valor = 12, - Corruption = 13, - Devastation = 14, - Radiance = 15, - Water = 16, - Berserker = 17, - BloodMagic = 18, - ShadowArts = 19, - Tools = 20, - Defense = 21, - Inspiration = 22, - Illusions = 23, - NatureMagic = 24, - Earth = 25, - Dragonhunter = 26, - DeadlyArts = 27, - Alchemy = 28, - Skirmishing = 29, - Fire = 30, - BeastMastery = 31, - WildernessSurvival = 32, - Reaper = 33, - CriticalStrikes = 34, - Arms = 35, - Arcane = 36, - Firearms = 37, - Curses = 38, - Chronomancer = 39, - Air = 40, - Zeal = 41, - Scrapper = 42, - Trickery = 43, - Chaos = 44, - Virtues = 45, - Inventions = 46, - Tempest = 47, - Honor = 48, - SoulReaping = 49, - Discipline = 50, - Herald = 51, - Spite = 52, - Acrobatics = 53, - Soulbeast = 54, - Weaver = 55, - Holosmith = 56, - Deadeye = 57, - Mirage = 58, - Scourge = 59, - Spellbreaker = 60, - Firebrand = 61, - Renegade = 62, - Harbinger = 63, - Willbender = 64, - Virtuoso = 65, - Catalyst = 66, - Bladesworn = 67, - Vindicator = 68, - Mechanist = 69, - Specter = 70, - Untamed = 71, -} - -impl FromStr for Specialization { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "dueling" => Self::Dueling, - "deathmagic" => Self::DeathMagic, - "invocation" => Self::Invocation, - "strength" => Self::Strength, - "druid" => Self::Druid, - "explosives" => Self::Explosives, - "daredevil" => Self::Daredevil, - "marksmanship" => Self::Marksmanship, - "retribution" => Self::Retribution, - "domination" => Self::Domination, - "tactics" => Self::Tactics, - "salvation" => Self::Salvation, - "valor" => Self::Valor, - "corruption" => Self::Corruption, - "devastation" => Self::Devastation, - "radiance" => Self::Radiance, - "water" => Self::Water, - "berserker" => Self::Berserker, - "bloodmagic" => Self::BloodMagic, - "shadowarts" => Self::ShadowArts, - "tools" => Self::Tools, - "defense" => Self::Defense, - "inspiration" => Self::Inspiration, - "illusions" => Self::Illusions, - "naturemagic" => Self::NatureMagic, - "earth" => Self::Earth, - "dragonhunter" => Self::Dragonhunter, - "deadlyarts" => Self::DeadlyArts, - "alchemy" => Self::Alchemy, - "skirmishing" => Self::Skirmishing, - "fire" => Self::Fire, - "beastmastery" => Self::BeastMastery, - "wildernesssurvival" => Self::WildernessSurvival, - "reaper" => Self::Reaper, - "criticalstrikes" => Self::CriticalStrikes, - "arms" => Self::Arms, - "arcane" => Self::Arcane, - "firearms" => Self::Firearms, - "curses" => Self::Curses, - "chronomancer" => Self::Chronomancer, - "air" => Self::Air, - "zeal" => Self::Zeal, - "scrapper" => Self::Scrapper, - "trickery" => Self::Trickery, - "chaos" => Self::Chaos, - "virtues" => Self::Virtues, - "inventions" => Self::Inventions, - "tempest" => Self::Tempest, - "honor" => Self::Honor, - "soulreaping" => Self::SoulReaping, - "discipline" => Self::Discipline, - "herald" => Self::Herald, - "spite" => Self::Spite, - "acrobatics" => Self::Acrobatics, - "soulbeast" => Self::Soulbeast, - "weaver" => Self::Weaver, - "holosmith" => Self::Holosmith, - "deadeye" => Self::Deadeye, - "mirage" => Self::Mirage, - "scourge" => Self::Scourge, - "spellbreaker" => Self::Spellbreaker, - "firebrand" => Self::Firebrand, - "renegade" => Self::Renegade, - "harbinger" => Self::Harbinger, - "willbender" => Self::Willbender, - "virtuoso" => Self::Virtuoso, - "catalyst" => Self::Catalyst, - "bladesworn" => Self::Bladesworn, - "vindicator" => Self::Vindicator, - "mechanist" => Self::Mechanist, - "specter" => Self::Specter, - "untamed" => Self::Untamed, - _ => return Err("invalid specialization"), - }) - } -} - -impl AsRef for Specialization { - fn as_ref(&self) -> &str { - match self { - Self::Dueling => "dueling", - Self::DeathMagic => "deathmagic", - Self::Invocation => "invocation", - Self::Strength => "strength", - Self::Druid => "druid", - Self::Explosives => "explosives", - Self::Daredevil => "daredevil", - Self::Marksmanship => "marksmanship", - Self::Retribution => "retribution", - Self::Domination => "domination", - Self::Tactics => "tactics", - Self::Salvation => "salvation", - Self::Valor => "valor", - Self::Corruption => "corruption", - Self::Devastation => "devastation", - Self::Radiance => "radiance", - Self::Water => "water", - Self::Berserker => "berserker", - Self::BloodMagic => "bloodmagic", - Self::ShadowArts => "shadowarts", - Self::Tools => "tools", - Self::Defense => "defense", - Self::Inspiration => "inspiration", - Self::Illusions => "illusions", - Self::NatureMagic => "naturemagic", - Self::Earth => "earth", - Self::Dragonhunter => "dragonhunter", - Self::DeadlyArts => "deadlyarts", - Self::Alchemy => "alchemy", - Self::Skirmishing => "skirmishing", - Self::Fire => "fire", - Self::BeastMastery => "beastmastery", - Self::WildernessSurvival => "wildernesssurvival", - Self::Reaper => "reaper", - Self::CriticalStrikes => "criticalstrikes", - Self::Arms => "arms", - Self::Arcane => "arcane", - Self::Firearms => "firearms", - Self::Curses => "curses", - Self::Chronomancer => "chronomancer", - Self::Air => "air", - Self::Zeal => "zeal", - Self::Scrapper => "scrapper", - Self::Trickery => "trickery", - Self::Chaos => "chaos", - Self::Virtues => "virtues", - Self::Inventions => "inventions", - Self::Tempest => "tempest", - Self::Honor => "honor", - Self::SoulReaping => "soulreaping", - Self::Discipline => "discipline", - Self::Herald => "herald", - Self::Spite => "spite", - Self::Acrobatics => "acrobatics", - Self::Soulbeast => "soulbeast", - Self::Weaver => "weaver", - Self::Holosmith => "holosmith", - Self::Deadeye => "deadeye", - Self::Mirage => "mirage", - Self::Scourge => "scourge", - Self::Spellbreaker => "spellbreaker", - Self::Firebrand => "firebrand", - Self::Renegade => "renegade", - Self::Harbinger => "harbinger", - Self::Willbender => "willbender", - Self::Virtuoso => "virtuoso", - Self::Catalyst => "catalyst", - Self::Bladesworn => "bladesworn", - Self::Vindicator => "vindicator", - Self::Mechanist => "mechanist", - Self::Specter => "specter", - Self::Untamed => "untamed", - } - } -} - -impl ToString for Specialization { - fn to_string(&self) -> String { - self.as_ref().to_string() +impl std::fmt::Display for Festival { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } @@ -1525,9 +1225,9 @@ impl AsRef for MapType { } } -impl ToString for MapType { - fn to_string(&self) -> String { - self.as_ref().to_string() +impl std::fmt::Display for MapType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index 086cb54..b949a95 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -198,7 +198,9 @@ impl Category { report.found_category_late(&value.full_category_name, value.guid); to_insert.relative_category_name = nth_chunk(&value.relative_category_name, '.', n); - to_insert.display_name = to_insert.relative_category_name.clone(); + to_insert + .display_name + .clone_from(&to_insert.relative_category_name); debug!( "parent_name: {:?}, new name: {}, old name: {}", last_name, to_insert.relative_category_name, &value.relative_category_name diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index d55c8fb..0bb4958 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -246,7 +246,7 @@ impl PackCore { pub fn partial(all_categories: &HashMap) -> Self { // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. let mut res: Self = Self::new(); - res.all_categories = all_categories.clone(); + res.all_categories.clone_from(all_categories); res } diff --git a/crates/joko_render_manager/src/billboard.rs b/crates/joko_render_manager/src/billboard.rs index c240787..273175e 100644 --- a/crates/joko_render_manager/src/billboard.rs +++ b/crates/joko_render_manager/src/billboard.rs @@ -76,8 +76,8 @@ impl BillBoardRenderer { self.markers_wip.len(), self.trails_wip.len() ); - self.markers = self.markers_wip.clone(); - self.trails = self.trails_wip.clone(); + self.markers.clone_from(&self.markers_wip); + self.trails.clone_from(&self.trails_wip); } pub fn swap(&mut self) { trace!( diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs index 162f34a..9d15d8e 100644 --- a/crates/joko_render_manager/src/renderer.rs +++ b/crates/joko_render_manager/src/renderer.rs @@ -23,7 +23,7 @@ use joko_component_models::Component; use joko_component_models::ComponentChannels; use joko_component_models::ComponentMessage; use joko_component_models::ComponentResult; -use joko_link_models::MumbleLinkResult; +use joko_link_models::MumbleLink; use joko_link_models::UIState; use joko_render_models::messages::MessageToRenderer; use joko_ui_models::UIArea; @@ -49,7 +49,7 @@ pub struct JokoRenderer { egui_context: egui::Context, pub gl: egui_render_three_d::ThreeDBackend, channels: Option, - link: MumbleLinkResult, + link: Option, } /// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation @@ -348,7 +348,7 @@ impl Component for JokoRenderer { let channels = self.channels.as_mut().unwrap(); let raw_link = channels.subscription_mumble_link.blocking_recv().unwrap(); - let link: MumbleLinkResult = from_broadcast(&raw_link); + let link: Option = from_broadcast(&raw_link); self.link = link; default_component_result() } @@ -366,7 +366,7 @@ impl UIPanel for JokoRenderer { fn gui(&mut self, _is_open: &mut bool, _area_id: &str, latest_time: f64) { self._window_tick(); - if let Some(link) = &self.link.link { + if let Some(link) = &self.link { //trace!("JokoRenderer {:?} {:?}", link.player_pos, link.cam_pos); //x positive => east //y positive => ascention diff --git a/crates/jokoapi/src/end_point.rs b/crates/jokoapi/src/end_point.rs index cf942e6..affabf9 100644 --- a/crates/jokoapi/src/end_point.rs +++ b/crates/jokoapi/src/end_point.rs @@ -14,7 +14,9 @@ pub use serde::{Deserialize, Serialize}; // pub mod quaggans; // pub mod races; pub mod mounts; +pub mod professions; pub mod races; +pub mod specializations; pub mod worlds; const AUTHORIZATION_HEADER_NAME: &str = "Authorization"; diff --git a/crates/jokoapi/src/end_point/mounts/mod.rs b/crates/jokoapi/src/end_point/mounts/mod.rs index cc6feef..374410b 100644 --- a/crates/jokoapi/src/end_point/mounts/mod.rs +++ b/crates/jokoapi/src/end_point/mounts/mod.rs @@ -70,8 +70,9 @@ impl AsRef for Mount { } } } -impl ToString for Mount { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for Mount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/crates/jokoapi/src/end_point/professions/mod.rs b/crates/jokoapi/src/end_point/professions/mod.rs index e69de29..ad5d358 100644 --- a/crates/jokoapi/src/end_point/professions/mod.rs +++ b/crates/jokoapi/src/end_point/professions/mod.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +use crate::prelude::*; + +/// Filter which professions the marker should be active for. if its null, its available for all professions +#[bitflags] +#[repr(u16)] +#[derive(Debug, Clone, Copy)] +pub enum Profession { + Elementalist = 1 << 0, + Engineer = 1 << 1, + Guardian = 1 << 2, + Mesmer = 1 << 3, + Necromancer = 1 << 4, + Ranger = 1 << 5, + Revenant = 1 << 6, + Thief = 1 << 7, + Warrior = 1 << 8, +} + +impl FromStr for Profession { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "guardian" => Profession::Guardian, + "warrior" => Profession::Warrior, + "engineer" => Profession::Engineer, + "ranger" => Profession::Ranger, + "thief" => Profession::Thief, + "elementalist" => Profession::Elementalist, + "mesmer" => Profession::Mesmer, + "necromancer" => Profession::Necromancer, + "revenant" => Profession::Revenant, + _ => return Err("invalid profession"), + }) + } +} + +impl AsRef for Profession { + fn as_ref(&self) -> &str { + match self { + Profession::Guardian => "guardian", + Profession::Warrior => "warrior", + Profession::Engineer => "engineer", + Profession::Ranger => "ranger", + Profession::Thief => "thief", + Profession::Elementalist => "elementalist", + Profession::Mesmer => "mesmer", + Profession::Necromancer => "necromancer", + Profession::Revenant => "revenant", + } + } +} + +impl std::fmt::Display for Profession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) + } +} diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index 7e3b402..202605c 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -54,8 +54,9 @@ impl AsRef for Race { } } } -impl ToString for Race { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for Race { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/crates/jokoapi/src/end_point/specializations/mod.rs b/crates/jokoapi/src/end_point/specializations/mod.rs index e69de29..a9058c8 100644 --- a/crates/jokoapi/src/end_point/specializations/mod.rs +++ b/crates/jokoapi/src/end_point/specializations/mod.rs @@ -0,0 +1,248 @@ +use std::str::FromStr; + +use crate::prelude::*; + +/// Filter for which specializations (the third traitline) will the marker be active for +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[repr(u8)] +pub enum Specialization { + Dueling = 0, + DeathMagic = 1, + Invocation = 2, + Strength = 3, + Druid = 4, + Explosives = 5, + Daredevil = 6, + Marksmanship = 7, + Retribution = 8, + Domination = 9, + Tactics = 10, + Salvation = 11, + Valor = 12, + Corruption = 13, + Devastation = 14, + Radiance = 15, + Water = 16, + Berserker = 17, + BloodMagic = 18, + ShadowArts = 19, + Tools = 20, + Defense = 21, + Inspiration = 22, + Illusions = 23, + NatureMagic = 24, + Earth = 25, + Dragonhunter = 26, + DeadlyArts = 27, + Alchemy = 28, + Skirmishing = 29, + Fire = 30, + BeastMastery = 31, + WildernessSurvival = 32, + Reaper = 33, + CriticalStrikes = 34, + Arms = 35, + Arcane = 36, + Firearms = 37, + Curses = 38, + Chronomancer = 39, + Air = 40, + Zeal = 41, + Scrapper = 42, + Trickery = 43, + Chaos = 44, + Virtues = 45, + Inventions = 46, + Tempest = 47, + Honor = 48, + SoulReaping = 49, + Discipline = 50, + Herald = 51, + Spite = 52, + Acrobatics = 53, + Soulbeast = 54, + Weaver = 55, + Holosmith = 56, + Deadeye = 57, + Mirage = 58, + Scourge = 59, + Spellbreaker = 60, + Firebrand = 61, + Renegade = 62, + Harbinger = 63, + Willbender = 64, + Virtuoso = 65, + Catalyst = 66, + Bladesworn = 67, + Vindicator = 68, + Mechanist = 69, + Specter = 70, + Untamed = 71, +} + +impl FromStr for Specialization { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "dueling" => Self::Dueling, + "deathmagic" => Self::DeathMagic, + "invocation" => Self::Invocation, + "strength" => Self::Strength, + "druid" => Self::Druid, + "explosives" => Self::Explosives, + "daredevil" => Self::Daredevil, + "marksmanship" => Self::Marksmanship, + "retribution" => Self::Retribution, + "domination" => Self::Domination, + "tactics" => Self::Tactics, + "salvation" => Self::Salvation, + "valor" => Self::Valor, + "corruption" => Self::Corruption, + "devastation" => Self::Devastation, + "radiance" => Self::Radiance, + "water" => Self::Water, + "berserker" => Self::Berserker, + "bloodmagic" => Self::BloodMagic, + "shadowarts" => Self::ShadowArts, + "tools" => Self::Tools, + "defense" => Self::Defense, + "inspiration" => Self::Inspiration, + "illusions" => Self::Illusions, + "naturemagic" => Self::NatureMagic, + "earth" => Self::Earth, + "dragonhunter" => Self::Dragonhunter, + "deadlyarts" => Self::DeadlyArts, + "alchemy" => Self::Alchemy, + "skirmishing" => Self::Skirmishing, + "fire" => Self::Fire, + "beastmastery" => Self::BeastMastery, + "wildernesssurvival" => Self::WildernessSurvival, + "reaper" => Self::Reaper, + "criticalstrikes" => Self::CriticalStrikes, + "arms" => Self::Arms, + "arcane" => Self::Arcane, + "firearms" => Self::Firearms, + "curses" => Self::Curses, + "chronomancer" => Self::Chronomancer, + "air" => Self::Air, + "zeal" => Self::Zeal, + "scrapper" => Self::Scrapper, + "trickery" => Self::Trickery, + "chaos" => Self::Chaos, + "virtues" => Self::Virtues, + "inventions" => Self::Inventions, + "tempest" => Self::Tempest, + "honor" => Self::Honor, + "soulreaping" => Self::SoulReaping, + "discipline" => Self::Discipline, + "herald" => Self::Herald, + "spite" => Self::Spite, + "acrobatics" => Self::Acrobatics, + "soulbeast" => Self::Soulbeast, + "weaver" => Self::Weaver, + "holosmith" => Self::Holosmith, + "deadeye" => Self::Deadeye, + "mirage" => Self::Mirage, + "scourge" => Self::Scourge, + "spellbreaker" => Self::Spellbreaker, + "firebrand" => Self::Firebrand, + "renegade" => Self::Renegade, + "harbinger" => Self::Harbinger, + "willbender" => Self::Willbender, + "virtuoso" => Self::Virtuoso, + "catalyst" => Self::Catalyst, + "bladesworn" => Self::Bladesworn, + "vindicator" => Self::Vindicator, + "mechanist" => Self::Mechanist, + "specter" => Self::Specter, + "untamed" => Self::Untamed, + _ => return Err("invalid specialization"), + }) + } +} + +impl AsRef for Specialization { + fn as_ref(&self) -> &str { + match self { + Self::Dueling => "dueling", + Self::DeathMagic => "deathmagic", + Self::Invocation => "invocation", + Self::Strength => "strength", + Self::Druid => "druid", + Self::Explosives => "explosives", + Self::Daredevil => "daredevil", + Self::Marksmanship => "marksmanship", + Self::Retribution => "retribution", + Self::Domination => "domination", + Self::Tactics => "tactics", + Self::Salvation => "salvation", + Self::Valor => "valor", + Self::Corruption => "corruption", + Self::Devastation => "devastation", + Self::Radiance => "radiance", + Self::Water => "water", + Self::Berserker => "berserker", + Self::BloodMagic => "bloodmagic", + Self::ShadowArts => "shadowarts", + Self::Tools => "tools", + Self::Defense => "defense", + Self::Inspiration => "inspiration", + Self::Illusions => "illusions", + Self::NatureMagic => "naturemagic", + Self::Earth => "earth", + Self::Dragonhunter => "dragonhunter", + Self::DeadlyArts => "deadlyarts", + Self::Alchemy => "alchemy", + Self::Skirmishing => "skirmishing", + Self::Fire => "fire", + Self::BeastMastery => "beastmastery", + Self::WildernessSurvival => "wildernesssurvival", + Self::Reaper => "reaper", + Self::CriticalStrikes => "criticalstrikes", + Self::Arms => "arms", + Self::Arcane => "arcane", + Self::Firearms => "firearms", + Self::Curses => "curses", + Self::Chronomancer => "chronomancer", + Self::Air => "air", + Self::Zeal => "zeal", + Self::Scrapper => "scrapper", + Self::Trickery => "trickery", + Self::Chaos => "chaos", + Self::Virtues => "virtues", + Self::Inventions => "inventions", + Self::Tempest => "tempest", + Self::Honor => "honor", + Self::SoulReaping => "soulreaping", + Self::Discipline => "discipline", + Self::Herald => "herald", + Self::Spite => "spite", + Self::Acrobatics => "acrobatics", + Self::Soulbeast => "soulbeast", + Self::Weaver => "weaver", + Self::Holosmith => "holosmith", + Self::Deadeye => "deadeye", + Self::Mirage => "mirage", + Self::Scourge => "scourge", + Self::Spellbreaker => "spellbreaker", + Self::Firebrand => "firebrand", + Self::Renegade => "renegade", + Self::Harbinger => "harbinger", + Self::Willbender => "willbender", + Self::Virtuoso => "virtuoso", + Self::Catalyst => "catalyst", + Self::Bladesworn => "bladesworn", + Self::Vindicator => "vindicator", + Self::Mechanist => "mechanist", + Self::Specter => "specter", + Self::Untamed => "untamed", + } + } +} + +impl std::fmt::Display for Specialization { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) + } +} diff --git a/crates/jokolay/src/app/menu.rs b/crates/jokolay/src/app/menu.rs index c6eed64..aee7f08 100644 --- a/crates/jokolay/src/app/menu.rs +++ b/crates/jokolay/src/app/menu.rs @@ -4,7 +4,7 @@ use egui_window_glfw_passthrough::GlfwBackend; use joko_component_models::{ default_component_result, from_broadcast, Component, ComponentChannels, ComponentResult, }; -use joko_link_models::{MumbleLinkResult, UISize}; +use joko_link_models::{MumbleLink, UISize}; use joko_ui_models::{UIArea, UIPanel}; use tracing::info; @@ -259,10 +259,10 @@ impl Component for MenuPanelManager { let channels = self.channels.as_mut().unwrap(); channels.subscription_mumblelink.try_recv().unwrap() }; - let link_result: MumbleLinkResult = from_broadcast(&raw_link); + let link: Option = from_broadcast(&raw_link); let mut ui_scaling_factor = 1.0; - if let Some(link) = link_result.link.as_ref() { + if let Some(link) = link.as_ref() { let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { (if link.dpi == 0 { 96.0 } else { link.dpi as f32 }) / 96.0 } else { diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index ad24cc2..32dd91a 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -62,7 +62,9 @@ impl Jokolay { let egui_context = egui::Context::default(); let mumble_ui = Arc::new(RwLock::new(MumbleUIManager::new(egui_context.clone()))); - let _ = component_manager.register("ui:mumbe_ui", mumble_ui.clone()); + component_manager + .register("ui:mumble_ui", mumble_ui.clone()) + .unwrap(); /* components can be migrated to plugins diff --git a/crates/jokolay/src/app/window.rs b/crates/jokolay/src/app/window.rs index 77eab99..cdb20e1 100644 --- a/crates/jokolay/src/app/window.rs +++ b/crates/jokolay/src/app/window.rs @@ -2,10 +2,9 @@ use std::sync::{Arc, RwLock}; use egui_window_glfw_passthrough::GlfwBackend; use joko_component_models::{ - default_component_result, from_broadcast, to_data, Component, ComponentChannels, - ComponentMessage, ComponentResult, + default_component_result, from_broadcast, Component, ComponentChannels, ComponentResult, }; -use joko_link_models::{MessageToMumbleLinkBack, MumbleChanges, MumbleLink, MumbleLinkResult}; +use joko_link_models::{MumbleChanges, MumbleLink}; pub(crate) const MINIMAL_WINDOW_WIDTH: u32 = 640; pub(crate) const MINIMAL_WINDOW_HEIGHT: u32 = 480; @@ -14,14 +13,12 @@ pub(crate) const MINIMAL_WINDOW_POSITION_Y: i32 = 0; struct WindowManagerChannels { subscription_mumblelink: tokio::sync::broadcast::Receiver, - mumble_link_back_notifier: tokio::sync::mpsc::Sender, } pub(crate) struct WindowManager { glfw_backend: Arc>, window_changed: bool, maximal_window_width: u32, maximal_window_height: u32, - editable_mumble: bool, last_known_link: Option, channels: Option, } @@ -51,7 +48,6 @@ impl WindowManager { window_changed: true, maximal_window_width, maximal_window_height, - editable_mumble: false, last_known_link: None, channels: None, } @@ -69,40 +65,24 @@ impl Component for WindowManager { fn bind(&mut self, mut channels: ComponentChannels) { let channels = WindowManagerChannels { subscription_mumblelink: channels.requirements.remove(&0).unwrap(), - mumble_link_back_notifier: channels.notify.remove(&1).unwrap(), }; self.channels = Some(channels); } - fn flush_all_messages(&mut self) { - //unimplemented!() - } - fn init(&mut self) { - //unimplemented!() - } + fn flush_all_messages(&mut self) {} + fn init(&mut self) {} fn requirements(&self) -> Vec<&str> { vec!["ui:mumble_link"] // is it ? } - fn notify(&self) -> Vec<&str> { - vec!["back:mumble_link"] - } fn tick(&mut self, _latest_time: f64) -> ComponentResult { assert!( self.channels.is_some(), "channels must be initialized before interacting with component." ); let channels = self.channels.as_mut().unwrap(); - if self.editable_mumble { - if let Some(last_known_link) = &mut self.last_known_link { - self.window_changed = true; - last_known_link.changes = enumflags2::BitFlags::all(); - let _ = channels.mumble_link_back_notifier.blocking_send(to_data( - MessageToMumbleLinkBack::Value(Some(last_known_link.clone())), - )); - } - } else if let Ok(data) = channels.subscription_mumblelink.try_recv() { - let res: MumbleLinkResult = from_broadcast(&data); - match res.link { + if let Ok(data) = channels.subscription_mumblelink.try_recv() { + let link: Option = from_broadcast(&data); + match link { Some(link) => { if link.changes.contains(MumbleChanges::WindowPosition) || link.changes.contains(MumbleChanges::WindowSize) diff --git a/crates/jokolay/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs index 474a053..25c67f8 100644 --- a/crates/jokolay/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -312,7 +312,7 @@ impl UIPanel for ThemeManager { .clicked() && !checked { - self.config.default_theme = theme_name.clone(); + self.config.default_theme.clone_from(theme_name); } } }); diff --git a/documentation/diagrams/components_generated.dotuml b/documentation/diagrams/components_generated.dotuml new file mode 100644 index 0000000..c51e44d --- /dev/null +++ b/documentation/diagrams/components_generated.dotuml @@ -0,0 +1,22 @@ +digraph { + 0 [ label = "back:mumble_link" ] + 1 [ label = "back:jokolay_package_manager" ] + 2 [ label = "ui:window_manager" ] + 3 [ label = "ui:configuration" ] + 4 [ label = "ui:mumble_link" ] + 5 [ label = "ui:jokolay_renderer" ] + 6 [ label = "back:configuration" ] + 7 [ label = "ui:jokolay_package_manager" ] + 8 [ label = "ui:menu_panel" ] + 1 -> 7 [ label = "Peer" ] + 7 -> 1 [ label = "Peer" ] + 1 -> 0 [ label = "Requires" ] + 2 -> 4 [ label = "Requires" ] + 3 -> 6 [ label = "Notify" ] + 4 -> 0 [ label = "Notify" ] + 5 -> 4 [ label = "Requires" ] + 7 -> 4 [ label = "Requires" ] + 7 -> 5 [ label = "Notify" ] + 8 -> 4 [ label = "Requires" ] +} + diff --git a/documentation/mumble_editable.dotuml b/documentation/mumble_editable.dotuml new file mode 100644 index 0000000..f7d4ca5 --- /dev/null +++ b/documentation/mumble_editable.dotuml @@ -0,0 +1,39 @@ +SequenceDiagram { + actor user + lifeline ui_interface + control ui_editable_mumble + lifeline mumble_back + control back_repeat_last_value + lifeline mumble_ui + control ui_repeat_last_value + + user -a-> ui_interface "select editable" + ui_interface --> ui_editable_mumble "activate" + activate ui_editable_mumble + + ui_interface -a-> mumble_back "BindedOnUI" + mumble_back --> back_repeat_last_value "activate" + activate back_repeat_last_value + back_repeat_last_value --> mumble_back "trigger last value repetition" + + mumble_back -a-> mumble_ui "BindedOnUI" + mumble_ui --> ui_repeat_last_value "activate" + activate ui_repeat_last_value + ui_repeat_last_value --> mumble_ui "trigger last value repetition" + + ui_editable_mumble --> ui_interface "trigger last value propagation (from form)" + ui_interface -a-> mumble_back "Value" + + user -a-> ui_interface "select live" + ui_interface --> ui_editable_mumble "deactivate" + deactivate ui_editable_mumble + ui_interface -a-> mumble_back "Autonomous" + mumble_back --> back_repeat_last_value "deactivate" + deactivate back_repeat_last_value + + mumble_back -a-> mumble_ui "Autonomous" + mumble_ui --> ui_repeat_last_value "deactivate" + deactivate ui_repeat_last_value + + +} \ No newline at end of file From 091c696f2c888d238bab6ebca64060e8a290f391 Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 9 May 2024 20:40:34 +0200 Subject: [PATCH 52/54] cleanup of OrderedHashMap (for IndexMap) + add demo of plugin manager --- Cargo.lock | 23 +--------- Cargo.toml | 1 - crates/joko_package_manager/Cargo.toml | 1 - .../src/io/deserialize.rs | 18 ++++---- crates/joko_package_manager/src/io/export.rs | 3 +- .../joko_package_manager/src/io/serialize.rs | 4 +- .../src/manager/pack/active.rs | 3 +- .../src/manager/pack/category_selection.rs | 43 +++++++++---------- .../src/manager/pack/loaded.rs | 10 ++--- crates/joko_package_models/Cargo.toml | 1 - crates/joko_package_models/src/category.rs | 26 +++++------ crates/joko_package_models/src/package.rs | 8 ++-- crates/joko_plugin_manager/src/lib.rs | 28 +++++++++++- crates/jokolay/src/app/mod.rs | 14 ++++++ 14 files changed, 96 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ecc55..d5ae9a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1278,15 +1278,6 @@ dependencies = [ "zerocopy 0.6.6", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -1358,7 +1349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown", "serde", ] @@ -1562,7 +1553,6 @@ dependencies = [ "jokoapi", "miette", "once", - "ordered_hash_map", "paste", "phf", "rayon", @@ -1598,7 +1588,6 @@ dependencies = [ "joko_core", "jokoapi", "miette", - "ordered_hash_map", "paste", "phf", "rstest", @@ -2077,16 +2066,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "ordered_hash_map" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176" -dependencies = [ - "hashbrown 0.13.2", - "serde", -] - [[package]] name = "overload" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index f78b23a..fa55b98 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ glam = { version = "*", features = ["fast-math"] } indexmap = { version = "2" } itertools = { version = "*" } miette = { version = "*", features = ["fancy"] } -ordered_hash_map = { version = "*", features= ["serde"] } paste = { version = "*" } once = "0.3.4" rayon = { version = "*" } diff --git a/crates/joko_package_manager/Cargo.toml b/crates/joko_package_manager/Cargo.toml index fe351c4..2bc7157 100644 --- a/crates/joko_package_manager/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -26,7 +26,6 @@ jokoapi = { path = "../jokoapi" } joko_link_models = { path = "../joko_link_models" } miette = { workspace = true } once = {workspace = true} -ordered_hash_map = { workspace = true } paste = { workspace = true } phf = { version = "*", features = ["macros"] } rayon = { workspace = true } diff --git a/crates/joko_package_manager/src/io/deserialize.rs b/crates/joko_package_manager/src/io/deserialize.rs index 92f4235..788295d 100644 --- a/crates/joko_package_manager/src/io/deserialize.rs +++ b/crates/joko_package_manager/src/io/deserialize.rs @@ -1,5 +1,6 @@ use crate::BASE64_ENGINE; use base64::Engine; +use indexmap::IndexMap; use joko_core::{serde_glam::Vec3, RelativePath}; use joko_package_models::{ attributes::{CommonAttributes, XotAttributeNameIDs}, @@ -9,7 +10,6 @@ use joko_package_models::{ route::Route, trail::{TBin, TBinStatus, Trail}, }; -use ordered_hash_map::OrderedHashMap; use std::{ collections::VecDeque, io::{Cursor, Read}, @@ -273,7 +273,7 @@ fn parse_categories( pack: &mut PackCore, tree: &Xot, tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, + first_pass_categories: &mut IndexMap, names: &XotAttributeNameIDs, source_file_uuid: &Uuid, ) { @@ -294,7 +294,7 @@ fn parse_categories_recursive( pack: &mut PackCore, tree: &Xot, tags: impl Iterator, - first_pass_categories: &mut OrderedHashMap, + first_pass_categories: &mut IndexMap, names: &XotAttributeNameIDs, parent_name: Option, source_file_uuid: &Uuid, @@ -346,7 +346,7 @@ fn parse_categories_recursive( parent_name ); if !first_pass_categories.contains_key(&full_category_name) { - let mut sources: OrderedHashMap = OrderedHashMap::new(); + let mut sources: IndexMap = IndexMap::new(); if let Some(icon_file) = common_attributes.get_icon_file() { if !pack.textures.contains_key(icon_file) { debug!(%icon_file, "failed to find this texture in this pack"); @@ -398,7 +398,7 @@ fn parse_categories_from_normalized_file( let overlay_data_node = tree.document_element(root_node).or(Err("no doc element"))?; if let Some(od) = tree.element(overlay_data_node) { - let mut categories: OrderedHashMap = Default::default(); + let mut categories: IndexMap = Default::default(); if od.name() == xot_names.overlay_data { parse_category_categories_xml_recursive( file_name, @@ -618,7 +618,7 @@ fn parse_category_categories_xml_recursive( _file_name: &String, //meant for future implementation of source file definition for categories tree: &Xot, tags: impl Iterator, - cats: &mut OrderedHashMap, + cats: &mut IndexMap, names: &XotAttributeNameIDs, parent_uuid: Option, parent_name: Option, @@ -707,7 +707,7 @@ fn parse_category_categories_xml_recursive( children: Default::default(), }; cats.insert(guid, c); - cats.back_mut().unwrap() + cats.last_mut().unwrap().1 }; parse_category_categories_xml_recursive( _file_name, @@ -888,7 +888,7 @@ fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result = Default::default(); + let mut first_pass_categories: IndexMap = Default::default(); for source_file_name in xmls.iter() { let source_file_name = source_file_name.to_string(); let span_guard = @@ -1019,7 +1019,7 @@ fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result = OrderedHashMap::new(); + let mut sources: IndexMap = IndexMap::new(); sources.insert(guid, source_file_uuid); first_pass_categories.insert( full_category_name.clone(), diff --git a/crates/joko_package_manager/src/io/export.rs b/crates/joko_package_manager/src/io/export.rs index 73fc2f2..8073c97 100644 --- a/crates/joko_package_manager/src/io/export.rs +++ b/crates/joko_package_manager/src/io/export.rs @@ -9,7 +9,6 @@ use joko_package_models::{ route::Route, trail::Trail, }; use miette::{Context, IntoDiagnostic, Result}; -use ordered_hash_map::OrderedHashMap; use std::io::Write; use tracing::info; use uuid::Uuid; @@ -176,7 +175,7 @@ pub(crate) fn save_pack_texture_to_dir( fn recursive_cat_serializer( tree: &mut Xot, names: &XotAttributeNameIDs, - cats: &OrderedHashMap, + cats: &IndexMap, parent: Node, ) -> Result<()> { for (_, cat) in cats { diff --git a/crates/joko_package_manager/src/io/serialize.rs b/crates/joko_package_manager/src/io/serialize.rs index 687933b..cbd3957 100644 --- a/crates/joko_package_manager/src/io/serialize.rs +++ b/crates/joko_package_manager/src/io/serialize.rs @@ -4,11 +4,11 @@ use crate::{ }; use base64::Engine; use glam::Vec3; +use indexmap::IndexMap; use joko_package_models::{ attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, }; use miette::Result; -use ordered_hash_map::OrderedHashMap; use std::{io::Write, path::Path}; use tracing::info; use uuid::Uuid; @@ -168,7 +168,7 @@ pub(crate) fn save_pack_texture_to_dir( fn recursive_cat_serializer( tree: &mut Xot, names: &XotAttributeNameIDs, - cats: &OrderedHashMap, + cats: &IndexMap, parent: Node, ) -> Result<(), String> { for (_, cat) in cats { diff --git a/crates/joko_package_manager/src/manager/pack/active.rs b/crates/joko_package_manager/src/manager/pack/active.rs index 03e92bd..f58194e 100644 --- a/crates/joko_package_manager/src/manager/pack/active.rs +++ b/crates/joko_package_manager/src/manager/pack/active.rs @@ -1,6 +1,5 @@ use joko_package_models::attributes::CommonAttributes; use jokoapi::end_point::mounts::Mount; -use ordered_hash_map::OrderedHashMap; use egui::TextureHandle; use indexmap::IndexMap; @@ -291,7 +290,7 @@ pub(crate) struct CurrentMapData { //pub map_id: u32, //pub active_elements: HashSet, /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: OrderedHashMap, + pub active_textures: IndexMap, /// The key is the index of the marker in the map markers /// Their position in the map markers serves as their "id" as uuids can be duplicates. pub active_markers: IndexMap, diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs index bd58f93..1d77ee3 100644 --- a/crates/joko_package_manager/src/manager/pack/category_selection.rs +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -1,10 +1,10 @@ +use indexmap::IndexMap; use joko_component_models::{to_data, ComponentMessage}; use joko_package_models::{ attributes::CommonAttributes, category::Category, package::{PackCore, PackageImportReport}, }; -use ordered_hash_map::OrderedHashMap; use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; @@ -22,16 +22,16 @@ pub struct CategorySelection { pub is_active: bool, //currently being displayed (i.e.: active) pub separator: bool, pub display_name: String, - pub children: OrderedHashMap, + pub children: IndexMap, } pub struct SelectedCategoryManager { - data: OrderedHashMap, + data: IndexMap, } impl<'a> SelectedCategoryManager { pub fn new( - selected_categories: &OrderedHashMap, - categories: &OrderedHashMap, + selected_categories: &IndexMap, + categories: &IndexMap, ) -> Self { let mut list_of_enabled_categories = Default::default(); CategorySelection::get_list_of_enabled_categories( @@ -46,7 +46,7 @@ impl<'a> SelectedCategoryManager { } } #[allow(dead_code)] - pub fn cloned_data(&self) -> OrderedHashMap { + pub fn cloned_data(&self) -> IndexMap { self.data.clone() } pub fn is_selected(&self, category: &Uuid) -> bool { @@ -59,21 +59,21 @@ impl<'a> SelectedCategoryManager { pub fn len(&self) -> usize { self.data.len() } - pub fn keys(&'a self) -> ordered_hash_map::ordered_map::Keys<'a, Uuid, CommonAttributes> { + pub fn keys(&'a self) -> indexmap::map::Keys<'a, Uuid, CommonAttributes> { self.data.keys() } } impl CategorySelection { - pub fn default_from_pack_core(pack: &PackCore) -> OrderedHashMap { - let mut selectable_categories = OrderedHashMap::new(); + pub fn default_from_pack_core(pack: &PackCore) -> IndexMap { + let mut selectable_categories = IndexMap::new(); Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); selectable_categories } fn get_list_of_enabled_categories( - selection: &OrderedHashMap, - categories: &OrderedHashMap, - list_of_enabled_categories: &mut OrderedHashMap, + selection: &IndexMap, + categories: &IndexMap, + list_of_enabled_categories: &mut IndexMap, parent_common_attributes: &CommonAttributes, ) { for (_, cat) in categories { @@ -94,7 +94,7 @@ impl CategorySelection { } } pub fn get( - selection: &mut OrderedHashMap, + selection: &mut IndexMap, uuid: Uuid, ) -> Option<&mut CategorySelection> { if selection.is_empty() { @@ -113,7 +113,7 @@ impl CategorySelection { } #[allow(dead_code)] pub fn recursive_populate_guids( - selection: &mut OrderedHashMap, + selection: &mut IndexMap, entities_parents: &mut HashMap, parent_uuid: Option, ) { @@ -130,8 +130,8 @@ impl CategorySelection { } } fn recursive_create_selectable_categories( - selectable_categories: &mut OrderedHashMap, - cats: &OrderedHashMap, + selectable_categories: &mut IndexMap, + cats: &IndexMap, ) { for (_, cat) in cats.iter() { if !selectable_categories.contains_key(&cat.relative_category_name) { @@ -155,7 +155,7 @@ impl CategorySelection { } pub fn recursive_set( - selection: &mut OrderedHashMap, + selection: &mut IndexMap, uuid: Uuid, status: bool, ) -> bool { @@ -177,10 +177,7 @@ impl CategorySelection { false } } - pub fn recursive_set_all( - selection: &mut OrderedHashMap, - status: bool, - ) { + pub fn recursive_set_all(selection: &mut IndexMap, status: bool) { if selection.is_empty() { return; } @@ -194,7 +191,7 @@ impl CategorySelection { } pub fn recursive_update_active_categories( - selection: &mut OrderedHashMap, + selection: &mut IndexMap, active_elements: &HashSet, ) -> bool { let mut is_active = false; @@ -237,7 +234,7 @@ impl CategorySelection { pub fn recursive_selection_ui( back_end_notifier: &tokio::sync::mpsc::Sender, - selection: &mut OrderedHashMap, + selection: &mut IndexMap, ui: &mut egui::Ui, is_dirty: &mut bool, show_only_active: bool, diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs index 9af007c..d2fb175 100644 --- a/crates/joko_package_manager/src/manager/pack/loaded.rs +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -4,6 +4,7 @@ use std::{ path::{Path, PathBuf}, }; +use indexmap::IndexMap; use joko_component_models::{to_data, ComponentMessage}; use joko_package_models::{ attributes::{Behavior, CommonAttributes}, @@ -12,7 +13,6 @@ use joko_package_models::{ package::{PackCore, PackageImportReport}, trail::TBin, }; -use ordered_hash_map::OrderedHashMap; use egui::{ColorImage, TextureHandle}; use image::EncodableLayout; @@ -69,7 +69,7 @@ pub struct LoadedPackData { pub path: PathBuf, /// The actual xml pack. //pub core: PackCore, - pub categories: OrderedHashMap, + pub categories: IndexMap, pub all_categories: HashMap, pub source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package pub maps: HashMap, @@ -77,7 +77,7 @@ pub struct LoadedPackData { _is_dirty: bool, //there was an edition in the package itself // loca copy in the data side of what is exposed in UI - selectable_categories: OrderedHashMap, + selectable_categories: IndexMap, pub entities_parents: HashMap, activation_data: ActivationData, active_elements: HashSet, //keep track of which elements are active @@ -99,7 +99,7 @@ pub struct LoadedPackTexture { pub textures: HashMap>, /// The selection of categories which are "enabled" and markers belonging to these may be rendered - selectable_categories: OrderedHashMap, + selectable_categories: IndexMap, #[serde(skip)] current_map_data: CurrentMapData, activation_data: ActivationData, @@ -253,7 +253,7 @@ impl LoadedPackData { fn load_selectable_categories( path: &Path, pack: &PackCore, - ) -> OrderedHashMap { + ) -> IndexMap { //FIXME: we need to patch those categories from the one in the files let target = path.join(Self::CATEGORY_SELECTION_FILE_NAME); trace!("load_selectable_categories open {:?}", target); diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml index 24940a6..6124f14 100644 --- a/crates/joko_package_models/Cargo.toml +++ b/crates/joko_package_models/Cargo.toml @@ -18,7 +18,6 @@ itertools = { workspace = true } joko_core = { path = "../joko_core" } jokoapi = { path = "../jokoapi" } miette = { workspace = true } -ordered_hash_map = { workspace = true } paste = { workspace = true } phf = { version = "*", features = ["macros"] } serde = { workspace = true } diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs index b949a95..d6604f8 100644 --- a/crates/joko_package_models/src/category.rs +++ b/crates/joko_package_models/src/category.rs @@ -1,5 +1,5 @@ use crate::{attributes::CommonAttributes, package::PackageImportReport}; -use ordered_hash_map::OrderedHashMap; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use tracing::debug; use uuid::Uuid; @@ -14,7 +14,7 @@ pub struct RawCategory { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub sources: OrderedHashMap, + pub sources: IndexMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,7 +27,7 @@ pub struct Category { pub separator: bool, pub default_enabled: bool, pub props: CommonAttributes, - pub children: OrderedHashMap, //TODO: make a branch to test if having an Vec associated with global list of categories is faster. + pub children: IndexMap, //TODO: make a branch to test if having an Vec associated with global list of categories is faster. } pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { @@ -76,7 +76,7 @@ impl Category { } } fn per_route<'a>( - categories: &'a mut OrderedHashMap, + categories: &'a mut IndexMap, route: &[&str], ) -> Option<&'a mut Category> { let mut route = route.to_owned(); @@ -84,7 +84,7 @@ impl Category { Category::_per_route(categories, &mut route) } fn _per_route<'a>( - categories: &'a mut OrderedHashMap, + categories: &'a mut IndexMap, route: &mut Vec<&str>, ) -> Option<&'a mut Category> { if let Some(relative_category_name) = route.pop() { @@ -102,7 +102,7 @@ impl Category { } #[allow(dead_code)] fn per_uuid<'a>( - categories: &'a mut OrderedHashMap, + categories: &'a mut IndexMap, uuid: &Uuid, ) -> Option<&'a mut Category> { /* @@ -122,17 +122,17 @@ impl Category { None } pub fn reassemble( - input_first_pass_categories: &OrderedHashMap, + input_first_pass_categories: &IndexMap, report: &mut PackageImportReport, - ) -> OrderedHashMap { + ) -> IndexMap { let start_initialize = std::time::SystemTime::now(); let mut first_pass_categories = input_first_pass_categories.clone(); - let mut second_pass_categories: OrderedHashMap = Default::default(); + let mut second_pass_categories: IndexMap = Default::default(); let mut need_a_pass: bool = true; - let mut third_pass_categories: OrderedHashMap = Default::default(); + let mut third_pass_categories: IndexMap = Default::default(); let mut third_pass_categories_ref: Vec = Default::default(); - let mut root: OrderedHashMap = Default::default(); + let mut root: IndexMap = Default::default(); let elaspsed_initialize = start_initialize.elapsed().unwrap_or_default(); report.telemetry.categories_reassemble.initialize = elaspsed_initialize.as_millis(); @@ -168,7 +168,7 @@ impl Category { let relative_category_name = nth_chunk(&value.relative_category_name, '.', n); debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); - let sources: OrderedHashMap = OrderedHashMap::new(); + let sources: IndexMap = IndexMap::new(); let to_insert = RawCategory { default_enabled: value.default_enabled, guid: new_uuid, @@ -263,7 +263,7 @@ impl Category { debug!("third_pass_categories_ref"); let start_tree_insertion = std::time::SystemTime::now(); for full_category_uuid in third_pass_categories_ref { - if let Some(cat) = third_pass_categories.remove(&full_category_uuid) { + if let Some(cat) = third_pass_categories.shift_remove(&full_category_uuid) { let mut route = Vec::from_iter(cat.full_category_name.split('.')); route.pop(); //it is now the parent route if let Some(parent) = cat.parent { diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs index 0bb4958..f7b4e52 100644 --- a/crates/joko_package_models/src/package.rs +++ b/crates/joko_package_models/src/package.rs @@ -4,8 +4,8 @@ use crate::marker::Marker; use crate::route::{route_to_tbin, route_to_trail, Route}; use crate::trail::{TBin, Trail}; use base64::Engine; +use indexmap::IndexMap; use joko_core::RelativePath; -use ordered_hash_map::OrderedHashMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::{BTreeMap, HashMap, HashSet}; use tracing::{debug, trace}; @@ -138,7 +138,7 @@ pub struct PackageImportReport { pub uuid: Uuid, pub number_of: PackageImportStatistics, // count everything we can think of pub telemetry: PackageImportTelemetry, // all the time spent in which step - late_discovered_categories: OrderedHashMap, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + late_discovered_categories: IndexMap, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. missing_categories: Vec, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. #[serde(skip)] _missing_categories_tracker: HashSet, // for tracking purpose to avoid duplicate @@ -158,7 +158,7 @@ pub struct PackCore { pub uuid: Uuid, pub textures: HashMap>, pub tbins: HashMap, - pub categories: OrderedHashMap, + pub categories: IndexMap, pub all_categories: HashMap, pub entities_parents: HashMap, pub active_source_files: BTreeMap, @@ -452,7 +452,7 @@ impl PackCore { } fn recursive_register_categories( entities_parents: &mut HashMap, - categories: &OrderedHashMap, + categories: &IndexMap, all_categories: &mut HashMap, ) { for (_, cat) in categories.iter() { diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs index ac188a1..7ef8fec 100644 --- a/crates/joko_plugin_manager/src/lib.rs +++ b/crates/joko_plugin_manager/src/lib.rs @@ -1,18 +1,42 @@ +use std::path::PathBuf; + use joko_component_models::{ default_component_result, Component, ComponentChannels, ComponentResult, }; pub struct JokolayPlugin {} -pub struct JokolayPluginManager {} +pub struct JokolayPluginManager { + #[allow(dead_code)] + path: PathBuf, +} -impl Component for JokolayPlugin { +impl JokolayPluginManager { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + pub fn create(&mut self, _name: String) -> JokolayPlugin { + JokolayPlugin {} + } +} +impl Component for JokolayPluginManager { fn init(&mut self) {} fn flush_all_messages(&mut self) {} fn tick(&mut self, _timestamp: f64) -> ComponentResult { default_component_result() } fn bind(&mut self, _channels: ComponentChannels) {} +} + +impl Component for JokolayPlugin { + fn init(&mut self) { + println!("initialize dummy plugin"); + } + fn flush_all_messages(&mut self) {} + fn tick(&mut self, _timestamp: f64) -> ComponentResult { + default_component_result() + } + fn bind(&mut self, _channels: ComponentChannels) {} fn requirements(&self) -> Vec<&str> { vec!["back:mumble_link"] } diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index 32dd91a..d2117d9 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,4 +1,5 @@ use std::{ + path::PathBuf, sync::{Arc, RwLock}, thread, }; @@ -6,6 +7,7 @@ use std::{ use cap_std::fs_utf8::Dir; use egui_window_glfw_passthrough::{GlfwBackend, GlfwConfig}; use joko_link_ui_manager::MumbleUIManager; +use joko_plugin_manager::JokolayPluginManager; mod init; mod menu; mod ui_parameters; @@ -85,6 +87,18 @@ impl Jokolay { ... */ + let plugin_manager = Arc::new(RwLock::new(JokolayPluginManager::new(PathBuf::from( + "plugins", + )))); + let dummy_plugin = Arc::new(RwLock::new( + plugin_manager + .as_ref() + .write() + .unwrap() + .create("dummy plugin".to_string()), + )); + let _ = component_manager.register("dummy plugin", dummy_plugin); + let _ = component_manager.register( "back:jokolay_package_manager", Arc::new(RwLock::new(PackageDataManager::new( From 7ae4be50e6b6c5be14d59823e77547f806317ca8 Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 9 May 2024 21:09:42 +0200 Subject: [PATCH 53/54] remove graph print on stdout (will need to make something better in the future) --- Cargo.lock | 516 +++++++++++------------ crates/joko_component_manager/src/lib.rs | 5 +- 2 files changed, 256 insertions(+), 265 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5ae9a2..8ade01e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +checksum = "6f90148830dac590fac7ccfe78ec4a8ea404c60f75a24e16407a71f0f40de775" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -54,7 +54,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.7.32", + "zerocopy 0.7.34", ] [[package]] @@ -143,32 +143,31 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" dependencies = [ - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ - "async-lock 3.3.0", "async-task", "concurrent-queue", "fastrand", @@ -178,11 +177,11 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ - "async-lock 3.3.0", + "async-lock", "blocking", "futures-lite", ] @@ -193,7 +192,7 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ - "async-lock 3.3.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", @@ -206,15 +205,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.3.0" @@ -239,41 +229,43 @@ dependencies = [ [[package]] name = "async-process" -version = "2.1.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" dependencies = [ "async-channel", "async-io", - "async-lock 3.3.0", + "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 5.2.0", + "event-listener 5.3.0", "futures-lite", "rustix", + "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-recursion" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "async-signal" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" dependencies = [ "async-io", - "async-lock 2.8.0", + "async-lock", "atomic-waker", "cfg-if", "futures-core", @@ -281,24 +273,24 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -309,9 +301,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" @@ -343,6 +335,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bimap" version = "0.6.3" @@ -390,18 +388,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", - "async-lock 3.3.0", + "async-lock", "async-task", - "fastrand", "futures-io", "futures-lite", "piper", - "tracing", ] [[package]] @@ -417,9 +413,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -438,7 +434,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -497,9 +493,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -525,16 +521,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -564,9 +560,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -679,9 +675,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dc7287237dd438b926a81a1a5605dad33d286870e5eee2db17bf2bcd9e92a" +checksum = "bb497fad022245b29c2a0351df572e2d67c1046bcef2260ebc022aec81efea82" dependencies = [ "cc", "cxxbridge-flags", @@ -691,9 +687,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47c6c8ad7c1a10d3ef0fe3ff6733f4db0d78f08ef0b13121543163ef327058b" +checksum = "9327c7f9fbd6329a200a5d4aa6f674c60ab256525ff0084b52a889d4e4c60cee" dependencies = [ "cc", "codespan-reporting", @@ -701,31 +697,31 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "cxxbridge-flags" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701a1ac7a697e249cdd8dc026d7a7dafbfd0dbcd8bd24ec55889f2bc13dd6287" +checksum = "688c799a4a846f1c0acb9f36bb9c6272d9b3d9457f3633c7753c6057270df13c" [[package]] name = "cxxbridge-macro" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b404f596046b0bb2d903a9c786b875a126261b52b7c3a64bbb66382c41c771df" +checksum = "928bc249a7e3cd554fd2e8e08a426e9670c50bbfc9a621653cfa9accc9641783" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deranged" @@ -737,17 +733,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "digest" version = "0.10.7" @@ -832,7 +817,7 @@ dependencies = [ "getrandom", "glow", "js-sys", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "tracing", "wasm-bindgen", "web-sys", @@ -846,7 +831,7 @@ checksum = "39bc7f5aab85ad422c53b2a1753a94a08bdca4b701346edc226ba015a0b2a7a8" dependencies = [ "egui", "egui_render_glow", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "three-d", ] @@ -863,9 +848,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "emath" @@ -885,9 +870,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -916,7 +901,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -937,7 +922,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -948,7 +933,7 @@ checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -975,20 +960,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "4.0.3" @@ -1002,9 +981,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", @@ -1023,19 +1002,19 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.3.0", "pin-project-lite", ] [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -1054,7 +1033,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1066,9 +1045,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1145,7 +1124,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -1199,9 +1178,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1235,7 +1214,7 @@ dependencies = [ "glfw-sys-passthrough", "objc", "raw-window-handle 0.5.2", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "winapi", ] @@ -1268,9 +1247,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1280,9 +1259,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" @@ -1355,9 +1334,9 @@ dependencies = [ [[package]] name = "indextree" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" +checksum = "3a6f7e29c1619ec492f411b021ac9f30649d5f522ca6f287f2467ee48c8dfe10" [[package]] name = "inotify" @@ -1533,7 +1512,7 @@ dependencies = [ name = "joko_package_manager" version = "0.2.1" dependencies = [ - "base64", + "base64 0.21.7", "bytemuck", "cxx", "cxx-build", @@ -1576,7 +1555,7 @@ dependencies = [ name = "joko_package_models" version = "0.2.1" dependencies = [ - "base64", + "base64 0.21.7", "bimap", "bytemuck", "cxx-build", @@ -1729,9 +1708,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -1741,13 +1720,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -1767,9 +1745,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1857,7 +1835,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -1993,14 +1971,14 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2095,9 +2073,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -2105,22 +2083,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -2130,9 +2108,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", @@ -2168,7 +2146,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -2182,9 +2160,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2218,9 +2196,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if", "concurrent-queue", @@ -2260,18 +2238,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2314,9 +2292,9 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "raw-window-handle" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b" [[package]] name = "rayon" @@ -2347,11 +2325,20 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2404,9 +2391,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "relative-path" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rfd" @@ -2423,7 +2410,7 @@ dependencies = [ "objc-foundation", "objc_id", "pollster", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", @@ -2454,9 +2441,9 @@ checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" [[package]] name = "rstest" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" dependencies = [ "rstest_macros", "rustc_version", @@ -2464,9 +2451,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" dependencies = [ "cfg-if", "glob", @@ -2475,15 +2462,15 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.55", + "syn 2.0.61", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" @@ -2496,9 +2483,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2525,15 +2512,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -2542,9 +2529,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2569,35 +2556,35 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2606,13 +2593,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -2646,9 +2633,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -2776,9 +2763,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -2829,22 +2816,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -2884,9 +2871,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -2905,9 +2892,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -2980,7 +2967,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -3014,7 +3001,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -3124,9 +3111,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -3148,11 +3135,11 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" [[package]] name = "ureq" -version = "2.9.6" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", @@ -3203,7 +3190,7 @@ checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -3255,7 +3242,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -3289,7 +3276,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3321,9 +3308,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -3343,18 +3330,18 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "winapi-wsapoll" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" dependencies = [ "winapi", ] @@ -3390,7 +3377,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3408,7 +3395,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3428,17 +3415,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3449,9 +3437,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3461,9 +3449,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3473,9 +3461,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3485,9 +3479,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3497,9 +3491,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3509,9 +3503,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3521,9 +3515,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -3536,9 +3530,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -3613,23 +3607,22 @@ dependencies = [ [[package]] name = "zbus" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" +checksum = "e5915716dff34abef1351d2b10305b019c8ef33dcf6c72d31a6e227d5d9d7a21" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", - "async-lock 3.3.0", + "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", - "derivative", "enumflags2", - "event-listener 5.2.0", + "event-listener 5.3.0", "futures-core", "futures-sink", "futures-util", @@ -3652,14 +3645,13 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.1.2" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" +checksum = "66fceb36d0c1c4a6b98f3ce40f410e64e5a134707ed71892e1b178abc4c695d4" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "regex", "syn 1.0.109", "zvariant_utils", ] @@ -3687,11 +3679,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ - "zerocopy-derive 0.7.32", + "zerocopy-derive 0.7.34", ] [[package]] @@ -3702,18 +3694,18 @@ checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.61", ] [[package]] @@ -3736,9 +3728,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.0.2" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +checksum = "877ef94e5e82b231d2a309c531f191a8152baba8241a7939ee04bd76b0171308" dependencies = [ "endi", "enumflags2", @@ -3750,9 +3742,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.0.2" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +checksum = "b7ca98581cc6a8120789d8f1f0997e9053837d6aa5346cbb43454d7121be6e39" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3763,9 +3755,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +checksum = "75fa7291bdd68cd13c4f97cc9d78cbf16d96305856dfc7ac942aeff4c2de7d5a" dependencies = [ "proc-macro2", "quote", diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs index 0a6eaf6..b588497 100644 --- a/crates/joko_component_manager/src/lib.rs +++ b/crates/joko_component_manager/src/lib.rs @@ -8,7 +8,6 @@ use std::{ use joko_component_models::{Component, ComponentChannels, ComponentMessage, ComponentResult}; use petgraph::{ csr::IndexType, - dot::Dot, graph::NodeIndex, stable_graph::{EdgeReference, StableDiGraph}, visit::{EdgeRef, IntoNodeIdentifiers}, @@ -319,8 +318,8 @@ impl ComponentManager { } //If we reached here, it means all peers agree. - //println!("{}", Dot::with_config(&depgraph, &[Config::EdgeNoLabel])); - println!("{}", Dot::with_config(&depgraph, &[])); + //TODO: create a text graph file grouping nodes per world + //println!("{}", Dot::with_config(&depgraph, &[])); //Is there a difference between keys of known_services vs hosted_services. let hosted_keys: HashSet = self.known_components.keys().cloned().collect(); From 43dd4220c8844252048a4a7e18ba96d7970cbb1e Mon Sep 17 00:00:00 2001 From: moi Date: Thu, 9 May 2024 23:09:46 +0200 Subject: [PATCH 54/54] update github workflow to build new target version of jokolink --- .github/workflows/jokolink_artifact.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/jokolink_artifact.yml b/.github/workflows/jokolink_artifact.yml index 477a4d6..0a14758 100644 --- a/.github/workflows/jokolink_artifact.yml +++ b/.github/workflows/jokolink_artifact.yml @@ -1,7 +1,7 @@ on: push: paths: - - 'crates/jokolink/**' + - 'crates/joko_link_manager/**' name: Jokolink DLL env: @@ -18,9 +18,9 @@ jobs: uses: Swatinem/rust-cache@v1 - name: Build Jokolink DLL - run: cargo build --release -p jokolink + run: cargo build --release -p joko_link_manager - uses: actions/upload-artifact@v3 with: - name: jokolink.dll - path: "./target/release/jokolink.dll" + name: joko_link_manager.dll + path: "./target/release/joko_link_manager.dll" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ecce1b..7e34623 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build Jokolink if: ${{matrix.os == 'windows'}} - run: cargo build --release -p jokolink + run: cargo build --release -p joko_link_manager - name: Upload Assets uses: xresloader/upload-to-github-release@v1 @@ -37,4 +37,4 @@ jobs: with: tags: true draft: true - file: "target/release/jokolay;target/release/jokolay.exe;target/release/jokolink.dll" \ No newline at end of file + file: "target/release/jokolay;target/release/jokolay.exe;target/release/joko_link_manager.dll" \ No newline at end of file