Files
Lophine/lophine-server/src/main/java/fun/bm/lophine/utils/EntitiesCounterUtil.java
T
xiaoxijun 07073cb4a3 #154 (#155)
* Add PCA sync protocol support

Introduce PCA sync protocol and integrate it into Carpet/Leaves.

- Added org/leavesmc/leaves/protocol/PcaSyncProtocol.java: full implementation of the PCA sync protocol (handlers for sync requests, watcher maps, update payloads, player/block watch management, scheduling, and config-driven behavior).
- Added fun/bm/lophine/config/modules/function/protocol/PcaSyncProtocolConfig.java and enum PcaPlayerEntityType.java to drive enabling and player-entity sync policy.
- Integrated protocol into Carpet: register new carpet rules (pcaSyncProtocol, pcaSyncPlayerEntity) in CarpetCompatSync and added batching to avoid excessive updates when rules change.
- Enhanced CarpetServerProtocol to track active players, batch rule updates, and send server rule data safely on the server thread.
- Added a Minecraft patch (0046-PCA-sync-protocol.patch) to trigger syncs from setChanged() in containers, chest boats, minecarts and BlockEntity updates so watched clients receive entity/block-entity updates.

This enables PCA clients to watch and receive synchronized block-entity and entity state on demand, while keeping updates gated by config and player permissions and improving rule-update performance via batching.

* Add Vanilla-like experience config and updates

Introduce a new VanillaLikeExperienceConfig module and a patch (0047) that gates multiple Paper/Luminol behavior changes behind this toggle to restore more vanilla-like technical behavior when enabled. The patch injects conditional checks across many server classes (entity merging, phantoms, pistons, hoppers, TNT handling, explosions, portal/piston protections, item merging, teleportation, spawn counting, etc.). Also adjust PCA sync protocol references in the existing 0046 patch to use the updated fun.bm.lophine package path. Includes the new fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig.java file.

* Restore vanilla ender pearl loading on Folia

Add patch (features/0048) that restores vanilla ender-pearl loading behavior on Folia. Reintroduces ender-pearl tracking and persistence: ServerPlayer now uses a thread-safe enderPearls set and saves pearl tags, ThrownEnderpearl maintains a lastSavedEnderPearlTag (updated on construction, owner changes, spawn and restore) and updates/clears it on tick/removal, and EnderpearlItem updates the saved tag when spawning a pearl. PlayerList and BotList now schedule pearl removal via the Bukkit taskScheduler when players/bots quit. The changes preserve Paper's legacyEnderPearlBehavior checks where applicable and add logging and safer tag handling to avoid saving invalid snapshots.

* Restore vanilla ender pearl loading

Restore vanilla ender-pearl loading behavior on Folia by preserving the last saved NBT for thrown pearls. ThrownEnderpearl.java: add a volatile CompoundTag lastSavedEnderPearlTag, provide a defensive-copying getter getLastSavedEnderPearlTag(), adjust @Nullable annotation placement, and update logger/comment. Minor index/format updates in ServerPlayer.java, PlayerList.java and EnderpearlItem.java.

* remove extra import

* Add Precise mob cap and adjust ender pearl patch

Introduce a Precise mob cap feature and integrate it into Paper/Folia world/region lifecycle. Adds new patch features/0049-Precise-mob-cap.patch which modifies RegionizedWorldData, ServerChunkCache and ServerLevel to support an incremental, event-driven mob counting system that can supersede the existing GlobalEntitiesCounter. Adds PreciseMobCapConfig (toggle and behavior flags) and PreciseMobCapCounter (unique id management, area map registration, per-level global counters, spawnable-chunk tracking, spawn state creation, and cleanup on unload). Also updates the existing 0048 ender-pearl loading patch file (minor import/index adjustments). The Precise mob cap counts only relevant mobs (configurable to match vanilla or Paper-optimized behavior), hooks into entity tracking start/end, and requires per-player-mob-spawns=false to be set in Paper world defaults when enabled.

* remove extra import

* pre merge config

* transform config

* merge config

* rebuild patches

* fix a bug in new counter

* simplify code

---------

Co-authored-by: Helvetica Volubi <suisuroru@blue-millennium.fun>
Co-authored-by: Helvetica Volubi <88063803+Suisuroru@users.noreply.github.com>
2026-07-02 18:11:12 +08:00

301 lines
14 KiB
Java

package fun.bm.lophine.utils;
import ca.spottedleaf.moonrise.common.list.ReferenceList;
import ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.config.modules.experiment.GlobalEntitiesCounter;
import fun.bm.lophine.config.modules.fixes.VanillaLikeExperienceConfig;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LocalMobCapCalculator;
import net.minecraft.world.level.NaturalSpawner;
import net.minecraft.world.level.PotentialCalculator;
import net.minecraft.world.level.biome.MobSpawnSettings;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class EntitiesCounterUtil {
// global entities counter
private static final Map<ServerLevel, Cache<Integer, ReferenceList<Entity>>> globalLoadedEntities = new ConcurrentHashMap<>();
private static final Map<ServerLevel, Object2IntOpenHashMap<MobCategory>> mobsMap = Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<ServerLevel, Cache<Integer, PositionCountingAreaMap<ServerPlayer>>> mobsAreaMap = new ConcurrentHashMap<>();
private static final Map<ServerLevel, Integer> spawnableChunkCount = new ConcurrentHashMap<>();
private static final Map<ServerLevel, CompletableFuture<Void>> tasks = new ConcurrentHashMap<>();
private static final Set<Integer> UniqueIds = ConcurrentHashMap.newKeySet();
// precise mob counter extra
private static final Map<ServerLevel, EnumMap<MobCategory, AtomicInteger>> globalMobCounts = new ConcurrentHashMap<>();
private static int lastUsedId = 0;
private static final int CLEANUP_INTERVAL = 200; // each 200 ids used
public static int generateUniqueId() {
synchronized (UniqueIds) {
if (lastUsedId % CLEANUP_INTERVAL == 0) runCleanUp();
int id = lastUsedId;
while (UniqueIds.contains(id)) {
id++;
}
lastUsedId = id;
UniqueIds.add(id);
return id;
}
}
public static void onWorldDataUnload(ServerLevel level, int uniqueId) {
UniqueIds.remove(uniqueId);
Cache<Integer, ReferenceList<Entity>> entitiesCache = globalLoadedEntities.get(level);
if (entitiesCache != null) {
entitiesCache.invalidate(uniqueId);
}
Cache<Integer, PositionCountingAreaMap<ServerPlayer>> areaCache = mobsAreaMap.get(level);
if (areaCache != null) {
areaCache.invalidate(uniqueId);
}
}
private static void runCleanUp() {
Set<Integer> logged = new HashSet<>();
for (Cache<Integer, ReferenceList<Entity>> collection : globalLoadedEntities.values()) {
logged.addAll(collection.asMap().keySet());
}
for (int num : UniqueIds) {
if (logged.contains(num)) continue;
UniqueIds.remove(num);
}
}
public static void addDataToLoaded(ServerLevel level, ReferenceList<Entity> data, int uniqueId) {
Cache<Integer, ReferenceList<Entity>> data0 = globalLoadedEntities.computeIfAbsent(level, _ -> CacheBuilder.newBuilder().concurrencyLevel(16).weakValues().build());
if (data0.asMap().containsKey(uniqueId)) return;
data0.put(uniqueId, data);
}
public static void reportAreaMap(ServerLevel level, PositionCountingAreaMap<ServerPlayer> areaMap, int uniqueId) {
Cache<Integer, PositionCountingAreaMap<ServerPlayer>> areaMap0 = mobsAreaMap.computeIfAbsent(level, _ -> CacheBuilder.newBuilder().concurrencyLevel(16).weakValues().build());
if (areaMap0.asMap().containsKey(uniqueId)) return;
areaMap0.put(uniqueId, areaMap);
}
public static int getTotalChunkCount(ServerLevel level) {
return spawnableChunkCount.getOrDefault(level, 0);
}
public static @Nullable Object2IntOpenHashMap<MobCategory> getMobsMap(ServerLevel level) {
return mobsMap.get(level);
}
public static boolean canRunNewTask(ServerLevel level) {
CompletableFuture<Void> task = tasks.get(level);
return task == null || task.isDone();
}
public static void tick(ServerLevel level) {
if (GlobalEntitiesCounter.isDefaultModule()) {
Runnable task = () -> {
try {
Cache<Integer, ReferenceList<Entity>> data0 = globalLoadedEntities.get(level);
if (data0 == null) return;
Object2IntOpenHashMap<MobCategory> map = new Object2IntOpenHashMap<>();
Collection<ReferenceList<Entity>> snapshot = data0.asMap().values();
for (ReferenceList<Entity> data : snapshot) {
if (data == null) continue;
for (Entity entity : GlobalEntitiesCounter.isAsync() ? data.copy() : data) {
if (entity == null || entity.isRemoved() || !entity.isAlive()) continue;
// Lophine start - Copy from net/minecraft/world/level/NaturalSpawner
MobCategory category = entity.getType().getCategory();
if (category != MobCategory.MISC) {
// Paper start - Only count natural spawns
if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
!(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL ||
entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
continue;
}
// Paper end - Only count natural spawns
map.addTo(category, 1);
}
}
// Lophine end - Copy from net/minecraft/world/level/NaturalSpawner
}
mobsMap.put(level, map);
Cache<Integer, PositionCountingAreaMap<ServerPlayer>> collection = mobsAreaMap.get(level);
if (collection != null) {
int count = 0;
for (PositionCountingAreaMap<ServerPlayer> areaMap : collection.asMap().values()) {
if (areaMap != null) {
count += areaMap.getTotalPositions();
}
}
spawnableChunkCount.put(level, count);
}
} catch (Exception e) {
LogUtils.getClassLogger().error("Failed to run task", e);
}
};
if (GlobalEntitiesCounter.isAsync()) {
tasks.put(level, CompletableFuture.runAsync(task).exceptionally(ex -> {
LogUtils.getClassLogger().error("Failed to run task", ex);
return null;
}));
} else {
task.run();
}
} else {
Cache<Integer, PositionCountingAreaMap<ServerPlayer>> cache = mobsAreaMap.get(level);
if (cache == null) return;
int total = 0;
for (PositionCountingAreaMap<ServerPlayer> map : cache.asMap().values()) {
if (map != null) {
total += map.getTotalPositions();
}
}
spawnableChunkCount.put(level, total);
}
}
public static NaturalSpawner.SpawnState createSpawnState(
ServerLevel level, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator, final boolean countMobs
) {
if (GlobalEntitiesCounter.isDefaultModule()) {
Object2IntOpenHashMap<MobCategory> map = getMobsMap(level);
if (map == null) return null; // skip if no data
// Lophine start - Copy from net/minecraft/world/level/NaturalSpawner
PotentialCalculator potentialCalculator = new PotentialCalculator();
for (Entity entity : entities) {
if (entity == null || entity.isRemoved() || !entity.isAlive()) continue;
// Paper start - Only count natural spawns
if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
!(entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL ||
entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
continue;
}
// Paper end - Only count natural spawns
BlockPos blockPos = entity.blockPosition();
chunkGetter.query(ChunkPos.pack(blockPos), chunk -> {
MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
if (mobSpawnCost != null) {
potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
}
if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
calculator.addMob(chunk.getPos(), entity.getType().getCategory());
}
// Paper start - Optional per player mob spawns
if (countMobs) {
chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
}
// Paper end - Optional per player mob spawns
});
}
return new NaturalSpawner.SpawnState(getTotalChunkCount(level), map, potentialCalculator, calculator);
// Lophine end - Copy from net/minecraft/world/level/NaturalSpawner
} else {
int chunkCount = getSpawnableChunkCount(level);
if (chunkCount == 0) return null;
Object2IntOpenHashMap<MobCategory> globalCounts = getGlobalCounts(level);
PotentialCalculator potentialCalculator = new PotentialCalculator();
for (Entity entity : entities) {
if (entity == null || entity.isRemoved() || !entity.isAlive()) continue;
if (!shouldCount(entity)) continue;
BlockPos pos = entity.blockPosition();
chunkGetter.query(ChunkPos.pack(pos), chunk -> {
MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
if (mobSpawnCost != null) {
potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
}
if (calculator != null && entity instanceof Mob) {
calculator.addMob(chunk.getPos(), entity.getType().getCategory());
}
});
}
return new NaturalSpawner.SpawnState(chunkCount, globalCounts, potentialCalculator, calculator);
}
}
public static Object2IntOpenHashMap<MobCategory> getGlobalCounts(ServerLevel level) {
EnumMap<MobCategory, AtomicInteger> counts = globalMobCounts.get(level);
Object2IntOpenHashMap<MobCategory> result = new Object2IntOpenHashMap<>();
if (counts != null) {
for (Map.Entry<MobCategory, AtomicInteger> entry : counts.entrySet()) {
result.put(entry.getKey(), entry.getValue().get());
}
}
return result;
}
public static int getSpawnableChunkCount(ServerLevel level) {
Integer value = spawnableChunkCount.get(level);
return value != null ? value : 0;
}
public static void onWorldUnload(ServerLevel level) {
globalMobCounts.remove(level);
mobsAreaMap.remove(level);
spawnableChunkCount.remove(level);
}
public static void onEntityTrackingStart(ServerLevel level, Entity entity) {
if (!shouldCount(entity)) return;
MobCategory category = entity.getType().getCategory();
getOrCreateCounts(level).computeIfAbsent(category, k -> new AtomicInteger(0)).incrementAndGet();
}
public static boolean shouldCount(Entity entity) {
MobCategory category = entity.getType().getCategory();
if (category == MobCategory.MISC) return false;
if (entity instanceof Mob mob && (mob.isPersistenceRequired() || mob.requiresCustomPersistence())) {
return false;
}
if (VanillaLikeExperienceConfig.enabled || entity.level().paperConfig().entities.spawning.countAllMobsForSpawning) {
return true;
}
return entity.spawnReason == CreatureSpawnEvent.SpawnReason.NATURAL
|| entity.spawnReason == CreatureSpawnEvent.SpawnReason.CHUNK_GEN;
}
private static EnumMap<MobCategory, AtomicInteger> getOrCreateCounts(ServerLevel level) {
return globalMobCounts.computeIfAbsent(level, _ -> new EnumMap<>(MobCategory.class));
}
public static void onEntityTrackingEnd(ServerLevel level, Entity entity) {
if (!shouldCount(entity)) return;
MobCategory category = entity.getType().getCategory();
EnumMap<MobCategory, AtomicInteger> counts = getOrCreateCounts(level);
AtomicInteger counter = counts.get(category);
if (counter != null) {
counter.updateAndGet(v -> Math.max(0, v - 1));
}
}
}