Skip to main content

yog_dimensions/
lib.rs

1//! yog-dimensions — custom Minecraft dimension definitions for Yog mods.
2//!
3//! Declares a dimension's *type* (sky, lighting, physics, coordinate scale —
4//! see [`YogDimensionTypeDef`]) via [`YogDimensionDef`], registered once at
5//! mod-init time through `Registry::register_dimension` (in `yog-api`), the
6//! same lifecycle window as blocks/items/recipes.
7//!
8//! **`id` isn't limited to new custom dimensions** — passing a vanilla id
9//! (`"minecraft:overworld"`, `"minecraft:the_nether"`, `"minecraft:the_end"`)
10//! patches that dimension's type/generator instead of creating a new one.
11//! This is how a mod replaces or tweaks the overworld's terrain, not just
12//! adds side dimensions.
13//!
14//! Chunk *generation* is intentionally **not** a fixed preset/config here —
15//! there's no single "noise" or "flat" style that covers every mod's needs,
16//! and real generators often layer several algorithms together. Instead,
17//! mods write their own generator as a plain closure and register it under a
18//! **named, reusable generator type** via `Registry::register_chunk_generator`
19//! (e.g. `"mymod:custom_terrain"`) — called once per chunk column with the
20//! [`GeneratedChunk`] the closure places blocks in however it likes
21//! (composing whatever noise functions or logic it wants — that's the mod's
22//! own code, not this crate's concern).
23//!
24//! Because generator types are named and independent of any one dimension, a
25//! [`YogDimensionDef`] just *references* one by id via
26//! [`YogDimensionDef::generator_type`] — the same generator type can back
27//! several dimensions, and (like Biomes O'Plenty's world-type presets) the
28//! platform host can list every registered generator type as a selectable
29//! option on the world-creation screen, letting a player pick which
30//! registered generator the overworld (or a custom dimension) uses for a
31//! new world.
32
33use std::collections::HashMap;
34
35use yog_data::Schema;
36
37// ── Dimension type ───────────────────────────────────────────────────────────
38
39/// Properties of a dimension type — mirrors Java's `YogDimensionType`.
40///
41/// All fields default to vanilla-overworld-like values so mods only need to
42/// override what actually differs.
43#[derive(Schema, Debug, Clone, PartialEq)]
44pub struct YogDimensionTypeDef {
45    pub min_y: i32,
46    pub height: i32,
47    pub logical_height: i32,
48    pub has_sky_light: bool,
49    pub sky_light_updates: bool,
50    pub ambient_light: f32,
51    pub coordinate_scale: f32,
52    pub ultrawarm: bool,
53    pub beds_explode: bool,
54    pub respawn_anchors_explode: bool,
55    pub piglin_safe: bool,
56    pub natural: bool,
57    pub has_ceiling: bool,
58    pub effects: String,
59    pub has_sky: bool,
60    pub has_clouds: bool,
61    pub cloud_height: f64,
62    pub has_fog: bool,
63    pub fog_color: Option<i32>,
64    pub sky_color: Option<i32>,
65    pub water_color: Option<i32>,
66    pub water_fog_color: Option<i32>,
67}
68
69/// The overworld's sky (`effects`) and cloud height — a dimension's unless it says.
70const OVERWORLD_EFFECTS: &str = "overworld";
71const OVERWORLD_CLOUD_HEIGHT: f64 = 192.0;
72
73impl Default for YogDimensionTypeDef {
74    fn default() -> Self {
75        Self {
76            min_y: -64,
77            height: 384,
78            logical_height: 384,
79            has_sky_light: true,
80            sky_light_updates: true,
81            ambient_light: 0.0,
82            coordinate_scale: 1.0,
83            ultrawarm: false,
84            beds_explode: false,
85            respawn_anchors_explode: false,
86            piglin_safe: false,
87            natural: true,
88            has_ceiling: false,
89            effects: OVERWORLD_EFFECTS.to_owned(),
90            has_sky: true,
91            has_clouds: true,
92            cloud_height: OVERWORLD_CLOUD_HEIGHT,
93            has_fog: false,
94            fog_color: None,
95            sky_color: None,
96            water_color: None,
97            water_fog_color: None,
98        }
99    }
100}
101
102impl YogDimensionTypeDef {
103    // ── Chained setters — start from `YogDimensionTypeDef::default()` and
104    // override only what differs from the overworld-like baseline. ─────────
105
106    pub fn min_y(mut self, v: i32) -> Self {
107        self.min_y = v;
108        self
109    }
110    pub fn height(mut self, v: i32) -> Self {
111        self.height = v;
112        self.logical_height = v;
113        self
114    }
115    pub fn logical_height(mut self, v: i32) -> Self {
116        self.logical_height = v;
117        self
118    }
119    pub fn has_sky_light(mut self, v: bool) -> Self {
120        self.has_sky_light = v;
121        self
122    }
123    pub fn sky_light_updates(mut self, v: bool) -> Self {
124        self.sky_light_updates = v;
125        self
126    }
127    pub fn ambient_light(mut self, v: f32) -> Self {
128        self.ambient_light = v;
129        self
130    }
131    pub fn coordinate_scale(mut self, v: f32) -> Self {
132        self.coordinate_scale = v;
133        self
134    }
135    pub fn ultrawarm(mut self, v: bool) -> Self {
136        self.ultrawarm = v;
137        self
138    }
139    pub fn beds_explode(mut self, v: bool) -> Self {
140        self.beds_explode = v;
141        self
142    }
143    pub fn respawn_anchors_explode(mut self, v: bool) -> Self {
144        self.respawn_anchors_explode = v;
145        self
146    }
147    pub fn piglin_safe(mut self, v: bool) -> Self {
148        self.piglin_safe = v;
149        self
150    }
151    pub fn natural(mut self, v: bool) -> Self {
152        self.natural = v;
153        self
154    }
155    pub fn has_ceiling(mut self, v: bool) -> Self {
156        self.has_ceiling = v;
157        self
158    }
159    pub fn effects(mut self, v: impl Into<String>) -> Self {
160        self.effects = v.into();
161        self
162    }
163    pub fn has_sky(mut self, v: bool) -> Self {
164        self.has_sky = v;
165        self
166    }
167    pub fn has_clouds(mut self, v: bool) -> Self {
168        self.has_clouds = v;
169        self
170    }
171    pub fn cloud_height(mut self, v: f64) -> Self {
172        self.cloud_height = v;
173        self
174    }
175    pub fn has_fog(mut self, v: bool) -> Self {
176        self.has_fog = v;
177        self
178    }
179    pub fn fog_color(mut self, v: i32) -> Self {
180        self.fog_color = Some(v);
181        self
182    }
183    pub fn sky_color(mut self, v: i32) -> Self {
184        self.sky_color = Some(v);
185        self
186    }
187    pub fn water_color(mut self, v: i32) -> Self {
188        self.water_color = Some(v);
189        self
190    }
191    pub fn water_fog_color(mut self, v: i32) -> Self {
192        self.water_fog_color = Some(v);
193        self
194    }
195}
196
197// ── Dimension definition ─────────────────────────────────────────────────────
198
199/// A dimension definition — what a mod registers to create a custom
200/// dimension, or to patch an existing (including vanilla) one. Sent as Yog
201/// Data via `Registry::register_dimension` (in `yog-api`); the host
202/// registers (or patches) a `LevelStem`/`DimensionType` entry from it at
203/// mod-init time.
204///
205/// The actual `ServerLevel` (world data) is created lazily at runtime — see
206/// the crate-level docs and `Registry::register_chunk_generator`.
207#[derive(Schema, Debug, Clone, PartialEq)]
208pub struct YogDimensionDef {
209    /// Registry id. A new id (e.g. `"mymod:my_dim"`) creates a custom
210    /// dimension; a vanilla id (`"minecraft:overworld"`, etc.) patches that
211    /// dimension's type/generator instead.
212    pub id: String,
213    pub dimension_type: YogDimensionTypeDef,
214    /// Id of a generator type registered via `Registry::register_chunk_generator`
215    /// (e.g. `"mymod:custom_terrain"`) that this dimension should use. `None`
216    /// keeps the dimension's existing/vanilla generator.
217    pub generator_type: Option<String>,
218    /// The one biome the dimension is made of (`"minecraft:plains"`): its
219    /// mobs, its features, the structures that may start in it.
220    pub biome: String,
221    /// Structures (villages, …) may generate. Only for dimensions with a
222    /// registered generator.
223    pub structures: bool,
224    /// The biome's features (trees, flowers, ores) are placed. Only for
225    /// dimensions with a registered generator.
226    pub features: bool,
227    /// Mobs spawn naturally (a registered generator also offers none to
228    /// the spawner when off).
229    pub mob_spawning: bool,
230    /// Extra platform/metadata mods want the host to know about (arbitrary
231    /// JSON-string values), for anything not modeled by `dimension_type`.
232    #[data(default)]
233    pub extra: HashMap<String, String>,
234}
235
236impl Default for YogDimensionDef {
237    fn default() -> Self {
238        Self {
239            id: String::new(),
240            dimension_type: YogDimensionTypeDef::default(),
241            generator_type: None,
242            biome: yog_ids::game::PLAINS.into(),
243            structures: true,
244            features: true,
245            mob_spawning: true,
246            extra: HashMap::new(),
247        }
248    }
249}
250
251impl YogDimensionDef {
252    /// Start a new definition with default (overworld-like) type properties.
253    pub fn new(id: impl Into<String>) -> Self {
254        Self {
255            id: id.into(),
256            ..Self::default()
257        }
258    }
259
260    /// Replace the dimension type properties.
261    pub fn dimension_type(mut self, dimension_type: YogDimensionTypeDef) -> Self {
262        self.dimension_type = dimension_type;
263        self
264    }
265
266    /// Use the generator type registered under this id (see
267    /// `Registry::register_chunk_generator`).
268    pub fn generator_type(mut self, generator_type_id: impl Into<String>) -> Self {
269        self.generator_type = Some(generator_type_id.into());
270        self
271    }
272
273    /// The biome the dimension is made of.
274    pub fn biome(mut self, biome: impl Into<String>) -> Self {
275        self.biome = biome.into();
276        self
277    }
278
279    /// Whether structures generate (default: yes).
280    pub fn structures(mut self, on: bool) -> Self {
281        self.structures = on;
282        self
283    }
284
285    /// Whether the biome's features are placed (default: yes).
286    pub fn features(mut self, on: bool) -> Self {
287        self.features = on;
288        self
289    }
290
291    /// Whether mobs spawn (default: yes).
292    pub fn mob_spawning(mut self, on: bool) -> Self {
293        self.mob_spawning = on;
294        self
295    }
296
297    /// Set an extra metadata property.
298    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
299        self.extra.insert(key.into(), value.into());
300        self
301    }
302}
303
304// ── Generated chunks ─────────────────────────────────────────────────────────
305
306/// Blocks across a chunk column, in x and z.
307pub const CHUNK_WIDTH: i32 = 16;
308
309/// A chunk column a generator fills (the `world/generation/chunk` event,
310/// keyed by `generator`): where it is, and every block placed so far — a
311/// palette and a cell per block, like the game's chunk sections, so the host
312/// sets them a section at a time. Several handlers fill one column in turn
313/// (terrain, then a mod's ores).
314#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
315pub struct GeneratedChunk {
316    /// The generator type (`mymod:custom_terrain`).
317    pub generator: String,
318    /// In chunks, not blocks.
319    pub chunk_x: i32,
320    pub chunk_z: i32,
321    /// The dimension type's `min_y` and `height`.
322    pub min_y: i32,
323    pub height: i32,
324    /// The block ids the cells name: cell value `n` is `palette[n - 1]`.
325    #[data(default)]
326    pub palette: Vec<String>,
327    /// One little-endian `u16` per block, Y then Z then X ([`CELL`] each;
328    /// `0`: nothing placed); empty until the first block.
329    #[data(with = "yog_data::with::Raw")]
330    pub cells: Vec<u8>,
331}
332
333/// Bytes per cell of [`GeneratedChunk::cells`].
334pub const CELL: usize = std::mem::size_of::<u16>();
335
336impl GeneratedChunk {
337    /// The cell of local `x`/`z` (0..16), world `y`; `None` outside the column.
338    fn cell(&self, x: i32, y: i32, z: i32) -> Option<usize> {
339        let inside = (0..CHUNK_WIDTH).contains(&x)
340            && (0..CHUNK_WIDTH).contains(&z)
341            && (self.min_y..self.min_y + self.height).contains(&y);
342        inside.then(|| (((y - self.min_y) * CHUNK_WIDTH + z) * CHUNK_WIDTH + x) as usize)
343    }
344
345    /// Places `block_id` at local `x`/`z` (0..16), world `y` (between
346    /// `min_y` and `min_y + height`) over whatever was there; `false`
347    /// outside the column (or past `u16::MAX` distinct blocks).
348    pub fn set_block(&mut self, x: i32, y: i32, z: i32, block_id: &str) -> bool {
349        let Some(cell) = self.cell(x, y, z) else {
350            return false;
351        };
352        let index = match self.palette.iter().position(|b| b == block_id) {
353            Some(i) => i,
354            None => {
355                self.palette.push(block_id.to_owned());
356                self.palette.len() - 1
357            }
358        };
359        let Ok(value) = u16::try_from(index + 1) else {
360            return false;
361        };
362        if self.cells.is_empty() {
363            self.cells = vec![0; (CHUNK_WIDTH * CHUNK_WIDTH * self.height.max(0)) as usize * CELL];
364        }
365        self.cells[cell * CELL..(cell + 1) * CELL].copy_from_slice(&value.to_le_bytes());
366        true
367    }
368
369    /// The block placed at local `x`/`z`, world `y`, if any.
370    pub fn block(&self, x: i32, y: i32, z: i32) -> Option<&str> {
371        let cell = self.cell(x, y, z)?;
372        let bytes = self.cells.get(cell * CELL..(cell + 1) * CELL)?;
373        let value = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
374        self.palette.get(value.checked_sub(1)?).map(String::as_str)
375    }
376
377    /// Every block placed, as (local x, world y, local z, id), Y then Z then X.
378    pub fn placed(&self) -> impl Iterator<Item = (i32, i32, i32, &str)> {
379        let width = CHUNK_WIDTH as usize;
380        self.cells
381            .as_chunks::<CELL>()
382            .0
383            .iter()
384            .enumerate()
385            .filter_map(move |(cell, b)| {
386                let value = u16::from_le_bytes(*b) as usize;
387                let id = self.palette.get(value.checked_sub(1)?)?;
388                let (x, z, y) = (cell % width, cell / width % width, cell / (width * width));
389                Some((x as i32, y as i32 + self.min_y, z as i32, id.as_str()))
390            })
391    }
392}
393
394#[cfg(test)]
395mod chunk_tests {
396    use super::*;
397
398    #[test]
399    fn placed_blocks_share_a_palette_and_stay_in_the_column() {
400        let mut chunk = GeneratedChunk {
401            min_y: -64,
402            height: 384,
403            ..Default::default()
404        };
405        assert!(
406            chunk.cells.is_empty(),
407            "nothing allocated before the first block"
408        );
409        assert!(chunk.set_block(0, -64, 15, "minecraft:bedrock"));
410        assert!(chunk.set_block(1, 0, 1, "minecraft:stone"));
411        assert!(
412            chunk.set_block(1, 0, 1, "minecraft:diamond_ore"),
413            "a later block replaces an earlier one"
414        );
415        assert!(chunk.set_block(2, 0, 2, "minecraft:stone"));
416        assert!(
417            !chunk.set_block(16, 0, 0, "minecraft:stone"),
418            "x past the column"
419        );
420        assert!(
421            !chunk.set_block(0, 320, 0, "minecraft:stone"),
422            "y past the top"
423        );
424        assert_eq!(
425            chunk.palette,
426            [
427                "minecraft:bedrock",
428                "minecraft:stone",
429                "minecraft:diamond_ore"
430            ]
431        );
432        assert_eq!(chunk.block(1, 0, 1), Some("minecraft:diamond_ore"));
433        assert_eq!(chunk.block(3, 0, 3), None);
434        let placed: Vec<_> = chunk.placed().collect();
435        assert_eq!(
436            placed,
437            [
438                (0, -64, 15, "minecraft:bedrock"),
439                (1, 0, 1, "minecraft:diamond_ore"),
440                (2, 0, 2, "minecraft:stone")
441            ]
442        );
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn dimension_def_roundtrips_through_yog_data() {
452        let def = YogDimensionDef::new("mymod:my_dim")
453            .dimension_type(
454                YogDimensionTypeDef::default()
455                    .ultrawarm(true)
456                    .effects("nether"),
457            )
458            .generator_type("mymod:custom_terrain")
459            .with_extra("note", "test");
460
461        let back: YogDimensionDef =
462            yog_data::read_bytes(&yog_data::to_bytes(&def)).expect("a def reads back");
463        assert_eq!(back, def);
464        assert_eq!(back.id, "mymod:my_dim");
465        assert!(back.dimension_type.ultrawarm);
466        assert_eq!(back.dimension_type.effects, "nether");
467        assert_eq!(back.generator_type.as_deref(), Some("mymod:custom_terrain"));
468        assert_eq!(back.extra.get("note").map(String::as_str), Some("test"));
469    }
470
471    #[test]
472    fn dimension_def_can_target_a_vanilla_id_to_patch_it() {
473        let def =
474            YogDimensionDef::new("minecraft:overworld").generator_type("mymod:custom_terrain");
475        assert_eq!(def.id, "minecraft:overworld");
476        assert_eq!(def.generator_type.as_deref(), Some("mymod:custom_terrain"));
477    }
478}