Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/ltk_mapgeo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ 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];
///
/// 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
pub type Result<T> = std::result::Result<T, ParseError>;
25 changes: 25 additions & 0 deletions crates/ltk_mapgeo/src/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -352,6 +369,7 @@ pub(crate) struct EnvironmentMeshBuilder {
base_vertex_declaration_id: usize,
submeshes: Vec<EnvironmentSubmesh>,
visibility_controller_path_hash: u32,
region_path_hash: u32,
disable_backface_culling: bool,
bounding_box: AABB,
transform: Mat4,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions crates/ltk_mapgeo/src/read/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<LE>()?
} else {
0
};

// Read visibility controller path hash (version >= 15)
let visibility_controller_path_hash = if version.has_visibility_controller_path_hash() {
reader.read_u32::<LE>()?
Expand Down Expand Up @@ -159,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)
Expand All @@ -168,6 +181,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)
Expand Down
4 changes: 2 additions & 2 deletions crates/ltk_mapgeo/src/read/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,13 @@ impl EnvironmentAsset {
version: MapGeoVersion,
) -> Result<Vec<BucketedGeometry>> {
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::<LE>()? 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)
}
Expand Down
14 changes: 13 additions & 1 deletion crates/ltk_mapgeo/src/read/scene_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ 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,
};

impl BucketedGeometry {
/// Reads a bucketed geometry from a binary stream
pub(crate) fn read<R: Read>(reader: &mut R, legacy: bool) -> Result<Self> {
pub(crate) fn read<R: Read>(
reader: &mut R,
legacy: bool,
version: MapGeoVersion,
) -> Result<Self> {
let region_path_hash = if !legacy && version.has_region_path_hash() {
reader.read_u32::<LE>()?
} else {
0
};
let visibility_controller_path_hash = if legacy { 0 } else { reader.read_u32::<LE>()? };

let min_x = reader.read_f32::<LE>()?;
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions crates/ltk_mapgeo/src/read/version.rs
Original file line number Diff line number Diff line change
@@ -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<string, bool>`, `map<string, Vec4>`, and
//! `map<string, MapGeoTextureOverride>` 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)]
Expand Down Expand Up @@ -102,6 +127,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 {
Expand Down
23 changes: 23 additions & 0 deletions crates/ltk_mapgeo/src/scene_graph/bucketed_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions docs/LTK_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---

Expand Down Expand Up @@ -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)

---

Expand Down
Loading