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//! Chunk *generation* is intentionally **not** a fixed preset/config here —
9//! there's no single "noise" or "flat" style that covers every mod's needs,
10//! and real generators often layer several algorithms together. Instead,
11//! mods write their own generator as a plain closure registered via
12//! `Registry::register_chunk_generator`, called once per chunk column with a
13//! [`ChunkWriter`] handle the closure uses to place blocks however it likes
14//! (composing whatever noise functions or logic it wants — that's the mod's
15//! own code, not this crate's concern).
16
17use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21// ── Dimension type ───────────────────────────────────────────────────────────
22
23/// Properties of a dimension type — mirrors Java's `YogDimensionType`.
24///
25/// All fields default to vanilla-overworld-like values so mods only need to
26/// override what actually differs. `#[serde(default)]` on every field keeps
27/// this forward-compatible: older mod JSON still deserializes on a runtime
28/// that's added new fields.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31pub struct YogDimensionTypeDef {
32    pub min_y: i32,
33    pub height: i32,
34    pub logical_height: i32,
35    pub has_sky_light: bool,
36    pub sky_light_updates: bool,
37    pub ambient_light: f32,
38    pub coordinate_scale: f32,
39    pub ultrawarm: bool,
40    pub beds_explode: bool,
41    pub respawn_anchors_explode: bool,
42    pub piglin_safe: bool,
43    pub natural: bool,
44    pub has_ceiling: bool,
45    pub effects: String,
46    pub has_sky: bool,
47    pub has_clouds: bool,
48    pub cloud_height: f64,
49    pub has_fog: bool,
50    pub fog_color: Option<i32>,
51    pub sky_color: Option<i32>,
52    pub water_color: Option<i32>,
53    pub water_fog_color: Option<i32>,
54}
55
56impl Default for YogDimensionTypeDef {
57    fn default() -> Self {
58        Self {
59            min_y: -64,
60            height: 384,
61            logical_height: 384,
62            has_sky_light: true,
63            sky_light_updates: true,
64            ambient_light: 0.0,
65            coordinate_scale: 1.0,
66            ultrawarm: false,
67            beds_explode: false,
68            respawn_anchors_explode: false,
69            piglin_safe: false,
70            natural: true,
71            has_ceiling: false,
72            effects: "overworld".to_owned(),
73            has_sky: true,
74            has_clouds: true,
75            cloud_height: 192.0,
76            has_fog: false,
77            fog_color: None,
78            sky_color: None,
79            water_color: None,
80            water_fog_color: None,
81        }
82    }
83}
84
85impl YogDimensionTypeDef {
86    // ── Chained setters — start from `YogDimensionTypeDef::default()` and
87    // override only what differs from the overworld-like baseline. ─────────
88
89    pub fn min_y(mut self, v: i32) -> Self {
90        self.min_y = v;
91        self
92    }
93    pub fn height(mut self, v: i32) -> Self {
94        self.height = v;
95        self.logical_height = v;
96        self
97    }
98    pub fn logical_height(mut self, v: i32) -> Self {
99        self.logical_height = v;
100        self
101    }
102    pub fn has_sky_light(mut self, v: bool) -> Self {
103        self.has_sky_light = v;
104        self
105    }
106    pub fn sky_light_updates(mut self, v: bool) -> Self {
107        self.sky_light_updates = v;
108        self
109    }
110    pub fn ambient_light(mut self, v: f32) -> Self {
111        self.ambient_light = v;
112        self
113    }
114    pub fn coordinate_scale(mut self, v: f32) -> Self {
115        self.coordinate_scale = v;
116        self
117    }
118    pub fn ultrawarm(mut self, v: bool) -> Self {
119        self.ultrawarm = v;
120        self
121    }
122    pub fn beds_explode(mut self, v: bool) -> Self {
123        self.beds_explode = v;
124        self
125    }
126    pub fn respawn_anchors_explode(mut self, v: bool) -> Self {
127        self.respawn_anchors_explode = v;
128        self
129    }
130    pub fn piglin_safe(mut self, v: bool) -> Self {
131        self.piglin_safe = v;
132        self
133    }
134    pub fn natural(mut self, v: bool) -> Self {
135        self.natural = v;
136        self
137    }
138    pub fn has_ceiling(mut self, v: bool) -> Self {
139        self.has_ceiling = v;
140        self
141    }
142    pub fn effects(mut self, v: impl Into<String>) -> Self {
143        self.effects = v.into();
144        self
145    }
146    pub fn has_sky(mut self, v: bool) -> Self {
147        self.has_sky = v;
148        self
149    }
150    pub fn has_clouds(mut self, v: bool) -> Self {
151        self.has_clouds = v;
152        self
153    }
154    pub fn cloud_height(mut self, v: f64) -> Self {
155        self.cloud_height = v;
156        self
157    }
158    pub fn has_fog(mut self, v: bool) -> Self {
159        self.has_fog = v;
160        self
161    }
162    pub fn fog_color(mut self, v: i32) -> Self {
163        self.fog_color = Some(v);
164        self
165    }
166    pub fn sky_color(mut self, v: i32) -> Self {
167        self.sky_color = Some(v);
168        self
169    }
170    pub fn water_color(mut self, v: i32) -> Self {
171        self.water_color = Some(v);
172        self
173    }
174    pub fn water_fog_color(mut self, v: i32) -> Self {
175        self.water_fog_color = Some(v);
176        self
177    }
178}
179
180// ── Dimension definition ─────────────────────────────────────────────────────
181
182/// A dimension definition — what a mod registers to create a custom
183/// dimension. Serialized to JSON and sent across the ABI via
184/// `Registry::register_dimension` (in `yog-api`), which the Java host parses
185/// to register a `LevelStem`/`DimensionType` entry at mod-init time.
186///
187/// The actual `ServerLevel` (world data) is created lazily at runtime — see
188/// the crate-level docs and `Registry::register_chunk_generator`.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(default)]
191pub struct YogDimensionDef {
192    pub id: String,
193    pub dimension_type: YogDimensionTypeDef,
194    /// Extra platform/metadata mods want the host to know about (arbitrary
195    /// JSON-string values), for anything not modeled by `dimension_type`.
196    pub extra: HashMap<String, String>,
197}
198
199impl Default for YogDimensionDef {
200    fn default() -> Self {
201        Self {
202            id: String::new(),
203            dimension_type: YogDimensionTypeDef::default(),
204            extra: HashMap::new(),
205        }
206    }
207}
208
209impl YogDimensionDef {
210    /// Start a new definition with default (overworld-like) type properties.
211    pub fn new(id: impl Into<String>) -> Self {
212        Self {
213            id: id.into(),
214            ..Self::default()
215        }
216    }
217
218    /// Replace the dimension type properties.
219    pub fn dimension_type(mut self, dimension_type: YogDimensionTypeDef) -> Self {
220        self.dimension_type = dimension_type;
221        self
222    }
223
224    /// Set an extra metadata property.
225    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
226        self.extra.insert(key.into(), value.into());
227        self
228    }
229
230    /// Serialize to the JSON wire format sent to the Java host.
231    pub fn to_json(&self) -> String {
232        serde_json::to_string(self).expect("YogDimensionDef serializes infallibly")
233    }
234}
235
236// ── Chunk writer handle ──────────────────────────────────────────────────────
237
238/// Handle passed to a chunk-generator closure (see the crate-level docs),
239/// wrapping the runtime's per-callback vtable — mirrors `GfxContext` in
240/// `yog-gfx`. Valid only for the duration of the generator callback.
241pub struct ChunkWriter(*const yog_abi::YogChunkWriterApi);
242
243impl ChunkWriter {
244    #[doc(hidden)]
245    pub unsafe fn from_raw(raw: *const yog_abi::YogChunkWriterApi) -> Self {
246        Self(raw)
247    }
248
249    #[inline]
250    fn api(&self) -> &yog_abi::YogChunkWriterApi {
251        unsafe { &*self.0 }
252    }
253
254    /// Chunk X coordinate (in chunks, not blocks).
255    pub fn chunk_x(&self) -> i32 {
256        self.api().chunk_x
257    }
258
259    /// Chunk Z coordinate (in chunks, not blocks).
260    pub fn chunk_z(&self) -> i32 {
261        self.api().chunk_z
262    }
263
264    /// Minimum build height (world Y) — matches the dimension type's `min_y`.
265    pub fn min_y(&self) -> i32 {
266        self.api().min_y
267    }
268
269    /// Total world height — matches the dimension type's `height`.
270    pub fn height(&self) -> i32 {
271        self.api().height
272    }
273
274    /// Set a block within this chunk column. `x`/`z` are **local** (0..16)
275    /// chunk-relative coordinates; `y` is world-absolute (between `min_y()`
276    /// and `min_y() + height()`).
277    pub fn set_block(&self, x: i32, y: i32, z: i32, block_id: &str) -> bool {
278        let api = self.api();
279        unsafe { (api.set_block)(api.ctx, x, y, z, yog_abi::YogStr::from_str(block_id)) }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn dimension_def_to_json_roundtrips_through_serde() {
289        let def = YogDimensionDef::new("mymod:my_dim")
290            .dimension_type(
291                YogDimensionTypeDef::default()
292                    .ultrawarm(true)
293                    .effects("nether"),
294            )
295            .with_extra("note", "test");
296
297        let json = def.to_json();
298        let back: YogDimensionDef = serde_json::from_str(&json).expect("valid json");
299        assert_eq!(back.id, "mymod:my_dim");
300        assert!(back.dimension_type.ultrawarm);
301        assert_eq!(back.dimension_type.effects, "nether");
302        assert_eq!(back.extra.get("note").map(String::as_str), Some("test"));
303    }
304}