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    /// Extra platform/metadata mods want the host to know about (arbitrary
219    /// JSON-string values), for anything not modeled by `dimension_type`.
220    pub extra: HashMap<String, String>,
221}
222
223impl Default for YogDimensionDef {
224    fn default() -> Self {
225        Self {
226            id: String::new(),
227            dimension_type: YogDimensionTypeDef::default(),
228            generator_type: None,
229            extra: HashMap::new(),
230        }
231    }
232}
233
234impl YogDimensionDef {
235    /// Start a new definition with default (overworld-like) type properties.
236    pub fn new(id: impl Into<String>) -> Self {
237        Self {
238            id: id.into(),
239            ..Self::default()
240        }
241    }
242
243    /// Replace the dimension type properties.
244    pub fn dimension_type(mut self, dimension_type: YogDimensionTypeDef) -> Self {
245        self.dimension_type = dimension_type;
246        self
247    }
248
249    /// Use the generator type registered under this id (see
250    /// `Registry::register_chunk_generator`).
251    pub fn generator_type(mut self, generator_type_id: impl Into<String>) -> Self {
252        self.generator_type = Some(generator_type_id.into());
253        self
254    }
255
256    /// Set an extra metadata property.
257    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
258        self.extra.insert(key.into(), value.into());
259        self
260    }
261
262    /// Serialize to the JSON wire format sent to the Java host.
263    pub fn to_json(&self) -> String {
264        serde_json::to_string(self).expect("YogDimensionDef serializes infallibly")
265    }
266}
267
268// ── Chunk writer handle ──────────────────────────────────────────────────────
269
270/// Handle passed to a chunk-generator closure (see the crate-level docs),
271/// wrapping the runtime's per-callback vtable — mirrors `GfxContext` in
272/// `yog-gfx`. Valid only for the duration of the generator callback.
273pub struct ChunkWriter(*const yog_abi::YogChunkWriterApi);
274
275impl ChunkWriter {
276    #[doc(hidden)]
277    pub unsafe fn from_raw(raw: *const yog_abi::YogChunkWriterApi) -> Self {
278        Self(raw)
279    }
280
281    #[inline]
282    fn api(&self) -> &yog_abi::YogChunkWriterApi {
283        unsafe { &*self.0 }
284    }
285
286    /// Chunk X coordinate (in chunks, not blocks).
287    pub fn chunk_x(&self) -> i32 {
288        self.api().chunk_x
289    }
290
291    /// Chunk Z coordinate (in chunks, not blocks).
292    pub fn chunk_z(&self) -> i32 {
293        self.api().chunk_z
294    }
295
296    /// Minimum build height (world Y) — matches the dimension type's `min_y`.
297    pub fn min_y(&self) -> i32 {
298        self.api().min_y
299    }
300
301    /// Total world height — matches the dimension type's `height`.
302    pub fn height(&self) -> i32 {
303        self.api().height
304    }
305
306    /// Set a block within this chunk column. `x`/`z` are **local** (0..16)
307    /// chunk-relative coordinates; `y` is world-absolute (between `min_y()`
308    /// and `min_y() + height()`).
309    pub fn set_block(&self, x: i32, y: i32, z: i32, block_id: &str) -> bool {
310        let api = self.api();
311        unsafe { (api.set_block)(api.ctx, x, y, z, yog_abi::YogStr::from_str(block_id)) }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn dimension_def_to_json_roundtrips_through_serde() {
321        let def = YogDimensionDef::new("mymod:my_dim")
322            .dimension_type(
323                YogDimensionTypeDef::default()
324                    .ultrawarm(true)
325                    .effects("nether"),
326            )
327            .generator_type("mymod:custom_terrain")
328            .with_extra("note", "test");
329
330        let json = def.to_json();
331        let back: YogDimensionDef = serde_json::from_str(&json).expect("valid json");
332        assert_eq!(back.id, "mymod:my_dim");
333        assert!(back.dimension_type.ultrawarm);
334        assert_eq!(back.dimension_type.effects, "nether");
335        assert_eq!(back.generator_type.as_deref(), Some("mymod:custom_terrain"));
336        assert_eq!(back.extra.get("note").map(String::as_str), Some("test"));
337    }
338
339    #[test]
340    fn dimension_def_can_target_a_vanilla_id_to_patch_it() {
341        let def =
342            YogDimensionDef::new("minecraft:overworld").generator_type("mymod:custom_terrain");
343        assert_eq!(def.id, "minecraft:overworld");
344        assert_eq!(def.generator_type.as_deref(), Some("mymod:custom_terrain"));
345    }
346}