package cc.maxmc.servux.dataproviders // native NBT // World Structure import cc.maxmc.servux.network.packet.StructureDataPacketHandler import cc.maxmc.servux.util.* import io.netty.buffer.Unpooled import it.unimi.dsi.fastutil.longs.LongOpenHashSet import it.unimi.dsi.fastutil.longs.LongSet import net.minecraft.nbt.NBTTagCompound import net.minecraft.nbt.NBTTagList import net.minecraft.world.level.ChunkCoordIntPair import net.minecraft.world.level.levelgen.feature.StructureGenerator import net.minecraft.world.level.levelgen.structure.StructureStart import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.World import org.bukkit.entity.Player import taboolib.platform.BukkitPlugin import java.util.* import kotlin.math.abs class StructureDataProvider : DataProviderBase("structure_bounding_boxes", StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PROTOCOL_VERSION, "Structure Bounding Boxes data for structures such as Witch Huts, Ocean Monuments, Nether Fortresses etc.") { companion object { private const val timeout = 30 * 20 private const val updateInterval = 40 } private val registeredPlayers = HashMap() private val timeouts = HashMap>() val metadata: NBTTagCompound = NBTTagCompound() init { metadata.putString("id", StructureDataPacketHandler.CHANNEL) metadata.putInt("timeout", timeout) metadata.putInt("version", StructureDataPacketHandler.PROTOCOL_VERSION) } private var retainDistance = 0 override fun shouldTick(): Boolean = true override fun tick(tickCounter: Int) { if (tickCounter % updateInterval == 0) { if (registeredPlayers.isNotEmpty()) { // System.out.printf("=======================\n"); // System.out.printf("tick: %d - %s\n", tickCounter, this.isEnabled()); retainDistance = Bukkit.getViewDistance() + 2 val uuidIter = registeredPlayers.keys.iterator() while (uuidIter.hasNext()) { val uuid = uuidIter.next() val player: Player = Bukkit.getPlayer(uuid) ?: return run { timeouts.remove(uuid) uuidIter.remove() } this.checkForDimensionChange(player) this.refreshTrackedChunks(player, tickCounter) } } } } private fun checkForDimensionChange(player: Player) { val uuid: UUID = player.uniqueId val playerPos: PlayerDimensionPosition? = registeredPlayers[uuid] if (playerPos == null || playerPos.dimensionChanged(player)) { timeouts.remove(uuid) registeredPlayers.computeIfAbsent(uuid) { PlayerDimensionPosition(player) }.setPosition(player) } } private fun refreshTrackedChunks(player: Player, tickCounter: Int) { val uuid: UUID = player.uniqueId val map: MutableMap? = timeouts[uuid] if (map != null) { // System.out.printf("refreshTrackedChunks: timeouts: %d\n", map.size()); this.sendAndRefreshExpiredStructures(player, map, tickCounter) } } private fun sendAndRefreshExpiredStructures( player: Player, map: MutableMap, tickCounter: Int, ) { val positionsToUpdate: MutableSet = HashSet() map.forEach { (key, timeout) -> if (timeout.needsUpdate(tickCounter, Companion.timeout)) { positionsToUpdate.add(key) } } if (positionsToUpdate.isNotEmpty()) { val world = player.world val center = player.location.chunk val references: MutableMap, LongSet> = HashMap() for (pos in positionsToUpdate) { if (this.isOutOfRange(pos, center)) { map.remove(pos) } else { this.getStructureReferencesFromChunk(pos.x, pos.z, world, references) val timeout = map[pos] timeout?.lastSync = tickCounter } } // System.out.printf("sendAndRefreshExpiredStructures: positionsToUpdate: %d -> references: %d, to: %d\n", positionsToUpdate.size(), references.size(), this.timeout); if (references.isNotEmpty()) { this.sendStructures(player, references, tickCounter) } } } private fun isOutOfRange(pos: ChunkPos, center: Chunk): Boolean { val chunkRadius = retainDistance return abs(pos.x - center.x) > chunkRadius || abs(pos.z - center.z) > chunkRadius } private fun getStructureReferencesFromChunk( chunkX: Int, chunkZ: Int, world: World, references: MutableMap, LongSet>, ) { if (!world.isChunkLoaded(chunkX, chunkZ)) { return } val nmsWorld = NMS.INSTANCE.getMinecraftServer().allLevels.findLast { it.serverLevelData.levelName == world.name }!! val chunk = nmsWorld.getChunk(chunkX, chunkZ) chunk.allReferences.forEach { (feature, startChunks) -> if (!startChunks.isEmpty() && feature != StructureGenerator.MINESHAFT) { references.merge(feature, startChunks) { oldSet, entrySet -> val newSet = LongOpenHashSet(oldSet) newSet.addAll(entrySet) return@merge newSet } } } } private fun sendStructures( player: Player, references: Map, LongSet>, tickCounter: Int, ) { val world: World = player.world val starts: Map> = this.getStructureStartsFromReferences(world, references) if (starts.isNotEmpty()) { this.addOrRefreshTimeouts(player.uniqueId, references, tickCounter) val structureList: NBTTagList = getStructureList(starts, world) // System.out.printf("sendStructures: starts: %d -> structureList: %d. refs: %s\n", starts.size(), structureList.size(), references.keySet()); val tag = NBTTagCompound() tag.put("Structures", structureList) sendPacketTypeAndCompound(StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PACKET_S2C_STRUCTURE_DATA, tag, player) } } private fun getStructureStartsFromReferences( world: World, references: Map, LongSet>, ): Map> { val starts = HashMap>() references.forEach { (feature, startChunks) -> val iter = startChunks.iterator() while (iter.hasNext()) { val pos = ChunkPos(iter.nextLong()) if (!world.isChunkLoaded(pos.x, pos.z)) { continue } val nmsWorld = NMS.INSTANCE.getMinecraftServer().allLevels.findLast { it.serverLevelData.levelName == world.name }!! val chunk = nmsWorld.getChunk(pos.x, pos.z) val start: StructureStart<*> = chunk.getStartForFeature(feature) ?: return@forEach starts[pos] = start } } // System.out.printf("getStructureStartsFromReferences: references: %d -> starts: %d\n", references.size(), starts.size()); return starts } private fun addOrRefreshTimeouts(uuid: UUID, references: Map, LongSet>, tickCounter: Int) { // System.out.printf("addOrRefreshTimeouts: references: %d\n", references.size()); val map: MutableMap = timeouts.computeIfAbsent(uuid ) { HashMap() } for (chunks: LongSet in references.values) { for (chunkPosLong in chunks) { val pos = ChunkPos(chunkPosLong) map.computeIfAbsent(pos) { Timeout(tickCounter) }.lastSync = tickCounter } } } private fun getStructureList(structures: Map>, world: World): NBTTagList { val list = NBTTagList() structures.forEach { (pos, value) -> list.add(value.createTag(StructurePieceSerializationContext.fromLevel(NMS.INSTANCE.getMinecraftServer().allLevels.findLast { it.serverLevelData.levelName == world.name }!!), ChunkCoordIntPair(pos.x, pos.z))) } return list } fun register(player: Player): Boolean { // System.out.printf("register\n"); var registered = false val uuid: UUID = player.uniqueId if (!registeredPlayers.containsKey(uuid)) { val bytebuf = Unpooled.buffer() sendPacketTypeAndCompound(StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PACKET_S2C_METADATA, metadata, player) registeredPlayers[uuid] = PlayerDimensionPosition(player) val tickCounter: Int = Bukkit.getServer().ticksPerAmbientSpawns this.initialSyncStructuresToPlayerWithinRange(player, Bukkit.getViewDistance(), tickCounter) registered = true } return registered } private fun initialSyncStructuresToPlayerWithinRange( player: Player, chunkRadius: Int, tickCounter: Int, ) { val uuid: UUID = player.uniqueId val center = ChunkPos(player.location.chunk) val references: Map, LongSet> = this.getStructureReferencesWithinRange(player.world, center, chunkRadius) timeouts.remove(uuid) registeredPlayers.computeIfAbsent(uuid) { u: UUID? -> PlayerDimensionPosition(player) }.setPosition(player) // System.out.printf("initialSyncStructuresToPlayerWithinRange: references: %d\n", references.size()); sendStructures(player, references, tickCounter) } private fun getStructureReferencesWithinRange( world: World, center: ChunkPos, chunkRadius: Int, ): Map, LongSet> { val references = HashMap, LongSet>() for (cx in center.x - chunkRadius..center.x + chunkRadius) { for (cz in center.z - chunkRadius..center.z + chunkRadius) { getStructureReferencesFromChunk(cx, cz, world, references) } } // System.out.printf("getStructureReferencesWithinRange: references: %d\n", references.size()); return references } private fun sendPacketTypeAndCompound( channel: String, packetType: Int, data: NBTTagCompound, player: Player, ) { val buf = Unpooled.buffer() buf.writeVarInt(packetType) buf.writeNBT(data) buf.readBytes(ByteArray(buf.capacity())) player.sendPluginMessage(BukkitPlugin.getInstance(), channel, buf.array()) } }