Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
|
||||
|
||||
/**
|
||||
* Abstract base class for kidnapper structure types.
|
||||
*
|
||||
* Phase 3: Refactoring - Consolidates common structure generation logic
|
||||
*
|
||||
* Subclasses:
|
||||
* - KidnapperCampStructure: Small, 2 tents
|
||||
* - KidnapperOutpostStructure: Medium, 2-3 cells around HQ
|
||||
* - KidnapperFortressStructure: Large, 4-6 cells with corridors
|
||||
*
|
||||
* All structures share terrain validation:
|
||||
* - Must be above sea level (Y >= 63)
|
||||
* - Terrain must be flat enough (configurable per type)
|
||||
* - No water at the surface
|
||||
*/
|
||||
public abstract class AbstractKidnapperStructure extends Structure {
|
||||
|
||||
/** Standard Y offset for most kidnapper structures */
|
||||
protected static final int DEFAULT_Y_OFFSET = -2;
|
||||
|
||||
/** Sea level - structures below this are rejected */
|
||||
private static final int SEA_LEVEL = 63;
|
||||
|
||||
protected AbstractKidnapperStructure(StructureSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the radius (in blocks) to check for terrain flatness.
|
||||
* Larger structures need larger check areas.
|
||||
*/
|
||||
protected abstract int getCheckRadius();
|
||||
|
||||
/**
|
||||
* Get the maximum allowed height difference across sample points.
|
||||
* Stricter values reject more uneven terrain.
|
||||
*/
|
||||
protected abstract int getMaxHeightDifference();
|
||||
|
||||
/**
|
||||
* Get the Y offset for this structure type.
|
||||
* Override for structures with underground sections.
|
||||
*/
|
||||
protected int getYOffset() {
|
||||
return DEFAULT_Y_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GenerationStub> findGenerationPoint(
|
||||
GenerationContext context
|
||||
) {
|
||||
ChunkPos chunkPos = context.chunkPos();
|
||||
|
||||
int x = chunkPos.getMiddleBlockX();
|
||||
int z = chunkPos.getMiddleBlockZ();
|
||||
int y = context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
x,
|
||||
z,
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
);
|
||||
|
||||
BlockPos centerPos = new BlockPos(x, y, z);
|
||||
|
||||
// Check 1: Not underground (above sea level)
|
||||
if (y < SEA_LEVEL) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[{}] Skipping at {} - below sea level (y={})",
|
||||
getClass().getSimpleName(),
|
||||
centerPos.toShortString(),
|
||||
y
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Check 2: Terrain is flat enough
|
||||
if (!isTerrainFlat(context, centerPos)) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[{}] Skipping at {} - terrain not flat enough",
|
||||
getClass().getSimpleName(),
|
||||
centerPos.toShortString()
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Check 3: No water at the surface
|
||||
if (hasWater(context, centerPos)) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[{}] Skipping at {} - water detected",
|
||||
getClass().getSimpleName(),
|
||||
centerPos.toShortString()
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
new GenerationStub(centerPos, builder -> {
|
||||
Rotation rotation = Rotation.getRandom(context.random());
|
||||
generatePieces(builder, context, centerPos, rotation);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the terrain is flat enough for this structure.
|
||||
* Samples heights at corners, edges, and midpoints of the footprint.
|
||||
*/
|
||||
private boolean isTerrainFlat(
|
||||
GenerationContext context,
|
||||
BlockPos centerPos
|
||||
) {
|
||||
int checkRadius = getCheckRadius();
|
||||
int centerY = centerPos.getY();
|
||||
int minY = centerY;
|
||||
int maxY = centerY;
|
||||
|
||||
int[][] sampleOffsets = {
|
||||
{ 0, 0 }, // Center
|
||||
{ checkRadius, 0 }, // East
|
||||
{ -checkRadius, 0 }, // West
|
||||
{ 0, checkRadius }, // South
|
||||
{ 0, -checkRadius }, // North
|
||||
{ checkRadius, checkRadius }, // SE
|
||||
{ checkRadius, -checkRadius }, // NE
|
||||
{ -checkRadius, checkRadius }, // SW
|
||||
{ -checkRadius, -checkRadius }, // NW
|
||||
{ checkRadius / 2, checkRadius / 2 }, // Mid SE
|
||||
{ checkRadius / 2, -checkRadius / 2 }, // Mid NE
|
||||
{ -checkRadius / 2, checkRadius / 2 }, // Mid SW
|
||||
{ -checkRadius / 2, -checkRadius / 2 }, // Mid NW
|
||||
};
|
||||
|
||||
for (int[] offset : sampleOffsets) {
|
||||
int sampleX = centerPos.getX() + offset[0];
|
||||
int sampleZ = centerPos.getZ() + offset[1];
|
||||
|
||||
int sampleY = context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
sampleX,
|
||||
sampleZ,
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
);
|
||||
|
||||
minY = Math.min(minY, sampleY);
|
||||
maxY = Math.max(maxY, sampleY);
|
||||
}
|
||||
|
||||
return (maxY - minY) <= getMaxHeightDifference();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is water at the structure location.
|
||||
* Compares WORLD_SURFACE_WG with OCEAN_FLOOR_WG - if the difference > 1, there's water.
|
||||
* Uses 13 sample points (same as isTerrainFlat) to catch diagonal rivers and edges.
|
||||
*/
|
||||
private boolean hasWater(GenerationContext context, BlockPos centerPos) {
|
||||
int checkRadius = getCheckRadius();
|
||||
|
||||
// Same 13-point sampling as isTerrainFlat — catches diagonal rivers
|
||||
int[][] sampleOffsets = {
|
||||
{ 0, 0 }, // Center
|
||||
{ checkRadius, 0 }, // East
|
||||
{ -checkRadius, 0 }, // West
|
||||
{ 0, checkRadius }, // South
|
||||
{ 0, -checkRadius }, // North
|
||||
{ checkRadius, checkRadius }, // SE
|
||||
{ checkRadius, -checkRadius }, // NE
|
||||
{ -checkRadius, checkRadius }, // SW
|
||||
{ -checkRadius, -checkRadius }, // NW
|
||||
{ checkRadius / 2, checkRadius / 2 }, // Mid SE
|
||||
{ checkRadius / 2, -checkRadius / 2 }, // Mid NE
|
||||
{ -checkRadius / 2, checkRadius / 2 }, // Mid SW
|
||||
{ -checkRadius / 2, -checkRadius / 2 }, // Mid NW
|
||||
};
|
||||
|
||||
for (int[] offset : sampleOffsets) {
|
||||
int sampleX = centerPos.getX() + offset[0];
|
||||
int sampleZ = centerPos.getZ() + offset[1];
|
||||
|
||||
int surfaceY = context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
sampleX,
|
||||
sampleZ,
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
);
|
||||
|
||||
int oceanFloorY = context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
sampleX,
|
||||
sampleZ,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
);
|
||||
|
||||
if (surfaceY - oceanFloorY > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the structure pieces.
|
||||
* Subclasses implement this to define their specific layout.
|
||||
*/
|
||||
protected abstract void generatePieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
Rotation rotation
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the ground height at a specific position with Y offset applied.
|
||||
*/
|
||||
protected int getGenerationHeight(GenerationContext context, int x, int z) {
|
||||
return (
|
||||
context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
x,
|
||||
z,
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
) +
|
||||
getYOffset()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate an offset position based on structure rotation.
|
||||
*/
|
||||
protected int[] rotateOffset(int x, int z, Rotation rotation) {
|
||||
return switch (rotation) {
|
||||
case NONE -> new int[] { x, z };
|
||||
case CLOCKWISE_90 -> new int[] { -z, x };
|
||||
case CLOCKWISE_180 -> new int[] { -x, -z };
|
||||
case COUNTERCLOCKWISE_90 -> new int[] { z, -x };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position for a piece with rotated offset.
|
||||
*/
|
||||
protected BlockPos calculatePiecePosition(
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
int offsetX,
|
||||
int offsetZ,
|
||||
Rotation rotation
|
||||
) {
|
||||
int[] rotatedOffset = rotateOffset(offsetX, offsetZ, rotation);
|
||||
int pieceX = centerPos.getX() + rotatedOffset[0];
|
||||
int pieceZ = centerPos.getZ() + rotatedOffset[1];
|
||||
int pieceY = getGenerationHeight(context, pieceX, pieceZ);
|
||||
return new BlockPos(pieceX, pieceY, pieceZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add surrounding pieces around a center position.
|
||||
* Common pattern for placing cells around a main building.
|
||||
*/
|
||||
protected void addSurroundingPieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
Rotation rotation,
|
||||
int[][] offsets,
|
||||
String pieceName,
|
||||
int count
|
||||
) {
|
||||
for (int i = 0; i < count && i < offsets.length; i++) {
|
||||
int offsetX = offsets[i][0];
|
||||
int offsetZ = offsets[i][1];
|
||||
|
||||
BlockPos piecePos = calculatePiecePosition(
|
||||
context,
|
||||
centerPos,
|
||||
offsetX,
|
||||
offsetZ,
|
||||
rotation
|
||||
);
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", pieceName),
|
||||
piecePos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
656
src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
Normal file
656
src/main/java/com/tiedup/remake/worldgen/HangingCagePiece.java
Normal file
@@ -0,0 +1,656 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.tiedup.remake.blocks.ModBlocks;
|
||||
import com.tiedup.remake.blocks.entity.TrappedChestBlockEntity;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.items.base.BindVariant;
|
||||
import com.tiedup.remake.items.base.BlindfoldVariant;
|
||||
import com.tiedup.remake.items.base.GagVariant;
|
||||
import com.tiedup.remake.v2.V2Blocks;
|
||||
import com.tiedup.remake.v2.blocks.PetCageBlock;
|
||||
import com.tiedup.remake.v2.blocks.PetCagePartBlock;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.DoubleTag;
|
||||
import net.minecraft.nbt.FloatTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
|
||||
/**
|
||||
* Structure piece for the hanging cage.
|
||||
*
|
||||
* Scans the cave geometry in postProcess() and places:
|
||||
* - Iron bars column from ceiling down to the top of the cage
|
||||
* - A Pet Cage (3x3x2 multi-block) suspended in the cave
|
||||
* - A Damsel entity inside the cage (25% shiny)
|
||||
*/
|
||||
public class HangingCagePiece extends StructurePiece {
|
||||
|
||||
private final BlockPos candidatePos;
|
||||
private final Direction facing;
|
||||
private boolean placed = false;
|
||||
|
||||
/** Minimum air gap between cave floor and bottom of cage. */
|
||||
private static final int MIN_GAP = 4;
|
||||
/** Height of the cage in blocks. */
|
||||
private static final int CAGE_HEIGHT = 2;
|
||||
/** Minimum cave height: gap + cage + at least 1 iron bar. */
|
||||
private static final int MIN_CAVE_HEIGHT = MIN_GAP + CAGE_HEIGHT + 1;
|
||||
/** Max scan distance up/down from candidate pos. */
|
||||
private static final int SCAN_RANGE = 64;
|
||||
|
||||
/** X/Z offsets to try around chunk center. Cage is 3x3 so we space by 4 to avoid overlap. */
|
||||
private static final int[][] COLUMN_OFFSETS = {
|
||||
{ 0, 0 },
|
||||
{ 4, 0 },
|
||||
{ -4, 0 },
|
||||
{ 0, 4 },
|
||||
{ 0, -4 },
|
||||
{ 4, 4 },
|
||||
{ -4, 4 },
|
||||
{ 4, -4 },
|
||||
{ -4, -4 },
|
||||
{ 2, 2 },
|
||||
{ -2, 2 },
|
||||
{ 2, -2 },
|
||||
{ -2, -2 },
|
||||
};
|
||||
|
||||
public HangingCagePiece(BlockPos candidatePos, RandomSource random) {
|
||||
super(
|
||||
ModStructures.HANGING_CAGE_PIECE.get(),
|
||||
0,
|
||||
makeBoundingBox(candidatePos)
|
||||
);
|
||||
this.candidatePos = candidatePos;
|
||||
this.facing = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||
}
|
||||
|
||||
public HangingCagePiece(CompoundTag tag) {
|
||||
super(ModStructures.HANGING_CAGE_PIECE.get(), tag);
|
||||
this.candidatePos = new BlockPos(
|
||||
tag.getInt("CandX"),
|
||||
tag.getInt("CandY"),
|
||||
tag.getInt("CandZ")
|
||||
);
|
||||
this.facing = Direction.from2DDataValue(tag.getInt("Facing"));
|
||||
}
|
||||
|
||||
private static BoundingBox makeBoundingBox(BlockPos pos) {
|
||||
// Generous bounding box: ±7 covers 13-wide room (±6) + column offsets, vertical covers scan + room
|
||||
return new BoundingBox(
|
||||
pos.getX() - 7,
|
||||
pos.getY() - SCAN_RANGE,
|
||||
pos.getZ() - 7,
|
||||
pos.getX() + 7,
|
||||
pos.getY() + SCAN_RANGE,
|
||||
pos.getZ() + 7
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalSaveData(
|
||||
StructurePieceSerializationContext context,
|
||||
CompoundTag tag
|
||||
) {
|
||||
tag.putInt("CandX", candidatePos.getX());
|
||||
tag.putInt("CandY", candidatePos.getY());
|
||||
tag.putInt("CandZ", candidatePos.getZ());
|
||||
tag.putInt("Facing", facing.get2DDataValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcess(
|
||||
WorldGenLevel level,
|
||||
net.minecraft.world.level.StructureManager structureManager,
|
||||
net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator,
|
||||
RandomSource random,
|
||||
BoundingBox chunkBB,
|
||||
ChunkPos chunkPos,
|
||||
BlockPos pivot
|
||||
) {
|
||||
if (placed) return;
|
||||
|
||||
// Try multiple columns around chunk center to maximize chance of finding a cave.
|
||||
// A single column has low odds of hitting a cave; 13 columns covers a wide area.
|
||||
for (int[] offset : COLUMN_OFFSETS) {
|
||||
int x = candidatePos.getX() + offset[0];
|
||||
int z = candidatePos.getZ() + offset[1];
|
||||
|
||||
if (scanColumnAndPlace(level, random, x, z, chunkBB)) {
|
||||
placed = true;
|
||||
return; // Success
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: carve a dungeon room and place the cage inside
|
||||
carveAndPlaceRoom(
|
||||
level,
|
||||
random,
|
||||
candidatePos.getX(),
|
||||
candidatePos.getZ(),
|
||||
chunkBB
|
||||
);
|
||||
placed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single column from Y=50 down to Y=-50, looking for caves tall enough.
|
||||
* @return true if a cage was placed
|
||||
*/
|
||||
private boolean scanColumnAndPlace(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int x,
|
||||
int z,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
int scanTop = 50;
|
||||
int scanBottom = -50;
|
||||
|
||||
int ceilingY = -1;
|
||||
boolean inAir = false;
|
||||
|
||||
for (int y = scanTop; y >= scanBottom; y--) {
|
||||
BlockState state = level.getBlockState(new BlockPos(x, y, z));
|
||||
boolean solid = !state.isAir() && !state.liquid();
|
||||
|
||||
if (!inAir && !solid) {
|
||||
// Transition solid -> air: ceiling is at y+1
|
||||
ceilingY = y + 1;
|
||||
inAir = true;
|
||||
} else if (inAir && solid) {
|
||||
// Transition air -> solid: floor is at y
|
||||
int floorY = y;
|
||||
inAir = false;
|
||||
|
||||
int caveHeight = ceilingY - floorY - 1;
|
||||
if (caveHeight >= MIN_CAVE_HEIGHT) {
|
||||
if (
|
||||
tryPlaceCage(
|
||||
level,
|
||||
random,
|
||||
x,
|
||||
z,
|
||||
floorY,
|
||||
ceilingY,
|
||||
chunkBB
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ceilingY = -1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to place the cage in a cave with the given floor and ceiling Y.
|
||||
* @return true if placement succeeded
|
||||
*/
|
||||
private boolean tryPlaceCage(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int x,
|
||||
int z,
|
||||
int floorY,
|
||||
int ceilingY,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
int cageMasterY = floorY + 1 + MIN_GAP;
|
||||
int cageTopY = cageMasterY + CAGE_HEIGHT;
|
||||
int numBars = ceilingY - cageTopY;
|
||||
if (numBars < 1) return false;
|
||||
|
||||
BlockPos masterPos = new BlockPos(x, cageMasterY, z);
|
||||
|
||||
// Validate 3x3x2 footprint is all air
|
||||
if (!level.getBlockState(masterPos).isAir()) return false;
|
||||
|
||||
BlockPos[] partPositions = PetCageBlock.getPartPositions(
|
||||
masterPos,
|
||||
facing
|
||||
);
|
||||
for (BlockPos partPos : partPositions) {
|
||||
if (!level.getBlockState(partPos).isAir()) return false;
|
||||
}
|
||||
|
||||
// Place iron bars from ceiling down to top of cage
|
||||
for (int y = cageTopY; y < ceilingY; y++) {
|
||||
safeSetBlock(
|
||||
level,
|
||||
new BlockPos(x, y, z),
|
||||
Blocks.IRON_BARS.defaultBlockState(),
|
||||
chunkBB
|
||||
);
|
||||
}
|
||||
|
||||
// Place Pet Cage master block
|
||||
BlockState masterState = V2Blocks.PET_CAGE.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetCageBlock.FACING, facing);
|
||||
safeSetBlock(level, masterPos, masterState, chunkBB);
|
||||
|
||||
// Place Pet Cage part blocks
|
||||
BlockState partState = V2Blocks.PET_CAGE_PART.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetCagePartBlock.FACING, facing);
|
||||
for (BlockPos partPos : partPositions) {
|
||||
safeSetBlock(level, partPos, partState, chunkBB);
|
||||
}
|
||||
|
||||
if (chunkBB.isInside(masterPos)) {
|
||||
scheduleDamselSpawn(level, masterPos, random);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[HangingCage] Placed cage in cave at {}",
|
||||
masterPos.toShortString()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// RoomLayout and RoomTheme extracted to separate files in this package.
|
||||
|
||||
// ── Fallback room generation ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fallback: carve a 13x13x12 dungeon room and place the cage inside.
|
||||
* Randomly selects a theme (Oubliette/Inferno) and layout (Square/Octagonal).
|
||||
*/
|
||||
private void carveAndPlaceRoom(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int centerX,
|
||||
int centerZ,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
RoomTheme theme = RoomTheme.values()[random.nextInt(
|
||||
RoomTheme.values().length
|
||||
)];
|
||||
RoomLayout layout = RoomLayout.values()[random.nextInt(
|
||||
RoomLayout.values().length
|
||||
)];
|
||||
|
||||
int ROOM = 13;
|
||||
int HEIGHT = 12;
|
||||
int baseX = centerX - 6;
|
||||
int baseZ = centerZ - 6;
|
||||
int floorY = candidatePos.getY() - 5;
|
||||
|
||||
// Phase 1: Shell — walls, floor, ceiling, air interior
|
||||
for (int rx = 0; rx < ROOM; rx++) {
|
||||
for (int rz = 0; rz < ROOM; rz++) {
|
||||
if (!layout.isInShape(rx, rz)) continue;
|
||||
|
||||
for (int ry = 0; ry < HEIGHT; ry++) {
|
||||
BlockPos pos = new BlockPos(
|
||||
baseX + rx,
|
||||
floorY + ry,
|
||||
baseZ + rz
|
||||
);
|
||||
boolean isWall = layout.isWall(rx, rz);
|
||||
boolean isFloor = ry == 0;
|
||||
boolean isCeiling = ry == 11;
|
||||
|
||||
if (isFloor) {
|
||||
if (isWall) {
|
||||
safeSetBlock(
|
||||
level,
|
||||
pos,
|
||||
theme.wallShellBlock(),
|
||||
chunkBB
|
||||
);
|
||||
} else {
|
||||
boolean isEdge = layout.isWallAdjacent(rx, rz);
|
||||
safeSetBlock(
|
||||
level,
|
||||
pos,
|
||||
theme.floorBlock(random, rx, rz, isEdge),
|
||||
chunkBB
|
||||
);
|
||||
}
|
||||
} else if (isCeiling) {
|
||||
safeSetBlock(
|
||||
level,
|
||||
pos,
|
||||
theme.ceilingBlock(random),
|
||||
chunkBB
|
||||
);
|
||||
} else if (isWall) {
|
||||
safeSetBlock(
|
||||
level,
|
||||
pos,
|
||||
theme.wallBlock(random, ry),
|
||||
chunkBB
|
||||
);
|
||||
} else {
|
||||
safeSetBlock(
|
||||
level,
|
||||
pos,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
chunkBB
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Theme-specific decorations
|
||||
theme.placeDecorations(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
);
|
||||
|
||||
// Phase 2b: Shared structural features
|
||||
RoomTheme.placeSharedPillars(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedFloorScatter(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedCeilingDecor(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedWallLighting(
|
||||
level,
|
||||
random,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
chunkBB
|
||||
);
|
||||
RoomTheme.placeSharedWallBands(
|
||||
level,
|
||||
baseX,
|
||||
baseZ,
|
||||
floorY,
|
||||
layout,
|
||||
theme,
|
||||
chunkBB
|
||||
);
|
||||
|
||||
// Phase 2c: Chests
|
||||
placeVanillaChest(level, random, baseX, baseZ, floorY, layout, chunkBB);
|
||||
placeTrappedChest(level, random, baseX, baseZ, floorY, layout, chunkBB);
|
||||
|
||||
// Phase 3: Iron bars from ry=7 to ry=10 at center [6,6]
|
||||
for (int ry = 7; ry <= 10; ry++) {
|
||||
safeSetBlock(
|
||||
level,
|
||||
new BlockPos(centerX, floorY + ry, centerZ),
|
||||
Blocks.IRON_BARS.defaultBlockState(),
|
||||
chunkBB
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 4: Pet Cage at [6,6] ry=5
|
||||
BlockPos masterPos = new BlockPos(centerX, floorY + 5, centerZ);
|
||||
|
||||
BlockState masterState = V2Blocks.PET_CAGE.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetCageBlock.FACING, facing);
|
||||
safeSetBlock(level, masterPos, masterState, chunkBB);
|
||||
|
||||
BlockState partState = V2Blocks.PET_CAGE_PART.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetCagePartBlock.FACING, facing);
|
||||
for (BlockPos partPos : PetCageBlock.getPartPositions(
|
||||
masterPos,
|
||||
facing
|
||||
)) {
|
||||
safeSetBlock(level, partPos, partState, chunkBB);
|
||||
}
|
||||
|
||||
if (chunkBB.isInside(masterPos)) {
|
||||
scheduleDamselSpawn(level, masterPos, random);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[HangingCage] Placed cage in carved room (theme={}, layout={}) at {}",
|
||||
theme.name(),
|
||||
layout.name(),
|
||||
masterPos.toShortString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a vanilla dungeon chest in the 3rd inner corner.
|
||||
* Uses BuiltInLootTables.SIMPLE_DUNGEON for standard dungeon loot.
|
||||
*/
|
||||
private void placeVanillaChest(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
int[][] corners = layout.innerCorners();
|
||||
if (corners.length < 3) return;
|
||||
int[] c = corners[2];
|
||||
if (!layout.isInShape(c[0], c[1]) || layout.isWall(c[0], c[1])) return;
|
||||
|
||||
BlockPos chestPos = new BlockPos(
|
||||
baseX + c[0],
|
||||
floorY + 1,
|
||||
baseZ + c[1]
|
||||
);
|
||||
if (!chunkBB.isInside(chestPos)) return;
|
||||
|
||||
// Face chest toward room center
|
||||
Direction chestFacing;
|
||||
if (c[0] < 6) chestFacing = Direction.EAST;
|
||||
else if (c[0] > 6) chestFacing = Direction.WEST;
|
||||
else if (c[1] < 6) chestFacing = Direction.SOUTH;
|
||||
else chestFacing = Direction.NORTH;
|
||||
|
||||
BlockState chestState = Blocks.CHEST.defaultBlockState().setValue(
|
||||
ChestBlock.FACING,
|
||||
chestFacing
|
||||
);
|
||||
level.setBlock(chestPos, chestState, 2);
|
||||
|
||||
BlockEntity be = level.getBlockEntity(chestPos);
|
||||
if (be instanceof RandomizableContainerBlockEntity container) {
|
||||
container.setLootTable(
|
||||
BuiltInLootTables.SIMPLE_DUNGEON,
|
||||
random.nextLong()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a TiedUp trapped chest in the 4th inner corner.
|
||||
* Filled with random bondage items (bind, gag, blindfold).
|
||||
*/
|
||||
private void placeTrappedChest(
|
||||
WorldGenLevel level,
|
||||
RandomSource random,
|
||||
int baseX,
|
||||
int baseZ,
|
||||
int floorY,
|
||||
RoomLayout layout,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
int[][] corners = layout.innerCorners();
|
||||
if (corners.length < 4) return;
|
||||
int[] c = corners[3];
|
||||
if (!layout.isInShape(c[0], c[1]) || layout.isWall(c[0], c[1])) return;
|
||||
|
||||
BlockPos chestPos = new BlockPos(
|
||||
baseX + c[0],
|
||||
floorY + 1,
|
||||
baseZ + c[1]
|
||||
);
|
||||
if (!chunkBB.isInside(chestPos)) return;
|
||||
|
||||
// Face chest toward room center
|
||||
Direction chestFacing;
|
||||
if (c[0] < 6) chestFacing = Direction.EAST;
|
||||
else if (c[0] > 6) chestFacing = Direction.WEST;
|
||||
else if (c[1] < 6) chestFacing = Direction.SOUTH;
|
||||
else chestFacing = Direction.NORTH;
|
||||
|
||||
BlockState chestState = ModBlocks.TRAPPED_CHEST.get()
|
||||
.defaultBlockState()
|
||||
.setValue(ChestBlock.FACING, chestFacing);
|
||||
level.setBlock(chestPos, chestState, 2);
|
||||
|
||||
BlockEntity be = level.getBlockEntity(chestPos);
|
||||
if (be instanceof TrappedChestBlockEntity trappedChest) {
|
||||
// Random bind
|
||||
BindVariant[] bindVariants = BindVariant.values();
|
||||
BindVariant chosenBind = bindVariants[random.nextInt(
|
||||
bindVariants.length
|
||||
)];
|
||||
ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
|
||||
trappedChest.setBind(bindStack);
|
||||
|
||||
// Random gag (50% chance)
|
||||
if (random.nextFloat() < 0.50f) {
|
||||
GagVariant[] gagVariants = GagVariant.values();
|
||||
GagVariant chosenGag = gagVariants[random.nextInt(
|
||||
gagVariants.length
|
||||
)];
|
||||
ItemStack gagStack = new ItemStack(ModItems.getGag(chosenGag));
|
||||
trappedChest.setGag(gagStack);
|
||||
}
|
||||
|
||||
// Random blindfold (30% chance)
|
||||
if (random.nextFloat() < 0.30f) {
|
||||
BlindfoldVariant[] bfVariants = BlindfoldVariant.values();
|
||||
BlindfoldVariant chosenBf = bfVariants[random.nextInt(
|
||||
bfVariants.length
|
||||
)];
|
||||
ItemStack bfStack = new ItemStack(
|
||||
ModItems.getBlindfold(chosenBf)
|
||||
);
|
||||
trappedChest.setBlindfold(bfStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void safeSetBlock(
|
||||
WorldGenLevel level,
|
||||
BlockPos pos,
|
||||
BlockState state,
|
||||
BoundingBox chunkBB
|
||||
) {
|
||||
if (chunkBB.isInside(pos)) {
|
||||
level.setBlock(pos, state, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write damsel entity NBT directly into the ProtoChunk, avoiding entity
|
||||
* construction on the worker thread (which deadlocks due to PlayerAnimator).
|
||||
* The entity will be created naturally when the chunk is promoted to a LevelChunk.
|
||||
*/
|
||||
private void scheduleDamselSpawn(
|
||||
WorldGenLevel level,
|
||||
BlockPos masterPos,
|
||||
RandomSource random
|
||||
) {
|
||||
boolean shiny = random.nextFloat() < 0.25f;
|
||||
String entityId = shiny
|
||||
? TiedUpMod.MOD_ID + ":damsel_shiny"
|
||||
: TiedUpMod.MOD_ID + ":damsel";
|
||||
|
||||
CompoundTag entityTag = new CompoundTag();
|
||||
entityTag.putString("id", entityId);
|
||||
|
||||
// Position: +1Y to be inside cage (above the thin 2px cage floor)
|
||||
ListTag posList = new ListTag();
|
||||
posList.add(DoubleTag.valueOf(masterPos.getX() + 0.5));
|
||||
posList.add(DoubleTag.valueOf(masterPos.getY() + 1.0));
|
||||
posList.add(DoubleTag.valueOf(masterPos.getZ() + 0.5));
|
||||
entityTag.put("Pos", posList);
|
||||
|
||||
// Motion (stationary)
|
||||
ListTag motionList = new ListTag();
|
||||
motionList.add(DoubleTag.valueOf(0.0));
|
||||
motionList.add(DoubleTag.valueOf(0.0));
|
||||
motionList.add(DoubleTag.valueOf(0.0));
|
||||
entityTag.put("Motion", motionList);
|
||||
|
||||
// Rotation (random yaw)
|
||||
ListTag rotList = new ListTag();
|
||||
rotList.add(FloatTag.valueOf(random.nextFloat() * 360F));
|
||||
rotList.add(FloatTag.valueOf(0.0F));
|
||||
entityTag.put("Rotation", rotList);
|
||||
|
||||
// Persistence + prevent fall death
|
||||
entityTag.putBoolean("PersistenceRequired", true);
|
||||
entityTag.putBoolean("OnGround", true);
|
||||
entityTag.putFloat("FallDistance", 0.0F);
|
||||
entityTag.putFloat("AbsorptionAmount", 20.0F);
|
||||
entityTag.putUUID("UUID", java.util.UUID.randomUUID());
|
||||
|
||||
// Random bind item — the damsel spawns already restrained
|
||||
BindVariant[] variants = BindVariant.values();
|
||||
BindVariant chosenBind = variants[random.nextInt(variants.length)];
|
||||
ItemStack bindStack = new ItemStack(ModItems.getBind(chosenBind));
|
||||
bindStack.getOrCreateTag().putString("bindMode", "full");
|
||||
entityTag.put("Bind", bindStack.save(new CompoundTag()));
|
||||
|
||||
// Add directly to chunk's pending entity list
|
||||
ChunkAccess chunk = level.getChunk(masterPos);
|
||||
if (chunk instanceof ProtoChunk protoChunk) {
|
||||
protoChunk.addEntity(entityTag);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[HangingCage] Scheduled {} damsel with {} at {}",
|
||||
shiny ? "shiny" : "regular",
|
||||
chosenBind.getRegistryName(),
|
||||
masterPos.toShortString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
|
||||
|
||||
/**
|
||||
* Hanging Cage Structure - Underground cage suspended from cave ceiling.
|
||||
*
|
||||
* Places a Pet Cage hanging from iron bars in a cave, with a Damsel inside.
|
||||
* 25% chance of shiny damsel variant.
|
||||
* The cage must be at least 4 blocks above the cave floor.
|
||||
*/
|
||||
public class HangingCageStructure extends Structure {
|
||||
|
||||
public static final Codec<HangingCageStructure> CODEC =
|
||||
RecordCodecBuilder.create(instance ->
|
||||
instance
|
||||
.group(settingsCodec(instance))
|
||||
.apply(instance, HangingCageStructure::new)
|
||||
);
|
||||
|
||||
public HangingCageStructure(StructureSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GenerationStub> findGenerationPoint(
|
||||
GenerationContext context
|
||||
) {
|
||||
// Choose a random Y between -20 and 40 for cave search
|
||||
int y = context.random().nextIntBetweenInclusive(-20, 40);
|
||||
|
||||
// Center of the chunk
|
||||
BlockPos pos = new BlockPos(
|
||||
context.chunkPos().getMiddleBlockX(),
|
||||
y,
|
||||
context.chunkPos().getMiddleBlockZ()
|
||||
);
|
||||
|
||||
return Optional.of(
|
||||
new GenerationStub(pos, builder -> {
|
||||
generatePieces(builder, context, pos);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void generatePieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos pos
|
||||
) {
|
||||
builder.addPiece(new HangingCagePiece(pos, context.random()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureType<?> type() {
|
||||
return ModStructures.HANGING_CAGE.get();
|
||||
}
|
||||
}
|
||||
399
src/main/java/com/tiedup/remake/worldgen/KidnapperCampPiece.java
Normal file
399
src/main/java/com/tiedup/remake/worldgen/KidnapperCampPiece.java
Normal file
@@ -0,0 +1,399 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.tiedup.remake.cells.CampOwnership;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityKidnapperElite;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.entities.ModEntities;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.MobSpawnType;
|
||||
import net.minecraft.world.level.ServerLevelAccessor;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.TemplateStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/**
|
||||
* Structure piece for kidnapper camp tents.
|
||||
*
|
||||
* Phase: Kidnapper Revamp - Cell System
|
||||
*
|
||||
* Handles data markers:
|
||||
* - "kidnapper" - Spawns a regular kidnapper
|
||||
* - "kidnapper_elite" - Spawns an elite kidnapper
|
||||
* - "slave_trader" - Spawns a slave trader (brain of the camp)
|
||||
* - "maid" - Spawns a maid (linked to trader)
|
||||
* - "loot" - Places a loot chest
|
||||
*/
|
||||
public class KidnapperCampPiece extends TemplateStructurePiece {
|
||||
|
||||
/** Temporary storage for trader UUID during structure generation */
|
||||
@Nullable
|
||||
private UUID spawnedTraderUUID;
|
||||
|
||||
/** Temporary storage for camp data during structure generation */
|
||||
@Nullable
|
||||
private CampOwnership.CampData currentCamp;
|
||||
|
||||
public KidnapperCampPiece(
|
||||
StructureTemplateManager templateManager,
|
||||
ResourceLocation templateLocation,
|
||||
BlockPos pos,
|
||||
Rotation rotation
|
||||
) {
|
||||
super(
|
||||
ModStructures.CAMP_PIECE.get(),
|
||||
0,
|
||||
templateManager,
|
||||
templateLocation,
|
||||
templateLocation.toString(),
|
||||
makeSettings(rotation),
|
||||
pos
|
||||
);
|
||||
}
|
||||
|
||||
public KidnapperCampPiece(
|
||||
StructureTemplateManager templateManager,
|
||||
CompoundTag tag
|
||||
) {
|
||||
super(ModStructures.CAMP_PIECE.get(), tag, templateManager, location ->
|
||||
makeSettings(Rotation.NONE)
|
||||
);
|
||||
}
|
||||
|
||||
private static StructurePlaceSettings makeSettings(Rotation rotation) {
|
||||
return new StructurePlaceSettings()
|
||||
.setRotation(rotation)
|
||||
.setMirror(Mirror.NONE)
|
||||
.setRotationPivot(BlockPos.ZERO)
|
||||
.addProcessor(BlockIgnoreProcessor.STRUCTURE_BLOCK)
|
||||
.addProcessor(new MarkerProcessor());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleDataMarker(
|
||||
String marker,
|
||||
BlockPos pos,
|
||||
ServerLevelAccessor level,
|
||||
RandomSource random,
|
||||
BoundingBox box
|
||||
) {
|
||||
// Handle data markers from structure blocks
|
||||
// Data markers are placed using jigsaw blocks or structure blocks with "Data" mode
|
||||
|
||||
if (!(level instanceof ServerLevel serverLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (marker.toLowerCase()) {
|
||||
case "kidnapper" -> {
|
||||
// Spawn a kidnapper at this position
|
||||
spawnKidnapper(serverLevel, pos, random, false);
|
||||
}
|
||||
case "kidnapper_elite" -> {
|
||||
// Spawn elite kidnapper at this position
|
||||
spawnKidnapper(serverLevel, pos, random, true);
|
||||
}
|
||||
case "slave_trader" -> {
|
||||
// Spawn slave trader (brain of camp)
|
||||
spawnSlaveTrader(serverLevel, pos, random);
|
||||
}
|
||||
case "maid" -> {
|
||||
// Spawn maid (linked to trader)
|
||||
spawnMaid(serverLevel, pos, random);
|
||||
}
|
||||
case "loot" -> {
|
||||
// TODO: Place loot chest
|
||||
}
|
||||
default -> {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[KidnapperCampPiece] Unknown data marker: {} at {}",
|
||||
marker,
|
||||
pos.toShortString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a kidnapper at the given position.
|
||||
*
|
||||
* @param level The server level
|
||||
* @param pos Position to spawn at
|
||||
* @param random Random source
|
||||
* @param elite Whether to spawn an elite kidnapper
|
||||
*/
|
||||
private void spawnKidnapper(
|
||||
ServerLevel level,
|
||||
BlockPos pos,
|
||||
RandomSource random,
|
||||
boolean elite
|
||||
) {
|
||||
EntityKidnapper kidnapper;
|
||||
|
||||
if (elite) {
|
||||
kidnapper = ModEntities.KIDNAPPER_ELITE.get().create(level);
|
||||
} else {
|
||||
kidnapper = ModEntities.KIDNAPPER.get().create(level);
|
||||
}
|
||||
|
||||
if (kidnapper != null) {
|
||||
kidnapper.moveTo(
|
||||
pos.getX() + 0.5,
|
||||
pos.getY(),
|
||||
pos.getZ() + 0.5,
|
||||
random.nextFloat() * 360F,
|
||||
0.0F
|
||||
);
|
||||
kidnapper.finalizeSpawn(
|
||||
level,
|
||||
level.getCurrentDifficultyAt(pos),
|
||||
MobSpawnType.STRUCTURE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
level.addFreshEntity(kidnapper);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperCampPiece] Spawned {} at {} from data marker",
|
||||
elite ? "elite kidnapper" : "kidnapper",
|
||||
pos.toShortString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a slave trader at the given position.
|
||||
* Creates/updates the camp data in CampOwnership.
|
||||
*
|
||||
* @param level The server level
|
||||
* @param pos Position to spawn at
|
||||
* @param random Random source
|
||||
*/
|
||||
private void spawnSlaveTrader(
|
||||
ServerLevel level,
|
||||
BlockPos pos,
|
||||
RandomSource random
|
||||
) {
|
||||
EntitySlaveTrader trader = ModEntities.SLAVE_TRADER.get().create(level);
|
||||
if (trader == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[KidnapperCampPiece] Failed to create SlaveTrader entity"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
trader.moveTo(
|
||||
pos.getX() + 0.5,
|
||||
pos.getY(),
|
||||
pos.getZ() + 0.5,
|
||||
random.nextFloat() * 360F,
|
||||
0.0F
|
||||
);
|
||||
trader.finalizeSpawn(
|
||||
level,
|
||||
level.getCurrentDifficultyAt(pos),
|
||||
MobSpawnType.STRUCTURE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// Create or get the camp in CampOwnership registry
|
||||
CampOwnership registry = CampOwnership.get(level);
|
||||
CampOwnership.CampData camp = registry.findNearestAliveCamp(pos, 50);
|
||||
|
||||
if (camp == null) {
|
||||
// Create a new camp
|
||||
UUID campId = UUID.randomUUID();
|
||||
registry.registerCamp(campId, trader.getUUID(), null, pos);
|
||||
camp = registry.getCamp(campId);
|
||||
} else {
|
||||
// Update existing camp with this trader
|
||||
camp.setTraderUUID(trader.getUUID());
|
||||
camp.setCenter(pos);
|
||||
registry.setDirty();
|
||||
}
|
||||
|
||||
// Link trader to camp
|
||||
if (camp != null) {
|
||||
trader.setCampUUID(camp.getCampId());
|
||||
this.currentCamp = camp;
|
||||
}
|
||||
|
||||
// Store for maid linking
|
||||
this.spawnedTraderUUID = trader.getUUID();
|
||||
|
||||
level.addFreshEntity(trader);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperCampPiece] Spawned slave trader {} at {} from data marker, camp={}",
|
||||
trader.getNpcName(),
|
||||
pos.toShortString(),
|
||||
camp != null ? camp.getCampId().toString().substring(0, 8) : "null"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a maid at the given position.
|
||||
* Must be called AFTER trader spawns to establish link.
|
||||
*
|
||||
* @param level The server level
|
||||
* @param pos Position to spawn at
|
||||
* @param random Random source
|
||||
*/
|
||||
private void spawnMaid(
|
||||
ServerLevel level,
|
||||
BlockPos pos,
|
||||
RandomSource random
|
||||
) {
|
||||
EntityMaid maid = ModEntities.MAID.get().create(level);
|
||||
if (maid == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[KidnapperCampPiece] Failed to create Maid entity"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
maid.moveTo(
|
||||
pos.getX() + 0.5,
|
||||
pos.getY(),
|
||||
pos.getZ() + 0.5,
|
||||
random.nextFloat() * 360F,
|
||||
0.0F
|
||||
);
|
||||
maid.finalizeSpawn(
|
||||
level,
|
||||
level.getCurrentDifficultyAt(pos),
|
||||
MobSpawnType.STRUCTURE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// Link to trader (must be spawned after trader)
|
||||
boolean linkedSuccessfully = false;
|
||||
|
||||
if (this.spawnedTraderUUID != null) {
|
||||
maid.setMasterTraderUUID(this.spawnedTraderUUID);
|
||||
|
||||
// Find the trader entity and update its maid reference
|
||||
Entity traderEntity = level.getEntity(this.spawnedTraderUUID);
|
||||
if (traderEntity instanceof EntitySlaveTrader trader) {
|
||||
trader.setMaidUUID(maid.getUUID());
|
||||
linkedSuccessfully = true;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[KidnapperCampPiece] Linked maid {} to trader {}",
|
||||
maid.getNpcName(),
|
||||
trader.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// Link maid to camp
|
||||
if (this.currentCamp != null) {
|
||||
this.currentCamp.setMaidUUID(maid.getUUID());
|
||||
CampOwnership.get(level).setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: If no trader link established, search for nearby trader/camp
|
||||
if (!linkedSuccessfully) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[KidnapperCampPiece] Maid at {} - no direct trader link, searching nearby...",
|
||||
pos.toShortString()
|
||||
);
|
||||
|
||||
// Try to find a nearby camp first
|
||||
CampOwnership registry = CampOwnership.get(level);
|
||||
CampOwnership.CampData camp = registry.findNearestAliveCamp(
|
||||
pos,
|
||||
50
|
||||
);
|
||||
|
||||
if (camp != null && camp.getTraderUUID() != null) {
|
||||
// Found a camp with a trader - link to it
|
||||
maid.setMasterTraderUUID(camp.getTraderUUID());
|
||||
camp.setMaidUUID(maid.getUUID());
|
||||
registry.setDirty();
|
||||
|
||||
// Try to update the trader entity too
|
||||
Entity traderEntity = level.getEntity(camp.getTraderUUID());
|
||||
if (traderEntity instanceof EntitySlaveTrader trader) {
|
||||
trader.setMaidUUID(maid.getUUID());
|
||||
linkedSuccessfully = true;
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperCampPiece] Maid {} linked to trader {} via camp fallback",
|
||||
maid.getNpcName(),
|
||||
trader.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: search for nearby trader entities
|
||||
if (!linkedSuccessfully) {
|
||||
java.util.List<EntitySlaveTrader> nearbyTraders =
|
||||
level.getEntitiesOfClass(
|
||||
EntitySlaveTrader.class,
|
||||
maid.getBoundingBox().inflate(50)
|
||||
);
|
||||
|
||||
if (!nearbyTraders.isEmpty()) {
|
||||
EntitySlaveTrader trader = nearbyTraders.get(0);
|
||||
maid.setMasterTraderUUID(trader.getUUID());
|
||||
trader.setMaidUUID(maid.getUUID());
|
||||
linkedSuccessfully = true;
|
||||
|
||||
// Update camp if trader has one
|
||||
if (trader.getCampUUID() != null) {
|
||||
CampOwnership.CampData traderCamp = registry.getCamp(
|
||||
trader.getCampUUID()
|
||||
);
|
||||
if (traderCamp != null) {
|
||||
traderCamp.setMaidUUID(maid.getUUID());
|
||||
registry.setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperCampPiece] Maid {} linked to nearby trader {} via entity search fallback",
|
||||
maid.getNpcName(),
|
||||
trader.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedSuccessfully) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[KidnapperCampPiece] Maid spawned at {} but no trader found! Maid will be orphaned.",
|
||||
pos.toShortString()
|
||||
);
|
||||
}
|
||||
|
||||
level.addFreshEntity(maid);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperCampPiece] Spawned maid {} at {} from data marker, linked to trader={}",
|
||||
maid.getNpcName(),
|
||||
pos.toShortString(),
|
||||
this.spawnedTraderUUID != null
|
||||
? this.spawnedTraderUUID.toString().substring(0, 8)
|
||||
: "null"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
|
||||
|
||||
/**
|
||||
* Custom structure that places kidnapper camp tents together.
|
||||
*
|
||||
* Phase: Kidnapper Revamp - Cell System
|
||||
*
|
||||
* Places three tents:
|
||||
* - kidnap_tent (with spawner and loot)
|
||||
* - cell_tent (prisoner cell)
|
||||
* - kidnap_tent_trader (with trader and maid spawn)
|
||||
*/
|
||||
public class KidnapperCampStructure extends AbstractKidnapperStructure {
|
||||
|
||||
public static final Codec<KidnapperCampStructure> CODEC =
|
||||
RecordCodecBuilder.create(instance ->
|
||||
instance
|
||||
.group(settingsCodec(instance))
|
||||
.apply(instance, KidnapperCampStructure::new)
|
||||
);
|
||||
|
||||
public KidnapperCampStructure(StructureSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getCheckRadius() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMaxHeightDifference() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void generatePieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
Rotation rotation
|
||||
) {
|
||||
// Calculate offsets for tent placement (triangular layout)
|
||||
int tentOffset = 10;
|
||||
boolean xFirst = context.random().nextBoolean();
|
||||
|
||||
// Cell tent offset
|
||||
int cellOffsetX = xFirst ? tentOffset : 0;
|
||||
int cellOffsetZ = xFirst ? 0 : tentOffset;
|
||||
|
||||
// Trader tent offset (opposite side or diagonal)
|
||||
int traderOffsetX = xFirst ? 0 : tentOffset;
|
||||
int traderOffsetZ = xFirst ? tentOffset : 0;
|
||||
|
||||
// Place kidnap_tent at center
|
||||
BlockPos kidnapTentPos = calculatePiecePosition(
|
||||
context,
|
||||
centerPos,
|
||||
0,
|
||||
0,
|
||||
rotation
|
||||
);
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "kidnap_tent"),
|
||||
kidnapTentPos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
|
||||
// Place cell_tent with offset
|
||||
BlockPos cellTentPos = calculatePiecePosition(
|
||||
context,
|
||||
centerPos,
|
||||
cellOffsetX,
|
||||
cellOffsetZ,
|
||||
rotation
|
||||
);
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "cell_tent"),
|
||||
cellTentPos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
|
||||
// Place kidnap_tent_trader with offset (trader and maid spawn)
|
||||
BlockPos traderTentPos = calculatePiecePosition(
|
||||
context,
|
||||
centerPos,
|
||||
traderOffsetX,
|
||||
traderOffsetZ,
|
||||
rotation
|
||||
);
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"kidnap_tent_trader"
|
||||
),
|
||||
traderTentPos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureType<?> type() {
|
||||
return ModStructures.KIDNAPPER_CAMP.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
|
||||
|
||||
/**
|
||||
* Kidnapper Fortress - Large, rare structure.
|
||||
*
|
||||
* Phase 4: Kidnapper Revamp - Varied Structures
|
||||
*
|
||||
* The largest kidnapper structure type.
|
||||
* Contains:
|
||||
* - Main fortress building (kidnap_fortress) with elite kidnapper boss, cells, etc.
|
||||
*
|
||||
* Very rare spawn, contains valuable loot and dangerous enemies.
|
||||
* Structure has 9 blocks underground, so Y offset is -9.
|
||||
* Only spawns on flat terrain (max 3 blocks height difference).
|
||||
*/
|
||||
public class KidnapperFortressStructure extends AbstractKidnapperStructure {
|
||||
|
||||
public static final Codec<KidnapperFortressStructure> CODEC =
|
||||
RecordCodecBuilder.create(instance ->
|
||||
instance
|
||||
.group(settingsCodec(instance))
|
||||
.apply(instance, KidnapperFortressStructure::new)
|
||||
);
|
||||
|
||||
public KidnapperFortressStructure(StructureSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getCheckRadius() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMaxHeightDifference() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getYOffset() {
|
||||
return -9;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void generatePieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
Rotation rotation
|
||||
) {
|
||||
// Calculate fortress position with custom Y offset (9 blocks underground)
|
||||
int y =
|
||||
context
|
||||
.chunkGenerator()
|
||||
.getFirstOccupiedHeight(
|
||||
centerPos.getX(),
|
||||
centerPos.getZ(),
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
context.heightAccessor(),
|
||||
context.randomState()
|
||||
) +
|
||||
getYOffset();
|
||||
|
||||
BlockPos fortressPos = new BlockPos(
|
||||
centerPos.getX(),
|
||||
y,
|
||||
centerPos.getZ()
|
||||
);
|
||||
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"kidnap_fortress"
|
||||
),
|
||||
fortressPos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureType<?> type() {
|
||||
return ModStructures.KIDNAPPER_FORTRESS.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
|
||||
|
||||
/**
|
||||
* Kidnapper Outpost - Medium-sized structure.
|
||||
*
|
||||
* Phase 4: Kidnapper Revamp - Varied Structures
|
||||
*
|
||||
* Single-piece structure that spawns on flat terrain (like fortress).
|
||||
* Uses kidnap_outpost.nbt template.
|
||||
*
|
||||
* Spawns in plains, forests, and other flat biomes.
|
||||
* Structure is lowered by 2 blocks (default Y_OFFSET).
|
||||
*/
|
||||
public class KidnapperOutpostStructure extends AbstractKidnapperStructure {
|
||||
|
||||
public static final Codec<KidnapperOutpostStructure> CODEC =
|
||||
RecordCodecBuilder.create(instance ->
|
||||
instance
|
||||
.group(settingsCodec(instance))
|
||||
.apply(instance, KidnapperOutpostStructure::new)
|
||||
);
|
||||
|
||||
public KidnapperOutpostStructure(StructureSettings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getCheckRadius() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMaxHeightDifference() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void generatePieces(
|
||||
StructurePiecesBuilder builder,
|
||||
GenerationContext context,
|
||||
BlockPos centerPos,
|
||||
Rotation rotation
|
||||
) {
|
||||
// Single-piece structure: kidnap_outpost.nbt
|
||||
// Y offset of -2 is applied via calculatePiecePosition
|
||||
BlockPos outpostPos = calculatePiecePosition(
|
||||
context,
|
||||
centerPos,
|
||||
0,
|
||||
0,
|
||||
rotation
|
||||
);
|
||||
builder.addPiece(
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"kidnap_outpost"
|
||||
),
|
||||
outpostPos,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureType<?> type() {
|
||||
return ModStructures.KIDNAPPER_OUTPOST.get();
|
||||
}
|
||||
}
|
||||
238
src/main/java/com/tiedup/remake/worldgen/MarkerProcessor.java
Normal file
238
src/main/java/com/tiedup/remake/worldgen/MarkerProcessor.java
Normal file
@@ -0,0 +1,238 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.tiedup.remake.blocks.ModBlocks;
|
||||
import com.tiedup.remake.cells.MarkerType;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Structure processor that handles MarkerBlockEntity and CellCoreBlockEntity
|
||||
* when structures are placed.
|
||||
*
|
||||
* Phase: Kidnapper Revamp - Cell System
|
||||
*
|
||||
* When a structure containing cell markers or Cell Cores is placed:
|
||||
* 1. Remaps cellId UUIDs to fresh ones (prevents collisions between structures)
|
||||
* 2. Clears cellIds from structure markers (ENTRANCE, PATROL, LOOT, SPAWNER)
|
||||
* 3. Handles Cell Core blocks: remaps cellId, rotates spawnPoint/deliveryPoint offsets
|
||||
*/
|
||||
public class MarkerProcessor extends StructureProcessor {
|
||||
|
||||
public static final Codec<MarkerProcessor> CODEC = Codec.unit(
|
||||
MarkerProcessor::new
|
||||
);
|
||||
|
||||
/**
|
||||
* Track cell ID mappings during structure placement.
|
||||
* Old UUID -> New UUID.
|
||||
*
|
||||
* Made instance-based instead of static ThreadLocal to prevent memory leaks.
|
||||
* Each structure placement gets its own MarkerProcessor instance, so mappings
|
||||
* are naturally scoped to a single structure and garbage collected when done.
|
||||
*/
|
||||
private final Map<UUID, UUID> cellIdMappings = new HashMap<>();
|
||||
|
||||
public MarkerProcessor() {}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public StructureTemplate.StructureBlockInfo processBlock(
|
||||
LevelReader level,
|
||||
BlockPos offset,
|
||||
BlockPos pos,
|
||||
StructureTemplate.StructureBlockInfo blockInfo,
|
||||
StructureTemplate.StructureBlockInfo relativeBlockInfo,
|
||||
StructurePlaceSettings settings
|
||||
) {
|
||||
// Process Cell Core blocks (V2)
|
||||
if (relativeBlockInfo.state().is(ModBlocks.CELL_CORE.get())) {
|
||||
return processCellCore(relativeBlockInfo, settings);
|
||||
}
|
||||
|
||||
// Process marker blocks (V1 + structure markers)
|
||||
if (!relativeBlockInfo.state().is(ModBlocks.MARKER.get())) {
|
||||
return relativeBlockInfo;
|
||||
}
|
||||
|
||||
CompoundTag nbt = relativeBlockInfo.nbt();
|
||||
if (nbt == null) {
|
||||
return relativeBlockInfo;
|
||||
}
|
||||
|
||||
// Get the marker type
|
||||
MarkerType markerType = MarkerType.WALL;
|
||||
if (nbt.contains("markerType")) {
|
||||
markerType = MarkerType.fromString(nbt.getString("markerType"));
|
||||
}
|
||||
|
||||
// Structure markers don't need cell IDs - just clear any old one
|
||||
if (markerType.isStructureMarker()) {
|
||||
CompoundTag newNbt = nbt.copy();
|
||||
newNbt.remove("cellId");
|
||||
return new StructureTemplate.StructureBlockInfo(
|
||||
relativeBlockInfo.pos(),
|
||||
relativeBlockInfo.state(),
|
||||
newNbt
|
||||
);
|
||||
}
|
||||
|
||||
// Cell markers need special handling
|
||||
// If this marker has a cellId, we need to map it to a new one
|
||||
if (nbt.contains("cellId")) {
|
||||
UUID oldCellId = nbt.getUUID("cellId");
|
||||
|
||||
UUID newCellId;
|
||||
if (cellIdMappings.containsKey(oldCellId)) {
|
||||
// Use existing mapping
|
||||
newCellId = cellIdMappings.get(oldCellId);
|
||||
} else {
|
||||
// Create new cell ID (actual cell will be created when block entity loads)
|
||||
newCellId = UUID.randomUUID();
|
||||
cellIdMappings.put(oldCellId, newCellId);
|
||||
}
|
||||
|
||||
// Update the NBT with new cell ID
|
||||
CompoundTag newNbt = nbt.copy();
|
||||
newNbt.putUUID("cellId", newCellId);
|
||||
|
||||
// Rotate cell positions if present (important for structure rotation)
|
||||
if (newNbt.contains("cellPositions")) {
|
||||
CompoundTag cellPositions = newNbt.getCompound("cellPositions");
|
||||
CompoundTag rotatedPositions = new CompoundTag();
|
||||
|
||||
for (String key : cellPositions.getAllKeys()) {
|
||||
ListTag posList = cellPositions.getList(
|
||||
key,
|
||||
Tag.TAG_COMPOUND
|
||||
);
|
||||
ListTag rotatedList = new ListTag();
|
||||
|
||||
for (int i = 0; i < posList.size(); i++) {
|
||||
BlockPos posOffset = NbtUtils.readBlockPos(
|
||||
posList.getCompound(i)
|
||||
);
|
||||
BlockPos rotated = posOffset.rotate(
|
||||
settings.getRotation()
|
||||
);
|
||||
rotatedList.add(NbtUtils.writeBlockPos(rotated));
|
||||
}
|
||||
rotatedPositions.put(key, rotatedList);
|
||||
}
|
||||
newNbt.put("cellPositions", rotatedPositions);
|
||||
|
||||
com.tiedup.remake.core.TiedUpMod.LOGGER.info(
|
||||
"[MarkerProcessor] Rotated cellPositions for cell {}, rotation={}",
|
||||
newCellId.toString().substring(0, 8),
|
||||
settings.getRotation()
|
||||
);
|
||||
}
|
||||
|
||||
return new StructureTemplate.StructureBlockInfo(
|
||||
relativeBlockInfo.pos(),
|
||||
relativeBlockInfo.state(),
|
||||
newNbt
|
||||
);
|
||||
}
|
||||
|
||||
return relativeBlockInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Cell Core block from a structure template.
|
||||
* Remaps the cellId UUID and rotates relative position offsets.
|
||||
*/
|
||||
private StructureTemplate.StructureBlockInfo processCellCore(
|
||||
StructureTemplate.StructureBlockInfo blockInfo,
|
||||
StructurePlaceSettings settings
|
||||
) {
|
||||
CompoundTag nbt = blockInfo.nbt();
|
||||
if (nbt == null || !nbt.contains("cellId")) {
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
UUID oldCellId = nbt.getUUID("cellId");
|
||||
UUID newCellId;
|
||||
if (cellIdMappings.containsKey(oldCellId)) {
|
||||
newCellId = cellIdMappings.get(oldCellId);
|
||||
} else {
|
||||
newCellId = UUID.randomUUID();
|
||||
cellIdMappings.put(oldCellId, newCellId);
|
||||
}
|
||||
|
||||
CompoundTag newNbt = nbt.copy();
|
||||
newNbt.putUUID("cellId", newCellId);
|
||||
|
||||
// Remove old absolute position keys (from pre-offset saves)
|
||||
newNbt.remove("spawnPoint");
|
||||
newNbt.remove("deliveryPoint");
|
||||
|
||||
// Rotate relative offset positions for structure rotation
|
||||
rotateOffset(newNbt, "spawnOffset", settings);
|
||||
rotateOffset(newNbt, "deliveryOffset", settings);
|
||||
|
||||
// Rotate pathWaypoint offsets
|
||||
if (newNbt.contains("pathWaypointOffsets")) {
|
||||
ListTag list = newNbt.getList(
|
||||
"pathWaypointOffsets",
|
||||
Tag.TAG_COMPOUND
|
||||
);
|
||||
ListTag rotated = new ListTag();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
BlockPos offset = NbtUtils.readBlockPos(list.getCompound(i));
|
||||
rotated.add(
|
||||
NbtUtils.writeBlockPos(
|
||||
offset.rotate(settings.getRotation())
|
||||
)
|
||||
);
|
||||
}
|
||||
newNbt.put("pathWaypointOffsets", rotated);
|
||||
}
|
||||
|
||||
com.tiedup.remake.core.TiedUpMod.LOGGER.debug(
|
||||
"[MarkerProcessor] Remapped Cell Core cellId {} -> {} at {}, rotation={}",
|
||||
oldCellId.toString().substring(0, 8),
|
||||
newCellId.toString().substring(0, 8),
|
||||
blockInfo.pos().toShortString(),
|
||||
settings.getRotation()
|
||||
);
|
||||
|
||||
return new StructureTemplate.StructureBlockInfo(
|
||||
blockInfo.pos(),
|
||||
blockInfo.state(),
|
||||
newNbt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a single BlockPos offset stored in NBT for structure rotation.
|
||||
*/
|
||||
private static void rotateOffset(
|
||||
CompoundTag nbt,
|
||||
String key,
|
||||
StructurePlaceSettings settings
|
||||
) {
|
||||
if (nbt.contains(key)) {
|
||||
BlockPos offset = NbtUtils.readBlockPos(nbt.getCompound(key));
|
||||
BlockPos rotated = offset.rotate(settings.getRotation());
|
||||
nbt.put(key, NbtUtils.writeBlockPos(rotated));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StructureProcessorType<?> getType() {
|
||||
return ModProcessors.MARKER_PROCESSOR.get();
|
||||
}
|
||||
}
|
||||
28
src/main/java/com/tiedup/remake/worldgen/ModProcessors.java
Normal file
28
src/main/java/com/tiedup/remake/worldgen/ModProcessors.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
/**
|
||||
* Registry for custom structure processors.
|
||||
*
|
||||
* Phase: Kidnapper Revamp - Cell System
|
||||
*/
|
||||
public class ModProcessors {
|
||||
|
||||
public static final DeferredRegister<StructureProcessorType<?>> PROCESSORS =
|
||||
DeferredRegister.create(
|
||||
Registries.STRUCTURE_PROCESSOR,
|
||||
TiedUpMod.MOD_ID
|
||||
);
|
||||
|
||||
public static final RegistryObject<
|
||||
StructureProcessorType<MarkerProcessor>
|
||||
> MARKER_PROCESSOR = PROCESSORS.register(
|
||||
"marker_processor",
|
||||
() -> () -> MarkerProcessor.CODEC
|
||||
);
|
||||
}
|
||||
84
src/main/java/com/tiedup/remake/worldgen/ModStructures.java
Normal file
84
src/main/java/com/tiedup/remake/worldgen/ModStructures.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
import net.minecraftforge.registries.RegistryObject;
|
||||
|
||||
/**
|
||||
* Registry for custom structures.
|
||||
*
|
||||
* Phase: Kidnapper Revamp - Cell System
|
||||
* Phase 4: Added Outpost and Fortress structures
|
||||
*/
|
||||
public class ModStructures {
|
||||
|
||||
// Structure Types
|
||||
public static final DeferredRegister<StructureType<?>> STRUCTURE_TYPES =
|
||||
DeferredRegister.create(Registries.STRUCTURE_TYPE, TiedUpMod.MOD_ID);
|
||||
|
||||
// Structure Piece Types
|
||||
public static final DeferredRegister<
|
||||
StructurePieceType
|
||||
> STRUCTURE_PIECE_TYPES = DeferredRegister.create(
|
||||
Registries.STRUCTURE_PIECE,
|
||||
TiedUpMod.MOD_ID
|
||||
);
|
||||
|
||||
// === Structure Types ===
|
||||
|
||||
// Kidnapper Camp - Small structure (2 tents)
|
||||
public static final RegistryObject<
|
||||
StructureType<KidnapperCampStructure>
|
||||
> KIDNAPPER_CAMP = STRUCTURE_TYPES.register(
|
||||
"kidnapper_camp",
|
||||
() -> () -> KidnapperCampStructure.CODEC
|
||||
);
|
||||
|
||||
// Kidnapper Outpost - Medium structure (main building + 2-3 cells)
|
||||
public static final RegistryObject<
|
||||
StructureType<KidnapperOutpostStructure>
|
||||
> KIDNAPPER_OUTPOST = STRUCTURE_TYPES.register(
|
||||
"kidnapper_outpost",
|
||||
() -> () -> KidnapperOutpostStructure.CODEC
|
||||
);
|
||||
|
||||
// Kidnapper Fortress - Large structure (keep + 4-6 cells + corridors, with elite)
|
||||
public static final RegistryObject<
|
||||
StructureType<KidnapperFortressStructure>
|
||||
> KIDNAPPER_FORTRESS = STRUCTURE_TYPES.register(
|
||||
"kidnapper_fortress",
|
||||
() -> () -> KidnapperFortressStructure.CODEC
|
||||
);
|
||||
|
||||
// Hanging Cage - Underground cage suspended from cave ceiling
|
||||
public static final RegistryObject<
|
||||
StructureType<HangingCageStructure>
|
||||
> HANGING_CAGE = STRUCTURE_TYPES.register(
|
||||
"hanging_cage",
|
||||
() -> () -> HangingCageStructure.CODEC
|
||||
);
|
||||
|
||||
// === Structure Piece Types ===
|
||||
|
||||
// Shared piece type for all kidnapper structures
|
||||
public static final RegistryObject<StructurePieceType> CAMP_PIECE =
|
||||
STRUCTURE_PIECE_TYPES.register(
|
||||
"camp_piece",
|
||||
() ->
|
||||
(context, tag) ->
|
||||
new KidnapperCampPiece(
|
||||
context.structureTemplateManager(),
|
||||
tag
|
||||
)
|
||||
);
|
||||
|
||||
// Hanging cage piece (programmatic, no NBT template)
|
||||
public static final RegistryObject<StructurePieceType> HANGING_CAGE_PIECE =
|
||||
STRUCTURE_PIECE_TYPES.register(
|
||||
"hanging_cage_piece",
|
||||
() -> (context, tag) -> new HangingCagePiece(tag)
|
||||
);
|
||||
}
|
||||
94
src/main/java/com/tiedup/remake/worldgen/RoomLayout.java
Normal file
94
src/main/java/com/tiedup/remake/worldgen/RoomLayout.java
Normal file
@@ -0,0 +1,94 @@
|
||||
package com.tiedup.remake.worldgen;
|
||||
|
||||
enum RoomLayout {
|
||||
SQUARE {
|
||||
@Override
|
||||
public boolean isInShape(int rx, int rz) {
|
||||
return rx >= 0 && rx <= 12 && rz >= 0 && rz <= 12;
|
||||
}
|
||||
},
|
||||
OCTAGONAL {
|
||||
@Override
|
||||
public boolean isInShape(int rx, int rz) {
|
||||
if (rx < 0 || rx > 12 || rz < 0 || rz > 12) return false;
|
||||
if (rx + rz <= 2) return false;
|
||||
if ((12 - rx) + rz <= 2) return false;
|
||||
if (rx + (12 - rz) <= 2) return false;
|
||||
if ((12 - rx) + (12 - rz) <= 2) return false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
CIRCULAR {
|
||||
@Override
|
||||
public boolean isInShape(int rx, int rz) {
|
||||
if (rx < 0 || rx > 12 || rz < 0 || rz > 12) return false;
|
||||
int dx = rx - 6;
|
||||
int dz = rz - 6;
|
||||
return dx * dx + dz * dz <= 36;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[][] innerCorners() {
|
||||
return new int[][] { { 6, 1 }, { 6, 11 }, { 1, 6 }, { 11, 6 } };
|
||||
}
|
||||
},
|
||||
CROSS {
|
||||
@Override
|
||||
public boolean isInShape(int rx, int rz) {
|
||||
if (rx < 0 || rx > 12 || rz < 0 || rz > 12) return false;
|
||||
return (rx >= 3 && rx <= 9) || (rz >= 3 && rz <= 9);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[][] innerCorners() {
|
||||
return new int[][] { { 3, 3 }, { 3, 9 }, { 9, 3 }, { 9, 9 } };
|
||||
}
|
||||
};
|
||||
|
||||
public abstract boolean isInShape(int rx, int rz);
|
||||
|
||||
public boolean isWall(int rx, int rz) {
|
||||
if (!isInShape(rx, rz)) return false;
|
||||
return (
|
||||
!isInShape(rx - 1, rz) ||
|
||||
!isInShape(rx + 1, rz) ||
|
||||
!isInShape(rx, rz - 1) ||
|
||||
!isInShape(rx, rz + 1)
|
||||
);
|
||||
}
|
||||
|
||||
/** Is this interior position adjacent to a wall? */
|
||||
public boolean isWallAdjacent(int rx, int rz) {
|
||||
if (!isInShape(rx, rz) || isWall(rx, rz)) return false;
|
||||
return (
|
||||
isWall(rx - 1, rz) ||
|
||||
isWall(rx + 1, rz) ||
|
||||
isWall(rx, rz - 1) ||
|
||||
isWall(rx, rz + 1)
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns the 4 innermost corner positions for this layout. */
|
||||
public int[][] innerCorners() {
|
||||
return switch (this) {
|
||||
case SQUARE -> new int[][] {
|
||||
{ 1, 1 },
|
||||
{ 1, 11 },
|
||||
{ 11, 1 },
|
||||
{ 11, 11 },
|
||||
};
|
||||
case OCTAGONAL -> new int[][] {
|
||||
{ 3, 1 },
|
||||
{ 1, 9 },
|
||||
{ 11, 3 },
|
||||
{ 9, 11 },
|
||||
};
|
||||
default -> new int[][] {
|
||||
{ 1, 1 },
|
||||
{ 1, 11 },
|
||||
{ 11, 1 },
|
||||
{ 11, 11 },
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
1516
src/main/java/com/tiedup/remake/worldgen/RoomTheme.java
Normal file
1516
src/main/java/com/tiedup/remake/worldgen/RoomTheme.java
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user