filename:
common/src/main/java/rearth/oritech/block/base/entity/FrameInteractionBlockEntity.java
branch:
1.21
back to repo
package rearth.oritech.block.base.entity;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.state.property.Properties;
import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Vec3i;
import net.minecraft.world.World;
import rearth.oritech.Oritech;
import rearth.oritech.block.base.block.FrameInteractionBlock;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.init.BlockContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.Geometry;
import java.util.HashMap;
import java.util.Objects;
import static rearth.oritech.util.Geometry.*;
public abstract class FrameInteractionBlockEntity extends BlockEntity implements BlockEntityTicker<FrameInteractionBlockEntity> {
private static final int MAX_SEARCH_LENGTH = Oritech.CONFIG.processingMachines.machineFrameMaxLength();
private static final HashMap<Vec3i, HashMap<Vec3i, Vec3i>> occupiedAreas = new HashMap<>();
private BlockPos areaMin; // both min and max are inclusive
private BlockPos areaMax;
private BlockPos currentTarget; // rendering is based just on this (and move time)
private BlockPos lastTarget;
private float currentProgress; // not synced
private boolean moving; // not synced
private Vec3i currentDirection = new Vec3i(1, 0, 0); // not synced
public long lastWorkedAt; // not synced
public boolean disabledViaRedstone;
public boolean networkDirty;
// client only
private long moveStartedAt;
// for smooth client rendering only
public Vec3d lastRenderedPosition = new Vec3d(0, 0, 0);
public FrameInteractionBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}
public boolean tryFindFrame() {
Oritech.LOGGER.debug("searching machine frame");
// select block on back (or based on offset of machine)
// from there on move right, till no more frame blocks are found
// then move back, searching again till end
// then move left, searching again till end
// then move forward, searching again till end
// then move right again, searching till start position
var facing = getFacing();
var backRelative = new Vec3i(getFrameOffset(), 0, 0);
var searchStart = (BlockPos) Geometry.offsetToWorldPosition(facing, backRelative, pos);
var endRightFront = searchFrameLine(searchStart, getRight(facing));
if (endRightFront.equals(BlockPos.ORIGIN)) {
highlightBlock(searchStart);
return false;
}
var endRightBack = searchFrameLine(endRightFront, getBackward(facing));
if (endRightBack.equals(endRightFront)) {
highlightBlock(endRightFront.add(getRight(facing)));
highlightBlock(endRightFront.add(getBackward(facing)));
return false;
}
var endLeftBack = searchFrameLine(endRightBack, getLeft(facing));
if (endLeftBack.equals(endRightBack)) {
highlightBlock(endRightBack.add(getBackward(facing)));
highlightBlock(endRightBack.add(getLeft(facing)));
return false;
}
var endLeftFront = searchFrameLine(endLeftBack, getForward(facing));
if (endLeftFront.equals(endLeftBack)) {
highlightBlock(endLeftBack.add(getLeft(facing)));
highlightBlock(endLeftBack.add(getForward(facing)));
return false;
}
var endMiddleFront = searchFrameLineEnd(endLeftFront, getRight(facing), searchStart);
if (endMiddleFront.equals(endLeftFront)) {
highlightBlock(endMiddleFront.add(getForward(facing)));
highlightBlock(endMiddleFront.add(getRight(facing)));
return false;
}
if (!endMiddleFront.equals(searchStart)) {
highlightBlock(endMiddleFront.add(getRight(facing)));
return false;
}
var innerValid = checkInnerEmpty(endLeftBack, endRightFront);
if (!innerValid) return false;
// offset values by 1 to define the working area instead of bounds
var startX = Math.min(endLeftFront.getX(), endRightBack.getX()) + 1;
var startZ = Math.min(endLeftFront.getZ(), endRightBack.getZ()) + 1;
areaMin = new BlockPos(startX, getPos().getY(), startZ);
var endX = Math.max(endLeftFront.getX(), endRightBack.getX()) - 1;
var endZ = Math.max(endLeftFront.getZ(), endRightBack.getZ()) - 1;
areaMax = new BlockPos(endX, getPos().getY(), endZ);
if (currentTarget == null || !isInBounds(currentTarget)) {
currentTarget = areaMin;
lastTarget = areaMin;
}
this.markDirty();
return true;
}
protected Direction getFacing() {
return Objects.requireNonNull(world).getBlockState(getPos()).get(Properties.HORIZONTAL_FACING);
}
private boolean checkInnerEmpty(BlockPos leftBack, BlockPos rightFront) {
assert world != null;
var lengthX = Math.abs(leftBack.getX() - rightFront.getX());
var lengthZ = Math.abs(leftBack.getZ() - rightFront.getZ());
var dirX = leftBack.getX() - rightFront.getX() > 0 ? -1 : 1;
var dirZ = leftBack.getZ() - rightFront.getZ() > 0 ? -1 : 1;
var valid = true;
for (int x = 1; x < lengthX; x++) {
for (int z = 1; z < lengthZ; z++) {
var offset = new BlockPos(dirX * x, 0, dirZ * z);
var checkPos = leftBack.add(offset);
var foundBlock = world.getBlockState(checkPos).getBlock();
if (!foundBlock.equals(Blocks.AIR)) {
highlightBlock(checkPos);
valid = false;
}
}
}
return valid;
}
private BlockPos searchFrameLine(BlockPos searchStart, Vec3i direction) {
var lastPosition = BlockPos.ORIGIN; // yes this will break if the frame starts at 0/0/0, however I'm willing to accept this
for (int i = 0; i < MAX_SEARCH_LENGTH; i++) {
var checkPos = searchStart.add(direction.multiply(i));
if (testForFrame(checkPos)) {
lastPosition = checkPos;
} else {
break;
}
}
return lastPosition;
}
private BlockPos searchFrameLineEnd(BlockPos searchStart, Vec3i direction, BlockPos searchEnd) {
var lastPosition = BlockPos.ORIGIN; // yes this will break if the frame starts at 0/0/0, however I'm willing to accept this
for (int i = 0; i < MAX_SEARCH_LENGTH; i++) {
var checkPos = searchStart.add(direction.multiply(i));
if (testForFrame(checkPos)) {
if (checkPos.equals(searchEnd)) {
Oritech.LOGGER.debug("found start, machine is valid");
return checkPos;
}
lastPosition = checkPos;
} else {
break;
}
}
return lastPosition;
}
@SuppressWarnings("DataFlowIssue")
private boolean testForFrame(BlockPos pos) {
var found = world.getBlockState(pos).getBlock();
return found.equals(BlockContent.MACHINE_FRAME_BLOCK);
}
@Override
public void tick(World world, BlockPos pos, BlockState state, FrameInteractionBlockEntity blockEntity) {
if (world.isClient || !isActive(state) || !state.get(FrameInteractionBlock.HAS_FRAME) || getAreaMin() == null)
return;
if (!canProgress()) return;
var posAtTickBegin = currentTarget.mutableCopy();
while (currentProgress > 0.01 && canProgress()) {
if (!moving && currentProgress >= getWorkTime()) {
currentProgress -= getWorkTime();
finishBlockWork(currentTarget);
moving = true;
this.markDirty();
} else if (moving && currentProgress >= getMoveTime() && finishBlockMove()) {
updateToolPosInFrame();
currentProgress -= getMoveTime();
this.networkDirty = true;
if (hasWorkAvailable(currentTarget)) moving = false;
} else {
break;
}
}
if (this.networkDirty)
sendMovementNetworkPacket(posAtTickBegin);
doProgress(moving);
currentProgress++;
lastWorkedAt = world.getTime();
}
private boolean isBlockAvailable(BlockPos target) {
if (!occupiedAreas.containsKey(areaMin)) {
occupiedAreas.put(areaMin, new HashMap<>(1));
return true;
}
var frameEntries = occupiedAreas.get(areaMin);
return !frameEntries.containsValue(target);
}
private void updateToolPosInFrame() {
var frameEntries = occupiedAreas.get(areaMin);
frameEntries.put(pos, currentTarget);
}
public void cleanup() {
var frameEntries = occupiedAreas.get(areaMin);
if (frameEntries != null)
frameEntries.remove(pos);
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected abstract boolean hasWorkAvailable(BlockPos toolPosition);
protected abstract void doProgress(boolean moving);
protected abstract boolean canProgress();
public abstract void finishBlockWork(BlockPos processed);
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.writeNbt(nbt, registryLookup);
if (getCachedState().get(FrameInteractionBlock.HAS_FRAME) && areaMin != null) {
nbt.putLong("areaMin", areaMin.asLong());
nbt.putLong("areaMax", areaMax.asLong());
nbt.putLong("currentTarget", currentTarget.asLong());
nbt.putLong("currentDirection", new BlockPos(currentDirection).asLong());
nbt.putInt("progress", (int) currentProgress);
nbt.putBoolean("moving", moving);
}
}
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
if (getCachedState().get(FrameInteractionBlock.HAS_FRAME)) {
areaMin = BlockPos.fromLong(nbt.getLong("areaMin"));
areaMax = BlockPos.fromLong(nbt.getLong("areaMax"));
currentTarget = BlockPos.fromLong(nbt.getLong("currentTarget"));
currentDirection = BlockPos.fromLong(nbt.getLong("currentDirection"));
lastTarget = currentTarget;
currentProgress = nbt.getInt("progress");
moving = nbt.getBoolean("moving");
}
}
private boolean finishBlockMove() {
var nextPos = currentTarget.add(currentDirection);
var nextDir = currentDirection;
if (!isInBounds(nextPos)) {
nextPos = currentTarget.add(0, 0, 1);
nextDir = currentDirection.multiply(-1);
if (!isInBounds(nextPos)) {
var data = resetWorkPosition();
nextPos = data.getLeft();
nextDir = data.getRight();
}
}
// tries to not put 2 tool heads in the same spot, but also allow overtaking if previous machine is too slow
if (!isBlockAvailable(nextPos) && currentProgress <= getWorkTime() * getSpeedMultiplier() * 2 + 4) return false;
lastTarget = currentTarget;
currentTarget = nextPos;
currentDirection = nextDir;
return true;
}
// return start position + direction
private Pair<BlockPos, BlockPos> resetWorkPosition() {
return new Pair<>(areaMin, new BlockPos(1, 0, 0));
}
private boolean isInBounds(BlockPos pos) {
return pos.getX() >= areaMin.getX() && pos.getX() <= areaMax.getX()
&& pos.getZ() >= areaMin.getZ() && pos.getZ() <= areaMax.getZ();
}
private void highlightBlock(BlockPos block) {
ParticleContent.HIGHLIGHT_BLOCK.spawn(world, Vec3d.of(block), null);
}
public void sendMovementNetworkPacket(BlockPos from) {
networkDirty = false;
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.MachineFrameMovementPacket(pos, currentTarget, from, areaMin, areaMax, disabledViaRedstone));
}
@Override
public void markDirty() {
if (this.world != null)
world.markDirty(pos);
}
public abstract BlockState getMachineHead();
public int getFrameOffset() {
return 1;
}
public float getSpeedMultiplier() {
return 1f;
}
public BlockPos getAreaMin() {
return areaMin;
}
public void setAreaMin(BlockPos areaMin) {
this.areaMin = areaMin;
}
public BlockPos getAreaMax() {
return areaMax;
}
public void setAreaMax(BlockPos areaMax) {
this.areaMax = areaMax;
}
public BlockPos getCurrentTarget() {
return currentTarget;
}
public void setCurrentTarget(BlockPos currentTarget) {
this.currentTarget = currentTarget;
}
public BlockPos getLastTarget() {
return lastTarget;
}
public void setLastTarget(BlockPos lastTarget) {
this.lastTarget = lastTarget;
}
public int getCurrentProgress() {
return (int) currentProgress;
}
public void setCurrentProgress(int currentProgress) {
this.currentProgress = currentProgress;
}
public boolean isActive(BlockState state) {
return true;
}
public boolean isMoving() {
return moving;
}
public void setMoving(boolean moving) {
this.moving = moving;
}
public Vec3i getCurrentDirection() {
return currentDirection;
}
public void setCurrentDirection(Vec3i currentDirection) {
this.currentDirection = currentDirection;
}
public abstract float getMoveTime();
public abstract float getWorkTime();
public ItemStack getToolheadAdditionalRender() {
return null;
}
public long getMoveStartedAt() {
return moveStartedAt;
}
public void setMoveStartedAt(long moveStartedAt) {
this.moveStartedAt = moveStartedAt;
}
}