Merge pull request 'chore/audit-uc02-roomtheme-datadriven' (#14) from chore/audit-uc02-roomtheme-datadriven into develop
Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
@@ -581,6 +581,14 @@ public class TiedUpMod {
|
||||
LOGGER.info(
|
||||
"Registered FurnitureServerReloadListener for data-driven furniture definitions"
|
||||
);
|
||||
|
||||
// Data-driven room theme definitions (server-side, from data/<namespace>/tiedup_room_themes/)
|
||||
event.addListener(
|
||||
new com.tiedup.remake.worldgen.RoomThemeReloadListener()
|
||||
);
|
||||
LOGGER.info(
|
||||
"Registered RoomThemeReloadListener for data-driven room themes"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
src/main/java/com/tiedup/remake/worldgen/BlockPalette.java
Normal file
74
src/main/java/com/tiedup/remake/worldgen/BlockPalette.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Weighted random block selection with named condition variants.
|
||||
*
|
||||
* <p>Each palette maps condition names (e.g. "default", "bottom_row", "edge",
|
||||
* "corner", "cap") to a list of weighted block state entries. When a block
|
||||
* is requested for a condition that has no entries, falls back to "default".</p>
|
||||
*/
|
||||
public final class BlockPalette {
|
||||
|
||||
/**
|
||||
* A single block state with its selection weight.
|
||||
*
|
||||
* @param state the block state to place
|
||||
* @param weight relative weight (higher = more likely)
|
||||
*/
|
||||
public record WeightedEntry(BlockState state, float weight) {}
|
||||
|
||||
private final Map<String, List<WeightedEntry>> variants;
|
||||
|
||||
/**
|
||||
* @param variants condition name to weighted entries map (defensively copied)
|
||||
*/
|
||||
public BlockPalette(Map<String, List<WeightedEntry>> variants) {
|
||||
this.variants = Map.copyOf(variants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random block state for the given condition.
|
||||
* Falls back to "default" if the condition has no entries.
|
||||
*
|
||||
* @param random the random source
|
||||
* @param condition the condition name (e.g. "default", "bottom_row")
|
||||
* @return a randomly selected block state
|
||||
*/
|
||||
public BlockState pick(RandomSource random, String condition) {
|
||||
List<WeightedEntry> entries = variants.getOrDefault(
|
||||
condition, variants.get("default")
|
||||
);
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
// Should never happen if parsed correctly -- safety fallback
|
||||
return Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
|
||||
float totalWeight = 0;
|
||||
for (WeightedEntry e : entries) {
|
||||
totalWeight += e.weight();
|
||||
}
|
||||
|
||||
float roll = random.nextFloat() * totalWeight;
|
||||
float cumulative = 0;
|
||||
for (WeightedEntry e : entries) {
|
||||
cumulative += e.weight();
|
||||
if (roll < cumulative) return e.state();
|
||||
}
|
||||
|
||||
// Floating-point edge case fallback
|
||||
return entries.get(entries.size() - 1).state();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return unmodifiable map of all condition variants
|
||||
*/
|
||||
public Map<String, List<WeightedEntry>> variants() {
|
||||
return variants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Configuration for decorative blocks placed in a room theme.
|
||||
*
|
||||
* <p>Captures corner decorations, wall midpoint blocks, optional special
|
||||
* first-corner placement, furniture cluster, and lighting/chain flags.</p>
|
||||
*
|
||||
* @param cornerDecorations blocks placed at room corners (each with a Y offset)
|
||||
* @param wallMidpointBlocks blocks placed at wall midpoints
|
||||
* @param firstCornerSpecial optional special block for the first corner only
|
||||
* @param furnitureCluster blocks placed as a furniture group
|
||||
* @param useTorchLighting whether to place torches for lighting
|
||||
* @param hasCeilingChain whether to place ceiling chains
|
||||
*/
|
||||
public record DecorationConfig(
|
||||
List<PositionedBlock> cornerDecorations,
|
||||
List<PositionedBlock> wallMidpointBlocks,
|
||||
@Nullable PositionedBlock firstCornerSpecial,
|
||||
List<PositionedBlock> furnitureCluster,
|
||||
boolean useTorchLighting,
|
||||
boolean hasCeilingChain
|
||||
) {
|
||||
|
||||
/**
|
||||
* A block state with positional offsets for placement.
|
||||
*
|
||||
* @param state the block state to place
|
||||
* @param xOffset horizontal X offset from base position (0 = no offset)
|
||||
* @param yOffset vertical offset from the base position
|
||||
* @param zOffset horizontal Z offset from base position (0 = no offset)
|
||||
*/
|
||||
public record PositionedBlock(BlockState state, int xOffset, int yOffset, int zOffset) {
|
||||
/** Convenience constructor for Y-offset-only blocks (most common case). */
|
||||
public PositionedBlock(BlockState state, int yOffset) {
|
||||
this(state, 0, yOffset, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.LanternBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -279,7 +280,7 @@ public class HangingCagePiece extends StructurePiece {
|
||||
|
||||
/**
|
||||
* Fallback: carve a 13x13x12 dungeon room and place the cage inside.
|
||||
* Randomly selects a theme (Oubliette/Inferno) and layout (Square/Octagonal).
|
||||
* Randomly selects a data-driven theme and layout (Square/Octagonal).
|
||||
*/
|
||||
private void carveAndPlaceRoom(
|
||||
WorldGenLevel level,
|
||||
@@ -288,9 +289,7 @@ public class HangingCagePiece extends StructurePiece {
|
||||
int centerZ,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
RoomTheme theme = RoomTheme.values()[random.nextInt(
|
||||
RoomTheme.values().length
|
||||
)];
|
||||
RoomThemeDefinition theme = RoomThemeRegistry.pickRandomOrFallback(random);
|
||||
RoomLayout layout = RoomLayout.values()[random.nextInt(
|
||||
RoomLayout.values().length
|
||||
)];
|
||||
@@ -358,62 +357,24 @@ public class HangingCagePiece extends StructurePiece {
|
||||
}
|
||||
}
|
||||
|
||||
theme.placeDecorations(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
placeThemeDecorations(
|
||||
level, random, baseX, baseZ, floorY, layout, chunkBB, theme
|
||||
);
|
||||
|
||||
RoomTheme.placeSharedPillars(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
placeSharedPillars(
|
||||
level, random, baseX, baseZ, floorY, layout, theme, chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedFloorScatter(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
placeSharedFloorScatter(
|
||||
level, random, baseX, baseZ, floorY, layout, theme, chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedCeilingDecor(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
placeSharedCeilingDecor(
|
||||
level, random, baseX, baseZ, floorY, layout, chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedWallLighting(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
placeSharedWallLighting(
|
||||
level, random, baseX, baseZ, floorY, layout, chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedWallBands(
|
||||
level,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
placeSharedWallBands(
|
||||
level, baseX, baseZ, floorY, layout, theme, chunkBB
|
||||
);
|
||||
|
||||
placeVanillaChest(level, random, baseX, baseZ, floorY, layout, chunkBB);
|
||||
@@ -451,7 +412,7 @@ public class HangingCagePiece extends StructurePiece {
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[HangingCage] Placed cage in carved room (theme={}, layout={}) at {}",
|
||||
theme.name(),
|
||||
theme.id(),
|
||||
layout.name(),
|
||||
masterPos.toShortString()
|
||||
);
|
||||
@@ -655,4 +616,331 @@ public class HangingCagePiece extends StructurePiece {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Data-driven theme decorations ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Place decorations from the data-driven {@link DecorationConfig}.
|
||||
* Replaces the old per-enum {@code RoomTheme.placeDecorations()} method.
|
||||
*/
|
||||
private static void placeThemeDecorations(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
BoundingBox chunkBB,
|
||||
RoomThemeDefinition theme
|
||||
) {
|
||||
DecorationConfig deco = theme.decorations();
|
||||
int[][] corners = layout.innerCorners();
|
||||
|
||||
// Corner decorations (e.g. cobwebs low+high, soul fire, snow layers)
|
||||
// Note: only y_offset is used here — x/z offsets are ignored because corners
|
||||
// are placed at all 4 inner corners and direction logic would differ per corner.
|
||||
for (DecorationConfig.PositionedBlock pb : deco.cornerDecorations()) {
|
||||
for (int[] c : corners) {
|
||||
if (layout.isInShape(c[0], c[1]) && !layout.isWall(c[0], c[1])) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + c[0], floorY + pb.yOffset(), baseZ + c[1]),
|
||||
pb.state(), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wall midpoint decorations (e.g. soul lanterns, crying obsidian, sculk veins)
|
||||
int[][] wallMidpoints = {{6, 1}, {6, 11}, {1, 6}, {11, 6}};
|
||||
for (DecorationConfig.PositionedBlock pb : deco.wallMidpointBlocks()) {
|
||||
for (int[] wp : wallMidpoints) {
|
||||
if (layout.isWall(wp[0], wp[1])) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + wp[0], floorY + pb.yOffset(), baseZ + wp[1]),
|
||||
pb.state(), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First corner special (e.g. water cauldron, skull, TNT, sculk catalyst)
|
||||
// x/z offsets are multiplied by inward direction (toward room center)
|
||||
if (deco.firstCornerSpecial() != null && corners.length > 0) {
|
||||
int[] sc = corners[0];
|
||||
int dirX = sc[0] < 6 ? 1 : -1;
|
||||
int dirZ = sc[1] < 6 ? 1 : -1;
|
||||
int scx = sc[0] + deco.firstCornerSpecial().xOffset() * dirX;
|
||||
int scz = sc[1] + deco.firstCornerSpecial().zOffset() * dirZ;
|
||||
if (layout.isInShape(scx, scz) && !layout.isWall(scx, scz)) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + scx, floorY + deco.firstCornerSpecial().yOffset(), baseZ + scz),
|
||||
deco.firstCornerSpecial().state(), chunkBB);
|
||||
}
|
||||
}
|
||||
|
||||
// Furniture cluster at corners[1] — each item has x/z offsets relative to the
|
||||
// base furniture position. x_offset/z_offset values are multiplied by the
|
||||
// inward direction (toward room center) to handle all 4 corner orientations.
|
||||
if (corners.length > 1) {
|
||||
int[] fc = corners[1];
|
||||
int dirX = fc[0] < 6 ? 1 : -1;
|
||||
int dirZ = fc[1] < 6 ? 1 : -1;
|
||||
int fcx = fc[0] + dirX;
|
||||
int fcz = fc[1] + dirZ;
|
||||
for (DecorationConfig.PositionedBlock pb : deco.furnitureCluster()) {
|
||||
int px = fcx + pb.xOffset() * dirX;
|
||||
int pz = fcz + pb.zOffset() * dirZ;
|
||||
if (layout.isInShape(px, pz) && !layout.isWall(px, pz)) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + px, floorY + pb.yOffset(), baseZ + pz),
|
||||
pb.state(), chunkBB);
|
||||
}
|
||||
}
|
||||
|
||||
// Ceiling chain above furniture
|
||||
if (deco.hasCeilingChain()) {
|
||||
for (int cy = 8; cy <= 10; cy++) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + fcx, floorY + cy, baseZ + fcz),
|
||||
Blocks.CHAIN.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Torch lighting (wall torches at outer wall midpoints) -- used by Crypt, Sandstone
|
||||
if (deco.useTorchLighting()) {
|
||||
for (int[] wallPos : new int[][] {{6, 0}, {6, 12}, {0, 6}, {12, 6}}) {
|
||||
if (layout.isWall(wallPos[0], wallPos[1])) {
|
||||
Direction torchDir = getTorchDirection(wallPos[0], wallPos[1]);
|
||||
if (torchDir != null) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + wallPos[0], floorY + 3, baseZ + wallPos[1]),
|
||||
Blocks.WALL_TORCH.defaultBlockState().setValue(
|
||||
net.minecraft.world.level.block.WallTorchBlock.FACING, torchDir),
|
||||
chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Side chains + hanging lanterns (shared by all themes)
|
||||
placeSharedChains(level, baseX, baseZ, floorY, chunkBB);
|
||||
placeSharedHangingLanterns(level, baseX, baseZ, floorY, chunkBB);
|
||||
}
|
||||
|
||||
// ── Shared structural features (moved from RoomTheme) ──────────
|
||||
|
||||
/** Pillar positions -- verified to be inside all 4 layouts. */
|
||||
private static final int[][] PILLAR_POSITIONS = {
|
||||
{4, 4}, {4, 8}, {8, 4}, {8, 8},
|
||||
};
|
||||
|
||||
/** Place 4 full-height pillars at verified positions. */
|
||||
private static void placeSharedPillars(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
RoomThemeDefinition theme,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
for (int[] p : PILLAR_POSITIONS) {
|
||||
if (!layout.isInShape(p[0], p[1]) || layout.isWall(p[0], p[1])) continue;
|
||||
for (int ry = 1; ry <= 10; ry++) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + p[0], floorY + ry, baseZ + p[1]),
|
||||
theme.pillarBlock(random, ry), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Place random floor scatter (~12% of interior positions). */
|
||||
private static void placeSharedFloorScatter(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
RoomThemeDefinition theme,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
for (int rx = 1; rx <= 11; rx++) {
|
||||
for (int rz = 1; rz <= 11; rz++) {
|
||||
if (!layout.isInShape(rx, rz) || layout.isWall(rx, rz)) continue;
|
||||
// Skip pillar positions
|
||||
if ((rx == 4 || rx == 8) && (rz == 4 || rz == 8)) continue;
|
||||
// Skip cage center area (5-7, 5-7)
|
||||
if (rx >= 5 && rx <= 7 && rz >= 5 && rz <= 7) continue;
|
||||
// Skip corner positions (used by decorations/chests)
|
||||
if ((rx <= 2 || rx >= 10) && (rz <= 2 || rz >= 10)) continue;
|
||||
if (random.nextFloat() < 0.12f) {
|
||||
BlockState scatter = theme.scatterBlock(random);
|
||||
if (scatter != null) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + rx, floorY + 1, baseZ + rz),
|
||||
scatter, chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Place ceiling cobwebs and extra hanging chains. */
|
||||
private static void placeSharedCeilingDecor(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
// Cobwebs along walls at ceiling level
|
||||
int[][] cobwebCandidates = {
|
||||
{2, 1}, {4, 1}, {8, 1}, {10, 1},
|
||||
{2, 11}, {4, 11}, {8, 11}, {10, 11},
|
||||
{1, 4}, {1, 8}, {11, 4}, {11, 8},
|
||||
};
|
||||
for (int[] pos : cobwebCandidates) {
|
||||
if (layout.isInShape(pos[0], pos[1])
|
||||
&& !layout.isWall(pos[0], pos[1])
|
||||
&& random.nextFloat() < 0.45f) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 10, baseZ + pos[1]),
|
||||
Blocks.COBWEB.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
// Extra hanging chains at random interior positions
|
||||
int[][] chainCandidates = {{5, 3}, {7, 9}, {3, 7}};
|
||||
for (int[] pos : chainCandidates) {
|
||||
if (layout.isInShape(pos[0], pos[1])
|
||||
&& !layout.isWall(pos[0], pos[1])
|
||||
&& random.nextFloat() < 0.6f) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 10, baseZ + pos[1]),
|
||||
Blocks.CHAIN.defaultBlockState(), chunkBB);
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 9, baseZ + pos[1]),
|
||||
Blocks.CHAIN.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Place additional wall-mounted lighting. */
|
||||
private static void placeSharedWallLighting(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
// Lanterns on pillar sides (facing center) at ry=1 (on the floor)
|
||||
int[][] pillarLanternPositions = {
|
||||
{5, 4}, {4, 7}, {8, 5}, {7, 8},
|
||||
};
|
||||
for (int[] pos : pillarLanternPositions) {
|
||||
if (layout.isInShape(pos[0], pos[1])
|
||||
&& !layout.isWall(pos[0], pos[1])) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 1, baseZ + pos[1]),
|
||||
Blocks.LANTERN.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
// Extra wall sconces at quarter-points
|
||||
int[][] wallSconces = {
|
||||
{4, 0}, {8, 0}, {4, 12}, {8, 12},
|
||||
{0, 4}, {0, 8}, {12, 4}, {12, 8},
|
||||
};
|
||||
for (int[] pos : wallSconces) {
|
||||
if (layout.isWall(pos[0], pos[1]) && random.nextFloat() < 0.5f) {
|
||||
Direction torchDir = getTorchDirection(pos[0], pos[1]);
|
||||
if (torchDir != null) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 3, baseZ + pos[1]),
|
||||
Blocks.WALL_TORCH.defaultBlockState().setValue(
|
||||
net.minecraft.world.level.block.WallTorchBlock.FACING, torchDir),
|
||||
chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Place wall accent bands at ry=5 and ry=8. */
|
||||
private static void placeSharedWallBands(
|
||||
WorldGenLevel level,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
RoomThemeDefinition theme,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
int[][] bandPositions = {
|
||||
{6, 1}, {6, 11}, {1, 6}, {11, 6},
|
||||
{4, 1}, {8, 1}, {4, 11}, {8, 11},
|
||||
{1, 4}, {1, 8}, {11, 4}, {11, 8},
|
||||
};
|
||||
for (int[] pos : bandPositions) {
|
||||
if (layout.isWall(pos[0], pos[1])) {
|
||||
// Band at ry=5
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 5, baseZ + pos[1]),
|
||||
theme.wallAccentBlock(), chunkBB);
|
||||
// Optional band at ry=8
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 8, baseZ + pos[1]),
|
||||
theme.wallAccentBlock(), chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Chains on cage flanks and ceiling corners -- shared by all themes. */
|
||||
private static void placeSharedChains(
|
||||
WorldGenLevel level,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
for (int[] chainPos : new int[][] {{3, 6}, {9, 6}}) {
|
||||
for (int ry = 5; ry <= 10; ry++) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + chainPos[0], floorY + ry, baseZ + chainPos[1]),
|
||||
Blocks.CHAIN.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
for (int[] chainPos : new int[][] {{3, 3}, {3, 9}, {9, 3}, {9, 9}}) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + chainPos[0], floorY + 10, baseZ + chainPos[1]),
|
||||
Blocks.CHAIN.defaultBlockState(), chunkBB);
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine torch facing direction for a wall position (torch faces inward). */
|
||||
private static Direction getTorchDirection(int rx, int rz) {
|
||||
if (rz == 0) return Direction.SOUTH;
|
||||
if (rz == 12) return Direction.NORTH;
|
||||
if (rx == 0) return Direction.EAST;
|
||||
if (rx == 12) return Direction.WEST;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Hanging lanterns -- shared by all themes. */
|
||||
private static void placeSharedHangingLanterns(
|
||||
WorldGenLevel level,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
for (int[] pos : new int[][] {{3, 3}, {9, 9}}) {
|
||||
safeSetBlock(level,
|
||||
new BlockPos(baseX + pos[0], floorY + 9, baseZ + pos[1]),
|
||||
Blocks.LANTERN.defaultBlockState().setValue(LanternBlock.HANGING, true),
|
||||
chunkBB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Immutable data-driven room theme definition, replacing the old {@link RoomTheme} enum.
|
||||
*
|
||||
* <p>Loaded from {@code data/<namespace>/tiedup_room_themes/*.json} and stored
|
||||
* in {@link RoomThemeRegistry}. Convenience methods mirror the old
|
||||
* abstract API so that {@code HangingCagePiece} can swap with minimal changes.</p>
|
||||
*
|
||||
* @param id unique identifier (e.g. "tiedup:oubliette")
|
||||
* @param weight random selection weight (higher = more likely)
|
||||
* @param wallPalette palette for wall blocks (supports "default" and "bottom_row")
|
||||
* @param floorPalette palette for floor blocks (supports "default", "edge", "corner")
|
||||
* @param ceilingPalette palette for ceiling blocks
|
||||
* @param wallShellBlock single block used for the outer wall shell
|
||||
* @param wallAccentBlock single accent block (e.g. for frames, trims)
|
||||
* @param pillarPalette palette for pillars (supports "default" and "cap")
|
||||
* @param scatterPalette optional palette for scatter decorations (cobwebs, etc.)
|
||||
* @param decorations decoration configuration (corners, midpoints, furniture, etc.)
|
||||
*/
|
||||
public record RoomThemeDefinition(
|
||||
ResourceLocation id,
|
||||
int weight,
|
||||
BlockPalette wallPalette,
|
||||
BlockPalette floorPalette,
|
||||
BlockPalette ceilingPalette,
|
||||
BlockState wallShellBlock,
|
||||
BlockState wallAccentBlock,
|
||||
BlockPalette pillarPalette,
|
||||
@Nullable BlockPalette scatterPalette,
|
||||
DecorationConfig decorations
|
||||
) {
|
||||
|
||||
/**
|
||||
* Pick a wall block. Uses "bottom_row" condition for ry==1, "default" otherwise.
|
||||
*/
|
||||
public BlockState wallBlock(RandomSource random, int ry) {
|
||||
return wallPalette.pick(random, ry == 1 ? "bottom_row" : "default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a floor block. Uses "corner" for corner positions, "edge" for edges,
|
||||
* "default" for interior positions.
|
||||
*/
|
||||
public BlockState floorBlock(RandomSource random, int rx, int rz, boolean isEdge) {
|
||||
boolean isCorner = (rx == 1 || rx == 11) && (rz == 1 || rz == 11);
|
||||
return floorPalette.pick(
|
||||
random, isCorner ? "corner" : isEdge ? "edge" : "default"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a ceiling block (always "default" condition).
|
||||
*/
|
||||
public BlockState ceilingBlock(RandomSource random) {
|
||||
return ceilingPalette.pick(random, "default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a pillar block. Uses "cap" for top/bottom rows, "default" for the shaft.
|
||||
*/
|
||||
public BlockState pillarBlock(RandomSource random, int ry) {
|
||||
return pillarPalette.pick(random, (ry == 1 || ry == 10) ? "cap" : "default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a scatter block, or null if no scatter palette is defined.
|
||||
*/
|
||||
@Nullable
|
||||
public BlockState scatterBlock(RandomSource random) {
|
||||
return scatterPalette != null ? scatterPalette.pick(random, "default") : null;
|
||||
}
|
||||
}
|
||||
496
src/main/java/com/tiedup/remake/worldgen/RoomThemeParser.java
Normal file
496
src/main/java/com/tiedup/remake/worldgen/RoomThemeParser.java
Normal file
@@ -0,0 +1,496 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.IntegerProperty;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Parses JSON files into {@link RoomThemeDefinition} instances.
|
||||
*
|
||||
* <p>Uses manual field extraction (not Gson deserialization) for strict
|
||||
* validation control. Invalid required fields cause the entire definition
|
||||
* to be rejected; optional fields use safe defaults.</p>
|
||||
*
|
||||
* <p>Expected JSON files in {@code data/<namespace>/tiedup_room_themes/}.</p>
|
||||
*/
|
||||
public final class RoomThemeParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("TiedUpWorldgen");
|
||||
private static final String TAG = "[RoomThemeParser]";
|
||||
|
||||
private RoomThemeParser() {}
|
||||
|
||||
/**
|
||||
* Parse a JSON input stream into a RoomThemeDefinition.
|
||||
*
|
||||
* @param input the JSON input stream
|
||||
* @param fileId the resource location derived from the file path
|
||||
* @return the parsed definition, or null if the file is invalid
|
||||
*/
|
||||
@Nullable
|
||||
public static RoomThemeDefinition parse(InputStream input, ResourceLocation fileId) {
|
||||
try {
|
||||
JsonObject root = JsonParser.parseReader(
|
||||
new InputStreamReader(input, StandardCharsets.UTF_8)
|
||||
).getAsJsonObject();
|
||||
|
||||
return parseRoot(root, fileId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("{} Failed to parse JSON {}: {}", TAG, fileId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the root JSON object into a RoomThemeDefinition.
|
||||
*/
|
||||
@Nullable
|
||||
private static RoomThemeDefinition parseRoot(JsonObject root, ResourceLocation fileId) {
|
||||
// --- Required: weight (default 10 if missing, clamped [1, 1000]) ---
|
||||
int weight = clampInt(getIntOrDefault(root, "weight", 10), 1, 1000);
|
||||
|
||||
// --- Required: wall_palette ---
|
||||
BlockPalette wallPalette = parsePaletteField(root, "wall_palette", fileId);
|
||||
if (wallPalette == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'wall_palette'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Required: floor_palette ---
|
||||
BlockPalette floorPalette = parsePaletteField(root, "floor_palette", fileId);
|
||||
if (floorPalette == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'floor_palette'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Required: ceiling_palette ---
|
||||
BlockPalette ceilingPalette = parsePaletteField(root, "ceiling_palette", fileId);
|
||||
if (ceilingPalette == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'ceiling_palette'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Required: wall_shell (single block string) ---
|
||||
BlockState wallShell = parseBlockStateField(root, "wall_shell", fileId);
|
||||
if (wallShell == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'wall_shell'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Required: wall_accent (single block string) ---
|
||||
BlockState wallAccent = parseBlockStateField(root, "wall_accent", fileId);
|
||||
if (wallAccent == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'wall_accent'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Required: pillar_palette ---
|
||||
BlockPalette pillarPalette = parsePaletteField(root, "pillar_palette", fileId);
|
||||
if (pillarPalette == null) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'pillar_palette'", TAG, fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Optional: scatter_palette ---
|
||||
BlockPalette scatterPalette = null;
|
||||
if (root.has("scatter_palette") && root.get("scatter_palette").isJsonObject()) {
|
||||
scatterPalette = parsePaletteField(root, "scatter_palette", fileId);
|
||||
// If scatter_palette is present but invalid, warn but don't reject
|
||||
if (scatterPalette == null) {
|
||||
LOGGER.warn(
|
||||
"{} In {}: 'scatter_palette' present but invalid, ignoring", TAG, fileId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Optional: decorations ---
|
||||
DecorationConfig decorations = new DecorationConfig(
|
||||
List.of(), List.of(), null, List.of(), false, false
|
||||
);
|
||||
if (root.has("decorations") && root.get("decorations").isJsonObject()) {
|
||||
DecorationConfig parsed = parseDecorations(
|
||||
root.getAsJsonObject("decorations"), fileId
|
||||
);
|
||||
if (parsed != null) {
|
||||
decorations = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return new RoomThemeDefinition(
|
||||
fileId, weight,
|
||||
wallPalette, floorPalette, ceilingPalette,
|
||||
wallShell, wallAccent,
|
||||
pillarPalette, scatterPalette,
|
||||
decorations
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Palette Parsing =====
|
||||
|
||||
/**
|
||||
* Parse a palette field from the root object. The field value must be a JSON object
|
||||
* mapping condition names to arrays of weighted entries.
|
||||
*
|
||||
* <p>Example:
|
||||
* <pre>{
|
||||
* "default": [{"block": "minecraft:stone_bricks", "weight": 0.8}],
|
||||
* "bottom_row": [{"block": "minecraft:mossy_stone_bricks", "weight": 1.0}]
|
||||
* }</pre>
|
||||
*/
|
||||
@Nullable
|
||||
private static BlockPalette parsePaletteField(
|
||||
JsonObject root, String fieldName, ResourceLocation fileId
|
||||
) {
|
||||
if (!root.has(fieldName) || !root.get(fieldName).isJsonObject()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonObject paletteObj = root.getAsJsonObject(fieldName);
|
||||
Map<String, List<BlockPalette.WeightedEntry>> variants = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, JsonElement> condEntry : paletteObj.entrySet()) {
|
||||
String condition = condEntry.getKey();
|
||||
if (!condEntry.getValue().isJsonArray()) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}': condition '{}' is not an array, skipping",
|
||||
TAG, fileId, fieldName, condition
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<BlockPalette.WeightedEntry> entries = parseWeightedEntries(
|
||||
condEntry.getValue().getAsJsonArray(), fileId, fieldName, condition
|
||||
);
|
||||
if (!entries.isEmpty()) {
|
||||
variants.put(condition, List.copyOf(entries));
|
||||
}
|
||||
}
|
||||
|
||||
if (variants.isEmpty() || !variants.containsKey("default")) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}': no valid 'default' condition found",
|
||||
TAG, fileId, fieldName
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BlockPalette(variants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an array of weighted block entries for a single palette condition.
|
||||
*
|
||||
* <p>Each entry is a JSON object with:
|
||||
* <ul>
|
||||
* <li>{@code "block"} — block state string (e.g. "minecraft:candle[lit=true]")</li>
|
||||
* <li>{@code "weight"} — float weight (default 1.0)</li>
|
||||
* <li>{@code "random_property"} — optional object with {@code "name"}, {@code "min"}, {@code "max"}
|
||||
* to expand a single entry into multiple entries with varying integer property values</li>
|
||||
* </ul>
|
||||
*/
|
||||
private static List<BlockPalette.WeightedEntry> parseWeightedEntries(
|
||||
JsonArray array, ResourceLocation fileId, String paletteName, String condition
|
||||
) {
|
||||
List<BlockPalette.WeightedEntry> entries = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
if (!array.get(i).isJsonObject()) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] is not an object, skipping",
|
||||
TAG, fileId, paletteName, condition, i
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject entryObj = array.get(i).getAsJsonObject();
|
||||
|
||||
String blockStr = getStringOrNull(entryObj, "block");
|
||||
if (blockStr == null || blockStr.isEmpty()) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] missing 'block', skipping",
|
||||
TAG, fileId, paletteName, condition, i
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockState baseState = parseBlockState(blockStr, fileId);
|
||||
if (baseState == null) {
|
||||
continue; // parseBlockState already logged warning
|
||||
}
|
||||
|
||||
float weight = getFloatOrDefault(entryObj, "weight", 1.0f);
|
||||
if (weight <= 0) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] has non-positive weight, skipping",
|
||||
TAG, fileId, paletteName, condition, i
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle random_property expansion
|
||||
if (entryObj.has("random_property") && entryObj.get("random_property").isJsonObject()) {
|
||||
List<BlockPalette.WeightedEntry> expanded = expandRandomProperty(
|
||||
baseState, weight, entryObj.getAsJsonObject("random_property"),
|
||||
fileId, paletteName, condition, i
|
||||
);
|
||||
entries.addAll(expanded);
|
||||
} else {
|
||||
entries.add(new BlockPalette.WeightedEntry(baseState, weight));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a single weighted entry into multiple entries by varying an integer
|
||||
* block state property across a range.
|
||||
*
|
||||
* <p>For example, {@code candle[candles=1]} with {@code random_property: {name: "candles", min: 1, max: 3}}
|
||||
* and weight 0.30 produces three entries: candles=1 (0.10), candles=2 (0.10), candles=3 (0.10).</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<BlockPalette.WeightedEntry> expandRandomProperty(
|
||||
BlockState baseState, float totalWeight, JsonObject propObj,
|
||||
ResourceLocation fileId, String paletteName, String condition, int entryIndex
|
||||
) {
|
||||
String propName = getStringOrNull(propObj, "name");
|
||||
if (propName == null) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] random_property missing 'name', using base state",
|
||||
TAG, fileId, paletteName, condition, entryIndex
|
||||
);
|
||||
return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
|
||||
}
|
||||
|
||||
int min = getIntOrDefault(propObj, "min", 1);
|
||||
int max = getIntOrDefault(propObj, "max", min);
|
||||
if (max < min) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] random_property max < min, using base state",
|
||||
TAG, fileId, paletteName, condition, entryIndex
|
||||
);
|
||||
return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
|
||||
}
|
||||
|
||||
Property<?> property = baseState.getBlock().getStateDefinition().getProperty(propName);
|
||||
if (!(property instanceof IntegerProperty intProp)) {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] property '{}' is not an IntegerProperty, using base state",
|
||||
TAG, fileId, paletteName, condition, entryIndex, propName
|
||||
);
|
||||
return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
|
||||
}
|
||||
|
||||
int count = max - min + 1;
|
||||
float perWeight = totalWeight / count;
|
||||
List<BlockPalette.WeightedEntry> expanded = new ArrayList<>(count);
|
||||
|
||||
for (int val = min; val <= max; val++) {
|
||||
if (intProp.getPossibleValues().contains(val)) {
|
||||
expanded.add(new BlockPalette.WeightedEntry(
|
||||
baseState.setValue(intProp, val), perWeight
|
||||
));
|
||||
} else {
|
||||
LOGGER.warn(
|
||||
"{} In {} palette '{}' condition '{}': entry [{}] property '{}' does not accept value {}, skipping",
|
||||
TAG, fileId, paletteName, condition, entryIndex, propName, val
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded.isEmpty()) {
|
||||
return List.of(new BlockPalette.WeightedEntry(baseState, totalWeight));
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
// ===== Block State Parsing =====
|
||||
|
||||
/**
|
||||
* Parse a block state string like "minecraft:stone_bricks" or "minecraft:candle[lit=true]"
|
||||
* using Minecraft's built-in BlockStateParser.
|
||||
*
|
||||
* @return the parsed BlockState, or null on failure (logged as warning)
|
||||
*/
|
||||
@Nullable
|
||||
private static BlockState parseBlockState(String blockString, ResourceLocation fileId) {
|
||||
try {
|
||||
return BlockStateParser.parseForBlock(
|
||||
BuiltInRegistries.BLOCK.asLookup(),
|
||||
blockString,
|
||||
false // don't allow NBT
|
||||
).blockState();
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn(
|
||||
"{} In {}: invalid block state '{}': {}",
|
||||
TAG, fileId, blockString, e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single block state from a string field in a JSON object.
|
||||
*
|
||||
* @return the parsed BlockState, or null if field is missing or invalid
|
||||
*/
|
||||
@Nullable
|
||||
private static BlockState parseBlockStateField(
|
||||
JsonObject obj, String fieldName, ResourceLocation fileId
|
||||
) {
|
||||
String blockStr = getStringOrNull(obj, fieldName);
|
||||
if (blockStr == null || blockStr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return parseBlockState(blockStr, fileId);
|
||||
}
|
||||
|
||||
// ===== Decoration Parsing =====
|
||||
|
||||
/**
|
||||
* Parse the "decorations" JSON object into a DecorationConfig.
|
||||
*/
|
||||
@Nullable
|
||||
private static DecorationConfig parseDecorations(JsonObject obj, ResourceLocation fileId) {
|
||||
try {
|
||||
List<DecorationConfig.PositionedBlock> cornerDecos = parsePositionedBlockList(
|
||||
obj, "corner_decorations", fileId
|
||||
);
|
||||
List<DecorationConfig.PositionedBlock> wallMidpoints = parsePositionedBlockList(
|
||||
obj, "wall_midpoint_blocks", fileId
|
||||
);
|
||||
|
||||
DecorationConfig.PositionedBlock firstCornerSpecial = null;
|
||||
if (obj.has("first_corner_special") && obj.get("first_corner_special").isJsonObject()) {
|
||||
firstCornerSpecial = parsePositionedBlock(
|
||||
obj.getAsJsonObject("first_corner_special"), fileId
|
||||
);
|
||||
}
|
||||
|
||||
List<DecorationConfig.PositionedBlock> furnitureCluster = parsePositionedBlockList(
|
||||
obj, "furniture_cluster", fileId
|
||||
);
|
||||
|
||||
boolean useTorchLighting = getBooleanOrDefault(obj, "use_torch_lighting", false);
|
||||
boolean hasCeilingChain = getBooleanOrDefault(obj, "has_ceiling_chain", false);
|
||||
|
||||
return new DecorationConfig(
|
||||
cornerDecos, wallMidpoints, firstCornerSpecial,
|
||||
furnitureCluster, useTorchLighting, hasCeilingChain
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn(
|
||||
"{} In {}: failed to parse decorations: {}", TAG, fileId, e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an array of positioned blocks from a field.
|
||||
*/
|
||||
private static List<DecorationConfig.PositionedBlock> parsePositionedBlockList(
|
||||
JsonObject parent, String fieldName, ResourceLocation fileId
|
||||
) {
|
||||
if (!parent.has(fieldName) || !parent.get(fieldName).isJsonArray()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
JsonArray array = parent.getAsJsonArray(fieldName);
|
||||
List<DecorationConfig.PositionedBlock> result = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
if (!array.get(i).isJsonObject()) continue;
|
||||
|
||||
DecorationConfig.PositionedBlock pb = parsePositionedBlock(
|
||||
array.get(i).getAsJsonObject(), fileId
|
||||
);
|
||||
if (pb != null) {
|
||||
result.add(pb);
|
||||
}
|
||||
}
|
||||
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single positioned block: { "block": "...", "x_offset": 0, "y_offset": 0, "z_offset": 0 }
|
||||
*/
|
||||
@Nullable
|
||||
private static DecorationConfig.PositionedBlock parsePositionedBlock(
|
||||
JsonObject obj, ResourceLocation fileId
|
||||
) {
|
||||
String blockStr = getStringOrNull(obj, "block");
|
||||
if (blockStr == null || blockStr.isEmpty()) return null;
|
||||
|
||||
BlockState state = parseBlockState(blockStr, fileId);
|
||||
if (state == null) return null;
|
||||
|
||||
int xOffset = getIntOrDefault(obj, "x_offset", 0);
|
||||
int yOffset = getIntOrDefault(obj, "y_offset", 0);
|
||||
int zOffset = getIntOrDefault(obj, "z_offset", 0);
|
||||
return new DecorationConfig.PositionedBlock(state, xOffset, yOffset, zOffset);
|
||||
}
|
||||
|
||||
// ===== Primitive Helpers =====
|
||||
|
||||
@Nullable
|
||||
private static String getStringOrNull(JsonObject obj, String key) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return null;
|
||||
try {
|
||||
return obj.get(key).getAsString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsInt();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsFloat();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsBoolean();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static int clampInt(int value, int min, int max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
142
src/main/java/com/tiedup/remake/worldgen/RoomThemeRegistry.java
Normal file
142
src/main/java/com/tiedup/remake/worldgen/RoomThemeRegistry.java
Normal file
@@ -0,0 +1,142 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Thread-safe registry for data-driven room theme definitions.
|
||||
*
|
||||
* <p>Server-authoritative: definitions are loaded from {@code data/<namespace>/tiedup_room_themes/}
|
||||
* JSON files by the server reload listener. Unlike furniture, there is no client sync
|
||||
* because room themes are only used during server-side worldgen.</p>
|
||||
*
|
||||
* <p>Uses volatile atomic swap to ensure threads always see a consistent snapshot.</p>
|
||||
*
|
||||
* @see RoomThemeDefinition
|
||||
*/
|
||||
public final class RoomThemeRegistry {
|
||||
|
||||
/**
|
||||
* Volatile reference to an unmodifiable map. {@link #reload} builds a new map
|
||||
* and swaps atomically; consumer threads always see a consistent snapshot.
|
||||
*/
|
||||
private static volatile Map<
|
||||
ResourceLocation,
|
||||
RoomThemeDefinition
|
||||
> DEFINITIONS = Map.of();
|
||||
|
||||
/** Hardcoded fallback for when the registry is empty (worldgen safety). */
|
||||
private static final RoomThemeDefinition FALLBACK = createFallback();
|
||||
|
||||
private RoomThemeRegistry() {}
|
||||
|
||||
/**
|
||||
* Atomically replace all definitions with a new set.
|
||||
* Called by the reload listener after parsing all JSON files.
|
||||
*
|
||||
* @param newDefs the new definitions map (will be defensively copied)
|
||||
*/
|
||||
public static void reload(Map<ResourceLocation, RoomThemeDefinition> newDefs) {
|
||||
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a definition by its unique ID.
|
||||
*
|
||||
* @param id the definition ID (e.g. "tiedup:oubliette")
|
||||
* @return the definition, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
public static RoomThemeDefinition get(ResourceLocation id) {
|
||||
return DEFINITIONS.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered definitions.
|
||||
*
|
||||
* @return unmodifiable collection of all definitions
|
||||
*/
|
||||
public static Collection<RoomThemeDefinition> getAll() {
|
||||
return DEFINITIONS.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random theme proportionally by weight.
|
||||
*
|
||||
* @param random the random source
|
||||
* @return a randomly selected theme, or null if registry is empty
|
||||
*/
|
||||
@Nullable
|
||||
public static RoomThemeDefinition pickRandom(RandomSource random) {
|
||||
Collection<RoomThemeDefinition> all = DEFINITIONS.values();
|
||||
if (all.isEmpty()) return null;
|
||||
|
||||
int totalWeight = 0;
|
||||
for (RoomThemeDefinition def : all) {
|
||||
totalWeight += def.weight();
|
||||
}
|
||||
|
||||
if (totalWeight <= 0) return null;
|
||||
|
||||
int roll = random.nextInt(totalWeight);
|
||||
int cumulative = 0;
|
||||
for (RoomThemeDefinition def : all) {
|
||||
cumulative += def.weight();
|
||||
if (roll < cumulative) return def;
|
||||
}
|
||||
|
||||
// Fallback (should not happen)
|
||||
return all.iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random theme, or return a hardcoded stone bricks fallback if the
|
||||
* registry is empty. Guarantees non-null for worldgen safety.
|
||||
*
|
||||
* @param random the random source
|
||||
* @return a theme definition (never null)
|
||||
*/
|
||||
public static RoomThemeDefinition pickRandomOrFallback(RandomSource random) {
|
||||
RoomThemeDefinition picked = pickRandom(random);
|
||||
return picked != null ? picked : FALLBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all definitions. Called on world unload or for testing.
|
||||
*/
|
||||
public static void clear() {
|
||||
DEFINITIONS = Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a minimal stone bricks fallback theme for when no JSON themes are loaded.
|
||||
*/
|
||||
private static RoomThemeDefinition createFallback() {
|
||||
BlockState stone = Blocks.STONE_BRICKS.defaultBlockState();
|
||||
BlockPalette single = new BlockPalette(
|
||||
Map.of("default", List.of(new BlockPalette.WeightedEntry(stone, 1.0f)))
|
||||
);
|
||||
return new RoomThemeDefinition(
|
||||
new ResourceLocation("tiedup", "fallback"),
|
||||
1,
|
||||
single, // wall
|
||||
single, // floor
|
||||
single, // ceiling
|
||||
stone, // wallShell
|
||||
stone, // wallAccent
|
||||
single, // pillar
|
||||
null, // no scatter
|
||||
new DecorationConfig(
|
||||
List.of(), List.of(), null, List.of(), false, false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Server-side resource reload listener that scans {@code data/<namespace>/tiedup_room_themes/}
|
||||
* for JSON files and populates the {@link RoomThemeRegistry}.
|
||||
*
|
||||
* <p>Room themes are server-authoritative only (used during worldgen). Unlike furniture,
|
||||
* there is no client sync packet -- the registry is atomically replaced via
|
||||
* {@link RoomThemeRegistry#reload(Map)} on each reload.</p>
|
||||
*
|
||||
* <p>Registered via {@link net.minecraftforge.event.AddReloadListenerEvent} in
|
||||
* {@link com.tiedup.remake.core.TiedUpMod.ForgeEvents}.</p>
|
||||
*/
|
||||
public class RoomThemeReloadListener
|
||||
extends SimplePreparableReloadListener<Void>
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("TiedUpWorldgen");
|
||||
|
||||
/** Resource directory containing room theme definition JSON files (under data/). */
|
||||
private static final String DIRECTORY = "tiedup_room_themes";
|
||||
|
||||
@Override
|
||||
protected Void prepare(
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
// No preparation needed -- parsing happens in apply phase
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(
|
||||
Void nothing,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
Map<ResourceLocation, RoomThemeDefinition> newDefs = new HashMap<>();
|
||||
|
||||
Map<ResourceLocation, Resource> resources =
|
||||
resourceManager.listResources(DIRECTORY, loc ->
|
||||
loc.getPath().endsWith(".json")
|
||||
);
|
||||
|
||||
int skipped = 0;
|
||||
|
||||
for (Map.Entry<ResourceLocation, Resource> entry : resources.entrySet()) {
|
||||
ResourceLocation fileId = entry.getKey();
|
||||
Resource resource = entry.getValue();
|
||||
|
||||
// Derive clean ID from file path: "tiedup:tiedup_room_themes/oubliette.json" -> "tiedup:oubliette"
|
||||
String rawPath = fileId.getPath();
|
||||
String cleanPath = rawPath.substring(rawPath.indexOf(DIRECTORY + "/") + DIRECTORY.length() + 1);
|
||||
if (cleanPath.endsWith(".json")) {
|
||||
cleanPath = cleanPath.substring(0, cleanPath.length() - 5);
|
||||
}
|
||||
ResourceLocation cleanId = new ResourceLocation(fileId.getNamespace(), cleanPath);
|
||||
|
||||
try (InputStream input = resource.open()) {
|
||||
RoomThemeDefinition def = RoomThemeParser.parse(input, cleanId);
|
||||
|
||||
if (def != null) {
|
||||
// Check for duplicate IDs (fileId IS the ID for room themes)
|
||||
if (newDefs.containsKey(def.id())) {
|
||||
LOGGER.warn(
|
||||
"[TiedUpWorldgen] Server: Duplicate room theme ID '{}' from file '{}' -- overwriting previous definition",
|
||||
def.id(),
|
||||
fileId
|
||||
);
|
||||
}
|
||||
|
||||
newDefs.put(def.id(), def);
|
||||
LOGGER.debug(
|
||||
"[TiedUpWorldgen] Server loaded room theme: {}",
|
||||
def.id()
|
||||
);
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(
|
||||
"[TiedUpWorldgen] Server: Failed to read resource {}: {}",
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// Atomically replace all definitions in the registry
|
||||
RoomThemeRegistry.reload(newDefs);
|
||||
|
||||
LOGGER.info(
|
||||
"[TiedUpWorldgen] Server loaded {} room theme definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(),
|
||||
skipped,
|
||||
resources.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
60
src/main/resources/data/tiedup/tiedup_room_themes/crypt.json
Normal file
60
src/main/resources/data/tiedup/tiedup_room_themes/crypt.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:mossy_stone_bricks", "weight": 0.15 },
|
||||
{ "block": "minecraft:cracked_stone_bricks", "weight": 0.15 },
|
||||
{ "block": "minecraft:stone_bricks", "weight": 0.70 }
|
||||
],
|
||||
"bottom_row": [
|
||||
{ "block": "minecraft:mossy_cobblestone", "weight": 0.20 },
|
||||
{ "block": "minecraft:cracked_stone_bricks", "weight": 0.10 },
|
||||
{ "block": "minecraft:stone_bricks", "weight": 0.70 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cobblestone", "weight": 0.20 },
|
||||
{ "block": "minecraft:stone_bricks", "weight": 0.80 }
|
||||
],
|
||||
"corner": [
|
||||
{ "block": "minecraft:mossy_cobblestone", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_stone_bricks", "weight": 0.20 },
|
||||
{ "block": "minecraft:stone_bricks", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:stone_bricks",
|
||||
"wall_accent": "minecraft:mossy_stone_bricks",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:stone_brick_wall", "weight": 1.0 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:chiseled_stone_bricks", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cobweb", "weight": 0.40 },
|
||||
{ "block": "minecraft:candle[lit=true]", "weight": 0.30, "random_property": { "name": "candles", "min": 1, "max": 3 } },
|
||||
{ "block": "minecraft:bone_block", "weight": 0.30 }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"corner_decorations": [
|
||||
{ "block": "minecraft:cobweb", "y_offset": 1 },
|
||||
{ "block": "minecraft:cobweb", "y_offset": 9 }
|
||||
],
|
||||
"first_corner_special": { "block": "minecraft:skeleton_skull", "y_offset": 1 },
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:lectern", "y_offset": 1 },
|
||||
{ "block": "minecraft:candle[lit=true,candles=4]", "x_offset": 1, "y_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": true,
|
||||
"has_ceiling_chain": false
|
||||
}
|
||||
}
|
||||
59
src/main/resources/data/tiedup/tiedup_room_themes/ice.json
Normal file
59
src/main/resources/data/tiedup/tiedup_room_themes/ice.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:ice", "weight": 0.10 },
|
||||
{ "block": "minecraft:blue_ice", "weight": 0.20 },
|
||||
{ "block": "minecraft:packed_ice", "weight": 0.70 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:snow_block", "weight": 0.20 },
|
||||
{ "block": "minecraft:packed_ice", "weight": 0.80 }
|
||||
],
|
||||
"edge": [
|
||||
{ "block": "minecraft:blue_ice", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:blue_ice", "weight": 0.20 },
|
||||
{ "block": "minecraft:packed_ice", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:packed_ice",
|
||||
"wall_accent": "minecraft:blue_ice",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:packed_ice", "weight": 0.50 },
|
||||
{ "block": "minecraft:blue_ice", "weight": 0.50 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:blue_ice", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:snow[layers=1]", "weight": 0.50, "random_property": { "name": "layers", "min": 1, "max": 2 } },
|
||||
{ "block": "minecraft:powder_snow", "weight": 0.20 },
|
||||
{ "block": "minecraft:ice", "weight": 0.30 }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"corner_decorations": [
|
||||
{ "block": "minecraft:snow[layers=3]", "y_offset": 1 },
|
||||
{ "block": "minecraft:ice", "y_offset": 10 }
|
||||
],
|
||||
"wall_midpoint_blocks": [
|
||||
{ "block": "minecraft:lantern", "y_offset": 3 }
|
||||
],
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:powder_snow_cauldron[level=3]", "y_offset": 1 },
|
||||
{ "block": "minecraft:blue_ice", "x_offset": 1, "y_offset": 1 },
|
||||
{ "block": "minecraft:lantern", "y_offset": 2, "z_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": false,
|
||||
"has_ceiling_chain": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_nether_bricks", "weight": 0.20 },
|
||||
{ "block": "minecraft:nether_bricks", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:gilded_blackstone", "weight": 0.08 },
|
||||
{ "block": "minecraft:blackstone", "weight": 0.92 }
|
||||
],
|
||||
"edge": [
|
||||
{ "block": "minecraft:magma_block", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_nether_bricks", "weight": 0.20 },
|
||||
{ "block": "minecraft:nether_bricks", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:nether_bricks",
|
||||
"wall_accent": "minecraft:red_nether_bricks",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:nether_brick_wall", "weight": 1.0 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:quartz_pillar", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:soul_sand", "weight": 0.40 },
|
||||
{ "block": "minecraft:nether_wart_block", "weight": 0.30 },
|
||||
{ "block": "minecraft:bone_block", "weight": 0.30 }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"corner_decorations": [
|
||||
{ "block": "minecraft:soul_sand", "y_offset": 0 },
|
||||
{ "block": "minecraft:soul_fire", "y_offset": 1 }
|
||||
],
|
||||
"wall_midpoint_blocks": [
|
||||
{ "block": "minecraft:crying_obsidian", "y_offset": 2 },
|
||||
{ "block": "minecraft:soul_lantern", "y_offset": 3 }
|
||||
],
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:soul_campfire", "y_offset": 1 },
|
||||
{ "block": "minecraft:lava_cauldron", "x_offset": 1, "y_offset": 1 },
|
||||
{ "block": "minecraft:gilded_blackstone", "y_offset": 1, "z_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": false,
|
||||
"has_ceiling_chain": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 0.20 },
|
||||
{ "block": "minecraft:deepslate_bricks", "weight": 0.80 }
|
||||
],
|
||||
"bottom_row": [
|
||||
{ "block": "minecraft:mossy_cobblestone", "weight": 0.30 },
|
||||
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 0.14 },
|
||||
{ "block": "minecraft:deepslate_bricks", "weight": 0.56 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cobblestone", "weight": 0.15 },
|
||||
{ "block": "minecraft:deepslate_tiles", "weight": 0.85 }
|
||||
],
|
||||
"corner": [
|
||||
{ "block": "minecraft:mossy_cobblestone", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 0.20 },
|
||||
{ "block": "minecraft:deepslate_bricks", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:deepslate_bricks",
|
||||
"wall_accent": "minecraft:polished_deepslate",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:deepslate_brick_wall", "weight": 1.0 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:polished_deepslate", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cobweb", "weight": 0.40 },
|
||||
{ "block": "minecraft:candle[lit=true]", "weight": 0.30, "random_property": { "name": "candles", "min": 1, "max": 3 } },
|
||||
{ "block": "minecraft:moss_carpet", "weight": 0.30 }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"corner_decorations": [
|
||||
{ "block": "minecraft:cobweb", "y_offset": 1 },
|
||||
{ "block": "minecraft:cobweb", "y_offset": 9 }
|
||||
],
|
||||
"wall_midpoint_blocks": [
|
||||
{ "block": "minecraft:soul_lantern", "y_offset": 3 }
|
||||
],
|
||||
"first_corner_special": { "block": "minecraft:water_cauldron[level=3]", "x_offset": 1, "y_offset": 1, "z_offset": 1 },
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:barrel", "y_offset": 1 },
|
||||
{ "block": "minecraft:brewing_stand", "x_offset": 1, "y_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": false,
|
||||
"has_ceiling_chain": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:chiseled_sandstone", "weight": 0.10 },
|
||||
{ "block": "minecraft:sandstone", "weight": 0.20 },
|
||||
{ "block": "minecraft:cut_sandstone", "weight": 0.70 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sand", "weight": 0.15 },
|
||||
{ "block": "minecraft:smooth_sandstone", "weight": 0.85 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sandstone", "weight": 0.20 },
|
||||
{ "block": "minecraft:cut_sandstone", "weight": 0.80 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:cut_sandstone",
|
||||
"wall_accent": "minecraft:chiseled_sandstone",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sandstone_wall", "weight": 1.0 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:chiseled_sandstone", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sand", "weight": 0.40 },
|
||||
{ "block": "minecraft:dead_bush", "weight": 0.30 },
|
||||
{ "block": "minecraft:candle[lit=true]", "weight": 0.30, "random_property": { "name": "candles", "min": 1, "max": 3 } }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"wall_midpoint_blocks": [
|
||||
{ "block": "minecraft:orange_terracotta", "y_offset": 2 }
|
||||
],
|
||||
"first_corner_special": { "block": "minecraft:tnt", "y_offset": 1 },
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:barrel", "y_offset": 1 },
|
||||
{ "block": "minecraft:flower_pot", "x_offset": 1, "y_offset": 1 },
|
||||
{ "block": "minecraft:orange_terracotta", "y_offset": 1, "z_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": true,
|
||||
"has_ceiling_chain": false
|
||||
}
|
||||
}
|
||||
58
src/main/resources/data/tiedup/tiedup_room_themes/sculk.json
Normal file
58
src/main/resources/data/tiedup/tiedup_room_themes/sculk.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"weight": 10,
|
||||
"wall_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 0.10 },
|
||||
{ "block": "minecraft:sculk", "weight": 0.30 },
|
||||
{ "block": "minecraft:deepslate_bricks", "weight": 0.60 }
|
||||
]
|
||||
},
|
||||
"floor_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sculk", "weight": 0.30 },
|
||||
{ "block": "minecraft:deepslate_tiles", "weight": 0.70 }
|
||||
],
|
||||
"edge": [
|
||||
{ "block": "minecraft:sculk", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"ceiling_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 0.10 },
|
||||
{ "block": "minecraft:sculk", "weight": 0.20 },
|
||||
{ "block": "minecraft:deepslate_bricks", "weight": 0.70 }
|
||||
]
|
||||
},
|
||||
"wall_shell": "minecraft:deepslate_bricks",
|
||||
"wall_accent": "minecraft:sculk_catalyst",
|
||||
"pillar_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sculk", "weight": 0.30 },
|
||||
{ "block": "minecraft:deepslate_tile_wall", "weight": 0.70 }
|
||||
],
|
||||
"cap": [
|
||||
{ "block": "minecraft:sculk", "weight": 1.0 }
|
||||
]
|
||||
},
|
||||
"scatter_palette": {
|
||||
"default": [
|
||||
{ "block": "minecraft:sculk", "weight": 0.50 },
|
||||
{ "block": "minecraft:sculk_vein", "weight": 0.30 },
|
||||
{ "block": "minecraft:moss_carpet", "weight": 0.20 }
|
||||
]
|
||||
},
|
||||
"decorations": {
|
||||
"wall_midpoint_blocks": [
|
||||
{ "block": "minecraft:sculk_vein", "y_offset": 2 },
|
||||
{ "block": "minecraft:soul_lantern", "y_offset": 3 }
|
||||
],
|
||||
"first_corner_special": { "block": "minecraft:sculk_catalyst", "y_offset": 1 },
|
||||
"furniture_cluster": [
|
||||
{ "block": "minecraft:sculk_shrieker", "y_offset": 1 },
|
||||
{ "block": "minecraft:sculk_sensor", "x_offset": 1, "y_offset": 1 },
|
||||
{ "block": "minecraft:candle[lit=true,candles=3]", "y_offset": 1, "z_offset": 1 }
|
||||
],
|
||||
"use_torch_lighting": false,
|
||||
"has_ceiling_chain": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user