Closed coderedart closed 2 years ago
required markers : folder that contains {mapID}.json files which contain markers. required trails: folder that contains {mapID}.json files which contain trails required images: folder that contains all the png files required tbins: folder that contains all the tbin files optional pack.json: file which contains info about a pack like version or name or download link or github repo etc..
optional : all of these CAN be packed into a zip file.
described in terms of rust structs.
the Option<T>
means they are optional
the Vec<T>
vector/array. could be empty.
the Map<T, U>
maps must have sorted by their key T, so use an ordered_map (cpp) or BTreeMap (rust) atleast when you are serializing to keep the reproducibility. could be empty
the Set<T>
is an ordered set of T (so no duplicates), and could be empty.
All T types will be validated while deserializing, so we will NOT be accepting a marker pack with invalid data.
Marker ID, Trail ID, Category ID, Map ID are all u16 (shorts) numbers. none of them have the potential to reach more than 65k in count. the number of total markers in tekkit pack are barely 35k+ after all these years. Category ID must be unique per pack. Marker and Trail ID must be unique per Map. Image and Trail names must be unique per pack.
pub struct JsonPack {
pub pack_description: PackDescription, // from pack.json
pub cattree: Vec<Cat> // menu. more than one top level category is supported
pub markers: Map<MapID, Map<MarkerID, Marker>>,// Markers referred to by their ID.
pub trails: Map<MapID, Map<TBinID, Trail>>,// Trails just like above
}
xml: doesn't exist
pub struct PackDescription {
pub name: String, // name of the pack
pub url: Option<Url>, // url of the pack website if it exists
pub git: Option<Url>, // if the pack has a git repo, its url can here.
pub version: Option<String> // semantic version
}
xml: represents the menu of categories by referring to them by their ID.
pub struct Cat {
pub name: String, // name of category matching the xml attribute
pub display_name: Option<String>, // shown to users in menu. same xml attribute
pub is_separator: Option<bool>, // whether it just exists as a heading. same xml attribute,
pub description: Option<String>
pub children: Vec<Cat>, // its children/submenus.
}
just recurse through the tree, and use the categories map to get the display name and show it. we will leave it to the overlay on how they want to store their category menu item is checked or not.
xml: Trail Tag
pub struct Trail {
pub tbin: Option<String>, // similar to xml attribute TrailData which refers to path. but we just use the name of the file without extension.
pub image: Option<String>, // the texture name to use for trail mesh, without png extension
}
xml: POI Tag
pub struct Marker {
pub position: [f32; 3], // x,y,z coordinates in inches (in game units)
pub alpha: Option<u8>, // alpha 0-255
pub color: Option<[u8; 4]>, // rgba bytes as array
pub fade_range: Option<[f32; 2]>, // fade distance ranges near to far in meters
pub flags: Option<MarkerFlags>, // a bunch of bools grouped up into bitflags to save space :)
// some filters like race, mount, festivals, achievements etc..
pub image: Option<String>, // texture, this also determines the quad dimensions
pub map_display_size: Option<u16>, // size of icon on map
pub map_fade_out_scale_level: Option<f32>,// after how many zoom levels will marker fade on map???
pub max_size: Option<u16>,// pixels
pub min_size: Option<u16>,// pixels
pub scale: Option<f32>,// scale by this factor
}
xml: combines POI attributes ingame, map and minimap visibility, screen edge herding, map zoom scaling, auto trigger and count down as bitflags.
bitflags::bitflags! {
pub struct MarkerFlags: u8 {
const IN_GAME_VISIBILITY = 0b00000001; // appears in game
const MAP_VISIBILITY = 0b00000010; // appears on map
const MINI_MAP_VISIBILITY = 0b00000100; // appears on minimap
const MINI_MAP_EDGE_HERD = 0b00001000; // gets stuck on minimap edges like personal waypoint when it is out of view
const MAP_ZOOM_SCALE = 0b00010000; // scale with map
const AUTO_TRIGGER = 0b00100000; // should trigger within range
const COUNT_DOWN = 0b01000000; // show countdown to wakeup time of marker
}
}
pub struct Dynamic {
pub trigger: Option<Trigger>, // if it has any triggers for behavior
pub achievement: Option<Achievement>, // visibility based on achievement status
pub info: Option<Info>,
}
pub struct Trigger {
pub range: f32, // range in meters
pub behavior: Option<Behavior>,
pub toggle_cat: Option<CategoryID>, // which cat should be toggled on trigger
}
pub struct Achievement {
pub id: u32,
pub bit: Option<u32>,
}
pub struct Info {
pub text: String, // text that should be displayed when within range
pub range: f32,
}
pub enum Behavior {
AlwaysVisible,
ReappearOnMapChange,
ReappearOnDailyReset,
OnlyVisibleBeforeActivation,
ReappearAfterTimer {
reset_length: u32, // in seconds
},
ReappearOnMapReset {
map_cycle_length: u32, // length of a map cycle in seconds
map_cycle_offset_after_reset: u32, // how many seconds after daily reset does the new map cycle start in seconds
},
OncePerInstance,
DailyPerChar,
OncePerInstancePerChar,
WvWObjective,
}
extras: there's also a bunch of other QoL things i did for the sake of jokolay. like making alpha a u8 instead of float, because color attribute is [u8; 4], where people set alpha from 0 to 255, but having alpha attribute be f32 0.0 -1.0 does not make sense. I compressed all the bools in marker pack into a single u8 with bitflags to save space both in json and ram.
instead of trail binary, image paths being hardcoded, i instead made them referenced by a id (u16). and instead of a separate file with image descriptions where we have a map with id as key and {name of image, width , height, source: optional}. same goes for trail files. then we store the image as "1.png", "2.png" etc.. in the images folder. f32 reasons for this:
the final and most important change is category tree. categories are a recursive structure in xml packs and they are referred to by the XPath extension of xml. eg: parent.child1.subchild1 (lets call this the "fullname" of a category as referenced by markers")
again. huge string allocations in xml packs, and recursive trees are not good for traversal in terms of performance. so, i separated the categories and the tree.
there's a cat descriptions map which has a Category ID with {category name, display name, is seperator, Authors {name, email, in game name} }.
then, there's a recursive tree structure CatTree { cat_id, Children: Vector<CatTree>}
.
This is absolutely the best decision i have made.
in xml:
<MarkerCategory name="fractal_underground" .../>
<MarkerCategory name="fractal_volcanic" .../>
`<POI type="fractal_underground">
if we change the category menu:
<MarkerCategory name="fractals" ...>
<MarkerCategory name="fractal_underground" .../>
<MarkerCategory name="fractal_volcanic" .../>
</MarkerCategory>
in json:
CatTree: [{catid: 1, ..}, {catid: 2, ..}]
Cats: {1: {name: "fractal_underground", ...}, 2: {name: "fractal_volanic", ...}}
markers: {34: {... cat: 1, ...}, ...}
after changing menu,
CatTree: [{catid: 3, children: [{catid: 1, ..}, {catid: 2, ..}] }]
Cats: {1: {name: "fractal_underground", ...}, 2: {name: "fractal_volanic", ...}, 3: {name: "fractals", ...}}
markers: {34: {... cat: 1, ...}, ...} // no changes for markers
i wanted unique id for cats, markers, images, trails, packs because of a ridiculous idea about storing markerpacks in a database . allows marker packs to be hosted on servers, allows markers/packs to be edited live by multiple players, allows backups, rollbacks with ALL the billions of dollars worth research/tooling support like gui, cross platform/language support by every language (godot has gdsqlite and other addons), allows multi boxing on local systems where if one gw2 window creates a new marker, other windows will see it too (advanced marker pack creators might use it to try to look at a marker from different perspectives/distances), improved memory usage as we don't need to keep all the markerpacks in ram anymore :) just query database by categories or maps on map change or enabling/disabling categories etc.. i could go on forever.
Tekkit's pack 3.0 has embraced a lot of blish-hud attributes. source: https://blishhud.com/docs/markers/development/attributes there's a bunch of attributes which are exclusive to blish.
Trails | IsWall | When enabled, a trail will be rendered on its edge like a boundary. |
---|---|---|
Markers, Trails | Festival | Hides the marker or trail if one of the specified festivals is not active. |
Markers, Trails | Mount | Hides the marker if the player is not currently riding one of the specified mounts. |
Markers, Trails | Profession | Hides the marker or trail if the player is not currently one of the specified professions. |
Markers, Trails | Race | Hides the marker or trail if the player is not currently one of the specified races. |
Markers, Trails | Specialization | Hides the marker or trail if the player is not currently one of the specified specializations. |
Markers, Trails | MapType | Hides the marker or trail if the current map is not one of the specified map types. |
Markers, Trails | Cull | Allows you to make markers or trails hidden depending on which direction they are looked at. |
Markers, Trails | CanFade | Allows a marker or trail to ignore fade features such as those which hide trails around the player or between the user and their camera. |
Markers | InvertBehavior | If enabled, the behavior attribute's effect is inverted. |
Markers | Rotate | Allows you to statically rotate a marker instead of it automatically facing the player. |
Markers | Bounce | Allows a marker to "bounce" when triggered. |
Markers | Copy | Allows you to set a value to the users clipboard. |
Markers | Show / Hide | Allows showing or hiding a specified category when triggered. |
Markers | Toggle | Toggles a category on or off when triggered. Similar in behavior to the Show & Hide attributes. |
Markers, Categories | Tip | Allows you to display a tooltip on markers on the minimap or categories in the category dropdown. |
Categories | DefaultToggle | Indicates if the category should be enabled by default. |
defaultToggle => only when parsing the pack for the first time or at resetting the category checkboxes.
festival, specialization, maptype, profession, race => are basic filters to be executed at map/char change events
mount is a dynamic filter based on mumble link
show/hide, toggle, Copy => normal triggers like behavior (we can integrate this into behavior enum)
cull => we will have to generate the mesh/quad in different order based on ccw or anticcw. but does it also exist for 2d markers? in trails we would probably go with up direction as the facing direction.
rotate => 3d direction
bounce: very very niche and again needs special attention. (ALL markers need to now have an offset based on some sin(time) uniform added to their y coordinate.) we ignore this attribute for now.
Allows you to display a tooltip on markers on the minimap or categories in the category dropdown.
tip-name arbitrary text tip-name="Golden Coin" The text to show as the title of the tooltip. tip-description arbitrary text tip-description="Coin 7 - under the cove south of the waypoint." The description text shown under the title.
If tip-name is not provided, the displayname of the parent category is used instead.
If you would like no tooltip to be shown for a marker, set tip-name="". Note that this will also hide the distance indicator and any set tip-description will also be ignored.
jokolay tries to match the render behavior of taco. we do this by just inheriting the marker's attributes from categories as usual. This is possible because the attributes until now affected either Category or Marker. example:
<MarkerCategory name="hello" displayName="Hello Sempai" alpha="0.5" />
<POIs>
<POI type="hello" />
</POIs>
name
and displayName
are exclusive to the Category itself. they are not inherited by child markers or categories.
alpha
is a marker/trail attribute and will be inherited by all the child markers of itself as well as the child categories recursively to their child markers/categories.
so, this is effectively equal to
<MarkerCategory name="hello" displayName="Hello Sempai" />
<POIs>
<POI type="hello" alpha="0.5" />
</POIs>
from this we get the json pack like this
{
"cats" : { "2" : { "name" : "hello", "displayName" : "hello Sempai" } }, // cats only have cat related attributes
"markers" : { "5" : { "alpha" : 0.5, "cat" : 2 } } // markers only have marker related attributes. no inheritance.
}
we can simply convert this back and it would still be effectively the same as the inherited xml above. so, this is effectively the same marker pack as the original as far as taco is concerned. lets say we add a new marker with no alpha attribute under the same category, this changes nothing.
{
"cats" : { "2" : { "name" : "hello", "displayName" : "hello Sempai" } }, // cats only have cat related attributes
"markers" : { "5" : { "alpha" : 0.5, "cat" : 2 }, "6" : { "cat" : 2 } // markers only have marker related attributes. no inheritance.
}
this is fine, categories don't have any marker/trail attributes when we export to xml, so alpha of marker 6 will be the default as expected (especially when we convert it back to json later). The round trip preserves the effective marker pack. lets replace the alpha with tip-description attribute. example:
<MarkerCategory name="hello" displayName="Hello Sempai" tip-description="whatever" />
<POIs>
<POI type="hello" />
</POIs>
name
and displayName
are exclusive to the Category itself. they are not inherited by child markers or categories. but tip-description
affects both Category AND Marker.
so, this is effectively equal to
<MarkerCategory name="hello" displayName="Hello Sempai" tip-description="whatever" />
<POIs>
<POI type="hello" alpha="0.5" tip-description="whatever" />
</POIs>
from this we get the json pack like this
{
"cats" : { "2" : { "name" : "hello", "displayName" : "hello Sempai", "tip-description" : "whatever" } }, // cats only have cat related attributes
"markers" : { "5" : { "alpha" : 0.5, "cat" : 2, "tip-description" : "whatever" } } // markers only have marker related attributes. no inheritance.
}
we can simply convert this back and it would still be effectively the same as the inherited xml above. so, this is effectively the same marker pack as the original as far as blish is concerned. lets say we add a new marker with no tip-description attribute under the same category, this changes nothing.
{
"cats" : { "2" : { "name" : "hello", "displayName" : "hello Sempai", "tip-description" : "whatever" } }, // cats only have cat related attributes
"markers" : { "5" : { "alpha" : 0.5, "cat" : 2, "tip-description" : "whatever" }, "6" : { "cat" : 2 } // markers only have marker related attributes. no inheritance.
}
BUT when we change back to xml, we get this:
<MarkerCategory name="hello" displayName="Hello Sempai" tip-description="whatever" />
<POIs>
<POI type="hello" tip-description="whatever" />
<POI type="hello" />
</POIs>
Blish now will show tip-description according to their implementation for the category, but more importantly, when we inherit this before converting back to json. we get:
<MarkerCategory name="hello" displayName="Hello Sempai" tip-description="whatever" />
<POIs>
<POI type="hello" tip-description="whatever" />
<POI type="hello" tip-description="whatever" />
</POIs>
in json, the new marker didn't have any tip attribute, but now it does because tip-description attribute exists for parent because this attribute affects both marker and category. ofcourse we can explicitly specify that there's no tip-description for the new marker by using an empty value
<POI type="hello" tip-description="" />
but when we convert back to json, this marker still has a tip-description attribute. although its an empty attribute, it still exists, where as we didn't have an attribute for this marker before. This breaks the reproducibility of a marker during the round trip of xml -> json -> xml -> json. We have to explicitly think of a workaround for this attribute or we lose our reproducibility guarantee.
for now, we will just add use the cat description attribute of json cats. we will output the same attribute for xml packs, so that they don't get inherited by child markers/cats just like displayName or is_separator. otoh, we will just use tip-descriptions for the markers.
Finally, lets get one thing working properly first. if we want to support more sophisticated features, lets just use nodejs + glfw + threejs i.e wgpu (deno if it gets node modules support). or even bevy / rg3d / rend3.
life is too short and i don't want to spend learning about things like gltf format and implement parsing etc..
having a common format will be helpful for other overlays, but as of right now, there's only blish that has any sort of activity. and trying to fit markers into gltf AND maintain compatibility with taco xml format is too much work for too little gain.
Burrito has also started to work on making a Protobuf (or other binary serialization) format for Markers.
after looking at gltf, its not "that" bad. babylonJS already uses custom properties in gltf to support things like billboarding. https://doc.babylonjs.com/typedoc/classes/babylon.mesh#billboardmode
okay, there's no reason for alpha to exist if we already have a color attribute. color attribute can just instead be [0, 0, 0, 0.5]
for 0.5
alpha.
I kind of understand how gltf works now and there's a decent enough gltf crate already. to quote myself from https://gitlab.com/coderedart/jokolay/-/issues/19#note_850649157
- no support for billboards
- markers are different from gltf scenes/nodes. they have dynamic attributes like fade or triggers etc..
- instead of dealing with a complicated marker format, we now deal with gltf AND a complicated marker format anyway because of all the special attributes that markers have
but gltf is only a common scene format and rendering is completely dependent on the application itself. so, we can attach custom properties and make sure that the gltf scene fallback works without custom properties for other renderers like threejs, and when you render with jokolay , it will recognize the custom properties of markers.
Advantages:
rend3
already exists at the moment for jokolay to use in rust to get started with. Disadvantages: fortunately, we can transfer most of the design in json packs to gltf. the primary blockers would be
now we store 6 vertices (vec3) instead of one position (vec3) per marker, but it also means that we don't need rotate attribute anymore. or we could just make markers point primitives instead of triangles. we only need one position vertex. and we can still use rotation property for rotate xml attribute, and the lack of a rotate property means its a billbaord or something like that.
I will try asking Burrito or electaco to see if anyone is interested in a gltf format.
https://github.com/takahirox/EXT_texture_video for video markers https://github.com/takahirox/EXT_text for 3d text if needed
it seems custom gltf extensions are a good way to actually be able to simply add more functionality to gltf files. https://github.com/takahirox/three-gltf-extensions has a bunch of extra plugins for threejs to support the above unofficial extensions.
just need to be careful that apps which don't support these extensions can easily fallback to core gltf scenes by providing default mesh/materials.
A list of attributes of marker that are inherited from cats to markers / trails. the only missing attributes are specific to elements like
attributes which decide whether marker is allowed to be active based on data from player.
attributes which make marker active or inactive based on custom data that overlay needs to persist across restart
color -> Color_0
alpha -> Color_0 (just use color mixing to simulate alpha)
height_offset -> vertices. just add to y component
icon_file, texture (trail) -> texture / images
trail_data_file -> mesh bin
iconSize, trailScale, map_display_size -> scale
rotate, rotate_xyz -> rotation
pub in_game_visibility: Option<bool>, // can be custom property. or we can simply create separate scenes for in game 3d, map and minimap.
pub map_visibility: Option<bool>, // same as above
pub mini_map_visibility: Option<bool>, // same as above
pub anim_speed: Option<f32>, // speed of animation of trail
pub can_fade: Option<bool>, // whether player being under this marker will make it go translucent to not block player character on screen
pub cull: Option<Cull>, // clock / anti clockwise will decide how we order the triangle vertices to cull
pub fade_far: Option<i32>, // fades out completely at this distance, so hidden (but still active for triggers or such)
pub fade_near: Option<i32>, // starts fading at this distance
pub has_countdown: Option<bool>, // shows activation countdown
pub is_wall: Option<bool>, // ?
pub keep_on_map_edge: Option<bool>, // herding on minimap. not possible via gltf.
pub map_fade_out_scale_level: Option<f32>, // ?
pub max_size: Option<u16>, // not possible via gltf
pub min_size: Option<u16>, // not possible via gltf
pub scale_on_map_with_zoom: Option<bool>,// ?
and bad news.
gltf is cool for read only packs, but editing them is a sort of messy. the accessors and buffer views and buffers are binary structs and we will never know how to properly edit them.
more importantly, we gltf nodes cannot refer to external gltf files. so, we cannot integrate 3d meshes and edit them easily. it will be highly inefficient and if we use any mesh optimizers, they will remove all the structure of gltf files. its easier for people to export a glb / gltf asset from their blender / editors and put it into a marker pack just like tbin meshes.
its not that we can't use gltf format. its more that we don't gain much by using gltf format if we can't add 3d meshes to a marker pack file properly.but by using json packs, we can eventually add gltf models support to them and get the best of both worlds. gltf files externally edited and marker packs format still easier to edit.
so, we need json packs, and just like Markers / Trails, we can add a new type for gltf objects. it will be similar to Trails using tbin meshes. gltf objects will refer to glb files in a folder. we can easily render the full object / scene with rend3 without needing to worry about enabling / disabling individual nodes inside the gltf.
but we learnt some things like using scale or rotate attributes, so we should continue with that style of marker pack and merge it into json packs. we can also just skip a bunch of attributes that don't interest us for now.
I think we have come full circle at this point. its time to go over what we want with a marker format.
rschema
as well as https://github.com/Stranger6667/jsonschema-rs to do this.we will just keep simple structs for markers and trails.
and use flat fields aid: u32, abit: u32
rather than structs like Achievement {id: u32, bit: u32}
.
use rschema to generate json schemas automatically. // hard to deal with bitflag structs. TODO: check that those structs work
to allow multiple people to edit a single pack, we need to have a server facade that takes in changes and sends them to all clients so that their packs actually reflect those changes.
generally speaking, we need to only synchronize changes like creation of a marker / trail / category, where there might be race conditions for taking an ID. editing attributes generally are fine as long as we send the full "edited" property value to make sure that all peers use the latest full value as decided by the main server.
Final design of the marker packs. i think we need to remind ourselves the current situation.
category
could have different alpha
attribute values in different files.alpha
attribute, do we add it to all files? in which file should we create a new category? how to reparent them etc..elementtree
is a godsend for now, but it doesn't support formatted output. serde_json
support. schemars
crate. cats.json
, get images / trls list and start validating each map file immediately. now, we will go through each aspect of JSON marker packs design along with their motivations. we assume everything to be valid Utf-8 encoded. the contents of files as well as the file names.
we separate the marker pack physically into 4 parts.
The pack root folder looks like this
cats.json
images/
{image_name}.png,*many
tbins/
{tbin_name}.tbin,*many
maps/
{map_id}.json,*many
cats.json
contains a recursive vector (array) of categories, and each of those might have an array of categories as children. parent.child.subchild.first_subchild
and so on.generally speaking, all 4 components must be edited separately. and editing one should not have an effect on other files. although, mismatch of data can lead to validation errors which must be fixed manually.
this requires some care to not have race conditions / bugs. we will go over the rough impl details here.
due to simpler rules, we can just iterate over the entries of a xml marker pack and start creating a json pack.
create a Json Pack with a name (maybe from file name of a marker pack). and start populating the pack as you find a relevant entry.
Validation is done at different levels.
this issue is all over the place. lets make a new one.
Xml Marker Packs
xml_files: xml files are placed anywhere in the pack folder and they will all need to be parsed to get a complete pack. the format is roughly like this: OverlayData is the root tag in xml file optional MarkerCategory tag which is recursive and represents the category menu along with marker attributes which are supposed to serve as "inheritance" properties.
optional POIs tag which contains POI and Trail tags
optional POI tag which represents a marker. two attributes need special mentioning here.
first is the "type" attribute which is an xpath (representing the path to category this marker belongs to).
second is the iconFile attribute which is a relative path from the pack root to a png image.
optional Trail tag which represents a trail. it also has the "type" attribute and its image tag is called "texture".
also has a special attribute called trailData to refer to the path of trail binary data just like png image.
Advantages of using xml packs:
Disadvantages of using xml packs:
Most of the above issues in Xml Marker packs can be condensed into three categories:
POI
orTrail
affecting reproducibility.the main issue is that these problems require existing overlays to also support a stricter version of marker packs with the above issues fixed. I doubt they want to do that and break workflows for existing users. So, it is better for us to go with json packs.
I have spent way way way way way too much time on marker packs, and kinda starting to regret this. one of the main reasons to make Jokolay instead of porting taco was that I didn't want to learn specific functionality like taco's UI toolkit which won't be useful to me in the future compared to learning something like Qt or imgui in c++ or egui in rust. Yet, I spent way too much time wasting on this custom format. if it was gltf or some other open format, it might have been okay. we still learnt a bunch of stuff regarding all kinds of formats (text and binary) or the issues regarding representation of data like premultiplied colors or referring to external files like images/trail binaries. but, not enough to justify the amount of time invested.
Why JSON
size difference between xml and json packs was roughly 2 MB for Tekkit's pack.
Databases
just for completion, one more option we considered was databases like sqlite. Advantages:
Disadvantages:
Result
honestly, databases are an attractive option (especially with sqlite). but I don't want to completely commit myself to a schema yet, especially without consulting with other overlays. so i will choose json based pack. eventually we can use sqlite internally, but when we want to export a pack, we can choose to do so in json. once we get enough experience, we can reconsider databases.