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:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,66 @@
package com.tiedup.remake.v2.blocks;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
/**
* Base block entity for blocks that render with OBJ models.
* Subclasses define the model and texture locations.
*/
public abstract class ObjBlockEntity extends BlockEntity {
public ObjBlockEntity(
BlockEntityType<?> type,
BlockPos pos,
BlockState state
) {
super(type, pos, state);
}
/**
* Get the OBJ model resource location.
* Example: "tiedup:models/obj/blocks/bowl/model.obj"
*/
@Nullable
public abstract ResourceLocation getModelLocation();
/**
* Get the texture resource location.
* Example: "tiedup:textures/block/bowl.png"
*/
@Nullable
public abstract ResourceLocation getTextureLocation();
/**
* Get the model scale (default 1.0).
* Override to scale the model up or down.
*/
public float getModelScale() {
return 1.0f;
}
/**
* Get the model position offset (x, y, z).
* Override to adjust model position within the block.
*/
public float[] getModelOffset() {
return new float[] { 0.0f, 0.0f, 0.0f };
}
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
// Subclasses can override to save additional data
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
// Subclasses can override to load additional data
}
}

View File

@@ -0,0 +1,181 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.V2BlockEntities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
/**
* Pet Bed Block - Used by Master to make pets sit and sleep.
* Features:
* - Right-click cycle: Stand -> SIT (wariza) -> SLEEP (curled) -> Stand
* - SIT: immobilized with animation, no night skip
* - SLEEP: vanilla sleeping + animation, night skips
* - Custom OBJ model rendering
*/
public class PetBedBlock extends Block implements EntityBlock {
public static final DirectionProperty FACING =
HorizontalDirectionalBlock.FACING;
public static final BooleanProperty OCCUPIED =
BlockStateProperties.OCCUPIED;
// Collision shape (rough approximation of pet bed)
private static final VoxelShape SHAPE = Block.box(
1.0,
0.0,
1.0,
15.0,
6.0,
15.0
);
public PetBedBlock(Properties properties) {
super(properties);
this.registerDefaultState(
this.stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(OCCUPIED, false)
);
}
@Override
protected void createBlockStateDefinition(
StateDefinition.Builder<Block, BlockState> builder
) {
builder.add(FACING, OCCUPIED);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState().setValue(
FACING,
context.getHorizontalDirection().getOpposite()
);
}
@Override
public VoxelShape getShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
return SHAPE;
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED; // Use block entity renderer
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new PetBedBlockEntity(V2BlockEntities.PET_BED.get(), pos, state);
}
@Override
public InteractionResult use(
BlockState state,
Level level,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit
) {
if (level.isClientSide) {
return InteractionResult.CONSUME;
}
if (!(player instanceof ServerPlayer sp)) {
return InteractionResult.PASS;
}
// SLEEP -> Stand: player is sleeping, wake them up
if (sp.isSleeping()) {
sp.stopSleeping();
PetBedManager.clearPlayer(sp);
TiedUpMod.LOGGER.debug(
"[PetBedBlock] {} woke up from bed at {}",
player.getName().getString(),
pos
);
return InteractionResult.SUCCESS;
}
// SIT -> SLEEP: player is already sitting on this bed
if (PetBedManager.isOnBed(sp, pos)) {
sp.startSleeping(pos);
// FIX: notify server to count this player for night skip
if (level instanceof ServerLevel sl) {
sl.updateSleepingPlayerList();
}
PetBedManager.setMode(sp, pos, PetBedManager.PetBedMode.SLEEP);
TiedUpMod.LOGGER.debug(
"[PetBedBlock] {} started sleeping in bed at {}",
player.getName().getString(),
pos
);
return InteractionResult.SUCCESS;
}
// Check if bed is already occupied by someone else
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof PetBedBlockEntity petBed && petBed.isOccupied()) {
return InteractionResult.FAIL;
}
// Stand -> SIT
PetBedManager.setMode(sp, pos, PetBedManager.PetBedMode.SIT);
TiedUpMod.LOGGER.debug(
"[PetBedBlock] {} sat down on bed at {}",
player.getName().getString(),
pos
);
return InteractionResult.SUCCESS;
}
@Override
public void setBedOccupied(
BlockState state,
Level level,
BlockPos pos,
net.minecraft.world.entity.LivingEntity sleeper,
boolean occupied
) {
level.setBlock(pos, state.setValue(OCCUPIED, occupied), 3);
}
@Override
public boolean isBed(
BlockState state,
BlockGetter level,
BlockPos pos,
@Nullable net.minecraft.world.entity.Entity player
) {
return true;
}
}

View File

@@ -0,0 +1,148 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
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.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
/**
* Block entity for Pet Bed.
* Used by Master to make pets sleep. Players must crouch to use it.
*/
public class PetBedBlockEntity extends ObjBlockEntity {
public static final ResourceLocation MODEL_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"models/obj/blocks/bed/model.obj"
);
public static final ResourceLocation TEXTURE_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"textures/block/pet_bed.png"
);
/** UUID of the pet currently using this bed (null if empty) */
@Nullable
private UUID occupantUUID = null;
/** UUID of the Master who owns this bed (null = public) */
@Nullable
private UUID ownerUUID = null;
public PetBedBlockEntity(
BlockEntityType<?> type,
BlockPos pos,
BlockState state
) {
super(type, pos, state);
}
@Override
public ResourceLocation getModelLocation() {
return MODEL_LOCATION;
}
@Override
public ResourceLocation getTextureLocation() {
return TEXTURE_LOCATION;
}
@Override
public float getModelScale() {
return 0.8f; // Scale down to fit in block
}
@Override
public float[] getModelOffset() {
return new float[] { 0.0f, 0.0f, 0.0f };
}
// ========================================
// OCCUPANCY
// ========================================
public boolean isOccupied() {
return occupantUUID != null;
}
@Nullable
public UUID getOccupantUUID() {
return occupantUUID;
}
/**
* Have a pet occupy this bed.
*
* @param petUUID The pet's UUID
* @return true if successful, false if already occupied
*/
public boolean occupy(UUID petUUID) {
if (isOccupied()) return false;
this.occupantUUID = petUUID;
setChanged();
return true;
}
/**
* Release the current occupant.
*/
public void release() {
this.occupantUUID = null;
setChanged();
}
// ========================================
// OWNERSHIP
// ========================================
@Nullable
public UUID getOwnerUUID() {
return ownerUUID;
}
public void setOwner(@Nullable UUID masterUUID) {
this.ownerUUID = masterUUID;
setChanged();
}
public boolean isOwnedBy(UUID uuid) {
return ownerUUID != null && ownerUUID.equals(uuid);
}
public boolean isPublic() {
return ownerUUID == null;
}
// ========================================
// NBT
// ========================================
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
if (occupantUUID != null) {
tag.putUUID("occupant", occupantUUID);
}
if (ownerUUID != null) {
tag.putUUID("owner", ownerUUID);
}
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
if (tag.hasUUID("occupant")) {
this.occupantUUID = tag.getUUID("occupant");
}
if (tag.hasUUID("owner")) {
this.ownerUUID = tag.getUUID("owner");
}
}
}

View File

@@ -0,0 +1,278 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncPetBedState;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
/**
* Server-side tracker for pet bed sit/sleep state per player.
* Manages immobilization, block entity occupancy, and client sync.
*/
public class PetBedManager {
public enum PetBedMode {
SIT,
SLEEP,
}
private static final UUID PET_BED_SPEED_MODIFIER_UUID = UUID.fromString(
"a1b2c3d4-e5f6-4789-abcd-ef0123456789"
);
private static final String PET_BED_SPEED_MODIFIER_NAME =
"tiedup.pet_bed_immobilize";
private static final double FULL_IMMOBILIZATION = -0.10;
/** Active pet bed entries keyed by player UUID */
private static final Map<UUID, PetBedEntry> entries =
new ConcurrentHashMap<>();
private static class PetBedEntry {
final BlockPos pos;
PetBedMode mode;
PetBedEntry(BlockPos pos, PetBedMode mode) {
this.pos = pos;
this.mode = mode;
}
}
/**
* Set a player's pet bed mode (SIT or SLEEP).
*/
public static void setMode(
ServerPlayer player,
BlockPos pos,
PetBedMode mode
) {
UUID uuid = player.getUUID();
PetBedEntry existing = entries.get(uuid);
if (existing != null && mode == PetBedMode.SLEEP) {
// Transitioning from SIT to SLEEP — update mode
existing.mode = mode;
} else {
// New entry (Stand → SIT)
entries.put(uuid, new PetBedEntry(pos.immutable(), mode));
}
// Occupy the block entity
BlockEntity be = player.level().getBlockEntity(pos);
if (be instanceof PetBedBlockEntity petBed) {
petBed.occupy(uuid);
}
// Teleport player to bed center and set rotation to bed facing
double cx = pos.getX() + 0.5;
double cy = pos.getY();
double cz = pos.getZ() + 0.5;
// Nudge player slightly backward on the bed
float bedYRot = getBedFacingAngle(player.level(), pos);
double rad = Math.toRadians(bedYRot);
double backOffset = 0.15;
cx += Math.sin(rad) * backOffset;
cz -= Math.cos(rad) * backOffset;
player.teleportTo(cx, cy, cz);
player.setYRot(bedYRot);
player.setYBodyRot(bedYRot);
player.setYHeadRot(bedYRot);
// Immobilize the player (both SIT and SLEEP)
applySpeedModifier(player);
// Sync to all clients
PacketSyncPetBedState packet = new PacketSyncPetBedState(
uuid,
mode == PetBedMode.SIT ? (byte) 1 : (byte) 2,
pos
);
ModNetwork.sendToAllTrackingAndSelf(packet, player);
TiedUpMod.LOGGER.debug(
"[PetBedManager] {} -> {} at {}",
player.getName().getString(),
mode,
pos
);
}
/**
* Clear a player from the pet bed system.
*/
public static void clearPlayer(ServerPlayer player) {
UUID uuid = player.getUUID();
PetBedEntry entry = entries.remove(uuid);
if (entry == null) return;
// Release block entity
BlockEntity be = player.level().getBlockEntity(entry.pos);
if (be instanceof PetBedBlockEntity petBed) {
petBed.release();
}
// Restore speed
removeSpeedModifier(player);
// Sync clear to clients
PacketSyncPetBedState packet = new PacketSyncPetBedState(
uuid,
(byte) 0,
entry.pos
);
ModNetwork.sendToAllTrackingAndSelf(packet, player);
TiedUpMod.LOGGER.debug(
"[PetBedManager] {} cleared from pet bed at {}",
player.getName().getString(),
entry.pos
);
}
/**
* Check if a player is on a pet bed at the given position.
*/
public static boolean isOnBed(ServerPlayer player, BlockPos pos) {
PetBedEntry entry = entries.get(player.getUUID());
return entry != null && entry.pos.equals(pos);
}
/**
* Check if a player is on any pet bed.
*/
public static boolean isOnAnyBed(ServerPlayer player) {
return entries.containsKey(player.getUUID());
}
/**
* Get the current mode for a player, or null if not on a bed.
*/
@Nullable
public static PetBedMode getMode(ServerPlayer player) {
PetBedEntry entry = entries.get(player.getUUID());
return entry != null ? entry.mode : null;
}
/**
* Tick a player to check if pet bed state should be cancelled.
* Called from server-side tick handler.
*/
public static void tickPlayer(ServerPlayer player) {
PetBedEntry entry = entries.get(player.getUUID());
if (entry == null) return;
// SLEEP: detect vanilla wakeup (night skip, etc.)
if (entry.mode == PetBedMode.SLEEP && !player.isSleeping()) {
clearPlayer(player);
return;
}
// Both modes: lock body rotation to bed facing (prevents camera from rotating model)
float bedYRot = getBedFacingAngle(player.level(), entry.pos);
player.setYBodyRot(bedYRot);
// SLEEP: enforce correct Y position (vanilla startSleeping sets Y+0.2)
if (entry.mode == PetBedMode.SLEEP) {
double correctY = entry.pos.getY();
if (Math.abs(player.getY() - correctY) > 0.01) {
player.teleportTo(player.getX(), correctY, player.getZ());
}
return;
}
// SIT: cancel on sneak (like dismounting a vehicle)
if (entry.mode != PetBedMode.SIT) return;
if (player.isShiftKeyDown()) {
clearPlayer(player);
return;
}
// Check if player moved too far from bed
double distSq = player.blockPosition().distSqr(entry.pos);
if (distSq > 2.25) {
// > 1.5 blocks
clearPlayer(player);
return;
}
// Check if the block is still a pet bed
if (
!(player.level().getBlockState(entry.pos).getBlock() instanceof
PetBedBlock)
) {
clearPlayer(player);
}
}
private static float getBedFacingAngle(Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
if (state.hasProperty(PetBedBlock.FACING)) {
return state.getValue(PetBedBlock.FACING).toYRot();
}
return 0f;
}
/**
* Clean up when a player disconnects.
*/
public static void onPlayerDisconnect(UUID uuid) {
entries.remove(uuid);
}
/**
* Remove any leftover pet bed speed modifier on login.
* The modifier persists on the entity through save/load, but the entries map doesn't,
* so we clean it up here to prevent the player from being stuck.
*/
public static void onPlayerLogin(ServerPlayer player) {
removeSpeedModifier(player);
}
private static void applySpeedModifier(ServerPlayer player) {
AttributeInstance movementSpeed = player.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (movementSpeed == null) return;
// Remove existing to avoid duplicates
if (movementSpeed.getModifier(PET_BED_SPEED_MODIFIER_UUID) != null) {
movementSpeed.removeModifier(PET_BED_SPEED_MODIFIER_UUID);
}
AttributeModifier modifier = new AttributeModifier(
PET_BED_SPEED_MODIFIER_UUID,
PET_BED_SPEED_MODIFIER_NAME,
FULL_IMMOBILIZATION,
AttributeModifier.Operation.ADDITION
);
movementSpeed.addPermanentModifier(modifier);
}
private static void removeSpeedModifier(ServerPlayer player) {
AttributeInstance movementSpeed = player.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (
movementSpeed != null &&
movementSpeed.getModifier(PET_BED_SPEED_MODIFIER_UUID) != null
) {
movementSpeed.removeModifier(PET_BED_SPEED_MODIFIER_UUID);
}
}
}

View File

@@ -0,0 +1,174 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.V2BlockEntities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
/**
* Pet Bowl Block - Used by Master to feed pets.
* Features:
* - Right-click with food to fill
* - Crouch + right-click to eat (only for pets)
* - Custom OBJ model rendering
*/
public class PetBowlBlock extends Block implements EntityBlock {
public static final DirectionProperty FACING =
HorizontalDirectionalBlock.FACING;
// Collision shape (rough approximation of bowl)
private static final VoxelShape SHAPE = Block.box(
3.0,
0.0,
3.0,
13.0,
4.0,
13.0
);
public PetBowlBlock(Properties properties) {
super(properties);
this.registerDefaultState(
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
);
}
@Override
protected void createBlockStateDefinition(
StateDefinition.Builder<Block, BlockState> builder
) {
builder.add(FACING);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState().setValue(
FACING,
context.getHorizontalDirection().getOpposite()
);
}
@Override
public VoxelShape getShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
return SHAPE;
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED; // Use block entity renderer
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new PetBowlBlockEntity(
V2BlockEntities.PET_BOWL.get(),
pos,
state
);
}
@Override
public InteractionResult use(
BlockState state,
Level level,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit
) {
if (level.isClientSide) {
return InteractionResult.SUCCESS;
}
BlockEntity be = level.getBlockEntity(pos);
if (!(be instanceof PetBowlBlockEntity bowl)) {
return InteractionResult.PASS;
}
ItemStack heldItem = player.getItemInHand(hand);
// Fill bowl with food items
if (isFood(heldItem)) {
int foodValue = getFoodValue(heldItem);
bowl.fillBowl(foodValue);
if (!player.getAbilities().instabuild) {
heldItem.shrink(1);
}
TiedUpMod.LOGGER.debug(
"[PetBowlBlock] {} filled bowl at {} with {}",
player.getName().getString(),
pos,
heldItem.getItem()
);
return InteractionResult.CONSUME;
}
// Eat from bowl (empty hand)
if (heldItem.isEmpty() && bowl.hasFood()) {
// TODO: Check if player is a pet and allowed to eat
int consumed = bowl.eatFromBowl();
if (consumed > 0) {
player.getFoodData().eat(consumed, 0.6f);
TiedUpMod.LOGGER.debug(
"[PetBowlBlock] {} ate {} from bowl at {}",
player.getName().getString(),
consumed,
pos
);
}
return InteractionResult.CONSUME;
}
return InteractionResult.PASS;
}
private boolean isFood(ItemStack stack) {
if (stack.isEmpty()) return false;
return (
stack.getItem().isEdible() ||
stack.is(Items.WHEAT) ||
stack.is(Items.CARROT) ||
stack.is(Items.APPLE)
);
}
private int getFoodValue(ItemStack stack) {
if (stack.getItem().isEdible()) {
var food = stack.getItem().getFoodProperties();
return food != null ? food.getNutrition() : 2;
}
return 2; // Default for non-standard foods
}
}

View File

@@ -0,0 +1,122 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
/**
* Block entity for Pet Bowl.
* Used by Master to feed pets. Players must crouch to eat from it.
*/
public class PetBowlBlockEntity extends ObjBlockEntity {
public static final ResourceLocation MODEL_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"models/obj/blocks/bowl/model.obj"
);
public static final ResourceLocation TEXTURE_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"textures/block/pet_bowl.png"
);
/** Whether the bowl currently has food */
private boolean hasFood = false;
/** Food saturation level (0-20) */
private int foodLevel = 0;
public PetBowlBlockEntity(
BlockEntityType<?> type,
BlockPos pos,
BlockState state
) {
super(type, pos, state);
}
@Override
public ResourceLocation getModelLocation() {
return MODEL_LOCATION;
}
@Override
public ResourceLocation getTextureLocation() {
return TEXTURE_LOCATION;
}
@Override
public float getModelScale() {
return 1.0f; // Adjust based on model size
}
// ========================================
// FOOD MANAGEMENT
// ========================================
public boolean hasFood() {
return hasFood && foodLevel > 0;
}
public int getFoodLevel() {
return foodLevel;
}
/**
* Fill the bowl with food.
*
* @param amount Amount of food to add (saturation points)
*/
public void fillBowl(int amount) {
this.foodLevel = Math.min(20, this.foodLevel + amount);
this.hasFood = this.foodLevel > 0;
setChanged();
}
/**
* Pet eats from the bowl.
*
* @return Amount of food consumed (0 if empty)
*/
public int eatFromBowl() {
if (!hasFood()) return 0;
int consumed = Math.min(4, foodLevel); // Eat up to 4 at a time
foodLevel -= consumed;
hasFood = foodLevel > 0;
setChanged();
return consumed;
}
/**
* Empty the bowl completely.
*/
public void emptyBowl() {
this.foodLevel = 0;
this.hasFood = false;
setChanged();
}
// ========================================
// NBT
// ========================================
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
tag.putBoolean("hasFood", hasFood);
tag.putInt("foodLevel", foodLevel);
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
this.hasFood = tag.getBoolean("hasFood");
this.foodLevel = tag.getInt("foodLevel");
}
}

View File

@@ -0,0 +1,538 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.V2BlockEntities;
import com.tiedup.remake.v2.V2Blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
/**
* Pet Cage Block (Master) - Multi-block cage (3x3x2).
*
* <p>Collision is computed from the actual OBJ model dimensions. Each block in
* the 3x3x2 grid gets collision shapes for any cage walls that pass through it,
* clipped to the block boundaries. This gives pixel-accurate cage collision
* that matches the visual model.
*/
public class PetCageBlock extends Block implements EntityBlock {
public static final DirectionProperty FACING =
HorizontalDirectionalBlock.FACING;
// ========================================
// OBJ MODEL DIMENSIONS (in model space, before rotation/translation)
// ========================================
private static final double MODEL_MIN_X = -1.137;
private static final double MODEL_MAX_X = 1.0;
private static final double MODEL_MIN_Z = -1.122;
private static final double MODEL_MAX_Z = 1.0;
private static final double MODEL_MAX_Y = 2.218;
/** Wall thickness in blocks. */
private static final double WALL_T = 0.125; // 2 pixels
private static final VoxelShape OUTLINE_SHAPE = Block.box(
0,
0,
0,
16,
16,
16
);
public PetCageBlock(Properties properties) {
super(properties);
this.registerDefaultState(
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
);
}
@Override
protected void createBlockStateDefinition(
StateDefinition.Builder<Block, BlockState> builder
) {
builder.add(FACING);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction facing = context.getHorizontalDirection().getOpposite();
BlockPos pos = context.getClickedPos();
for (BlockPos partPos : getPartPositions(pos, facing)) {
if (
!context
.getLevel()
.getBlockState(partPos)
.canBeReplaced(context)
) {
return null;
}
}
return this.defaultBlockState().setValue(FACING, facing);
}
@Override
public void onPlace(
BlockState state,
Level level,
BlockPos pos,
BlockState oldState,
boolean isMoving
) {
super.onPlace(state, level, pos, oldState, isMoving);
if (level.isClientSide || oldState.is(this)) return;
Direction facing = state.getValue(FACING);
BlockState partState = V2Blocks.PET_CAGE_PART.get()
.defaultBlockState()
.setValue(PetCagePartBlock.FACING, facing);
for (BlockPos partPos : getPartPositions(pos, facing)) {
level.setBlock(partPos, partState, 3);
}
}
@Override
public VoxelShape getShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
return OUTLINE_SHAPE;
}
@Override
public VoxelShape getCollisionShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
Direction facing = state.getValue(FACING);
return computeCageCollision(pos, facing, pos);
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED;
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new PetCageBlockEntity(
V2BlockEntities.PET_CAGE.get(),
pos,
state
);
}
@Override
public InteractionResult use(
BlockState state,
Level level,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit
) {
if (level.isClientSide) {
return InteractionResult.CONSUME;
}
if (!(player instanceof ServerPlayer sp)) {
return InteractionResult.PASS;
}
BlockEntity be = level.getBlockEntity(pos);
if (!(be instanceof PetCageBlockEntity cageBE)) {
return InteractionResult.PASS;
}
if (cageBE.hasOccupant()) {
ServerPlayer occupant = cageBE.getOccupant(level.getServer());
if (occupant != null) {
PetCageManager.releasePlayer(occupant);
TiedUpMod.LOGGER.debug(
"[PetCageBlock] {} released {} from cage at {}",
player.getName().getString(),
occupant.getName().getString(),
pos
);
} else {
cageBE.clearOccupant();
}
return InteractionResult.SUCCESS;
}
double cx = pos.getX() + 0.5;
double cy = pos.getY();
double cz = pos.getZ() + 0.5;
for (ServerPlayer nearby : level
.getServer()
.getPlayerList()
.getPlayers()) {
if (nearby == player) continue;
if (nearby.distanceToSqr(cx, cy, cz) > 9.0) continue;
IBondageState kidState = KidnappedHelper.getKidnappedState(nearby);
if (kidState != null && kidState.isTiedUp()) {
PetCageManager.cagePlayer(nearby, pos);
cageBE.setOccupant(nearby.getUUID());
TiedUpMod.LOGGER.debug(
"[PetCageBlock] {} caged {} at {}",
player.getName().getString(),
nearby.getName().getString(),
pos
);
return InteractionResult.SUCCESS;
}
}
return InteractionResult.PASS;
}
@Override
public void onRemove(
BlockState state,
Level level,
BlockPos pos,
BlockState newState,
boolean isMoving
) {
if (!state.is(newState.getBlock())) {
BlockEntity be = level.getBlockEntity(pos);
if (
be instanceof PetCageBlockEntity cageBE &&
cageBE.hasOccupant() &&
level.getServer() != null
) {
ServerPlayer occupant = cageBE.getOccupant(level.getServer());
if (occupant != null) {
PetCageManager.releasePlayer(occupant);
}
}
Direction facing = state.getValue(FACING);
for (BlockPos partPos : getPartPositions(pos, facing)) {
BlockState partState = level.getBlockState(partPos);
if (partState.getBlock() instanceof PetCagePartBlock) {
level.setBlock(
partPos,
Blocks.AIR.defaultBlockState(),
3 | 64
);
}
}
}
super.onRemove(state, level, pos, newState, isMoving);
}
// ========================================
// CAGE COLLISION (model-accurate)
// ========================================
/**
* Compute the cage AABB in world coordinates for a given master position and facing.
* The renderer does translate(0.5, 0, 0.5) then rotate, so vertices are
* first rotated then translated.
*
* @return {minX, minZ, maxX, maxZ, maxY}
*/
public static double[] getCageWorldBounds(
BlockPos masterPos,
Direction facing
) {
double cx = masterPos.getX() + 0.5;
double cz = masterPos.getZ() + 0.5;
double wMinX, wMaxX, wMinZ, wMaxZ;
switch (facing) {
case SOUTH -> {
// 180°: (x,z) → (-x,-z)
wMinX = cx - MODEL_MAX_X;
wMaxX = cx - MODEL_MIN_X;
wMinZ = cz - MODEL_MAX_Z;
wMaxZ = cz - MODEL_MIN_Z;
}
case WEST -> {
// 90°: (x,z) → (z,-x)
wMinX = cx + MODEL_MIN_Z;
wMaxX = cx + MODEL_MAX_Z;
wMinZ = cz - MODEL_MAX_X;
wMaxZ = cz - MODEL_MIN_X;
}
case EAST -> {
// -90°: (x,z) → (-z,x)
wMinX = cx - MODEL_MAX_Z;
wMaxX = cx - MODEL_MIN_Z;
wMinZ = cz + MODEL_MIN_X;
wMaxZ = cz + MODEL_MAX_X;
}
default -> {
// NORTH, 0°: (x,z) → (x,z)
wMinX = cx + MODEL_MIN_X;
wMaxX = cx + MODEL_MAX_X;
wMinZ = cz + MODEL_MIN_Z;
wMaxZ = cz + MODEL_MAX_Z;
}
}
return new double[] {
wMinX,
wMinZ,
wMaxX,
wMaxZ,
masterPos.getY() + MODEL_MAX_Y,
};
}
/**
* Compute the cage collision VoxelShape for any block position within the cage grid.
* Clips the 4 cage walls + floor to the block's boundaries.
*/
public static VoxelShape computeCageCollision(
BlockPos masterPos,
Direction facing,
BlockPos blockPos
) {
double[] bounds = getCageWorldBounds(masterPos, facing);
double cMinX = bounds[0],
cMinZ = bounds[1];
double cMaxX = bounds[2],
cMaxZ = bounds[3];
double cMaxY = bounds[4];
double cMinY = masterPos.getY(); // Cage base = master Y
double bx = blockPos.getX();
double by = blockPos.getY();
double bz = blockPos.getZ();
VoxelShape shape = Shapes.empty();
// Left wall (at cMinX, perpendicular to X) — full height from base to ceiling
shape = addClippedWall(
shape,
cMinX - WALL_T / 2,
cMinY,
cMinZ,
cMinX + WALL_T / 2,
cMaxY,
cMaxZ,
bx,
by,
bz
);
// Right wall (at cMaxX)
shape = addClippedWall(
shape,
cMaxX - WALL_T / 2,
cMinY,
cMinZ,
cMaxX + WALL_T / 2,
cMaxY,
cMaxZ,
bx,
by,
bz
);
// Front wall (at cMinZ, perpendicular to Z)
shape = addClippedWall(
shape,
cMinX,
cMinY,
cMinZ - WALL_T / 2,
cMaxX,
cMaxY,
cMinZ + WALL_T / 2,
bx,
by,
bz
);
// Back wall (at cMaxZ)
shape = addClippedWall(
shape,
cMinX,
cMinY,
cMaxZ - WALL_T / 2,
cMaxX,
cMaxY,
cMaxZ + WALL_T / 2,
bx,
by,
bz
);
// Floor (at cage base only)
shape = addClippedWall(
shape,
cMinX,
cMinY,
cMinZ,
cMaxX,
cMinY + WALL_T,
cMaxZ,
bx,
by,
bz
);
// Ceiling: 1px thin at [1.9375, 2.0] — player head at 1.925 < 1.9375, works from both sides
double ceilY = cMinY + 2.0;
double ceilT = 0.0625; // 1 pixel thin so player (1.8 tall) fits under it
shape = addClippedWall(
shape,
cMinX,
ceilY - ceilT,
cMinZ,
cMaxX,
ceilY,
cMaxZ,
bx,
by,
bz
);
return shape;
}
/**
* Clip a world-space AABB to a block's local bounds and add it as a VoxelShape.
* X and Z are clipped to [0,1] (entities in other blocks won't query this one).
* Y max is allowed to extend above 1.0 (like fences) since entities below
* will still query this block and collide with the extended shape.
*/
private static VoxelShape addClippedWall(
VoxelShape existing,
double wMinX,
double wMinY,
double wMinZ,
double wMaxX,
double wMaxY,
double wMaxZ,
double blockX,
double blockY,
double blockZ
) {
double lMinX = Math.max(0, wMinX - blockX);
double lMinY = Math.max(0, wMinY - blockY);
double lMinZ = Math.max(0, wMinZ - blockZ);
double lMaxX = Math.min(1, wMaxX - blockX);
double lMaxY = Math.min(1.5, wMaxY - blockY); // Allow extending up to 0.5 above block (like fences)
double lMaxZ = Math.min(1, wMaxZ - blockZ);
if (lMinX >= lMaxX || lMinY >= lMaxY || lMinZ >= lMaxZ || lMaxY <= 0) {
return existing;
}
return Shapes.or(
existing,
Shapes.box(lMinX, lMinY, lMinZ, lMaxX, lMaxY, lMaxZ)
);
}
// ========================================
// MULTI-BLOCK LAYOUT (3x3x2)
// ========================================
private static final int[][] GRID_OFFSETS;
static {
java.util.List<int[]> offsets = new java.util.ArrayList<>();
for (int dy = 0; dy <= 1; dy++) {
for (int dl = -1; dl <= 1; dl++) {
for (int df = -1; df <= 1; df++) {
if (dl == 0 && dy == 0 && df == 0) continue;
offsets.add(new int[] { dl, dy, df });
}
}
}
GRID_OFFSETS = offsets.toArray(new int[0][]);
}
public static BlockPos[] getPartPositions(
BlockPos masterPos,
Direction facing
) {
Direction left = facing.getCounterClockWise();
int lx = left.getStepX();
int lz = left.getStepZ();
int fx = facing.getStepX();
int fz = facing.getStepZ();
BlockPos[] positions = new BlockPos[GRID_OFFSETS.length];
for (int i = 0; i < GRID_OFFSETS.length; i++) {
int dl = GRID_OFFSETS[i][0];
int dy = GRID_OFFSETS[i][1];
int df = GRID_OFFSETS[i][2];
int wx = dl * lx + df * fx;
int wz = dl * lz + df * fz;
positions[i] = masterPos.offset(wx, dy, wz);
}
return positions;
}
@Nullable
public static int[] getPartLocalOffset(
BlockPos masterPos,
Direction facing,
BlockPos partPos
) {
Direction left = facing.getCounterClockWise();
int dx = partPos.getX() - masterPos.getX();
int dy = partPos.getY() - masterPos.getY();
int dz = partPos.getZ() - masterPos.getZ();
int lx = left.getStepX(),
lz = left.getStepZ();
int fx = facing.getStepX(),
fz = facing.getStepZ();
int det = lx * fz - fx * lz;
if (det == 0) return null;
int dl = (dx * fz - dz * fx) / det;
int df = (dz * lx - dx * lz) / det;
if (
dl < -1 || dl > 1 || df < -1 || df > 1 || dy < 0 || dy > 1
) return null;
if (dl == 0 && dy == 0 && df == 0) return null;
return new int[] { dl, dy, df };
}
}

View File

@@ -0,0 +1,129 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Nullable;
/**
* Block entity for Pet Cage.
* Tracks the occupant UUID and provides OBJ model rendering data.
*/
public class PetCageBlockEntity extends ObjBlockEntity {
public static final ResourceLocation MODEL_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"models/obj/blocks/cage/model.obj"
);
public static final ResourceLocation TEXTURE_LOCATION =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"textures/block/pet_cage.png"
);
@Nullable
private UUID occupantUUID;
public PetCageBlockEntity(
BlockEntityType<?> type,
BlockPos pos,
BlockState state
) {
super(type, pos, state);
}
@Override
public ResourceLocation getModelLocation() {
return MODEL_LOCATION;
}
@Override
public ResourceLocation getTextureLocation() {
return TEXTURE_LOCATION;
}
@Override
public float getModelScale() {
return 1.0f;
}
/**
* Large render bounding box so the cage doesn't disappear when the camera
* is inside the model (e.g. player caged) or looking away from the master block.
*/
@Override
public AABB getRenderBoundingBox() {
BlockPos p = getBlockPos();
return new AABB(
p.getX() - 2,
p.getY(),
p.getZ() - 2,
p.getX() + 3,
p.getY() + 3,
p.getZ() + 3
);
}
// ========================================
// OCCUPANT MANAGEMENT
// ========================================
public boolean hasOccupant() {
return occupantUUID != null;
}
@Nullable
public UUID getOccupantUUID() {
return occupantUUID;
}
public void setOccupant(UUID uuid) {
this.occupantUUID = uuid;
setChanged();
}
public void clearOccupant() {
this.occupantUUID = null;
setChanged();
}
/**
* Get the occupant ServerPlayer if online.
*/
@Nullable
public ServerPlayer getOccupant(@Nullable MinecraftServer server) {
if (server == null || occupantUUID == null) return null;
return server.getPlayerList().getPlayer(occupantUUID);
}
// ========================================
// NBT
// ========================================
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
if (occupantUUID != null) {
tag.putUUID("occupantUUID", occupantUUID);
}
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
if (tag.hasUUID("occupantUUID")) {
this.occupantUUID = tag.getUUID("occupantUUID");
} else {
this.occupantUUID = null;
}
}
}

View File

@@ -0,0 +1,189 @@
package com.tiedup.remake.v2.blocks;
import com.tiedup.remake.core.TiedUpMod;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.level.block.entity.BlockEntity;
/**
* Server-side manager for pet cage confinement.
* Tracks which players are caged, immobilizes them, and handles cleanup.
*/
public class PetCageManager {
private static final UUID PET_CAGE_SPEED_MODIFIER_UUID = UUID.fromString(
"b2c3d4e5-f6a7-4890-bcde-f01234567890"
);
private static final String PET_CAGE_SPEED_MODIFIER_NAME =
"tiedup.pet_cage_immobilize";
private static final double FULL_IMMOBILIZATION = -0.10;
/** Active cage entries keyed by player UUID */
private static final Map<UUID, CageEntry> entries =
new ConcurrentHashMap<>();
private static class CageEntry {
final BlockPos pos;
CageEntry(BlockPos pos) {
this.pos = pos;
}
}
/**
* Cage a player at the given master block position.
* Teleports them to the center of the 3x3x3 cage and immobilizes.
*/
public static void cagePlayer(ServerPlayer player, BlockPos masterPos) {
UUID uuid = player.getUUID();
entries.put(uuid, new CageEntry(masterPos.immutable()));
// Teleport to cage center (master is at center of 3x3x3 grid)
double[] center = getCageCenter(player, masterPos);
player.teleportTo(center[0], center[1], center[2]);
// Immobilize
applySpeedModifier(player);
TiedUpMod.LOGGER.debug(
"[PetCageManager] {} caged at {}",
player.getName().getString(),
masterPos
);
}
/**
* Release a player from cage confinement.
*/
public static void releasePlayer(ServerPlayer player) {
UUID uuid = player.getUUID();
CageEntry entry = entries.remove(uuid);
if (entry == null) return;
// Release block entity
BlockEntity be = player.level().getBlockEntity(entry.pos);
if (be instanceof PetCageBlockEntity cageBE) {
cageBE.clearOccupant();
}
// Restore speed
removeSpeedModifier(player);
TiedUpMod.LOGGER.debug(
"[PetCageManager] {} released from cage at {}",
player.getName().getString(),
entry.pos
);
}
/**
* Check if a player is in a cage.
*/
public static boolean isInCage(ServerPlayer player) {
return entries.containsKey(player.getUUID());
}
/**
* Get the cage position for a player, or null if not caged.
*/
@Nullable
public static BlockPos getCagePos(ServerPlayer player) {
CageEntry entry = entries.get(player.getUUID());
return entry != null ? entry.pos : null;
}
/**
* Tick a caged player to verify cage still exists.
* Called from server-side tick handler.
*/
public static void tickPlayer(ServerPlayer player) {
CageEntry entry = entries.get(player.getUUID());
if (entry == null) return;
// Check if the block is still a cage
if (
!(player.level().getBlockState(entry.pos).getBlock() instanceof
PetCageBlock)
) {
releasePlayer(player);
return;
}
// Keep player at cage center (prevent movement exploits)
double[] center = getCageCenter(player, entry.pos);
double distSq = player.distanceToSqr(center[0], center[1], center[2]);
if (distSq > 0.25) {
// > 0.5 blocks
player.teleportTo(center[0], center[1], center[2]);
}
}
/**
* Calculate the center of the 3x3x3 cage from the master block position.
* The master is at the center of the grid, so the cage center is the master block center.
*/
private static double[] getCageCenter(
ServerPlayer player,
BlockPos masterPos
) {
return new double[] {
masterPos.getX() + 0.5,
masterPos.getY(),
masterPos.getZ() + 0.5,
};
}
/**
* Clean up when a player disconnects.
*/
public static void onPlayerDisconnect(UUID uuid) {
entries.remove(uuid);
}
/**
* Remove any leftover cage speed modifier on login.
*/
public static void onPlayerLogin(ServerPlayer player) {
removeSpeedModifier(player);
}
private static void applySpeedModifier(ServerPlayer player) {
AttributeInstance movementSpeed = player.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (movementSpeed == null) return;
// Remove existing to avoid duplicates
if (movementSpeed.getModifier(PET_CAGE_SPEED_MODIFIER_UUID) != null) {
movementSpeed.removeModifier(PET_CAGE_SPEED_MODIFIER_UUID);
}
AttributeModifier modifier = new AttributeModifier(
PET_CAGE_SPEED_MODIFIER_UUID,
PET_CAGE_SPEED_MODIFIER_NAME,
FULL_IMMOBILIZATION,
AttributeModifier.Operation.ADDITION
);
movementSpeed.addPermanentModifier(modifier);
}
private static void removeSpeedModifier(ServerPlayer player) {
AttributeInstance movementSpeed = player.getAttribute(
Attributes.MOVEMENT_SPEED
);
if (
movementSpeed != null &&
movementSpeed.getModifier(PET_CAGE_SPEED_MODIFIER_UUID) != null
) {
movementSpeed.removeModifier(PET_CAGE_SPEED_MODIFIER_UUID);
}
}
}

View File

@@ -0,0 +1,169 @@
package com.tiedup.remake.v2.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
/**
* Pet Cage Part Block (Slave) - Invisible structural part of a 3x3x2 multi-block cage.
*
* <p>No visible model, no selection outline. Provides collision shapes computed
* from the actual OBJ model dimensions (delegated to {@link PetCageBlock#computeCageCollision}).
* Interactions and breaking are delegated to the master.
*/
public class PetCagePartBlock extends Block {
public static final DirectionProperty FACING =
HorizontalDirectionalBlock.FACING;
public PetCagePartBlock(Properties properties) {
super(properties);
this.registerDefaultState(
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
);
}
@Override
protected void createBlockStateDefinition(
StateDefinition.Builder<Block, BlockState> builder
) {
builder.add(FACING);
}
@Override
public VoxelShape getShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
// Invisible: no outline, no selection box
return Shapes.empty();
}
@Override
public VoxelShape getCollisionShape(
BlockState state,
BlockGetter level,
BlockPos pos,
CollisionContext context
) {
Direction facing = state.getValue(FACING);
BlockPos masterPos = findMasterPos(state, level, pos);
if (masterPos == null) return Shapes.empty();
return PetCageBlock.computeCageCollision(masterPos, facing, pos);
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.INVISIBLE;
}
@Override
public InteractionResult use(
BlockState state,
Level level,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit
) {
BlockPos masterPos = findMasterPos(state, level, pos);
if (masterPos != null) {
BlockState masterState = level.getBlockState(masterPos);
if (masterState.getBlock() instanceof PetCageBlock cage) {
return cage.use(
masterState,
level,
masterPos,
player,
hand,
hit
);
}
}
return InteractionResult.PASS;
}
@Override
public void onRemove(
BlockState state,
Level level,
BlockPos pos,
BlockState newState,
boolean isMoving
) {
if (!state.is(newState.getBlock())) {
BlockPos masterPos = findMasterPos(state, level, pos);
if (masterPos != null) {
BlockState masterState = level.getBlockState(masterPos);
if (masterState.getBlock() instanceof PetCageBlock) {
level.destroyBlock(masterPos, true);
}
}
}
super.onRemove(state, level, pos, newState, isMoving);
}
@Override
public void playerWillDestroy(
Level level,
BlockPos pos,
BlockState state,
Player player
) {
// Don't call super to prevent double drops
}
@Nullable
private BlockPos findMasterPos(
BlockState partState,
BlockGetter level,
BlockPos partPos
) {
Direction facing = partState.getValue(FACING);
Direction left = facing.getCounterClockWise();
int lx = left.getStepX(),
lz = left.getStepZ();
int fx = facing.getStepX(),
fz = facing.getStepZ();
for (int dy = 0; dy <= 1; dy++) {
for (int dl = -1; dl <= 1; dl++) {
for (int df = -1; df <= 1; df++) {
if (dl == 0 && dy == 0 && df == 0) continue;
int wx = -(dl * lx + df * fx);
int wz = -(dl * lz + df * fz);
BlockPos candidate = partPos.offset(wx, -dy, wz);
BlockState candidateState = level.getBlockState(candidate);
if (
candidateState.getBlock() instanceof PetCageBlock &&
candidateState.getValue(PetCageBlock.FACING) == facing
) {
return candidate;
}
}
}
}
return null;
}
}