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 a
20//! [`ChunkWriter`] handle the closure uses to place blocks 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 serde::{Deserialize, Serialize};
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. `#[serde(default)]` on every field keeps
43/// this forward-compatible: older mod JSON still deserializes on a runtime
44/// that's added new fields.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(default)]
47pub struct YogDimensionTypeDef {
48    pub min_y: i32,
49    pub height: i32,
50    pub logical_height: i32,
51    pub has_sky_light: bool,
52    pub sky_light_updates: bool,
53    pub ambient_light: f32,
54    pub coordinate_scale: f32,
55    pub ultrawarm: bool,
56    pub beds_explode: bool,
57    pub respawn_anchors_explode: bool,
58    pub piglin_safe: bool,
59    pub natural: bool,
60    pub has_ceiling: bool,
61    pub effects: String,
62    pub has_sky: bool,
63    pub has_clouds: bool,
64    pub cloud_height: f64,
65    pub has_fog: bool,
66    pub fog_color: Option<i32>,
67    pub sky_color: Option<i32>,
68    pub water_color: Option<i32>,
69    pub water_fog_color: Option<i32>,
70}
71
72impl Default for YogDimensionTypeDef {
73    fn default() -> Self {
74        Self {
75            min_y: -64,
76            height: 384,
77            logical_height: 384,
78            has_sky_light: true,
79            sky_light_updates: true,
80            ambient_light: 0.0,
81            coordinate_scale: 1.0,
82            ultrawarm: false,
83            beds_explode: false,
84            respawn_anchors_explode: false,
85            piglin_safe: false,
86            natural: true,
87            has_ceiling: false,
88            effects: "overworld".to_owned(),
89            has_sky: true,
90            has_clouds: true,
91            cloud_height: 192.0,
92            has_fog: false,
93            fog_color: None,
94            sky_color: None,
95            water_color: None,
96            water_fog_color: None,
97        }
98    }
99}
100
101impl YogDimensionTypeDef {
102    // ── Chained setters — start from `YogDimensionTypeDef::default()` and
103    // override only what differs from the overworld-like baseline. ─────────
104
105    pub fn min_y(mut self, v: i32) -> Self {
106        self.min_y = v;
107        self
108    }
109    pub fn height(mut self, v: i32) -> Self {
110        self.height = v;
111        self.logical_height = v;
112        self
113    }
114    pub fn logical_height(mut self, v: i32) -> Self {
115        self.logical_height = v;
116        self
117    }
118    pub fn has_sky_light(mut self, v: bool) -> Self {
119        self.has_sky_light = v;
120        self
121    }
122    pub fn sky_light_updates(mut self, v: bool) -> Self {
123        self.sky_light_updates = v;
124        self
125    }
126    pub fn ambient_light(mut self, v: f32) -> Self {
127        self.ambient_light = v;
128        self
129    }
130    pub fn coordinate_scale(mut self, v: f32) -> Self {
131        self.coordinate_scale = v;
132        self
133    }
134    pub fn ultrawarm(mut self, v: bool) -> Self {
135        self.ultrawarm = v;
136        self
137    }
138    pub fn beds_explode(mut self, v: bool) -> Self {
139        self.beds_explode = v;
140        self
141    }
142    pub fn respawn_anchors_explode(mut self, v: bool) -> Self {
143        self.respawn_anchors_explode = v;
144        self
145    }
146    pub fn piglin_safe(mut self, v: bool) -> Self {
147        self.piglin_safe = v;
148        self
149    }
150    pub fn natural(mut self, v: bool) -> Self {
151        self.natural = v;
152        self
153    }
154    pub fn has_ceiling(mut self, v: bool) -> Self {
155        self.has_ceiling = v;
156        self
157    }
158    pub fn effects(mut self, v: impl Into<String>) -> Self {
159        self.effects = v.into();
160        self
161    }
162    pub fn has_sky(mut self, v: bool) -> Self {
163        self.has_sky = v;
164        self
165    }
166    pub fn has_clouds(mut self, v: bool) -> Self {
167        self.has_clouds = v;
168        self
169    }
170    pub fn cloud_height(mut self, v: f64) -> Self {
171        self.cloud_height = v;
172        self
173    }
174    pub fn has_fog(mut self, v: bool) -> Self {
175        self.has_fog = v;
176        self
177    }
178    pub fn fog_color(mut self, v: i32) -> Self {
179        self.fog_color = Some(v);
180        self
181    }
182    pub fn sky_color(mut self, v: i32) -> Self {
183        self.sky_color = Some(v);
184        self
185    }
186    pub fn water_color(mut self, v: i32) -> Self {
187        self.water_color = Some(v);
188        self
189    }
190    pub fn water_fog_color(mut self, v: i32) -> Self {
191        self.water_fog_color = Some(v);
192        self
193    }
194}
195
196// ── Dimension definition ─────────────────────────────────────────────────────
197
198/// A dimension definition — what a mod registers to create a custom
199/// dimension, or to patch an existing (including vanilla) one. Serialized
200/// to JSON and sent across the ABI via `Registry::register_dimension` (in
201/// `yog-api`), which the Java host parses to register (or patch) a
202/// `LevelStem`/`DimensionType` entry at mod-init time.
203///
204/// The actual `ServerLevel` (world data) is created lazily at runtime — see
205/// the crate-level docs and `Registry::register_chunk_generator`.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(default)]
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    pub extra: HashMap<String, String>,
233}
234
235impl Default for YogDimensionDef {
236    fn default() -> Self {
237        Self {
238            id: String::new(),
239            dimension_type: YogDimensionTypeDef::default(),
240            generator_type: None,
241            biome: "minecraft:plains".into(),
242            structures: true,
243            features: true,
244            mob_spawning: true,
245            extra: HashMap::new(),
246        }
247    }
248}
249
250impl YogDimensionDef {
251    /// Start a new definition with default (overworld-like) type properties.
252    pub fn new(id: impl Into<String>) -> Self {
253        Self {
254            id: id.into(),
255            ..Self::default()
256        }
257    }
258
259    /// Replace the dimension type properties.
260    pub fn dimension_type(mut self, dimension_type: YogDimensionTypeDef) -> Self {
261        self.dimension_type = dimension_type;
262        self
263    }
264
265    /// Use the generator type registered under this id (see
266    /// `Registry::register_chunk_generator`).
267    pub fn generator_type(mut self, generator_type_id: impl Into<String>) -> Self {
268        self.generator_type = Some(generator_type_id.into());
269        self
270    }
271
272    /// The biome the dimension is made of.
273    pub fn biome(mut self, biome: impl Into<String>) -> Self {
274        self.biome = biome.into();
275        self
276    }
277
278    /// Whether structures generate (default: yes).
279    pub fn structures(mut self, on: bool) -> Self {
280        self.structures = on;
281        self
282    }
283
284    /// Whether the biome's features are placed (default: yes).
285    pub fn features(mut self, on: bool) -> Self {
286        self.features = on;
287        self
288    }
289
290    /// Whether mobs spawn (default: yes).
291    pub fn mob_spawning(mut self, on: bool) -> Self {
292        self.mob_spawning = on;
293        self
294    }
295
296    /// Set an extra metadata property.
297    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
298        self.extra.insert(key.into(), value.into());
299        self
300    }
301
302    /// Serialize to the JSON wire format sent to the Java host.
303    pub fn to_json(&self) -> String {
304        serde_json::to_string(self).expect("YogDimensionDef serializes infallibly")
305    }
306}
307
308// ── Chunk writer handle ──────────────────────────────────────────────────────
309
310/// Handle passed to a chunk-generator closure (see the crate-level docs),
311/// wrapping the runtime's per-callback vtable — mirrors `GfxContext` in
312/// `yog-gfx`. Valid only for the duration of the generator callback.
313pub struct ChunkWriter(*const yog_abi::YogChunkWriterApi);
314
315impl ChunkWriter {
316    #[doc(hidden)]
317    pub unsafe fn from_raw(raw: *const yog_abi::YogChunkWriterApi) -> Self {
318        Self(raw)
319    }
320
321    #[inline]
322    fn api(&self) -> &yog_abi::YogChunkWriterApi {
323        unsafe { &*self.0 }
324    }
325
326    /// Chunk X coordinate (in chunks, not blocks).
327    pub fn chunk_x(&self) -> i32 {
328        self.api().chunk_x
329    }
330
331    /// Chunk Z coordinate (in chunks, not blocks).
332    pub fn chunk_z(&self) -> i32 {
333        self.api().chunk_z
334    }
335
336    /// Minimum build height (world Y) — matches the dimension type's `min_y`.
337    pub fn min_y(&self) -> i32 {
338        self.api().min_y
339    }
340
341    /// Total world height — matches the dimension type's `height`.
342    pub fn height(&self) -> i32 {
343        self.api().height
344    }
345
346    /// Set a block within this chunk column. `x`/`z` are **local** (0..16)
347    /// chunk-relative coordinates; `y` is world-absolute (between `min_y()`
348    /// and `min_y() + height()`).
349    pub fn set_block(&self, x: i32, y: i32, z: i32, block_id: &str) -> bool {
350        let api = self.api();
351        unsafe { (api.set_block)(api.ctx, x, y, z, yog_abi::YogStr::from_str(block_id)) }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn dimension_def_to_json_roundtrips_through_serde() {
361        let def = YogDimensionDef::new("mymod:my_dim")
362            .dimension_type(
363                YogDimensionTypeDef::default()
364                    .ultrawarm(true)
365                    .effects("nether"),
366            )
367            .generator_type("mymod:custom_terrain")
368            .with_extra("note", "test");
369
370        let json = def.to_json();
371        let back: YogDimensionDef = serde_json::from_str(&json).expect("valid json");
372        assert_eq!(back.id, "mymod:my_dim");
373        assert!(back.dimension_type.ultrawarm);
374        assert_eq!(back.dimension_type.effects, "nether");
375        assert_eq!(back.generator_type.as_deref(), Some("mymod:custom_terrain"));
376        assert_eq!(back.extra.get("note").map(String::as_str), Some("test"));
377    }
378
379    #[test]
380    fn dimension_def_can_target_a_vanilla_id_to_patch_it() {
381        let def =
382            YogDimensionDef::new("minecraft:overworld").generator_type("mymod:custom_terrain");
383        assert_eq!(def.id, "minecraft:overworld");
384        assert_eq!(def.generator_type.as_deref(), Some("mymod:custom_terrain"));
385    }
386}