* 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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user