package com.tiedup.remake.client.events; import com.mojang.blaze3d.vertex.PoseStack; import com.tiedup.remake.blocks.BlockMarker; import com.tiedup.remake.blocks.entity.MarkerBlockEntity; import com.tiedup.remake.cells.CellDataV2; import com.tiedup.remake.cells.CellRegistryV2; import com.tiedup.remake.cells.MarkerType; import com.tiedup.remake.client.renderer.CellOutlineRenderer; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.ItemAdminWand; import java.util.UUID; import net.minecraft.client.Camera; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.phys.Vec3; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.RenderLevelStageEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; /** * Event handler for rendering cell outlines when holding an admin wand. * * Phase: Kidnapper Revamp - Cell System * * Renders colored outlines around cell positions when: * - Player is holding an Admin Wand * - A cell is currently selected in the wand * * The outlines help builders visualize which blocks are part of the cell. * * Network sync: On dedicated servers, cell data is synced via PacketSyncCellData. * On integrated servers, direct access to server data is used as a fallback. */ @Mod.EventBusSubscriber( modid = TiedUpMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT ) public class CellHighlightHandler { // Client-side cache of cell data (synced from server via PacketSyncCellData) private static final java.util.Map syncedCells = new java.util.concurrent.ConcurrentHashMap<>(); // Legacy single-cell cache for backward compatibility private static CellDataV2 cachedCellData = null; private static UUID cachedCellId = null; /** * Render cell outlines after translucent blocks. */ @SubscribeEvent public static void onRenderLevelStage(RenderLevelStageEvent event) { // Only render after translucent stage (so outlines appear on top) if ( event.getStage() != RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS ) { return; } Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null) return; // Check if player is holding an Admin Wand ItemStack mainHand = player.getMainHandItem(); ItemStack offHand = player.getOffhandItem(); boolean holdingAdminWand = mainHand.getItem() instanceof ItemAdminWand || offHand.getItem() instanceof ItemAdminWand; if (!holdingAdminWand) { cachedCellData = null; cachedCellId = null; return; } PoseStack poseStack = event.getPoseStack(); Camera camera = event.getCamera(); // If holding Admin Wand, render nearby structure markers and preview renderNearbyStructureMarkers(poseStack, camera, player); renderAdminWandPreview(poseStack, camera, player, mainHand, offHand); } /** * Render a preview outline showing where the Admin Wand will place a marker. */ private static void renderAdminWandPreview( PoseStack poseStack, Camera camera, LocalPlayer player, ItemStack mainHand, ItemStack offHand ) { // Get the block the player is looking at net.minecraft.world.phys.HitResult hitResult = Minecraft.getInstance().hitResult; if ( hitResult == null || hitResult.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK ) { return; } net.minecraft.world.phys.BlockHitResult blockHit = (net.minecraft.world.phys.BlockHitResult) hitResult; BlockPos targetPos = blockHit.getBlockPos().above(); // Marker goes above the clicked block // Get the current marker type from the wand MarkerType type; if (mainHand.getItem() instanceof ItemAdminWand) { type = ItemAdminWand.getCurrentType(mainHand); } else { type = ItemAdminWand.getCurrentType(offHand); } Vec3 cameraPos = camera.getPosition(); float[] color = CellOutlineRenderer.getColorForType(type); // Make preview semi-transparent and pulsing float alpha = 0.5f + 0.3f * (float) Math.sin(System.currentTimeMillis() / 200.0); float[] previewColor = { color[0], color[1], color[2], alpha }; // Setup rendering (depth test off so preview shows through blocks) com.mojang.blaze3d.systems.RenderSystem.enableBlend(); com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc(); com.mojang.blaze3d.systems.RenderSystem.disableDepthTest(); com.mojang.blaze3d.systems.RenderSystem.depthMask(false); com.mojang.blaze3d.systems.RenderSystem.setShader( net.minecraft.client.renderer.GameRenderer::getPositionColorShader ); CellOutlineRenderer.renderFilledBlock( poseStack, targetPos, cameraPos, previewColor ); com.mojang.blaze3d.systems.RenderSystem.depthMask(true); com.mojang.blaze3d.systems.RenderSystem.enableDepthTest(); com.mojang.blaze3d.systems.RenderSystem.disableBlend(); } /** * Render outlines for nearby structure markers (markers without cell IDs). */ private static void renderNearbyStructureMarkers( PoseStack poseStack, Camera camera, LocalPlayer player ) { Level level = player.level(); BlockPos playerPos = player.blockPosition(); Vec3 cameraPos = camera.getPosition(); // Collect markers first to check if we need to render anything java.util.List< java.util.Map.Entry > markersToRender = new java.util.ArrayList<>(); // Scan in a 32-block radius for structure markers int radius = 32; for (int x = -radius; x <= radius; x++) { for (int y = -radius / 2; y <= radius / 2; y++) { for (int z = -radius; z <= radius; z++) { BlockPos pos = playerPos.offset(x, y, z); if ( level.getBlockState(pos).getBlock() instanceof BlockMarker ) { BlockEntity be = level.getBlockEntity(pos); if (be instanceof MarkerBlockEntity marker) { // Only render structure markers (no cell ID) if (marker.getCellId() == null) { markersToRender.add( java.util.Map.entry( pos, marker.getMarkerType() ) ); } } } } } } // Only setup rendering if we have markers to render if (!markersToRender.isEmpty()) { // Setup rendering state (depth test off so markers show through blocks) com.mojang.blaze3d.systems.RenderSystem.enableBlend(); com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc(); com.mojang.blaze3d.systems.RenderSystem.disableDepthTest(); com.mojang.blaze3d.systems.RenderSystem.depthMask(false); com.mojang.blaze3d.systems.RenderSystem.setShader( net.minecraft.client.renderer .GameRenderer::getPositionColorShader ); for (var entry : markersToRender) { BlockPos pos = entry.getKey(); MarkerType type = entry.getValue(); float[] baseColor = CellOutlineRenderer.getColorForType(type); // Semi-transparent fill float[] fillColor = { baseColor[0], baseColor[1], baseColor[2], 0.4f, }; CellOutlineRenderer.renderFilledBlock( poseStack, pos, cameraPos, fillColor ); } // Restore rendering state com.mojang.blaze3d.systems.RenderSystem.depthMask(true); com.mojang.blaze3d.systems.RenderSystem.enableDepthTest(); com.mojang.blaze3d.systems.RenderSystem.disableBlend(); } } /** * Get cell data on client side. * First checks the network-synced cache, then falls back to integrated server access. * * @param cellId The cell UUID to look up * @return CellDataV2 if found, null otherwise */ private static CellDataV2 getCellDataClient(UUID cellId) { if (cellId == null) return null; // Priority 1: Check network-synced cache (works on dedicated servers) CellDataV2 synced = syncedCells.get(cellId); if (synced != null) { return synced; } // Priority 2: Check legacy single-cell cache if (cellId.equals(cachedCellId) && cachedCellData != null) { return cachedCellData; } // Priority 3: On integrated server, access server level directly (fallback) Minecraft mc = Minecraft.getInstance(); if (mc.getSingleplayerServer() != null) { ServerLevel serverLevel = mc.getSingleplayerServer().overworld(); if (serverLevel != null) { CellRegistryV2 registry = CellRegistryV2.get(serverLevel); CellDataV2 cell = registry.getCell(cellId); if (cell != null) { // Cache for future use cachedCellId = cellId; cachedCellData = cell; return cell; } } } // Not found - on dedicated server, packet hasn't arrived yet return null; } /** * Update cached cell data (called from network sync - PacketSyncCellData). * Stores in both the synced map and legacy cache for compatibility. * * @param cell The cell data received from server */ public static void updateCachedCell(CellDataV2 cell) { if (cell != null) { // Store in synced map syncedCells.put(cell.getId(), cell); // Also update legacy cache cachedCellId = cell.getId(); cachedCellData = cell; } } /** * Remove a cell from the cache (e.g., when cell is deleted). * * @param cellId The cell UUID to remove */ public static void removeCachedCell(UUID cellId) { if (cellId != null) { syncedCells.remove(cellId); if (cellId.equals(cachedCellId)) { cachedCellId = null; cachedCellData = null; } } } /** * Clear all cached cell data. * Called when disconnecting from server or on dimension change. */ public static void clearCache() { syncedCells.clear(); cachedCellId = null; cachedCellData = null; } }