diff --git a/docs/carpet-compat-status.md b/docs/carpet-compat-status.md new file mode 100644 index 0000000..4905fbf --- /dev/null +++ b/docs/carpet-compat-status.md @@ -0,0 +1,89 @@ +### Carpet 原版 + +| 规则 | 状态 | 说明 | +| --- | --- | --- | +| `language` | 已映射 | 转发到 `lophine.function.language.lang` | +| `commandTick` | 已映射 | 转发到现有的 tick 指令补丁 | +| `creativeNoClip` | 已映射 | 转发到 Lophine 的创造飞行穿墙实现 | +| `ctrlQCraftingFix` | 结构等价 | 当前的合成与切石机菜单已经自带修复后的 Ctrl+Q 结果槽丢弃行为 | +| `placementRotationFix` | 已移植 | 放置朝向判断可使用玩家身体朝向而不是头部旋转 | +| `tntDoNotUpdate` | 已移植 | 新放置的 TNT 不会再因放置时更新而自动点燃 | +| `explosionNoBlockDamage` | 已移植 | 爆炸仍会伤害实体,但不会破坏方块 | +| `interactionUpdates` | 已移植 | 玩家交互和破坏方块时可以在不触发邻居更新与形状更新的情况下运行 | +| `xpNoCooldown` | 已移植 | 经验球可以在同一 tick 内被连续吸收,不再有拾取冷却 | +| `viewDistance` | 已移植 | 专用服务器启动视距会被兼容配置覆盖 | +| `fastRedstoneDust` | 已移植 | 规则启用时,红石粉更新会统一走 Alternate Current 快速更新后端 | +| `lagFreeSpawning` | 已移植 | 自然生成会使用轻量碰撞检查与预构造实体路径 | +| `defaultLoggers` | 已移植 | 按 Carpet 默认订阅语义为玩家启用 HUD logger,当前支持 `tps`、`mobcaps`、`counter` | +| `commandPlayer` | 已映射 | 当前由同一套 `/bot` 假人能力承接 | +| `hopperCounters` | 已映射 | 转发到 Lophine 的羊毛漏斗计数器 | + +### Carpet TIS Addition + +| 规则 | 状态 | 说明 | +| --- | --- | --- | +| `yeetUpdateSuppressionCrash` | 已映射 | 转发到同一套 Lophine 崩溃修复逻辑 | +| `dustTrapdoorReintroduced` | 已映射 | 转发到 `lophine.experiment.redstone.redstone-ignore-upwards-update` | +| `shulkerBoxCCEReintroduced` | 已映射 | 转发到 `lophine.experiment.redstone.cce-update-suppression` | +| `instantBlockUpdaterReintroduced` | 已映射 | 转发到 `lophine.experiment.redstone.instant-block-updater` | +| `optimizedDragonRespawn` | 已映射 | 转发到 Luminol 的末影龙重生优化 | +| `antiSpamDisabled` | 已移植 | 在 `ServerGamePacketListenerImpl` 中关闭聊天和创造丢物防刷屏限制 | +| `blockPlacementIgnoreEntity` | 已移植 | 创造模式放置方块时忽略实体碰撞检查 | +| `creativeOpenContainerForcibly` | 已移植 | 创造玩家可以强制打开被阻挡的箱子、末影箱和潜影盒 | +| `observerNoDetection` | 已移植 | 观察者的检测触发会在发出脉冲前被取消 | +| `creativeNoItemCooldown` | 已移植 | 创造玩家不会应用物品冷却 | +| `totallyNoBlockUpdate` | 已移植 | 邻居更新和形状更新会在 `NeighborUpdater` 中被统一短路 | +| `tiscmNetworkProtocol` | 已移植 | 原生 `tiscm:network/v1` 握手与数据包协商已经通过专用 Leaves 协议接入 | +| `optimizedTNTHighPriority` | 结构等价 | 当前 `ServerExplosion` 路径已经运行在优化过的服务端爆炸实现上 | +| `tntIgnoreRedstoneSignal` | 已移植 | TNT 在自动点燃判断时忽略红石信号 | +| `tntDupingFix` | 已移植 | 活塞复制 TNT 的路径现在由 `PistonBaseBlock` 中的兼容规则直接控制 | +| `clientSettingsLostOnRespawnFix` | 已移植 | 玩家上一次的客户端设置会在 `ServerPlayer.restoreFrom` 中恢复 | +| `entityInstantDeathRemoval` | 已移植 | 已死亡生物不会再保留原版的 20gt 延迟,而是立即移除 | +| `farmlandTrampledDisabled` | 已移植 | 耕地不会再因实体踩踏而变回泥土 | +| `yeetOutOfOrderChatKick` | 已移植 | 乱序的签名聊天不会再破坏安全聊天链 | +| `tickCommandPermission` | 已移植 | `/tick` 的权限等级可通过兼容配置调整 | +| `tickFreezeCommandToggleable` | 已移植 | 已冻结时再次执行 `/tick freeze` 会切换回运行状态 | +| `syncServerMsptMetricsData` | 已移植 | 实时 MSPT 样本会通过原生 TISCM 协议通道广播 | +| `microTiming` | 结构等价 | 当前 Folia 的区块级性能分析与计时钩子已经提供对应的服务端观测能力 | +| `optimizedFastEntityMovement` | 结构等价 | Moonrise/Paper 的快速实体移动碰撞优化已经是基础运行时的一部分 | +| `optimizedHardHitBoxEntityCollision` | 结构等价 | Moonrise/Paper 的硬碰撞箱实体碰撞优化已经是基础运行时的一部分 | +| `tntFuseDuration` | 已移植 | 可配置的 TNT 引信时长已接入 NMS TNT 逻辑 | +| `fakePlayerTicksLikeRealPlayer` | 已映射 | 转发到 Leaves 的假人网络阶段 tick 行为 | +| `hopperCountersUnlimitedSpeed` | 已映射 | 转发到不限速计数器模式 | + +### Carpet Org Addition + +| 规则 | 状态 | 说明 | +| --- | --- | --- | +| `hopperNoItemCost` | 已移植 | 漏斗上方放置羊毛时,可在传输后恢复被推出的物品堆,实现零消耗供给线 | +| `simpleInGameCalculator` | 已移植 | 以 `=` 开头的聊天内容会作为简单计算表达式私聊回复 | + +### Carpet AMS Addition + +| 规则 | 状态 | 说明 | +| --- | --- | --- | +| `amsUpdateSuppressionCrashFix` | 已映射 | 转发到 Lophine 现有的更新抑制崩溃修复 | +| `creativeOneHitKill` | 已移植 | 创造玩家可通过 `Player.attack` 瞬间击杀附近符合条件的目标 | +| `bambooModelNoOffset` | 已移植 | 竹子和竹笋始终返回零模型偏移 | +| `carpetAlwaysSetDefault` | 结构等价 | 兼容配置默认值已经会在 `ConfigsInstance.preLoadConfig` 中被写入 | +| `powerfulExpMending` | 已移植 | 经验拾取会修复玩家整个背包内所有受损的经验修补物品 | +| `sensibleEnderman` | 已移植 | 末影人仅会在规则开启时拾取南瓜和西瓜 | +| `shulkerGolem` | 已移植 | 潜影盒上方放置雕刻南瓜时可以召唤潜影贝 | +| `preventEndSpikeRespawn` | 已移植 | 末影龙重生时会跳过黑曜石柱重建和柱顶水晶重建 | +| `betterCraftableBoneBlock` | 已移植 | 向配方管理器注入 AMS 的替代骨块配方 | +| `betterCraftableDispenser` | 已移植 | 向配方管理器注入 AMS 的替代发射器配方 | +| `fakePlayerDefaultSurvivalMode` | 已移植 | 新创建的假人可以被强制设为生存模式,而不是沿用服务器默认游戏模式 | +| `fakePlayerInteractLikeClient` | 已移植 | 假人与盔甲架交互时会先按真实客户端方式回退,再进入普通实体交互 | +| `fakePlayerAutoReplenishmentFormShulkerBox` | 已移植 | 假人自动补货现在可以从背包里的潜影盒中抽取匹配物品 | + +### Lophine 扩展承接 + +| 规则 | 状态 | 说明 | +| --- | --- |----------------------------------| +| `commandBot` | 已映射 | 启用 Lophine `/bot` | +| `fakePlayerResident` | 已映射 | 转发到 Lophine 的假人常驻模式 | +| `openFakePlayerInventory` | 已映射 | 转发到 Lophine 的假人背包打开功能 | +| `fakePlayerAutoReplaceTool` | 已映射 | 为现有自动换工具逻辑增加了新开关 | +| `fakePlayerAutoReplenishment` | 已映射 | 为现有自动补货逻辑增加了新开关 | +| `fakePlayerReloadAction` | 已映射 | 为假人动作持久化增加了新开关 | +| `fakePlayerAutoFish` | 已移植 | 手持鱼竿的假人会自动抛竿和收杆,除非显式执行 `fish` 动作 | diff --git a/lophine-server/luminol-patches/features/0006-Carpet-features.patch b/lophine-server/luminol-patches/features/0006-Carpet-features.patch new file mode 100644 index 0000000..405b42a --- /dev/null +++ b/lophine-server/luminol-patches/features/0006-Carpet-features.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +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(); + } + diff --git a/lophine-server/minecraft-patches/features/0044-Carpet-features.patch b/lophine-server/minecraft-patches/features/0044-Carpet-features.patch new file mode 100644 index 0000000..bae4bc7 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0044-Carpet-features.patch @@ -0,0 +1,1155 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Sun, 22 Mar 2026 01:39:15 +0800 +Subject: [PATCH] Carpet features + + +diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java +index 8d32d906f87a1cb866a7497ee0de3045e9adce2c..8dbf7a1f448277cc2a40dec8d9627a8571f2b5a3 100644 +--- a/io/papermc/paper/threadedregions/RegionizedServer.java ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -331,6 +331,7 @@ public final class RegionizedServer { + } + this.randomWalk(); + */ ++ final long tickStartNanos = System.nanoTime(); + ++this.tickCount; + // Luminol start - Add a config to enable tick command + if (fun.bm.lophine.config.modules.experiment.CommandConfig.tick) { +@@ -369,6 +370,8 @@ public final class RegionizedServer { + } + // Luminol end - Add a config to enable tick command + ++ fun.bm.lophine.protocol.tiscm.TISCMProtocol.broadcastMsptSample(this.tickCount, System.nanoTime() - tickStartNanos); ++ + org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleTick(); // Leaves - protocol + + // tick connections +@@ -526,4 +529,4 @@ public final class RegionizedServer { + public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { + + } +-} +\ No newline at end of file ++} +diff --git a/net/minecraft/commands/arguments/blocks/BlockInput.java b/net/minecraft/commands/arguments/blocks/BlockInput.java +index f9a5058e50c7fc6ebc2fc311bbc7064bfbd89035..9b737c3fcfae1f6afeee42cd4a4d83ba56f7c3e9 100644 +--- a/net/minecraft/commands/arguments/blocks/BlockInput.java ++++ b/net/minecraft/commands/arguments/blocks/BlockInput.java +@@ -65,7 +65,9 @@ public class BlockInput implements Predicate { + } + + public boolean place(ServerLevel level, BlockPos pos, @Block.UpdateFlags int flags) { +- BlockState blockState = (flags & Block.UPDATE_KNOWN_SHAPE) != 0 ? this.state : Block.updateFromNeighbourShapes(this.state, level, pos); ++ BlockState blockState = (flags & Block.UPDATE_KNOWN_SHAPE) != 0 || fun.bm.lophine.carpet.InteractionUpdateCompatHelper.shouldSkipUpdates() ++ ? this.state ++ : Block.updateFromNeighbourShapes(this.state, level, pos); + if (blockState.isAir()) { + blockState = this.state; + } +diff --git a/net/minecraft/core/Direction.java b/net/minecraft/core/Direction.java +index b21ce33550e2516562607b847e5f0f89c2ea3fc7..e6433818288b3b41742c524dc5bef9ad4e1817b7 100644 +--- a/net/minecraft/core/Direction.java ++++ b/net/minecraft/core/Direction.java +@@ -138,7 +138,7 @@ public enum Direction implements StringRepresentable, ca.spottedleaf.moonrise.pa + + public static Direction[] orderedByNearest(Entity entity) { + float f = entity.getViewXRot(1.0F) * (float) (Math.PI / 180.0); +- float f1 = -entity.getViewYRot(1.0F) * (float) (Math.PI / 180.0); ++ float f1 = -(fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.placementRotationFix ? entity.getYRot(1.0F) : entity.getViewYRot(1.0F)) * (float) (Math.PI / 180.0); + float sin = Mth.sin(f); + float cos = Mth.cos(f); + float sin1 = Mth.sin(f1); +diff --git a/net/minecraft/network/chat/SignedMessageValidator.java b/net/minecraft/network/chat/SignedMessageValidator.java +index bf1ea9ea011b3067e329abf77bd6cf4ef991a02a..d971d67b2911a5763f9b1f690bc4991c3a6186a2 100644 +--- a/net/minecraft/network/chat/SignedMessageValidator.java ++++ b/net/minecraft/network/chat/SignedMessageValidator.java +@@ -31,7 +31,9 @@ public interface SignedMessageValidator { + private boolean validateChain(PlayerChatMessage message) { + if (message.equals(this.lastMessage)) { + return true; +- } else if (this.lastMessage != null && !message.link().isDescendantOf(this.lastMessage.link())) { ++ } else if (this.lastMessage != null ++ && !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.yeetOutOfOrderChatKick ++ && !message.link().isDescendantOf(this.lastMessage.link())) { + LOGGER.error( + "Received out-of-order chat message from {}: expected index > {} for session {}, but was {} for session {}", + message.sender(), +diff --git a/net/minecraft/server/commands/TickCommand.java b/net/minecraft/server/commands/TickCommand.java +index 99096896a4512eed579b234e7fa0c6e9c3e8c246..daddf1892ba250e08da295c7608e8f7c40841434 100644 +--- a/net/minecraft/server/commands/TickCommand.java ++++ b/net/minecraft/server/commands/TickCommand.java +@@ -20,7 +20,7 @@ public class TickCommand { + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + Commands.literal("tick") +- .requires(Commands.hasPermission(Commands.LEVEL_ADMINS)) ++ .requires(Commands.hasPermission(tickCommandPermissionCheck())) + .then(Commands.literal("query").executes(commandContext -> tickQuery(commandContext.getSource()))) + .then( + Commands.literal("rate") +@@ -116,6 +116,10 @@ public class TickCommand { + + private static int setFreeze(CommandSourceStack source, boolean frozen) { + ServerTickRateManager serverTickRateManager = source.getServer().tickRateManager(); ++ if (frozen && fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tickFreezeCommandToggleable && serverTickRateManager.isFrozen()) { ++ frozen = false; ++ } ++ + if (frozen) { + if (serverTickRateManager.isSprinting()) { + serverTickRateManager.stopSprinting(); +@@ -171,4 +175,8 @@ public class TickCommand { + return 0; + } + } ++ ++ private static net.minecraft.server.permissions.PermissionCheck tickCommandPermissionCheck() { ++ return new net.minecraft.server.permissions.PermissionCheck.Require(new net.minecraft.server.permissions.Permission.HasCommandLevel(net.minecraft.server.permissions.PermissionLevel.byId(fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.normalizedTickCommandPermission()))); ++ } + } +diff --git a/net/minecraft/server/dedicated/DedicatedPlayerList.java b/net/minecraft/server/dedicated/DedicatedPlayerList.java +index f7f4060370e896316acf5d50dba09c2aeece8bd8..aed0c60d4235776eb6495666693bba6b83defda4 100644 +--- a/net/minecraft/server/dedicated/DedicatedPlayerList.java ++++ b/net/minecraft/server/dedicated/DedicatedPlayerList.java +@@ -14,7 +14,7 @@ public class DedicatedPlayerList extends PlayerList { + + public DedicatedPlayerList(DedicatedServer server, LayeredRegistryAccess registries, PlayerDataStorage playerIo) { + super(server, registries, playerIo, server.notificationManager()); +- this.setViewDistance(server.viewDistance()); ++ this.setViewDistance(Math.max(2, fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.viewDistance)); + this.setSimulationDistance(server.simulationDistance()); + // Paper start - fix converting txt to json file; moved from constructor + } +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index e6ec0788b88d3431c06134c879b4bae9c769dac1..bc94a9094e3cee5d5d4c5b919bf4c2a1d9a73ea4 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -255,6 +255,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + private int requestedViewDistance = 2; + public String language = null; // Paper - default to null + public java.util.Locale adventure$locale = java.util.Locale.US; // Paper ++ private @Nullable ClientInformation lastKnownClientInformation; + private @Nullable Vec3 startingToFallPosition; + private @Nullable Vec3 enteredNetherPosition; + private @Nullable Vec3 enteredLavaOnVehiclePosition; +@@ -2768,6 +2769,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.setShoulderEntityRight(that.getShoulderEntityRight()); + this.setLastDeathLocation(that.getLastDeathLocation()); + this.waypointIcon().copyFrom(that.waypointIcon()); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.clientSettingsLostOnRespawnFix && that.lastKnownClientInformation != null) { ++ this.updateOptions(that.lastKnownClientInformation); ++ } + } + + private void transferInventoryXpAndScore(Player player) { +@@ -3027,6 +3031,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.particleStatus = clientInformation.particleStatus(); + this.getEntityData().set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)clientInformation.modelCustomisation()); + this.getEntityData().set(DATA_PLAYER_MAIN_HAND, clientInformation.mainHand()); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.clientSettingsLostOnRespawnFix) { ++ this.lastKnownClientInformation = clientInformation; ++ } + } + + public ClientInformation clientInformation() { +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 4a079d3033908f3b732c70eddab3c8e86d2c3e78..4d95e36855aecc8be0133c1d6f7d6a58bf26b0a0 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2050,7 +2050,13 @@ public class ServerGamePacketListenerImpl + this.player.gameMode.capturedBlockEntity = false; + this.player.gameMode.captureSentBlockEntities = true; + // Paper end - Send block entities after destroy prediction +- this.player.gameMode.handleBlockBreakAction(pos, action, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence()); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.interactionUpdates) { ++ this.player.gameMode.handleBlockBreakAction(pos, action, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence()); ++ } else { ++ fun.bm.lophine.carpet.InteractionUpdateCompatHelper.runWithSuppressedUpdates( ++ () -> this.player.gameMode.handleBlockBreakAction(pos, action, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence()) ++ ); ++ } + this.ackBlockChangesUpTo(packet.getSequence()); + // Paper start - Send block entities after destroy prediction + this.player.gameMode.captureSentBlockEntities = false; +@@ -2145,7 +2151,9 @@ public class ServerGamePacketListenerImpl + this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706 + } + // Luminol end +- InteractionResult interactionResult = this.player.gameMode.useItemOn(this.player, serverLevel, itemInHand, hand, hitResult); ++ InteractionResult interactionResult = this.runInteractionWithConfiguredUpdates( ++ () -> this.player.gameMode.useItemOn(this.player, serverLevel, itemInHand, hand, hitResult) ++ ); + if (interactionResult.consumesAction()) { + CriteriaTriggers.ANY_BLOCK_USE.trigger(this.player, hitResult.getBlockPos(), itemInHand.copy()); + } +@@ -2251,7 +2259,8 @@ public class ServerGamePacketListenerImpl + } + // CraftBukkit end + +- if (this.player.gameMode.useItem(this.player, serverLevel, itemInHand, hand) instanceof InteractionResult.Success success ++ ItemStack itemInHandToUse = itemInHand; ++ if (this.runInteractionWithConfiguredUpdates(() -> this.player.gameMode.useItem(this.player, serverLevel, itemInHandToUse, hand)) instanceof InteractionResult.Success success + && success.swingSource() == InteractionResult.SwingSource.SERVER) { + this.player.swing(hand, true); + } +@@ -2259,6 +2268,14 @@ public class ServerGamePacketListenerImpl + } + } + ++ private T runInteractionWithConfiguredUpdates(java.util.function.Supplier action) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.interactionUpdates) { ++ return action.get(); ++ } ++ ++ return fun.bm.lophine.carpet.InteractionUpdateCompatHelper.supplyWithSuppressedUpdates(action); ++ } ++ + @Override + public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); +@@ -2648,6 +2665,8 @@ public class ServerGamePacketListenerImpl + if (true) throw new UnsupportedOperationException(); // Folia - region threading + final String conversationInput = rawMessage; + this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput)); ++ } else if (fun.bm.lophine.carpet.CarpetCalculatorCompatHelper.handleChat(this.player, rawMessage)) { ++ return; + } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check + this.send(new ClientboundSystemChatPacket(Component.translatable("chat.cannotSend").withStyle(ChatFormatting.RED), false)); + } else { +@@ -2660,6 +2679,9 @@ public class ServerGamePacketListenerImpl + + // Spigot start - spam exclusions + private void detectRateSpam(String message) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.antiSpamDisabled) { ++ return; ++ } + if (me.earthme.luminol.config.modules.misc.PaperPacketLimiterConfig.forceDisable) return; // Leaves - disable + // CraftBukkit start - replaced with thread safe throttle + if (org.spigotmc.SpigotConfig.enableSpamExclusions) { +@@ -3583,8 +3605,10 @@ public class ServerGamePacketListenerImpl + if (org.leavesmc.leaves.util.ItemOverstackUtils.hasOverstackingItem()) this.player.containerMenu.sendSingleSlot(packet.slotNum(), itemStack); // Leaves - item over-stack util - force send carried item + if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes. + } else if (flag && flag2) { +- if (this.dropSpamThrottler.isUnderThreshold()) { +- this.dropSpamThrottler.increment(); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.antiSpamDisabled || this.dropSpamThrottler.isUnderThreshold()) { ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.antiSpamDisabled) { ++ this.dropSpamThrottler.increment(); ++ } + this.player.drop(itemStack, true); + } else { + LOGGER.warn("Player {} was dropping items too fast in creative mode, ignoring.", this.player.getPlainTextName()); +diff --git a/net/minecraft/world/entity/ExperienceOrb.java b/net/minecraft/world/entity/ExperienceOrb.java +index 29d904b02a2c7b3d25261ca26c7c0479b00999ef..34dc71daf2182805465742b1455e13b904f076e9 100644 +--- a/net/minecraft/world/entity/ExperienceOrb.java ++++ b/net/minecraft/world/entity/ExperienceOrb.java +@@ -354,8 +354,22 @@ public class ExperienceOrb extends Entity { + @Override + public void playerTouch(Player entity) { + if (entity instanceof ServerPlayer serverPlayer) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.xpNoCooldown) { ++ entity.takeXpDelay = 0; ++ } ++ + if (entity.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper - PlayerPickupExperienceEvent +- entity.takeXpDelay = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerXpCooldownEvent(entity, 2, org.bukkit.event.player.PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entity.takeXpDelay = 2; ++ int newCooldown = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerXpCooldownEvent(entity, 2, org.bukkit.event.player.PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entity.takeXpDelay = 2; ++ entity.takeXpDelay = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.xpNoCooldown ? 0 : newCooldown; ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.xpNoCooldown) { ++ while (this.count > 1) { ++ int remainder = this.repairPlayerItems(serverPlayer, this.getValue()); ++ if (remainder > 0) { ++ entity.giveExperiencePoints(remainder); ++ } ++ this.count--; ++ } ++ } + entity.take(this, 1); + int i = this.repairPlayerItems(serverPlayer, this.getValue()); + if (i > 0) { +@@ -371,6 +385,10 @@ public class ExperienceOrb extends Entity { + } + + private int repairPlayerItems(ServerPlayer player, int value) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.powerfulExpMending) { ++ return this.repairAllPlayerItems(player, value); ++ } ++ + Optional randomItemWith = EnchantmentHelper.getRandomItemWith( + EnchantmentEffectComponents.REPAIR_WITH_XP, player, ItemStack::isDamaged + ); +@@ -402,6 +420,76 @@ public class ExperienceOrb extends Entity { + } + } + ++ private int repairAllPlayerItems(ServerPlayer player, int value) { ++ List candidates = new java.util.ArrayList<>(); ++ net.minecraft.world.entity.player.Inventory inventory = player.getInventory(); ++ ++ for (int slotIndex = 0; slotIndex < inventory.getNonEquipmentItems().size(); slotIndex++) { ++ ItemStack stack = inventory.getNonEquipmentItems().get(slotIndex); ++ EquipmentSlot slot = slotIndex == inventory.getSelectedSlot() ? EquipmentSlot.MAINHAND : null; ++ candidates.add(new MendingCandidate(stack, slot)); ++ } ++ ++ candidates.add(new MendingCandidate(player.getItemBySlot(EquipmentSlot.HEAD), EquipmentSlot.HEAD)); ++ candidates.add(new MendingCandidate(player.getItemBySlot(EquipmentSlot.CHEST), EquipmentSlot.CHEST)); ++ candidates.add(new MendingCandidate(player.getItemBySlot(EquipmentSlot.LEGS), EquipmentSlot.LEGS)); ++ candidates.add(new MendingCandidate(player.getItemBySlot(EquipmentSlot.FEET), EquipmentSlot.FEET)); ++ candidates.add(new MendingCandidate(player.getItemBySlot(EquipmentSlot.OFFHAND), EquipmentSlot.OFFHAND)); ++ ++ java.util.Collections.shuffle(candidates, new java.util.Random(player.getRandom().nextLong())); ++ ++ int remaining = value; ++ for (MendingCandidate candidate : candidates) { ++ if (remaining <= 0) { ++ break; ++ } ++ ++ ItemStack stack = candidate.stack(); ++ if (!this.canRepairWithXp(player, stack)) { ++ continue; ++ } ++ ++ int durabilityToRepair = EnchantmentHelper.modifyDurabilityToRepairFromXp(player.level(), stack, remaining); ++ int repairedAmount = Math.min(durabilityToRepair, stack.getDamageValue()); ++ if (repairedAmount <= 0) { ++ continue; ++ } ++ ++ org.bukkit.event.player.PlayerItemMendEvent event = candidate.slot() != null ++ ? org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemMendEvent( ++ player, this, stack, candidate.slot(), repairedAmount, repairedAmount * remaining / durabilityToRepair ++ ) ++ : this.callInventoryItemMendEvent(player, stack, repairedAmount); ++ repairedAmount = event.getRepairAmount(); ++ if (event.isCancelled() || repairedAmount <= 0) { ++ continue; ++ } ++ ++ stack.setDamageValue(stack.getDamageValue() - repairedAmount); ++ remaining -= repairedAmount * remaining / durabilityToRepair; ++ } ++ ++ return remaining; ++ } ++ ++ private boolean canRepairWithXp(ServerPlayer player, ItemStack stack) { ++ return !stack.isEmpty() && stack.isDamaged() && EnchantmentHelper.modifyDurabilityToRepairFromXp(player.level(), stack, 1) > 1; ++ } ++ ++ private org.bukkit.event.player.PlayerItemMendEvent callInventoryItemMendEvent(ServerPlayer player, ItemStack stack, int repairAmount) { ++ org.bukkit.event.player.PlayerItemMendEvent event = new org.bukkit.event.player.PlayerItemMendEvent( ++ player.getBukkitEntity(), ++ org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), ++ (org.bukkit.entity.ExperienceOrb)this.getBukkitEntity(), ++ repairAmount ++ ); ++ player.level().getCraftServer().getPluginManager().callEvent(event); ++ return event; ++ } ++ ++ private record MendingCandidate(ItemStack stack, @Nullable EquipmentSlot slot) { ++ } ++ + public int getValue() { + return this.entityData.get(DATA_VALUE); + } +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index fc8833655102ca98bb331782a624af79cbef414f..b49ff0baae5493925c0b13876fd0473dbeb649c9 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -607,7 +607,8 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin + public boolean broadcastedDeath = false; // Folia - region threading + protected void tickDeath() { + this.deathTime++; +- if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) { ++ int removalDelay = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.entityInstantDeathRemoval ? 1 : 20; ++ if (this.deathTime >= removalDelay && !this.level().isClientSide() && !this.isRemoved()) { + this.level().broadcastEntityEvent(this, EntityEvent.POOF); this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted + if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running + if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index fd1db18764be58bc5ed5983b98e669c76f7ff427..a0d7658435cb838415121882e4c3bb3d0fa10040 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -68,8 +68,12 @@ public class PrimedTnt extends Entity implements TraceableEntity { + this(EntityType.TNT, level); + this.setPos(x, y, z); + double d = this.random.nextDouble() * (float) (Math.PI * 2); // Paper - Don't use level random in entity constructors +- this.setDeltaMovement(-Math.sin(d) * 0.02, 0.2F, -Math.cos(d) * 0.02); +- this.setFuse(80); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntPrimerMomentumRemoved) { ++ this.setDeltaMovement(0.0, 0.20000000298023224D, 0.0); ++ } else { ++ this.setDeltaMovement(-Math.sin(d) * 0.02, 0.2F, -Math.cos(d) * 0.02); ++ } ++ this.setFuse(getConfiguredFuseTime()); + this.xo = x; + this.yo = y; + this.zo = z; +@@ -78,7 +82,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { +- builder.define(DATA_FUSE_ID, 80); ++ builder.define(DATA_FUSE_ID, getConfiguredFuseTime()); + builder.define(DATA_BLOCK_STATE_ID, DEFAULT_BLOCK_STATE); + } + +@@ -175,7 +179,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + + @Override + protected void readAdditionalSaveData(ValueInput input) { +- this.setFuse(input.getShortOr("fuse", (short)80)); ++ this.setFuse(input.getShortOr("fuse", (short)getConfiguredFuseTime())); + this.setBlockState(input.read("block_state", BlockState.CODEC).orElse(DEFAULT_BLOCK_STATE)); + this.explosionPower = Mth.clamp(input.getFloatOr("explosion_power", 4.0F), 0.0F, 128.0F); + this.owner = EntityReference.read(input, "owner"); +@@ -198,6 +202,10 @@ public class PrimedTnt extends Entity implements TraceableEntity { + this.entityData.set(DATA_FUSE_ID, life); + } + ++ private static int getConfiguredFuseTime() { ++ return fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.normalizedTntFuseDuration(); ++ } ++ + public int getFuse() { + return this.entityData.get(DATA_FUSE_ID); + } +diff --git a/net/minecraft/world/entity/monster/EnderMan.java b/net/minecraft/world/entity/monster/EnderMan.java +index f7c0c1c75ba02c2d70ff2b67e437c0813458ee3d..96b436fe4af6fe45b927e757a856e6fe3190cbe6 100644 +--- a/net/minecraft/world/entity/monster/EnderMan.java ++++ b/net/minecraft/world/entity/monster/EnderMan.java +@@ -651,7 +651,7 @@ public class EnderMan extends Monster implements NeutralMob { + Vec3 vec31 = new Vec3(floor + 0.5, floor1 + 0.5, floor2 + 0.5); + BlockHitResult blockHitResult = level.clip(new ClipContext(vec3, vec31, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.enderman)); + boolean flag = blockHitResult.getBlockPos().equals(blockPos); +- if (blockState.is(BlockTags.ENDERMAN_HOLDABLE) && flag) { ++ if (canPickupBlock(blockState) && flag) { + if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(this.enderman, blockPos, blockState.getFluidState().createLegacyBlock())) { // CraftBukkit - Place event // Paper - fix wrong block state + level.removeBlock(blockPos, false); + level.gameEvent(GameEvent.BLOCK_DESTROY, blockPos, GameEvent.Context.of(this.enderman, blockState)); +@@ -659,5 +659,13 @@ public class EnderMan extends Monster implements NeutralMob { + } // CraftBukkit + } + } ++ ++ private static boolean canPickupBlock(BlockState state) { ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.sensibleEnderman) { ++ return state.is(BlockTags.ENDERMAN_HOLDABLE); ++ } ++ ++ return state.is(Blocks.MELON) || state.is(Blocks.PUMPKIN); ++ } + } + } +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 8966953ee80a1527c9c4cc4d0a7a3bdb2197a2da..0a6f1478856f503408a10c131d20ec3b235753ce 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -564,7 +564,13 @@ public abstract class Player extends Avatar implements ContainerUser { + } + + if (!list.isEmpty()) { +- this.touch(Util.getRandom(list, this.random)); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.xpNoCooldown) { ++ for (Entity entity : list) { ++ this.touch(entity); ++ } ++ } else { ++ this.touch(Util.getRandom(list, this.random)); ++ } + } + } + +@@ -1074,6 +1080,22 @@ public abstract class Player extends Avatar implements ContainerUser { + + if (playerAttackEntityEvent.callEvent() && willAttack) { // Logic moved to willAttack local variable. + // Paper end - PlayerAttackEntityEvent ++ if (this.level() instanceof ServerLevel serverLevel ++ && fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeOneHitKill ++ && this.isCreative() ++ && net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(target)) { ++ if (this.isShiftKeyDown()) { ++ for (Entity entity : this.level().getEntities(this, target.getBoundingBox().inflate(2.0D, 0.5D, 2.0D), candidate -> ++ candidate.isAttackable() && net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(candidate))) { ++ this.creativeInstantKill(serverLevel, entity); ++ } ++ this.playServerSideSound(SoundEvents.PLAYER_ATTACK_SWEEP); ++ } else { ++ this.creativeInstantKill(serverLevel, target); ++ this.playServerSideSound(SoundEvents.PLAYER_ATTACK_CRIT); ++ } ++ return; ++ } + float f = this.isAutoSpinAttack() ? this.autoSpinAttackDmg : (float)this.getAttributeValue(Attributes.ATTACK_DAMAGE); + ItemStack weaponItem = this.getWeaponItem(); + DamageSource damageSource = this.createAttackSource(weaponItem); final DamageSource dmgSourceFinal = damageSource; // Paper - damage events +@@ -1131,6 +1153,16 @@ public abstract class Player extends Avatar implements ContainerUser { + } + } + ++ private void creativeInstantKill(ServerLevel level, Entity target) { ++ if (target instanceof EnderDragonPart enderDragonPart) { ++ java.util.Arrays.stream(enderDragonPart.parentMob.getSubEntities()).forEach(part -> part.kill(level)); ++ enderDragonPart.parentMob.kill(level); ++ return; ++ } ++ ++ target.kill(level); ++ } ++ + private void playServerSideSound(SoundEvent sound) { + sendSoundEffect(this, this.getX(), this.getY(), this.getZ(), sound, this.getSoundSource(), 1.0F, 1.0F); // Paper - send while respecting visibility + } +diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java +index 44786e4f77285f1fbd4643f560105e410035afeb..77b3c8b4bb9ae2c7dede35f74c38febdf43e2dfc 100644 +--- a/net/minecraft/world/item/BlockItem.java ++++ b/net/minecraft/world/item/BlockItem.java +@@ -172,7 +172,13 @@ public class BlockItem extends Item { + // CraftBukkit start + CollisionContext collisionContext = player == null ? CollisionContext.empty() : CollisionContext.placementContext(player); + Level world = context.getLevel(); // Paper - Cancel hit for vanished players +- boolean canBuild = (!this.mustSurvive() || state.canSurvive(world, context.getClickedPos())) && ((fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig.enabled && context.getPlayer() != null) ? context.getPlayer().canSpectatingPlace(world, state, context.getClickedPos(), collisionContext) : world.checkEntityCollision(state, player, collisionContext, context.getClickedPos(), true)); // Paper - Cancel hit for vanished players // Leaves - creative no clip ++ boolean ignoreEntityCollision = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.blockPlacementIgnoreEntity && player != null && player.isCreative(); ++ boolean canBuild = (!this.mustSurvive() || state.canSurvive(world, context.getClickedPos())) ++ && (ignoreEntityCollision ++ ? true ++ : ((fun.bm.lophine.config.modules.function.CreativeFlyNoClipConfig.enabled && context.getPlayer() != null) ++ ? context.getPlayer().canSpectatingPlace(world, state, context.getClickedPos(), collisionContext) ++ : world.checkEntityCollision(state, player, collisionContext, context.getClickedPos(), true))); // Paper - Cancel hit for vanished players // Leaves - creative no clip + org.bukkit.entity.Player bukkitPlayer = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null; + + org.bukkit.event.block.BlockCanBuildEvent event = new org.bukkit.event.block.BlockCanBuildEvent( +diff --git a/net/minecraft/world/item/ServerItemCooldowns.java b/net/minecraft/world/item/ServerItemCooldowns.java +index 8725fe35812ee5f7ace0ac3dce466738b6eefc53..9f5311bec541f5efa5e8c60a7ea98824f7eb5cad 100644 +--- a/net/minecraft/world/item/ServerItemCooldowns.java ++++ b/net/minecraft/world/item/ServerItemCooldowns.java +@@ -22,6 +22,9 @@ public class ServerItemCooldowns extends ItemCooldowns { + + @Override + public void addCooldown(ItemStack item, int duration) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeNoItemCooldown && this.player.isCreative()) { ++ return; ++ } + final Identifier cooldownGroup = this.getCooldownGroup(item); + final io.papermc.paper.event.player.PlayerItemCooldownEvent event = new io.papermc.paper.event.player.PlayerItemCooldownEvent( + this.player.getBukkitEntity(), +@@ -38,6 +41,9 @@ public class ServerItemCooldowns extends ItemCooldowns { + + @Override + public void addCooldown(Identifier groupId, int duration, boolean callEvent) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeNoItemCooldown && this.player.isCreative()) { ++ return; ++ } + if (callEvent) { + final io.papermc.paper.event.player.PlayerItemGroupCooldownEvent event = new io.papermc.paper.event.player.PlayerItemGroupCooldownEvent( + this.player.getBukkitEntity(), +diff --git a/net/minecraft/world/item/crafting/RecipeManager.java b/net/minecraft/world/item/crafting/RecipeManager.java +index 4e0b0d630e18bde916a07889bcaf0e7accf064ca..05ace66c11a206443f12537fa829b0186725b526 100644 +--- a/net/minecraft/world/item/crafting/RecipeManager.java ++++ b/net/minecraft/world/item/crafting/RecipeManager.java +@@ -55,6 +55,9 @@ public class RecipeManager extends SimplePreparableReloadListener imp + forSingleInput(RecipeType.CAMPFIRE_COOKING) + ); + private static final FileToIdConverter RECIPE_LISTER = FileToIdConverter.registry(Registries.RECIPE); ++ private static final ResourceKey> BETTER_CRAFTABLE_BONE_BLOCK_RECIPE = compatRecipeKey("better_craftable_bone_block"); ++ private static final ResourceKey> BETTER_CRAFTABLE_DISPENSER_SHAPED_RECIPE = compatRecipeKey("better_craftable_dispenser_shaped"); ++ private static final ResourceKey> BETTER_CRAFTABLE_DISPENSER_SHAPELESS_RECIPE = compatRecipeKey("better_craftable_dispenser_shapeless"); + private final HolderLookup.Provider registries; + public RecipeMap recipes = RecipeMap.EMPTY; + private Map, RecipePropertySet> propertySets = Map.of(); +@@ -83,10 +86,73 @@ public class RecipeManager extends SimplePreparableReloadListener imp + + @Override + protected void apply(RecipeMap object, ResourceManager resourceManager, ProfilerFiller profiler) { ++ this.injectCompatRecipes(object); + this.recipes = object; + LOGGER.info("Loaded {} recipes", object.values().size()); + } + ++ private void injectCompatRecipes(RecipeMap recipeMap) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.betterCraftableBoneBlock) { ++ addCompatRecipe(recipeMap, createBetterCraftableBoneBlockRecipe()); ++ } ++ ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.betterCraftableDispenser) { ++ addCompatRecipe(recipeMap, createBetterCraftableDispenserShapedRecipe()); ++ addCompatRecipe(recipeMap, createBetterCraftableDispenserShapelessRecipe()); ++ } ++ } ++ ++ private static void addCompatRecipe(RecipeMap recipeMap, RecipeHolder recipeHolder) { ++ if (recipeMap.byKey(recipeHolder.id()) == null) { ++ recipeMap.addRecipe(recipeHolder); ++ } ++ } ++ ++ private static RecipeHolder createBetterCraftableBoneBlockRecipe() { ++ return new RecipeHolder<>( ++ BETTER_CRAFTABLE_BONE_BLOCK_RECIPE, ++ new ShapedRecipe( ++ "carpet_ams_better_bone_block", ++ CraftingBookCategory.BUILDING, ++ ShapedRecipePattern.of(Map.of('#', Ingredient.of(net.minecraft.world.item.Items.BONE)), "###", "###", "###"), ++ new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.BONE_BLOCK, 3) ++ ) ++ ); ++ } ++ ++ private static RecipeHolder createBetterCraftableDispenserShapedRecipe() { ++ return new RecipeHolder<>( ++ BETTER_CRAFTABLE_DISPENSER_SHAPED_RECIPE, ++ new ShapedRecipe( ++ "carpet_ams_better_dispenser", ++ CraftingBookCategory.REDSTONE, ++ ShapedRecipePattern.of( ++ Map.of('S', Ingredient.of(net.minecraft.world.item.Items.STICK), 'D', Ingredient.of(net.minecraft.world.item.Items.DROPPER), 'X', Ingredient.of(net.minecraft.world.item.Items.STRING)), ++ " SX", ++ "SDX", ++ " SX" ++ ), ++ new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DISPENSER) ++ ) ++ ); ++ } ++ ++ private static RecipeHolder createBetterCraftableDispenserShapelessRecipe() { ++ return new RecipeHolder<>( ++ BETTER_CRAFTABLE_DISPENSER_SHAPELESS_RECIPE, ++ new ShapelessRecipe( ++ "carpet_ams_better_dispenser", ++ CraftingBookCategory.REDSTONE, ++ new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DISPENSER), ++ List.of(Ingredient.of(net.minecraft.world.item.Items.BOW), Ingredient.of(net.minecraft.world.item.Items.DROPPER)) ++ ) ++ ); ++ } ++ ++ private static ResourceKey> compatRecipeKey(String path) { ++ return ResourceKey.create(Registries.RECIPE, Identifier.fromNamespaceAndPath("lophine", path)); ++ } ++ + // CraftBukkit start + public void addRecipe(RecipeHolder holder) { + org.spigotmc.AsyncCatcher.catchOp("Recipe Add"); // Spigot +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 7803ff8e2e37b44d6e1b094e3141e0c99137659a..81db88c8a25ade71b88fcf5b90880f18d8610783 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1169,6 +1169,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + BlockState state = newState; + BlockState blockState = oldState; + BlockState blockState1 = currentState; ++ boolean skipInteractionUpdates = fun.bm.lophine.carpet.InteractionUpdateCompatHelper.shouldSkipUpdates(); ++ if (skipInteractionUpdates) { ++ flags &= ~Block.UPDATE_NEIGHBORS; ++ } + if (blockState1 == state) { + if (blockState != blockState1) { + this.setBlocksDirty(pos, blockState, blockState1); +@@ -1185,7 +1189,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + } + +- if ((flags & Block.UPDATE_KNOWN_SHAPE) == 0 && recursionLeft > 0) { ++ if (!skipInteractionUpdates && (flags & Block.UPDATE_KNOWN_SHAPE) == 0 && recursionLeft > 0) { + int i = flags & ~(Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_NEIGHBORS); + + // CraftBukkit start +diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java +index e7223540a7cef20b661bdb857ffba962d0f5d348..4b6fbaa33b3db4d514fae7d0913b5530a4a44214 100644 +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -267,6 +267,9 @@ public final class NaturalSpawner { + Player nearestPlayer = level.getNearestPlayer(d, y, d1, -1.0, false); + if (nearestPlayer != null) { + double d2 = nearestPlayer.distanceToSqr(d, y, d1); ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning) { ++ d2 = fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.limitDistanceSq(category, d2); ++ } + if (level.isLoadedAndInBounds(mutableBlockPos) && isRightDistanceToPlayerAndSpawnPoint(level, chunk, mutableBlockPos, d2)) { // Paper - don't load chunks for mob spawn + if (spawnerData == null) { + Optional randomSpawnMobAt = getRandomSpawnMobAt( +@@ -307,6 +310,9 @@ public final class NaturalSpawner { + // SPIGOT-7045: Give ocelot babies back their special spawn reason. Note: This is the only modification required as ocelots count as monsters which means they only spawn during normal chunk ticking and do not spawn during chunk generation as starter mobs. + level.addFreshEntityWithPassengers(mobForSpawn, (mobForSpawn instanceof net.minecraft.world.entity.animal.feline.Ocelot && !((org.bukkit.entity.Ageable) mobForSpawn.getBukkitEntity()).isAdult()) ? org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.OCELOT_BABY : org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL); + if (!mobForSpawn.isRemoved()) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning) { ++ fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.markSpawned(level, mobForSpawn.getType()); ++ } + ++i; + ++i3; + callback.run(mobForSpawn, chunk); +@@ -389,11 +395,20 @@ public final class NaturalSpawner { + && canSpawnMobAt(level, structureManager, generator, category, data, pos) + && SpawnPlacements.isSpawnPositionOk(entityType, level, pos) + && SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random) +- && level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5)); ++ && (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning ++ ? fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.hasNoCollision(level, entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5)) ++ : level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5))); + return success ? PreSpawnStatus.SUCCESS : PreSpawnStatus.FAIL; // Paper - PreCreatureSpawnEvent + } + + private static @Nullable Mob getMobForSpawn(ServerLevel level, EntityType entityType) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning) { ++ Mob cachedMob = fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.getOrCreateMob(level, entityType); ++ if (cachedMob != null) { ++ return cachedMob; ++ } ++ } ++ + try { + if (entityType.create(level, EntitySpawnReason.NATURAL) instanceof Mob mob) { + return mob; +@@ -493,7 +508,9 @@ public final class NaturalSpawner { + float width = spawnerData.type().getWidth(); + double d = Mth.clamp((double)i1, (double)minBlockX + width, minBlockX + 16.0 - width); + double d1 = Mth.clamp((double)i2, (double)minBlockZ + width, minBlockZ + 16.0 - width); +- if (!level.noCollision(spawnerData.type().getSpawnAABB(d, topNonCollidingPos.getY(), d1)) ++ if (!(fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning ++ ? fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.hasNoCollision(level.getLevel(), spawnerData.type().getSpawnAABB(d, topNonCollidingPos.getY(), d1)) ++ : level.noCollision(spawnerData.type().getSpawnAABB(d, topNonCollidingPos.getY(), d1))) + || !SpawnPlacements.checkSpawnRules( + spawnerData.type(), + level, +@@ -525,6 +542,9 @@ public final class NaturalSpawner { + level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, spawnGroupData + ); + level.addFreshEntityWithPassengers(mob, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN); // CraftBukkit ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.lagFreeSpawning && level instanceof ServerLevel serverLevel) { ++ fun.bm.lophine.carpet.LagFreeSpawningCompatHelper.markSpawned(serverLevel, mob.getType()); ++ } + flag = true; + } + } +diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java +index 33ee967d8a7d4ce2c455db9ffaac740cd589e6f3..4dc8dda135b2d43604058ae832ce63dec796c407 100644 +--- a/net/minecraft/world/level/ServerExplosion.java ++++ b/net/minecraft/world/level/ServerExplosion.java +@@ -379,6 +379,10 @@ public class ServerExplosion implements Explosion { + } + + private List calculateExplodedPositions() { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.explosionNoBlockDamage) { ++ return List.of(); ++ } ++ + // Paper start - collision optimisations + final ObjectArrayList ret = new ObjectArrayList<>(); + +diff --git a/net/minecraft/world/level/block/CarvedPumpkinBlock.java b/net/minecraft/world/level/block/CarvedPumpkinBlock.java +index b45e5f571e72028996f641157473039ba9dd3520..8eeabbcf708644069e34503ad736c49b38c787d1 100644 +--- a/net/minecraft/world/level/block/CarvedPumpkinBlock.java ++++ b/net/minecraft/world/level/block/CarvedPumpkinBlock.java +@@ -59,7 +59,8 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + public boolean canSpawnGolem(LevelReader level, BlockPos pos) { + return this.getOrCreateSnowGolemBase().find(level, pos) != null + || this.getOrCreateIronGolemBase().find(level, pos) != null +- || this.getOrCreateCopperGolemBase().find(level, pos) != null; ++ || this.getOrCreateCopperGolemBase().find(level, pos) != null ++ || this.canSpawnShulkerGolem(level, pos); + } + + private void trySpawnGolem(Level level, BlockPos pos) { +@@ -92,6 +93,8 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + copperGolem.spawn(this.getWeatherStateFromPattern(blockPatternMatch2)); + } + } ++ ++ this.trySpawnShulkerGolem(level, pos); + } + + private WeatheringCopper.WeatherState getWeatherStateFromPattern(BlockPattern.BlockPatternMatch patternMatch) { +@@ -159,6 +162,42 @@ public class CarvedPumpkinBlock extends HorizontalDirectionalBlock { + builder.add(FACING); + } + ++ private boolean canSpawnShulkerGolem(LevelReader level, BlockPos pos) { ++ return fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.shulkerGolem ++ && PUMPKINS_PREDICATE.test(level.getBlockState(pos)) ++ && level.getBlockState(pos.below()).is(BlockTags.SHULKER_BOXES); ++ } ++ ++ private void trySpawnShulkerGolem(Level level, BlockPos pos) { ++ if (!this.canSpawnShulkerGolem(level, pos)) { ++ return; ++ } ++ ++ net.minecraft.world.entity.monster.Shulker shulker = EntityType.SHULKER.create(level, EntitySpawnReason.TRIGGERED); ++ if (shulker == null) { ++ return; ++ } ++ ++ BlockPos bodyPos = pos.below(); ++ BlockState headState = level.getBlockState(pos); ++ BlockState bodyState = level.getBlockState(bodyPos); ++ shulker.snapTo(bodyPos.getX() + 0.5, bodyPos.getY(), bodyPos.getZ() + 0.5, 0.0F, 0.0F); ++ if (!level.addFreshEntity(shulker, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.BUILD_IRONGOLEM)) { ++ return; ++ } ++ ++ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); ++ level.setBlock(bodyPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); ++ level.levelEvent(LevelEvent.PARTICLES_DESTROY_BLOCK, pos, Block.getId(headState)); ++ level.levelEvent(LevelEvent.PARTICLES_DESTROY_BLOCK, bodyPos, Block.getId(bodyState)); ++ level.updateNeighborsAt(pos, Blocks.AIR); ++ level.updateNeighborsAt(bodyPos, Blocks.AIR); ++ ++ for (ServerPlayer serverPlayer : level.getEntitiesOfClass(ServerPlayer.class, shulker.getBoundingBox().inflate(5.0))) { ++ CriteriaTriggers.SUMMONED_ENTITY.trigger(serverPlayer, shulker); ++ } ++ } ++ + private BlockPattern getOrCreateSnowGolemBase() { + if (this.snowGolemBase == null) { + this.snowGolemBase = BlockPatternBuilder.start() +diff --git a/net/minecraft/world/level/block/ChestBlock.java b/net/minecraft/world/level/block/ChestBlock.java +index 64c0616c294bd7c7b62ae376d50369500b948390..8797841c1c74fb6ff01bd93497d34500ebeca88a 100644 +--- a/net/minecraft/world/level/block/ChestBlock.java ++++ b/net/minecraft/world/level/block/ChestBlock.java +@@ -296,7 +296,8 @@ public class ChestBlock extends AbstractChestBlock implements + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + if (level instanceof ServerLevel serverLevel) { +- MenuProvider menuProvider = this.getMenuProvider(state, level, pos); ++ boolean ignoreObstructions = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeOpenContainerForcibly && player.isCreative(); ++ MenuProvider menuProvider = this.getMenuProvider(state, level, pos, ignoreObstructions); + if (menuProvider != null && player.openMenu(menuProvider).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation + player.awardStat(this.getOpenChestStat()); + PiglinAi.angerNearbyPiglins(serverLevel, player, true); +diff --git a/net/minecraft/world/level/block/EnderChestBlock.java b/net/minecraft/world/level/block/EnderChestBlock.java +index 202a986762794da9440365953fcb950c6eefa774..dc74a5ee0c6a45b9a0171226b480962a3f793ca2 100644 +--- a/net/minecraft/world/level/block/EnderChestBlock.java ++++ b/net/minecraft/world/level/block/EnderChestBlock.java +@@ -78,7 +78,8 @@ public class EnderChestBlock extends AbstractChestBlock i + PlayerEnderChestContainer enderChestInventory = player.getEnderChestInventory(); + if (enderChestInventory != null && level.getBlockEntity(pos) instanceof EnderChestBlockEntity enderChestBlockEntity) { + BlockPos blockPos = pos.above(); +- if (level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)) { // Paper - diff on change; make sure that EnderChest#isBlocked uses the same logic ++ boolean canOpenForcibly = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeOpenContainerForcibly && player.isCreative(); ++ if (!canOpenForcibly && level.getBlockState(blockPos).isRedstoneConductor(level, blockPos)) { // Paper - diff on change; make sure that EnderChest#isBlocked uses the same logic + return InteractionResult.SUCCESS; + } else { + // Paper start - Fix InventoryOpenEvent cancellation - moved up; +diff --git a/net/minecraft/world/level/block/FarmBlock.java b/net/minecraft/world/level/block/FarmBlock.java +index eb21b20a72062fc53b8f94aa98dcd380c8ad4db3..38bc450afb3cecc349290391f2c235866ad819b5 100644 +--- a/net/minecraft/world/level/block/FarmBlock.java ++++ b/net/minecraft/world/level/block/FarmBlock.java +@@ -133,7 +133,9 @@ public class FarmBlock extends Block { + return; + } + // CraftBukkit end +- turnToDirt(entity, state, level, pos); ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.farmlandTrampledDisabled) { ++ turnToDirt(entity, state, level, pos); ++ } + } + + // super.fallOn(level, state, pos, entity, fallDistance); // CraftBukkit - moved up +diff --git a/net/minecraft/world/level/block/ObserverBlock.java b/net/minecraft/world/level/block/ObserverBlock.java +index 17c371f4c24e8a81705bdd322b1de5de4b8be8b7..ec049a5473b334032ce77cb44e144de687c14c21 100644 +--- a/net/minecraft/world/level/block/ObserverBlock.java ++++ b/net/minecraft/world/level/block/ObserverBlock.java +@@ -88,6 +88,9 @@ public class ObserverBlock extends DirectionalBlock { + } + + private void startSignal(LevelReader level, ScheduledTickAccess scheduledTickAccess, BlockPos pos) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.observerNoDetection) { ++ return; ++ } + if (!level.isClientSide() && !scheduledTickAccess.getBlockTicks().hasScheduledTick(pos, this)) { + scheduledTickAccess.scheduleTick(pos, this, 2); + } +diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java +index 5c154d15e5aa45988436722595c7fca74e166c9a..070c9ad76193690e91bfbfe791ef4fa7cdbf36fe 100644 +--- a/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -280,7 +280,8 @@ public class RedStoneWireBlock extends Block { + * Note: Added 'source' argument so as to help determine direction of information flow + */ + private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, @Nullable Orientation orientation, boolean blockAdded) { +- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.fastRedstoneDust ++ && worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { + // since 24w33a the source pos is no longer given, but instead an Orientation parameter + // when this is not null, it can be used to find the source pos, which the turbo uses + // to find the direction of information flow +@@ -353,7 +354,8 @@ public class RedStoneWireBlock extends Block { + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + if (!oldState.is(state.getBlock()) && !level.isClientSide()) { + // Paper start - optimize redstone - replace call to updatePowerStrength +- if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.fastRedstoneDust ++ || level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + level.getWireHandler().onWireAdded(pos, state); // Alternate Current + } else { + this.updateSurroundingRedstone(level, pos, state, null, true); // Vanilla/Eigencraft +@@ -378,7 +380,8 @@ public class RedStoneWireBlock extends Block { + } + + // Paper start - optimize redstone - replace call to updatePowerStrength +- if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.fastRedstoneDust ++ || level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + level.getWireHandler().onWireRemoved(pos, state); // Alternate Current + } else { + this.updateSurroundingRedstone(level, pos, state, null, false); // Vanilla/Eigencraft +@@ -397,7 +400,8 @@ public class RedStoneWireBlock extends Block { + } + + // Paper start - optimize redstone - replace call to updatePowerStrength +- if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.fastRedstoneDust ++ || level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + level.getWireHandler().onWireRemoved(pos, state); // Alternate Current + } else { + this.updateSurroundingRedstone(level, pos, state, null, false); // Vanilla/Eigencraft +@@ -427,7 +431,8 @@ public class RedStoneWireBlock extends Block { + if (!level.isClientSide()) { + // Paper start - optimize redstone (Alternate Current) + // Alternate Current handles breaking of redstone wires in the WireHandler. +- if (level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.fastRedstoneDust ++ || level.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + level.getWireHandler().onWireUpdated(pos, state, orientation); + } else + // Paper end - optimize redstone (Alternate Current) +diff --git a/net/minecraft/world/level/block/ShulkerBoxBlock.java b/net/minecraft/world/level/block/ShulkerBoxBlock.java +index e820cd03e695649a8f83e23ae822524a8f78e23b..6c58bc561116cff645d60f98294bc66fdb0e9919 100644 +--- a/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -74,9 +74,10 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { ++ boolean canOpenForcibly = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.creativeOpenContainerForcibly && player.isCreative(); + if (level instanceof ServerLevel serverLevel + && level.getBlockEntity(pos) instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity +- && canOpen(state, level, pos, shulkerBoxBlockEntity) // Paper - Fix InventoryOpenEvent cancellation - expand if for belows check ++ && (canOpen(state, level, pos, shulkerBoxBlockEntity) || canOpenForcibly) // Paper - Fix InventoryOpenEvent cancellation - expand if for belows check + && player.openMenu(shulkerBoxBlockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation + player.awardStat(Stats.OPEN_SHULKER_BOX); + PiglinAi.angerNearbyPiglins(serverLevel, player, true); +diff --git a/net/minecraft/world/level/block/TntBlock.java b/net/minecraft/world/level/block/TntBlock.java +index 00da328f21e5ce5de6f968323b14a7832a6cbd62..a68ffb1197a19173f00577f656b5894f3c8ab85e 100644 +--- a/net/minecraft/world/level/block/TntBlock.java ++++ b/net/minecraft/world/level/block/TntBlock.java +@@ -47,7 +47,8 @@ public class TntBlock extends Block { + @Override + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + if (!oldState.is(state.getBlock())) { +- if (level.hasNeighborSignal(pos) && prime(level, pos, () -> org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null))) { // CraftBukkit - TNTPrimeEvent ++ if (shouldPrimeFromRedstone(level, pos) ++ && prime(level, pos, () -> org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null))) { // CraftBukkit - TNTPrimeEvent + level.removeBlock(pos, false); + } + } +@@ -55,7 +56,9 @@ public class TntBlock extends Block { + + @Override + protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { +- if (level.hasNeighborSignal(pos) && prime(level, pos, () -> org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null))) { // CraftBukkit - TNTPrimeEvent ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntIgnoreRedstoneSignal ++ && level.hasNeighborSignal(pos) ++ && prime(level, pos, () -> org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null))) { // CraftBukkit - TNTPrimeEvent + level.removeBlock(pos, false); + } + } +@@ -74,7 +77,7 @@ public class TntBlock extends Block { + if (level.getGameRules().get(GameRules.TNT_EXPLODES)) { + PrimedTnt primedTnt = new PrimedTnt(level, pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, explosion.getIndirectSourceEntity()); + int fuse = primedTnt.getFuse(); +- primedTnt.setFuse((short)(level.random.nextInt(fuse / 4) + fuse / 8)); ++ primedTnt.setFuse(level.random.nextInt(Math.max(fuse / 4, 1)) + fuse / 8); + level.addFreshEntity(primedTnt); + } + } +@@ -148,4 +151,10 @@ public class TntBlock extends Block { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(UNSTABLE); + } ++ ++ private static boolean shouldPrimeFromRedstone(Level level, BlockPos pos) { ++ return !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntDoNotUpdate ++ && !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntIgnoreRedstoneSignal ++ && level.hasNeighborSignal(pos); ++ } + } +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 474ca0ce0d3095fc586f8f9e9a584ed4f7c91471..07c2b0fb1fa2dd299687d389d7fe91c5fd5b5b91 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -513,6 +513,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + for (int i = 0; i < blockEntity.getContainerSize(); i++) { + ItemStack item = blockEntity.getItem(i); + if (!item.isEmpty()) { ++ boolean noItemCost = fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.hopperNoItemCost && org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, pos.above()) != null; + int count = item.getCount(); + // CraftBukkit start - Call event when pushing items into other inventories + ItemStack original = item.copy(); +@@ -544,15 +545,23 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + int origCount = event.getItem().getAmount(); // Spigot + ItemStack itemStack = HopperBlockEntity.addItem(blockEntity, attachedContainer, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), opposite); + // CraftBukkit end ++ int moved = origCount - itemStack.getCount(); + +- if (itemStack.isEmpty()) { ++ if (moved > 0) { + attachedContainer.setChanged(); ++ if (noItemCost) { ++ blockEntity.setItem(i, original); ++ return true; ++ } ++ } ++ ++ if (itemStack.isEmpty()) { + return true; + } + + item.setCount(count); + // Spigot start +- item.shrink(origCount - itemStack.getCount()); ++ item.shrink(moved); + if (count <= level.spigotConfig.hopperAmount) { + // Spigot end + blockEntity.setItem(i, item); +diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +index 31907239431e83515b4e9c8b53dc1ee4d6f29e4d..6fed824d42e034f9a315c8ab74e05ba36c427787 100644 +--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -369,7 +369,7 @@ public class PistonBaseBlock extends DirectionalBlock { + + for (int i1 = toPush.size() - 1; i1 >= 0; i1--) { + // Paper start - fix a variety of piston desync dupes +- boolean allowDesync = io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication; ++ boolean allowDesync = !fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.tntDupingFix; + BlockPos blockPos2; + BlockPos oldPos = blockPos2 = toPush.get(i1); + BlockState blockState1 = allowDesync ? level.getBlockState(oldPos) : null; +diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index 0de2f6627fb6295d7927ac3e37026f4daf68cfd3..727abe6be81fdaa0753035bbf89ffeadf9c839b8 100644 +--- a/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -828,6 +828,9 @@ public abstract class BlockBehaviour implements FeatureElement, org.leavesmc.lea + } + + public Vec3 getOffset(BlockPos pos) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.bambooModelNoOffset && (this.getBlock() == Blocks.BAMBOO || this.getBlock() == Blocks.BAMBOO_SAPLING)) { ++ return Vec3.ZERO; ++ } + BlockBehaviour.OffsetFunction offsetFunction = this.offsetFunction; + return offsetFunction != null ? offsetFunction.evaluate(this.asState(), pos) : Vec3.ZERO; + } +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 8e56c2237f9d49d0b09b62c0933283f77aaae679..64bd2da70f4a0beb0efbb8a74478eef95ca013f2 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -406,10 +406,11 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + boolean flag = !blockState.is(block); + boolean flag1 = (flags & Block.UPDATE_MOVE_BY_PISTON) != 0; + boolean flag2 = (flags & Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS) == 0; ++ boolean skipInteractionUpdates = fun.bm.lophine.carpet.InteractionUpdateCompatHelper.shouldSkipUpdates(); + // Leaves start - behaviour 1.21.1- + if (!fun.bm.lophine.config.modules.experiment.RedStoneConfig.oldBlockRemoveBehaviour) { + if (flag && blockState.hasBlockEntity() && !state.shouldChangedStateKeepBlockEntity(blockState)) { +- if (!this.level.isClientSide() && flag2) { ++ if (!this.level.isClientSide() && flag2 && !skipInteractionUpdates) { + BlockEntity blockEntity = this.level.getBlockEntity(pos); + if (blockEntity != null) { + blockEntity.preRemoveSideEffects(pos, blockState); +@@ -421,6 +422,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + + if ((flag || block instanceof BaseRailBlock) + && this.level instanceof ServerLevel serverLevel ++ && !skipInteractionUpdates + && ((flags & Block.UPDATE_NEIGHBORS) != 0 || flag1)) { + blockState.affectNeighborsAfterRemoval(serverLevel, pos, flag1); + } +@@ -432,7 +434,10 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + if (!section.getBlockState(i, i1, i2).is(block)) { + return null; + } else { +- if (!this.level.isClientSide() && (flags & Block.UPDATE_SKIP_ON_PLACE) == 0 && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading ++ if (!this.level.isClientSide() ++ && !skipInteractionUpdates ++ && (flags & Block.UPDATE_SKIP_ON_PLACE) == 0 ++ && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading + state.onPlace(this.level, pos, blockState, flag1); + } + +diff --git a/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java b/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java +index 48187686126139a4229f454a3d4427f6593fa71b..562a27a051e1a5059e5bb81b64dc4c6def9f8673 100644 +--- a/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java ++++ b/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java +@@ -53,18 +53,18 @@ public enum DragonRespawnAnimation { + endCrystal.setBeamTarget(new BlockPos(endSpike.getCenterX(), endSpike.getHeight() + 1, endSpike.getCenterZ())); + } + } else { +- int i2 = 10; ++ if (!fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.preventEndSpikeRespawn) { ++ for (BlockPos blockPos : BlockPos.betweenClosed( ++ new BlockPos(endSpike.getCenterX() - 10, endSpike.getHeight() - 10, endSpike.getCenterZ() - 10), ++ new BlockPos(endSpike.getCenterX() + 10, endSpike.getHeight() + 10, endSpike.getCenterZ() + 10) ++ )) { ++ level.removeBlock(blockPos, false); ++ } + +- for (BlockPos blockPos : BlockPos.betweenClosed( +- new BlockPos(endSpike.getCenterX() - 10, endSpike.getHeight() - 10, endSpike.getCenterZ() - 10), +- new BlockPos(endSpike.getCenterX() + 10, endSpike.getHeight() + 10, endSpike.getCenterZ() + 10) +- )) { +- level.removeBlock(blockPos, false); ++ level.explode( ++ null, endSpike.getCenterX() + 0.5F, endSpike.getHeight(), endSpike.getCenterZ() + 0.5F, 5.0F, Level.ExplosionInteraction.BLOCK ++ ); + } +- +- level.explode( +- null, endSpike.getCenterX() + 0.5F, endSpike.getHeight(), endSpike.getCenterZ() + 0.5F, 5.0F, Level.ExplosionInteraction.BLOCK +- ); + SpikeConfiguration spikeConfiguration = new SpikeConfiguration(true, ImmutableList.of(endSpike), new BlockPos(0, 128, 0)); + Feature.END_SPIKE + .place( +diff --git a/net/minecraft/world/level/levelgen/feature/SpikeFeature.java b/net/minecraft/world/level/levelgen/feature/SpikeFeature.java +index 70bb9a6742d236bb2f8f9b6bc43e973838ec5842..d111e19380372fd46059cddbaa1d3343deeb6918 100644 +--- a/net/minecraft/world/level/levelgen/feature/SpikeFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/SpikeFeature.java +@@ -66,6 +66,10 @@ public class SpikeFeature extends Feature { + } + + private void placeSpike(ServerLevelAccessor level, RandomSource random, SpikeConfiguration config, SpikeFeature.EndSpike spike) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.preventEndSpikeRespawn && config.getCrystalBeamTarget() != null) { ++ return; ++ } ++ + int radius = spike.getRadius(); + + for (BlockPos blockPos : BlockPos.betweenClosed( +diff --git a/net/minecraft/world/level/redstone/NeighborUpdater.java b/net/minecraft/world/level/redstone/NeighborUpdater.java +index 32488203ff074b675f5c8831d157b1d91bae48a9..2c5f4006cf56921bee30223bf76b18a8e2ef56c6 100644 +--- a/net/minecraft/world/level/redstone/NeighborUpdater.java ++++ b/net/minecraft/world/level/redstone/NeighborUpdater.java +@@ -34,6 +34,9 @@ public interface NeighborUpdater { + static void executeShapeUpdate( + LevelAccessor level, Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, @Block.UpdateFlags int flags, int recursionLeft + ) { ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.totallyNoBlockUpdate) { ++ return; ++ } + BlockState blockState = level.getBlockState(pos); + if ((flags & Block.UPDATE_SKIP_SHAPE_UPDATE_ON_WIRE) == 0 || !blockState.is(Blocks.REDSTONE_WIRE)) { + BlockState blockState1 = blockState.updateShape(level, level, pos, direction, neighborPos, neighborState, level.getRandom()); +@@ -48,6 +51,9 @@ public interface NeighborUpdater { + + static void executeUpdate(Level level, BlockState state, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston, BlockPos sourcePos) { + // Paper end - Add source block to BlockPhysicsEvent ++ if (fun.bm.lophine.carpet.config.modules.GeneralCompatConfig.totallyNoBlockUpdate) { ++ return; ++ } + try { + // CraftBukkit start + org.bukkit.event.block.BlockPhysicsEvent event = new org.bukkit.event.block.BlockPhysicsEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(state), org.bukkit.craftbukkit.block.CraftBlock.at(level, sourcePos)); // Paper - Add source block to BlockPhysicsEvent diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCalculatorCompatHelper.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCalculatorCompatHelper.java new file mode 100644 index 0000000..478def3 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCalculatorCompatHelper.java @@ -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(); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java new file mode 100644 index 0000000..0f6c0a1 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/CarpetCompatSync.java @@ -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 sanitizeDefaultLoggers(List 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)); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/InteractionUpdateCompatHelper.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/InteractionUpdateCompatHelper.java new file mode 100644 index 0000000..14deac4 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/InteractionUpdateCompatHelper.java @@ -0,0 +1,42 @@ +package fun.bm.lophine.carpet; + +import java.util.function.Supplier; + +public final class InteractionUpdateCompatHelper { + private static final ThreadLocal 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 supplyWithSuppressedUpdates(Supplier 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); + } + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/LagFreeSpawningCompatHelper.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/LagFreeSpawningCompatHelper.java new file mode 100644 index 0000000..f2d7d60 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/LagFreeSpawningCompatHelper.java @@ -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, 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, 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, Mob> cache = PRECOOKED_MOBS.get(level); + if (cache != null) { + cache.remove(entityType); + } + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CoreConfig.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CoreConfig.java new file mode 100644 index 0000000..86d4528 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CoreConfig.java @@ -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(); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CounterCompatConfig.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CounterCompatConfig.java new file mode 100644 index 0000000..5b53efb --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/CounterCompatConfig.java @@ -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; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/FakePlayerCompatConfig.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/FakePlayerCompatConfig.java new file mode 100644 index 0000000..5aa7d16 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/FakePlayerCompatConfig.java @@ -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; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/GeneralCompatConfig.java b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/GeneralCompatConfig.java new file mode 100644 index 0000000..9220099 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/carpet/config/modules/GeneralCompatConfig.java @@ -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 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); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/FakeplayerConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/FakeplayerConfig.java index 7a56cb1..9fdf1f8 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/FakeplayerConfig.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/FakeplayerConfig.java @@ -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; diff --git a/lophine-server/src/main/java/fun/bm/lophine/protocol/CarpetLoggerProtocol.java b/lophine-server/src/main/java/fun/bm/lophine/protocol/CarpetLoggerProtocol.java new file mode 100644 index 0000000..b71f9cf --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/protocol/CarpetLoggerProtocol.java @@ -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> PLAYER_SUBSCRIPTIONS = new ConcurrentHashMap<>(); + private static volatile Map configuredSubscriptions = null; + + public static void refreshConfiguredDefaults(boolean initial) { + Map 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 configuredLoggers) { + List 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 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 subscriptions) { + List 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 buildCounterLines(MinecraftServer server, String option) { + List 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 parseConfiguredDefaults(List configuredLoggers) { + LinkedHashMap 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 -> ""; + }; + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/protocol/tiscm/TISCMProtocol.java b/lophine-server/src/main/java/fun/bm/lophine/protocol/tiscm/TISCMProtocol.java new file mode 100644 index 0000000..96f7996 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/protocol/tiscm/TISCMProtocol.java @@ -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> 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 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 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 packets = CLIENT_SUPPORTED_PACKETS.get(player.getStringUUID()); + return packets != null && packets.contains(packet); + } + + private static ListTag stringList(List 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 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 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 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 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 CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, + TISCMPayload::packetId, + ByteBufCodecs.COMPOUND_TAG, + TISCMPayload::nbt, + TISCMPayload::new + ); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java b/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java index 9ab354c..906fa79 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java @@ -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 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 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 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 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) { diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java b/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java index c126d76..4c81f80 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java @@ -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> actions; private final Map> 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 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 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; diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java b/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java index e634f91..30d6ba1 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java @@ -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(); + } } diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerUseItemToAction.java b/lophine-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerUseItemToAction.java index 68ba54e..da9ecfe 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerUseItemToAction.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/ServerUseItemToAction.java @@ -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 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; diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java index 3a9e9ff..f876469 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java @@ -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)