From 012985441f4e2f37a3413b0cfe8571425e4e7f0f Mon Sep 17 00:00:00 2001 From: XevianLight <63034748+XevianLight@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:46:13 -0700 Subject: [PATCH] Debug rendering for oxygen (VERY LAGGY BUT WORKS) --- .../net/xevianlight/aphelion/Aphelion.java | 14 +- .../block/custom/OxygenTestBlock.java | 42 +++- .../entity/custom/OxygenTestBlockEntity.java | 150 +++++++++++--- .../custom/renderer/OxygenTestRenderer.java | 10 +- .../aphelion/client/AphelionDebugOverlay.java | 10 + .../aphelion/client/ClientOxygenCache.java | 10 + .../aphelion/client/OxygenDebugRender.java | 118 +++++++++++ .../core/saveddata/EnvironmentSavedData.java | 114 ++++++++++- .../aphelion/util/TechnoFloodFill.java | 189 ++++++++++++++++++ .../data/aphelion/planet/overworld.json | 2 +- 10 files changed, 611 insertions(+), 48 deletions(-) create mode 100644 src/main/java/net/xevianlight/aphelion/client/ClientOxygenCache.java create mode 100644 src/main/java/net/xevianlight/aphelion/client/OxygenDebugRender.java create mode 100644 src/main/java/net/xevianlight/aphelion/util/TechnoFloodFill.java diff --git a/src/main/java/net/xevianlight/aphelion/Aphelion.java b/src/main/java/net/xevianlight/aphelion/Aphelion.java index 3e45d9f..4510d86 100644 --- a/src/main/java/net/xevianlight/aphelion/Aphelion.java +++ b/src/main/java/net/xevianlight/aphelion/Aphelion.java @@ -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); + } } } diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/OxygenTestBlock.java b/src/main/java/net/xevianlight/aphelion/block/custom/OxygenTestBlock.java index 7f98102..5c615ae 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/OxygenTestBlock.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/OxygenTestBlock.java @@ -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 CODEC = simpleCodec(OxygenTestBlock::new); + + @Override + protected MapCodec 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 BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType 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); + } } diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/OxygenTestBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/OxygenTestBlockEntity.java index 4e24eab..79bbe22 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/OxygenTestBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/OxygenTestBlockEntity.java @@ -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 enclosedCache; - public List getEnclosedBlocks() { - if (level == null) return List.of(); - if (enclosedCache != null) return enclosedCache; + private Set enclosedCache; + + + public Set getEnclosedCache() { + return enclosedCache; + } + + public Set getEnclosedBlocks() { + if (level == null) return Set.of(); +// if (enclosedCache != null) return enclosedCache; long start = System.nanoTime(); - List enclosedBlocks = new ArrayList<>(); - // make this bitch BIIIIIG - BigBoolGrid seen = new BigBoolGrid(8, this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ()); + Set 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 stack = new Stack<>(); - Stack 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 stack = new Stack<>(); +// Stack 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 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); + } + } } diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/OxygenTestRenderer.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/OxygenTestRenderer.java index 6ea54f0..604c52d 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/OxygenTestRenderer.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/OxygenTestRenderer.java @@ -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 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 cache; + private Set relativePositionsCache; @Override // If in debug mode, renders a model made from the blocks @@ -42,7 +48,7 @@ public class OxygenTestRenderer implements BlockEntityRenderer positionsToRender = toBlockPositions(be); BlockPos originPos = be.getBlockPos(); - if (true) return; +// if (true) return; Set relativePositions; if (relativePositionsCache != null) relativePositions = relativePositionsCache; diff --git a/src/main/java/net/xevianlight/aphelion/client/AphelionDebugOverlay.java b/src/main/java/net/xevianlight/aphelion/client/AphelionDebugOverlay.java index 9d40ff1..7a5b604 100644 --- a/src/main/java/net/xevianlight/aphelion/client/AphelionDebugOverlay.java +++ b/src/main/java/net/xevianlight/aphelion/client/AphelionDebugOverlay.java @@ -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())); + } } } \ No newline at end of file diff --git a/src/main/java/net/xevianlight/aphelion/client/ClientOxygenCache.java b/src/main/java/net/xevianlight/aphelion/client/ClientOxygenCache.java new file mode 100644 index 0000000..012cf51 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/client/ClientOxygenCache.java @@ -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; +} diff --git a/src/main/java/net/xevianlight/aphelion/client/OxygenDebugRender.java b/src/main/java/net/xevianlight/aphelion/client/OxygenDebugRender.java new file mode 100644 index 0000000..f9e9423 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/client/OxygenDebugRender.java @@ -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); + } +} diff --git a/src/main/java/net/xevianlight/aphelion/core/saveddata/EnvironmentSavedData.java b/src/main/java/net/xevianlight/aphelion/core/saveddata/EnvironmentSavedData.java index 933ca6d..8535f71 100644 --- a/src/main/java/net/xevianlight/aphelion/core/saveddata/EnvironmentSavedData.java +++ b/src/main/java/net/xevianlight/aphelion/core/saveddata/EnvironmentSavedData.java @@ -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 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 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 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 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; // don’t 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; + }); + }); + } + } \ No newline at end of file diff --git a/src/main/java/net/xevianlight/aphelion/util/TechnoFloodFill.java b/src/main/java/net/xevianlight/aphelion/util/TechnoFloodFill.java new file mode 100644 index 0000000..d930e57 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/util/TechnoFloodFill.java @@ -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 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 out = new ArrayList<>(); + Deque 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; + } + } +} diff --git a/src/main/resources/data/aphelion/planet/overworld.json b/src/main/resources/data/aphelion/planet/overworld.json index 7b594d2..d875eb5 100644 --- a/src/main/resources/data/aphelion/planet/overworld.json +++ b/src/main/resources/data/aphelion/planet/overworld.json @@ -3,5 +3,5 @@ "orbit_distance": 1, "star_system": "aphelon:sol", "gravity": 1, - "oxygen": true + "oxygen": false } \ No newline at end of file