Skip to main content

steel_worldgen/structure/
ocean_monument.rs

1//! Ocean monument structure start generation and runtime placement state.
2//!
3//! Special biome check: every biome in a 29-block (3D) radius around
4//! `(chunkMinX+9, seaLevel, chunkMinZ+9)` must be in `#required_ocean_monument_surrounding`.
5
6use glam::IVec3;
7use steel_registry::structure::StructureData;
8use steel_utils::random::Random;
9use steel_utils::random::legacy_random::LegacyRandom;
10use steel_utils::{BlockPos, BoundingBox, Direction, Identifier};
11
12use crate::structure::{
13    GenerationStub, ProceduralPieceData, Structure, StructureGenerationContext, StructurePiece,
14    StructurePiecePayload, make_oriented_piece_bounding_box, random_horizontal_direction,
15};
16
17const WIDTH: i32 = 58;
18const HEIGHT: i32 = 23;
19const DEPTH: i32 = 58;
20const MIN_Y: i32 = 39;
21const BIOME_RANGE_CHECK: i32 = 29;
22const GRID_WIDTH: i32 = 5;
23const GRID_DEPTH: i32 = 5;
24const GRID_FLOOR_COUNT: i32 = GRID_WIDTH * GRID_DEPTH;
25const GRID_SIZE: usize = 75;
26const GRIDROOM_SOURCE_INDEX: i32 = get_room_index(2, 0, 0);
27const GRIDROOM_TOP_CONNECT_INDEX: i32 = get_room_index(2, 2, 0);
28const GRIDROOM_LEFTWING_CONNECT_INDEX: i32 = get_room_index(0, 1, 0);
29const GRIDROOM_RIGHTWING_CONNECT_INDEX: i32 = get_room_index(4, 1, 0);
30const LEFTWING_INDEX: i32 = 1001;
31const RIGHTWING_INDEX: i32 = 1002;
32const PENTHOUSE_INDEX: i32 = 1003;
33
34/// `#minecraft:required_ocean_monument_surrounding`.
35const SURROUNDING_BIOMES: &[&str] = &[
36    "deep_frozen_ocean",
37    "deep_cold_ocean",
38    "deep_ocean",
39    "deep_lukewarm_ocean",
40    "frozen_ocean",
41    "cold_ocean",
42    "ocean",
43    "lukewarm_ocean",
44    "warm_ocean",
45    "river",
46    "frozen_river",
47];
48
49/// Runtime state for vanilla `OceanMonumentPieces.MonumentBuilding`.
50#[derive(Debug, Clone, PartialEq)]
51pub struct OceanMonumentPieceData {
52    /// Internal child pieces generated and placed by `MonumentBuilding`.
53    pub child_pieces: Vec<OceanMonumentChildPiece>,
54}
55
56/// One internal ocean-monument child piece.
57#[derive(Debug, Clone, PartialEq)]
58pub struct OceanMonumentChildPiece {
59    /// World-space child bounding box after vanilla's building-relative offset.
60    pub bounding_box: BoundingBox,
61    /// Child piece variant and variant-specific persisted state.
62    pub kind: OceanMonumentChildPieceKind,
63}
64
65/// Variant-specific data for monument child pieces.
66#[derive(Debug, Clone, PartialEq)]
67pub enum OceanMonumentChildPieceKind {
68    /// `OceanMonumentEntryRoom`.
69    EntryRoom {
70        /// Source room snapshot.
71        room: OceanMonumentRoomData,
72    },
73    /// `OceanMonumentCoreRoom`.
74    CoreRoom,
75    /// `OceanMonumentDoubleXRoom`.
76    DoubleXRoom {
77        /// Western room snapshot.
78        west: OceanMonumentRoomData,
79        /// Eastern room snapshot.
80        east: OceanMonumentRoomData,
81    },
82    /// `OceanMonumentDoubleXYRoom`.
83    DoubleXYRoom {
84        /// Lower western room.
85        west: OceanMonumentRoomData,
86        /// Lower eastern room.
87        east: OceanMonumentRoomData,
88        /// Upper western room.
89        west_up: OceanMonumentRoomData,
90        /// Upper eastern room.
91        east_up: OceanMonumentRoomData,
92    },
93    /// `OceanMonumentDoubleYRoom`.
94    DoubleYRoom {
95        /// Lower room.
96        room: OceanMonumentRoomData,
97        /// Upper room.
98        above: OceanMonumentRoomData,
99    },
100    /// `OceanMonumentDoubleYZRoom`.
101    DoubleYZRoom {
102        /// Southern lower room.
103        south: OceanMonumentRoomData,
104        /// Northern lower room.
105        north: OceanMonumentRoomData,
106        /// Southern upper room.
107        south_up: OceanMonumentRoomData,
108        /// Northern upper room.
109        north_up: OceanMonumentRoomData,
110    },
111    /// `OceanMonumentDoubleZRoom`.
112    DoubleZRoom {
113        /// Southern room.
114        south: OceanMonumentRoomData,
115        /// Northern room.
116        north: OceanMonumentRoomData,
117    },
118    /// `OceanMonumentSimpleRoom`.
119    SimpleRoom {
120        /// Room snapshot.
121        room: OceanMonumentRoomData,
122        /// Vanilla `mainDesign`.
123        main_design: i32,
124    },
125    /// `OceanMonumentSimpleTopRoom`.
126    SimpleTopRoom {
127        /// Room snapshot.
128        room: OceanMonumentRoomData,
129    },
130    /// `OceanMonumentWingRoom`.
131    WingRoom {
132        /// Vanilla `mainDesign`.
133        main_design: i32,
134    },
135    /// `OceanMonumentPenthouse`.
136    Penthouse,
137}
138
139/// Persisted room state consumed by child placement.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct OceanMonumentRoomData {
142    /// Vanilla room index.
143    pub index: i32,
144    /// Vanilla `hasOpening`, ordered by `Direction.get3DDataValue`.
145    pub has_opening: [bool; 6],
146    /// Whether `connections[UP] != null`; distinct from an open upward passage.
147    pub has_up_connection: bool,
148}
149
150impl OceanMonumentRoomData {
151    /// Counts open sides exactly like vanilla `RoomDefinition.countOpenings`.
152    #[must_use]
153    pub fn count_openings(self) -> i32 {
154        self.has_opening.iter().filter(|&&open| open).count() as i32
155    }
156}
157
158/// Registered under `"minecraft:ocean_monument"`.
159pub struct OceanMonumentStructure;
160
161impl Structure for OceanMonumentStructure {
162    fn find_generation_point(
163        &self,
164        ctx: &mut dyn StructureGenerationContext,
165        structure: &StructureData,
166        rng: &mut LegacyRandom,
167    ) -> Option<GenerationStub> {
168        let check_x = ctx.chunk_min_x() + 9;
169        let check_z = ctx.chunk_min_z() + 9;
170        let check_y = ctx.sea_level();
171
172        let x_range = ((check_x - BIOME_RANGE_CHECK) >> 2)..=((check_x + BIOME_RANGE_CHECK) >> 2);
173        let z_range = ((check_z - BIOME_RANGE_CHECK) >> 2)..=((check_z + BIOME_RANGE_CHECK) >> 2);
174        let y_range = ((check_y - BIOME_RANGE_CHECK) >> 2)..=((check_y + BIOME_RANGE_CHECK) >> 2);
175
176        for qz in z_range {
177            for qx in x_range.clone() {
178                for qy in y_range.clone() {
179                    let biome = ctx.biome_at(qx << 2, qy << 2, qz << 2);
180                    if !SURROUNDING_BIOMES
181                        .iter()
182                        .any(|&b| biome.key == Identifier::vanilla_static(b))
183                    {
184                        return None;
185                    }
186                }
187            }
188        }
189
190        let surface_y = ctx.surface_y();
191        let biome = ctx.biome_at(ctx.center_block_x(), surface_y, ctx.center_block_z());
192        if !structure.allowed_biomes.contains(&biome.key) {
193            return None;
194        }
195
196        let west = ctx.chunk_min_x() - 29;
197        let north = ctx.chunk_min_z() - 29;
198        let orientation = random_horizontal_direction(rng);
199        let piece = monument_building_piece(west, north, orientation, rng);
200        Some(GenerationStub {
201            position: (ctx.center_block_x(), surface_y, ctx.center_block_z()),
202            pieces: vec![piece],
203        })
204    }
205}
206
207fn monument_building_piece(
208    west: i32,
209    north: i32,
210    orientation: Direction,
211    rng: &mut LegacyRandom,
212) -> StructurePiece {
213    let bounding_box =
214        make_oriented_piece_bounding_box(west, MIN_Y, north, orientation, WIDTH, HEIGHT, DEPTH);
215    StructurePiece {
216        piece_type: Identifier::new_static("minecraft", "omb"),
217        bounding_box,
218        gen_depth: 0,
219        orientation: Some(orientation),
220        payload: StructurePiecePayload::Procedural(ProceduralPieceData::OceanMonument(
221            OceanMonumentPieceData {
222                child_pieces: generate_child_pieces(bounding_box, orientation, rng),
223            },
224        )),
225        ground_level_delta: 0,
226        junctions: Vec::new(),
227        projection: None,
228    }
229}
230
231fn generate_child_pieces(
232    building_box: BoundingBox,
233    orientation: Direction,
234    rng: &mut LegacyRandom,
235) -> Vec<OceanMonumentChildPiece> {
236    let mut graph = generate_room_graph(rng);
237    let mut child_pieces = Vec::new();
238
239    graph.rooms[graph.source_room].claimed = true;
240    push_room_child(
241        &mut child_pieces,
242        OceanMonumentChildPieceKind::EntryRoom {
243            room: graph.snapshot(graph.source_room),
244        },
245        orientation,
246        graph.rooms[graph.source_room].index,
247        (1, 1, 1),
248    );
249    push_room_child(
250        &mut child_pieces,
251        OceanMonumentChildPieceKind::CoreRoom,
252        orientation,
253        graph.rooms[graph.core_room].index,
254        (2, 2, 2),
255    );
256
257    for order_index in 0..graph.room_order.len() {
258        let room_index = graph.room_order[order_index];
259        if graph.rooms[room_index].claimed || graph.rooms[room_index].is_special() {
260            continue;
261        }
262
263        if fit_double_xy_room(&mut graph, &mut child_pieces, orientation, room_index) {
264            continue;
265        }
266        if fit_double_yz_room(&mut graph, &mut child_pieces, orientation, room_index) {
267            continue;
268        }
269        if fit_double_z_room(&mut graph, &mut child_pieces, orientation, room_index) {
270            continue;
271        }
272        if fit_double_x_room(&mut graph, &mut child_pieces, orientation, room_index) {
273            continue;
274        }
275        if fit_double_y_room(&mut graph, &mut child_pieces, orientation, room_index) {
276            continue;
277        }
278        if fit_simple_top_room(&mut graph, &mut child_pieces, orientation, room_index) {
279            continue;
280        }
281        fit_simple_room(&mut graph, &mut child_pieces, orientation, room_index, rng);
282    }
283
284    let offset = world_pos(building_box, Some(orientation), 9, 0, 22);
285    for child in &mut child_pieces {
286        child.bounding_box = child.bounding_box.translate(offset.0);
287    }
288
289    let left_wing = BoundingBox::from_corners(
290        world_pos(building_box, Some(orientation), 1, 1, 1),
291        world_pos(building_box, Some(orientation), 23, 8, 21),
292    );
293    let right_wing = BoundingBox::from_corners(
294        world_pos(building_box, Some(orientation), 34, 1, 1),
295        world_pos(building_box, Some(orientation), 56, 8, 21),
296    );
297    let penthouse = BoundingBox::from_corners(
298        world_pos(building_box, Some(orientation), 22, 13, 22),
299        world_pos(building_box, Some(orientation), 35, 17, 35),
300    );
301    let wing_random = rng.next_i32();
302    child_pieces.push(OceanMonumentChildPiece {
303        bounding_box: left_wing,
304        kind: OceanMonumentChildPieceKind::WingRoom {
305            main_design: wing_random & 1,
306        },
307    });
308    child_pieces.push(OceanMonumentChildPiece {
309        bounding_box: right_wing,
310        kind: OceanMonumentChildPieceKind::WingRoom {
311            main_design: wing_random.wrapping_add(1) & 1,
312        },
313    });
314    child_pieces.push(OceanMonumentChildPiece {
315        bounding_box: penthouse,
316        kind: OceanMonumentChildPieceKind::Penthouse,
317    });
318
319    child_pieces
320}
321
322struct RoomGraph {
323    rooms: Vec<RoomDefinition>,
324    room_order: Vec<usize>,
325    source_room: usize,
326    core_room: usize,
327}
328
329impl RoomGraph {
330    fn snapshot(&self, room: usize) -> OceanMonumentRoomData {
331        let definition = &self.rooms[room];
332        OceanMonumentRoomData {
333            index: definition.index,
334            has_opening: definition.has_opening,
335            has_up_connection: definition.connections[direction_index(Direction::Up)].is_some(),
336        }
337    }
338
339    fn connection(&self, room: usize, direction: Direction) -> usize {
340        let Some(connection) = self.rooms[room].connections[direction_index(direction)] else {
341            panic!(
342                "ocean monument room {} missing {:?} connection",
343                self.rooms[room].index, direction
344            );
345        };
346        connection
347    }
348}
349
350#[derive(Debug, Clone)]
351struct RoomDefinition {
352    index: i32,
353    connections: [Option<usize>; 6],
354    has_opening: [bool; 6],
355    claimed: bool,
356    is_source: bool,
357    scan_index: i32,
358}
359
360impl RoomDefinition {
361    const fn new(index: i32) -> Self {
362        Self {
363            index,
364            connections: [None; 6],
365            has_opening: [false; 6],
366            claimed: false,
367            is_source: false,
368            scan_index: 0,
369        }
370    }
371
372    const fn is_special(&self) -> bool {
373        self.index >= GRID_SIZE as i32
374    }
375
376    fn update_openings(&mut self) {
377        for i in 0..6 {
378            self.has_opening[i] = self.connections[i].is_some();
379        }
380    }
381}
382
383#[expect(
384    clippy::too_many_lines,
385    reason = "direct port of vanilla MonumentBuilding.generateRoomGraph"
386)]
387fn generate_room_graph(rng: &mut LegacyRandom) -> RoomGraph {
388    let mut rooms = Vec::new();
389    let mut room_grid = [None; GRID_SIZE];
390
391    for x in 0..5 {
392        for z in 0..4 {
393            let pos = get_room_index(x, 0, z);
394            room_grid[pos as usize] = Some(add_room(&mut rooms, pos));
395        }
396    }
397
398    for x in 0..5 {
399        for z in 0..4 {
400            let pos = get_room_index(x, 1, z);
401            room_grid[pos as usize] = Some(add_room(&mut rooms, pos));
402        }
403    }
404
405    for x in 1..4 {
406        for z in 0..2 {
407            let pos = get_room_index(x, 2, z);
408            room_grid[pos as usize] = Some(add_room(&mut rooms, pos));
409        }
410    }
411
412    let source_room = room_at(&room_grid, GRIDROOM_SOURCE_INDEX);
413
414    for x in 0..5 {
415        for z in 0..5 {
416            for y in 0..3 {
417                let pos = get_room_index(x, y, z);
418                let Some(room_index) = room_grid[pos as usize] else {
419                    continue;
420                };
421
422                for direction in VANILLA_DIRECTIONS {
423                    let (step_x, step_y, step_z) = direction.offset();
424                    let neigh_x = x + step_x;
425                    let neigh_y = y + step_y;
426                    let neigh_z = z + step_z;
427                    if !(0..5).contains(&neigh_x)
428                        || !(0..5).contains(&neigh_z)
429                        || !(0..3).contains(&neigh_y)
430                    {
431                        continue;
432                    }
433
434                    let neigh_pos = get_room_index(neigh_x, neigh_y, neigh_z);
435                    let Some(neighbor) = room_grid[neigh_pos as usize] else {
436                        continue;
437                    };
438                    let connection_direction = if neigh_z == z {
439                        direction
440                    } else {
441                        direction.opposite()
442                    };
443                    set_connection(&mut rooms, room_index, connection_direction, neighbor);
444                }
445            }
446        }
447    }
448
449    let roof_room = add_room(&mut rooms, PENTHOUSE_INDEX);
450    let left_wing = add_room(&mut rooms, LEFTWING_INDEX);
451    let right_wing = add_room(&mut rooms, RIGHTWING_INDEX);
452    set_connection(
453        &mut rooms,
454        room_at(&room_grid, GRIDROOM_TOP_CONNECT_INDEX),
455        Direction::Up,
456        roof_room,
457    );
458    set_connection(
459        &mut rooms,
460        room_at(&room_grid, GRIDROOM_LEFTWING_CONNECT_INDEX),
461        Direction::South,
462        left_wing,
463    );
464    set_connection(
465        &mut rooms,
466        room_at(&room_grid, GRIDROOM_RIGHTWING_CONNECT_INDEX),
467        Direction::South,
468        right_wing,
469    );
470    rooms[roof_room].claimed = true;
471    rooms[left_wing].claimed = true;
472    rooms[right_wing].claimed = true;
473    rooms[source_room].is_source = true;
474
475    let core_room = room_at(&room_grid, get_room_index(rng.next_i32_bounded(4), 0, 2));
476    rooms[core_room].claimed = true;
477    let core_east = connected_room(&rooms, core_room, Direction::East);
478    let core_north = connected_room(&rooms, core_room, Direction::North);
479    let core_east_north = connected_room(&rooms, core_east, Direction::North);
480    let core_up = connected_room(&rooms, core_room, Direction::Up);
481    let core_east_up = connected_room(&rooms, core_east, Direction::Up);
482    let core_north_up = connected_room(&rooms, core_north, Direction::Up);
483    let core_east_north_up = connected_room(&rooms, core_east_north, Direction::Up);
484    for room in [
485        core_east,
486        core_north,
487        core_east_north,
488        core_up,
489        core_east_up,
490        core_north_up,
491        core_east_north_up,
492    ] {
493        rooms[room].claimed = true;
494    }
495
496    let mut room_order = Vec::new();
497    for room in room_grid.into_iter().flatten() {
498        rooms[room].update_openings();
499        room_order.push(room);
500    }
501    rooms[roof_room].update_openings();
502    vanilla_shuffle(&mut room_order, rng);
503
504    let mut scan_index = 1;
505    for room in room_order.iter().copied() {
506        let mut close_count = 0;
507        let mut attempt_count = 0;
508
509        while close_count < 2 && attempt_count < 5 {
510            attempt_count += 1;
511            let direction = rng.next_i32_bounded(6) as usize;
512            if !rooms[room].has_opening[direction] {
513                continue;
514            }
515
516            let Some(connection) = rooms[room].connections[direction] else {
517                continue;
518            };
519            let opposite = direction_index(direction_from_index(direction).opposite());
520            rooms[room].has_opening[direction] = false;
521            rooms[connection].has_opening[opposite] = false;
522
523            let room_scan = scan_index;
524            scan_index += 1;
525            let connection_scan = scan_index;
526            scan_index += 1;
527            if find_source(&mut rooms, room, room_scan)
528                && find_source(&mut rooms, connection, connection_scan)
529            {
530                close_count += 1;
531            } else {
532                rooms[room].has_opening[direction] = true;
533                rooms[connection].has_opening[opposite] = true;
534            }
535        }
536    }
537
538    room_order.push(roof_room);
539    room_order.push(left_wing);
540    room_order.push(right_wing);
541
542    RoomGraph {
543        rooms,
544        room_order,
545        source_room,
546        core_room,
547    }
548}
549
550fn add_room(rooms: &mut Vec<RoomDefinition>, index: i32) -> usize {
551    let room = rooms.len();
552    rooms.push(RoomDefinition::new(index));
553    room
554}
555
556fn room_at(room_grid: &[Option<usize>; GRID_SIZE], room_index: i32) -> usize {
557    let Some(room) = room_grid[room_index as usize] else {
558        panic!("ocean monument missing generated room {room_index}");
559    };
560    room
561}
562
563fn connected_room(rooms: &[RoomDefinition], room: usize, direction: Direction) -> usize {
564    let Some(connection) = rooms[room].connections[direction_index(direction)] else {
565        panic!(
566            "ocean monument room {} missing {:?} connection",
567            rooms[room].index, direction
568        );
569    };
570    connection
571}
572
573const fn set_connection(
574    rooms: &mut [RoomDefinition],
575    room: usize,
576    direction: Direction,
577    connection: usize,
578) {
579    rooms[room].connections[direction_index(direction)] = Some(connection);
580    rooms[connection].connections[direction_index(direction.opposite())] = Some(room);
581}
582
583fn find_source(rooms: &mut [RoomDefinition], start: usize, scan_index: i32) -> bool {
584    let mut stack = vec![start];
585    while let Some(room) = stack.pop() {
586        if rooms[room].is_source {
587            return true;
588        }
589        if rooms[room].scan_index == scan_index {
590            continue;
591        }
592        rooms[room].scan_index = scan_index;
593
594        for direction in 0..6 {
595            let Some(connection) = rooms[room].connections[direction] else {
596                continue;
597            };
598            if rooms[room].has_opening[direction] && rooms[connection].scan_index != scan_index {
599                stack.push(connection);
600            }
601        }
602    }
603    false
604}
605
606fn fit_double_xy_room(
607    graph: &mut RoomGraph,
608    child_pieces: &mut Vec<OceanMonumentChildPiece>,
609    orientation: Direction,
610    room: usize,
611) -> bool {
612    if !graph.rooms[room].has_opening[direction_index(Direction::East)]
613        || !graph.rooms[room].has_opening[direction_index(Direction::Up)]
614    {
615        return false;
616    }
617
618    let east = graph.connection(room, Direction::East);
619    let up = graph.connection(room, Direction::Up);
620    if graph.rooms[east].claimed || graph.rooms[up].claimed {
621        return false;
622    }
623
624    if !graph.rooms[east].has_opening[direction_index(Direction::Up)] {
625        return false;
626    }
627    let east_up = graph.connection(east, Direction::Up);
628    if graph.rooms[east_up].claimed {
629        return false;
630    }
631
632    for claimed in [room, east, up, east_up] {
633        graph.rooms[claimed].claimed = true;
634    }
635    push_room_child(
636        child_pieces,
637        OceanMonumentChildPieceKind::DoubleXYRoom {
638            west: graph.snapshot(room),
639            east: graph.snapshot(east),
640            west_up: graph.snapshot(up),
641            east_up: graph.snapshot(east_up),
642        },
643        orientation,
644        graph.rooms[room].index,
645        (2, 2, 1),
646    );
647    true
648}
649
650fn fit_double_yz_room(
651    graph: &mut RoomGraph,
652    child_pieces: &mut Vec<OceanMonumentChildPiece>,
653    orientation: Direction,
654    room: usize,
655) -> bool {
656    if !graph.rooms[room].has_opening[direction_index(Direction::North)]
657        || !graph.rooms[room].has_opening[direction_index(Direction::Up)]
658    {
659        return false;
660    }
661
662    let north = graph.connection(room, Direction::North);
663    let up = graph.connection(room, Direction::Up);
664    if graph.rooms[north].claimed || graph.rooms[up].claimed {
665        return false;
666    }
667
668    if !graph.rooms[north].has_opening[direction_index(Direction::Up)] {
669        return false;
670    }
671    let north_up = graph.connection(north, Direction::Up);
672    if graph.rooms[north_up].claimed {
673        return false;
674    }
675
676    for claimed in [room, north, up, north_up] {
677        graph.rooms[claimed].claimed = true;
678    }
679    push_room_child(
680        child_pieces,
681        OceanMonumentChildPieceKind::DoubleYZRoom {
682            south: graph.snapshot(room),
683            north: graph.snapshot(north),
684            south_up: graph.snapshot(up),
685            north_up: graph.snapshot(north_up),
686        },
687        orientation,
688        graph.rooms[room].index,
689        (1, 2, 2),
690    );
691    true
692}
693
694fn fit_double_z_room(
695    graph: &mut RoomGraph,
696    child_pieces: &mut Vec<OceanMonumentChildPiece>,
697    orientation: Direction,
698    room: usize,
699) -> bool {
700    if !graph.rooms[room].has_opening[direction_index(Direction::North)]
701        || graph.rooms[graph.connection(room, Direction::North)].claimed
702    {
703        return false;
704    }
705
706    let north = graph.connection(room, Direction::North);
707    graph.rooms[room].claimed = true;
708    graph.rooms[north].claimed = true;
709    push_room_child(
710        child_pieces,
711        OceanMonumentChildPieceKind::DoubleZRoom {
712            south: graph.snapshot(room),
713            north: graph.snapshot(north),
714        },
715        orientation,
716        graph.rooms[room].index,
717        (1, 1, 2),
718    );
719    true
720}
721
722fn fit_double_x_room(
723    graph: &mut RoomGraph,
724    child_pieces: &mut Vec<OceanMonumentChildPiece>,
725    orientation: Direction,
726    room: usize,
727) -> bool {
728    if !graph.rooms[room].has_opening[direction_index(Direction::East)] {
729        return false;
730    }
731
732    let east = graph.connection(room, Direction::East);
733    if graph.rooms[east].claimed {
734        return false;
735    }
736
737    graph.rooms[room].claimed = true;
738    graph.rooms[east].claimed = true;
739    push_room_child(
740        child_pieces,
741        OceanMonumentChildPieceKind::DoubleXRoom {
742            west: graph.snapshot(room),
743            east: graph.snapshot(east),
744        },
745        orientation,
746        graph.rooms[room].index,
747        (2, 1, 1),
748    );
749    true
750}
751
752fn fit_double_y_room(
753    graph: &mut RoomGraph,
754    child_pieces: &mut Vec<OceanMonumentChildPiece>,
755    orientation: Direction,
756    room: usize,
757) -> bool {
758    if !graph.rooms[room].has_opening[direction_index(Direction::Up)] {
759        return false;
760    }
761
762    let above = graph.connection(room, Direction::Up);
763    if graph.rooms[above].claimed {
764        return false;
765    }
766
767    graph.rooms[room].claimed = true;
768    graph.rooms[above].claimed = true;
769    push_room_child(
770        child_pieces,
771        OceanMonumentChildPieceKind::DoubleYRoom {
772            room: graph.snapshot(room),
773            above: graph.snapshot(above),
774        },
775        orientation,
776        graph.rooms[room].index,
777        (1, 2, 1),
778    );
779    true
780}
781
782fn fit_simple_top_room(
783    graph: &mut RoomGraph,
784    child_pieces: &mut Vec<OceanMonumentChildPiece>,
785    orientation: Direction,
786    room: usize,
787) -> bool {
788    let definition = &graph.rooms[room];
789    if definition.has_opening[direction_index(Direction::West)]
790        || definition.has_opening[direction_index(Direction::East)]
791        || definition.has_opening[direction_index(Direction::North)]
792        || definition.has_opening[direction_index(Direction::South)]
793        || definition.has_opening[direction_index(Direction::Up)]
794    {
795        return false;
796    }
797
798    graph.rooms[room].claimed = true;
799    push_room_child(
800        child_pieces,
801        OceanMonumentChildPieceKind::SimpleTopRoom {
802            room: graph.snapshot(room),
803        },
804        orientation,
805        graph.rooms[room].index,
806        (1, 1, 1),
807    );
808    true
809}
810
811fn fit_simple_room(
812    graph: &mut RoomGraph,
813    child_pieces: &mut Vec<OceanMonumentChildPiece>,
814    orientation: Direction,
815    room: usize,
816    rng: &mut LegacyRandom,
817) {
818    graph.rooms[room].claimed = true;
819    push_room_child(
820        child_pieces,
821        OceanMonumentChildPieceKind::SimpleRoom {
822            room: graph.snapshot(room),
823            main_design: rng.next_i32_bounded(3),
824        },
825        orientation,
826        graph.rooms[room].index,
827        (1, 1, 1),
828    );
829}
830
831fn push_room_child(
832    child_pieces: &mut Vec<OceanMonumentChildPiece>,
833    kind: OceanMonumentChildPieceKind,
834    orientation: Direction,
835    room_index: i32,
836    size: (i32, i32, i32),
837) {
838    child_pieces.push(OceanMonumentChildPiece {
839        bounding_box: make_room_bounding_box(orientation, room_index, size.0, size.1, size.2),
840        kind,
841    });
842}
843
844fn make_room_bounding_box(
845    orientation: Direction,
846    room_index: i32,
847    room_width: i32,
848    room_height: i32,
849    room_depth: i32,
850) -> BoundingBox {
851    let room_x = room_index % GRID_WIDTH;
852    let room_z = room_index / GRID_WIDTH % GRID_DEPTH;
853    let room_y = room_index / GRID_FLOOR_COUNT;
854    let bounding_box = make_oriented_piece_bounding_box(
855        0,
856        0,
857        0,
858        orientation,
859        room_width * 8,
860        room_height * 4,
861        room_depth * 8,
862    );
863
864    match orientation {
865        Direction::North => bounding_box.translate(IVec3::new(
866            room_x * 8,
867            room_y * 4,
868            -(room_z + room_depth) * 8 + 1,
869        )),
870        Direction::South => bounding_box.translate(IVec3::new(room_x * 8, room_y * 4, room_z * 8)),
871        Direction::West => bounding_box.translate(IVec3::new(
872            -(room_z + room_depth) * 8 + 1,
873            room_y * 4,
874            room_x * 8,
875        )),
876        Direction::East => bounding_box.translate(IVec3::new(room_z * 8, room_y * 4, room_x * 8)),
877        Direction::Down | Direction::Up => panic!("ocean monument room has vertical orientation"),
878    }
879}
880
881const fn world_pos(
882    bounding_box: BoundingBox,
883    orientation: Option<Direction>,
884    x: i32,
885    y: i32,
886    z: i32,
887) -> BlockPos {
888    let world_y = if orientation.is_some() {
889        y + bounding_box.min_y()
890    } else {
891        y
892    };
893    let (world_x, world_z) = match orientation {
894        None | Some(Direction::Up | Direction::Down) => (x, z),
895        Some(Direction::North) => (bounding_box.min_x() + x, bounding_box.max_z() - z),
896        Some(Direction::South) => (bounding_box.min_x() + x, bounding_box.min_z() + z),
897        Some(Direction::West) => (bounding_box.max_x() - z, bounding_box.min_z() + x),
898        Some(Direction::East) => (bounding_box.min_x() + z, bounding_box.min_z() + x),
899    };
900    BlockPos::new(world_x, world_y, world_z)
901}
902
903const VANILLA_DIRECTIONS: [Direction; 6] = [
904    Direction::Down,
905    Direction::Up,
906    Direction::North,
907    Direction::South,
908    Direction::West,
909    Direction::East,
910];
911
912const fn direction_index(direction: Direction) -> usize {
913    match direction {
914        Direction::Down => 0,
915        Direction::Up => 1,
916        Direction::North => 2,
917        Direction::South => 3,
918        Direction::West => 4,
919        Direction::East => 5,
920    }
921}
922
923const fn direction_from_index(index: usize) -> Direction {
924    match index {
925        0 => Direction::Down,
926        1 => Direction::Up,
927        2 => Direction::North,
928        3 => Direction::South,
929        4 => Direction::West,
930        _ => Direction::East,
931    }
932}
933
934const fn get_room_index(room_x: i32, room_y: i32, room_z: i32) -> i32 {
935    room_y * GRID_FLOOR_COUNT + room_z * GRID_WIDTH + room_x
936}
937
938fn vanilla_shuffle<T>(items: &mut [T], rng: &mut LegacyRandom) {
939    for i in (1..items.len()).rev() {
940        let j = rng.next_i32_bounded((i + 1) as i32) as usize;
941        items.swap(i, j);
942    }
943}
944
945#[cfg(test)]
946mod tests {
947    use glam::IVec3;
948
949    use super::*;
950
951    #[test]
952    fn monument_building_uses_full_procedural_payload() {
953        let mut rng = LegacyRandom::from_seed(1234);
954        let piece = monument_building_piece(16, 32, Direction::West, &mut rng);
955
956        assert_eq!(piece.piece_type, Identifier::new_static("minecraft", "omb"));
957        assert_eq!(piece.gen_depth, 0);
958        assert_eq!(piece.orientation, Some(Direction::West));
959        assert_eq!(
960            piece.bounding_box,
961            BoundingBox::new(IVec3::new(16, 39, 32), IVec3::new(73, 61, 89))
962        );
963        let StructurePiecePayload::Procedural(ProceduralPieceData::OceanMonument(data)) =
964            piece.payload
965        else {
966            panic!("ocean monument should use its procedural payload");
967        };
968        assert!(!data.child_pieces.is_empty());
969        assert!(matches!(
970            data.child_pieces[0].kind,
971            OceanMonumentChildPieceKind::EntryRoom { .. }
972        ));
973        assert!(matches!(
974            data.child_pieces[1].kind,
975            OceanMonumentChildPieceKind::CoreRoom
976        ));
977        assert!(matches!(
978            data.child_pieces.last().expect("penthouse child").kind,
979            OceanMonumentChildPieceKind::Penthouse
980        ));
981    }
982
983    #[test]
984    fn generated_child_order_captures_vanilla_fixed_children() {
985        let mut rng = LegacyRandom::from_seed(9876);
986        let piece = monument_building_piece(-29, -29, Direction::South, &mut rng);
987        let StructurePiecePayload::Procedural(ProceduralPieceData::OceanMonument(data)) =
988            piece.payload
989        else {
990            panic!("ocean monument should use its procedural payload");
991        };
992
993        assert!(data.child_pieces.len() >= 5);
994        assert!(matches!(
995            data.child_pieces[data.child_pieces.len() - 3].kind,
996            OceanMonumentChildPieceKind::WingRoom { .. }
997        ));
998        assert!(matches!(
999            data.child_pieces[data.child_pieces.len() - 2].kind,
1000            OceanMonumentChildPieceKind::WingRoom { .. }
1001        ));
1002        assert!(matches!(
1003            data.child_pieces[data.child_pieces.len() - 1].kind,
1004            OceanMonumentChildPieceKind::Penthouse
1005        ));
1006    }
1007}