1use std::collections::HashMap;
34
35use yog_data::Schema;
36
37#[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
69const 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 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#[derive(Schema, Debug, Clone, PartialEq)]
208pub struct YogDimensionDef {
209 pub id: String,
213 pub dimension_type: YogDimensionTypeDef,
214 pub generator_type: Option<String>,
218 pub biome: String,
221 pub structures: bool,
224 pub features: bool,
227 pub mob_spawning: bool,
230 #[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 pub fn new(id: impl Into<String>) -> Self {
254 Self {
255 id: id.into(),
256 ..Self::default()
257 }
258 }
259
260 pub fn dimension_type(mut self, dimension_type: YogDimensionTypeDef) -> Self {
262 self.dimension_type = dimension_type;
263 self
264 }
265
266 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 pub fn biome(mut self, biome: impl Into<String>) -> Self {
275 self.biome = biome.into();
276 self
277 }
278
279 pub fn structures(mut self, on: bool) -> Self {
281 self.structures = on;
282 self
283 }
284
285 pub fn features(mut self, on: bool) -> Self {
287 self.features = on;
288 self
289 }
290
291 pub fn mob_spawning(mut self, on: bool) -> Self {
293 self.mob_spawning = on;
294 self
295 }
296
297 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
304pub const CHUNK_WIDTH: i32 = 16;
308
309#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
315pub struct GeneratedChunk {
316 pub generator: String,
318 pub chunk_x: i32,
320 pub chunk_z: i32,
321 pub min_y: i32,
323 pub height: i32,
324 #[data(default)]
326 pub palette: Vec<String>,
327 #[data(with = "yog_data::with::Raw")]
330 pub cells: Vec<u8>,
331}
332
333pub const CELL: usize = std::mem::size_of::<u16>();
335
336impl GeneratedChunk {
337 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 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 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 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}