Skip to main content

steel_worldgen/structure/
end_city.rs

1//! End city. Vanilla's `EndCityPieces`: recursive template-based piece generation
2//! (base → towers → bridges → house towers/ships/fat towers), depth ≤ 8.
3
4use glam::IVec3;
5use rustc_hash::FxHashMap;
6use steel_registry::structure::LiquidSettingsData;
7use steel_registry::template_pool::TemplateData;
8use steel_utils::random::Random;
9use steel_utils::random::legacy_random::LegacyRandom;
10use steel_utils::{BoundingBox, Direction, Identifier, Rotation};
11
12use crate::structure::{
13    GenerationStub, Structure, StructureBlockIgnore, StructureGenerationContext, StructureMirror,
14    StructurePiece, StructurePiecePayload, TemplateMarkerHandling, TemplatePieceData,
15    TemplatePlacementAdjustment, TemplatePlacementClip, TemplatePostProcess, TemplateProcessorList,
16};
17use steel_registry::structure::StructureData;
18
19const MAX_GEN_DEPTH: i32 = 8;
20
21/// End-city piece (template name, world template-origin, rotation).
22#[derive(Debug, Clone)]
23pub struct EndCityPiece {
24    /// Template name relative to `minecraft:end_city/`.
25    pub template_name: String,
26    /// World-space template-origin.
27    pub template_position: IVec3,
28    /// Piece rotation.
29    pub rotation: Rotation,
30    /// Gen-depth tag; overwritten when the parent's `recursiveChildren` finishes.
31    pub gen_depth: i32,
32    /// Vanilla overwrite flag selecting structure-block-only vs structure-and-air ignore.
33    pub overwrite: bool,
34}
35
36type Templates = FxHashMap<Identifier, TemplateData>;
37
38fn template_size(templates: &Templates, name: &str) -> Option<IVec3> {
39    let id = Identifier::new("minecraft", format!("end_city/{name}"));
40    templates.get(&id).map(|t| IVec3::from(t.size))
41}
42
43fn piece_bb(templates: &Templates, piece: &EndCityPiece) -> BoundingBox {
44    let size = template_size(templates, &piece.template_name)
45        .unwrap_or_else(|| panic!("missing end_city template: {}", piece.template_name));
46    piece
47        .rotation
48        .get_bounding_box(piece.template_position, size)
49}
50
51/// Vanilla's `addPiece`. With pivot=ZERO and mirror=NONE,
52/// `calculateConnectedPosition` reduces to `rotate(offset, parent.rotation)`
53/// added to the parent's template position.
54fn add_piece(
55    parent: &EndCityPiece,
56    offset: IVec3,
57    template_name: &str,
58    rotation: Rotation,
59    overwrite: bool,
60) -> EndCityPiece {
61    let rotated = parent.rotation.transform_pos(offset, IVec3::ZERO);
62    EndCityPiece {
63        template_name: template_name.to_string(),
64        template_position: parent.template_position + rotated,
65        rotation,
66        gen_depth: 0,
67        overwrite,
68    }
69}
70
71/// Mirrors vanilla's `TOWER_BRIDGE_GENERATOR.shipCreated`.
72struct SharedState {
73    ship_created: bool,
74}
75
76/// Produces child pieces for a given section-generator kind.
77#[derive(Debug, Clone, Copy)]
78enum SectionKind {
79    HouseTower,
80    Tower,
81    TowerBridge,
82    FatTower,
83}
84
85#[expect(
86    clippy::too_many_arguments,
87    reason = "threads parent + rotation + shared state as in vanilla's recursive dispatch"
88)]
89fn recursive_children(
90    templates: &Templates,
91    kind: SectionKind,
92    gen_depth: i32,
93    parent: &EndCityPiece,
94    offset: IVec3,
95    pieces: &mut Vec<EndCityPiece>,
96    shared: &mut SharedState,
97    rng: &mut LegacyRandom,
98) -> bool {
99    if gen_depth > MAX_GEN_DEPTH {
100        return false;
101    }
102    let mut child_pieces: Vec<EndCityPiece> = Vec::new();
103    let ok = match kind {
104        SectionKind::HouseTower => generate_house_tower(
105            templates,
106            gen_depth,
107            parent,
108            offset,
109            &mut child_pieces,
110            shared,
111            rng,
112        ),
113        SectionKind::Tower => {
114            generate_tower(templates, gen_depth, parent, &mut child_pieces, shared, rng)
115        }
116        SectionKind::TowerBridge => {
117            generate_tower_bridge(templates, gen_depth, parent, &mut child_pieces, shared, rng)
118        }
119        SectionKind::FatTower => {
120            generate_fat_tower(templates, gen_depth, parent, &mut child_pieces, shared, rng)
121        }
122    };
123    if !ok {
124        return false;
125    }
126
127    let child_tag = rng.next_i32();
128    let parent_tag = parent.gen_depth;
129    for child in &mut child_pieces {
130        child.gen_depth = child_tag;
131        let child_bb = piece_bb(templates, child);
132        if pieces
133            .iter()
134            .filter(|e| e.gen_depth != parent_tag)
135            .any(|e| piece_bb(templates, e).intersects(child_bb))
136        {
137            return false;
138        }
139    }
140    pieces.extend(child_pieces);
141    true
142}
143
144fn generate_house_tower(
145    templates: &Templates,
146    gen_depth: i32,
147    parent: &EndCityPiece,
148    offset: IVec3,
149    pieces: &mut Vec<EndCityPiece>,
150    shared: &mut SharedState,
151    rng: &mut LegacyRandom,
152) -> bool {
153    if gen_depth > MAX_GEN_DEPTH {
154        return false;
155    }
156    let rotation = parent.rotation;
157    let mut last = add_piece(parent, offset, "base_floor", rotation, true);
158    pieces.push(last.clone());
159    let num_floors = rng.next_i32_bounded(3);
160
161    let mut push = |last: &mut EndCityPiece, off: IVec3, name, overwrite| {
162        let p = add_piece(last, off, name, rotation, overwrite);
163        pieces.push(p.clone());
164        *last = p;
165    };
166
167    if num_floors == 0 {
168        push(&mut last, IVec3::new(-1, 4, -1), "base_roof", true);
169    } else {
170        push(&mut last, IVec3::new(-1, 0, -1), "second_floor_2", false);
171        if num_floors == 1 {
172            push(&mut last, IVec3::new(-1, 8, -1), "second_roof", false);
173        } else if num_floors == 2 {
174            push(&mut last, IVec3::new(-1, 4, -1), "third_floor_2", false);
175            push(&mut last, IVec3::new(-1, 8, -1), "third_roof", true);
176        }
177        if num_floors >= 1 {
178            recursive_children(
179                templates,
180                SectionKind::Tower,
181                gen_depth + 1,
182                &last,
183                IVec3::ZERO,
184                pieces,
185                shared,
186                rng,
187            );
188        }
189    }
190    true
191}
192
193const TOWER_BRIDGES: [(Rotation, IVec3); 4] = [
194    (Rotation::None, IVec3::new(1, -1, 0)),
195    (Rotation::Clockwise90, IVec3::new(6, -1, 1)),
196    (Rotation::CounterClockwise90, IVec3::new(0, -1, 5)),
197    (Rotation::Clockwise180, IVec3::new(5, -1, 6)),
198];
199
200const FAT_TOWER_BRIDGES: [(Rotation, IVec3); 4] = [
201    (Rotation::None, IVec3::new(4, -1, 0)),
202    (Rotation::Clockwise90, IVec3::new(12, -1, 4)),
203    (Rotation::CounterClockwise90, IVec3::new(0, -1, 8)),
204    (Rotation::Clockwise180, IVec3::new(8, -1, 12)),
205];
206
207fn generate_tower(
208    templates: &Templates,
209    gen_depth: i32,
210    parent: &EndCityPiece,
211    pieces: &mut Vec<EndCityPiece>,
212    shared: &mut SharedState,
213    rng: &mut LegacyRandom,
214) -> bool {
215    let rotation = parent.rotation;
216    let x_off = 3 + rng.next_i32_bounded(2);
217    let z_off = 3 + rng.next_i32_bounded(2);
218    let mut last = add_piece(
219        parent,
220        IVec3::new(x_off, -3, z_off),
221        "tower_base",
222        rotation,
223        true,
224    );
225    pieces.push(last.clone());
226    let p = add_piece(&last, IVec3::new(0, 7, 0), "tower_piece", rotation, true);
227    pieces.push(p.clone());
228    last = p;
229
230    let mut bridge_piece: Option<EndCityPiece> =
231        (rng.next_i32_bounded(3) == 0).then(|| last.clone());
232    let tower_height = 1 + rng.next_i32_bounded(3);
233    for i in 0..tower_height {
234        let p = add_piece(&last, IVec3::new(0, 4, 0), "tower_piece", rotation, true);
235        pieces.push(p.clone());
236        last = p;
237        if i < tower_height - 1 && rng.next_bool() {
238            bridge_piece = Some(last.clone());
239        }
240    }
241
242    if let Some(bridge_anchor) = bridge_piece {
243        for (rot_offset, offset) in TOWER_BRIDGES {
244            if rng.next_bool() {
245                let child_rot = rotation.then(rot_offset);
246                let bridge_start = add_piece(&bridge_anchor, offset, "bridge_end", child_rot, true);
247                pieces.push(bridge_start.clone());
248                recursive_children(
249                    templates,
250                    SectionKind::TowerBridge,
251                    gen_depth + 1,
252                    &bridge_start,
253                    IVec3::ZERO,
254                    pieces,
255                    shared,
256                    rng,
257                );
258            }
259        }
260        pieces.push(add_piece(
261            &last,
262            IVec3::new(-1, 4, -1),
263            "tower_top",
264            rotation,
265            true,
266        ));
267    } else if gen_depth != 7 {
268        return recursive_children(
269            templates,
270            SectionKind::FatTower,
271            gen_depth + 1,
272            &last,
273            IVec3::ZERO,
274            pieces,
275            shared,
276            rng,
277        );
278    } else {
279        pieces.push(add_piece(
280            &last,
281            IVec3::new(-1, 4, -1),
282            "tower_top",
283            rotation,
284            true,
285        ));
286    }
287    true
288}
289
290fn generate_tower_bridge(
291    templates: &Templates,
292    gen_depth: i32,
293    parent: &EndCityPiece,
294    pieces: &mut Vec<EndCityPiece>,
295    shared: &mut SharedState,
296    rng: &mut LegacyRandom,
297) -> bool {
298    let rotation = parent.rotation;
299    let bridge_length = rng.next_i32_bounded(4) + 1;
300
301    // Vanilla's setGenDepth(-1) marks the first/last bridge_piece as a "different
302    // batch" to sub-recursion collision checks; childTag later overwrites it.
303    let mut first = add_piece(parent, IVec3::new(0, 0, -4), "bridge_piece", rotation, true);
304    first.gen_depth = -1;
305    pieces.push(first.clone());
306
307    let mut next_y = 0;
308    let mut last = first;
309    for _ in 0..bridge_length {
310        if rng.next_bool() {
311            let p = add_piece(
312                &last,
313                IVec3::new(0, next_y, -4),
314                "bridge_piece",
315                rotation,
316                true,
317            );
318            pieces.push(p.clone());
319            last = p;
320            next_y = 0;
321        } else {
322            let (name, dz) = if rng.next_bool() {
323                ("bridge_steep_stairs", -4)
324            } else {
325                ("bridge_gentle_stairs", -8)
326            };
327            let p = add_piece(&last, IVec3::new(0, next_y, dz), name, rotation, true);
328            pieces.push(p.clone());
329            last = p;
330            next_y = 4;
331        }
332    }
333
334    if !shared.ship_created && rng.next_i32_bounded(10 - gen_depth) == 0 {
335        let ship_x = -8 + rng.next_i32_bounded(8);
336        let ship_z = -70 + rng.next_i32_bounded(10);
337        pieces.push(add_piece(
338            &last,
339            IVec3::new(ship_x, next_y, ship_z),
340            "ship",
341            rotation,
342            true,
343        ));
344        shared.ship_created = true;
345    } else if !recursive_children(
346        templates,
347        SectionKind::HouseTower,
348        gen_depth + 1,
349        &last,
350        IVec3::new(-3, next_y + 1, -11),
351        pieces,
352        shared,
353        rng,
354    ) {
355        return false;
356    }
357
358    let end_rot = rotation.then(Rotation::Clockwise180);
359    let mut end = add_piece(&last, IVec3::new(4, next_y, 0), "bridge_end", end_rot, true);
360    end.gen_depth = -1;
361    pieces.push(end);
362    true
363}
364
365fn generate_fat_tower(
366    templates: &Templates,
367    gen_depth: i32,
368    parent: &EndCityPiece,
369    pieces: &mut Vec<EndCityPiece>,
370    shared: &mut SharedState,
371    rng: &mut LegacyRandom,
372) -> bool {
373    let rotation = parent.rotation;
374    let mut last = add_piece(
375        parent,
376        IVec3::new(-3, 4, -3),
377        "fat_tower_base",
378        rotation,
379        true,
380    );
381    pieces.push(last.clone());
382    let p = add_piece(
383        &last,
384        IVec3::new(0, 4, 0),
385        "fat_tower_middle",
386        rotation,
387        true,
388    );
389    pieces.push(p.clone());
390    last = p;
391
392    // Vanilla: `for (i = 0; i < 2 && random.nextInt(3) != 0; i++)` — each iteration
393    // consumes one nextInt(3); 0 exits without body.
394    for _ in 0..2 {
395        if rng.next_i32_bounded(3) == 0 {
396            break;
397        }
398        let p = add_piece(
399            &last,
400            IVec3::new(0, 8, 0),
401            "fat_tower_middle",
402            rotation,
403            true,
404        );
405        pieces.push(p.clone());
406        last = p;
407
408        for (rot_offset, offset) in FAT_TOWER_BRIDGES {
409            if rng.next_bool() {
410                let child_rot = rotation.then(rot_offset);
411                let bridge_start = add_piece(&last, offset, "bridge_end", child_rot, true);
412                pieces.push(bridge_start.clone());
413                recursive_children(
414                    templates,
415                    SectionKind::TowerBridge,
416                    gen_depth + 1,
417                    &bridge_start,
418                    IVec3::ZERO,
419                    pieces,
420                    shared,
421                    rng,
422                );
423            }
424        }
425    }
426    pieces.push(add_piece(
427        &last,
428        IVec3::new(-2, 8, -2),
429        "fat_tower_top",
430        rotation,
431        true,
432    ));
433    true
434}
435
436/// Entry point. Mirrors vanilla's `EndCityPieces.startHouseTower`.
437pub fn start_house_tower(
438    templates: &Templates,
439    origin: IVec3,
440    rotation: Rotation,
441    rng: &mut LegacyRandom,
442) -> Vec<EndCityPiece> {
443    let mut pieces: Vec<EndCityPiece> = Vec::new();
444    let mut shared = SharedState {
445        ship_created: false,
446    };
447
448    // Root: base_floor at origin (constructor, no offset math).
449    let mut last = EndCityPiece {
450        template_name: "base_floor".to_string(),
451        template_position: origin,
452        rotation,
453        gen_depth: 0,
454        overwrite: true,
455    };
456    pieces.push(last.clone());
457
458    for (off, name, overwrite) in [
459        (IVec3::new(-1, 0, -1), "second_floor_1", false),
460        (IVec3::new(-1, 4, -1), "third_floor_1", false),
461        (IVec3::new(-1, 8, -1), "third_roof", true),
462    ] {
463        let p = add_piece(&last, off, name, rotation, overwrite);
464        pieces.push(p.clone());
465        last = p;
466    }
467
468    recursive_children(
469        templates,
470        SectionKind::Tower,
471        1,
472        &last,
473        IVec3::ZERO,
474        &mut pieces,
475        &mut shared,
476        rng,
477    );
478    pieces
479}
480
481fn make_end_city_structure_piece(templates: &Templates, piece: EndCityPiece) -> StructurePiece {
482    let template_id = Identifier::new("minecraft", format!("end_city/{}", piece.template_name));
483    let size = templates
484        .get(&template_id)
485        .map_or(IVec3::ONE, |t| IVec3::from(t.size));
486    StructurePiece {
487        piece_type: Identifier::new_static("minecraft", "ecp"),
488        bounding_box: piece
489            .rotation
490            .get_bounding_box(piece.template_position, size),
491        gen_depth: piece.gen_depth,
492        orientation: Some(Direction::North),
493        payload: StructurePiecePayload::Template(TemplatePieceData {
494            template_id,
495            template_position: piece.template_position,
496            rotation: piece.rotation,
497            mirror: StructureMirror::None,
498            rotation_pivot: IVec3::ZERO,
499            block_ignore: if piece.overwrite {
500                StructureBlockIgnore::StructureBlock
501            } else {
502                StructureBlockIgnore::StructureAndAir
503            },
504            late_block_ignore: StructureBlockIgnore::None,
505            processors: TemplateProcessorList::Empty,
506            liquid_settings: LiquidSettingsData::ApplyWaterlogging,
507            marker_handling: TemplateMarkerHandling::EndCity,
508            placement_adjustment: TemplatePlacementAdjustment::None,
509            placement_clip: TemplatePlacementClip::CenterChunk,
510            post_process: TemplatePostProcess::None,
511        }),
512        ground_level_delta: 0,
513        junctions: Vec::new(),
514        projection: None,
515    }
516}
517
518/// Registered under `"minecraft:end_city"`. Consumes rotation RNG first, then probes
519/// a rotation-dependent 5×5 `lowestY` (reject <60). Biome-checks at the final position.
520pub struct EndCityStructure;
521
522impl Structure for EndCityStructure {
523    fn find_generation_point(
524        &self,
525        ctx: &mut dyn StructureGenerationContext,
526        structure: &StructureData,
527        rng: &mut LegacyRandom,
528    ) -> Option<GenerationStub> {
529        let rotation = Rotation::get_random(rng);
530        let off_xz = match rotation {
531            Rotation::None => IVec3::new(5, 0, 5),
532            Rotation::Clockwise90 => IVec3::new(-5, 0, 5),
533            Rotation::Clockwise180 => IVec3::new(-5, 0, -5),
534            Rotation::CounterClockwise90 => IVec3::new(5, 0, -5),
535        };
536        let (bx, bz) = (ctx.chunk_min_x() + 7, ctx.chunk_min_z() + 7);
537        // End uses `base_height_full`: `preliminary_surface_level = 0` makes the
538        // capped variant miss islands at Y≥50.
539        let h0 = ctx.base_height_full(bx, bz, false) - 1;
540        let h1 = ctx.base_height_full(bx, bz + off_xz.z, false) - 1;
541        let h2 = ctx.base_height_full(bx + off_xz.x, bz, false) - 1;
542        let h3 = ctx.base_height_full(bx + off_xz.x, bz + off_xz.z, false) - 1;
543        let lowest = h0.min(h1).min(h2).min(h3);
544        if lowest < 60 {
545            return None;
546        }
547
548        let biome = ctx.biome_at(bx, lowest, bz);
549        if !structure.allowed_biomes.contains(&biome.key) {
550            return None;
551        }
552
553        let origin = IVec3::new(bx, lowest, bz);
554        Some(GenerationStub {
555            position: (origin.x, origin.y, origin.z),
556            pieces: start_house_tower(ctx.templates(), origin, rotation, rng)
557                .into_iter()
558                .map(|p| make_end_city_structure_piece(ctx.templates(), p))
559                .collect(),
560        })
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    fn single_template(name: &str, size: IVec3) -> Templates {
569        let mut templates = FxHashMap::default();
570        templates.insert(
571            Identifier::new("minecraft", format!("end_city/{name}")),
572            TemplateData {
573                size: size.into(),
574                jigsaws: Vec::new(),
575            },
576        );
577        templates
578    }
579
580    #[test]
581    fn end_city_piece_uses_template_payload_and_overwrite_processor() {
582        let templates = single_template("third_roof", IVec3::new(6, 7, 8));
583        let runtime_piece = make_end_city_structure_piece(
584            &templates,
585            EndCityPiece {
586                template_name: "third_roof".to_owned(),
587                template_position: IVec3::new(10, 70, 20),
588                rotation: Rotation::Clockwise90,
589                gen_depth: 4,
590                overwrite: true,
591            },
592        );
593
594        assert_eq!(runtime_piece.piece_type, Identifier::vanilla_static("ecp"));
595        assert_eq!(runtime_piece.gen_depth, 4);
596        let StructurePiecePayload::Template(data) = runtime_piece.payload else {
597            panic!("end city should use template payload");
598        };
599        assert_eq!(
600            data.template_id,
601            Identifier::vanilla_static("end_city/third_roof")
602        );
603        assert_eq!(data.template_position, (10, 70, 20).into());
604        assert_eq!(data.rotation, Rotation::Clockwise90);
605        assert_eq!(data.block_ignore, StructureBlockIgnore::StructureBlock);
606        assert_eq!(data.processors, TemplateProcessorList::Empty);
607        assert_eq!(data.marker_handling, TemplateMarkerHandling::EndCity);
608        assert_eq!(data.placement_clip, TemplatePlacementClip::CenterChunk);
609
610        let runtime_piece = make_end_city_structure_piece(
611            &templates,
612            EndCityPiece {
613                template_name: "third_roof".to_owned(),
614                template_position: IVec3::new(10, 70, 20),
615                rotation: Rotation::Clockwise90,
616                gen_depth: 4,
617                overwrite: false,
618            },
619        );
620        let StructurePiecePayload::Template(data) = runtime_piece.payload else {
621            panic!("end city should use template payload");
622        };
623        assert_eq!(data.block_ignore, StructureBlockIgnore::StructureAndAir);
624    }
625}