* 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>
This commit is contained in:
xiaoxijun
2026-07-02 18:11:12 +08:00
committed by GitHub
parent 37c22202ab
commit 07073cb4a3
16 changed files with 1542 additions and 173 deletions
@@ -6,6 +6,7 @@ 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;
@@ -24,10 +25,10 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import static net.minecraft.world.level.NaturalSpawner.getRoughBiome;
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<>();
@@ -35,6 +36,9 @@ public class EntitiesCounterUtil {
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
@@ -104,94 +108,193 @@ public class EntitiesCounterUtil {
}
public static void tick(ServerLevel level) {
Runnable task = () -> {
try {
Cache<Integer, ReferenceList<Entity>> data0 = globalLoadedEntities.get(level);
if (data0 == null) return;
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();
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.async ? 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;
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);
}
// Paper end - Only count natural spawns
map.addTo(category, 1);
}
// Lophine end - Copy from net/minecraft/world/level/NaturalSpawner
}
// Lophine end - Copy from net/minecraft/world/level/NaturalSpawner
}
mobsMap.put(level, map);
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();
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);
}
spawnableChunkCount.put(level, count);
} catch (Exception e) {
LogUtils.getClassLogger().error("Failed to run task", e);
}
} 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();
}
};
if (GlobalEntitiesCounter.async) {
tasks.put(level, CompletableFuture.runAsync(task).exceptionally(ex -> {
LogUtils.getClassLogger().error("Failed to run task", ex);
return null;
}));
} else {
task.run();
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 runRemainingTasks(
public static NaturalSpawner.SpawnState createSpawnState(
ServerLevel level, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkGetter, LocalMobCapCalculator calculator, final boolean countMobs
) {
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;
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
});
}
// Paper end - Only count natural spawns
BlockPos blockPos = entity.blockPosition();
chunkGetter.query(ChunkPos.pack(blockPos), chunk -> {
MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
if (mobSpawnCost != null) {
potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
}
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;
if (calculator != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
calculator.addMob(chunk.getPos(), entity.getType().getCategory());
}
Object2IntOpenHashMap<MobCategory> globalCounts = getGlobalCounts(level);
// Paper start - Optional per player mob spawns
if (countMobs) {
chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
}
// Paper end - Optional per player mob spawns
});
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));
}
return new NaturalSpawner.SpawnState(getTotalChunkCount(level), map, potentialCalculator, calculator);
// Lophine end - Copy from net/minecraft/world/level/NaturalSpawner
}
}