Carpet features (#134)

* Carpet features

* refactor(config): move up config

* refactor file

diff in file system

* Update Luminol

* move up - stage 1

* Revert "Update Luminol"

This reverts commit 6f02993bfe.

* move up - stage 2

* new carpet features

* upload docs

* stage - pre update

* [ci skip]Update Luminol

only update for dev branch, so skip ci

* remove blank

* feat:add carpet core config & move some function to config load stage

* remove some unused value & logic

* fix up

* Update Luminol

* fix up

* remove imports update

* add warning to carpet system if it is enabled

* Update Luminol

* use new function to sync config

* oops!

* Update Luminol

* fix up a bug

* refactor

* Update Luminol

* move up directory

* Update Luminol

* oops!

* refactor need

* feat: add CoreConfig to enable general rules in CarpetCompatSync

* modify some default value

* fix: enhance HUD logger subscriptions and improve player data handling

---------

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-04-13 19:45:35 +08:00
committed by GitHub
parent 97a17dbf78
commit f65b7f176b
20 changed files with 2613 additions and 20 deletions
@@ -0,0 +1,24 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Bacteriawa <A3167717663@hotmail.com>
Date: Sun, 22 Mar 2026 01:39:17 +0800
Subject: [PATCH] Carpet features
diff --git a/src/main/java/me/earthme/luminol/config/ConfigManager.java b/src/main/java/me/earthme/luminol/config/ConfigManager.java
index f5fb4e210c07f32e52eb8c1ba166d1571a6778c7..e990c147e1b632ef5a33e4f3c37b5cd608040ac7 100644
--- a/src/main/java/me/earthme/luminol/config/ConfigManager.java
+++ b/src/main/java/me/earthme/luminol/config/ConfigManager.java
@@ -25,6 +25,13 @@ public class ConfigManager {
public static void initConfigs() {
configfiles.put("luminol", ConfigsInstance.of("luminol", "me.earthme.luminol.config.modules"));
configfiles.put("lophine", ConfigsInstance.of("lophine", "fun.bm.lophine.config.modules")); // add lophine global config
+ configfiles.put("lophine_carpet", ConfigsInstance.of(
+ new java.io.File("lophine_config"),
+ "lophine_carpet",
+ "lophine_carpet_config.toml",
+ "lophinecarpetconfig",
+ "fun.bm.lophine.carpet.config.modules"
+ )); // add lophine carpet config
preLoad();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
package fun.bm.lophine.carpet;
import fun.bm.lophine.carpet.config.modules.GeneralCompatConfig;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.objecthunter.exp4j.ExpressionBuilder;
import java.math.BigDecimal;
public final class CarpetCalculatorCompatHelper {
public static boolean handleChat(ServerPlayer player, String rawMessage) {
if (!GeneralCompatConfig.simpleInGameCalculator || !rawMessage.startsWith("=")) {
return false;
}
String expression = rawMessage.substring(1).trim();
if (expression.isEmpty()) {
return false;
}
Component response;
try {
double result = new ExpressionBuilder(expression).build().evaluate();
response = Component.literal(expression + " = " + formatNumber(result)).withStyle(ChatFormatting.YELLOW);
} catch (RuntimeException exception) {
response = Component.literal("Calculator error: " + exception.getMessage()).withStyle(ChatFormatting.RED);
}
Component scheduledResponse = response;
player.getBukkitEntity().taskScheduler.schedule((ServerPlayer scheduledPlayer) -> scheduledPlayer.sendSystemMessage(scheduledResponse), null, 1L);
return true;
}
private static String formatNumber(double value) {
if (Double.isFinite(value) && Math.rint(value) == value && value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) {
return Long.toString((long) value);
}
return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
}
}
@@ -0,0 +1,135 @@
package fun.bm.lophine.carpet;
import fun.bm.lophine.carpet.config.modules.CoreConfig;
import fun.bm.lophine.carpet.config.modules.CounterCompatConfig;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import fun.bm.lophine.carpet.config.modules.GeneralCompatConfig;
import fun.bm.lophine.config.modules.experiment.CommandConfig;
import fun.bm.lophine.config.modules.fixes.UpdateSuppressionCrashFixConfig;
import fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig;
import fun.bm.lophine.config.modules.function.FakeplayerConfig;
import fun.bm.lophine.config.modules.function.LanguageConfig;
import fun.bm.lophine.config.modules.function.WoolHopperCounterConfig;
import fun.bm.lophine.protocol.CarpetLoggerProtocol;
import me.earthme.luminol.config.modules.optimizations.OptimizedDragonRespawnConfig;
import org.leavesmc.leaves.bot.ServerBot;
import org.leavesmc.leaves.protocol.CarpetServerProtocol;
import java.util.List;
// WARNING: THIS FILE NEED TO FULLY REWRITTEN
public final class CarpetCompatSync {
private static boolean init = false;
public static void apply() {
if (init) return;
if (CoreConfig.enabled) applyGeneralRules();
applyFakePlayerRules();
applyCounterRules();
registerProtocolRules();
init = true;
}
private static void applyGeneralRules() {
LanguageConfig.lang = GeneralCompatConfig.language;
UpdateSuppressionCrashFixConfig.enabled = GeneralCompatConfig.amsUpdateSuppressionCrashFix || GeneralCompatConfig.yeetUpdateSuppressionCrash;
fun.bm.lophine.config.modules.experiment.RedStoneConfig.redstoneIgnoreUpwardsUpdate = GeneralCompatConfig.dustTrapdoorReintroduced;
fun.bm.lophine.config.modules.experiment.RedStoneConfig.cce = GeneralCompatConfig.shulkerBoxCCEReintroduced;
fun.bm.lophine.config.modules.experiment.RedStoneConfig.instantBlockUpdater = GeneralCompatConfig.instantBlockUpdaterReintroduced;
CommandConfig.tick = GeneralCompatConfig.commandTick;
CreativeFlyNoClipConfig.enabled = GeneralCompatConfig.creativeNoClip;
OptimizedDragonRespawnConfig.optimizedRespawn = GeneralCompatConfig.optimizedDragonRespawn;
CarpetLoggerProtocol.refreshConfiguredDefaults(!init);
}
private static void applyFakePlayerRules() {
FakeplayerConfig.enable = FakePlayerCompatConfig.commandBot || FakePlayerCompatConfig.commandPlayer;
FakeplayerConfig.canResident = FakePlayerCompatConfig.fakePlayerResident;
FakeplayerConfig.canOpenInventory = FakePlayerCompatConfig.openFakePlayerInventory;
FakeplayerConfig.tickType = FakePlayerCompatConfig.fakePlayerTicksLikeRealPlayer
? ServerBot.TickType.NETWORK
: ServerBot.TickType.ENTITY_LIST;
}
private static void applyCounterRules() {
WoolHopperCounterConfig.enabled = CounterCompatConfig.hopperCounters;
WoolHopperCounterConfig.unlimitedSpeed = CounterCompatConfig.hopperCountersUnlimitedSpeed;
}
private static List<String> sanitizeDefaultLoggers(List<String> configuredLoggers) {
return configuredLoggers == null ? List.of() : List.copyOf(configuredLoggers);
}
private static void registerProtocolRules() {
CarpetServerProtocol.CarpetRules.clear();
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "language", GeneralCompatConfig.language));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "amsUpdateSuppressionCrashFix", GeneralCompatConfig.amsUpdateSuppressionCrashFix));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetUpdateSuppressionCrash", GeneralCompatConfig.yeetUpdateSuppressionCrash));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "dustTrapdoorReintroduced", GeneralCompatConfig.dustTrapdoorReintroduced));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "shulkerBoxCCEReintroduced", GeneralCompatConfig.shulkerBoxCCEReintroduced));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "instantBlockUpdaterReintroduced", GeneralCompatConfig.instantBlockUpdaterReintroduced));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandTick", GeneralCompatConfig.commandTick));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "creativeNoClip", GeneralCompatConfig.creativeNoClip));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedDragonRespawn", GeneralCompatConfig.optimizedDragonRespawn));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "antiSpamDisabled", GeneralCompatConfig.antiSpamDisabled));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "blockPlacementIgnoreEntity", GeneralCompatConfig.blockPlacementIgnoreEntity));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeOpenContainerForcibly", GeneralCompatConfig.creativeOpenContainerForcibly));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "creativeOneHitKill", GeneralCompatConfig.creativeOneHitKill));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "observerNoDetection", GeneralCompatConfig.observerNoDetection));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "bambooModelNoOffset", GeneralCompatConfig.bambooModelNoOffset));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "creativeNoItemCooldown", GeneralCompatConfig.creativeNoItemCooldown));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "ctrlQCraftingFix", GeneralCompatConfig.ctrlQCraftingFix));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "carpetAlwaysSetDefault", GeneralCompatConfig.carpetAlwaysSetDefault));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "placementRotationFix", GeneralCompatConfig.placementRotationFix));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntDoNotUpdate", GeneralCompatConfig.tntDoNotUpdate));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "totallyNoBlockUpdate", GeneralCompatConfig.totallyNoBlockUpdate));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tiscmNetworkProtocol", GeneralCompatConfig.tiscmNetworkProtocol));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "hopperNoItemCost", GeneralCompatConfig.hopperNoItemCost));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "explosionNoBlockDamage", GeneralCompatConfig.explosionNoBlockDamage));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedTNTHighPriority", GeneralCompatConfig.optimizedTNTHighPriority));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "tntPrimerMomentumRemoved", GeneralCompatConfig.tntPrimerMomentumRemoved));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntIgnoreRedstoneSignal", GeneralCompatConfig.tntIgnoreRedstoneSignal));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntDupingFix", GeneralCompatConfig.tntDupingFix));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "interactionUpdates", GeneralCompatConfig.interactionUpdates));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "xpNoCooldown", GeneralCompatConfig.xpNoCooldown));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "powerfulExpMending", GeneralCompatConfig.powerfulExpMending));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "clientSettingsLostOnRespawnFix", GeneralCompatConfig.clientSettingsLostOnRespawnFix));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "sensibleEnderman", GeneralCompatConfig.sensibleEnderman));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "entityInstantDeathRemoval", GeneralCompatConfig.entityInstantDeathRemoval));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "farmlandTrampledDisabled", GeneralCompatConfig.farmlandTrampledDisabled));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "shulkerGolem", GeneralCompatConfig.shulkerGolem));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "preventEndSpikeRespawn", GeneralCompatConfig.preventEndSpikeRespawn));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "yeetOutOfOrderChatKick", GeneralCompatConfig.yeetOutOfOrderChatKick));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableBoneBlock", GeneralCompatConfig.betterCraftableBoneBlock));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "betterCraftableDispenser", GeneralCompatConfig.betterCraftableDispenser));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "viewDistance", GeneralCompatConfig.viewDistance));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickCommandPermission", GeneralCompatConfig.normalizedTickCommandPermission()));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tickFreezeCommandToggleable", GeneralCompatConfig.tickFreezeCommandToggleable));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "syncServerMsptMetricsData", GeneralCompatConfig.syncServerMsptMetricsData));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetorgaddition", "simpleInGameCalculator", GeneralCompatConfig.simpleInGameCalculator));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "microTiming", GeneralCompatConfig.microTiming));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "fastRedstoneDust", GeneralCompatConfig.fastRedstoneDust));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "lagFreeSpawning", GeneralCompatConfig.lagFreeSpawning));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedFastEntityMovement", GeneralCompatConfig.optimizedFastEntityMovement));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "optimizedHardHitBoxEntityCollision", GeneralCompatConfig.optimizedHardHitBoxEntityCollision));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "tntFuseDuration", GeneralCompatConfig.normalizedTntFuseDuration()));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "defaultLoggers", CarpetLoggerProtocol.serializeConfiguredDefaults(sanitizeDefaultLoggers(GeneralCompatConfig.defaultLoggers))));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "commandBot", FakePlayerCompatConfig.commandBot));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "commandPlayer", FakePlayerCompatConfig.commandPlayer));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerResident", FakePlayerCompatConfig.fakePlayerResident));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "openFakePlayerInventory", FakePlayerCompatConfig.openFakePlayerInventory));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "fakePlayerTicksLikeRealPlayer", FakePlayerCompatConfig.fakePlayerTicksLikeRealPlayer));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerDefaultSurvivalMode", FakePlayerCompatConfig.fakePlayerDefaultSurvivalMode));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerInteractLikeClient", FakePlayerCompatConfig.fakePlayerInteractLikeClient));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplaceTool", FakePlayerCompatConfig.fakePlayerAutoReplaceTool));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoReplenishment", FakePlayerCompatConfig.fakePlayerAutoReplenishment));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpetamsaddition", "fakePlayerAutoReplenishmentFormShulkerBox", FakePlayerCompatConfig.fakePlayerAutoReplenishmentFormShulkerBox));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerAutoFish", FakePlayerCompatConfig.fakePlayerAutoFish));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("lophine", "fakePlayerReloadAction", FakePlayerCompatConfig.fakePlayerReloadAction));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpet", "hopperCounters", CounterCompatConfig.hopperCounters));
CarpetServerProtocol.CarpetRules.register(CarpetServerProtocol.CarpetRule.of("carpettisaddition", "hopperCountersUnlimitedSpeed", CounterCompatConfig.hopperCountersUnlimitedSpeed));
}
}
@@ -0,0 +1,42 @@
package fun.bm.lophine.carpet;
import java.util.function.Supplier;
public final class InteractionUpdateCompatHelper {
private static final ThreadLocal<Integer> SUPPRESSED_DEPTH = ThreadLocal.withInitial(() -> 0);
public static boolean shouldSkipUpdates() {
return SUPPRESSED_DEPTH.get() > 0;
}
public static void runWithSuppressedUpdates(Runnable action) {
push();
try {
action.run();
} finally {
pop();
}
}
public static <T> T supplyWithSuppressedUpdates(Supplier<T> action) {
push();
try {
return action.get();
} finally {
pop();
}
}
private static void push() {
SUPPRESSED_DEPTH.set(SUPPRESSED_DEPTH.get() + 1);
}
private static void pop() {
int depth = SUPPRESSED_DEPTH.get();
if (depth <= 1) {
SUPPRESSED_DEPTH.remove();
} else {
SUPPRESSED_DEPTH.set(depth - 1);
}
}
}
@@ -0,0 +1,112 @@
package fun.bm.lophine.carpet;
import com.mojang.logging.LogUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public final class LagFreeSpawningCompatHelper {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Map<ServerLevel, Map<EntityType<?>, Mob>> PRECOOKED_MOBS = new WeakHashMap<>();
public static boolean hasNoCollision(ServerLevel world, AABB bb) {
int minX = Mth.floor(bb.minX);
int minY = Mth.floor(bb.minY);
int minZ = Mth.floor(bb.minZ);
int maxY = Mth.ceil(bb.maxY) - 1;
BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos();
if (bb.getXsize() <= 1.0 && bb.getZsize() <= 1.0) {
for (int y = minY; y <= maxY; y++) {
blockPos.set(minX, y, minZ);
VoxelShape shape = world.getBlockState(blockPos).getCollisionShape(world, blockPos);
if (shape != Shapes.empty()) {
return shape != Shapes.block() && world.noCollision(bb);
}
}
return true;
}
int maxX = Mth.ceil(bb.maxX) - 1;
int maxZ = Mth.ceil(bb.maxZ) - 1;
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
blockPos.set(x, y, z);
VoxelShape shape = world.getBlockState(blockPos).getCollisionShape(world, blockPos);
if (shape != Shapes.empty()) {
return shape != Shapes.block() && world.noCollision(bb);
}
}
}
}
int minBelow = minY - 1;
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
blockPos.set(x, minBelow, z);
BlockState state = world.getBlockState(blockPos);
Block block = state.getBlock();
if (state.is(BlockTags.FENCES)
|| state.is(BlockTags.WALLS)
|| block instanceof FenceGateBlock && !state.getValue(FenceGateBlock.OPEN)) {
if (x == minX || x == maxX || z == minZ || z == maxZ) {
return world.noCollision(bb);
}
return false;
}
}
}
return true;
}
public static double limitDistanceSq(MobCategory category, double distanceSq) {
return distanceSq > 16384.0 && category != MobCategory.CREATURE ? 0.0 : distanceSq;
}
public static @Nullable Mob getOrCreateMob(ServerLevel level, EntityType<?> entityType) {
Map<EntityType<?>, Mob> cache = PRECOOKED_MOBS.computeIfAbsent(level, key -> new HashMap<>());
Mob mob = cache.get(entityType);
if (mob != null && !mob.isRemoved()) {
return mob;
}
try {
if (entityType.create(level, EntitySpawnReason.NATURAL) instanceof Mob created) {
cache.put(entityType, created);
return created;
}
LOGGER.warn("Can't precook non-mob entity type: {}", entityType);
} catch (Exception exception) {
LOGGER.warn("Failed to precook mob {}", entityType, exception);
}
return null;
}
public static void markSpawned(ServerLevel level, EntityType<?> entityType) {
Map<EntityType<?>, Mob> cache = PRECOOKED_MOBS.get(level);
if (cache != null) {
cache.remove(entityType);
}
}
}
@@ -0,0 +1,28 @@
package fun.bm.lophine.carpet.config.modules;
import fun.bm.lophine.carpet.CarpetCompatSync;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(
category = EnumConfigCategory.ROOT,
name = "core",
directory = {"carpet"}
)
public class CoreConfig implements IConfigModule {
@ConfigInfo(name = "enabled", comments = """
Enable carpet features.
If you want to use any function in general directory from Carpet modifier,
you need to enable it.
ONLY GENERAL DIRECTORY WAS CONTROLLED BY THIS OPTION.
WARNING: IF YOU ENABLED IT, ORIGINAL CONFIG IN LOPHINE CONFIG WILL BE OVERWRITTEN.""")
public static boolean enabled = false;
@Override
public void beforeFinalLoad() {
CarpetCompatSync.apply();
}
}
@@ -0,0 +1,24 @@
package fun.bm.lophine.carpet.config.modules;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(
category = EnumConfigCategory.ROOT,
name = "hopper_counter",
directory = {"carpet"},
comments = """
Hopper counter compatibility mapped onto Lophine's wool hopper counter implementation."""
)
public class CounterCompatConfig implements IConfigModule {
@ConfigInfo(name = "hopperCounters", comments = """
Enable the existing wool hopper counter implementation.""")
public static boolean hopperCounters = false;
@ConfigInfo(name = "hopperCountersUnlimitedSpeed", comments = """
Remove the hopper transfer speed limit for counters.
Only effective when hopperCounters is enabled.""")
public static boolean hopperCountersUnlimitedSpeed = false;
}
@@ -0,0 +1,64 @@
package fun.bm.lophine.carpet.config.modules;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(
category = EnumConfigCategory.ROOT,
name = "fakeplayer",
directory = {"carpet"},
comments = """
Carpet fakeplayer compatibility mapped onto Lophine fakeplayers.
commandPlayer is currently backed by Lophine's /bot command surface."""
)
public class FakePlayerCompatConfig implements IConfigModule {
@ConfigInfo(name = "commandBot", comments = """
Enable Lophine's /bot command.""")
public static boolean commandBot = false;
@ConfigInfo(name = "commandPlayer", comments = """
Map Carpet's commandPlayer rule to the same fakeplayer command surface used by /bot.""")
public static boolean commandPlayer = false;
@ConfigInfo(name = "fakePlayerResident", comments = """
Keep fakeplayers resident across unload and restart.""")
public static boolean fakePlayerResident = false;
@ConfigInfo(name = "openFakePlayerInventory", comments = """
Allow opening fakeplayer inventories.""")
public static boolean openFakePlayerInventory = false;
@ConfigInfo(name = "fakePlayerTicksLikeRealPlayer", comments = """
Tick fakeplayers in the network phase to better match real player timing.""")
public static boolean fakePlayerTicksLikeRealPlayer = false;
@ConfigInfo(name = "fakePlayerDefaultSurvivalMode", comments = """
Force newly created fakeplayers to start in survival instead of the server default gamemode.""")
public static boolean fakePlayerDefaultSurvivalMode = false;
@ConfigInfo(name = "fakePlayerInteractLikeClient", comments = """
Make fakeplayer entity interaction follow client-side fallback behavior more closely.""")
public static boolean fakePlayerInteractLikeClient = false;
@ConfigInfo(name = "fakePlayerAutoReplaceTool", comments = """
Toggle automatic tool replacement for fakeplayers.""")
public static boolean fakePlayerAutoReplaceTool = false;
@ConfigInfo(name = "fakePlayerAutoReplenishment", comments = """
Toggle automatic stack replenishment for fakeplayers.""")
public static boolean fakePlayerAutoReplenishment = false;
@ConfigInfo(name = "fakePlayerAutoReplenishmentFormShulkerBox", comments = """
Let fakeplayer replenishment pull matching items out of shulker boxes in the inventory.""")
public static boolean fakePlayerAutoReplenishmentFormShulkerBox = false;
@ConfigInfo(name = "fakePlayerAutoFish", comments = """
Let fakeplayers holding a fishing rod automatically cast and reel it in.""")
public static boolean fakePlayerAutoFish = false;
@ConfigInfo(name = "fakePlayerReloadAction", comments = """
Persist queued fakeplayer actions across save and reload.""")
public static boolean fakePlayerReloadAction = false;
}
@@ -0,0 +1,239 @@
package fun.bm.lophine.carpet.config.modules;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
import java.util.List;
@ConfigClassInfo(
category = EnumConfigCategory.ROOT,
name = "general",
directory = {"carpet"},
comments = """
Carpet/AMS/TIS/Org compatibility rules backed by existing Lophine features.
Only rules that already have a working server-side implementation are exposed here."""
)
public class GeneralCompatConfig implements IConfigModule {
@ConfigInfo(name = "language", comments = """
Carpet language value forwarded to lophine.function.language.lang.""")
public static String language = "en_us";
@ConfigInfo(name = "amsUpdateSuppressionCrashFix", comments = """
Map AMS update suppression crash protection to Lophine's existing crash fix.""")
public static boolean amsUpdateSuppressionCrashFix = false;
@ConfigInfo(name = "yeetUpdateSuppressionCrash", comments = """
Map TIS update suppression crash yeeting to the same Lophine crash fix.""")
public static boolean yeetUpdateSuppressionCrash = false;
@ConfigInfo(name = "dustTrapdoorReintroduced", comments = """
Map dust-on-open-trapdoor behavior to Lophine's redstone-ignore-upwards-update option.""")
public static boolean dustTrapdoorReintroduced = false;
@ConfigInfo(name = "shulkerBoxCCEReintroduced", comments = """
Map shulker-box CCE update suppression to Lophine's cce-update-suppression option.""")
public static boolean shulkerBoxCCEReintroduced = false;
@ConfigInfo(name = "instantBlockUpdaterReintroduced", comments = """
Enable the existing instant block updater patch already carried by Lophine.""")
public static boolean instantBlockUpdaterReintroduced = false;
@ConfigInfo(name = "commandTick", comments = """
Enable the tick command support already patched into Lophine.""")
public static boolean commandTick = false;
@ConfigInfo(name = "creativeNoClip", comments = """
Enable the existing creative fly no clip implementation.""")
public static boolean creativeNoClip = false;
@ConfigInfo(name = "optimizedDragonRespawn", comments = """
Enable the existing optimized dragon respawn implementation from Luminol.""")
public static boolean optimizedDragonRespawn = false;
@ConfigInfo(name = "antiSpamDisabled", comments = """
Disable the server-side chat and creative-drop spam throttles used by vanilla/Spigot.""")
public static boolean antiSpamDisabled = false;
@ConfigInfo(name = "blockPlacementIgnoreEntity", comments = """
Allow creative players to place blocks without entity collision checks.""")
public static boolean blockPlacementIgnoreEntity = false;
@ConfigInfo(name = "creativeOpenContainerForcibly", comments = """
Allow creative players to forcibly open blocked chests, ender chests and shulker boxes.""")
public static boolean creativeOpenContainerForcibly = false;
@ConfigInfo(name = "creativeOneHitKill", comments = """
Allow creative players to instantly kill attackable non-creative, non-spectator entities.
Sneaking expands the effect into a small area attack.""")
public static boolean creativeOneHitKill = false;
@ConfigInfo(name = "observerNoDetection", comments = """
Disable observer detection pulses entirely.""")
public static boolean observerNoDetection = false;
@ConfigInfo(name = "bambooModelNoOffset", comments = """
Remove the random horizontal model offset from bamboo and bamboo saplings.""")
public static boolean bambooModelNoOffset = false;
@ConfigInfo(name = "creativeNoItemCooldown", comments = """
Skip item cooldown application for creative players.""")
public static boolean creativeNoItemCooldown = false;
@ConfigInfo(name = "ctrlQCraftingFix", comments = """
Compatibility flag for the upstream result-slot Ctrl+Q crafting fix already present in the current menu code.""")
public static boolean ctrlQCraftingFix = false;
@ConfigInfo(name = "carpetAlwaysSetDefault", comments = """
Compatibility flag for Lophine's config loader, which already writes default values into the compat config during preload.""")
public static boolean carpetAlwaysSetDefault = false;
@ConfigInfo(name = "placementRotationFix", comments = """
Use the player's main body rotation for placement direction checks instead of interpolated head yaw.""")
public static boolean placementRotationFix = false;
@ConfigInfo(name = "tntDoNotUpdate", comments = """
Prevent TNT from checking redstone power when first placed.""")
public static boolean tntDoNotUpdate = false;
@ConfigInfo(name = "totallyNoBlockUpdate", comments = """
Suppress neighbor and shape updates globally for block changes.""")
public static boolean totallyNoBlockUpdate = false;
@ConfigInfo(name = "tiscmNetworkProtocol", comments = """
Enable the native Carpet TIS Addition network channel on `tiscm:network/v1`.""")
public static boolean tiscmNetworkProtocol = false;
@ConfigInfo(name = "hopperNoItemCost", comments = """
Restore the transferred stack into a hopper when a wool block is placed on top of it.""")
public static boolean hopperNoItemCost = false;
@ConfigInfo(name = "explosionNoBlockDamage", comments = """
Let explosions damage entities without breaking blocks.""")
public static boolean explosionNoBlockDamage = false;
@ConfigInfo(name = "optimizedTNTHighPriority", comments = """
Compatibility flag for the already optimized server explosion path carried by the current runtime.""")
public static boolean optimizedTNTHighPriority = false;
@ConfigInfo(name = "tntPrimerMomentumRemoved", comments = """
Remove the random horizontal launch momentum from newly primed TNT.""")
public static boolean tntPrimerMomentumRemoved = false;
@ConfigInfo(name = "tntIgnoreRedstoneSignal", comments = """
Ignore redstone power when deciding whether TNT should auto-prime.""")
public static boolean tntIgnoreRedstoneSignal = false;
@ConfigInfo(name = "tntDupingFix", comments = """
Toggle the piston desync path used by vanilla TNT duplication setups.""")
public static boolean tntDupingFix = false;
@ConfigInfo(name = "interactionUpdates", comments = """
Control whether player interaction block changes emit normal block updates.
Set to false to suppress neighbor and shape updates during block use and breaking.""")
public static boolean interactionUpdates = true;
@ConfigInfo(name = "xpNoCooldown", comments = """
Allow players to absorb multiple experience orbs in the same tick without pickup delay.""")
public static boolean xpNoCooldown = false;
@ConfigInfo(name = "powerfulExpMending", comments = """
Let picked-up experience repair all damaged mending items in the player's inventory, not only equipped gear.""")
public static boolean powerfulExpMending = false;
@ConfigInfo(name = "clientSettingsLostOnRespawnFix", comments = """
Reapply the player's last known client settings after respawn.""")
public static boolean clientSettingsLostOnRespawnFix = false;
@ConfigInfo(name = "sensibleEnderman", comments = """
Restrict enderman block pickup to pumpkins and melons only.""")
public static boolean sensibleEnderman = false;
@ConfigInfo(name = "entityInstantDeathRemoval", comments = """
Remove the normal 20gt delay before dead living entities are discarded.""")
public static boolean entityInstantDeathRemoval = false;
@ConfigInfo(name = "farmlandTrampledDisabled", comments = """
Prevent farmland from turning into dirt when entities land on it.""")
public static boolean farmlandTrampledDisabled = false;
@ConfigInfo(name = "shulkerGolem", comments = """
Allow a carved pumpkin on top of a shulker box to summon a shulker.""")
public static boolean shulkerGolem = false;
@ConfigInfo(name = "preventEndSpikeRespawn", comments = """
Skip obsidian spike regeneration during dragon respawn.""")
public static boolean preventEndSpikeRespawn = false;
@ConfigInfo(name = "yeetOutOfOrderChatKick", comments = """
Ignore out-of-order secure chat chain checks instead of invalidating the chat session.""")
public static boolean yeetOutOfOrderChatKick = false;
@ConfigInfo(name = "betterCraftableBoneBlock", comments = """
Add the AMS alternate bone block recipe that yields 3 bone blocks from 9 bones.""")
public static boolean betterCraftableBoneBlock = false;
@ConfigInfo(name = "betterCraftableDispenser", comments = """
Add the AMS alternate dispenser recipes using a dropper.""")
public static boolean betterCraftableDispenser = false;
@ConfigInfo(name = "viewDistance", comments = """
Override the dedicated server's startup view distance with the Carpet-compatible value.""")
public static int viewDistance = 12;
@ConfigInfo(name = "tickCommandPermission", comments = """
Override the `/tick` command permission level.
Accepts values in the range 0..4, where 2 matches old Carpet behavior and 3 keeps vanilla.""")
public static int tickCommandPermission = 3;
@ConfigInfo(name = "tickFreezeCommandToggleable", comments = """
Make `/tick freeze` toggle back to running when executed while the server is already frozen.""")
public static boolean tickFreezeCommandToggleable = false;
@ConfigInfo(name = "syncServerMsptMetricsData", comments = """
Broadcast live MSPT samples through the native TISCM protocol channel.""")
public static boolean syncServerMsptMetricsData = false;
@ConfigInfo(name = "simpleInGameCalculator", comments = """
Evaluate chat messages prefixed with `=` as a simple calculator expression and reply privately.""")
public static boolean simpleInGameCalculator = false;
@ConfigInfo(name = "microTiming", comments = """
Compatibility flag for the built-in region profiler and timing instrumentation carried by Folia/Moonrise.""")
public static boolean microTiming = false;
@ConfigInfo(name = "fastRedstoneDust", comments = """
Route redstone dust updates through the Alternate Current fast-update backend.""")
public static boolean fastRedstoneDust = false;
@ConfigInfo(name = "lagFreeSpawning", comments = """
Use the lightweight collision and precooked-mob spawning path for natural spawning checks.""")
public static boolean lagFreeSpawning = false;
@ConfigInfo(name = "optimizedFastEntityMovement", comments = """
Compatibility flag for the always-on Moonrise/Paper fast entity movement collision pipeline.""")
public static boolean optimizedFastEntityMovement = false;
@ConfigInfo(name = "optimizedHardHitBoxEntityCollision", comments = """
Compatibility flag for the always-on Moonrise/Paper hard-hitbox entity collision optimizations.""")
public static boolean optimizedHardHitBoxEntityCollision = false;
@ConfigInfo(name = "tntFuseDuration", comments = """
Override the default primed TNT fuse duration in ticks.
Accepts values in the range 0..32767.""")
public static int tntFuseDuration = 80;
@ConfigInfo(name = "defaultLoggers", comments = """
Carpet-style default logger subscriptions for players.
Examples: ["tps", "mob_caps", "counter white"]""")
public static List<String> defaultLoggers = List.of();
public static int normalizedTntFuseDuration() {
return Math.clamp(tntFuseDuration, 0, Short.MAX_VALUE);
}
public static int normalizedTickCommandPermission() {
return Math.clamp(tickCommandPermission, 0, 4);
}
}
@@ -81,7 +81,6 @@ public class FakeplayerConfig implements IConfigModule {
@ConfigInfo(name = "enable-locator-bar", comments = """
Enable locator bar for fakeplayers""")
public static boolean enableLocatorBar = false;
public static ServerBot.TickType tickType = ServerBot.TickType.ENTITY_LIST;
private BotCommand command = null;
@@ -0,0 +1,302 @@
package fun.bm.lophine.protocol;
import fun.bm.lophine.carpet.config.modules.GeneralCompatConfig;
import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.threadedregions.RegionizedWorldData;
import io.papermc.paper.threadedregions.TickRegionScheduler;
import net.kyori.adventure.text.Component;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.protocol.game.ClientboundTabListPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.ServerTickRateManager;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.NaturalSpawner;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.util.HopperCounter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@LeavesProtocol.Register(namespace = "carpet")
public class CarpetLoggerProtocol implements LeavesProtocol {
private static final Logger LOGGER = LoggerFactory.getLogger("CarpetLoggerProtocol");
private static final Map<String, Map<String, String>> PLAYER_SUBSCRIPTIONS = new ConcurrentHashMap<>();
private static volatile Map<String, String> configuredSubscriptions = null;
public static void refreshConfiguredDefaults(boolean initial) {
Map<String, String> defaults = parseConfiguredDefaults(GeneralCompatConfig.defaultLoggers);
configuredSubscriptions = defaults;
if (defaults == null || defaults.isEmpty()) {
PLAYER_SUBSCRIPTIONS.clear();
if (initial) return;
for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) {
clearHud(player);
}
return;
}
PLAYER_SUBSCRIPTIONS.replaceAll((playerName, ignored) -> defaults);
}
public static String serializeConfiguredDefaults(List<String> configuredLoggers) {
List<String> serialized = new ArrayList<>();
if (configuredLoggers != null) {
for (String entry : configuredLoggers) {
if (entry == null) {
continue;
}
String trimmed = entry.trim();
if (!trimmed.isEmpty()) {
serialized.add(trimmed);
}
}
}
return serialized.isEmpty() ? "none" : String.join(",", serialized);
}
@ProtocolHandler.PlayerJoin
public static void onPlayerJoin(ServerPlayer player) {
if (configuredSubscriptions != null && !configuredSubscriptions.isEmpty()) {
PLAYER_SUBSCRIPTIONS.putIfAbsent(player.getScoreboardName(), configuredSubscriptions);
}
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLeave(ServerPlayer player) {
clearHud(player);
}
@ProtocolHandler.Ticker(tickerId = "hud")
public static void onHudTick() {
if (PLAYER_SUBSCRIPTIONS.isEmpty()) {
return;
}
MinecraftServer server = MinecraftServer.getServer();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
Map<String, String> subscriptions = PLAYER_SUBSCRIPTIONS.get(player.getScoreboardName());
if (subscriptions == null || subscriptions.isEmpty()) {
continue;
}
player.getBukkitEntity().taskScheduler.schedule((LivingEntity livingEntity) -> {
if (livingEntity instanceof ServerPlayer scheduledPlayer) {
sendHud(server, scheduledPlayer, subscriptions);
}
}, null, 1L);
}
}
@Override
public boolean isActive() {
return true;
}
@Override
public int tickerInterval(String tickerID) {
return "hud".equals(tickerID) ? 20 : 1;
}
private static void sendHud(MinecraftServer server, ServerPlayer player, Map<String, String> subscriptions) {
List<net.minecraft.network.chat.Component> lines = new ArrayList<>();
subscriptions.forEach((loggerName, option) -> {
switch (loggerName) {
case "tps" -> lines.add(buildTpsLine(server));
case "mobcaps" -> {
net.minecraft.network.chat.Component line = buildMobcapsLine(player, option);
if (line != null) {
lines.add(line);
}
}
case "counter" -> lines.addAll(buildCounterLines(server, option));
default -> {
}
}
});
MutableComponent footer = net.minecraft.network.chat.Component.empty();
for (int i = 0; i < lines.size(); i++) {
if (i > 0) {
footer.append(net.minecraft.network.chat.Component.literal("\n"));
}
footer.append(lines.get(i));
}
player.connection.send(new ClientboundTabListPacket(net.minecraft.network.chat.Component.empty(), footer));
}
private static void clearHud(ServerPlayer player) {
player.connection.send(new ClientboundTabListPacket(net.minecraft.network.chat.Component.empty(), net.minecraft.network.chat.Component.empty()));
}
private static net.minecraft.network.chat.Component buildTpsLine(MinecraftServer server) {
ServerTickRateManager tickManager = server.tickRateManager();
ca.spottedleaf.moonrise.common.time.TickData.TickReportData tickData = TickRegionScheduler.getCurrentRegion().getData().getRegionSchedulingHandle().getTickReport5s(System.nanoTime());
final double tps = tickData.tpsData().segmentAll().average();
final double mspt = tickData.timePerTickData().segmentAll().average() / 1.0E6;
ChatFormatting color = heatmapColor(mspt, tickManager.millisecondsPerTick());
return net.minecraft.network.chat.Component.empty()
.append(net.minecraft.network.chat.Component.literal("TPS: ").withStyle(ChatFormatting.GRAY))
.append(net.minecraft.network.chat.Component.literal(String.format(Locale.US, "%.1f", tps)).withStyle(color))
.append(net.minecraft.network.chat.Component.literal(" MSPT: ").withStyle(ChatFormatting.GRAY))
.append(net.minecraft.network.chat.Component.literal(String.format(Locale.US, "%.1f", mspt)).withStyle(color));
}
private static net.minecraft.network.chat.Component buildMobcapsLine(ServerPlayer player, String option) {
final ServerLevel level = resolveMobcapsLevel(player, option);
if (level == null) {
return null;
}
final RegionizedWorldData data = level.getCurrentWorldData();
if (data == null) {
return null;
}
final NaturalSpawner.SpawnState spawnState = data.lastSpawnState;
if (spawnState == null) {
return net.minecraft.network.chat.Component.literal("Mobcaps: unavailable").withStyle(ChatFormatting.DARK_GRAY);
}
int chunks = spawnState.getSpawnableChunkCount();
var counts = spawnState.getMobCategoryCounts();
MutableComponent line = net.minecraft.network.chat.Component.literal("Mobcaps").withStyle(ChatFormatting.GRAY);
for (MobCategory category : MobCategory.values()) {
if (category == MobCategory.MISC) {
continue;
}
int current = counts.getOrDefault(category, 0);
int limit = NaturalSpawner.globalLimitForCategory(level, category, chunks);
line.append(net.minecraft.network.chat.Component.literal(" " + shortName(category) + " ").withStyle(ChatFormatting.DARK_GRAY));
line.append(net.minecraft.network.chat.Component.literal(current + "/" + limit).withStyle(categoryColor(current, limit)));
}
return line;
}
private static List<net.minecraft.network.chat.Component> buildCounterLines(MinecraftServer server, String option) {
List<net.minecraft.network.chat.Component> lines = new ArrayList<>();
String colors = option == null || option.isBlank() ? "white" : option;
for (String rawColor : colors.split(",")) {
String colorName = rawColor.trim();
if (colorName.isEmpty()) {
continue;
}
DyeColor color = DyeColor.byName(colorName, null);
if (color == null) {
continue;
}
HopperCounter counter = HopperCounter.getCounter(color);
if (counter == null) {
continue;
}
for (Component component : counter.format(server, false)) {
lines.add(PaperAdventure.asVanilla(component));
}
}
return lines;
}
private static ServerLevel resolveMobcapsLevel(ServerPlayer player, String option) {
if (option == null || option.isBlank() || option.equalsIgnoreCase("dynamic")) {
return player.level();
}
return switch (option.toLowerCase(Locale.ROOT)) {
case "overworld" -> player.level().dimension() == Level.OVERWORLD ? player.level() : null;
case "nether" -> player.level().dimension() == Level.NETHER ? player.level() : null;
case "end" -> player.level().dimension() == Level.END ? player.level() : null;
default -> player.level();
};
}
private static ChatFormatting heatmapColor(double actual, double reference) {
if (actual > reference) {
return ChatFormatting.LIGHT_PURPLE;
}
if (actual > 0.8D * reference) {
return ChatFormatting.RED;
}
if (actual > 0.5D * reference) {
return ChatFormatting.YELLOW;
}
if (actual >= 0.0D) {
return ChatFormatting.DARK_GREEN;
}
return ChatFormatting.GRAY;
}
private static ChatFormatting categoryColor(int current, int limit) {
if (limit <= 0) {
return ChatFormatting.DARK_GRAY;
}
double ratio = current / (double) limit;
if (ratio >= 1.0D) {
return ChatFormatting.RED;
}
if (ratio >= 0.8D) {
return ChatFormatting.YELLOW;
}
return ChatFormatting.GREEN;
}
private static String shortName(MobCategory category) {
return switch (category) {
case MONSTER -> "M";
case CREATURE -> "C";
case AMBIENT -> "A";
case AXOLOTLS -> "Ax";
case UNDERGROUND_WATER_CREATURE -> "UWC";
case WATER_CREATURE -> "WC";
case WATER_AMBIENT -> "WA";
case MISC -> "X";
};
}
private static Map<String, String> parseConfiguredDefaults(List<String> configuredLoggers) {
LinkedHashMap<String, String> subscriptions = new LinkedHashMap<>();
if (configuredLoggers == null) {
return null;
}
for (String entry : configuredLoggers) {
if (entry == null) {
continue;
}
for (String chunk : entry.split(",")) {
String token = chunk.trim();
if (token.isEmpty() || token.equalsIgnoreCase("none")) {
continue;
}
String[] parts = token.split("\\s+", 2);
String loggerName = parts[0].toLowerCase(Locale.ROOT);
if (!isSupported(loggerName)) {
LOGGER.debug("Ignoring unsupported Carpet default logger '{}'", loggerName);
continue;
}
String option = parts.length == 1 ? defaultOption(loggerName) : parts[1].trim();
if (option.isEmpty()) {
option = defaultOption(loggerName);
}
subscriptions.put(loggerName, option);
}
}
return subscriptions.isEmpty() ? null : Map.copyOf(subscriptions);
}
private static boolean isSupported(String loggerName) {
return Arrays.asList("tps", "mobcaps", "counter").contains(loggerName);
}
private static @NotNull String defaultOption(String loggerName) {
return switch (loggerName) {
case "mobcaps" -> "dynamic";
case "counter" -> "white";
default -> "";
};
}
}
@@ -0,0 +1,201 @@
package fun.bm.lophine.protocol.tiscm;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.carpet.config.modules.GeneralCompatConfig;
import io.papermc.paper.ServerBuildInfo;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.Contract;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.slf4j.Logger;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@LeavesProtocol.Register(namespace = TISCMProtocol.PROTOCOL_ID)
public class TISCMProtocol implements LeavesProtocol {
private static final Logger LOGGER = LogUtils.getLogger();
public static final String PROTOCOL_ID = "tiscm";
private static final String PLATFORM_NAME = "Lophine";
private static final String PLATFORM_VERSION = ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE);
private static final Map<String, EnumSet<S2CPacket>> CLIENT_SUPPORTED_PACKETS = new ConcurrentHashMap<>();
@Contract("_ -> new")
public static Identifier id(String path) {
return Identifier.fromNamespaceAndPath(PROTOCOL_ID, path);
}
public static void broadcastMsptSample(long tickCounter, long nanosecond) {
if (!GeneralCompatConfig.tiscmNetworkProtocol || !GeneralCompatConfig.syncServerMsptMetricsData) {
return;
}
CompoundTag nbt = new CompoundTag();
nbt.putInt("version", 2);
nbt.putLong("millisecond", nanosecond / 1_000_000L);
nbt.putLong("nanosecond", nanosecond);
nbt.putString("type", "tick_server_method");
TISCMPayload payload = new TISCMPayload(S2CPacket.MSPT_METRICS_SAMPLE.id, nbt);
for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) {
if (supports(player, S2CPacket.MSPT_METRICS_SAMPLE)) {
ProtocolUtils.sendPayloadPacket(player, payload);
}
}
}
@ProtocolHandler.PayloadReceiver(payload = TISCMPayload.class)
public static void handlePayload(ServerPlayer player, TISCMPayload payload) {
if (!GeneralCompatConfig.tiscmNetworkProtocol) {
return;
}
Optional<C2SPacket> packetType = C2SPacket.fromId(payload.packetId());
if (packetType.isEmpty()) {
return;
}
switch (packetType.get()) {
case HI -> handleHi(player, payload.nbt());
case SUPPORTED_S2C_PACKETS -> handleSupportedS2CPackets(player, payload.nbt());
default -> {
}
}
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLeave(ServerPlayer player) {
CLIENT_SUPPORTED_PACKETS.remove(player.getStringUUID());
}
private static void handleHi(ServerPlayer player, CompoundTag payload) {
String platformName = payload.getString("platform_name").orElse("Unknown");
String platformVersion = payload.getString("platform_version").orElse("Unknown");
LOGGER.info("Player {} connected with TISCM protocol support ({} @ {})", player.getScoreboardName(), platformName, platformVersion);
send(player, S2CPacket.HELLO, nbt -> {
nbt.putString("platform_name", PLATFORM_NAME);
nbt.putString("platform_version", PLATFORM_VERSION);
});
send(player, S2CPacket.SUPPORTED_C2S_PACKETS, nbt -> nbt.put("supported_c2s_packets", stringList(List.of(
C2SPacket.HI.id,
C2SPacket.SUPPORTED_S2C_PACKETS.id
))));
}
private static void handleSupportedS2CPackets(ServerPlayer player, CompoundTag payload) {
EnumSet<S2CPacket> packets = EnumSet.noneOf(S2CPacket.class);
ListTag listTag = payload.getListOrEmpty("supported_s2c_packets");
for (int i = 0; i < listTag.size(); i++) {
S2CPacket.fromId(listTag.getString(i).orElse("")).ifPresent(packets::add);
}
CLIENT_SUPPORTED_PACKETS.put(player.getStringUUID(), packets);
}
private static void send(ServerPlayer player, S2CPacket packet, PayloadBuilder builder) {
if (!supports(player, packet)) {
return;
}
CompoundTag nbt = new CompoundTag();
builder.accept(nbt);
ProtocolUtils.sendPayloadPacket(player, new TISCMPayload(packet.id, nbt));
}
private static boolean supports(ServerPlayer player, S2CPacket packet) {
if (packet.handshake) {
return true;
}
EnumSet<S2CPacket> packets = CLIENT_SUPPORTED_PACKETS.get(player.getStringUUID());
return packets != null && packets.contains(packet);
}
private static ListTag stringList(List<String> values) {
ListTag list = new ListTag();
for (String value : values) {
list.add(StringTag.valueOf(value));
}
return list;
}
@Override
public boolean isActive() {
return GeneralCompatConfig.tiscmNetworkProtocol;
}
private interface PayloadBuilder {
void accept(CompoundTag nbt);
}
public enum C2SPacket {
HI("hi", true),
SUPPORTED_S2C_PACKETS("supported_s2c_packets", true),
SPEED_TEST_UPLOAD_PAYLOAD("speed_test_upload_payload", false),
SPEED_TEST_PING("speed_test_ping", false);
private static final Map<String, C2SPacket> BY_ID = Arrays.stream(values()).collect(Collectors.toMap(packet -> packet.id, packet -> packet));
private final String id;
private final boolean handshake;
C2SPacket(String id, boolean handshake) {
this.id = id;
this.handshake = handshake;
}
public static Optional<C2SPacket> fromId(String id) {
return Optional.ofNullable(BY_ID.get(id));
}
}
public enum S2CPacket {
HELLO("hello", true),
SUPPORTED_C2S_PACKETS("supported_c2s_packets", true),
MSPT_METRICS_SAMPLE("mspt_metrics_sample", false),
SPEED_TEST_DOWNLOAD_PAYLOAD("speed_test_download_payload", false),
SPEED_TEST_UPLOAD_REQUEST("speed_test_upload_request", false),
SPEED_TEST_PING("speed_test_ping", false),
SPEED_TEST_ABORT("speed_test_abort", false);
private static final Map<String, S2CPacket> BY_ID = Arrays.stream(values()).collect(Collectors.toMap(packet -> packet.id, packet -> packet));
private final String id;
private final boolean handshake;
S2CPacket(String id, boolean handshake) {
this.id = id;
this.handshake = handshake;
}
public static Optional<S2CPacket> fromId(String id) {
return Optional.ofNullable(BY_ID.get(id));
}
}
public record TISCMPayload(String packetId, CompoundTag nbt) implements LeavesCustomPayload {
@ID
private static final Identifier NETWORK_ID = TISCMProtocol.id("network/v1");
@Codec
private static final StreamCodec<RegistryFriendlyByteBuf, TISCMPayload> CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
TISCMPayload::packetId,
ByteBufCodecs.COMPOUND_TAG,
TISCMPayload::nbt,
TISCMPayload::new
);
}
}
@@ -18,11 +18,15 @@
package org.leavesmc.leaves.bot;
import com.google.common.base.Charsets;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import fun.bm.lophine.config.modules.function.FakeplayerConfig;
import net.minecraft.core.NonNullList;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.ItemContainerContents;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
@@ -34,23 +38,66 @@ public class BotUtil {
public static void replenishment(@NotNull ItemStack itemStack, NonNullList<ItemStack> itemStackList) {
int count = itemStack.getMaxStackSize() / 2;
if (itemStack.getCount() <= 8 && count > 8) {
for (ItemStack itemStack1 : itemStackList) {
if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) {
if (pullMatchingStack(itemStack, itemStackList, count)) {
return;
}
if (FakePlayerCompatConfig.fakePlayerAutoReplenishmentFormShulkerBox) {
pullMatchingStackFromShulkerBox(itemStack, itemStackList, count);
}
}
}
private static boolean pullMatchingStack(@NotNull ItemStack targetStack, NonNullList<ItemStack> itemStackList, int transferLimit) {
for (ItemStack inventoryStack : itemStackList) {
if (inventoryStack == ItemStack.EMPTY || inventoryStack == targetStack) {
continue;
}
if (ItemStack.isSameItemSameComponents(inventoryStack, targetStack)) {
if (inventoryStack.getCount() > transferLimit) {
targetStack.setCount(targetStack.getCount() + transferLimit);
inventoryStack.setCount(inventoryStack.getCount() - transferLimit);
} else {
targetStack.setCount(targetStack.getCount() + inventoryStack.getCount());
inventoryStack.setCount(0);
}
return true;
}
}
return false;
}
private static boolean pullMatchingStackFromShulkerBox(@NotNull ItemStack targetStack, NonNullList<ItemStack> itemStackList, int transferLimit) {
for (ItemStack containerStack : itemStackList) {
if (containerStack == ItemStack.EMPTY || !containerStack.is(ItemTags.SHULKER_BOXES)) {
continue;
}
ItemContainerContents contents = containerStack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY);
NonNullList<ItemStack> shulkerItems = NonNullList.withSize(contents.items.size(), ItemStack.EMPTY);
contents.copyInto(shulkerItems);
for (int slot = 0; slot < shulkerItems.size(); slot++) {
ItemStack shulkerStack = shulkerItems.get(slot);
if (shulkerStack.isEmpty() || !ItemStack.isSameItemSameComponents(shulkerStack, targetStack)) {
continue;
}
if (ItemStack.isSameItemSameComponents(itemStack1, itemStack)) {
if (itemStack1.getCount() > count) {
itemStack.setCount(itemStack.getCount() + count);
itemStack1.setCount(itemStack1.getCount() - count);
} else {
itemStack.setCount(itemStack.getCount() + itemStack1.getCount());
itemStack1.setCount(0);
}
break;
int moved = Math.min(Math.min(transferLimit, shulkerStack.getCount()), targetStack.getMaxStackSize() - targetStack.getCount());
if (moved <= 0) {
return false;
}
targetStack.grow(moved);
shulkerStack.shrink(moved);
shulkerItems.set(slot, shulkerStack.isEmpty() ? ItemStack.EMPTY : shulkerStack);
containerStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(shulkerItems));
return true;
}
}
return false;
}
public static void replaceTool(@NotNull EquipmentSlot slot, @NotNull ServerBot bot) {
@@ -20,6 +20,7 @@ package org.leavesmc.leaves.bot;
import com.google.common.collect.ImmutableMap;
import com.mojang.authlib.GameProfile;
import fun.bm.lophine.LophineLogger;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import fun.bm.lophine.config.modules.function.FakeplayerConfig;
import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.event.entity.EntityKnockbackEvent;
@@ -52,9 +53,11 @@ import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Input;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.FishingHook;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.item.FishingRodItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.level.gameevent.GameEvent;
@@ -84,6 +87,8 @@ import java.util.function.Predicate;
import static net.minecraft.server.MinecraftServer.getServer;
public class ServerBot extends ServerPlayer {
private static final int AUTO_FISH_RECAST_DELAY = 4;
private static final int AUTO_FISH_ENTITY_DELAY = 20;
private final List<AbstractBotAction<?>> actions;
private final Map<String, AbstractBotConfig<?, ?>> configs;
@@ -100,6 +105,7 @@ public class ServerBot extends ServerPlayer {
public int notSleepTicks;
public int removeTaskId = -1;
private int autoFishCooldown = 0;
public ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) {
super(server, world, profile, ClientInformation.createDefault());
@@ -237,6 +243,7 @@ public class ServerBot extends ServerPlayer {
}
this.getCooldowns().tick();
this.tickAutoFish();
this.updatePlayerPose();
}
@@ -339,8 +346,10 @@ public class ServerBot extends ServerPlayer {
ItemStack item = this.getItemInHand(hand);
if (!item.isEmpty()) {
BotUtil.replenishment(item, getInventory().getNonEquipmentItems());
if (BotUtil.isDamage(item, 10)) {
if (FakePlayerCompatConfig.fakePlayerAutoReplenishment) {
BotUtil.replenishment(item, getInventory().getNonEquipmentItems());
}
if (FakePlayerCompatConfig.fakePlayerAutoReplaceTool && BotUtil.isDamage(item, 10)) {
BotUtil.replaceTool(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND, this);
}
}
@@ -388,7 +397,7 @@ public class ServerBot extends ServerPlayer {
nbt.store("createStatus", CompoundTag.CODEC, createNbt);
if (!this.actions.isEmpty()) {
if (FakePlayerCompatConfig.fakePlayerReloadAction && !this.actions.isEmpty()) {
ValueOutput.TypedOutputList<CompoundTag> actionNbt = nbt.list("actions", CompoundTag.CODEC);
for (AbstractBotAction<?> action : this.actions) {
actionNbt.add(action.save(new CompoundTag()));
@@ -431,7 +440,7 @@ public class ServerBot extends ServerPlayer {
this.gameProfile = BotList.createBotProfile(this.getUUID(), this.createState.fullName(), this.createState.skin());
if (nbt.list("actions", CompoundTag.CODEC).isPresent()) {
if (FakePlayerCompatConfig.fakePlayerReloadAction && nbt.list("actions", CompoundTag.CODEC).isPresent()) {
ValueInput.TypedInputList<CompoundTag> actionNbt = nbt.list("actions", CompoundTag.CODEC).orElseThrow();
actionNbt.forEach(actionTag -> {
AbstractBotAction<?> action = Actions.getForName(actionTag.getString("actionName").orElseThrow());
@@ -656,6 +665,45 @@ public class ServerBot extends ServerPlayer {
}
}
private void tickAutoFish() {
if (!fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig.fakePlayerAutoFish || this.hasActiveAction("fish")) {
this.autoFishCooldown = 0;
return;
}
if (this.autoFishCooldown > 0) {
this.autoFishCooldown--;
return;
}
ItemStack mainHand = this.getMainHandItem();
if (mainHand.isEmpty() || !(mainHand.getItem() instanceof FishingRodItem)) {
return;
}
FishingHook fishingHook = this.fishing;
if (fishingHook != null) {
if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) {
mainHand.use(this.level(), this, InteractionHand.MAIN_HAND);
this.autoFishCooldown = AUTO_FISH_ENTITY_DELAY;
return;
}
if (fishingHook.nibble > 0) {
mainHand.use(this.level(), this, InteractionHand.MAIN_HAND);
this.autoFishCooldown = AUTO_FISH_RECAST_DELAY;
}
return;
}
mainHand.use(this.level(), this, InteractionHand.MAIN_HAND);
this.autoFishCooldown = AUTO_FISH_RECAST_DELAY;
}
private boolean hasActiveAction(String actionName) {
return this.actions.stream().anyMatch(action -> !action.isCancelled() && action.getName().equals(actionName));
}
public boolean addBotAction(AbstractBotAction<?> action, CommandSender sender) {
if (!FakeplayerConfig.canUseAction) {
return false;
@@ -17,8 +17,10 @@
package org.leavesmc.leaves.bot;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import net.kyori.adventure.text.Component;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.world.InteractionHand;
@@ -40,7 +42,7 @@ public class ServerBotGameMode extends ServerPlayerGameMode {
public ServerBotGameMode(ServerBot bot) {
super(bot);
super.setGameModeForPlayer(GameType.SURVIVAL, null);
super.setGameModeForPlayer(getInitialGameMode(bot), null);
}
@Override
@@ -133,4 +135,8 @@ public class ServerBotGameMode extends ServerPlayerGameMode {
return InteractionResult.PASS;
}
}
private static GameType getInitialGameMode(ServerBot bot) {
return FakePlayerCompatConfig.fakePlayerDefaultSurvivalMode ? GameType.SURVIVAL : MinecraftServer.getServer().getDefaultGameType();
}
}
@@ -17,9 +17,12 @@
package org.leavesmc.leaves.bot.agent.actions;
import fun.bm.lophine.carpet.config.modules.FakePlayerCompatConfig;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.item.Items;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
@@ -51,6 +54,13 @@ public class ServerUseItemToAction extends AbstractUseBotAction<ServerUseItemToA
Vec3 vec3 = hitResult.getLocation().subtract(entity.getX(), entity.getY(), entity.getZ());
bot.updateItemInHand(hand);
InteractionResult interactionResult = entity.interactAt(bot, vec3, hand);
if (FakePlayerCompatConfig.fakePlayerInteractLikeClient
&& entity instanceof ArmorStand stand
&& !stand.isMarker()
&& !bot.isSpectator()
&& !bot.getItemInHand(hand).is(Items.NAME_TAG)) {
interactionResult = InteractionResult.PASS;
}
if (!interactionResult.consumesAction()) {
interactionResult = bot.interactOn(hitResult.getEntity(), hand);
}
@@ -1,7 +1,6 @@
package org.leavesmc.leaves.protocol;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
@@ -54,8 +53,8 @@ public class CarpetServerProtocol implements LeavesProtocol {
@Override
public boolean isActive() {
return CreativeFlyNoClipConfig.enabled;
} // re-edit for creative no clip
return CarpetRules.hasRules();
}
public static class CarpetRules {
@@ -71,6 +70,14 @@ public class CarpetServerProtocol implements LeavesProtocol {
public static void register(CarpetRule rule) {
rules.put(rule.name, rule);
}
public static void clear() {
rules.clear();
}
public static boolean hasRules() {
return !rules.isEmpty();
}
}
public record CarpetRule(String identifier, String name, String value) {
@@ -87,6 +94,24 @@ public class CarpetServerProtocol implements LeavesProtocol {
return new CarpetRule(identifier, name, Boolean.toString(value));
}
@NotNull
@Contract("_, _, _ -> new")
public static CarpetRule of(String identifier, String name, int value) {
return new CarpetRule(identifier, name, Integer.toString(value));
}
@NotNull
@Contract("_, _, _ -> new")
public static CarpetRule of(String identifier, String name, long value) {
return new CarpetRule(identifier, name, Long.toString(value));
}
@NotNull
@Contract("_, _, _ -> new")
public static CarpetRule of(String identifier, String name, String value) {
return new CarpetRule(identifier, name, value);
}
public void writeNBT(@NotNull CompoundTag rules) {
CompoundTag rule = new CompoundTag();
String key = name;
@@ -84,6 +84,7 @@ public class ServuxHudDataProtocol implements LeavesProtocol {
private static void onPlayerLeave(ServerPlayer player) {
players.remove(player);
loggerPlayers.remove(player);
DATA.rowMap().values().forEach(row -> row.remove(player));
}
@ProtocolHandler.PayloadReceiver(payload = HudDataPayload.class)