Debug rendering for oxygen (VERY LAGGY BUT WORKS)

This commit is contained in:
XevianLight
2026-01-30 21:46:13 -07:00
parent cc93d2fb42
commit 012985441f
10 changed files with 611 additions and 48 deletions

View File

@@ -1,16 +1,23 @@
package net.xevianlight.aphelion;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent;
import net.neoforged.neoforge.event.AddReloadListenerEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.network.PacketDistributor;
import net.xevianlight.aphelion.block.dummy.renderer.MultiblockDummyRenderer;
import net.xevianlight.aphelion.block.entity.custom.renderer.OxygenTestRenderer;
import net.xevianlight.aphelion.client.AphelionConfig;
import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData;
import net.xevianlight.aphelion.network.packet.PartitionPayload;
import net.xevianlight.aphelion.planet.AphelionPlanetJSONLoader;
import net.xevianlight.aphelion.core.init.*;
import net.xevianlight.aphelion.fluid.BaseFluidType;
@@ -132,7 +139,7 @@ public class Aphelion {
@SubscribeEvent
public static void registerBER(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.VAF_MULTIBLOCK_DUMMY_ENTITY.get(), MultiblockDummyRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), OxygenTestRenderer::new);
// event.registerBlockEntityRenderer(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), OxygenTestRenderer::new);
}
@SubscribeEvent
@@ -146,5 +153,10 @@ public class Aphelion {
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.ROCKET.get(), RocketRenderer::new);
}
@SubscribeEvent
public static void onClientTick(ClientTickEvent.Post e) {
EnvironmentSavedData.refreshFromIntegratedServerIfNeeded(Minecraft.getInstance(), 64, 10000);
}
}
}

View File

@@ -1,19 +1,34 @@
package net.xevianlight.aphelion.block.custom;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.xevianlight.aphelion.block.entity.custom.OxygenTestBlockEntity;
import net.xevianlight.aphelion.block.entity.custom.TestBlockEntity;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import org.jetbrains.annotations.Nullable;
public class OxygenTestBlock extends Block implements EntityBlock {
public class OxygenTestBlock extends BaseEntityBlock {
public OxygenTestBlock(Properties properties) {
super(properties);
}
public static final MapCodec<OxygenTestBlock> CODEC = simpleCodec(OxygenTestBlock::new);
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new OxygenTestBlockEntity(blockPos, blockState);
@@ -22,4 +37,29 @@ public class OxygenTestBlock extends Block implements EntityBlock {
public static Properties getProperties() {
return Properties.of();
}
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
if (level.isClientSide) {
return null;
}
return createTickerHelper(blockEntityType, ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), (level1, blockPos, blockState, oxygenTestBlockEntity) -> oxygenTestBlockEntity.tick(level1, blockPos, blockState));
}
@Override
public RenderShape getRenderShape(BlockState pState) {
return RenderShape.MODEL;
}
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity= level.getBlockEntity(pos);
if (blockEntity instanceof OxygenTestBlockEntity oxygenTestBlockEntity) {
oxygenTestBlockEntity.removeEnclosed();
}
}
super.onRemove(state, level, pos, newState, movedByPiston);
}
}

View File

@@ -1,18 +1,17 @@
package net.xevianlight.aphelion.block.entity.custom;
import it.unimi.dsi.fastutil.longs.Long2ObjectArrayMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.SectionPos;
import net.minecraft.core.Vec3i;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import static net.xevianlight.aphelion.Aphelion.LOGGER;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData;
import net.xevianlight.aphelion.util.FloodFill3D;
import org.openjdk.nashorn.internal.runtime.regexp.joni.exception.ValueException;
import java.util.*;
@@ -43,11 +42,25 @@ public class OxygenTestBlockEntity extends BlockEntity {
return level != null && fastBlockState(pos).isAir();
}
public static final int MAX_RANGE = 100;
public static final int MAX_RANGE = 16;
public boolean isInRange(BlockPos pos1, BlockPos pos2) {
return Math.abs(pos1.getX() - pos2.getX()) + Math.abs(pos1.getY() - pos2.getY()) + Math.abs(pos1.getZ() - pos2.getZ()) <= MAX_RANGE;
}
public void removeEnclosed() {
if (enclosedCache != null) {
var singleplayerServer = Minecraft.getInstance().getSingleplayerServer();
if (singleplayerServer != null) {
var serverLevel = singleplayerServer.getLevel(level.dimension());
if (serverLevel != null) {
var savedData = EnvironmentSavedData.get(serverLevel);
savedData.resetOxygen(serverLevel, enclosedCache);
enclosedCache = Set.of();
}
}
}
}
/// 256*256*256 grid of booleans
private class BigBoolGrid {
int bitsSize;
@@ -94,7 +107,7 @@ public class OxygenTestBlockEntity extends BlockEntity {
// wrapping order is X->Z->Y, so we go along the X axis,
// wrap to the Z axis to make a square, and once the square is full
// we step up once along the Y axis.
int bitPos = inX & 7; // bottom 4 bits of X is bit pos
int bitPos = inX & 15; // bottom 4 bits of X is bit pos
int bit = (1 << bitPos);
// First (bitsSize-4) bits are for X, next (bitsSize) bits are for Z, next (bitsSize) bits are for Y
@@ -106,22 +119,29 @@ public class OxygenTestBlockEntity extends BlockEntity {
}
}
private List<BlockPos> enclosedCache;
public List<BlockPos> getEnclosedBlocks() {
if (level == null) return List.of();
if (enclosedCache != null) return enclosedCache;
private Set<BlockPos> enclosedCache;
public Set<BlockPos> getEnclosedCache() {
return enclosedCache;
}
public Set<BlockPos> getEnclosedBlocks() {
if (level == null) return Set.of();
// if (enclosedCache != null) return enclosedCache;
long start = System.nanoTime();
List<BlockPos> enclosedBlocks = new ArrayList<>();
// make this bitch BIIIIIG
BigBoolGrid seen = new BigBoolGrid(8, this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ());
Set<BlockPos> enclosedBlocks = FloodFill3D.run(level, getBlockPos(), 6000, FloodFill3D.TEST_FULL_SEAL, true);
// It's... a reasonable assumption that we won't have to include more blocks at once than the area of a sphere?
// maybe a bit more, IDK how exactly it scales to blocks.
Stack<BlockPos> stack = new Stack<>();
Stack<Integer> radiusStack = new Stack<>();
stack.add(this.getBlockPos());
// // make this bitch BIIIIIG
// BigBoolGrid seen = new BigBoolGrid(8, this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ());
//
// // It's... a reasonable assumption that we won't have to include more blocks at once than the area of a sphere?
// // maybe a bit more, IDK how exactly it scales to blocks.
// Stack<BlockPos> stack = new Stack<>();
// Stack<Integer> radiusStack = new Stack<>();
//
// stack.add(this.getBlockPos());
// Do flood fill out from this block
// Push on the top of the stack (newest), pop from the bottom of the stack (oldest).
@@ -130,21 +150,48 @@ public class OxygenTestBlockEntity extends BlockEntity {
// and you'd see that every pos of layer 1 is together, then layer 2, then layer 3...
BlockPos ourPos = getBlockPos();
while (!stack.isEmpty()) {
BlockPos spreadFromPos = stack.pop();
for (Direction d : Direction.values()) {
BlockPos relativePos = spreadFromPos.relative(d);
// BlockPos ourPos = getBlockPos();
// while (!stack.isEmpty()) {
// BlockPos spreadFromPos = stack.pop();
// for (Direction d : Direction.values()) {
// BlockPos relativePos = spreadFromPos.relative(d);
//
// if (isInRange(relativePos, ourPos) && canSpreadTo(relativePos)) {
// // seen.add runs seen.contains under the hood,
// // + this is the most expensive operation.
// // should save a lot of time!
// if (seen.add(relativePos.getX(), relativePos.getY(), relativePos.getZ())) {
// enclosedBlocks.add(relativePos);
// stack.add(relativePos);
// }
// }
// }
// }
if (isInRange(relativePos, ourPos) && canSpreadTo(relativePos)) {
// seen.add runs seen.contains under the hood,
// + this is the most expensive operation.
// should save a lot of time!
if (seen.add(relativePos.getX(), relativePos.getY(), relativePos.getZ())) {
enclosedBlocks.add(relativePos);
stack.add(relativePos);
var singleplayerServer = Minecraft.getInstance().getSingleplayerServer();
if (singleplayerServer != null) {
var serverLevel = singleplayerServer.getLevel(level.dimension());
if (serverLevel != null) {
// Build a set of longs for the newly computed blocks (order-independent)
boolean changed = isChanged(enclosedBlocks);
// boolean changed = false;
if (changed) {
var savedData = EnvironmentSavedData.get(serverLevel);
// Revert old affected area back to defaults
if (enclosedCache != null) {
savedData.resetOxygen(serverLevel, enclosedCache);
}
// Apply oxygen to new affected area
savedData.setOxygen(serverLevel, enclosedBlocks, true);
LOGGER.info("Saved data for {} blocks to leveldata", enclosedBlocks.size());
}
// Update the cache no matter what (so next compare is correct)
enclosedCache = enclosedBlocks;
}
}
long durationNanos = System.nanoTime() - start;
@@ -152,12 +199,49 @@ public class OxygenTestBlockEntity extends BlockEntity {
LOGGER.info("Flood fill completed in {}µs, {} blocks at {}µs/block", durationMicros, enclosedBlocks.size(), durationMicros / enclosedBlocks.size());
enclosedCache = enclosedBlocks;
return enclosedBlocks;
return enclosedCache;
}
private boolean isChanged(Set<BlockPos> enclosedBlocks) {
LongOpenHashSet newSet = new LongOpenHashSet(enclosedBlocks.size());
for (BlockPos p : enclosedBlocks) {
newSet.add(p.asLong());
}
// Build a set of longs for the cached blocks (if any)
LongOpenHashSet oldSet = null;
if (enclosedCache != null) {
oldSet = new LongOpenHashSet(enclosedCache.size());
for (BlockPos p : enclosedCache) {
oldSet.add(p.asLong());
}
}
// Only save if the set of affected blocks has changed
boolean changed = (oldSet == null) || !oldSet.equals(newSet);
return changed;
}
private void helper() {
var myVar = new BlockPos(1, 1, 1).hashCode();
}
int ticks = 0;
int refreshAfter = 20;
public void tick(Level level, BlockPos pos, BlockState blockState) {
if (level.isClientSide) return;
ticks++;
if (ticks >= refreshAfter) {
getEnclosedBlocks();
ticks = 0;
// UNCOMMENT FOR DEBUG ONLY!!! EXTREMELY TPS INTENSIVE!!!
// EnvironmentSavedData.refreshFromIntegratedServerIfNeeded(Minecraft.getInstance(), 64, 10000);
}
}
}

View File

@@ -12,6 +12,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import net.minecraft.world.phys.AABB;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.block.entity.custom.OxygenTestBlockEntity;
import org.joml.Matrix4f;
import org.joml.Vector3f;
@@ -23,7 +24,10 @@ public class OxygenTestRenderer implements BlockEntityRenderer<OxygenTestBlockEn
public OxygenTestRenderer (BlockEntityRendererProvider.Context context) {}
private List<BlockPos> toBlockPositions(OxygenTestBlockEntity be) {
return be.getEnclosedBlocks();
// cache = be.getEnclosedBlocks();
// if (cache != null)
// return cache;
return List.of();
}
@Override
@@ -31,6 +35,8 @@ public class OxygenTestRenderer implements BlockEntityRenderer<OxygenTestBlockEn
return AABB.ofSize(blockEntity.getBlockPos().getCenter(), OxygenTestBlockEntity.MAX_RANGE*2, OxygenTestBlockEntity.MAX_RANGE*2, OxygenTestBlockEntity.MAX_RANGE*2);
}
List<BlockPos> cache;
private Set<BlockPos> relativePositionsCache;
@Override
// If in debug mode, renders a model made from the blocks
@@ -42,7 +48,7 @@ public class OxygenTestRenderer implements BlockEntityRenderer<OxygenTestBlockEn
// Renderers are relative to our block pos, so transform all points to be relative to block pos as well
List<BlockPos> positionsToRender = toBlockPositions(be);
BlockPos originPos = be.getBlockPos();
if (true) return;
// if (true) return;
Set<BlockPos> relativePositions;
if (relativePositionsCache != null) relativePositions = relativePositionsCache;

View File

@@ -2,6 +2,8 @@ package net.xevianlight.aphelion.client;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
@@ -11,6 +13,7 @@ import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.client.dimension.DimensionRenderer;
import net.xevianlight.aphelion.client.dimension.DimensionRendererCache;
import net.xevianlight.aphelion.client.dimension.SpaceSkyEffects;
import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData;
import net.xevianlight.aphelion.core.saveddata.SpacePartitionSavedData;
import net.xevianlight.aphelion.util.SpacePartitionHelper;
@@ -51,5 +54,12 @@ public class AphelionDebugOverlay {
// event.getLeft().add(" Sky: " + rendererSummary);
event.getLeft().add(" Station: " + x + " " + z + " ID: " + SpacePartitionSavedData.pack(x,z));
event.getLeft().add(" Station Destination:" + PartitionClientState.lastData().getDestination());
var server = mc.getSingleplayerServer();
ServerLevel singlePlayerLevel;
if (server != null) {
singlePlayerLevel = server.getLevel(mc.level.dimension());
if (singlePlayerLevel != null)
event.getLeft().add(" Oxygen: " + EnvironmentSavedData.get(singlePlayerLevel).hasOxygen(singlePlayerLevel, mc.player.blockPosition()));
}
}
}

View File

@@ -0,0 +1,10 @@
package net.xevianlight.aphelion.client;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.core.BlockPos;
public final class ClientOxygenCache {
public static final LongOpenHashSet OXYGEN = new LongOpenHashSet();
public static BlockPos lastCenter = BlockPos.ZERO;
public static long lastUpdateGameTime = -1;
}

View File

@@ -0,0 +1,118 @@
package net.xevianlight.aphelion.client;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.RenderLevelStageEvent;
import net.xevianlight.aphelion.Aphelion;
import org.joml.Matrix4f;
@EventBusSubscriber(modid = Aphelion.MOD_ID, value = Dist.CLIENT)
public final class OxygenDebugRender {
// Untextured translucent quads (POSITION_COLOR only)
private static final RenderType OXYGEN_FILL = RenderType.create(
"aphelion_oxygen_fill",
DefaultVertexFormat.POSITION_COLOR,
VertexFormat.Mode.QUADS,
256,
false,
true,
RenderType.CompositeState.builder()
.setShaderState(RenderStateShard.POSITION_COLOR_SHADER)
.setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY)
.setCullState(RenderStateShard.NO_CULL)
.setDepthTestState(RenderStateShard.LEQUAL_DEPTH_TEST)
.setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE)
.createCompositeState(true)
);
@SubscribeEvent
public static void onRenderLevel(RenderLevelStageEvent event) {
// One stage only (pick one that exists and looks good)
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS) return;
Minecraft mc = Minecraft.getInstance();
if (mc.level == null || mc.player == null) return;
if (!mc.gui.getDebugOverlay().showDebugScreen()) return;
PoseStack poseStack = event.getPoseStack();
var cam = mc.gameRenderer.getMainCamera();
var camPos = cam.getPosition();
poseStack.pushPose();
poseStack.translate(-camPos.x, -camPos.y, -camPos.z);
MultiBufferSource.BufferSource bufferSource = mc.renderBuffers().bufferSource();
VertexConsumer vc = bufferSource.getBuffer(OXYGEN_FILL);
// Render surface faces only (fast + pretty)
for (long l : ClientOxygenCache.OXYGEN) {
BlockPos p = BlockPos.of(l);
drawSurfaceFaces(poseStack, vc, p);
}
poseStack.popPose();
bufferSource.endBatch(OXYGEN_FILL);
}
private static void drawSurfaceFaces(PoseStack poseStack, VertexConsumer vc, BlockPos p) {
// Neighbor checks: only render faces exposed to non-oxygen
boolean up = ClientOxygenCache.OXYGEN.contains(p.above().asLong());
boolean down = ClientOxygenCache.OXYGEN.contains(p.below().asLong());
boolean north = ClientOxygenCache.OXYGEN.contains(p.north().asLong());
boolean south = ClientOxygenCache.OXYGEN.contains(p.south().asLong());
boolean east = ClientOxygenCache.OXYGEN.contains(p.east().asLong());
boolean west = ClientOxygenCache.OXYGEN.contains(p.west().asLong());
if (up && down && north && south && east && west) return;
final float eps = 0.0025f;
float x0 = p.getX() + eps;
float y0 = p.getY() + eps;
float z0 = p.getZ() + eps;
float x1 = p.getX() + 1 - eps;
float y1 = p.getY() + 1 - eps;
float z1 = p.getZ() + 1 - eps;
// Color (ARGB-ish but as floats)
float r = 0.2f, g = 0.8f, b = 1.0f, a = 0.18f;
Matrix4f mat = poseStack.last().pose();
// IMPORTANT: vertex winding should be consistent (counter-clockwise)
if (!up) quad(mat, vc, x0,y1,z0, x1,y1,z0, x1,y1,z1, x0,y1,z1, r,g,b,a);
if (!down) quad(mat, vc, x0,y0,z1, x1,y0,z1, x1,y0,z0, x0,y0,z0, r,g,b,a);
if (!north) quad(mat, vc, x1,y0,z0, x0,y0,z0, x0,y1,z0, x1,y1,z0, r,g,b,a);
if (!south) quad(mat, vc, x0,y0,z1, x1,y0,z1, x1,y1,z1, x0,y1,z1, r,g,b,a);
if (!east) quad(mat, vc, x1,y0,z1, x1,y0,z0, x1,y1,z0, x1,y1,z1, r,g,b,a);
if (!west) quad(mat, vc, x0,y0,z0, x0,y0,z1, x0,y1,z1, x0,y1,z0, r,g,b,a);
}
private static void quad(
Matrix4f mat, VertexConsumer vc,
float x0, float y0, float z0,
float x1, float y1, float z1,
float x2, float y2, float z2,
float x3, float y3, float z3,
float r, float g, float b, float a
) {
// POSITION_COLOR format: ONLY position + color.
vc.addVertex(mat, x0, y0, z0).setColor(r, g, b, a);
vc.addVertex(mat, x1, y1, z1).setColor(r, g, b, a);
vc.addVertex(mat, x2, y2, z2).setColor(r, g, b, a);
vc.addVertex(mat, x3, y3, z3).setColor(r, g, b, a);
}
}

View File

@@ -1,6 +1,9 @@
package net.xevianlight.aphelion.core.saveddata;
import com.jcraft.jorbis.Block;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
@@ -9,6 +12,7 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.SavedData;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.client.ClientOxygenCache;
import net.xevianlight.aphelion.core.saveddata.types.EnvironmentData;
import net.xevianlight.aphelion.planet.Planet;
import net.xevianlight.aphelion.planet.PlanetCache;
@@ -60,7 +64,7 @@ public class EnvironmentSavedData extends SavedData {
if (!tag.contains("Position", Tag.TAG_LONG_ARRAY) || !tag.contains("Value", Tag.TAG_INT_ARRAY)) { return data; }
long[] positions = tag.getLongArray("Positions");
long[] positions = tag.getLongArray("Position");
int[] values = tag.getIntArray("Value");
int length = Math.min(positions.length, values.length);
@@ -89,7 +93,7 @@ public class EnvironmentSavedData extends SavedData {
}
public void setDataForPosition(Level level, BlockPos pos, EnvironmentData data) {
putOrRemove(pos.asLong(), data.pack());
putOrRemove(level, pos.asLong(), data.pack());
}
public boolean hasOxygen(Level level, BlockPos pos) {
@@ -100,8 +104,8 @@ public class EnvironmentSavedData extends SavedData {
public void setOxygen(Level level, BlockPos pos, boolean value) {
var data = getDataForPosition(level, pos);
data.setOxygen(value);
Aphelion.LOGGER.info("Set oxygen for {} to {}", pos, value);
putOrRemove(pos.asLong(), data.pack());
// Aphelion.LOGGER.info("Set oxygen for {} to {}", pos, value);
putOrRemove(level, pos.asLong(), data.pack());
}
public void setOxygen(Level level, Collection<BlockPos> positions, boolean value) {
@@ -112,8 +116,14 @@ public class EnvironmentSavedData extends SavedData {
public void resetOxygen(Level level, BlockPos pos) {
var data = getDataForPosition(level, pos);
data.setOxygen(EnvironmentData.DEFAULT_OXYGEN);
putOrRemove(pos.asLong(), data.pack());
data.setOxygen(defaultData(level).hasOxygen());
putOrRemove(level, pos.asLong(), data.pack());
}
public void resetOxygen(Level level, Collection<BlockPos> positions) {
for (BlockPos pos : positions) {
resetOxygen(level, pos);
}
}
public float getGravity(Level level, BlockPos pos) {
@@ -124,7 +134,7 @@ public class EnvironmentSavedData extends SavedData {
public void setGravity(Level level, BlockPos pos, float value) {
var data = getDataForPosition(level, pos);
data.setGravity(value);
putOrRemove(pos.asLong(), data.pack());
putOrRemove(level, pos.asLong(), data.pack());
}
public void setGravity(Level level, Collection<BlockPos> positions, float value) {
@@ -133,6 +143,12 @@ public class EnvironmentSavedData extends SavedData {
}
}
public void resetGravity(Level level, BlockPos pos) {
var data = getDataForPosition(level, pos);
data.setGravity(defaultData(level).getGravity());
putOrRemove(level, pos.asLong(), data.pack());
}
public short getTemperature(Level level, BlockPos pos) {
var data = getDataForPosition(level, pos);
return data.getTemperature();
@@ -141,7 +157,7 @@ public class EnvironmentSavedData extends SavedData {
public void setTemperature(Level level, BlockPos pos, short value) {
var data = getDataForPosition(level, pos);
data.setTemperature(value);
putOrRemove(pos.asLong(), data.pack());
putOrRemove(level, pos.asLong(), data.pack());
}
public void setTemperature(Level level, Collection<BlockPos> positions, short value) {
@@ -150,8 +166,14 @@ public class EnvironmentSavedData extends SavedData {
}
}
private void putOrRemove(long key, int packed) {
if (packed == EnvironmentData.DEFAULT_PACKED) {
public void resetTemperature(Level level, BlockPos pos) {
var data = getDataForPosition(level, pos);
data.setTemperature(defaultData(level).getTemperature());
putOrRemove(level, pos.asLong(), data.pack());
}
private void putOrRemove(Level level, long key, int packed) {
if (packed == defaultPacked(level)) {
envData.remove(key);
} else {
envData.put(key, packed);
@@ -159,10 +181,82 @@ public class EnvironmentSavedData extends SavedData {
setDirty();
}
private static int defaultPacked(Level level) {
Planet planet = PlanetCache.getByDimensionOrNull(level.dimension());
if (planet == null) return EnvironmentData.DEFAULT_PACKED;
// NOTE: adjust gravity/temperature defaults to whatever your data model intends
EnvironmentData planetData = new EnvironmentData(
planet.oxygen(),
EnvironmentData.DEFAULT_TEMPERATURE,
(short) planet.gravity()
);
return planetData.pack();
}
private static EnvironmentData defaultData(Level level) {
return EnvironmentData.unpack(defaultPacked(level));
}
public static EnvironmentSavedData get(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(
new Factory<>(EnvironmentSavedData::create, EnvironmentSavedData::load),
NAME
);
}
public static void refreshFromIntegratedServerIfNeeded(Minecraft mc, int radius, int maxBlocks) {
if (mc.level == null || mc.player == null) return;
long gameTime = mc.level.getGameTime();
if (ClientOxygenCache.lastUpdateGameTime != -1 && gameTime - ClientOxygenCache.lastUpdateGameTime < 20) return; // every 1s
BlockPos center = mc.player.blockPosition();
if (center.distManhattan(ClientOxygenCache.lastCenter) < 1) return; // dont refresh if player barely moved
var server = mc.getSingleplayerServer();
if (server == null) return;
// IMPORTANT: execute on server thread
server.execute(() -> {
var serverLevel = server.getLevel(mc.level.dimension());
if (serverLevel == null) return;
EnvironmentSavedData env = EnvironmentSavedData.get(serverLevel);
LongOpenHashSet found = new LongOpenHashSet();
int r = radius;
int scanned = 0;
// Scan a cube-ish region
for (int dy = -r; dy <= r; dy++) {
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
if (found.size() >= maxBlocks) break;
BlockPos p = center.offset(dx, dy, dz);
// optional: skip non-air or skip solid blocks (visual preference)
// if (!serverLevel.getBlockState(p).isAir()) continue;
if (env.hasOxygen(serverLevel, p)) {
found.add(p.asLong());
}
scanned++;
}
}
}
// Copy results back to client thread safely
mc.execute(() -> {
ClientOxygenCache.OXYGEN.clear();
ClientOxygenCache.OXYGEN.addAll(found);
ClientOxygenCache.lastCenter = center;
ClientOxygenCache.lastUpdateGameTime = gameTime;
});
});
}
}

View File

@@ -0,0 +1,189 @@
package net.xevianlight.aphelion.util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* Standalone flood-fill utility.
* - Traverses blocks starting from origin
* - Visits only positions allowed by Passable predicate
* - Bounded by maxRange (Manhattan distance)
* - Returns all visited positions (excluding origin by default)
*/
public final class TechnoFloodFill {
private TechnoFloodFill() {}
@FunctionalInterface
public interface Passable {
boolean test(Level level, BlockPos pos, BlockState state);
}
/** Convenience predicate: treat air as passable. */
public static final Passable AIR_ONLY = (level, pos, state) -> state.isAir();
/**
* Runs a bounded flood fill.
*
* @param level the world
* @param origin starting position (typically BE position)
* @param maxRange Manhattan range limit (|dx|+|dy|+|dz|)
* @param passable which blocks can be entered/added
* @param includeOrigin whether to include origin in the returned list
*/
public static List<BlockPos> run(Level level, BlockPos origin, int maxRange, Passable passable, boolean includeOrigin) {
if (level == null) return List.of();
// Choose a grid size big enough to cover maxRange in all axes.
// We need coordinates within [-maxRange, +maxRange] around origin.
// So size must be >= (2*maxRange + 1). Next power-of-two for cheap indexing.
int needed = 2 * maxRange + 1;
int sizePow2 = nextPow2(needed);
int bits = Integer.numberOfTrailingZeros(sizePow2); // since pow2
BigVisitedGrid seen = new BigVisitedGrid(bits, origin.getX(), origin.getY(), origin.getZ());
// Chunk-cached blockstate fetch (no BE state needed)
ChunkCache chunkCache = new ChunkCache(level);
List<BlockPos> out = new ArrayList<>();
Deque<BlockPos> stack = new ArrayDeque<>();
if (includeOrigin) {
out.add(origin);
}
// Mark origin visited so we don't bounce back into it.
seen.add(origin.getX(), origin.getY(), origin.getZ());
stack.push(origin);
while (!stack.isEmpty()) {
BlockPos from = stack.pop();
for (Direction d : Direction.values()) {
BlockPos next = from.relative(d);
if (!inRangeManhattan(origin, next, maxRange)) continue;
// visited check first: cheapest early-out
if (!seen.add(next.getX(), next.getY(), next.getZ())) continue;
BlockState st = chunkCache.getBlockState(next);
if (!passable.test(level, next, st)) continue;
out.add(next);
stack.push(next);
}
}
return out;
}
private static boolean inRangeManhattan(BlockPos a, BlockPos b, int max) {
int dx = Math.abs(a.getX() - b.getX());
int dy = Math.abs(a.getY() - b.getY());
int dz = Math.abs(a.getZ() - b.getZ());
return dx + dy + dz <= max;
}
private static int nextPow2(int x) {
// smallest power of 2 >= x, with a minimum of 8
int v = 1;
while (v < x) v <<= 1;
return Math.max(v, 8);
}
/**
* Simple chunk cache for repeated reads in flood fill.
*/
private static final class ChunkCache {
private final Level level;
private LevelChunk lastChunk;
private int lastCx = Integer.MIN_VALUE;
private int lastCz = Integer.MIN_VALUE;
private ChunkCache(Level level) {
this.level = level;
}
BlockState getBlockState(BlockPos pos) {
int cx = pos.getX() >> 4;
int cz = pos.getZ() >> 4;
if (cx != lastCx || cz != lastCz || lastChunk == null) {
lastChunk = level.getChunk(cx, cz); // may load/generate; okay for your current usage
lastCx = cx;
lastCz = cz;
}
return lastChunk.getBlockState(pos);
}
}
/**
* Packed visited grid centered on an origin.
*
* Uses an int[] with 32 bits per word:
* - size = 2^bits (must be power-of-two)
* - Word index layout: ((y * size) + z) * wordsPerRow + (x / 32)
* - Bit inside the word: (x % 32)
*/
private static final class BigVisitedGrid {
private final int bits;
private final int size;
private final int wordsPerRow;
private final int[] words;
private final int xOff, yOff, zOff;
BigVisitedGrid(int bits, int xOrigin, int yOrigin, int zOrigin) {
if (bits < 3) throw new IllegalArgumentException("Grid too small (bits=" + bits + ")");
if (bits > 12) throw new IllegalArgumentException("Grid too large (bits=" + bits + ")");
this.bits = bits;
this.size = 1 << bits;
// Center origin at middle of grid
this.xOff = -xOrigin + (size / 2);
this.yOff = -yOrigin + (size / 2);
this.zOff = -zOrigin + (size / 2);
if ((size & 31) != 0) {
// to keep wordsPerRow integer
throw new IllegalArgumentException("Grid size must be divisible by 32, got " + size);
}
this.wordsPerRow = size >>> 5; // size / 32
int totalWords = wordsPerRow * size * size; // (y,z) rows * x-words
this.words = new int[totalWords];
}
/**
* @return true if it was NOT previously visited and is now marked visited
*/
boolean add(int x, int y, int z) {
int inX = x + xOff;
int inY = y + yOff;
int inZ = z + zOff;
// Bounds check (fast and safe)
if ((inX | inY | inZ) < 0 || inX >= size || inY >= size || inZ >= size) {
return false; // out of grid => treat as "already seen" to prevent expansion
}
int wordIndex = ((inY * size) + inZ) * wordsPerRow + (inX >>> 5);
int bit = 1 << (inX & 31);
int prev = words[wordIndex];
if ((prev & bit) != 0) return false;
words[wordIndex] = prev | bit;
return true;
}
}
}

View File

@@ -3,5 +3,5 @@
"orbit_distance": 1,
"star_system": "aphelon:sol",
"gravity": 1,
"oxygen": true
"oxygen": false
}