From 92da688f0ccbbc539f59f2784dea06ab544173af Mon Sep 17 00:00:00 2001 From: Crauzer <0xcrauzer@proton.me> Date: Fri, 24 Jul 2026 19:33:38 +0200 Subject: [PATCH 1/2] feat(ltk_mapgeo): add v18 support --- crates/ltk_mapgeo/src/lib.rs | 2 +- crates/ltk_mapgeo/src/mesh.rs | 25 +++++++++++++++++++ crates/ltk_mapgeo/src/read/mesh.rs | 9 +++++++ crates/ltk_mapgeo/src/read/mod.rs | 4 +-- crates/ltk_mapgeo/src/read/scene_graph.rs | 14 ++++++++++- crates/ltk_mapgeo/src/read/version.rs | 6 +++++ .../src/scene_graph/bucketed_geometry.rs | 23 +++++++++++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/ltk_mapgeo/src/lib.rs b/crates/ltk_mapgeo/src/lib.rs index a98cb67d..ab720135 100644 --- a/crates/ltk_mapgeo/src/lib.rs +++ b/crates/ltk_mapgeo/src/lib.rs @@ -57,7 +57,7 @@ pub(crate) mod read; pub const MAGIC: &[u8; 4] = b"OEGM"; /// Supported file format versions -pub const SUPPORTED_VERSIONS: &[u32] = &[5, 6, 7, 9, 11, 12, 13, 14, 15, 17]; +pub const SUPPORTED_VERSIONS: &[u32] = &[5, 6, 7, 9, 11, 12, 13, 14, 15, 17, 18]; /// Result type alias for this crate pub type Result = std::result::Result; diff --git a/crates/ltk_mapgeo/src/mesh.rs b/crates/ltk_mapgeo/src/mesh.rs index 26c182c8..55483652 100644 --- a/crates/ltk_mapgeo/src/mesh.rs +++ b/crates/ltk_mapgeo/src/mesh.rs @@ -39,6 +39,9 @@ pub struct EnvironmentMesh { /// Hash of the visibility controller path (scene graph) visibility_controller_path_hash: u32, + /// Hash of a region placeable this mesh is anchored to (version >= 18) + region_path_hash: u32, + /// Whether to disable backface culling disable_backface_culling: bool, @@ -160,6 +163,20 @@ impl EnvironmentMesh { self.visibility_controller_path_hash } + /// Hash of a map region placeable this mesh is anchored to (version >= 18). + /// + /// This is the container-key ("path") hash of an item in the map's + /// `MapPlaceableContainer` (materials.bin) — the same referencing scheme as + /// [`visibility_controller_path_hash`](Self::visibility_controller_path_hash). + /// When non-zero, the game positions this mesh via the referenced region + /// entity instead of the static world origin, and may substitute `|flipped` + /// material variants for mirrored placement. `0` means the mesh is not + /// anchored to a region. + #[inline] + pub fn region_path_hash(&self) -> u32 { + self.region_path_hash + } + /// Whether backface culling is disabled #[inline] pub fn disable_backface_culling(&self) -> bool { @@ -352,6 +369,7 @@ pub(crate) struct EnvironmentMeshBuilder { base_vertex_declaration_id: usize, submeshes: Vec, visibility_controller_path_hash: u32, + region_path_hash: u32, disable_backface_culling: bool, bounding_box: AABB, transform: Mat4, @@ -378,6 +396,7 @@ impl Default for EnvironmentMeshBuilder { base_vertex_declaration_id: 0, submeshes: Vec::new(), visibility_controller_path_hash: 0, + region_path_hash: 0, disable_backface_culling: false, bounding_box: AABB::default(), transform: Mat4::IDENTITY, @@ -436,6 +455,11 @@ impl EnvironmentMeshBuilder { self } + pub fn region_path_hash(mut self, hash: u32) -> Self { + self.region_path_hash = hash; + self + } + pub fn disable_backface_culling(mut self, disable: bool) -> Self { self.disable_backface_culling = disable; self @@ -511,6 +535,7 @@ impl EnvironmentMeshBuilder { base_vertex_declaration_id: self.base_vertex_declaration_id, submeshes: self.submeshes, visibility_controller_path_hash: self.visibility_controller_path_hash, + region_path_hash: self.region_path_hash, disable_backface_culling: self.disable_backface_culling, bounding_box: self.bounding_box, transform: self.transform, diff --git a/crates/ltk_mapgeo/src/read/mesh.rs b/crates/ltk_mapgeo/src/read/mesh.rs index 98a00173..7bca29de 100644 --- a/crates/ltk_mapgeo/src/read/mesh.rs +++ b/crates/ltk_mapgeo/src/read/mesh.rs @@ -47,6 +47,14 @@ impl EnvironmentMesh { visibility = EnvironmentVisibility::from_bits_truncate(reader.read_u8()?); } + // Read region path hash (version >= 18); references a region placeable + // in the map's MapPlaceableContainer by container-key hash + let region_path_hash = if version.has_region_path_hash() { + reader.read_u32::()? + } else { + 0 + }; + // Read visibility controller path hash (version >= 15) let visibility_controller_path_hash = if version.has_visibility_controller_path_hash() { reader.read_u32::()? @@ -168,6 +176,7 @@ impl EnvironmentMesh { .base_vertex_declaration_id(base_vertex_declaration_id) .submeshes(submeshes) .visibility_controller_path_hash(visibility_controller_path_hash) + .region_path_hash(region_path_hash) .disable_backface_culling(disable_backface_culling) .bounding_box(bounding_box) .transform(transform) diff --git a/crates/ltk_mapgeo/src/read/mod.rs b/crates/ltk_mapgeo/src/read/mod.rs index e97871c5..3172c053 100644 --- a/crates/ltk_mapgeo/src/read/mod.rs +++ b/crates/ltk_mapgeo/src/read/mod.rs @@ -305,13 +305,13 @@ impl EnvironmentAsset { version: MapGeoVersion, ) -> Result> { if !version.has_multiple_scene_graphs() { - return Ok(vec![BucketedGeometry::read(reader, true)?]); + return Ok(vec![BucketedGeometry::read(reader, true, version)?]); } let count = reader.read_u32::()? as usize; let mut graphs = Vec::with_capacity(count); for _ in 0..count { - graphs.push(BucketedGeometry::read(reader, false)?); + graphs.push(BucketedGeometry::read(reader, false, version)?); } Ok(graphs) } diff --git a/crates/ltk_mapgeo/src/read/scene_graph.rs b/crates/ltk_mapgeo/src/read/scene_graph.rs index 33b5480b..8867a6b7 100644 --- a/crates/ltk_mapgeo/src/read/scene_graph.rs +++ b/crates/ltk_mapgeo/src/read/scene_graph.rs @@ -5,6 +5,7 @@ use std::io::Read; use byteorder::{ReadBytesExt, LE}; use ltk_io_ext::ReaderExt; +use super::MapGeoVersion; use crate::{ scene_graph::{BucketedGeometryBuilder, BucketedGeometryFlags}, BucketedGeometry, EnvironmentVisibility, GeometryBucket, Result, @@ -12,7 +13,16 @@ use crate::{ impl BucketedGeometry { /// Reads a bucketed geometry from a binary stream - pub(crate) fn read(reader: &mut R, legacy: bool) -> Result { + pub(crate) fn read( + reader: &mut R, + legacy: bool, + version: MapGeoVersion, + ) -> Result { + let region_path_hash = if !legacy && version.has_region_path_hash() { + reader.read_u32::()? + } else { + 0 + }; let visibility_controller_path_hash = if legacy { 0 } else { reader.read_u32::()? }; let min_x = reader.read_f32::()?; @@ -35,6 +45,7 @@ impl BucketedGeometry { if is_disabled { return Ok(BucketedGeometryBuilder::default() + .region_path_hash(region_path_hash) .visibility_controller_path_hash(visibility_controller_path_hash) .bounds(min_x, min_z, max_x, max_z) .max_stick_out(max_stick_out_x, max_stick_out_z) @@ -74,6 +85,7 @@ impl BucketedGeometry { }; Ok(BucketedGeometryBuilder::default() + .region_path_hash(region_path_hash) .visibility_controller_path_hash(visibility_controller_path_hash) .bounds(min_x, min_z, max_x, max_z) .max_stick_out(max_stick_out_x, max_stick_out_z) diff --git a/crates/ltk_mapgeo/src/read/version.rs b/crates/ltk_mapgeo/src/read/version.rs index d5335bda..186c7a27 100644 --- a/crates/ltk_mapgeo/src/read/version.rs +++ b/crates/ltk_mapgeo/src/read/version.rs @@ -102,6 +102,12 @@ impl MapGeoVersion { self.0 >= 17 } + /// Version has a region path hash on meshes and scene graphs + #[inline] + pub fn has_region_path_hash(&self) -> bool { + self.0 >= 18 + } + /// Version has first shader texture override (sampler index 0) #[inline] pub fn has_first_shader_override(&self) -> bool { diff --git a/crates/ltk_mapgeo/src/scene_graph/bucketed_geometry.rs b/crates/ltk_mapgeo/src/scene_graph/bucketed_geometry.rs index 9128caf1..e72ce085 100644 --- a/crates/ltk_mapgeo/src/scene_graph/bucketed_geometry.rs +++ b/crates/ltk_mapgeo/src/scene_graph/bucketed_geometry.rs @@ -27,6 +27,9 @@ use super::GeometryBucket; /// ``` #[derive(Debug, Clone)] pub struct BucketedGeometry { + /// Hash of a region placeable this graph is anchored to (version >= 18) + region_path_hash: u32, + /// Hash of the visibility controller path visibility_controller_path_hash: u32, @@ -85,6 +88,7 @@ impl BucketedGeometry { /// Creates a new empty (disabled) bucketed geometry pub fn empty() -> Self { Self { + region_path_hash: 0, visibility_controller_path_hash: 0, min_x: 0.0, min_z: 0.0, @@ -110,6 +114,18 @@ impl BucketedGeometry { self.visibility_controller_path_hash } + /// Hash of a map region placeable this graph is anchored to (version >= 18). + /// + /// Container-key ("path") hash of an item in the map's + /// `MapPlaceableContainer` (materials.bin), like + /// [`visibility_controller_path_hash`](Self::visibility_controller_path_hash). + /// When non-zero, the game positions the graph's bounds via the referenced + /// region entity instead of the static world origin. `0` means not anchored. + #[inline] + pub fn region_path_hash(&self) -> u32 { + self.region_path_hash + } + /// Minimum bounds of the grid (X, Z) #[inline] pub fn min_bounds(&self) -> Vec2 { @@ -201,6 +217,7 @@ impl BucketedGeometry { /// Builder for constructing [`BucketedGeometry`] instances #[derive(Default)] pub(crate) struct BucketedGeometryBuilder { + region_path_hash: u32, visibility_controller_path_hash: u32, min_x: f32, min_z: f32, @@ -225,6 +242,11 @@ impl BucketedGeometryBuilder { self } + pub fn region_path_hash(mut self, hash: u32) -> Self { + self.region_path_hash = hash; + self + } + pub fn bounds(mut self, min_x: f32, min_z: f32, max_x: f32, max_z: f32) -> Self { self.min_x = min_x; self.min_z = min_z; @@ -282,6 +304,7 @@ impl BucketedGeometryBuilder { pub fn build(self) -> BucketedGeometry { BucketedGeometry { + region_path_hash: self.region_path_hash, visibility_controller_path_hash: self.visibility_controller_path_hash, min_x: self.min_x, min_z: self.min_z, From 9c8a9d67d5430c390cb153b02a5eaa51929e7638 Mon Sep 17 00:00:00 2001 From: Crauzer <0xcrauzer@proton.me> Date: Fri, 24 Jul 2026 20:40:27 +0200 Subject: [PATCH 2/2] docs: update supported versions to include v18 and clarify unsupported v19/v20 details --- crates/ltk_mapgeo/src/lib.rs | 5 +++++ crates/ltk_mapgeo/src/read/mesh.rs | 5 +++++ crates/ltk_mapgeo/src/read/version.rs | 25 +++++++++++++++++++++++++ docs/LTK_GUIDE.md | 10 ++++++++-- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/ltk_mapgeo/src/lib.rs b/crates/ltk_mapgeo/src/lib.rs index ab720135..c94c6ad3 100644 --- a/crates/ltk_mapgeo/src/lib.rs +++ b/crates/ltk_mapgeo/src/lib.rs @@ -57,6 +57,11 @@ pub(crate) mod read; pub const MAGIC: &[u8; 4] = b"OEGM"; /// Supported file format versions +/// +/// The game client's parser also accepts v19 and v20, but no map has ever +/// shipped above v18 and the client discards the extra data both versions +/// add, so they are intentionally unsupported. The format deltas are +/// documented in `src/read/version.rs`. pub const SUPPORTED_VERSIONS: &[u32] = &[5, 6, 7, 9, 11, 12, 13, 14, 15, 17, 18]; /// Result type alias for this crate diff --git a/crates/ltk_mapgeo/src/read/mesh.rs b/crates/ltk_mapgeo/src/read/mesh.rs index 7bca29de..e24931d0 100644 --- a/crates/ltk_mapgeo/src/read/mesh.rs +++ b/crates/ltk_mapgeo/src/read/mesh.rs @@ -167,6 +167,11 @@ impl EnvironmentMesh { } } + // Unshipped versions add more per-mesh data here: v19 a single dead + // u8, v20 a length-prefixed MapGeoExtension reflection blob. The game + // client parses and discards both, so we don't read them — see the + // module docs in `read/version.rs` for the full layout. + Ok(EnvironmentMeshBuilder::default() .name(name) .vertex_count(vertex_count) diff --git a/crates/ltk_mapgeo/src/read/version.rs b/crates/ltk_mapgeo/src/read/version.rs index 186c7a27..24b57226 100644 --- a/crates/ltk_mapgeo/src/read/version.rs +++ b/crates/ltk_mapgeo/src/read/version.rs @@ -1,4 +1,29 @@ //! Version-dependent feature flags +//! +//! # Known but unsupported versions: v19 (0x13) and v20 (0x14) +//! +//! The live game client's parser accepts up to v20, but no map has ever +//! shipped above v18 (current Summoner's Rift), and the client throws away +//! the extra data both versions add — so this crate intentionally does not +//! parse them. For future reference, the deltas (reversed from the live +//! client, 2026-07) are: +//! +//! - **v19**: one extra `u8` per mesh, immediately after the baked paint +//! scale/bias (default 1). The client reads it into the mesh record but no +//! code path ever consumes it — a dead byte. +//! - **v20**: each mesh record is followed by a `u32` byte-length prefix and +//! a reflection-serialized `MapGeoExtension` meta object (FNV1a-32 class +//! hash `0xD3F07247`): a named parameter-override bag of three +//! string-keyed maps — `map`, `map`, and +//! `map` where `MapGeoTextureOverride` +//! (class hash `0x32902D31`) is `{ texturePath: string, two unresolved +//! u32 fields }`. It is the reflection-based successor to the v17 +//! shader/mesh texture override mechanism, but the client parses the +//! object and immediately destroys it — never stored on the mesh, never +//! handed to the renderer. The length prefix makes the blob skippable. +//! +//! If either version ever ships with the data actually consumed, support +//! belongs here as `has_*` flags plus reads in `read/mesh.rs`. /// Helper struct for tracking file version capabilities #[derive(Debug, Clone, Copy)] diff --git a/docs/LTK_GUIDE.md b/docs/LTK_GUIDE.md index 7ede43f7..e3442cea 100644 --- a/docs/LTK_GUIDE.md +++ b/docs/LTK_GUIDE.md @@ -304,7 +304,13 @@ for scene_graph in asset.scene_graphs() { } ``` -**Supported Versions**: 5, 6, 7, 9, 11, 12, 13, 14, 15, 17 +**Supported Versions**: 5, 6, 7, 9, 11, 12, 13, 14, 15, 17, 18 + +Versions 19 and 20 are known (the game client's parser accepts them) but have +never shipped, and the client discards the extra data they add — a dead byte +per mesh in v19, a parsed-then-destroyed `MapGeoExtension` reflection blob per +mesh in v20 — so they are intentionally unsupported. The format deltas are +documented in `crates/ltk_mapgeo/src/read/version.rs`. --- @@ -484,7 +490,7 @@ use glam::{Vec2, Vec3, Vec4, Mat4, Quat}; - All crates follow semantic versioning - The umbrella `league-toolkit` crate versions independently from sub-crates - Breaking changes in sub-crates cause major version bumps -- File format version support is documented per-crate (e.g., mapgeo supports versions 5-17) +- File format version support is documented per-crate (e.g., mapgeo supports versions 5-18) ---