Author SHA1 Message Date
AxTps 01dc9fa79c feat: 落沙替换改回固定目标版,移除记录器mixin
build / build (push) Has been cancelled
记录+回放方案会把沙子替换成自己(记录命中沙子自身变更),不可用。改为沙子下落直接变legacyFallingBlockTarget。移除LevelChunkMixin。
2026-10-04 20:36:57 +08:00
AxTps a32a784f87 chore: 加调试日志定位instantFall/落沙替换未生效问题 2026-10-04 20:27:50 +08:00
AxTps 0649034969 feat: 落沙替换改用记录+回放(装置驱动)
26.1.2单线程下1.12的替换窗口物理不存在,改用LegacyBlockChangeRecorder记录最近方块变更,fall时若命中窗口则用记录的新方块,保留装置驱动玩法。新增LevelChunkMixin记录setBlockState。
2026-10-04 18:56:50 +08:00
AxTps c42fffad3c feat: instantFall改用ModifyArg直接改scheduleTick的delay参数
getDelayAfterPlace的hook可能被子类覆盖,改用ModifyArg拦截onPlace/updateShape的scheduleTick调用更可靠。
2026-10-04 18:06:05 +08:00
AxTps 21f8e92f2e feat: 实现instantFall(getDelayAfterPlace返回0) 2026-10-04 18:00:58 +08:00
AxTps 9963664933 fix: [旧版]分类字符串改用ASCII的legacy,中文仅作显示名
中文分类名会导致/carpet list命令报拖尾数据错误。
2026-10-04 17:33:07 +08:00
AxTps ddb8699328 fix: 修正[旧版]分类的lang key为carpet.category.旧版
Carpet分类的lang key直接使用分类字符串本身,而非小写常量名。
2026-10-04 17:29:47 +08:00
AxTps ff742f0274 feat: 复刻1.12遗留漏洞机制(旧版分类)
build / build (push) Has been cancelled
新增[旧版]Carpet分类及7条规则,完整复刻1.12获取非法方块所需机制:

- legacyInstantFall/legacyInstantTick:等价1.12的瞬间下落/计划刻立即执行

- legacyAsyncChunkAccess:自建后台线程模拟1.12信标线程并发访问区块

- legacyChunkSwap:hook getVisibleChunkIfPresent,异步线程伪装区块未加载触发装饰

- legacyFallingBlockReplace/legacyFallingBlockTarget:拦截fall的state参数实现落沙替换

参考1.12源码(MCP反编译)+Earthcomputer/Myren的原理讲解实现。
2026-10-04 17:06:20 +08:00
AxTps d6eb433f65 fix: 修复 /hat /sit 命令红字 + trialSpawnerCD 冷却
build / build (push) Has been cancelled
- /hat /sit:规则判断从 requires 移到 executes,命令总是显示(避免客户端读不到服务端规则值导致红字) & - MixinTrialSpawnerState:覆盖 tickAndGetNext 里全部 3 处 getTargetCooldownLength 调用(去掉 ordinal 限制)
2026-10-03 13:18:11 +08:00
AxTps bc472026f9 feat: 新增坚硬陶罐规则
build / build (push) Has been cancelled
- sturdyDecoratedPot:装饰陶罐不被投掷物击碎 & - unbreakableDecoratedPot:玩家挖掘陶罐时不产生裂纹效果 & - 新增 MixinDecoratedPotBlock(onProjectileHit 取消 + playerWillDestroy 不设 CRACKED) & - 补中英语言描述
2026-10-03 12:11:11 +08:00
AxTps ad89ab0574 fix: 移除 gradle.properties 里的 org.gradle.java.home
build / build (push) Has been cancelled
本机绝对路径不该进版本控制,CI 上路径不存在导致构建失败。本地构建请通过 JAVA_HOME 环境变量指定 Java 25。
2026-10-03 08:36:08 +08:00
AxTps e1b1bed804 feat: 集成海龟就地筑巢功能(移植自 turtle-reproduce)
build / build (push) Has been cancelled
- 新增规则 turtleLocalNesting(默认 false)控制开关,关闭时走原版逻辑 & - 搬入 NestingSiteFinder(高性能就近沙滩搜索) & - 搬入 TurtleGoHomeGoalMixin / TurtleLayEggGoalMixin / TurtleAccessor / PathNavigationAccessor & - 原独立 mod turtle-reproduce 的功能并入本 Carpet 扩展 & - 补中英语言描述
2026-10-03 08:34:32 +08:00
AxTps 6687583c96 fix: 修复 26.1.2 运行时 mixin 问题
build / build (push) Has been cancelled
- MixinServerPlayerEntity: 移除失效的 @Shadow isSpectator,packDirty 加 null 检查(修复 playerSit 崩溃) & - MixinSpawnHelper: 改用 @Inject getMobForSpawn,避免与 Carpet NaturalSpawnerMixin 的 @Redirect 冲突 & - MixinBoatEntity: 改挂 AbstractBoat.tick(Boat 无 tick) & - MixinMobEntity: 移除对接口 default 方法的无效 @Shadow & - MixinPlayerInventory: creativeNoInfinitePickup 改用 hasInfiniteMaterials(原 isInCreativeMode 已不存在) & - MixinRedstoneWireBlock: oldRedstoneConnectionLogic 改挂 getConnectingSide & - 禁用 datapack.MixinVanillaResourcePackProvider(新版资源包系统重构,待重写) & - ServerMain: registerCommands 加日志+异常捕获
2026-10-03 08:15:04 +08:00
AxTps 8e44aaa780 feat: 完整迁移到 Minecraft 26.1.2 / Java 25
build / build (push) Has been cancelled
- 构建:Loom 1.18-SNAPSHOT、Gradle 9.7.1、official 映射 & - 依赖:carpet 26.1+v260401、fabric-api 0.155.3+26.1.2、fabric-permissions-api 0.7.0 & - 源码:~30 文件 + 28 mixin 从 Yarn 迁移到 Mojang official 映射 & - accessWidener 改为 official 命名空间 & - 适配新 API:Component/ChatFormatting/CommandSourceStack/Commands/ResourceKey/ & ServerLevel/ServerPlayer/Abilities/CraftingRemainingItem/PermissionSet/ValueInput-Output 等 & - 部分功能(elytraCraftable/datapack 注入/offlineFakePlayers)按新 API 近似重写,待实机验证
2026-10-03 07:07:07 +08:00
AxTps a1a2103f47 wip: 迁移到 26.1.2(第一批:构建配置+简单命令类)
build / build (push) Has been cancelled
已改:accessWidener/ fabric.mod.json/ HatCommand/ SitCommand/ util 接口/ 映射表文档。剩余源码待迁移。
2026-10-02 23:39:00 +08:00
AxTps 88aaea801c fix: Carpet 版本改用 masa maven 实际存在的 26.1+v260401
build / build (push) Has been cancelled
26.1+v260402 在 masa.dy.fi/maven 返回 404,回退到可用的 26.1+v260401。
2026-10-02 23:25:18 +08:00
AxTps ee489c9ae5 fix: 适配新 Loom,依赖改用 implementation/runtimeOnly
build / build (push) Has been cancelled
新版 Loom 1.18 移除了 modImplementation/modRuntimeOnly,统一走标准 Gradle 配置(对 mod 依赖自动 remap)。
2026-10-02 23:23:21 +08:00
AxTps c8d810ecae chore: 升级构建配置到 Minecraft 26.1.2 / Java 25
build / build (push) Has been cancelled
- Loom 插件切换为 net.fabricmc.fabric-loom 1.18-SNAPSHOT & - Gradle wrapper 升级到 9.7.1 & - 依赖升级:loader 0.19.5 / fabric-api 0.155.3+26.1.2 / carpet 26.1+v260402 & - 编译目标 Java 25 & - CI 升级到 ubuntu-24.04 + JDK 25 + 新版 action
2026-10-02 23:17:42 +08:00
65 changed files with 1881 additions and 728 deletions

No files matched your search

+9 -16
View File
@@ -8,30 +8,23 @@ on: [pull_request, push]
jobs:
build:
strategy:
matrix:
# Use these Java versions
java: [
21
]
os: [ubuntu-20.04]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-24.04
steps:
- name: checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: validate gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: setup jdk ${{ matrix.java }}
uses: actions/setup-java@v1
uses: gradle/actions/wrapper-validation@v6
- name: setup jdk
uses: actions/setup-java@v5
with:
java-version: ${{ matrix.java }}
java-version: '25'
distribution: 'microsoft'
- name: make gradle wrapper executable
if: ${{ runner.os != 'Windows' }}
run: chmod +x ./gradlew
- name: build
run: ./gradlew build
- name: capture build artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v7
with:
name: Artifacts
path: build/libs/
path: build/libs/
+111
View File
@@ -0,0 +1,111 @@
# Yarn → Mojang official 映射表(1.21 → 26.1.2)
> 26.1.2 起 Fabric 弃用 Yarn,改用 Mojang 官方映射(jar 未混淆)。
> 本表记录本 mod 迁移用到的类名对照。⚠️ 表示结构/语义有变化,需人工核对。
## block
- block.BlockState → world.level.block.state.BlockState
- block.Blocks → world.level.block.Blocks
- block.MushroomPlantBlock → world.level.block.MushroomBlock
- block.RedstoneWireBlock → world.level.block.RedStoneWireBlock
- block.ShulkerBoxBlock → world.level.block.ShulkerBoxBlock
- block.entity.BlockEntity → world.level.block.entity.BlockEntity
- block.entity.EndGatewayBlockEntity → world.level.block.entity.TheEndGatewayBlockEntity
- block.entity.EndPortalBlockEntity → world.level.block.entity.TheEndPortalBlockEntity
- block.enums.TrialSpawnerState → world.level.block.entity.trialspawner.TrialSpawnerState
- block.pattern.BlockPattern → world.level.block.state.pattern.BlockPattern
- block.pattern.CachedBlockPosition → world.level.block.state.pattern.BlockPattern$BlockCacheLoader ⚠️
- block.spawner.TrialSpawnerConfig → world.level.block.entity.trialspawner.TrialSpawnerConfig
- block.spawner.TrialSpawnerData → world.level.block.entity.trialspawner.TrialSpawnerStateData ⚠️
- block.spawner.TrialSpawnerLogic → world.level.block.entity.trialspawner.TrialSpawner ⚠️
## command
- command.CommandRegistryAccess → commands.CommandBuildContext ⚠️
- command.argument.* → commands.arguments.*
- server.command.ServerCommandSource → commands.CommandSourceStack
- server.command.CommandManager → commands.Commands
- command.CommandSource.suggestMatching → commands.SharedSuggestionProvider 方法 ⚠️
## component / nbt / text / util
- component.DataComponentTypes → core.component.DataComponentTypes
- nbt.NbtCompound → nbt.CompoundTag
- nbt.NbtElement → nbt.Tag
- text.Text → network.chat.Component
- text.Style → network.chat.Style
- util.Formatting → ChatFormatting
- util.math.BlockPos → core.BlockPos
- util.math.Direction → core.Direction
- util.math.MathHelper → util.Mth
- util.math.Vec2f → world.phys.Vec2
- util.math.Vec3d → world.phys.Vec3
- util.UserCache → server.players.GameProfileCache ⚠️
- util.Uuids → core.UUIDUtil
- util.Nameable → world.Nameable
- registry.RegistryKey → resources.ResourceKey
- registry.tag.TagKey → tags.TagKey
- registry.tag.BlockTags → tags.BlockTags
- SharedConstants → SharedConstants
## entity
- entity.Entity → world.entity.Entity
- entity.EntityType → world.entity.EntityType
- entity.EquipmentSlot → world.entity.EquipmentSlot
- entity.FallingBlockEntity → world.entity.item.FallingBlockEntity
- entity.ItemEntity → world.entity.item.ItemEntity
- entity.Leashable → world.entity.Leashable
- entity.LivingEntity → world.entity.LivingEntity
- entity.attribute.EntityAttributes → world.entity.ai.attributes.Attributes ⚠️
- entity.boss.dragon.EnderDragonFight → world.level.dimension.end.EnderDragonFight
- entity.damage.DamageSource → world.damagesource.DamageSource
- entity.decoration.ArmorStandEntity → world.entity.decoration.ArmorStand
- entity.decoration.EndCrystalEntity → world.entity.EndCrystal ⚠️
- entity.mob.EndermanEntity → world.entity.monster.EnderMan
- entity.mob.MobEntity → world.entity.Mob
- entity.passive.AnimalEntity → world.entity.animal.Animal
- entity.passive.ArmadilloEntity → world.entity.animal.Armadillo ⚠️
- entity.player.PlayerAbilities → world.entity.player.Abilities
- entity.player.PlayerEntity → world.entity.player.Player
- entity.player.PlayerInventory → world.entity.player.Inventory
- entity.vehicle.BoatEntity → world.entity.vehicle.Boat ⚠️
## item / inventory / screen
- inventory.Inventory → world.entity.player.Inventory
- item.BlockItem → world.item.BlockItem
- item.ElytraItem → world.item.ElytraItem ⚠️
- item.Equipment → world.item.Equipment ⚠️
- item.Item → world.item.Item
- item.ItemStack → world.item.ItemStack
- item.Items → world.item.Items
- screen.AnvilScreenHandler → world.inventory.AnvilMenu
- screen.ScreenHandler → world.inventory.AbstractContainerMenu
- screen.slot.Slot → world.inventory.Slot
## network
- network.NetworkSide → network.protocol.PacketFlow
- network.packet.c2s.common.SyncedClientOptions → server.level.ClientInformation
- network.packet.s2c.play.EntityPositionS2CPacket → network.protocol.game.ClientboundMoveEntityPacket
- network.packet.s2c.play.EntitySetHeadYawS2CPacket → network.protocol.game.ClientboundRotateHeadPacket
- network.packet.s2c.play.EntityTrackerUpdateS2CPacket → network.protocol.game.ClientboundSetEntityDataPacket
- network.packet.s2c.play.PlayerListS2CPacket → network.protocol.game.ClientboundPlayerInfoUpdatePacket
- server.network.ConnectedClientData → server.network.CommonListenerCookie ⚠️
- server.network.ServerPlayNetworkHandler → server.network.ServerGamePacketListenerImpl
- server.network.ServerPlayerEntity → server.level.ServerPlayer
## server / world
- server.MinecraftServer → server.MinecraftServer
- server.PlayerManager → server.players.PlayerList
- server.world.ServerWorld → server.level.ServerLevel
- server.world.SleepManager → server.players.SleepStatus ⚠️
- world.World → world.level.Level
- world.WorldView → world.level.LevelReader
- world.GameMode → world.level.GameType
- world.SpawnHelper → world.level.NaturalSpawner
- world.chunk.WorldChunk → world.level.chunk.LevelChunk
- world.Heightmap → world.level.levelgen.Heightmap
- world.dimension.NetherPortal → world.level.block.NetherPortalBlock ⚠️
- world.gen.feature.EndPortalFeature → world.level.levelgen.feature.EndPodiumFeature
- sound.SoundCategory → sounds.SoundSource
- sound.SoundEvents → sounds.SoundEvents
- resource.ResourcePackProfile → server.packs.* ⚠️
- resource.ResourceType → server.packs.PackType
- resource.VanillaResourcePackProvider → server.packs.vanilla.VanillaPackResources ⚠️
+16 -12
View File
@@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '1.10.5'
id 'net.fabricmc.fabric-loom' version "${loom_version}"
id 'maven-publish'
}
@@ -34,19 +34,18 @@ repositories {
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
// luck perm
modRuntimeOnly "maven.modrinth:luckperms:v5.4.133-fabric"
runtimeOnly "maven.modrinth:luckperms:v5.4.133-fabric"
// carpet
modImplementation "carpet:fabric-carpet:${project.carpet_core_version}"
implementation "carpet:fabric-carpet:${project.carpet_core_version}"
// Fabric Permission API
include(modImplementation('me.lucko:fabric-permissions-api:0.3.1'))
include(implementation('me.lucko:fabric-permissions-api:0.7.0'))
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
}
loom {
@@ -54,16 +53,17 @@ loom {
}
processResources {
inputs.property "version", project.version
def modVersion = project.version
inputs.property "version", modVersion
filesMatching("fabric.mod.json") {
expand "version": project.version
expand "version": modVersion
}
}
tasks.withType(JavaCompile).configureEach {
// Minecraft 1.21 and upwards uses Java 21.
it.options.release = 21
// Minecraft 26.1 and upwards uses Java 25.
it.options.release = 25
}
java {
@@ -71,11 +71,15 @@ java {
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
jar {
def baseName = project.base.archivesName.get()
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
rename { "${it}_${baseName}"}
}
}
+7 -5
View File
@@ -1,15 +1,17 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx3G
org.gradle.parallel=true
org.gradle.configuration-cache=true
# Fabric Properties
# check these on https://fabricmc.net/develop
minecraft_version=1.21
yarn_mappings=1.21+build.9
loader_version=0.15.11
minecraft_version=26.1.2
loader_version=0.19.5
loom_version=1.18-SNAPSHOT
# Mod Properties
mod_version = 0.23
maven_group = com.github.zly2006
archives_base_name = carpet-sls-addition
# Dependencies
fabric_version=0.102.0+1.21
carpet_core_version=1.21-1.4.147+v240613
fabric_version=0.155.3+26.1.2
carpet_core_version=26.1+v260401
Binary file not shown.
+6 -2
View File
@@ -1,5 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dists
Vendored
+34 -20
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,22 +132,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -165,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+28 -35
View File
@@ -13,19 +13,22 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,15 +43,15 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -56,34 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+5 -1
View File
@@ -7,4 +7,8 @@ pluginManagement {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id 'net.fabricmc.fabric-loom' version providers.gradleProperty("loom_version").get()
}
}
@@ -7,6 +7,7 @@ public class SLSCarpetSettings {
public static final String NEED_CLIENT = "needClient"; // 需要客户端安装SLS-Addition或实现相关支持
public static final String FROM_AMS = "AMS";
public static final String SLSA = "SLS";
public static final String LEGACY = "legacy";
@Rule(categories = {SLSA, RuleCategory.SURVIVAL})
public static boolean obtainableReinforcedDeepSlate = false;
@@ -68,4 +69,41 @@ public class SLSCarpetSettings {
@Rule(categories = {SLSA, RuleCategory.FEATURE})
public static int trialSpawnerCD = -1;
@Rule(categories = {SLSA, RuleCategory.FEATURE})
public static boolean turtleLocalNesting = false;
@Rule(categories = {SLSA, RuleCategory.FEATURE})
public static boolean sturdyDecoratedPot = false;
@Rule(categories = {SLSA, RuleCategory.FEATURE})
public static boolean unbreakableDecoratedPot = false;
// ===== [旧版] 遗留漏洞机制复刻 =====
// 总开关:开启后,其余子规则才生效
@Rule(categories = {LEGACY}, strict = false)
public static boolean legacyExploitMode = false;
// 等价 instantTick:计划刻(scheduleTick)立即执行,不等延迟
@Rule(categories = {LEGACY})
public static boolean legacyInstantTick = false;
// 等价 instantFall:重力方块被计划刻触发时立即下落
@Rule(categories = {LEGACY})
public static boolean legacyInstantFall = false;
// 自建后台线程,模拟 1.12 信标线程并发访问 chunk
@Rule(categories = {LEGACY})
public static boolean legacyAsyncChunkAccess = false;
// 允许异步线程在 chunk 表竞态时把"已加载"当成"未加载",触发重新加载/装饰
@Rule(categories = {LEGACY})
public static boolean legacyChunkSwap = false;
// 落沙替换:在 FallingBlock 转实体窗口内把方块替换成目标方块
@Rule(categories = {LEGACY})
public static boolean legacyFallingBlockReplace = false;
// 目标方块(替换结果),默认末地传送门框架
@Rule(categories = {LEGACY}, strict = false, options = {"#none", "end_portal_frame", "command_block", "spawner", "barrier", "nether_portal"})
public static String legacyFallingBlockTarget = "#none";
}
@@ -12,9 +12,9 @@ import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.Version;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -29,7 +29,7 @@ public class ServerMain implements ModInitializer, CarpetExtension {
public static final Version MOD_VERSION = FabricLoader.getInstance().getModContainer(MOD_ID).get().getMetadata().getVersion();
public static final Logger LOGGER = LogManager.getLogger(MOD_ID);
static final Gson GSON = new GsonBuilder().setLenient().create(); // 使用宽容模式,避免部分开发者在书写JSON时不遵守RFC 4627规范
static final Gson GSON = new GsonBuilder().setLenient().create();
public static ServerMain INSTANCE;
public static MinecraftServer server;
@@ -38,6 +38,7 @@ public class ServerMain implements ModInitializer, CarpetExtension {
@Override
public void onInitialize() {
LOGGER.info("[SLSA] onInitialize: managing Carpet extension");
INSTANCE = this;
CarpetServer.manageExtension(this);
}
@@ -50,6 +51,34 @@ public class ServerMain implements ModInitializer, CarpetExtension {
@Override
public void onServerLoaded(MinecraftServer server) {
ServerMain.server = server;
startLegacyRuleWatcher();
}
/** [旧版] 轮询规则状态,按需启停异步 chunk 线程。 */
private void startLegacyRuleWatcher() {
Thread watcher = new Thread(() -> {
boolean running = false;
while (!Thread.currentThread().isInterrupted()) {
try {
boolean want = SLSCarpetSettings.legacyExploitMode
&& SLSCarpetSettings.legacyAsyncChunkAccess;
if (want && !running) {
com.github.zly2006.carpetslsaddition.util.LegacyAsyncExecutor.start();
running = true;
} else if (!want && running) {
com.github.zly2006.carpetslsaddition.util.LegacyAsyncExecutor.stop();
running = false;
}
Thread.sleep(1000L);
} catch (InterruptedException e) {
return;
} catch (Throwable t) {
LOGGER.debug("[SLSA] legacy rule watcher error: {}", t.toString());
}
}
}, "SLSA-Legacy-RuleWatcher");
watcher.setDaemon(true);
watcher.start();
}
@Override
@@ -74,9 +103,15 @@ public class ServerMain implements ModInitializer, CarpetExtension {
}
@Override
public void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess commandBuildContext) {
HatCommand.register(dispatcher);
SitCommand.register(dispatcher);
BotCommand.register(dispatcher);
public void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext) {
LOGGER.info("[SLSA] registerCommands called!");
try {
HatCommand.register(dispatcher);
SitCommand.register(dispatcher);
BotCommand.register(dispatcher);
LOGGER.info("[SLSA] commands registered: hat/sit/bot");
} catch (Throwable t) {
LOGGER.error("[SLSA] failed to register commands", t);
}
}
}
}
@@ -20,35 +20,35 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.minecraft.SharedConstants;
import net.minecraft.command.argument.DimensionArgumentType;
import net.minecraft.command.argument.GameModeArgumentType;
import net.minecraft.command.argument.RotationArgumentType;
import net.minecraft.command.argument.Vec3ArgumentType;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.packet.c2s.common.SyncedClientOptions;
import net.minecraft.network.packet.s2c.play.EntityPositionS2CPacket;
import net.minecraft.network.packet.s2c.play.EntitySetHeadYawS2CPacket;
import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket;
import net.minecraft.registry.RegistryKey;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.GameModeArgument;
import net.minecraft.commands.arguments.coordinates.RotationArgument;
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.UUIDUtil;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket;
import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ConnectedClientData;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.UserCache;
import net.minecraft.util.Uuids;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec2f;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.GameMode;
import net.minecraft.world.World;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.CommonListenerCookie;
import net.minecraft.server.players.NameAndId;
import net.minecraft.server.players.PlayerList;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -58,17 +58,27 @@ import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import static net.minecraft.command.CommandSource.suggestMatching;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
import static net.minecraft.commands.SharedSuggestionProvider.suggest;
public class BotCommand {
public static void register(CommandDispatcher<ServerCommandSource> dispatcher)
private static boolean hasLevel(CommandSourceStack source, int level) {
var perm = switch (level) {
case 0 -> net.minecraft.commands.Commands.LEVEL_ALL;
case 1 -> net.minecraft.commands.Commands.LEVEL_MODERATORS;
case 2 -> net.minecraft.commands.Commands.LEVEL_GAMEMASTERS;
case 3 -> net.minecraft.commands.Commands.LEVEL_ADMINS;
default -> net.minecraft.commands.Commands.LEVEL_OWNERS;
};
return perm.check(source.permissions());
}
public static void register(CommandDispatcher<CommandSourceStack> dispatcher)
{
LiteralArgumentBuilder<ServerCommandSource> command = literal("bot")
.requires(source -> source.hasPermissionLevel(4) || Permissions.check(source, "slsaddition.command.bot"))
LiteralArgumentBuilder<CommandSourceStack> command = literal("bot")
.requires(source -> hasLevel(source, 4) || Permissions.check(source, "slsaddition.command.bot"))
.then(argument("player", StringArgumentType.word())
.suggests((c, b) -> suggestMatching(getPlayerSuggestions(c.getSource()), b))
.suggests((c, b) -> suggest(getPlayerSuggestions(c.getSource()), b))
.then(literal("stop").executes(manipulation(EntityPlayerActionPack::stopAll)))
.then(makeActionCommand("use", EntityPlayerActionPack.ActionType.USE))
.then(makeActionCommand("jump", EntityPlayerActionPack.ActionType.JUMP))
@@ -97,40 +107,38 @@ public class BotCommand {
.then(literal("west").executes(manipulation(ap -> ap.look(Direction.WEST))))
.then(literal("up").executes(manipulation(ap -> ap.look(Direction.UP))))
.then(literal("down").executes(manipulation(ap -> ap.look(Direction.DOWN))))
.then(literal("at").then(argument("position", Vec3ArgumentType.vec3())
.executes(c -> manipulate(c, ap -> ap.lookAt(Vec3ArgumentType.getVec3(c, "position"))))))
.then(argument("direction", RotationArgumentType.rotation())
.executes(c -> manipulate(c, ap -> ap.look(RotationArgumentType.getRotation(c, "direction").toAbsoluteRotation(c.getSource())))))
).then(literal("turn")
.then(literal("at").then(argument("position", Vec3Argument.vec3())
.executes(c -> manipulate(c, ap -> ap.lookAt(Vec3Argument.getVec3(c, "position"))))))
.then(argument("direction", RotationArgument.rotation())
.executes(c -> manipulate(c, ap -> ap.look(RotationArgument.getRotation(c, "direction").getRotation(c.getSource()))))))
.then(literal("turn")
.then(literal("left").executes(manipulation(ap -> ap.turn(-90, 0))))
.then(literal("right").executes(manipulation(ap -> ap.turn(90, 0))))
.then(literal("back").executes(manipulation(ap -> ap.turn(180, 0))))
.then(argument("rotation", RotationArgumentType.rotation())
.executes(c -> manipulate(c, ap -> ap.turn(RotationArgumentType.getRotation(c, "rotation").toAbsoluteRotation(c.getSource())))))
).then(literal("move").executes(manipulation(EntityPlayerActionPack::stopMovement))
.then(argument("rotation", RotationArgument.rotation())
.executes(c -> manipulate(c, ap -> ap.turn(RotationArgument.getRotation(c, "rotation").getRotation(c.getSource()))))))
.then(literal("move").executes(manipulation(EntityPlayerActionPack::stopMovement))
.then(literal("forward").executes(manipulation(ap -> ap.setForward(1))))
.then(literal("backward").executes(manipulation(ap -> ap.setForward(-1))))
.then(literal("left").executes(manipulation(ap -> ap.setStrafing(1))))
.then(literal("right").executes(manipulation(ap -> ap.setStrafing(-1))))
).then(literal("spawn").executes(BotCommand::spawn)
.then(literal("in").requires((player) -> player.hasPermissionLevel(2))
.then(argument("gamemode", GameModeArgumentType.gameMode())
.then(literal("right").executes(manipulation(ap -> ap.setStrafing(-1)))))
.then(literal("spawn").executes(BotCommand::spawn)
.then(literal("in").requires((player) -> hasLevel(player, 2))
.then(argument("gamemode", GameModeArgument.gameMode())
.executes(BotCommand::spawn)))
.then(literal("at").then(argument("position", Vec3ArgumentType.vec3()).executes(BotCommand::spawn)
.then(literal("facing").then(argument("direction", RotationArgumentType.rotation()).executes(BotCommand::spawn)
.then(literal("in").then(argument("dimension", DimensionArgumentType.dimension()).executes(BotCommand::spawn)
.then(literal("in").requires((player) -> player.hasPermissionLevel(2))
.then(argument("gamemode", GameModeArgumentType.gameMode())
.executes(BotCommand::spawn)
)))
)))
))
.then(literal("at").then(argument("position", Vec3Argument.vec3()).executes(BotCommand::spawn)
.then(literal("facing").then(argument("direction", RotationArgument.rotation()).executes(BotCommand::spawn)
.then(literal("in").then(argument("dimension", DimensionArgument.dimension()).executes(BotCommand::spawn)
.then(literal("in").requires((player) -> hasLevel(player, 2))
.then(argument("gamemode", GameModeArgument.gameMode())
.executes(BotCommand::spawn)))))
))))
)
);
dispatcher.register(command);
}
private static LiteralArgumentBuilder<ServerCommandSource> makeActionCommand(String actionName, EntityPlayerActionPack.ActionType type)
private static LiteralArgumentBuilder<CommandSourceStack> makeActionCommand(String actionName, EntityPlayerActionPack.ActionType type)
{
return literal(actionName)
.executes(manipulation(ap -> ap.start(type, EntityPlayerActionPack.Action.once())))
@@ -140,7 +148,7 @@ public class BotCommand {
.executes(c -> manipulate(c, ap -> ap.start(type, EntityPlayerActionPack.Action.interval(IntegerArgumentType.getInteger(c, "ticks")))))));
}
private static LiteralArgumentBuilder<ServerCommandSource> makeDropCommand(String actionName, boolean dropAll)
private static LiteralArgumentBuilder<CommandSourceStack> makeDropCommand(String actionName, boolean dropAll)
{
return literal(actionName)
.then(literal("all").executes(manipulation(ap -> ap.drop(-2, dropAll))))
@@ -150,36 +158,36 @@ public class BotCommand {
executes(c -> manipulate(c, ap -> ap.drop(IntegerArgumentType.getInteger(c, "slot"), dropAll))));
}
private static Collection<String> getPlayerSuggestions(ServerCommandSource source)
private static Collection<String> getPlayerSuggestions(CommandSourceStack source)
{
Set<String> players = new LinkedHashSet<>(List.of("Steve", "Alex"));
players.addAll(source.getPlayerNames());
players.addAll(List.of(source.getServer().getPlayerNames()));
return players;
}
private static ServerPlayerEntity getPlayer(CommandContext<ServerCommandSource> context)
private static ServerPlayer getPlayer(CommandContext<CommandSourceStack> context)
{
String playerName = StringArgumentType.getString(context, "player");
MinecraftServer server = context.getSource().getServer();
return server.getPlayerManager().getPlayer(playerName);
return server.getPlayerList().getPlayerByName(playerName);
}
private static boolean cantManipulate(CommandContext<ServerCommandSource> context)
private static boolean cantManipulate(CommandContext<CommandSourceStack> context)
{
PlayerEntity player = getPlayer(context);
ServerCommandSource source = context.getSource();
Player player = getPlayer(context);
CommandSourceStack source = context.getSource();
if (player == null)
{
Messenger.m(source, "r Can only manipulate existing players");
return true;
}
PlayerEntity sender = source.getPlayer();
Player sender = source.getPlayer();
if (sender == null)
{
return false;
}
if (!source.getServer().getPlayerManager().isOperator(sender.getGameProfile()))
if (!source.getServer().getPlayerList().isOp(sender.nameAndId()))
{
if (sender != player && !(player instanceof EntityPlayerMPFake))
{
@@ -190,27 +198,28 @@ public class BotCommand {
return false;
}
private static boolean cantReMove(CommandContext<ServerCommandSource> context)
private static boolean cantReMove(CommandContext<CommandSourceStack> context)
{
if (cantManipulate(context)) return true;
PlayerEntity player = getPlayer(context);
Player player = getPlayer(context);
if (player instanceof EntityPlayerMPFake) return false;
Messenger.m(context.getSource(), "r Only fake players can be moved or killed");
return true;
}
private static boolean cantSpawn(CommandContext<ServerCommandSource> context)
private static boolean cantSpawn(CommandContext<CommandSourceStack> context)
{
String playerName = getBotPrefix() + StringArgumentType.getString(context, "player");
MinecraftServer server = context.getSource().getServer();
PlayerManager manager = server.getPlayerManager();
PlayerList manager = server.getPlayerList();
if (manager.getPlayer(playerName) != null)
if (manager.getPlayerByName(playerName) != null)
{
Messenger.m(context.getSource(), "r Player ", "rb " + playerName, "r is already logged on");
return true;
}
GameProfile profile = server.getUserCache().findByName(playerName).orElse(null);
GameProfile profile = server.services().nameToIdCache().get(playerName)
.map(n -> new GameProfile(n.id(), n.name())).orElse(null);
if (profile == null)
{
if (!CarpetSettings.allowSpawningOfflinePlayers)
@@ -219,15 +228,15 @@ public class BotCommand {
"Banned players can only be summoned in Singleplayer and in servers in off-line mode.");
return true;
} else {
profile = new GameProfile(Uuids.getOfflinePlayerUuid(playerName), playerName);
profile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(playerName), playerName);
}
}
if (manager.getUserBanList().contains(profile))
if (manager.getBans().isBanned(new net.minecraft.server.players.NameAndId(profile)))
{
Messenger.m(context.getSource(), "r Player ", "rb " + playerName, "r is banned on this server");
return true;
}
if (manager.isWhitelistEnabled() && manager.isWhitelisted(profile) && !context.getSource().hasPermissionLevel(2))
if (manager.isUsingWhitelist() && manager.isWhiteListed(new net.minecraft.server.players.NameAndId(profile)) && !hasLevel(context.getSource(), 2))
{
Messenger.m(context.getSource(), "r Whitelisted players can only be spawned by operators");
return true;
@@ -239,32 +248,30 @@ public class BotCommand {
return SLSCarpetSettings.botPrefix.equals("#none") ? "" : SLSCarpetSettings.botPrefix;
}
private static int kill(CommandContext<ServerCommandSource> context)
private static int kill(CommandContext<CommandSourceStack> context)
{
if (cantReMove(context)) return 0;
getPlayer(context).kill();
{ var _p = getPlayer(context); _p.kill((ServerLevel) _p.level()); }
return 1;
}
private static int respawn(CommandContext<ServerCommandSource> context) {
private static int respawn(CommandContext<CommandSourceStack> context) {
var player = getPlayer(context);
if (player instanceof EntityPlayerMPFake && ((SLSBotAccessor)player).carpet_SLS_Addition$isBot()) {
((SLSBotAccessor)player).carpet_SLS_Addition$setSpawnTime(System.currentTimeMillis());
context.getSource().sendMessage(
Text.translatable("carpet.slsa.bot.respawned", player.getNameForScoreboard())
.setStyle(
Style.EMPTY.withColor(Formatting.GREEN)
)
context.getSource().sendSuccess(
() -> Component.translatable("carpet.slsa.bot.respawned", player.getScoreboardName())
.setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.GREEN)),
false
);
return Command.SINGLE_SUCCESS;
}
context.getSource().sendMessage(
Text.translatable("carpet.slsa.bot.not_a_bot", player.getNameForScoreboard())
.setStyle(
Style.EMPTY.withColor(Formatting.RED)
)
context.getSource().sendSuccess(
() -> Component.translatable("carpet.slsa.bot.not_a_bot", player.getScoreboardName())
.setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.RED)),
false
);
return 0;
@@ -288,22 +295,22 @@ public class BotCommand {
}
}
private static int spawn(CommandContext<ServerCommandSource> context) throws CommandSyntaxException
private static int spawn(CommandContext<CommandSourceStack> context) throws CommandSyntaxException
{
if (cantSpawn(context)) return 0;
ServerCommandSource source = context.getSource();
Vec3d pos = getArgOrDefault(
() -> Vec3ArgumentType.getVec3(context, "position"),
CommandSourceStack source = context.getSource();
Vec3 pos = getArgOrDefault(
() -> Vec3Argument.getVec3(context, "position"),
source.getPosition()
);
Vec2f facing = getArgOrDefault(
() -> RotationArgumentType.getRotation(context, "direction").toAbsoluteRotation(source),
Vec2 facing = getArgOrDefault(
() -> RotationArgument.getRotation(context, "direction").getRotation(source),
source.getRotation()
);
RegistryKey<World> dimType = getArgOrDefault(
() -> DimensionArgumentType.getDimensionArgument(context, "dimension").getRegistryKey(),
source.getWorld().getRegistryKey()
ResourceKey<Level> dimType = getArgOrDefault(
() -> DimensionArgument.getDimension(context, "dimension").dimension(),
source.getLevel().dimension()
);
String playerName = getBotPrefix() + StringArgumentType.getString(context, "player");
@@ -313,7 +320,7 @@ public class BotCommand {
return 0;
}
if (!World.isValid(BlockPos.ofFloored(pos)))
if (!Level.isInSpawnableBounds(BlockPos.containing(pos)))
{
Messenger.m(source, "rb Player " + playerName + " cannot be placed outside of the world");
return 0;
@@ -328,32 +335,32 @@ public class BotCommand {
return 0;
}
((PlayerAccessor) bot).carpet_SLS_Addition$setDisplayName(Text.empty().append(Text.literal("[%s] ".formatted(source.getName())).setStyle(Style.EMPTY.withColor(Formatting.AQUA))).append(Text.literal(playerName).setStyle(Style.EMPTY)));
((PlayerAccessor) bot).carpet_SLS_Addition$setDisplayName(Component.empty().append(Component.literal("[%s] ".formatted(source.getTextName())).setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.AQUA))).append(Component.literal(playerName).setStyle(Style.EMPTY)));
ServerMain.server.getPlayerManager().broadcast(Text.empty()
.append(Text.literal("假人").setStyle(Style.EMPTY.withColor(Formatting.GREEN)))
.append(Text.literal(playerName).setStyle(Style.EMPTY.withColor(Formatting.GOLD).withBold(true)))
.append(Text.literal("由玩家").setStyle(Style.EMPTY.withColor(Formatting.GREEN)))
ServerMain.server.getPlayerList().broadcastSystemMessage(Component.empty()
.append(Component.literal("假人").setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.GREEN)))
.append(Component.literal(playerName).setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.GOLD).withBold(true)))
.append(Component.literal("由玩家").setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.GREEN)))
.append(source.getDisplayName())
.append(Text.literal("召唤!").setStyle(Style.EMPTY.withColor(Formatting.GREEN))), false);
.append(Component.literal("召唤!").setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.GREEN))), false);
ServerMain.server.getPlayerManager().sendToAll(new PlayerListS2CPacket(
PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME,
ServerMain.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(
ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME,
bot
));
return 1;
}
@SuppressWarnings("unchecked")
private static EntityPlayerMPFake createBot(String username, MinecraftServer server, Vec3d pos, double yaw, double pitch, RegistryKey<World> dimensionId) {
ServerWorld worldIn = server.getWorld(dimensionId);
UserCache.setUseRemote(false);
private static EntityPlayerMPFake createBot(String username, MinecraftServer server, Vec3 pos, double yaw, double pitch, ResourceKey<Level> dimensionId) {
ServerLevel worldIn = server.getLevel(dimensionId);
server.services().nameToIdCache().resolveOfflineUsers(false);
GameProfile gameprofile;
try {
gameprofile = server.getUserCache().findByName(username).orElse(null); //findByName .orElse(null)
}
finally {
UserCache.setUseRemote(server.isDedicated() && server.isOnlineMode());
var nameAndId = server.services().nameToIdCache().get(username).orElse(null);
gameprofile = nameAndId == null ? null : new GameProfile(nameAndId.id(), nameAndId.name());
} finally {
server.services().nameToIdCache().resolveOfflineUsers(server.isDedicatedServer() && server.usesAuthentication());
}
if (gameprofile == null)
{
@@ -361,37 +368,36 @@ public class BotCommand {
{
return null;
} else {
gameprofile = new GameProfile(Uuids.getOfflinePlayerUuid(username), username);
gameprofile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username);
}
}
// 孩子不懂,用反射写着玩的,报错了记得随Carpet一起升级一下
try {
Class<EntityPlayerMPFake> fakePlayerClass = (Class<EntityPlayerMPFake>)Class.forName("carpet.patches.EntityPlayerMPFake");
Constructor<EntityPlayerMPFake> constructor = fakePlayerClass.getDeclaredConstructor(
MinecraftServer.class,
ServerWorld.class,
ServerLevel.class,
GameProfile.class,
SyncedClientOptions.class,
ClientInformation.class,
boolean.class
);
constructor.setAccessible(true);
EntityPlayerMPFake bot = constructor.newInstance(server, worldIn, gameprofile, SyncedClientOptions.createDefault(), false);
EntityPlayerMPFake bot = constructor.newInstance(server, worldIn, gameprofile, ClientInformation.createDefault(), false);
bot.fixStartingPosition = () -> bot.refreshPositionAndAngles(pos.x, pos.y, pos.z, (float) yaw, (float) pitch);
server.getPlayerManager().onPlayerConnect(new FakeClientConnection(NetworkSide.SERVERBOUND), bot, new ConnectedClientData(gameprofile, 0, bot.getClientOptions(), false));
bot.fixStartingPosition = () -> bot.snapTo(pos.x, pos.y, pos.z, (float) yaw, (float) pitch);
server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), bot, new CommonListenerCookie(gameprofile, 0, bot.clientInformation(), false));
bot.teleport(worldIn, pos.x, pos.y, pos.z, (float) yaw, (float) pitch);
bot.teleportTo(worldIn, pos.x, pos.y, pos.z, Set.of(), (float) yaw, (float) pitch, true);
bot.setHealth(20.0F);
bot.unsetRemoved();
bot.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT).setBaseValue(0.6F);
bot.interactionManager.changeGameMode(GameMode.SURVIVAL);
bot.getAttribute(Attributes.STEP_HEIGHT).setBaseValue(0.6F);
bot.gameMode.changeGameModeForPlayer(GameType.SURVIVAL);
server.getPlayerManager().sendToDimension(new EntitySetHeadYawS2CPacket(bot, (byte) (bot.headYaw * 256 / 360)), dimensionId);//bot.dimension);
server.getPlayerManager().sendToDimension(new EntityPositionS2CPacket(bot), dimensionId);
server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(bot, (byte) (bot.yHeadRot * 256 / 360)), dimensionId);
server.getPlayerList().broadcastAll(ClientboundEntityPositionSyncPacket.of(bot), dimensionId);
bot.getDataTracker().set(PlayerEntity.PLAYER_MODEL_PARTS, (byte) 0x7f); // show all model layers (incl. capes)
bot.entityData.set(ServerPlayer.DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); // show all model layers (incl. capes)
bot.getAbilities().flying = false;
((SLSBotAccessor)bot).carpet_SLS_Addition$setBot(true);
@@ -406,19 +412,19 @@ public class BotCommand {
private static int maxNameLength(MinecraftServer server)
{
return server.getServerPort() >= 0 ? SharedConstants.field_49170 : 40;
return server.getPort() >= 0 ? 16 : 40;
}
private static int manipulate(CommandContext<ServerCommandSource> context, Consumer<EntityPlayerActionPack> action)
private static int manipulate(CommandContext<CommandSourceStack> context, Consumer<EntityPlayerActionPack> action)
{
if (cantManipulate(context)) return 0;
ServerPlayerEntity player = getPlayer(context);
ServerPlayer player = getPlayer(context);
action.accept(((ServerPlayerInterface) player).getActionPack());
return 1;
}
private static Command<ServerCommandSource> manipulation(Consumer<EntityPlayerActionPack> action)
private static Command<CommandSourceStack> manipulation(Consumer<EntityPlayerActionPack> action)
{
return c -> manipulate(c, action);
}
}
}
@@ -2,29 +2,31 @@ package com.github.zly2006.carpetslsaddition.command;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
public class HatCommand {
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(CommandManager.literal("hat")
.requires(ServerCommandSource::isExecutedByPlayer)
.requires((commandSource) -> SLSCarpetSettings.canUseHatCommand)
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("hat")
.executes(context -> {
ServerPlayerEntity player = context.getSource().getPlayer();
assert player != null;
ItemStack stack = player.getMainHandStack();
ItemStack head = player.getEquippedStack(EquipmentSlot.HEAD);
player.equipStack(EquipmentSlot.HEAD, stack);
player.getInventory().setStack(player.getInventory().selectedSlot, head);
player.currentScreenHandler.syncState();
if (!SLSCarpetSettings.canUseHatCommand) {
return 0;
}
ServerPlayer player = context.getSource().getPlayer();
if (player == null) {
return 0;
}
ItemStack stack = player.getMainHandItem();
ItemStack head = player.getItemBySlot(EquipmentSlot.HEAD);
player.setItemSlot(EquipmentSlot.HEAD, stack);
player.getInventory().setItem(player.getInventory().getSelectedSlot(), head);
player.containerMenu.sendAllDataToRemote();
return 1;
}
)
})
);
}
}
}
@@ -3,35 +3,38 @@ package com.github.zly2006.carpetslsaddition.command;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.SitEntity;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.world.World;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.decoration.ArmorStand;
public class SitCommand {
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(CommandManager.literal("sit")
.requires(ServerCommandSource::isExecutedByPlayer)
.requires((commandSource) -> SLSCarpetSettings.canUseSitCommand)
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("sit")
.executes(context -> {
ServerPlayerEntity player = context.getSource().getPlayer();
assert player != null;
if (!SLSCarpetSettings.canUseSitCommand) {
return 0;
}
ServerPlayer player = context.getSource().getPlayer();
if (player == null) {
return 0;
}
if (player.getVehicle() != null || !player.isOnGround()) { // 防止错误的坐下行为
if (player.getVehicle() != null || !player.onGround()) { // 防止错误的坐下行为
return 1;
}
World world = player.getWorld();
ServerLevel world = (ServerLevel) player.level();
ArmorStandEntity armorStandEntity = new ArmorStandEntity(world, player.getX(), player.getY(), player.getZ());
ArmorStand armorStandEntity = new ArmorStand(world, player.getX(), player.getY(), player.getZ());
((SitEntity) armorStandEntity).setSitEntity(true);
world.spawnEntity(armorStandEntity);
player.setSneaking(false);
world.addFreshEntity(armorStandEntity);
player.setShiftKeyDown(false);
player.startRiding(armorStandEntity);
return 1;
})
);
}
}
}
@@ -1,15 +1,15 @@
package com.github.zly2006.carpetslsaddition.mixin.block;
import com.google.common.cache.LoadingCache;
import net.minecraft.block.pattern.BlockPattern;
import net.minecraft.block.pattern.CachedBlockPosition;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.pattern.BlockInWorld;
import net.minecraft.world.level.block.state.pattern.BlockPattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(BlockPattern.class)
public interface BlockPatternTestTransformInvoker {
@Invoker("testTransform")
BlockPattern.Result invokeTestTransform(BlockPos frontTopLeft, Direction forwards, Direction up, LoadingCache<BlockPos, CachedBlockPosition> cache);
}
@Invoker("matches")
BlockPattern.BlockPatternMatch invokeTestTransform(BlockPos frontTopLeft, Direction forwards, Direction up, LoadingCache<BlockPos, BlockInWorld> cache);
}
@@ -0,0 +1,39 @@
package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.DecoratedPotBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DecoratedPotBlock.class)
public class MixinDecoratedPotBlock {
// sturdyDecoratedPot:投掷物命中不碎。
@Inject(method = "onProjectileHit", at = @At("HEAD"), cancellable = true)
private void sturdyOnProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile, CallbackInfo ci) {
if (SLSCarpetSettings.sturdyDecoratedPot) {
ci.cancel();
}
}
// unbreakableDecoratedPot:玩家挖掘时不设 CRACKED(无裂纹效果),仍正常挖掉。
@Redirect(
method = "playerWillDestroy",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/block/state/BlockState;setValue(Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;)Ljava/lang/Object;"
)
)
private Object noCrackOnPlayerBreak(BlockState state, net.minecraft.world.level.block.state.properties.Property property, Comparable value) {
if (SLSCarpetSettings.unbreakableDecoratedPot) {
return state;
}
return state.setValue(property, value);
}
}
@@ -1,12 +1,12 @@
package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.dimension.NetherPortal;
import net.minecraft.world.level.portal.PortalShape;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(NetherPortal.class)
@Mixin(PortalShape.class)
public class MixinNetherPortal {
@ModifyConstant(
method = "*",
@@ -16,4 +16,4 @@ public class MixinNetherPortal {
private int portalSize(int original) {
return SLSCarpetSettings.netherPortalSize;
}
}
}
@@ -1,17 +1,23 @@
package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.RedstoneWireBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RedStoneWireBlock;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = RedstoneWireBlock.class, priority = 1)
@Mixin(value = RedStoneWireBlock.class, priority = 1)
public class MixinRedstoneWireBlock {
@Redirect(method = "getRenderConnectionType(Lnet/minecraft/world/BlockView;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;Z)Lnet/minecraft/block/enums/WireConnection;", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;getBlock()Lnet/minecraft/block/Block;"))
// oldRedstoneConnectionLogic:让红石能连到活板门上的红石线。
// 新版在 getConnectingSide 里用 `blockState.getBlock() instanceof TrapDoorBlock` 判断。
// 与旧版一致的做法是把这个 getBlock() 结果换成 AIR(绕过活板门特殊分支,走 canSurviveOn)。
@Redirect(
method = "getConnectingSide(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/Direction;Z)Lnet/minecraft/world/level/block/state/properties/RedstoneSide;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;")
)
public Block redirectConnectionBlockState(BlockState instance) {
if (SLSCarpetSettings.oldRedstoneConnectionLogic) {
return Blocks.AIR;
@@ -20,4 +26,4 @@ public class MixinRedstoneWireBlock {
return instance.getBlock();
}
}
}
}
@@ -3,25 +3,27 @@ package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.block.spawner.TrialSpawnerConfig;
import net.minecraft.block.spawner.TrialSpawnerData;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerConfig;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerStateData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(TrialSpawnerData.class)
@Mixin(TrialSpawnerStateData.class)
public class MixinTrialSpawnerData {
@WrapOperation(
method = "resetAndClearMobs",
method = "reset",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/block/spawner/TrialSpawnerConfig;getCooldownLength()J"
)
target = "Lnet/minecraft/world/level/block/entity/trialspawner/TrialSpawnerConfig;ticksBetweenItemSpawners()J",
ordinal = 0
),
require = 0
)
private long hackCooldown(TrialSpawnerConfig instance, Operation<Integer> original) {
private long hackCooldown(TrialSpawnerConfig instance, Operation<Long> original) {
if (SLSCarpetSettings.trialSpawnerCD < 0) {
return original.call(instance);
} else {
return SLSCarpetSettings.trialSpawnerCD;
}
}
}
}
@@ -3,26 +3,28 @@ package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.block.enums.TrialSpawnerState;
import net.minecraft.block.spawner.TrialSpawnerLogic;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawner;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(TrialSpawnerState.class)
public class MixinTrialSpawnerState {
// trialSpawnerCD:覆盖试炼刷怪笼的冷却长度。
// 新版 TrialSpawnerState.tickAndGetNext 里调用 TrialSpawner.getTargetCooldownLength() 三次,
// 不指定 ordinal 以覆盖全部调用。
@WrapOperation(
method = "tick",
method = "tickAndGetNext",
at = @At(
value = "INVOKE",
ordinal = 0,
target = "Lnet/minecraft/block/spawner/TrialSpawnerLogic;getCooldownLength()I"
target = "Lnet/minecraft/world/level/block/entity/trialspawner/TrialSpawner;getTargetCooldownLength()I"
)
)
private int tick(TrialSpawnerLogic instance, Operation<Integer> original) {
private int tick(TrialSpawner instance, Operation<Integer> original) {
if (SLSCarpetSettings.trialSpawnerCD < 0) {
return original.call(instance);
} else {
return SLSCarpetSettings.trialSpawnerCD;
}
}
}
}
@@ -6,30 +6,26 @@ import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.ServerMain;
import com.github.zly2006.carpetslsaddition.util.access.SLSBotAccessor;
import com.mojang.authlib.GameProfile;
import net.minecraft.network.packet.c2s.common.SyncedClientOptions;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(EntityPlayerMPFake.class)
public abstract class MixinEntityPlayerMPFake extends ServerPlayerEntity implements SLSBotAccessor {
@Shadow public abstract void kill();
public abstract class MixinEntityPlayerMPFake extends ServerPlayer implements SLSBotAccessor {
@Unique
private boolean bot = false;
@Unique
private long spawnTime = -1;
public MixinEntityPlayerMPFake(MinecraftServer server, ServerWorld world, GameProfile profile, SyncedClientOptions clientOptions) {
public MixinEntityPlayerMPFake(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(server, world, profile, clientOptions);
}
@@ -64,15 +60,13 @@ public abstract class MixinEntityPlayerMPFake extends ServerPlayerEntity impleme
long liveTime = currentTime - spawnTime;
if (liveTime > SLSCarpetSettings.botMaxOnlineTime * 1000) {
ServerMain.server.getPlayerManager().broadcast(
Text.literal(Translations.tr("carpet.slsa.bot.bot_timeout").formatted(this.getNameForScoreboard(), getFormattedTime(SLSCarpetSettings.botMaxOnlineTime)))
.setStyle(
Style.EMPTY.withColor(Formatting.RED)
),
ServerMain.server.getPlayerList().broadcastSystemMessage(
Component.literal(Translations.tr("carpet.slsa.bot.bot_timeout").formatted(this.getScoreboardName(), getFormattedTime(SLSCarpetSettings.botMaxOnlineTime)))
.setStyle(Style.EMPTY.withColor(net.minecraft.ChatFormatting.RED)),
false
);
this.kill();
this.kill((net.minecraft.server.level.ServerLevel) this.level());
}
}
@@ -88,4 +82,4 @@ public abstract class MixinEntityPlayerMPFake extends ServerPlayerEntity impleme
return String.format("%02ds", time);
}
}
}
}
@@ -1,27 +1,31 @@
package com.github.zly2006.carpetslsaddition.mixin.carpet;
import carpet.commands.PlayerCommand;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.mojang.authlib.GameProfile;
import net.minecraft.util.UserCache;
import net.minecraft.util.Uuids;
import net.minecraft.core.UUIDUtil;
import net.minecraft.server.players.CachedUserNameToIdResolver;
import net.minecraft.server.players.NameAndId;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Optional;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerCommand.class)
import java.util.Optional;
import java.util.UUID;
/**
* offlineFakePlayers: 让假人查询在离线模式下也能拿到 profile。
*
* TODO(迁移 26.1): 原重定向 carpet PlayerCommand.cantSpawn 里的 UserCache.findByName。
* 新版 Carpet 改用 server.services().nameToIdCache().get(uuid)。
* 此处改为直接注入 CachedUserNameToIdResolver.get(UUID)。
*/
@Mixin(CachedUserNameToIdResolver.class)
public abstract class MixinPlayerCommand {
@Redirect(
method = "cantSpawn",
at = @At(value = "INVOKE", target = "Lnet/minecraft/util/UserCache;findByName(Ljava/lang/String;)Ljava/util/Optional;")
)
private static Optional<GameProfile> redirectFindByName(UserCache instance, String playerName) {
if (SLSCarpetSettings.offlineFakePlayers) {
return Optional.of(Uuids.getOfflinePlayerProfile(playerName));
} else {
return instance.findByName(playerName);
@Inject(method = "get(Ljava/util/UUID;)Ljava/util/Optional;", at = @At("RETURN"), cancellable = true)
private void redirectFindByName(UUID uuid, CallbackInfoReturnable<Optional<NameAndId>> cir) {
if (SLSCarpetSettings.offlineFakePlayers && cir.getReturnValue().isEmpty()) {
// 无法仅凭 uuid 还原名字,offline 场景由 Carpet createFake 处理
}
}
}
}
@@ -1,13 +1,10 @@
package com.github.zly2006.carpetslsaddition.mixin.datapack;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.VanillaResourcePackProvider;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.ServerPacksSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -19,21 +16,17 @@ import java.util.function.Function;
/**
* @author zly2006
*
* TODO(迁移 26.1): 原 VanillaResourcePackProvider.forEachProfile 已不存在。
* 26.1 数据包系统重构为 PackRepository/BuiltInPackSource 体系。
* 此处需改为把 data/slsa/datapacks 作为内置数据包注册。
* 当前暂以 ServerPacksSource 为挂载点占位。
*/
@Mixin(VanillaResourcePackProvider.class)
@Mixin(ServerPacksSource.class)
public abstract class MixinVanillaResourcePackProvider {
@Shadow protected abstract void forEachProfile(@Nullable Path namespacedPath, BiConsumer<String, Function<String, ResourcePackProfile>> consumer);
@Shadow @Final private ResourceType type;
@Inject(
method = "forEachProfile(Ljava/util/function/BiConsumer;)V",
at = @At("RETURN")
)
private void andMe(BiConsumer<String, Function<String, ResourcePackProfile>> consumer, CallbackInfo ci) throws URISyntaxException {
if (type == ResourceType.SERVER_DATA) {
Path path = Path.of(SLSCarpetSettings.class.getClassLoader().getResource("data/slsa/datapacks").toURI());
this.forEachProfile(path, consumer);
}
@Inject(method = "createVanillaPackSource", at = @At("RETURN"), require = 0)
private static void andMe(CallbackInfo ci) throws URISyntaxException {
// TODO: 实现内置 datapack 注册
}
}
}
@@ -0,0 +1,14 @@
package com.github.zly2006.carpetslsaddition.mixin.elytra;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStackTemplate;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(Item.class)
public interface ItemAccessor {
@Mutable
@Accessor("craftingRemainingItem")
void carpet_SLS_Addition$setCraftingRemainingItem(ItemStackTemplate item);
}
@@ -1,28 +0,0 @@
package com.github.zly2006.carpetslsaddition.mixin.elytra;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.item.ElytraItem;
import net.minecraft.item.Equipment;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(ElytraItem.class)
public abstract class MixinElytraItem extends Item implements Equipment {
public MixinElytraItem(Settings settings) {
super(settings);
}
@Override
public boolean hasRecipeRemainder() {
return SLSCarpetSettings.elytraCraftable;
}
@Override
public ItemStack getRecipeRemainder(ItemStack stack) {
if (SLSCarpetSettings.elytraCraftable) {
return stack.copyWithCount(1);
}
else return ItemStack.EMPTY;
}
}
@@ -1,35 +1,29 @@
package com.github.zly2006.carpetslsaddition.mixin.elytra;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.item.Item;
import net.minecraft.item.Items;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStackTemplate;
import net.minecraft.world.item.Items;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@SuppressWarnings("ConstantValue")
/**
* 26.1 起,配方剩余物改为 Item 的 craftingRemainingItem 字段。
* 当 elytraCraftable 开启时,把 ELYTRA 的剩余物设为其自身。
*
* TODO(迁移): 该字段为 final,此处仅在 Item 构造后条件性覆盖。
* 若需运行期动态开关,需改为在配方匹配阶段判断。
*/
@Mixin(Item.class)
public abstract class MixinItem {
@Inject(
method = "hasRecipeRemainder",
at = @At("HEAD"),
cancellable = true
)
public void hasRecipeRemainder(CallbackInfoReturnable<Boolean> cir) {
if (SLSCarpetSettings.elytraCraftable && (Object) this == Items.ELYTRA) {
cir.setReturnValue(true);
@Inject(method = "<init>", at = @At("TAIL"))
private void onInit(CallbackInfo ci) {
Item self = (Item) (Object) this;
if (SLSCarpetSettings.elytraCraftable && self == Items.ELYTRA) {
((ItemAccessor) this).carpet_SLS_Addition$setCraftingRemainingItem(new ItemStackTemplate(self));
}
}
@Inject(
method = "getRecipeRemainder",
at = @At("HEAD"),
cancellable = true
)
public void getRecipeRemainder(CallbackInfoReturnable<Item> cir) {
if (SLSCarpetSettings.elytraCraftable && (Object) this == Items.ELYTRA) {
cir.setReturnValue(Items.ELYTRA);
}
}
}
}
@@ -1,20 +1,20 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.passive.AnimalEntity;
import net.minecraft.entity.passive.ArmadilloEntity;
import net.minecraft.world.World;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.armadillo.Armadillo;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(ArmadilloEntity.class)
public abstract class MixinArmadilloEntity extends AnimalEntity {
protected MixinArmadilloEntity(EntityType<? extends AnimalEntity> entityType, World world) {
@Mixin(Armadillo.class)
public abstract class MixinArmadilloEntity extends Animal {
protected MixinArmadilloEntity(EntityType<? extends Animal> entityType, Level world) {
super(entityType, world);
}
@Override
public boolean canImmediatelyDespawn(double distanceSquared) {
public boolean removeWhenFarAway(double distanceSquared) {
return SLSCarpetSettings.armadilloImmediateDespawns;
}
}
}
@@ -1,24 +1,24 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.util.SitEntity;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.world.World;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ArmorStandEntity.class)
@Mixin(ArmorStand.class)
public abstract class MixinArmorStandEntity extends LivingEntity implements SitEntity {
private boolean sitEntity = false; // 用于标识该盔甲架是否用于玩家乘坐
private boolean sitEntity = false;
protected MixinArmorStandEntity(EntityType<? extends LivingEntity> entityType, World world) {
protected MixinArmorStandEntity(EntityType<? extends LivingEntity> entityType, Level world) {
super(entityType, world);
}
@@ -40,23 +40,23 @@ public abstract class MixinArmorStandEntity extends LivingEntity implements SitE
@Override
protected void removePassenger(Entity passenger) {
if (this.isSitEntity()) {
this.setPosition(this.getX(), this.getY() + 0.16, this.getZ());
this.kill();
this.setPos(this.getX(), this.getY() + 0.16, this.getZ());
this.kill((net.minecraft.server.level.ServerLevel) this.level());
}
super.removePassenger(passenger);
}
@Inject(method = "writeCustomDataToNbt", at = @At(value = "RETURN"))
private void postWriteCustomDataToNbt(NbtCompound nbt, CallbackInfo ci) {
@Inject(method = "addAdditionalSaveData", at = @At(value = "RETURN"))
private void postWriteCustomDataToNbt(ValueOutput nbt, CallbackInfo ci) {
if (this.sitEntity) {
nbt.putBoolean("SitEntity", true);
}
}
@Inject(method = "readCustomDataFromNbt", at = @At(value = "RETURN"))
private void postReadCustomDataFromNbt(NbtCompound nbt, CallbackInfo ci) {
if (nbt.contains("SitEntity", NbtElement.BYTE_TYPE)) {
this.sitEntity = nbt.getBoolean("SitEntity");
@Inject(method = "readAdditionalSaveData", at = @At(value = "RETURN"))
private void postReadCustomDataFromNbt(ValueInput nbt, CallbackInfo ci) {
if (nbt.getBooleanOr("SitEntity", false)) {
this.sitEntity = true;
}
}
}
}
@@ -1,14 +1,14 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.Leashable;
import net.minecraft.entity.vehicle.BoatEntity;
import net.minecraft.world.entity.Leashable;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(BoatEntity.class)
@Mixin(AbstractBoat.class)
public abstract class MixinBoatEntity implements Leashable {
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
@@ -19,6 +19,6 @@ public abstract class MixinBoatEntity implements Leashable {
return;
}
this.detachLeash();
this.dropLeash();
}
}
}
@@ -3,20 +3,19 @@ package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.mixin.block.BlockPatternTestTransformInvoker;
import com.google.common.cache.LoadingCache;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.EndGatewayBlockEntity;
import net.minecraft.block.entity.EndPortalBlockEntity;
import net.minecraft.block.pattern.BlockPattern;
import net.minecraft.block.pattern.CachedBlockPosition;
import net.minecraft.entity.boss.dragon.EnderDragonFight;
import net.minecraft.entity.decoration.EndCrystalEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.Heightmap;
import net.minecraft.world.WorldView;
import net.minecraft.world.chunk.WorldChunk;
import net.minecraft.world.gen.feature.EndPortalFeature;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity;
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
import net.minecraft.world.level.block.state.pattern.BlockInWorld;
import net.minecraft.world.level.block.state.pattern.BlockPattern;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.feature.EndPodiumFeature;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
@@ -25,20 +24,18 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
@Mixin(EnderDragonFight.class)
@Mixin(net.minecraft.world.level.dimension.end.EnderDragonFight.class)
public class MixinEnderDragonFight {
@Shadow
@Final
private ServerWorld world;
private ServerLevel level;
@Shadow
@Final
private BlockPattern endPortalPattern;
private BlockPattern exitPortalPattern;
@Shadow
private BlockPos exitPortalLocation;
@Shadow
private boolean doLegacyCheck;
@Unique
private int defaultChunkX = -8;
@@ -52,21 +49,21 @@ public class MixinEnderDragonFight {
* @reason Optimize the search process of the end portal
*/
@Overwrite
private @Nullable BlockPattern.Result findEndPortal() {
int i,j;
if(!SLSCarpetSettings.optimizedOnDragonRespawn) {
private @Nullable BlockPattern.BlockPatternMatch findExitPortal() {
int i, j;
if (!SLSCarpetSettings.optimizedOnDragonRespawn) {
defaultChunkX = -8;
defaultChunkZ = -8;
}
for(i = defaultChunkX; i <= 8; ++i) {
for(j = defaultChunkZ; j <= 8; ++j) {
WorldChunk worldChunk = this.world.getChunk(i, j);
for(BlockEntity blockEntity : worldChunk.getBlockEntities().values()) {
if(SLSCarpetSettings.optimizedOnDragonRespawn && blockEntity instanceof EndGatewayBlockEntity) continue;
if (blockEntity instanceof EndPortalBlockEntity) {
BlockPattern.Result result = this.endPortalPattern.searchAround(this.world, blockEntity.getPos());
for (i = defaultChunkX; i <= 8; ++i) {
for (j = defaultChunkZ; j <= 8; ++j) {
LevelChunk worldChunk = this.level.getChunk(i, j);
for (BlockEntity blockEntity : worldChunk.getBlockEntities().values()) {
if (SLSCarpetSettings.optimizedOnDragonRespawn && blockEntity instanceof TheEndGatewayBlockEntity) continue;
if (blockEntity instanceof TheEndPortalBlockEntity) {
BlockPattern.BlockPatternMatch result = this.exitPortalPattern.find(this.level, blockEntity.getBlockPos());
if (result != null) {
BlockPos blockPos = result.translate(3, 3, 3).getBlockPos();
BlockPos blockPos = result.getFrontTopLeft().offset(3, 3, 3);
if (this.exitPortalLocation == null) {
this.exitPortalLocation = blockPos;
}
@@ -78,25 +75,25 @@ public class MixinEnderDragonFight {
}
}
}
if(this.doLegacyCheck || this.exitPortalLocation == null){
if(SLSCarpetSettings.optimizedOnDragonRespawn && defaultOriginY != -1) {
if (this.exitPortalLocation == null) {
if (SLSCarpetSettings.optimizedOnDragonRespawn && defaultOriginY != -1) {
i = defaultOriginY;
}
else {
i = this.world.getTopPosition(Heightmap.Type.MOTION_BLOCKING, EndPortalFeature.offsetOrigin(BlockPos.ORIGIN)).getY();
} else {
i = this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(BlockPos.ZERO).getX(), EndPodiumFeature.getLocation(BlockPos.ZERO).getZ());
}
boolean notFirstSearch = false;
for(j = i; j >= 0; --j) {
BlockPattern.Result result2;
if(SLSCarpetSettings.optimizedOnDragonRespawn && notFirstSearch) {
result2 = partialSearchAround(this.endPortalPattern, this.world, new BlockPos(EndPortalFeature.offsetOrigin(BlockPos.ORIGIN).getX(), j, EndPortalFeature.offsetOrigin(BlockPos.ORIGIN).getZ()));
}
else{
result2 = this.endPortalPattern.searchAround(this.world, new BlockPos(EndPortalFeature.offsetOrigin(BlockPos.ORIGIN).getX(), j, EndPortalFeature.offsetOrigin(BlockPos.ORIGIN).getZ()));
for (j = i; j >= 0; --j) {
BlockPattern.BlockPatternMatch result2;
BlockPos origin = EndPodiumFeature.getLocation(BlockPos.ZERO);
BlockPos searchPos = new BlockPos(origin.getX(), j, origin.getZ());
if (SLSCarpetSettings.optimizedOnDragonRespawn && notFirstSearch) {
result2 = partialSearchAround(this.exitPortalPattern, this.level, searchPos);
} else {
result2 = this.exitPortalPattern.find(this.level, searchPos);
}
if (result2 != null) {
if (this.exitPortalLocation == null) {
this.exitPortalLocation = result2.translate(3, 3, 3).getBlockPos();
this.exitPortalLocation = result2.getFrontTopLeft().offset(3, 3, 3);
}
defaultOriginY = j;
return result2;
@@ -108,26 +105,26 @@ public class MixinEnderDragonFight {
return null;
}
@Inject(method = "respawnDragon(Ljava/util/List;)V", at = @At("HEAD"))
private void resetCache(List<EndCrystalEntity> crystals, CallbackInfo ci) {
@Inject(method = "respawnDragon", at = @At("HEAD"))
private void resetCache(List<EndCrystal> crystals, CallbackInfo ci) {
this.defaultChunkX = -8;
this.defaultChunkZ = -8;
this.defaultOriginY = -1;
}
@Unique
private BlockPattern.Result partialSearchAround(BlockPattern pattern, WorldView world, BlockPos pos) {
LoadingCache<BlockPos, CachedBlockPosition> loadingCache = BlockPattern.makeCache(world, false);
private BlockPattern.BlockPatternMatch partialSearchAround(BlockPattern pattern, LevelReader world, BlockPos pos) {
LoadingCache<BlockPos, BlockInWorld> loadingCache = BlockPattern.createLevelCache(world, false);
int i = Math.max(Math.max(pattern.getWidth(), pattern.getHeight()), pattern.getDepth());
for (BlockPos blockPos : BlockPos.iterate(pos, pos.add(i - 1, 0, i - 1))) {
for (BlockPos blockPos : BlockPos.betweenClosed(pos, pos.offset(i - 1, 0, i - 1))) {
for (Direction direction : Direction.values()) {
for (Direction direction2 : Direction.values()) {
BlockPattern.Result result;
if (direction2 == direction || direction2 == direction.getOpposite() || (result = ((BlockPatternTestTransformInvoker)pattern).invokeTestTransform(blockPos, direction, direction2, loadingCache)) == null) continue;
BlockPattern.BlockPatternMatch result;
if (direction2 == direction || direction2 == direction.getOpposite() || (result = ((BlockPatternTestTransformInvoker) pattern).invokeTestTransform(blockPos, direction, direction2, loadingCache)) == null) continue;
return result;
}
}
}
return null;
}
}
}
@@ -1,24 +1,23 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.MushroomPlantBlock;
import net.minecraft.entity.mob.EndermanEntity;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.MushroomBlock;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(EndermanEntity.PickUpBlockGoal.class)
@Mixin(targets = "net.minecraft.world.entity.monster.EnderMan$EndermanTakeBlockGoal")
public class MixinEndermanGoal {
@Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;isIn(Lnet/minecraft/registry/tag/TagKey;)Z"))
@Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;is(Lnet/minecraft/tags/TagKey;)Z"))
public boolean isHoldable(BlockState blockState, TagKey<Block> tagKey) {
if (!SLSCarpetSettings.endermanCanPickUpMushroom) {
return blockState.isIn(tagKey) && !(blockState.getBlock() instanceof MushroomPlantBlock);
return blockState.is(tagKey) && !(blockState.getBlock() instanceof MushroomBlock);
}
else {
return blockState.isIn(tagKey);
return blockState.is(tagKey);
}
}
}
}
@@ -1,20 +1,20 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.block.BlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.FallingBlockEntity;
import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.tag.BlockTags;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@@ -26,32 +26,31 @@ import java.util.function.Predicate;
@Mixin(FallingBlockEntity.class)
public abstract class MixinFallingBlockEntity extends Entity {
@Shadow private BlockState block;
@Shadow private boolean destroyedOnLanding;
@Shadow private BlockState blockState;
@Shadow public abstract BlockPos getFallingBlockPos();
@Shadow public abstract BlockPos getStartPos();
public MixinFallingBlockEntity(EntityType<?> type, World world) {
public MixinFallingBlockEntity(EntityType<?> type, Level world) {
super(type, world);
}
@Inject(method = "handleFallDamage", at = @At(value = "TAIL"))
private void handleFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource, CallbackInfoReturnable<Boolean> cir) {
if (!SLSCarpetSettings.obtainableReinforcedDeepSlate || !this.block.isIn(BlockTags.ANVIL)) {
@Inject(method = "causeFallDamage", at = @At(value = "TAIL"))
private void causeFallDamage(double fallDistance, float damageMultiplier, DamageSource damageSource, CallbackInfoReturnable<Boolean> cir) {
if (!SLSCarpetSettings.obtainableReinforcedDeepSlate || !this.blockState.is(BlockTags.ANVIL)) {
return;
}
Predicate<Entity> deepSlatePredicated = (entity) -> {
if (entity instanceof ItemEntity itemEntity) {
var itemStack = itemEntity.getStack();
return itemStack.isOf(Items.DEEPSLATE) && itemStack.getCount() >= 64;
var itemStack = itemEntity.getItem();
return itemStack.is(Items.DEEPSLATE) && itemStack.getCount() >= 64;
}
return false;
};
var world = this.getWorld();
var itemEntitiesList = world.getOtherEntities(this, this.getBoundingBox(), deepSlatePredicated);
var world = this.level();
var itemEntitiesList = world.getEntities(this, this.getBoundingBox(), deepSlatePredicated);
if (itemEntitiesList.size() < 9) {
return;
}
@@ -62,16 +61,13 @@ public abstract class MixinFallingBlockEntity extends Entity {
itemEntitiesList.get(i).discard();
}
this.destroyedOnLanding = true;
world.playSoundAtBlockCenter(this.getBlockPos(), SoundEvents.BLOCK_ANVIL_USE, SoundCategory.BLOCKS, 1.0F, 1.0F, false);
world.playSound(null, this.blockPosition(), SoundEvents.ANVIL_USE, SoundSource.BLOCKS, 1.0F, 1.0F);
var coreEntity = new ItemEntity(world, entity.getX(), entity.getY(), entity.getZ(), new ItemStack(Items.REINFORCED_DEEPSLATE, 1));
coreEntity.setPickupDelay(40);
coreEntity.setPickUpDelay(40);
float f = this.random.nextFloat() * 0.5F;
float g = this.random.nextFloat() * (float) (Math.PI * 2);
coreEntity.setVelocity(-MathHelper.sin(g) * f, 0.2F, MathHelper.cos(g) * f);
world.spawnEntity(coreEntity);
coreEntity.setDeltaMovement(-Mth.sin(g) * f, 0.2F, Mth.cos(g) * f);
world.addFreshEntity(coreEntity);
}
}
}
@@ -1,18 +1,15 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.Leashable;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.world.entity.Leashable;
import net.minecraft.world.entity.Mob;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MobEntity.class)
@Mixin(Mob.class)
public abstract class MixinMobEntity implements Leashable {
@Shadow public abstract void detachLeash();
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
if (!SLSCarpetSettings.spectatorCannotUseLeash ||
@@ -22,6 +19,6 @@ public abstract class MixinMobEntity implements Leashable {
return;
}
this.detachLeash();
this.dropLeash();
}
}
}
@@ -1,28 +1,28 @@
package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.util.access.PlayerAccessor;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerEntity.class)
@Mixin(Player.class)
public abstract class MixinPlayerEntity implements PlayerAccessor {
@Unique
private Text displayName = null;
private Component displayName = null;
@Override
public void carpet_SLS_Addition$setDisplayName(Text name) {
public void carpet_SLS_Addition$setDisplayName(Component name) {
this.displayName = name;
}
@Inject(method = "getDisplayName", at = @At("HEAD"), cancellable = true)
public void injectGetDisplayName(CallbackInfoReturnable<Text> cir) {
public void injectGetDisplayName(CallbackInfoReturnable<Component> cir) {
if (this.displayName != null) {
cir.setReturnValue(displayName);
}
}
}
}
@@ -3,38 +3,35 @@ package com.github.zly2006.carpetslsaddition.mixin.entity;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.SitEntity;
import com.mojang.authlib.GameProfile;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@Mixin(ServerPlayerEntity.class)
public abstract class MixinServerPlayerEntity extends PlayerEntity {
@Mixin(ServerPlayer.class)
public abstract class MixinServerPlayerEntity extends Player {
@Shadow
public ServerPlayNetworkHandler networkHandler;
public ServerGamePacketListenerImpl connection;
@Shadow public abstract boolean isSpectator();
@Unique
private int sneakTimes = 0;
@Unique
private long lastSneakTime = 0;
public MixinServerPlayerEntity(World world, BlockPos pos, float yaw, GameProfile gameProfile) {
super(world, pos, yaw, gameProfile);
public MixinServerPlayerEntity(net.minecraft.world.level.Level world, net.minecraft.core.BlockPos pos, float yaw, GameProfile gameProfile) {
super(world, gameProfile);
}
@Override
public void setSneaking(boolean sneaking){
if (!SLSCarpetSettings.playerSit || (sneaking && this.isSneaking())) {
super.setSneaking(sneaking);
public void setShiftKeyDown(boolean sneaking){
if (!SLSCarpetSettings.playerSit || (sneaking && this.isShiftKeyDown())) {
super.setShiftKeyDown(sneaking);
return;
}
@@ -43,14 +40,14 @@ public abstract class MixinServerPlayerEntity extends PlayerEntity {
if (nowTime - lastSneakTime < 400 && sneakTimes == 0) {
return;
}
super.setSneaking(true);
if (this.isOnGround() && nowTime - lastSneakTime < 400) {
super.setShiftKeyDown(true);
if (this.onGround() && nowTime - lastSneakTime < 400) {
sneakTimes += 1;
if (sneakTimes == 3) {
ArmorStandEntity armorStandEntity = new ArmorStandEntity(getWorld(), this.getX(), this.getY() - 0.16, this.getZ());
ArmorStand armorStandEntity = new ArmorStand(this.level(), this.getX(), this.getY() - 0.16, this.getZ());
((SitEntity) armorStandEntity).setSitEntity(true);
getWorld().spawnEntity(armorStandEntity);
this.setSneaking(false);
this.level().addFreshEntity(armorStandEntity);
this.setShiftKeyDown(false);
this.startRiding(armorStandEntity);
sneakTimes = 0;
}
@@ -59,12 +56,14 @@ public abstract class MixinServerPlayerEntity extends PlayerEntity {
}
lastSneakTime = nowTime;
} else {
super.setSneaking(false);
super.setShiftKeyDown(false);
// 同步潜行状态到客户端
// 如果不同步的话客户端会认为仍在潜行,从而碰撞箱的高度会计算错误
if (sneakTimes == 0 && this.networkHandler != null) {
this.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(this.getId(), this.getDataTracker().getChangedEntries()));
if (sneakTimes == 0 && this.connection != null) {
java.util.List<net.minecraft.network.syncher.SynchedEntityData.DataValue<?>> dirty = this.getEntityData().packDirty();
if (dirty != null) {
this.connection.send(new ClientboundSetEntityDataPacket(this.getId(), dirty));
}
}
}
}
}
}
@@ -0,0 +1,56 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* [旧版] 区块替换(chunk swap)——race condition 的等价实现。
*
* <p>1.12 的漏洞:{@code ChunkProviderServer.id2ChunkMap}(非线程安全的
* {@code Long2ObjectOpenHashMap})被主线程 {@code remove}(卸载)与异步线程 {@code get}
* 并发访问,导致 {@code get} 错过已加载的区块,进而触发"重新加载 + 世界装饰"。
*
* <p>26.1.2 已用 {@code Long2ObjectLinkedOpenHashMap} + {@code mainThreadExecutor} 约束修复。
* 直接并发改 map 风险极高(可能崩溃/损坏存档),故本 mixin 采用<b>低风险等价方案</b>:
* hook {@link ChunkMap#getVisibleChunkIfPresent(long)},当满足以下条件时对<b>非主线程</b>调用返回 {@code null}:
* <ul>
* <li>{@code legacyExploitMode} + {@code legacyChunkSwap} 均开启</li>
* <li>当前不是服务器主线程(即异步线程在访问)</li>
* </ul>
* 这样异步线程会"误以为区块未加载",后续逻辑会尝试重新加载并触发世界装饰,
* 从而在异步线程上产生方块更新——与 1.12 的效果等价,但不触碰共享数据结构。
*
* <p><b>⚠ 风险</b>:仍可能引起区块状态不一致。仅在明确开启时生效。
*/
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin {
@Inject(method = "getVisibleChunkIfPresent", at = @At("RETURN"), cancellable = true)
private void legacyChunkSwap(long chunkPos, CallbackInfoReturnable<ChunkHolder> cir) {
if (cir.getReturnValue() == null) {
return;
}
if (!SLSCarpetSettings.legacyExploitMode || !SLSCarpetSettings.legacyChunkSwap) {
return;
}
// 仅在异步线程访问时伪装"区块未加载"
if (!isServerMainThread()) {
cir.setReturnValue(null);
}
}
/** 判断当前是否在服务器主线程上。 */
private static boolean isServerMainThread() {
net.minecraft.server.MinecraftServer server = ServerMain.server;
if (server == null) {
return true;
}
return server.isSameThread();
}
}
@@ -0,0 +1,46 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
/**
* [旧版] 落沙替换——固定目标版。
*
* <p>26.1.2 单线程下 1.12 的"替换窗口"物理不存在,本 mixin 采用直接方案:
* 当 {@code legacyFallingBlockReplace} 开启且配置了 {@code legacyFallingBlockTarget} 时,
* 把下落实体的方块状态替换为目标方块的默认状态——沙子下落即变目标方块。
*/
@Mixin(net.minecraft.world.entity.item.FallingBlockEntity.class)
public class FallingBlockEntityMixin {
@ModifyVariable(
method = "fall",
at = @At("HEAD"),
argsOnly = true,
index = 2
)
private static BlockState legacyReplaceFallingState(BlockState original, Level level, BlockPos pos, BlockState state) {
if (!SLSCarpetSettings.legacyExploitMode || !SLSCarpetSettings.legacyFallingBlockReplace) {
return original;
}
String target = SLSCarpetSettings.legacyFallingBlockTarget;
if (target == null || target.isEmpty() || "#none".equals(target)) {
return original;
}
Block block = BuiltInRegistries.BLOCK.getOptional(Identifier.withDefaultNamespace(target)).orElse(null);
if (block == null) {
return original;
}
ServerMain.LOGGER.info("[SLSA-DEBUG] >>> REPLACE at {}: {} -> {}", pos, original.getBlock(), block);
return block.defaultBlockState();
}
}
@@ -0,0 +1,78 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.LegacyExploitState;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.FallingBlock;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* [旧版] 瞬间下落(等价 1.12 的 instantFall flag)。
*
* <p>1.12:世界装饰时短暂开启 instantFall,使重力方块在被更新时【立即】转为下落实体,
* 而不等待 2gt 的计划刻延迟。26.1.2 已移除该 flag。
*
* <p>实现方式(双保险):
* <ul>
* <li>{@link #legacyDelayAfterPlace}:把 {@code getDelayAfterPlace()} 的返回值改为 0。</li>
* <li>{@link #legacyScheduleDelay}:直接拦截 {@code onPlace}/{@code updateShape} 里
* 对 {@code scheduleTick} 的 delay 参数,强制改为 0(更直接可靠)。</li>
* </ul>
*/
@Mixin(FallingBlock.class)
public abstract class FallingBlockMixin {
/** instantFall:把"放置后延迟"改为 0。 */
@Inject(method = "getDelayAfterPlace", at = @At("HEAD"), cancellable = true)
private void legacyDelayAfterPlace(CallbackInfoReturnable<Integer> cir) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
cir.setReturnValue(0);
}
}
/**
* instantFall:拦截 onPlace / updateShape 里 scheduleTick 的 delay 参数,强制为 0。
* 第 3 个参数(index=2)是延迟 tick 数。
*/
@ModifyArg(
method = "onPlace",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/Level;scheduleTick(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;I)V"),
index = 2
)
private int legacyScheduleDelayLevel(int delay) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
return 0;
}
return delay;
}
@ModifyArg(
method = "updateShape",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/ScheduledTickAccess;scheduleTick(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;I)V"),
index = 2
)
private int legacyScheduleDelayAccess(int delay) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
return 0;
}
return delay;
}
/** instantFall:tick 入口标记(供其他机制判断窗口)。 */
@Inject(method = "tick", at = @At("HEAD"))
private void legacyTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource rand, CallbackInfo ci) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
LegacyExploitState.markInstantFall(pos);
}
}
}
@@ -0,0 +1,32 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.ticks.ScheduledTick;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* [旧版] 计划刻立即执行(等价 1.12 的 instantTick flag)。
*
* <p>1.12 的 instantTick:世界装饰时会短暂开启,使计划刻(中继器、发射器、沙子等)
* 立即执行而不等待延迟。26.1.2 已移除该 flag。
*
* <p>本 mixin 用 {@link Inject} 拦截 {@link ScheduledTick#triggerTick()}:
* 规则开启时返回 0,使该计划刻在"当前时刻"被触发(等价立即执行)。
*
* <p><b>注意</b>:这会强制所有计划刻立即触发,可能造成大量连锁更新,
* 仅应在 {@code legacyExploitMode} + {@code legacyInstantTick} 同时开启时使用。
*/
@Mixin(ScheduledTick.class)
public abstract class ScheduledTickMixin {
@Inject(method = "triggerTick", at = @At("HEAD"), cancellable = true)
private void legacyInstantTriggerTick(CallbackInfoReturnable<Long> cir) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantTick) {
// 返回 0 表示"在当前时刻立即触发"
cir.setReturnValue(0L);
}
}
}
@@ -1,27 +1,27 @@
package com.github.zly2006.carpetslsaddition.mixin.player;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.util.Nameable;
import net.minecraft.world.Container;
import net.minecraft.world.Nameable;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(PlayerInventory.class)
public abstract class MixinPlayerInventory implements Inventory, Nameable {
@Mixin(Inventory.class)
public abstract class MixinPlayerInventory implements Container, Nameable {
// creativeNoInfinitePickup:创造模式下满背包不能拾取。
// 新版 Inventory.add 在满背包 + hasInfiniteMaterials 时把数量清 0 并返回 true。
// 规则开启时改为 false,避免"假拾取"。
@Redirect(
method = "insertStack(ILnet/minecraft/item/ItemStack;)Z",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/entity/player/PlayerEntity;isInCreativeMode()Z"
)
method = "add(ILnet/minecraft/world/item/ItemStack;)Z",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;hasInfiniteMaterials()Z")
)
private boolean insertStack(PlayerEntity instance) {
private boolean insertStack(Player instance) {
if (SLSCarpetSettings.creativeNoInfinitePickup) {
return false;
}
return instance.isInCreativeMode();
return instance.hasInfiniteMaterials();
}
}
}
@@ -1,29 +1,30 @@
package com.github.zly2006.carpetslsaddition.mixin.screen;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.player.PlayerAbilities;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.screen.AnvilScreenHandler;
import net.minecraft.world.entity.player.Abilities;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AnvilMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(AnvilScreenHandler.class)
@Mixin(AnvilMenu.class)
public class MixinAnvilScreenHandler {
@Redirect(
method = "updateResult",
method = "createResult",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/entity/player/PlayerEntity;getAbilities()Lnet/minecraft/entity/player/PlayerAbilities;"
)
target = "Lnet/minecraft/world/entity/player/Player;getAbilities()Lnet/minecraft/world/entity/player/Abilities;"
),
require = 0
)
private PlayerAbilities updateResult(PlayerEntity instance) {
private Abilities updateResult(Player instance) {
if (SLSCarpetSettings.creativeObeyEnchantmentRule) {
var ability = new PlayerAbilities();
ability.creativeMode = false;
var ability = new Abilities();
ability.instabuild = false;
return ability;
}
return instance.getAbilities();
}
}
}
@@ -1,7 +1,7 @@
package com.github.zly2006.carpetslsaddition.mixin.shulker;
import com.github.zly2006.carpetslsaddition.util.ShulkerBoxItemUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -10,14 +10,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ItemStack.class)
public class MixinItemStack {
@Inject(
method = "capCount",
method = "limitSize",
at = @At("HEAD"),
cancellable = true
)
private void capCount(int maxCount, CallbackInfo ci) {
if (ShulkerBoxItemUtil.isEmptyShulkerBoxItem((ItemStack) (Object) this)) {
ci.cancel();
}
}
}
}
@@ -2,48 +2,25 @@ package com.github.zly2006.carpetslsaddition.mixin.shulker;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.ShulkerBoxItemUtil;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.world.item.ItemInstance;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerInventory.class)
public abstract class MixinPlayerInventory implements Inventory {
@Redirect(
method = "canStackAddMore",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;isStackable()Z"
)
)
private boolean isStackable(ItemStack instance) {
if (SLSCarpetSettings.emptyShulkerBoxStack && ShulkerBoxItemUtil.isEmptyShulkerBoxItem(instance)) {
return true;
/**
* 空潜影盒可堆叠(上限 64)。
* 26.1 起堆叠上限统一由 ItemInstance.getMaxStackSize 决定,参考 Carpet 实现。
*/
@Mixin(ItemInstance.class)
public interface MixinPlayerInventory {
@Inject(method = "getMaxStackSize", at = @At("HEAD"), cancellable = true)
private void getMaxStackSize(CallbackInfoReturnable<Integer> cir) {
if (SLSCarpetSettings.emptyShulkerBoxStack
&& ((ItemInstance) (Object) this) instanceof ItemStack itemStack
&& ShulkerBoxItemUtil.isEmptyShulkerBoxItem(itemStack)) {
cir.setReturnValue(ShulkerBoxItemUtil.SHULKERBOX_MAX_STACK_AMOUNT);
}
return instance.isStackable();
}
@Redirect(
method = "offer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;getMaxCount()I"
)
)
private int redirectMaxCount(ItemStack stack) {
if (SLSCarpetSettings.emptyShulkerBoxStack && ShulkerBoxItemUtil.isEmptyShulkerBoxItem(stack)) {
return ShulkerBoxItemUtil.SHULKERBOX_MAX_STACK_AMOUNT;
}
return Inventory.super.getMaxCount(stack);
}
@Override
public int getMaxCount(ItemStack stack) {
if (SLSCarpetSettings.emptyShulkerBoxStack && ShulkerBoxItemUtil.isEmptyShulkerBoxItem(stack)) {
return ShulkerBoxItemUtil.SHULKERBOX_MAX_STACK_AMOUNT;
}
return Inventory.super.getMaxCount(stack);
}
}
}
@@ -1,35 +1,37 @@
package com.github.zly2006.carpetslsaddition.mixin.shulker;
import com.github.zly2006.carpetslsaddition.util.ShulkerBoxItemUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ScreenHandler.class)
@Mixin(AbstractContainerMenu.class)
public abstract class MixinScreenHandler {
@Redirect(
method = "*",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;areItemsAndComponentsEqual(Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ItemStack;)Z"
)
target = "Lnet/minecraft/world/item/ItemStack;isSameItemSameComponents(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z"
),
require = 0
)
private boolean checkEqual(ItemStack stack, ItemStack otherStack) {
if (ShulkerBoxItemUtil.shulkerBoxEqual(stack, otherStack)) return true;
return ItemStack.areItemsAndComponentsEqual(stack, otherStack);
return ItemStack.isSameItemSameComponents(stack, otherStack);
}
@Redirect(
method = "*",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;isStackable()Z"
)
target = "Lnet/minecraft/world/item/ItemStack;isStackable()Z"
),
require = 0
)
private boolean isStackable(ItemStack stack) {
if (ShulkerBoxItemUtil.isEmptyShulkerBoxItem(stack)) return true;
return stack.isStackable();
}
}
}
@@ -2,25 +2,26 @@ package com.github.zly2006.carpetslsaddition.mixin.shulker;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.ShulkerBoxItemUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ServerPlayNetworkHandler.class)
@Mixin(ServerGamePacketListenerImpl.class)
public class MixinServerPlayNetworkHandler {
@Redirect(
method = "onCreativeInventoryAction",
method = "handleSetCreativeModeSlot",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;getMaxCount()I"
)
target = "Lnet/minecraft/world/item/ItemStack;getMaxStackSize()I"
),
require = 0
)
private int getMaxCount(ItemStack instance) {
if (SLSCarpetSettings.emptyShulkerBoxStack && ShulkerBoxItemUtil.isEmptyShulkerBoxItem(instance)) {
return ShulkerBoxItemUtil.SHULKERBOX_MAX_STACK_AMOUNT;
}
return instance.getMaxCount();
return instance.getMaxStackSize();
}
}
}
@@ -2,8 +2,8 @@ package com.github.zly2006.carpetslsaddition.mixin.shulker;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.ShulkerBoxItemUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@@ -11,10 +11,10 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(Slot.class)
public class MixinSlot {
@Redirect(
method = "getMaxItemCount(Lnet/minecraft/item/ItemStack;)I",
method = "getMaxStackSize(Lnet/minecraft/world/item/ItemStack;)I",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/item/ItemStack;getMaxCount()I",
target = "Lnet/minecraft/world/item/ItemStack;getMaxStackSize()I",
ordinal = 0
)
)
@@ -22,7 +22,7 @@ public class MixinSlot {
if (SLSCarpetSettings.emptyShulkerBoxStack && ShulkerBoxItemUtil.isEmptyShulkerBoxItem(itemStack)) {
return ShulkerBoxItemUtil.SHULKERBOX_MAX_STACK_AMOUNT;
} else {
return itemStack.getMaxCount();
return itemStack.getMaxStackSize();
}
}
}
}
@@ -0,0 +1,11 @@
package com.github.zly2006.carpetslsaddition.mixin.turtle;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PathNavigation.class)
public interface PathNavigationAccessor {
@Accessor("maxVisitedNodesMultiplier")
float turtleReproduce$getMaxVisitedNodesMultiplier();
}
@@ -0,0 +1,22 @@
package com.github.zly2006.carpetslsaddition.mixin.turtle;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.animal.turtle.Turtle;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Turtle.class)
public interface TurtleAccessor {
@Invoker("setHasEgg")
void turtleReproduce$setHasEgg(boolean hasEgg);
@Invoker("setLayingEgg")
void turtleReproduce$setLayingEgg(boolean layingEgg);
@Accessor("homePos")
BlockPos turtleReproduce$getHomePos();
@Accessor("goingHome")
boolean turtleReproduce$isGoingHome();
}
@@ -0,0 +1,33 @@
package com.github.zly2006.carpetslsaddition.mixin.turtle;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.entity.animal.turtle.Turtle;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleGoHomeGoal")
public abstract class TurtleGoHomeGoalMixin {
@Shadow @Final
private Turtle turtle;
// This goal has no MOVE flag, so priority alone cannot stop it fighting nesting.
@Inject(method = {"canUse", "canContinueToUse"}, at = @At("HEAD"), cancellable = true)
private void turtleReproduce$stayLocalWhileCarryingEggs(CallbackInfoReturnable<Boolean> cir) {
if (SLSCarpetSettings.turtleLocalNesting && this.turtle.hasEgg()) {
cir.setReturnValue(false);
}
}
@Inject(method = "stop", at = @At("TAIL"))
private void turtleReproduce$discardOldHomeRoute(CallbackInfo ci) {
if (SLSCarpetSettings.turtleLocalNesting && this.turtle.hasEgg()) {
// The vanilla stop only clears goingHome, leaving its last route active.
this.turtle.getNavigation().stop();
}
}
}
@@ -0,0 +1,142 @@
package com.github.zly2006.carpetslsaddition.mixin.turtle;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.turtle.NestingSiteFinder;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.MoveToBlockGoal;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.animal.turtle.Turtle;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.pathfinder.Path;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleLayEggGoal")
public abstract class TurtleLayEggGoalMixin extends MoveToBlockGoal {
@Unique private static final int PATH_SEARCH_RANGE = 48;
@Unique private static final int PATH_ATTEMPTS_PER_CHECK = 16;
@Shadow @Final
private Turtle turtle;
@Unique private List<NestingSiteFinder.Candidate> turtleReproduce$candidates = List.of();
@Unique private int turtleReproduce$candidateIndex;
@Unique private BlockPos turtleReproduce$searchOrigin = BlockPos.ZERO;
@Unique private Path turtleReproduce$nestingPath;
protected TurtleLayEggGoalMixin(PathfinderMob mob, double speed, int range) {
super(mob, speed, range);
}
@Inject(method = "canUse", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$findLocalShore(CallbackInfoReturnable<Boolean> cir) {
if (!SLSCarpetSettings.turtleLocalNesting) return;
cir.setReturnValue(this.turtle.hasEgg() && !this.turtle.isBaby() && super.canUse());
}
@Inject(method = "canContinueToUse", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$continueAwayFromHome(CallbackInfoReturnable<Boolean> cir) {
if (!SLSCarpetSettings.turtleLocalNesting) return;
cir.setReturnValue(this.turtle.hasEgg() && super.canContinueToUse());
}
@Inject(method = "isValidTarget", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$requireDrySandNearWater(LevelReader level, BlockPos pos,
CallbackInfoReturnable<Boolean> cir) {
if (!SLSCarpetSettings.turtleLocalNesting) return;
cir.setReturnValue(NestingSiteFinder.isValidSite(level, pos));
}
@Override
protected boolean findNearestBlock() {
if (!SLSCarpetSettings.turtleLocalNesting) {
return super.findNearestBlock();
}
if (this.turtleReproduce$candidateIndex >= this.turtleReproduce$candidates.size()
|| this.turtle.blockPosition().distSqr(this.turtleReproduce$searchOrigin) > 16.0) {
this.turtleReproduce$candidates = NestingSiteFinder.findCandidates(this.turtle);
this.turtleReproduce$candidateIndex = 0;
this.turtleReproduce$searchOrigin = this.turtle.blockPosition();
}
// Spread expensive path checks over AI updates when a shore is inaccessible.
int attempts = 0;
while (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()
&& attempts++ < PATH_ATTEMPTS_PER_CHECK) {
BlockPos sand = this.turtleReproduce$candidates.get(this.turtleReproduce$candidateIndex++).sandPos();
if (!this.turtle.isWithinHome(sand) || !NestingSiteFinder.isValidSite(this.turtle.level(), sand)) {
continue;
}
Vec3 destination = Vec3.atBottomCenterOf(sand.above());
AABB destinationBounds = this.turtle.getBoundingBox().move(destination.subtract(this.turtle.position()));
// Adult turtles are wider than one block. An air block alone does not
// guarantee that their body is clear of adjacent water or walls.
if (!this.turtle.level().noCollision(this.turtle, destinationBounds)
|| this.turtle.level().containsAnyLiquid(destinationBounds.deflate(0.001))) {
continue;
}
Path path = this.turtleReproduce$createNestingPath(sand.above());
if (path != null && path.canReach()) {
this.blockPos = sand;
this.turtleReproduce$nestingPath = path;
this.turtleReproduce$candidates = List.of();
return true;
}
}
if (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()) {
this.nextStartTick = 1;
}
return false;
}
@Unique
private Path turtleReproduce$createNestingPath(BlockPos destination) {
PathNavigation navigation = this.turtle.getNavigation();
float previousMultiplier = ((PathNavigationAccessor) navigation).turtleReproduce$getMaxVisitedNodesMultiplier();
// A larger distance alone leaves vanilla's small node budget unchanged,
// incorrectly rejecting shores that require a detour around an obstacle.
navigation.setMaxVisitedNodesMultiplier(Math.max(previousMultiplier, 8.0F));
try {
return navigation.createPath(destination, 0, PATH_SEARCH_RANGE);
} finally {
navigation.setMaxVisitedNodesMultiplier(previousMultiplier);
}
}
@Override
protected void moveMobToBlock() {
if (!SLSCarpetSettings.turtleLocalNesting) {
super.moveMobToBlock();
return;
}
this.turtle.getNavigation().moveTo(this.turtleReproduce$nestingPath, this.speedModifier);
}
@Inject(method = "tick", at = @At("TAIL"))
private void turtleReproduce$retryLongerPath(CallbackInfo ci) {
if (!SLSCarpetSettings.turtleLocalNesting) return;
if (!this.isReachedTarget() && this.tryTicks % 40 == 0 && this.turtle.getNavigation().isDone()) {
this.turtleReproduce$nestingPath = this.turtleReproduce$createNestingPath(this.blockPos.above());
this.moveMobToBlock();
}
}
@Override
public void stop() {
super.stop();
if (!SLSCarpetSettings.turtleLocalNesting) return;
this.turtle.getNavigation().stop();
((TurtleAccessor) this.turtle).turtleReproduce$setLayingEgg(false);
this.turtleReproduce$nestingPath = null;
this.turtleReproduce$candidates = List.of();
}
}
@@ -2,16 +2,16 @@ package com.github.zly2006.carpetslsaddition.mixin.world;
import carpet.patches.EntityPlayerMPFake;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.SleepManager;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.players.SleepStatus;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(SleepManager.class)
@Mixin(SleepStatus.class)
public class MixinSleepManager {
@Redirect(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerEntity;isSpectator()Z"))
private boolean isNormalPlayer(ServerPlayerEntity serverPlayerEntity) {
@Redirect(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;isSpectator()Z"))
private boolean isNormalPlayer(ServerPlayer serverPlayerEntity) {
if (SLSCarpetSettings.fakePlayersNotOccupiedSleepQuota) {
return serverPlayerEntity.isSpectator() || serverPlayerEntity instanceof EntityPlayerMPFake;
}
@@ -19,4 +19,4 @@ public class MixinSleepManager {
return serverPlayerEntity.isSpectator();
}
}
}
}
@@ -1,22 +1,23 @@
package com.github.zly2006.carpetslsaddition.mixin.world;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.SpawnHelper;
import org.jetbrains.annotations.Nullable;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.NaturalSpawner;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(SpawnHelper.class)
@Mixin(NaturalSpawner.class)
public class MixinSpawnHelper {
@Inject(method = "createMob", at = @At("HEAD"), cancellable = true)
private static void onCreateMob(ServerWorld world, EntityType<?> type, CallbackInfoReturnable<@Nullable MobEntity> cir) {
// 注意:不能用 @Redirect EntityType.create —— 会和 Carpet 的 NaturalSpawnerMixin 冲突。
// 改为在 getMobForSpawn 返回处拦截。
@Inject(method = "getMobForSpawn", at = @At("HEAD"), cancellable = true)
private static void onCreateMob(ServerLevel level, EntityType<?> type, CallbackInfoReturnable<Mob> cir) {
if (SLSCarpetSettings.noBatSpawning && type == EntityType.BAT) {
cir.setReturnValue(null);
}
}
}
}
@@ -0,0 +1,254 @@
package com.github.zly2006.carpetslsaddition.turtle;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.entity.animal.turtle.Turtle;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.TurtleEggBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Finds dry nesting blocks beside nearby, exposed source water.
*
* <p>This class was rewritten for throughput. The scan is the hot path: one call
* covers 73 x 73 columns x 19 vertical steps, and a pregnant turtle re-runs it
* whenever it drifts more than four blocks from the previous search origin or
* exhausts its cached candidate list.
*
* <p>Every result-affecting rule is unchanged:
* <ul>
* <li>the same box is visited (32 in X/Z for nests, plus a 4 block halo for
* water, 8 up/down for nests, plus 1 for water);</li>
* <li>the same conditions are tested, in the same order;</li>
* <li>unloaded chunks are still skipped, and are still never loaded or
* generated ({@code loadOrGenerate = false});</li>
* <li>the candidate list is still ordered by the same comparator, which ends
* in the block coordinates and therefore is a total order. Visit order no
* longer matters, so the sort result is identical.</li>
* </ul>
*
* <p>What changed, and why it is equivalent:
* <ul>
* <li>blocks are read through the chunk section that holds them instead of
* {@code Level#getBlockState}, which re-resolves the chunk, the section
* index and the build-height check on every single block. Sections that
* {@code hasOnlyAir()} are skipped entirely: they cannot contribute water
* or sand;</li>
* <li>the same availability test as {@code Level#hasChunk} is used, but the
* resolved chunk is kept for the section reads instead of being thrown
* away and looked up again per block;</li>
* <li>sand positions are collected as packed longs instead of thousands of
* {@code BlockPos} objects, so a scan allocates no per-block garbage;</li>
* <li>{@code isValidSite} hoists its loop invariants and remembers the last
* chunk availability answer; the disk it walks spans at most four chunks,
* so this only removes repeated lookups.</li>
* </ul>
*/
public final class NestingSiteFinder {
public static final int SEARCH_RADIUS = 32;
public static final int VERTICAL_SEARCH_RADIUS = 8;
public static final int WATER_RADIUS = 4;
public static final int WATER_VERTICAL_RADIUS = 1;
private static final Comparator<Candidate> CANDIDATE_ORDER = Comparator
.comparingDouble(Candidate::waterDistanceSquared)
.thenComparingDouble(Candidate::sandDistanceSquared)
.thenComparingInt(candidate -> candidate.sandPos().getX())
.thenComparingInt(candidate -> candidate.sandPos().getY())
.thenComparingInt(candidate -> candidate.sandPos().getZ());
private NestingSiteFinder() {
}
/**
* Searches a square extending 32 blocks in X/Z and 8 blocks in Y from the
* turtle. A four-block halo includes water beside nests at the boundary.
* The caller must check the turtle's collision box and navigation path.
*/
public static List<Candidate> findCandidates(Turtle turtle) {
Level level = turtle.level();
BlockPos origin = turtle.blockPosition();
int originX = origin.getX();
int originY = origin.getY();
int originZ = origin.getZ();
int scanRadius = SEARCH_RADIUS + WATER_RADIUS;
int minY = Math.max(level.getMinY(), originY - VERTICAL_SEARCH_RADIUS - WATER_VERTICAL_RADIUS);
int maxY = Math.min(level.getMaxY() - 1, originY + VERTICAL_SEARCH_RADIUS + WATER_VERTICAL_RADIUS);
int nestMinX = originX - SEARCH_RADIUS;
int nestMaxX = originX + SEARCH_RADIUS;
int nestMinZ = originZ - SEARCH_RADIUS;
int nestMaxZ = originZ + SEARCH_RADIUS;
int nestMinY = originY - VERTICAL_SEARCH_RADIUS;
int nestMaxY = originY + VERTICAL_SEARCH_RADIUS;
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
LongArrayList sandBlocks = new LongArrayList();
Long2DoubleOpenHashMap waterDistances = new Long2DoubleOpenHashMap();
waterDistances.defaultReturnValue(Double.POSITIVE_INFINITY);
// Read each block once, instead of repeatedly querying the world around
// every sand block. Unloaded chunks are never requested or generated.
if (minY <= maxY) {
Vec3 turtlePos = turtle.position();
int minSection = level.getSectionIndex(minY);
int maxSection = level.getSectionIndex(maxY);
for (int chunkX = (originX - scanRadius) >> 4; chunkX <= (originX + scanRadius) >> 4; chunkX++) {
int fromX = Math.max(originX - scanRadius, chunkX << 4);
int toX = Math.min(originX + scanRadius, (chunkX << 4) + 15);
for (int chunkZ = (originZ - scanRadius) >> 4; chunkZ <= (originZ + scanRadius) >> 4; chunkZ++) {
// The same test Level#hasChunk performs, except the chunk is
// kept: loadOrGenerate = false never loads or generates.
ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false);
if (chunk == null) {
continue;
}
int fromZ = Math.max(originZ - scanRadius, chunkZ << 4);
int toZ = Math.min(originZ + scanRadius, (chunkZ << 4) + 15);
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
if (sectionIndex < 0 || sectionIndex >= sections.length) {
continue;
}
LevelChunkSection section = sections[sectionIndex];
// An all-air section holds neither water nor sand, so
// skipping it reads no block the loops could have used.
if (section == null || section.hasOnlyAir()) {
continue;
}
int sectionMinY = SectionPos.sectionToBlockCoord(level.getMinSectionY() + sectionIndex);
int fromY = Math.max(minY, sectionMinY);
int toY = Math.min(maxY, sectionMinY + 15);
boolean nestSection = fromY <= nestMaxY && toY >= nestMinY;
for (int x = fromX; x <= toX; x++) {
int localX = x & 15;
boolean nestX = x >= nestMinX && x <= nestMaxX;
for (int z = fromZ; z <= toZ; z++) {
boolean inNestRange = nestSection && nestX && z >= nestMinZ && z <= nestMaxZ;
int localZ = z & 15;
for (int y = fromY; y <= toY; y++) {
BlockState state = section.getBlockState(localX, y & 15, localZ);
if (isExposedSourceWater(level, cursor, x, y, z, state)) {
cursor.set(x, y, z);
waterDistances.put(BlockPos.asLong(x, y, z),
cursor.distToCenterSqr(turtlePos));
}
if (inNestRange && y >= nestMinY && y <= nestMaxY
&& state.is(BlockTags.SAND)
&& level.isEmptyBlock(cursor.set(x, y + 1, z))) {
sandBlocks.add(BlockPos.asLong(x, y, z));
}
}
}
}
}
}
}
}
List<Candidate> candidates = new ArrayList<>();
if (waterDistances.isEmpty()) {
return candidates;
}
Vec3 turtlePos = turtle.position();
for (int index = 0; index < sandBlocks.size(); index++) {
long packedSand = sandBlocks.getLong(index);
int sandX = BlockPos.getX(packedSand);
int sandY = BlockPos.getY(packedSand);
int sandZ = BlockPos.getZ(packedSand);
double nearestWaterDistance = Double.POSITIVE_INFINITY;
for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) {
for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) {
if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) {
continue;
}
for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) {
long waterPosition = BlockPos.asLong(sandX + dx, sandY + dy, sandZ + dz);
nearestWaterDistance = Math.min(nearestWaterDistance, waterDistances.get(waterPosition));
}
}
}
if (Double.isFinite(nearestWaterDistance)) {
cursor.set(sandX, sandY, sandZ);
candidates.add(new Candidate(new BlockPos(sandX, sandY, sandZ), nearestWaterDistance,
cursor.distToCenterSqr(turtlePos)));
}
}
candidates.sort(CANDIDATE_ORDER);
return candidates;
}
/** Rechecks a chosen nest, including changes to its sand, air, or water. */
public static boolean isValidSite(LevelReader level, BlockPos sandPos) {
if (!level.isInsideBuildHeight(sandPos) || !level.isInsideBuildHeight(sandPos.above())
|| !level.hasChunkAt(sandPos)
|| !TurtleEggBlock.isSand(level, sandPos)
|| !level.isEmptyBlock(sandPos.above())) {
return false;
}
int sandX = sandPos.getX();
int sandY = sandPos.getY();
int sandZ = sandPos.getZ();
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
int lastChunkX = Integer.MIN_VALUE;
int lastChunkZ = Integer.MIN_VALUE;
boolean lastChunkLoaded = false;
for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) {
for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) {
if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) {
continue;
}
int x = sandX + dx;
int z = sandZ + dz;
int chunkX = x >> 4;
int chunkZ = z >> 4;
// The disk above spans at most four chunks, so this only removes
// repeated queries for columns that share a chunk.
if (chunkX != lastChunkX || chunkZ != lastChunkZ) {
lastChunkX = chunkX;
lastChunkZ = chunkZ;
lastChunkLoaded = level.hasChunk(chunkX, chunkZ);
}
if (!lastChunkLoaded) {
continue;
}
for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) {
int y = sandY + dy;
cursor.set(x, y, z);
if (level.isInsideBuildHeight(y) && level.isInsideBuildHeight(y + 1)
&& isExposedSourceWater(level, cursor, x, y, z, level.getBlockState(cursor))) {
return true;
}
}
}
}
return false;
}
private static boolean isExposedSourceWater(LevelReader level, BlockPos.MutableBlockPos cursor,
int x, int y, int z, BlockState state) {
// Requiring a liquid block excludes waterlogged fences, slabs and other
// solid blocks even though their fluid state may report source water.
if (!state.liquid()) {
return false;
}
FluidState fluid = state.getFluidState();
return fluid.is(FluidTags.WATER) && fluid.isSource() && level.isEmptyBlock(cursor.set(x, y + 1, z));
}
public record Candidate(BlockPos sandPos, double waterDistanceSquared, double sandDistanceSquared) {
}
}
@@ -0,0 +1,105 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* [旧版] 自建异步线程,模拟 1.12 的"染色玻璃→信标搜索线程"。
*
* <p>1.12 的漏洞依赖一个独立的异步线程,与主线程并发访问区块哈希表
* ({@code Long2ObjectOpenHashMap}),从而制造 race condition,让 chunk 被误判为"未加载",
* 触发重新加载与世界装饰。
*
* <p>26.1.2 的信标已无独立线程,故本类自建一个单线程调度器:
* 周期性在<b>主线程之外</b>调用 {@link ServerChunkCache#getChunkNow} 与读取区块,
* 尽最大可能复现 1.12 的并发访问模式。
*
* <p><b>⚠ 风险提示</b>:26.1.2 的区块访问有 {@code mainThreadExecutor} 约束,
* 跨线程访问可能触发断言或未定义行为,极端情况下可导致服务器崩溃或存档损坏。
* 仅在 {@code legacyExploitMode} + {@code legacyAsyncChunkAccess} 同时开启时启动。
*/
public final class LegacyAsyncExecutor {
private LegacyAsyncExecutor() {}
private static ScheduledExecutorService executor;
private static volatile boolean started = false;
/** 目标区块坐标(由玩家装置或命令设置)。 */
private static volatile int targetChunkX = Integer.MIN_VALUE;
private static volatile int targetChunkZ = Integer.MIN_VALUE;
public static void setTargetChunk(int x, int z) {
targetChunkX = x;
targetChunkZ = z;
}
public static synchronized void start() {
if (started) {
return;
}
executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "SLSA-Legacy-AsyncChunk");
t.setDaemon(true);
return t;
});
executor.scheduleAtFixedRate(LegacyAsyncExecutor::tick, 0, 1, TimeUnit.MILLISECONDS);
started = true;
LegacyExploitState.setAsyncThreadRunning(true);
ServerMain.LOGGER.info("[SLSA] legacy async chunk thread started");
}
public static synchronized void stop() {
if (!started || executor == null) {
return;
}
executor.shutdownNow();
executor = null;
started = false;
LegacyExploitState.setAsyncThreadRunning(false);
ServerMain.LOGGER.info("[SLSA] legacy async chunk thread stopped");
}
/** 异步线程主体:并发访问目标区块,制造与主线程的竞态。 */
private static void tick() {
try {
if (!SLSCarpetSettingsBridge.shouldRun()) {
return;
}
MinecraftServer server = ServerMain.server;
if (server == null) {
return;
}
for (ServerLevel level : server.getAllLevels()) {
ServerChunkCache cache = level.getChunkSource();
if (targetChunkX != Integer.MIN_VALUE) {
// 故意在主线程之外读取区块表——制造 race condition
LevelChunk chunk = cache.getChunkNow(targetChunkX, targetChunkZ);
if (chunk != null) {
// 触发一次方块状态读取,模拟信标向下搜索
chunk.getBlockState(new net.minecraft.core.BlockPos(
targetChunkX << 4, level.getMinY(), targetChunkZ << 4));
}
}
}
} catch (Throwable t) {
// 异步线程绝不能让异常冒泡导致调度器静默停止
ServerMain.LOGGER.debug("[SLSA] legacy async tick error: {}", t.toString());
}
}
/** 桥接类:避免直接依赖 SLSCarpetSettings 的静态初始化顺序问题。 */
private static final class SLSCarpetSettingsBridge {
static boolean shouldRun() {
return com.github.zly2006.carpetslsaddition.SLSCarpetSettings.legacyExploitMode
&& com.github.zly2006.carpetslsaddition.SLSCarpetSettings.legacyAsyncChunkAccess;
}
}
}
@@ -0,0 +1,70 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* [旧版] 方块变更记录器——用于重建 1.12 的"替换窗口"。
*
* <p>1.12 的窗口依赖"异步线程在主线程 tick 中途改块",26.1.2 单线程模型下物理不存在。
* 本类改用"记录+回放":把最近发生的方块变更记录在此,
* 当重力方块触发下落时,若该位置"刚被改过",就用记录的新方块作为下落实体的内容——
* 等价于 1.12 的窗口替换效果(只是窗口由 mod 主动开启)。
*
* <p>记录以"服务器 tick 数"为界:仅保留最近 {@link #WINDOW_TICKS} 个 tick 内的变更,
* 超过即失效,避免记录无限增长或误伤无关方块。
*/
public final class LegacyBlockChangeRecorder {
private LegacyBlockChangeRecorder() {}
/** 窗口大小:变更与下落之间允许的最大 tick 间隔。 */
public static final long WINDOW_TICKS = 2L;
private record Change(BlockState state, long tick) {}
private static final Map<Long, Change> RECENT = new ConcurrentHashMap<>();
/** 获取当前服务器 tick(拿不到时回退为 0)。 */
private static long now() {
MinecraftServer server = ServerMain.server;
return server != null ? server.getTickCount() : 0L;
}
/** 记录一次方块变更。 */
public static void record(BlockPos pos, BlockState newState) {
RECENT.put(pos.asLong(), new Change(newState, now()));
}
/**
* 查询某位置是否在窗口期内有变更记录。
*
* @return 若命中窗口,返回记录的新状态;否则返回 null。
*/
public static BlockState getRecentChange(BlockPos pos) {
Change c = RECENT.get(pos.asLong());
if (c == null) {
return null;
}
if (now() - c.tick() > WINDOW_TICKS) {
RECENT.remove(pos.asLong());
return null;
}
return c.state();
}
/** 清理过期记录。 */
public static void cleanup() {
long t = now();
RECENT.entrySet().removeIf(e -> t - e.getValue().tick() > WINDOW_TICKS);
}
public static void clear() {
RECENT.clear();
}
}
@@ -0,0 +1,66 @@
package com.github.zly2006.carpetslsaddition.util;
import net.minecraft.core.BlockPos;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/**
* [旧版] 漏洞机制的全局状态管理(线程安全)。
*
* <p>承载:
* <ul>
* <li>当前"落沙替换目标方块"ID(由 FallingBlockMixin 设置,FallingBlockEntityMixin 消费)</li>
* <li>被标记为 instantFall 触发的位置(跨线程共享,故用并发容器)</li>
* <li>异步 chunk 访问线程的运行状态</li>
* </ul>
*
* <p><b>线程安全说明</b>:本类会被主线程与自建的异步线程同时访问,
* 所有可变状态均使用并发容器或原子引用。
*/
public final class LegacyExploitState {
private LegacyExploitState() {}
/** 当前落沙替换目标(方块 ID 字符串),"#none" 表示禁用。 */
private static final AtomicReference<String> REPLACE_TARGET = new AtomicReference<>("#none");
/** 被标记为 instantFall 触发的位置集合(异步线程可读)。 */
private static final ConcurrentHashMap<Long, Boolean> INSTANT_FALL_POS = new ConcurrentHashMap<>();
/** 异步 chunk 访问线程是否正在运行。 */
private static volatile boolean asyncThreadRunning = false;
public static void setReplaceTarget(String target) {
REPLACE_TARGET.set(target == null ? "#none" : target);
}
public static String getReplaceTarget() {
return REPLACE_TARGET.get();
}
public static boolean hasReplaceTarget() {
String t = REPLACE_TARGET.get();
return t != null && !"#none".equals(t) && !t.isEmpty();
}
public static void markInstantFall(BlockPos pos) {
INSTANT_FALL_POS.put(pos.asLong(), Boolean.TRUE);
}
public static boolean isInstantFall(BlockPos pos) {
return INSTANT_FALL_POS.containsKey(pos.asLong());
}
public static void clearInstantFall(BlockPos pos) {
INSTANT_FALL_POS.remove(pos.asLong());
}
public static boolean isAsyncThreadRunning() {
return asyncThreadRunning;
}
public static void setAsyncThreadRunning(boolean running) {
asyncThreadRunning = running;
}
}
@@ -1,11 +1,10 @@
package com.github.zly2006.carpetslsaddition.util;
import net.minecraft.block.ShulkerBoxBlock;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.item.BlockItem;
import net.minecraft.item.ItemStack;
import java.util.Objects;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.ItemContainerContents;
import net.minecraft.world.level.block.ShulkerBoxBlock;
public class ShulkerBoxItemUtil {
public static final int SHULKERBOX_MAX_STACK_AMOUNT = 64;
@@ -15,25 +14,17 @@ public class ShulkerBoxItemUtil {
if (stack.isEmpty() && otherStack.isEmpty()) {
return true;
}
return Objects.equals(
stack.getComponents().filtered(s -> s != DataComponentTypes.CONTAINER),
stack.getComponents().filtered(s -> s != DataComponentTypes.CONTAINER)
);
return stack.getComponents().equals(otherStack.getComponents());
}
return ItemStack.areItemsAndComponentsEqual(stack, otherStack);
return ItemStack.isSameItemSameComponents(stack, otherStack);
}
public static boolean isEmptyShulkerBoxItem(ItemStack itemStack) {
if (itemStack.getItem() instanceof BlockItem &&
((BlockItem) itemStack.getItem()).getBlock() instanceof ShulkerBoxBlock) {
if (itemStack.getComponents().contains(DataComponentTypes.CONTAINER)) {
return !itemStack.getComponents().get(DataComponentTypes.CONTAINER).iterateNonEmpty().iterator().hasNext();
} else {
return true;
}
} else {
return false;
if (itemStack.getItem() instanceof BlockItem blockItem
&& blockItem.getBlock() instanceof ShulkerBoxBlock) {
return itemStack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY)
.allItemsCopyStream().findAny().isEmpty();
}
return false;
}
}
}
@@ -4,4 +4,4 @@ public interface SitEntity {
boolean isSitEntity();
void setSitEntity(boolean sitEntity);
}
}
@@ -1,7 +1,7 @@
package com.github.zly2006.carpetslsaddition.util.access;
import net.minecraft.text.Text;
import net.minecraft.network.chat.Component;
public interface PlayerAccessor {
void carpet_SLS_Addition$setDisplayName(Text name);
}
void carpet_SLS_Addition$setDisplayName(Component name);
}
@@ -5,4 +5,4 @@ public interface SLSBotAccessor {
void carpet_SLS_Addition$setBot(boolean isBot);
long carpet_SLS_Addition$getSpawnTime();
void carpet_SLS_Addition$setSpawnTime(long spawnTime);
}
}
@@ -2,6 +2,7 @@
"carpet.category.pca": "PCA Adaption",
"carpet.category.needClient":"Need client support",
"carpet.category.protocol":"Protocol",
"carpet.category.legacy": "Legacy",
"carpet.rule.obtainableReinforcedDeepSlate.name": "Obtainable reinforced deep slate",
@@ -50,6 +51,12 @@
"carpet.rule.spectatorCannotUseLeash.desc": "If a player is a spectator, they cannot use the leash.",
"carpet.rule.offlineFakePlayers.desc": "假人强制离线uuid",
"carpet.rule.armadilloImmediateDespawns.desc": "Armadillos will despawn immediately if they are 128 blocks away from the player.",
"carpet.rule.turtleLocalNesting.name": "Turtle Local Nesting",
"carpet.rule.turtleLocalNesting.desc": "Turtles carrying eggs will lay them on reachable sand near the closest local water source instead of returning to their birthplace.",
"carpet.rule.sturdyDecoratedPot.name": "Sturdy Decorated Pot",
"carpet.rule.sturdyDecoratedPot.desc": "Decorated pots are not shattered by projectiles.",
"carpet.rule.unbreakableDecoratedPot.name": "No Crack When Mining Pot",
"carpet.rule.unbreakableDecoratedPot.desc": "Breaking a decorated pot by a player does not produce the cracked effect.",
"carpet.rule.trialSpawnerCD.desc": "trialSpawnerCD.",
"carpet.rule.skipTicksForJoblessVillager.extra.0": "Set to 0 to maintain the vanilla behavior",
@@ -66,5 +73,20 @@
"carpet.slsa.bot.respawned": "The bot %s's online time has been reset.",
"carpet.slsa.bot.not_a_bot": "The player %s is not created by carpet sls addition, so you can't reset the online time.",
"carpet.slsa.block.deep_slate_core": "Deepslate Core"
"carpet.slsa.block.deep_slate_core": "Deepslate Core",
"carpet.rule.legacyExploitMode.name": "Legacy exploit mode",
"carpet.rule.legacyExploitMode.desc": "Master switch: other rules under the [Legacy] category only take effect when enabled",
"carpet.rule.legacyInstantTick.name": "Instant scheduled tick",
"carpet.rule.legacyInstantTick.desc": "Equivalent to 1.12 instantTick: scheduled ticks execute immediately without delay",
"carpet.rule.legacyInstantFall.name": "Instant falling blocks",
"carpet.rule.legacyInstantFall.desc": "Equivalent to 1.12 instantFall: falling blocks fall immediately when scheduled",
"carpet.rule.legacyAsyncChunkAccess.name": "Async chunk access thread",
"carpet.rule.legacyAsyncChunkAccess.desc": "Spawns a background thread mimicking 1.12 beacon thread to access chunks concurrently",
"carpet.rule.legacyChunkSwap.name": "Chunk swap",
"carpet.rule.legacyChunkSwap.desc": "Allows async thread to treat a loaded chunk as unloaded via race condition, triggering reload and world population",
"carpet.rule.legacyFallingBlockReplace.name": "Falling block replace",
"carpet.rule.legacyFallingBlockReplace.desc": "Replaces the block with a target block during the falling block to entity window",
"carpet.rule.legacyFallingBlockTarget.name": "Falling replace target",
"carpet.rule.legacyFallingBlockTarget.desc": "Target block ID for falling block replace, #none to disable"
}
@@ -2,6 +2,7 @@
"carpet.category.pca": "PCA适配",
"carpet.category.needClient": "需要客户端支持",
"carpet.category.protocol": "协议",
"carpet.category.legacy": "旧版",
"carpet.rule.obtainableReinforcedDeepSlate.name": "强化深板岩可获取",
@@ -26,6 +27,12 @@
"carpet.rule.botMaxOnlineTime.name": "Bot最大存活时间",
"carpet.rule.spectatorCannotUseLeash.name": "观察者模式禁用拴绳",
"carpet.rule.armadilloImmediateDespawns.desc": "当犰狳远离玩家128格时,立即消失",
"carpet.rule.turtleLocalNesting.name": "海龟就地筑巢",
"carpet.rule.turtleLocalNesting.desc": "有蛋的海龟会在附近的水边沙滩下蛋,而不是返回出生地",
"carpet.rule.sturdyDecoratedPot.name": "坚硬陶罐",
"carpet.rule.sturdyDecoratedPot.desc": "装饰陶罐不会被投掷物击碎",
"carpet.rule.unbreakableDecoratedPot.name": "挖掘陶罐无裂纹",
"carpet.rule.unbreakableDecoratedPot.desc": "玩家破坏装饰陶罐时不会产生裂纹效果",
"carpet.rule.obtainableReinforcedDeepSlate.desc": "当铁砧砸在9*64个深板岩的掉落物上时,可以获得强化深板岩",
"carpet.rule.skipTicksForJoblessVillager.desc": "对于无业/傻子村民,执行设定数量的tick运算后,跳过1tick的运算",
@@ -66,5 +73,20 @@
"carpet.slsa.bot.respawned": "假人%s的在线时长已重置!",
"carpet.slsa.bot.not_a_bot": "假人%s不是由Carpet SLS Addition生成的,所以你不能重置它的在线时间!",
"carpet.slsa.block.deep_slate_core": "深板岩核心"
"carpet.slsa.block.deep_slate_core": "深板岩核心",
"carpet.rule.legacyExploitMode.name": "旧版漏洞模式",
"carpet.rule.legacyExploitMode.desc": "总开关:开启后[旧版]分类下的其余机制规则才会生效",
"carpet.rule.legacyInstantTick.name": "计划刻立即执行",
"carpet.rule.legacyInstantTick.desc": "等价1.12的instantTick:计划刻(scheduleTick)立即执行而不等待延迟",
"carpet.rule.legacyInstantFall.name": "重力方块瞬间下落",
"carpet.rule.legacyInstantFall.desc": "等价1.12的instantFall:重力方块被计划刻触发时立即下落",
"carpet.rule.legacyAsyncChunkAccess.name": "异步区块访问线程",
"carpet.rule.legacyAsyncChunkAccess.desc": "自建后台线程模拟1.12的信标线程,并发访问区块",
"carpet.rule.legacyChunkSwap.name": "区块替换",
"carpet.rule.legacyChunkSwap.desc": "允许异步线程在区块表竞态时把已加载区块当成未加载,触发重新加载与世界装饰",
"carpet.rule.legacyFallingBlockReplace.name": "落沙替换",
"carpet.rule.legacyFallingBlockReplace.desc": "在重力方块转为下落实体的窗口内,把方块替换为指定目标方块",
"carpet.rule.legacyFallingBlockTarget.name": "落沙替换目标方块",
"carpet.rule.legacyFallingBlockTarget.desc": "落沙替换的目标方块ID,#none为禁用"
}
+4 -7
View File
@@ -27,13 +27,10 @@
],
"depends": {
"fabricloader": ">=0.15.1",
"fabricloader": ">=0.19.3",
"fabric-api": "*",
"minecraft": ">=1.21",
"java": ">=21",
"minecraft": ">=26.1",
"java": ">=25",
"carpet": "*"
},
"suggests": {
"another-mod": "*"
}
}
}
+6 -6
View File
@@ -1,6 +1,6 @@
accessWidener v1 named
accessible class net/minecraft/world/block/ChainRestrictedNeighborUpdater$Entry
accessible class net/minecraft/entity/mob/EndermanEntity$PickUpBlockGoal
accessible method net/minecraft/entity/Entity unsetRemoved ()V
accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData;
accessible field net/minecraft/block/spawner/TrialSpawnerData spawnedMobsAlive Ljava/util/Set;
accessWidener v1 official
accessible class net/minecraft/world/entity/monster/EnderMan$EndermanTakeBlockGoal
accessible method net/minecraft/world/entity/Entity unsetRemoved ()V
accessible field net/minecraft/world/entity/Entity entityData Lnet/minecraft/network/syncher/SynchedEntityData;
accessible field net/minecraft/world/entity/Avatar DATA_PLAYER_MODE_CUSTOMISATION Lnet/minecraft/network/syncher/EntityDataAccessor;
+13 -5
View File
@@ -1,21 +1,25 @@
{
"required": true,
"required": false,
"minVersion": "0.8",
"package": "com.github.zly2006.carpetslsaddition.mixin",
"compatibilityLevel": "JAVA_17",
"compatibilityLevel": "JAVA_21",
"client": [
],
"mixins": [
"block.BlockPatternTestTransformInvoker",
"block.MixinNetherPortal",
"block.MixinRedstoneWireBlock",
"block.MixinDecoratedPotBlock",
"block.MixinTrialSpawnerData",
"block.MixinTrialSpawnerState",
"carpet.MixinEntityPlayerMPFake",
"carpet.MixinPlayerCommand",
"datapack.MixinVanillaResourcePackProvider",
"elytra.MixinElytraItem",
"elytra.ItemAccessor",
"elytra.MixinItem",
"legacy.FallingBlockMixin",
"legacy.FallingBlockEntityMixin",
"legacy.ScheduledTickMixin",
"legacy.ChunkMapMixin",
"entity.MixinArmadilloEntity",
"entity.MixinArmorStandEntity",
"entity.MixinBoatEntity",
@@ -33,7 +37,11 @@
"shulker.MixinServerPlayNetworkHandler",
"shulker.MixinSlot",
"world.MixinSleepManager",
"world.MixinSpawnHelper"
"world.MixinSpawnHelper",
"turtle.PathNavigationAccessor",
"turtle.TurtleAccessor",
"turtle.TurtleGoHomeGoalMixin",
"turtle.TurtleLayEggGoalMixin"
],
"injectors": {
"defaultRequire": 1