diff --git a/common/src/main/java/net/xalcon/torchmaster/Torchmaster.java b/common/src/main/java/net/xalcon/torchmaster/Torchmaster.java index 8f29d6f..eccef2a 100644 --- a/common/src/main/java/net/xalcon/torchmaster/Torchmaster.java +++ b/common/src/main/java/net/xalcon/torchmaster/Torchmaster.java @@ -47,6 +47,7 @@ public static void init() { LOG.info("Debug Logging Enabled: {}", LOG.isDebugEnabled()); LOG.debug("If you can see this while the system property torchmaster.enableDebugLogging is not set to 1, report this on github!"); ModRegistry.initialize(); + Services.PLATFORM.getNetwork().registerPayloads(); } public static Optional getRegistryForLevel(Level level) diff --git a/common/src/main/java/net/xalcon/torchmaster/blocks/EntityBlockingLightBlock.java b/common/src/main/java/net/xalcon/torchmaster/blocks/EntityBlockingLightBlock.java index cc16b53..3f3b6f1 100644 --- a/common/src/main/java/net/xalcon/torchmaster/blocks/EntityBlockingLightBlock.java +++ b/common/src/main/java/net/xalcon/torchmaster/blocks/EntityBlockingLightBlock.java @@ -3,22 +3,46 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; import net.xalcon.torchmaster.Torchmaster; +import net.xalcon.torchmaster.client.MegaTorchScreenOpener; public class EntityBlockingLightBlock extends Block { + public static final BooleanProperty OVERLAY_VISIBLE = BooleanProperty.create("overlay_visible"); + private final LightType lightType; public EntityBlockingLightBlock(Properties properties, LightType lightType) { super(properties); this.lightType = lightType; + this.registerDefaultState(this.stateDefinition.any().setValue(OVERLAY_VISIBLE, false)); + } + + public LightType getLightType() + { + return this.lightType; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) + { + super.createBlockStateDefinition(builder); + builder.add(OVERLAY_VISIBLE); } @Override @@ -36,6 +60,34 @@ public void animateTick(BlockState state, Level level, BlockPos pos, RandomSourc level.addParticle(ParticleTypes.FLAME, d0, d1, d2, 0.0f, 0.0f, 0.0f); } + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) + { + if (this.lightType != LightType.MegaTorch) + return InteractionResult.PASS; + + if (level.isClientSide) + { + MegaTorchScreenOpener.open(pos); + return InteractionResult.SUCCESS; + } + return InteractionResult.CONSUME; + } + + @Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) + { + if (this.lightType != LightType.MegaTorch || player.isShiftKeyDown()) + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + + if (level.isClientSide) + { + MegaTorchScreenOpener.open(pos); + return ItemInteractionResult.SUCCESS; + } + return ItemInteractionResult.CONSUME; + } + @Override public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean moving) { super.onPlace(state, level, pos, oldState, moving); diff --git a/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchOverlayRenderer.java b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchOverlayRenderer.java new file mode 100644 index 0000000..bd8d628 --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchOverlayRenderer.java @@ -0,0 +1,132 @@ +package net.xalcon.torchmaster.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.List; + +public final class MegaTorchOverlayRenderer +{ + private static final int OVERLAY_COLOR_ARGB = 0x60_00C800; + private static final double MAX_RENDER_DISTANCE_SQR = 256.0D * 256.0D; + private static final int MAX_OVERLAYS_PER_FRAME = 32; + + private MegaTorchOverlayRenderer() {} + + public static void render(PoseStack poseStack, Camera camera, Frustum frustum, ClientLevel level, int radius) + { + if (level == null || radius <= 0) return; + + Vec3 camPos = camera.getPosition(); + List candidates = new ArrayList<>(); + + OverlayCache.forEach(level.dimension(), (long packed) -> + { + int x = BlockPos.getX(packed); + int y = BlockPos.getY(packed); + int z = BlockPos.getZ(packed); + + double minX = x - radius; + double minY = y - radius; + double minZ = z - radius; + double maxX = x + radius + 1; + double maxY = y + radius + 1; + double maxZ = z + radius + 1; + + double centerX = (minX + maxX) * 0.5; + double centerY = (minY + maxY) * 0.5; + double centerZ = (minZ + maxZ) * 0.5; + + double dx = centerX - camPos.x; + double dy = centerY - camPos.y; + double dz = centerZ - camPos.z; + double distSqr = dx * dx + dy * dy + dz * dz; + if (distSqr > MAX_RENDER_DISTANCE_SQR) return; + + AABB aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ); + if (!frustum.isVisible(aabb)) return; + + candidates.add(new Candidate(minX, minY, minZ, maxX, maxY, maxZ, distSqr)); + }); + + if (candidates.isEmpty()) return; + + candidates.sort((a, b) -> Double.compare(a.distSqr, b.distSqr)); + int limit = Math.min(candidates.size(), MAX_OVERLAYS_PER_FRAME); + + MultiBufferSource.BufferSource bufferSource = Minecraft.getInstance().renderBuffers().bufferSource(); + VertexConsumer consumer = bufferSource.getBuffer(RangeBoxRenderType.INSTANCE); + + poseStack.pushPose(); + poseStack.translate(-camPos.x, -camPos.y, -camPos.z); + Matrix4f pose = poseStack.last().pose(); + + for (int i = 0; i < limit; i++) + { + Candidate c = candidates.get(i); + drawBox(consumer, pose, + (float) c.minX, (float) c.minY, (float) c.minZ, + (float) c.maxX, (float) c.maxY, (float) c.maxZ, + OVERLAY_COLOR_ARGB); + } + + poseStack.popPose(); + bufferSource.endBatch(RangeBoxRenderType.INSTANCE); + } + + private static void drawBox(VertexConsumer c, Matrix4f m, + float x0, float y0, float z0, + float x1, float y1, float z1, + int argb) + { + // Bottom (y = y0) + c.addVertex(m, x0, y0, z0).setColor(argb); + c.addVertex(m, x1, y0, z0).setColor(argb); + c.addVertex(m, x1, y0, z1).setColor(argb); + c.addVertex(m, x0, y0, z1).setColor(argb); + + // Top (y = y1) + c.addVertex(m, x0, y1, z0).setColor(argb); + c.addVertex(m, x0, y1, z1).setColor(argb); + c.addVertex(m, x1, y1, z1).setColor(argb); + c.addVertex(m, x1, y1, z0).setColor(argb); + + // North (z = z0) + c.addVertex(m, x0, y0, z0).setColor(argb); + c.addVertex(m, x0, y1, z0).setColor(argb); + c.addVertex(m, x1, y1, z0).setColor(argb); + c.addVertex(m, x1, y0, z0).setColor(argb); + + // South (z = z1) + c.addVertex(m, x0, y0, z1).setColor(argb); + c.addVertex(m, x1, y0, z1).setColor(argb); + c.addVertex(m, x1, y1, z1).setColor(argb); + c.addVertex(m, x0, y1, z1).setColor(argb); + + // West (x = x0) + c.addVertex(m, x0, y0, z0).setColor(argb); + c.addVertex(m, x0, y0, z1).setColor(argb); + c.addVertex(m, x0, y1, z1).setColor(argb); + c.addVertex(m, x0, y1, z0).setColor(argb); + + // East (x = x1) + c.addVertex(m, x1, y0, z0).setColor(argb); + c.addVertex(m, x1, y1, z0).setColor(argb); + c.addVertex(m, x1, y1, z1).setColor(argb); + c.addVertex(m, x1, y0, z1).setColor(argb); + } + + private record Candidate(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ, + double distSqr) {} +} diff --git a/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreen.java b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreen.java new file mode 100644 index 0000000..57f018d --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreen.java @@ -0,0 +1,101 @@ +package net.xalcon.torchmaster.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.state.BlockState; +import net.xalcon.torchmaster.ModRegistry; +import net.xalcon.torchmaster.blocks.EntityBlockingLightBlock; +import net.xalcon.torchmaster.platform.Services; + +public class MegaTorchScreen extends net.minecraft.client.gui.screens.Screen +{ + private static final int BUTTON_WIDTH = 200; + private static final int BUTTON_HEIGHT = 20; + private static final int PANEL_WIDTH = 220; + private static final int PANEL_HEIGHT = 130; + + private final BlockPos torchPos; + private boolean overlayOn; + private int radius; + private Button toggleButton; + + public MegaTorchScreen(BlockPos torchPos) + { + super(Component.translatable("screen.torchmaster.megatorch.title")); + this.torchPos = torchPos.immutable(); + } + + @Override + protected void init() + { + super.init(); + + this.radius = Services.PLATFORM.getConfig().getMegaTorchRadius(); + this.overlayOn = readCurrentOverlayState(); + + int centerX = this.width / 2; + int panelTop = (this.height - PANEL_HEIGHT) / 2; + + this.toggleButton = Button.builder(toggleLabel(), b -> onToggle()) + .bounds(centerX - BUTTON_WIDTH / 2, panelTop + 60, BUTTON_WIDTH, BUTTON_HEIGHT) + .build(); + this.addRenderableWidget(this.toggleButton); + + this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, b -> this.onClose()) + .bounds(centerX - BUTTON_WIDTH / 2, panelTop + PANEL_HEIGHT - 28, BUTTON_WIDTH, BUTTON_HEIGHT) + .build()); + } + + private boolean readCurrentOverlayState() + { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return false; + BlockState state = level.getBlockState(this.torchPos); + if (!state.is(ModRegistry.blockMegaTorch.get())) return false; + return state.getValue(EntityBlockingLightBlock.OVERLAY_VISIBLE); + } + + private void onToggle() + { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return; + + Services.PLATFORM.getNetwork().sendToggleOverlayToServer(this.torchPos); + this.overlayOn = !this.overlayOn; + OverlayCache.toggle(level.dimension(), this.torchPos); + this.toggleButton.setMessage(toggleLabel()); + } + + private Component toggleLabel() + { + return this.overlayOn + ? Component.translatable("screen.torchmaster.megatorch.toggle_on") + : Component.translatable("screen.torchmaster.megatorch.toggle_off"); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) + { + this.renderBackground(graphics, mouseX, mouseY, partialTick); + super.render(graphics, mouseX, mouseY, partialTick); + + int centerX = this.width / 2; + int panelTop = (this.height - PANEL_HEIGHT) / 2; + + graphics.drawCenteredString(this.font, this.title, centerX, panelTop + 10, 0xFFFFFFFF); + graphics.drawCenteredString(this.font, + Component.translatable("screen.torchmaster.megatorch.radius", this.radius), + centerX, panelTop + 35, 0xFFCCCCCC); + } + + @Override + public boolean isPauseScreen() + { + return false; + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreenOpener.java b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreenOpener.java new file mode 100644 index 0000000..edbcb57 --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/MegaTorchScreenOpener.java @@ -0,0 +1,14 @@ +package net.xalcon.torchmaster.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; + +public final class MegaTorchScreenOpener +{ + private MegaTorchScreenOpener() {} + + public static void open(BlockPos pos) + { + Minecraft.getInstance().setScreen(new MegaTorchScreen(pos)); + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/client/OverlayCache.java b/common/src/main/java/net/xalcon/torchmaster/client/OverlayCache.java new file mode 100644 index 0000000..a028f57 --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/OverlayCache.java @@ -0,0 +1,77 @@ +package net.xalcon.torchmaster.client; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongConsumer; + +public final class OverlayCache +{ + private static final Map, LongSet> CACHE = new HashMap<>(); + + private OverlayCache() {} + + public static synchronized void add(ResourceKey dimension, BlockPos pos) + { + CACHE.computeIfAbsent(dimension, k -> new LongOpenHashSet()).add(pos.asLong()); + } + + public static synchronized void remove(ResourceKey dimension, BlockPos pos) + { + LongSet set = CACHE.get(dimension); + if (set != null) set.remove(pos.asLong()); + } + + public static synchronized void toggle(ResourceKey dimension, BlockPos pos) + { + LongSet set = CACHE.computeIfAbsent(dimension, k -> new LongOpenHashSet()); + long key = pos.asLong(); + if (!set.add(key)) set.remove(key); + } + + public static synchronized boolean contains(ResourceKey dimension, BlockPos pos) + { + LongSet set = CACHE.get(dimension); + return set != null && set.contains(pos.asLong()); + } + + public static synchronized void clearLevel(ResourceKey dimension) + { + CACHE.remove(dimension); + } + + public static synchronized void clearChunk(ResourceKey dimension, ChunkPos chunkPos) + { + LongSet set = CACHE.get(dimension); + if (set == null) return; + int minX = chunkPos.getMinBlockX(); + int minZ = chunkPos.getMinBlockZ(); + int maxX = chunkPos.getMaxBlockX(); + int maxZ = chunkPos.getMaxBlockZ(); + set.removeIf((long packed) -> + { + int x = BlockPos.getX(packed); + int z = BlockPos.getZ(packed); + return x >= minX && x <= maxX && z >= minZ && z <= maxZ; + }); + } + + public static synchronized void forEach(ResourceKey dimension, LongConsumer consumer) + { + LongSet set = CACHE.get(dimension); + if (set == null) return; + set.forEach(consumer); + } + + public static synchronized int size(ResourceKey dimension) + { + LongSet set = CACHE.get(dimension); + return set == null ? 0 : set.size(); + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/client/OverlayChunkScanner.java b/common/src/main/java/net/xalcon/torchmaster/client/OverlayChunkScanner.java new file mode 100644 index 0000000..dc6e05c --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/OverlayChunkScanner.java @@ -0,0 +1,52 @@ +package net.xalcon.torchmaster.client; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.xalcon.torchmaster.ModRegistry; +import net.xalcon.torchmaster.blocks.EntityBlockingLightBlock; + +public final class OverlayChunkScanner +{ + private OverlayChunkScanner() {} + + public static void scan(LevelChunk chunk, ResourceKey dimension) + { + Block torchBlock = ModRegistry.blockMegaTorch.get(); + LevelChunkSection[] sections = chunk.getSections(); + ChunkPos cp = chunk.getPos(); + int chunkMinX = cp.getMinBlockX(); + int chunkMinZ = cp.getMinBlockZ(); + + for (int sIdx = 0; sIdx < sections.length; sIdx++) + { + LevelChunkSection section = sections[sIdx]; + if (section == null || section.hasOnlyAir()) continue; + if (!section.maybeHas(s -> s.is(torchBlock))) continue; + + int sectionMinY = chunk.getSectionYFromSectionIndex(sIdx) << 4; + for (int xx = 0; xx < 16; xx++) + { + for (int yy = 0; yy < 16; yy++) + { + for (int zz = 0; zz < 16; zz++) + { + BlockState state = section.getBlockState(xx, yy, zz); + if (state.is(torchBlock) && state.getValue(EntityBlockingLightBlock.OVERLAY_VISIBLE)) + { + OverlayCache.add(dimension, new BlockPos( + chunkMinX + xx, + sectionMinY + yy, + chunkMinZ + zz)); + } + } + } + } + } + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/client/RangeBoxRenderType.java b/common/src/main/java/net/xalcon/torchmaster/client/RangeBoxRenderType.java new file mode 100644 index 0000000..d8bb5ff --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/client/RangeBoxRenderType.java @@ -0,0 +1,10 @@ +package net.xalcon.torchmaster.client; + +import net.minecraft.client.renderer.RenderType; + +public final class RangeBoxRenderType +{ + public static final RenderType INSTANCE = RenderType.debugStructureQuads(); + + private RangeBoxRenderType() {} +} diff --git a/common/src/main/java/net/xalcon/torchmaster/network/INetworkHelper.java b/common/src/main/java/net/xalcon/torchmaster/network/INetworkHelper.java new file mode 100644 index 0000000..bdec68f --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/network/INetworkHelper.java @@ -0,0 +1,10 @@ +package net.xalcon.torchmaster.network; + +import net.minecraft.core.BlockPos; + +public interface INetworkHelper +{ + void registerPayloads(); + + void sendToggleOverlayToServer(BlockPos pos); +} diff --git a/common/src/main/java/net/xalcon/torchmaster/network/ToggleMegaTorchOverlayPayload.java b/common/src/main/java/net/xalcon/torchmaster/network/ToggleMegaTorchOverlayPayload.java new file mode 100644 index 0000000..c7d6132 --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/network/ToggleMegaTorchOverlayPayload.java @@ -0,0 +1,26 @@ +package net.xalcon.torchmaster.network; + +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.xalcon.torchmaster.Constants; + +public record ToggleMegaTorchOverlayPayload(BlockPos pos) implements CustomPacketPayload +{ + public static final CustomPacketPayload.Type TYPE = + new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "toggle_overlay")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + BlockPos.STREAM_CODEC, + ToggleMegaTorchOverlayPayload::pos, + ToggleMegaTorchOverlayPayload::new); + + @Override + public Type type() + { + return TYPE; + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/network/ToggleOverlayHandler.java b/common/src/main/java/net/xalcon/torchmaster/network/ToggleOverlayHandler.java new file mode 100644 index 0000000..6254c39 --- /dev/null +++ b/common/src/main/java/net/xalcon/torchmaster/network/ToggleOverlayHandler.java @@ -0,0 +1,43 @@ +package net.xalcon.torchmaster.network; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.xalcon.torchmaster.ModRegistry; +import net.xalcon.torchmaster.Torchmaster; +import net.xalcon.torchmaster.blocks.EntityBlockingLightBlock; + +public final class ToggleOverlayHandler +{ + private static final double MAX_REACH_SQR = 64.0D; + + private ToggleOverlayHandler() {} + + public static void handle(ToggleMegaTorchOverlayPayload payload, ServerPlayer player) + { + if (player == null) return; + BlockPos pos = payload.pos(); + if (pos == null) return; + + ServerLevel level = player.serverLevel(); + if (!level.isLoaded(pos)) return; + + if (player.position().distanceToSqr(Vec3.atCenterOf(pos)) > MAX_REACH_SQR) + { + Torchmaster.LOG.debug("Rejected overlay toggle from {}: out of reach", player.getGameProfile().getName()); + return; + } + + BlockState state = level.getBlockState(pos); + if (!state.is(ModRegistry.blockMegaTorch.get())) + { + Torchmaster.LOG.debug("Rejected overlay toggle from {}: block at {} is not a megatorch", player.getGameProfile().getName(), pos); + return; + } + + level.setBlock(pos, state.cycle(EntityBlockingLightBlock.OVERLAY_VISIBLE), Block.UPDATE_ALL); + } +} diff --git a/common/src/main/java/net/xalcon/torchmaster/platform/services/IPlatformHelper.java b/common/src/main/java/net/xalcon/torchmaster/platform/services/IPlatformHelper.java index 6c755f8..0b8e762 100644 --- a/common/src/main/java/net/xalcon/torchmaster/platform/services/IPlatformHelper.java +++ b/common/src/main/java/net/xalcon/torchmaster/platform/services/IPlatformHelper.java @@ -8,6 +8,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.xalcon.torchmaster.config.ITorchmasterConfig; +import net.xalcon.torchmaster.network.INetworkHelper; import net.xalcon.torchmaster.platform.RegistryObject; import java.util.Collection; @@ -55,4 +56,6 @@ interface BlockEntitySupplier { BlockEntityType createBlockEntityType(BlockEntitySupplier supplier, Block... blocks); ITorchmasterConfig getConfig(); + + INetworkHelper getNetwork(); } \ No newline at end of file diff --git a/common/src/main/resources/assets/torchmaster/blockstates/dreadlamp.json b/common/src/main/resources/assets/torchmaster/blockstates/dreadlamp.json index b19e2f8..792f5c2 100644 --- a/common/src/main/resources/assets/torchmaster/blockstates/dreadlamp.json +++ b/common/src/main/resources/assets/torchmaster/blockstates/dreadlamp.json @@ -1,5 +1,6 @@ { "variants": { - "": { "model": "torchmaster:block/dreadlamp" } + "overlay_visible=false": { "model": "torchmaster:block/dreadlamp" }, + "overlay_visible=true": { "model": "torchmaster:block/dreadlamp" } } } diff --git a/common/src/main/resources/assets/torchmaster/blockstates/megatorch.json b/common/src/main/resources/assets/torchmaster/blockstates/megatorch.json index b9bc575..b53a997 100644 --- a/common/src/main/resources/assets/torchmaster/blockstates/megatorch.json +++ b/common/src/main/resources/assets/torchmaster/blockstates/megatorch.json @@ -1,5 +1,6 @@ { "variants": { - "": { "model": "torchmaster:block/megatorch" } + "overlay_visible=false": { "model": "torchmaster:block/megatorch" }, + "overlay_visible=true": { "model": "torchmaster:block/megatorch" } } } diff --git a/common/src/main/resources/assets/torchmaster/lang/en_us.json b/common/src/main/resources/assets/torchmaster/lang/en_us.json index 1dc5941..40d4521 100644 --- a/common/src/main/resources/assets/torchmaster/lang/en_us.json +++ b/common/src/main/resources/assets/torchmaster/lang/en_us.json @@ -1,6 +1,10 @@ { "block.torchmaster.megatorch": "Mega Torch", "block.torchmaster.megatorch.tooltip": "Prevents natural spawning of hostile monsters in a big radius around the torch", + "screen.torchmaster.megatorch.title": "Mega Torch", + "screen.torchmaster.megatorch.toggle_on": "Show Range Overlay: ON", + "screen.torchmaster.megatorch.toggle_off": "Show Range Overlay: OFF", + "screen.torchmaster.megatorch.radius": "Radius: %s blocks", "block.torchmaster.dreadlamp": "Dread Lamp", "block.torchmaster.dreadlamp.tooltip": "Prevents natural spawning of passive animals like squids, bats or ocelots in a big radius around the lamp", "block.torchmaster.feral_flare_lantern": "Feral Flare Lantern", diff --git a/fabric/src/main/java/net/xalcon/torchmaster/TorchmasterFabricClient.java b/fabric/src/main/java/net/xalcon/torchmaster/TorchmasterFabricClient.java index c7c8fb7..7dde5cc 100644 --- a/fabric/src/main/java/net/xalcon/torchmaster/TorchmasterFabricClient.java +++ b/fabric/src/main/java/net/xalcon/torchmaster/TorchmasterFabricClient.java @@ -5,11 +5,13 @@ import net.fabricmc.api.Environment; import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; import net.minecraft.client.renderer.RenderType; +import net.xalcon.torchmaster.client.FabricRangeOverlayHooks; @Environment(EnvType.CLIENT) public class TorchmasterFabricClient implements ClientModInitializer { public void onInitializeClient() { BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.blockDreadLamp.get(), RenderType.cutout()); + FabricRangeOverlayHooks.register(); } } \ No newline at end of file diff --git a/fabric/src/main/java/net/xalcon/torchmaster/client/FabricRangeOverlayHooks.java b/fabric/src/main/java/net/xalcon/torchmaster/client/FabricRangeOverlayHooks.java new file mode 100644 index 0000000..f4828d0 --- /dev/null +++ b/fabric/src/main/java/net/xalcon/torchmaster/client/FabricRangeOverlayHooks.java @@ -0,0 +1,37 @@ +package net.xalcon.torchmaster.client; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.xalcon.torchmaster.platform.Services; + +@Environment(EnvType.CLIENT) +public final class FabricRangeOverlayHooks +{ + private FabricRangeOverlayHooks() {} + + public static void register() + { + WorldRenderEvents.AFTER_TRANSLUCENT.register(ctx -> + { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return; + int radius = Services.PLATFORM.getConfig().getMegaTorchRadius(); + MegaTorchOverlayRenderer.render( + ctx.matrixStack(), + ctx.camera(), + ctx.frustum(), + level, + radius); + }); + + ClientChunkEvents.CHUNK_LOAD.register((level, chunk) -> + OverlayChunkScanner.scan(chunk, level.dimension())); + + ClientChunkEvents.CHUNK_UNLOAD.register((level, chunk) -> + OverlayCache.clearChunk(level.dimension(), chunk.getPos())); + } +} diff --git a/fabric/src/main/java/net/xalcon/torchmaster/network/FabricClientNetworkSender.java b/fabric/src/main/java/net/xalcon/torchmaster/network/FabricClientNetworkSender.java new file mode 100644 index 0000000..b699677 --- /dev/null +++ b/fabric/src/main/java/net/xalcon/torchmaster/network/FabricClientNetworkSender.java @@ -0,0 +1,17 @@ +package net.xalcon.torchmaster.network; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.core.BlockPos; + +@Environment(EnvType.CLIENT) +final class FabricClientNetworkSender +{ + private FabricClientNetworkSender() {} + + static void send(BlockPos pos) + { + ClientPlayNetworking.send(new ToggleMegaTorchOverlayPayload(pos)); + } +} diff --git a/fabric/src/main/java/net/xalcon/torchmaster/network/FabricNetworkHelper.java b/fabric/src/main/java/net/xalcon/torchmaster/network/FabricNetworkHelper.java new file mode 100644 index 0000000..bb052a0 --- /dev/null +++ b/fabric/src/main/java/net/xalcon/torchmaster/network/FabricNetworkHelper.java @@ -0,0 +1,30 @@ +package net.xalcon.torchmaster.network; + +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; + +public class FabricNetworkHelper implements INetworkHelper +{ + @Override + public void registerPayloads() + { + PayloadTypeRegistry.playC2S().register( + ToggleMegaTorchOverlayPayload.TYPE, + ToggleMegaTorchOverlayPayload.STREAM_CODEC); + + ServerPlayNetworking.registerGlobalReceiver( + ToggleMegaTorchOverlayPayload.TYPE, + (payload, context) -> context.player().server.execute( + () -> ToggleOverlayHandler.handle(payload, context.player()))); + } + + @Override + public void sendToggleOverlayToServer(BlockPos pos) + { + if (FabricLoader.getInstance().getEnvironmentType() != EnvType.CLIENT) return; + FabricClientNetworkSender.send(pos); + } +} diff --git a/fabric/src/main/java/net/xalcon/torchmaster/platform/FabricPlatformHelper.java b/fabric/src/main/java/net/xalcon/torchmaster/platform/FabricPlatformHelper.java index 7c720ef..c7f1b6e 100644 --- a/fabric/src/main/java/net/xalcon/torchmaster/platform/FabricPlatformHelper.java +++ b/fabric/src/main/java/net/xalcon/torchmaster/platform/FabricPlatformHelper.java @@ -11,6 +11,8 @@ import net.xalcon.torchmaster.ModRegistry; import net.xalcon.torchmaster.TorchmasterOwOConfigWrapper; import net.xalcon.torchmaster.config.ITorchmasterConfig; +import net.xalcon.torchmaster.network.INetworkHelper; +import net.xalcon.torchmaster.platform.Services; import net.xalcon.torchmaster.platform.services.IPlatformHelper; import net.fabricmc.loader.api.FabricLoader; @@ -18,6 +20,8 @@ public class FabricPlatformHelper implements IPlatformHelper { + private static volatile INetworkHelper NETWORK; + @Override public String getPlatformName() { return "Fabric"; @@ -56,4 +60,22 @@ public ITorchmasterConfig getConfig() { return TorchmasterOwOConfigWrapper.INSTANCE; } + + @Override + public INetworkHelper getNetwork() + { + INetworkHelper local = NETWORK; + if (local == null) + { + synchronized (FabricPlatformHelper.class) + { + local = NETWORK; + if (local == null) + { + NETWORK = local = Services.load(INetworkHelper.class); + } + } + } + return local; + } } diff --git a/fabric/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper b/fabric/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper new file mode 100644 index 0000000..b6c8892 --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper @@ -0,0 +1 @@ +net.xalcon.torchmaster.network.FabricNetworkHelper diff --git a/neoforge/src/main/java/net/xalcon/torchmaster/client/NeoforgeRangeOverlayHooks.java b/neoforge/src/main/java/net/xalcon/torchmaster/client/NeoforgeRangeOverlayHooks.java new file mode 100644 index 0000000..0750d92 --- /dev/null +++ b/neoforge/src/main/java/net/xalcon/torchmaster/client/NeoforgeRangeOverlayHooks.java @@ -0,0 +1,54 @@ +package net.xalcon.torchmaster.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.level.chunk.LevelChunk; +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.neoforged.neoforge.event.level.ChunkEvent; +import net.xalcon.torchmaster.Constants; +import net.xalcon.torchmaster.platform.Services; + +@EventBusSubscriber(value = Dist.CLIENT, modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.GAME) +public final class NeoforgeRangeOverlayHooks +{ + private NeoforgeRangeOverlayHooks() {} + + @SubscribeEvent + public static void onRenderLevelStage(RenderLevelStageEvent event) + { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS) return; + + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return; + + int radius = Services.PLATFORM.getConfig().getMegaTorchRadius(); + MegaTorchOverlayRenderer.render( + event.getPoseStack(), + event.getCamera(), + event.getFrustum(), + level, + radius); + } + + @SubscribeEvent + public static void onChunkLoad(ChunkEvent.Load event) + { + if (!event.getLevel().isClientSide()) return; + if (!(event.getChunk() instanceof LevelChunk lc)) return; + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return; + OverlayChunkScanner.scan(lc, level.dimension()); + } + + @SubscribeEvent + public static void onChunkUnload(ChunkEvent.Unload event) + { + if (!event.getLevel().isClientSide()) return; + ClientLevel level = Minecraft.getInstance().level; + if (level == null) return; + OverlayCache.clearChunk(level.dimension(), event.getChunk().getPos()); + } +} diff --git a/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeClientNetworkSender.java b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeClientNetworkSender.java new file mode 100644 index 0000000..31c2077 --- /dev/null +++ b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeClientNetworkSender.java @@ -0,0 +1,17 @@ +package net.xalcon.torchmaster.network; + +import net.minecraft.core.BlockPos; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.neoforge.network.PacketDistributor; + +@OnlyIn(Dist.CLIENT) +final class NeoforgeClientNetworkSender +{ + private NeoforgeClientNetworkSender() {} + + static void send(BlockPos pos) + { + PacketDistributor.sendToServer(new ToggleMegaTorchOverlayPayload(pos)); + } +} diff --git a/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkHelper.java b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkHelper.java new file mode 100644 index 0000000..541d78e --- /dev/null +++ b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkHelper.java @@ -0,0 +1,20 @@ +package net.xalcon.torchmaster.network; + +import net.minecraft.core.BlockPos; +import net.neoforged.fml.loading.FMLEnvironment; + +public class NeoforgeNetworkHelper implements INetworkHelper +{ + @Override + public void registerPayloads() + { + // NeoForge requires registration via RegisterPayloadHandlersEvent — see NeoforgeNetworkRegistration. + } + + @Override + public void sendToggleOverlayToServer(BlockPos pos) + { + if (FMLEnvironment.dist.isDedicatedServer()) return; + NeoforgeClientNetworkSender.send(pos); + } +} diff --git a/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkRegistration.java b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkRegistration.java new file mode 100644 index 0000000..b4e695c --- /dev/null +++ b/neoforge/src/main/java/net/xalcon/torchmaster/network/NeoforgeNetworkRegistration.java @@ -0,0 +1,25 @@ +package net.xalcon.torchmaster.network; + +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.xalcon.torchmaster.Constants; + +@EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.MOD) +public final class NeoforgeNetworkRegistration +{ + private NeoforgeNetworkRegistration() {} + + @SubscribeEvent + public static void register(RegisterPayloadHandlersEvent event) + { + PayloadRegistrar registrar = event.registrar("1"); + registrar.playToServer( + ToggleMegaTorchOverlayPayload.TYPE, + ToggleMegaTorchOverlayPayload.STREAM_CODEC, + (payload, context) -> context.enqueueWork( + () -> ToggleOverlayHandler.handle(payload, (ServerPlayer) context.player()))); + } +} diff --git a/neoforge/src/main/java/net/xalcon/torchmaster/platform/NeoForgePlatformHelper.java b/neoforge/src/main/java/net/xalcon/torchmaster/platform/NeoForgePlatformHelper.java index ce3f0ba..7a7ef75 100644 --- a/neoforge/src/main/java/net/xalcon/torchmaster/platform/NeoForgePlatformHelper.java +++ b/neoforge/src/main/java/net/xalcon/torchmaster/platform/NeoForgePlatformHelper.java @@ -10,6 +10,8 @@ import net.xalcon.torchmaster.ModRegistry; import net.xalcon.torchmaster.TorchmasterNeoforgeConfig; import net.xalcon.torchmaster.config.ITorchmasterConfig; +import net.xalcon.torchmaster.network.INetworkHelper; +import net.xalcon.torchmaster.platform.Services; import net.xalcon.torchmaster.platform.services.IPlatformHelper; import net.neoforged.fml.ModList; import net.neoforged.fml.loading.FMLLoader; @@ -20,6 +22,8 @@ public class NeoForgePlatformHelper implements IPlatformHelper { + private static volatile INetworkHelper NETWORK; + @Override public String getPlatformName() { @@ -59,4 +63,22 @@ public ITorchmasterConfig getConfig() { return TorchmasterNeoforgeConfig.WRAPPED_CONFIG; } + + @Override + public INetworkHelper getNetwork() + { + INetworkHelper local = NETWORK; + if (local == null) + { + synchronized (NeoForgePlatformHelper.class) + { + local = NETWORK; + if (local == null) + { + NETWORK = local = Services.load(INetworkHelper.class); + } + } + } + return local; + } } \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper b/neoforge/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper new file mode 100644 index 0000000..f71e0c6 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/net.xalcon.torchmaster.network.INetworkHelper @@ -0,0 +1 @@ +net.xalcon.torchmaster.network.NeoforgeNetworkHelper