huge perf improvements to underside manager and dimensional tears

This commit is contained in:
craisinlord committed 2026-09-01 22:27:52 -05:00
1 parent e173ae66f5
commit 4ecd7906d6
7 files changed
+168 -327

No files matched your search

@@ -22,8 +22,16 @@ public class DimensionalTearMarkerBlockEntity extends BlockEntity {
private static final int PARTNER_ORIGIN_ATTEMPTS = 12;
private static final int PARTNER_SEARCH_ATTEMPTS = 48;
private static final int LOCAL_SEARCH_ATTEMPTS = 18;
private static final int SEARCH_ATTEMPTS_PER_TICK = 2;
private int attemptTicks;
private int localAttempts;
private int partnerOriginAttempts;
private int partnerSearchAttempts;
@Nullable
private BlockPos firstPocket;
@Nullable
private BlockPos partnerOrigin;
public DimensionalTearMarkerBlockEntity(
BlockPos pos,
@@ -43,18 +51,40 @@ public class DimensionalTearMarkerBlockEntity extends BlockEntity {
private boolean trySpawnLinkedPair(ServerLevel level, BlockPos origin) {
RandomSource random = level.getRandom();
BlockPos first = findOpenPocket(level, origin, random, LOCAL_SEARCH_ATTEMPTS);
if (first == null) {
return false;
for (int i = 0; i < SEARCH_ATTEMPTS_PER_TICK; i++) {
if (firstPocket == null) {
if (localAttempts >= LOCAL_SEARCH_ATTEMPTS) {
return false;
}
localAttempts++;
BlockPos candidate = randomCandidate(origin, random);
if (isUsablePocket(level, candidate)) {
firstPocket = candidate;
}
continue;
}
if (partnerOrigin == null || partnerSearchAttempts >= PARTNER_SEARCH_ATTEMPTS) {
if (partnerOriginAttempts >= PARTNER_ORIGIN_ATTEMPTS) {
return false;
}
partnerOrigin = linkedOrigin(firstPocket, random);
partnerOriginAttempts++;
partnerSearchAttempts = 0;
}
partnerSearchAttempts++;
BlockPos candidate = randomCandidate(partnerOrigin, random);
if (!isUsablePocket(level, candidate)) {
continue;
}
return spawnPair(level, random, firstPocket, candidate);
}
BlockPos second = null;
for (int i = 0; i < PARTNER_ORIGIN_ATTEMPTS && second == null; i++) {
second = findOpenPocket(level, linkedOrigin(first, random), random, PARTNER_SEARCH_ATTEMPTS);
}
if (second == null) {
return false;
}
return false;
}
private boolean spawnPair(ServerLevel level, RandomSource random, BlockPos first, BlockPos second) {
int lifetime = Math.max(1200, AntarchySettings.dimensionalTearLifetimeTicks());
float yawA = random.nextFloat() * 360.0F;
@@ -68,6 +98,14 @@ public class DimensionalTearMarkerBlockEntity extends BlockEntity {
return true;
}
private static BlockPos randomCandidate(BlockPos origin, RandomSource random) {
return origin.offset(
Mth.nextInt(random, -16, 16),
Mth.nextInt(random, -24, 24),
Mth.nextInt(random, -16, 16)
);
}
private static BlockPos linkedOrigin(BlockPos first, RandomSource random) {
double angle = random.nextDouble() * Math.PI * 2.0D;
int distance = Mth.nextInt(random, MIN_LINK_RANGE, MAX_LINK_RANGE);
@@ -77,25 +115,12 @@ public class DimensionalTearMarkerBlockEntity extends BlockEntity {
return first.offset(dx, dy, dz);
}
@Nullable
private static BlockPos findOpenPocket(ServerLevel level, BlockPos origin, RandomSource random, int attempts) {
private static boolean isUsablePocket(ServerLevel level, BlockPos pos) {
int minY = level.getMinBuildHeight() + 8;
int maxY = level.getMaxBuildHeight() - 8;
for (int i = 0; i < attempts; i++) {
BlockPos candidate = origin.offset(
Mth.nextInt(random, -16, 16),
Mth.nextInt(random, -24, 24),
Mth.nextInt(random, -16, 16)
);
candidate = new BlockPos(candidate.getX(), Mth.clamp(candidate.getY(), minY, maxY), candidate.getZ());
if (isUsablePocket(level, candidate)) {
return candidate;
}
if (pos.getY() < minY || pos.getY() > maxY) {
return false;
}
return null;
}
private static boolean isUsablePocket(ServerLevel level, BlockPos pos) {
for (int dx = -2; dx <= 2; dx++) {
for (int dy = -2; dy <= 3; dy++) {
for (int dz = -4; dz <= 4; dz++) {
@@ -120,11 +145,25 @@ public class DimensionalTearMarkerBlockEntity extends BlockEntity {
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("attempt_ticks", this.attemptTicks);
tag.putInt("local_attempts", this.localAttempts);
tag.putInt("partner_origin_attempts", this.partnerOriginAttempts);
tag.putInt("partner_search_attempts", this.partnerSearchAttempts);
if (this.firstPocket != null) {
tag.putLong("first_pocket", this.firstPocket.asLong());
}
if (this.partnerOrigin != null) {
tag.putLong("partner_origin", this.partnerOrigin.asLong());
}
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
this.attemptTicks = tag.getInt("attempt_ticks");
this.localAttempts = tag.getInt("local_attempts");
this.partnerOriginAttempts = tag.getInt("partner_origin_attempts");
this.partnerSearchAttempts = tag.getInt("partner_search_attempts");
this.firstPocket = tag.contains("first_pocket") ? BlockPos.of(tag.getLong("first_pocket")) : null;
this.partnerOrigin = tag.contains("partner_origin") ? BlockPos.of(tag.getLong("partner_origin")) : null;
}
}
@@ -37,7 +37,8 @@ public final class DimensionalTearCommand {
}
private static Vec3 anchorPos(ServerPlayer player) {
return player.position().add(0.0D, VERTICAL_OFFSET, 0.0D);
Vec3 forward = player.getLookAngle().normalize();
return player.position().add(forward.scale(4.0D)).add(0.0D, VERTICAL_OFFSET, 0.0D);
}
private static int spawnSingle(CommandContext<CommandSourceStack> context) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
@@ -6,8 +6,10 @@ import com.craisinlord.antarchy.content.AntarchyTags;
import com.craisinlord.antarchy.content.entity.ManticoreEntity;
import com.craisinlord.antarchy.content.entity.royal.QueenEntity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
@@ -67,9 +69,12 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
private static final double PORTAL_RADIUS = 1.35D;
private static final double EXIT_OFFSET = 2.4D;
private static final Map<UUID, Long> TELEPORT_COOLDOWNS = new HashMap<>();
private static final Map<UUID, Set<UUID>> QUEEN_TEAR_IDS = new HashMap<>();
private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this);
private UUID linkedTearId;
@Nullable
private DimensionalTearEntity linkedTear;
private Vec3 linkedFallbackPos = Vec3.ZERO;
private int ageTicks;
private int lifetimeTicks = DEFAULT_LIFETIME_TICKS;
@@ -105,6 +110,7 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
DimensionalTearEntity tear = create(level, pos, yaw, lifetimeTicks);
tear.queenOwnerId = queenId;
tear.queenManticoreCount = Math.max(1, count);
QUEEN_TEAR_IDS.computeIfAbsent(queenId, ignored -> new HashSet<>()).add(tear.getUUID());
return tear;
}
@@ -119,6 +125,7 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
public void linkTo(DimensionalTearEntity other) {
this.linkedTearId = other.getUUID();
this.linkedFallbackPos = other.position();
this.linkedTear = other;
}
@Override
@@ -282,10 +289,15 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
}
public static void discardQueenOwnedTears(ServerLevel level, UUID queenId) {
for (DimensionalTearEntity tear : level.getEntitiesOfClass(DimensionalTearEntity.class,
new AABB(-3.0E7D, level.getMinBuildHeight(), -3.0E7D, 3.0E7D, level.getMaxBuildHeight(), 3.0E7D),
tear -> queenId.equals(tear.queenOwnerId))) {
tear.discard();
Set<UUID> tearIds = QUEEN_TEAR_IDS.remove(queenId);
if (tearIds == null) {
return;
}
for (UUID tearId : tearIds) {
Entity entity = level.getEntity(tearId);
if (entity instanceof DimensionalTearEntity tear) {
tear.discard();
}
}
}
@@ -351,11 +363,15 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
@Nullable
private DimensionalTearEntity findLinkedTear() {
if (this.linkedTear != null && this.linkedTear.isAlive()) {
return this.linkedTear;
}
if (!(this.level() instanceof ServerLevel serverLevel) || this.linkedTearId == null) {
return null;
}
Entity entity = serverLevel.getEntity(this.linkedTearId);
return entity instanceof DimensionalTearEntity tear ? tear : null;
this.linkedTear = entity instanceof DimensionalTearEntity tear ? tear : null;
return this.linkedTear;
}
private void removeLinkedPair() {
@@ -366,6 +382,20 @@ public class DimensionalTearEntity extends Entity implements GeoEntity {
this.discard();
}
@Override
public void remove(RemovalReason reason) {
if (this.queenOwnerId != null) {
Set<UUID> tearIds = QUEEN_TEAR_IDS.get(this.queenOwnerId);
if (tearIds != null) {
tearIds.remove(this.getUUID());
if (tearIds.isEmpty()) {
QUEEN_TEAR_IDS.remove(this.queenOwnerId);
}
}
}
super.remove(reason);
}
private int randomEventDelay() {
int min = Math.max(20, AntarchySettings.dimensionalTearEmergenceMinIntervalTicks());
int max = Math.max(min, AntarchySettings.dimensionalTearEmergenceMaxIntervalTicks());
@@ -1,268 +0,0 @@
package com.craisinlord.antarchy.content.worldgen.thoraxis;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.mojang.serialization.Codec;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
import net.minecraft.world.level.material.FluidState;
public final class ThoraxisAntiwaterPoolFeature extends Feature<ThoraxisAntiwaterPoolConfiguration> {
private static final int MIN_CAVITY_HEIGHT = 8;
private static final int MAX_CAVITY_HEIGHT = 40;
public ThoraxisAntiwaterPoolFeature(Codec<ThoraxisAntiwaterPoolConfiguration> codec) {
super(codec);
}
@Override
public boolean place(FeaturePlaceContext<ThoraxisAntiwaterPoolConfiguration> context) {
WorldGenLevel level = context.level();
BlockPos origin = context.origin();
RandomSource random = context.random();
ThoraxisAntiwaterPoolConfiguration config = context.config();
int radius = randomBetween(random, config.minRadius(), config.maxRadius());
int depth = randomBetween(random, config.minDepth(), config.maxDepth());
BlockState antiwaterSourceState = config.state().getState(random, origin);
BlockState nyxiteState = AntarchyObjects.NYXITE.get().defaultBlockState();
int pools = randomBetween(random, 1, Math.max(1, 1 + depth / 8));
boolean placedAny = false;
for (int i = 0; i < pools; i++) {
int offsetX = random.nextInt(radius * 2 + 1) - radius;
int offsetZ = random.nextInt(radius * 2 + 1) - radius;
int centerX = origin.getX() + offsetX;
int centerZ = origin.getZ() + offsetZ;
int floorY = findFloorY(level, centerX, centerZ);
if (floorY <= level.getMinBuildHeight()) {
continue;
}
int ceilingY = findCeilingY(level, centerX, centerZ, floorY);
if (ceilingY <= floorY + MIN_CAVITY_HEIGHT || ceilingY - floorY > MAX_CAVITY_HEIGHT) {
continue;
}
int poolRadius = randomBetween(random, Math.max(2, radius / 6), Math.max(3, radius / 4 + 1));
int ventCount = randomBetween(random, 0, Math.max(1, depth / 4));
placedAny |= placeSparsePool(level, random, centerX, centerZ, floorY, ceilingY, poolRadius, ventCount, antiwaterSourceState, nyxiteState);
}
return placedAny;
}
private boolean placeSparsePool(
WorldGenLevel level,
RandomSource random,
int centerX,
int centerZ,
int floorY,
int ceilingY,
int poolRadius,
int ventCount,
BlockState antiwaterSourceState,
BlockState nyxiteState
) {
boolean placedAny = false;
int ceilingBandY = ceilingY - 1;
int sourceY = floorY + 1;
placedAny |= placeNyxiteBase(level, random, centerX, floorY, centerZ, poolRadius, nyxiteState);
placedAny |= seedAntiwaterVent(level, sourceY, centerX, centerZ, antiwaterSourceState);
for (int i = 0; i < ventCount; i++) {
int ventX = centerX + random.nextInt(poolRadius * 2 + 1) - poolRadius;
int ventZ = centerZ + random.nextInt(poolRadius * 2 + 1) - poolRadius;
int distance = Math.abs(ventX - centerX) + Math.abs(ventZ - centerZ);
if (distance > poolRadius + random.nextInt(2)) {
continue;
}
placedAny |= placeNyxiteBase(level, random, ventX, floorY, ventZ, Math.max(1, poolRadius - 1), nyxiteState);
placedAny |= seedAntiwaterVent(level, sourceY, ventX, ventZ, antiwaterSourceState);
placedAny |= seedCeilingPocket(level, random, ventX, ventZ, ceilingBandY, Math.max(1, poolRadius - 1), antiwaterSourceState);
placedAny |= seedStem(level, ventX, ventZ, floorY, ceilingBandY, antiwaterSourceState);
}
return placedAny;
}
private static boolean placeNyxiteBase(WorldGenLevel level, RandomSource random, int centerX, int floorY, int centerZ, int radius, BlockState nyxiteState) {
boolean placedAny = false;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
double distance = Math.sqrt(dx * dx + dz * dz);
if (distance > radius + random.nextDouble()) {
continue;
}
double density = 1.0D - (distance / Math.max(1.0D, radius));
double chance = Mth.clamp(0.18D + density * 0.45D, 0.0D, 0.78D);
if (random.nextDouble() > chance) {
continue;
}
mutable.set(centerX + dx, floorY, centerZ + dz);
BlockState existing = level.getBlockState(mutable);
if (existing.is(Blocks.BEDROCK) || existing.blocksMotion()) {
continue;
}
level.setBlock(mutable, nyxiteState, 2);
placedAny = true;
}
}
return placedAny;
}
private static boolean seedAntiwaterVent(WorldGenLevel level, int y, int x, int z, BlockState antiwaterSourceState) {
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
FluidState fluidState = state.getFluidState();
if (state.is(Blocks.BEDROCK) || state.blocksMotion() || !fluidState.isEmpty()) {
return false;
}
level.setBlock(pos, antiwaterSourceState, 2);
scheduleFluidTick(level, pos, antiwaterSourceState);
return true;
}
private static boolean seedCeilingPocket(WorldGenLevel level, RandomSource random, int centerX, int centerZ, int ceilingBandY, int radius, BlockState antiwaterSourceState) {
boolean placedAny = false;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
BlockState flowingAntiwater = flowingAntiwaterState(antiwaterSourceState, Math.max(1, Math.min(7, 6 + random.nextInt(2))));
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
double distance = Math.sqrt(dx * dx + dz * dz);
if (distance > radius + random.nextDouble() * 0.5D) {
continue;
}
double density = 1.0D - (distance / Math.max(1.0D, radius));
double chance = Mth.clamp(0.22D + density * 0.52D, 0.0D, 0.84D);
if (random.nextDouble() > chance) {
continue;
}
mutable.set(centerX + dx, ceilingBandY, centerZ + dz);
BlockState existing = level.getBlockState(mutable);
FluidState fluidState = existing.getFluidState();
if (existing.is(Blocks.BEDROCK) || existing.blocksMotion() || !fluidState.isEmpty()) {
continue;
}
level.setBlock(mutable, flowingAntiwater, 2);
scheduleFluidTick(level, mutable, flowingAntiwater);
placedAny = true;
}
}
return placedAny;
}
private static boolean seedStem(WorldGenLevel level, int x, int z, int floorY, int ceilingBandY, BlockState antiwaterSourceState) {
boolean placedAny = false;
int stemHeight = Math.max(2, Math.min(ceilingBandY - floorY - 1, 5));
for (int i = 1; i <= stemHeight; i++) {
int y = floorY + i;
if (y >= ceilingBandY) {
break;
}
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
if (state.is(Blocks.BEDROCK) || state.blocksMotion()) {
break;
}
BlockState stemState = i == 1 ? antiwaterSourceState : flowingAntiwaterState(antiwaterSourceState, Math.max(1, 8 - i));
level.setBlock(pos, stemState, 2);
scheduleFluidTick(level, pos, stemState);
placedAny = true;
}
return placedAny;
}
private static BlockState flowingAntiwaterState(BlockState sourceState, int level) {
if (!sourceState.hasProperty(LiquidBlock.LEVEL)) {
return sourceState;
}
int clamped = Mth.clamp(level, 1, 7);
return sourceState.setValue(LiquidBlock.LEVEL, clamped);
}
private static void scheduleFluidTick(WorldGenLevel level, BlockPos pos, BlockState state) {
FluidState fluidState = state.getFluidState();
if (!fluidState.isEmpty()) {
level.scheduleTick(pos, fluidState.getType(), fluidState.getType().getTickDelay(level));
}
}
private static int findFloorY(WorldGenLevel level, int x, int z) {
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
int floorY = Integer.MIN_VALUE;
for (int y = level.getMinBuildHeight() + 1; y < level.getMaxBuildHeight() - 1; y++) {
mutable.set(x, y, z);
BlockState state = level.getBlockState(mutable);
if (!isSolidSurface(state)) {
continue;
}
mutable.set(x, y + 1, z);
if (isOpenBlock(level.getBlockState(mutable))) {
floorY = y;
}
}
return floorY;
}
private static int findCeilingY(WorldGenLevel level, int x, int z, int floorY) {
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int y = floorY + 2; y < level.getMaxBuildHeight() - 1; y++) {
mutable.set(x, y, z);
BlockState state = level.getBlockState(mutable);
if (!isSolidSurface(state)) {
continue;
}
mutable.set(x, y - 1, z);
if (isOpenBlock(level.getBlockState(mutable))) {
return y;
}
}
return Integer.MIN_VALUE;
}
private static boolean isSolidSurface(BlockState state) {
return !state.isAir() && state.blocksMotion() && state.getFluidState().isEmpty();
}
private static boolean isOpenBlock(BlockState state) {
return state.isAir();
}
private static int randomBetween(RandomSource random, int minInclusive, int maxInclusive) {
int min = Math.min(minInclusive, maxInclusive);
int max = Math.max(minInclusive, maxInclusive);
return min >= max ? min : min + random.nextInt(max - min + 1);
}
}
@@ -18,6 +18,8 @@ import java.util.ArrayList;
import java.util.List;
public final class ThoraxisBloodCrystalFeature extends Feature<ThoraxisBloodCrystalConfiguration> {
private static final int SURFACE_SEARCH_RANGE = 32;
public ThoraxisBloodCrystalFeature(Codec<ThoraxisBloodCrystalConfiguration> codec) {
super(codec);
}
@@ -199,7 +201,8 @@ public final class ThoraxisBloodCrystalFeature extends Feature<ThoraxisBloodCrys
int z = origin.getZ();
int startY = Mth.clamp(origin.getY(), level.getMinBuildHeight() + 1, level.getMaxBuildHeight() - 2);
for (int y = startY; y > level.getMinBuildHeight(); y--) {
int minY = Math.max(level.getMinBuildHeight() + 1, startY - SURFACE_SEARCH_RANGE);
for (int y = startY; y > minY; y--) {
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
if (state.isAir() || state.canBeReplaced() || !state.getFluidState().isEmpty()) {
@@ -219,7 +222,8 @@ public final class ThoraxisBloodCrystalFeature extends Feature<ThoraxisBloodCrys
int z = origin.getZ();
int startY = Mth.clamp(origin.getY(), level.getMinBuildHeight() + 1, level.getMaxBuildHeight() - 2);
for (int y = startY; y < level.getMaxBuildHeight() - 1; y++) {
int maxY = Math.min(level.getMaxBuildHeight() - 2, startY + SURFACE_SEARCH_RANGE);
for (int y = startY; y < maxY; y++) {
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
if (state.isAir() || state.canBeReplaced() || !state.getFluidState().isEmpty()) {
@@ -240,4 +244,3 @@ public final class ThoraxisBloodCrystalFeature extends Feature<ThoraxisBloodCrys
return min >= max ? min : min + random.nextInt(max - min + 1);
}
}
@@ -6,15 +6,17 @@ import com.craisinlord.antarchy.content.block.DreamSandBlock;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityApi;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityDirection;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityTransition;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
@@ -22,6 +24,7 @@ import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
public final class ThoraxisUndersideManager {
public static final int GRAVITY_FLIP_Y = 0;
@@ -29,27 +32,36 @@ public final class ThoraxisUndersideManager {
private static final AntarchyGravityTransition TRANSITION = new AntarchyGravityTransition(12);
private static final int EFFECT_DURATION_TICKS = 50;
private static final int EFFECT_REFRESH_THRESHOLD_TICKS = 10;
private static final int DISCOVERY_INTERVAL_TICKS = 5;
private static final int SIMULATION_DISTANCE_MARGIN_CHUNKS = 1;
private static final int MAX_FLIPS_PER_TICK = 8;
private static final int ENTER_UNDERSIDE_Y = GRAVITY_FLIP_Y - 4;
private static final int EXIT_UNDERSIDE_Y = GRAVITY_FLIP_Y + 4;
private static final int FLIP_COOLDOWN_TICKS = 40;
private static final Set<UUID> UNDERSIDE_FORCED_GRAVITY = new HashSet<>();
private static final Set<UUID> ACTIVE_ITEMS_SCRATCH = new HashSet<>();
private static final Map<UUID, Long> LAST_FLIP_TICK = new HashMap<>();
private static final Map<ServerLevel, TrackingState> STATES = new WeakHashMap<>();
private ThoraxisUndersideManager() {
}
public static void tick(ServerLevel level) {
if (!isThoraxis(level) || level.players().isEmpty()) {
if (!isThoraxis(level)) {
return;
}
TrackingState tracking = STATES.computeIfAbsent(level, ignored -> new TrackingState());
if (level.players().isEmpty()) {
tracking.clear();
STATES.remove(level);
return;
}
long now = level.getGameTime();
Set<UUID> activeThisTick = ACTIVE_ITEMS_SCRATCH;
activeThisTick.clear();
if (now % DISCOVERY_INTERVAL_TICKS == 0L) {
tracking.discover(level);
}
int flipsRemaining = MAX_FLIPS_PER_TICK;
for (Entity entity : level.getAllEntities()) {
for (Entity entity : tracking.entities) {
if (!entity.isAlive() || entity.isSpectator()) {
continue;
}
@@ -68,20 +80,19 @@ public final class ThoraxisUndersideManager {
}
UUID uuid = entity.getUUID();
boolean tracked = UNDERSIDE_FORCED_GRAVITY.contains(uuid);
boolean tracked = tracking.forcedItems.contains(entity);
boolean shouldInvert = tracked ? entity.getY() < EXIT_UNDERSIDE_Y : entity.getY() < ENTER_UNDERSIDE_Y;
if (shouldInvert) {
boolean needsFlip = AntarchyGravityApi.getGravityDirection(entity) != AntarchyGravityDirection.UP
|| !AntarchyGravityApi.isGravityForced(entity);
activeThisTick.add(uuid);
UNDERSIDE_FORCED_GRAVITY.add(uuid);
if (needsFlip && (flipsRemaining <= 0 || !flipReady(uuid, now))) {
tracking.forcedItems.add(entity);
if (needsFlip && (flipsRemaining <= 0 || !flipReady(tracking, uuid, now))) {
continue;
}
if (needsFlip) {
flipsRemaining--;
LAST_FLIP_TICK.put(uuid, now);
tracking.lastFlipTick.put(uuid, now);
AntarchyGravityApi.setForcedGravityDirection(entity, AntarchyGravityDirection.UP, TRANSITION);
}
continue;
@@ -90,24 +101,51 @@ public final class ThoraxisUndersideManager {
if (tracked) {
boolean isForcedUp = AntarchyGravityApi.getGravityDirection(entity) == AntarchyGravityDirection.UP
&& AntarchyGravityApi.isGravityForced(entity);
if (isForcedUp && !flipReady(uuid, now)) {
activeThisTick.add(uuid);
if (isForcedUp && !flipReady(tracking, uuid, now)) {
continue;
}
UNDERSIDE_FORCED_GRAVITY.remove(uuid);
tracking.forcedItems.remove(entity);
if (isForcedUp) {
LAST_FLIP_TICK.put(uuid, now);
tracking.lastFlipTick.put(uuid, now);
AntarchyGravityApi.setGravityDirection(entity, AntarchyGravityDirection.DOWN, false, TRANSITION);
}
}
}
UNDERSIDE_FORCED_GRAVITY.retainAll(activeThisTick);
LAST_FLIP_TICK.values().removeIf(t -> now - t > FLIP_COOLDOWN_TICKS * 4L);
tracking.entities.removeIf(entity -> !entity.isAlive() || entity.isRemoved());
tracking.forcedItems.removeIf(entity -> !entity.isAlive() || entity.isRemoved());
tracking.lastFlipTick.entrySet().removeIf(entry -> now - entry.getValue() > FLIP_COOLDOWN_TICKS * 4L);
}
private static boolean flipReady(UUID uuid, long now) {
Long last = LAST_FLIP_TICK.get(uuid);
private static final class TrackingState {
private final Set<Entity> entities = Collections.newSetFromMap(new IdentityHashMap<>());
private final Set<Entity> forcedItems = Collections.newSetFromMap(new IdentityHashMap<>());
private final Map<UUID, Long> lastFlipTick = new java.util.HashMap<>();
private void discover(ServerLevel level) {
int radius = (level.getServer().getPlayerList().getSimulationDistance() + SIMULATION_DISTANCE_MARGIN_CHUNKS) * 16;
for (ServerPlayer player : level.players()) {
AABB area = new AABB(
player.getX() - radius, level.getMinBuildHeight(), player.getZ() - radius,
player.getX() + radius, level.getMaxBuildHeight(), player.getZ() + radius
);
entities.add(player);
entities.addAll(level.getEntitiesOfClass(LivingEntity.class, area,
entity -> !entity.isSpectator() && entity.getY() < EXIT_UNDERSIDE_Y));
entities.addAll(level.getEntitiesOfClass(ItemEntity.class, area,
entity -> !entity.isSpectator() && entity.getY() < EXIT_UNDERSIDE_Y));
}
}
private void clear() {
entities.clear();
forcedItems.clear();
lastFlipTick.clear();
}
}
private static boolean flipReady(TrackingState tracking, UUID uuid, long now) {
Long last = tracking.lastFlipTick.get(uuid);
return last == null || now - last >= FLIP_COOLDOWN_TICKS;
}