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