Compare commits

...
10 Commits
Author SHA1 Message Date
iceBear67andClaude Opus 5.5 b8d4725e7b update 26.3 dependencies and pack mods
- polymer 0.18.0+26.3-rc-1 -> 0.18.2+26.3 (first 26.3 final build)
- fabric-api 0.160.5 -> 0.161.0, loom 1.18.1 -> 1.18.2
- pack: move squaremap, syncmatica, farmers-delight (+polymer patch),
  jei, worldedit, lithium to their 26.3 builds; bump fabric-api, polymer
- lithium now resolved from Modrinth instead of CurseForge (same mod, same author)
- docs: fix stale hurtMarked reference, drop carpet snapshot warning

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 03:14:03 +00:00
iceBear67andClaude Opus 5 2b70971b80 port to Minecraft 26.3
Source changes for 26.3 API breakage:
- block codec system removed upstream (Block has no MapCodec/codec()/
  simpleCodec anymore): drop CODEC + codec() from AmethystCauldronBlock
  and GravityCrystalBlock
- Entity.hurtMarked -> Entity.syncVelocity (pure rename, still public)
- LivingEntity.drop() now takes a Prediction; our drops are server-
  initiated and unpredicted, so they pass SERVER_ONLY
- canSimulateMovement() became final; the ravager/hoglin mount mixins
  now override getMoveSimulationType() and return
  AUTHORITATIVE_SIDE_AND_SERVER, which is the same "server always
  simulates" semantics the old !isClientSide branch had
- loot number providers split into providers.number.{ints,floats} and
  became Holder-based: UniformGenerator.between(1f,3f) ->
  ContextIntProviders.between(1,3)
- BonemealableBlock methods take a BonemealSource (INTERACTION here)
- clockManager().getTotalTicks(c) ->
  clockManager().getInstance(c).totalTicks()
- EnderMan -> Enderman (capitalisation only)

Build: MC 26.3, loader 0.19.5, fabric-api 0.160.5+26.3, loom 1.18.1,
gradle wrapper 9.6.0 -> 9.7.1 (loom 1.18.x requires Gradle >= 9.7).
Polymer is 0.18.0+26.3-rc-1: upstream has not published a 26.3-final
build yet, that rc build is the only one on the 26.3 line.

server-deploy: pack.toml -> 26.3 / loader 0.19.5; fabric-api, polymer
and simple-voice-chat are the only pack mods with 26.3 builds and were
updated (index hashes regenerated). The remaining third-party mods have
no 26.3 release yet and are deliberately left untouched. Dockerfile now
pins the justbackup and fabric-carpet forks to their 26.3 branches
instead of silently building whatever the default branch holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 15:54:54 +00:00
iceBear67 08be6bc3ae fix: use og server host for transfer 2026-09-02 00:23:43 +08:00
iceBear67 bf38f8b2b2 fix elytra rush collison 2026-08-24 20:12:59 +08:00
iceBear67 63250d48ae fix: beheading dups mob head w/o beheading enchant 2026-08-24 19:49:13 +08:00
iceBear67 f026c65915 support 4k image for mapfor feature 2026-08-24 18:40:58 +08:00
iceBear67 b1c86c63aa update readme 2026-08-10 17:43:51 +08:00
iceBear67 f7f417174d fix playgrd timeline 2026-08-08 17:38:58 +08:00
iceBear67 ffa6d9826c fix ci cache calculation 2026-08-08 16:34:06 +08:00
iceBear67 f7980342c3 bump version 2.2 2026-08-08 16:05:09 +08:00
56 changed files with 1146 additions and 164 deletions

No files matched your search

+2 -2
View File
@@ -63,8 +63,8 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
context: server-deploy
file: server-deploy/Dockerfile
context: .
file: Dockerfile
# mc-server-runner is downloaded as linux_amd64 in the Dockerfile.
platforms: linux/amd64
push: true
+20 -10
View File
@@ -1,6 +1,6 @@
# sfcraft Agent 工作指南
Fabric **纯服务端** mod,Minecraft **26.2**,Java 25,官方 Mojang 映射,自定义内容经 [Polymer](https://polymer.pb4.eu) 伪装成原版内容。整体结构见 [ARCHITECTURE.md](./ARCHITECTURE.md),先读它再动代码。
Fabric **纯服务端** mod,Minecraft **26.3**,Java 25,官方 Mojang 映射,自定义内容经 [Polymer](https://polymer.pb4.eu) 伪装成原版内容。整体结构见 [ARCHITECTURE.md](./ARCHITECTURE.md),先读它再动代码。
## ⚠️ 工作范围(最高优先级规则)
@@ -22,23 +22,22 @@ Agent 的主要工作范围**只有**这两个包:
## 工具使用
- 探索、读写代码**优先使用 IDE 的 MCP 工具**:`list_directory_tree`(代替 `ls`/`find`)、`read_file`(代替 `cat`/`sed`)、`search_text`/`search_regex`/`search_symbol`(代替 `rg`/`grep`)、`get_symbol_info`、`analyze_calls`;编辑用 `apply_patch`/`create_new_file`。
- 查 26.2 原版类签名**优先用 `read_file` 直接读 loom 缓存 jar 内的类**(会反编译,可读上下文):`~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.2/minecraft-merged-deobf-26.2.jar!/net/minecraft/.../SomeClass.class`,`javap` 仅作回退。
- 查 26.3 原版类签名**优先用 `read_file` 直接读 loom 缓存 jar 内的类**(会反编译,可读上下文):`~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3/minecraft-merged-deobf-26.3.jar!/net/minecraft/.../SomeClass.class`,`javap` 仅作回退。
- shell 只留给 MCP 做不了的事:gradle 编译/运行、git、文件权限操作等。
## 构建与验证
仓库**没有** gradlew wrapper,使用系统 gradle + sdkman JDK:
仓库自带 gradlew wrapper(**9.7.1** —— loom 1.18.x 要求 Gradle ≥ 9.7,Dockerfile 也走 `./gradlew`):
```bash
export JAVA_HOME=~/.sdkman/candidates/java/25.0.1-amzn
~/.sdkman/candidates/gradle/9.6.0/bin/gradle compileJava --console=plain # 编译(mixin AP 会校验注入目标)
~/.sdkman/candidates/gradle/9.6.0/bin/gradle runServer --console=plain # dev 服务器(run/ 目录,EULA 已接受)
./gradlew compileJava --console=plain # 编译(mixin AP 会校验注入目标)
./gradlew runServer --console=plain # dev 服务器(run/ 目录,EULA 已接受)
```
查 26.2 真实签名(解决 cannot find symbol 的最快办法):
查 26.3 真实签名(解决 cannot find symbol 的最快办法):
```bash
javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.2/minecraft-merged-deobf-26.2.jar <类全名>
javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3/minecraft-merged-deobf-26.3.jar <类全名>
```
## 新增/修改玩法特性的套路
@@ -61,10 +60,21 @@ javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-me
- **配置**:数值参数进 `GameConfig`(`io.ib67.sfcraft.config`)对应特性的嵌套类,运行期通过注入的 `GameConfigService#get()` **每次现读,不要缓存**(否则 `/sfcraft reload` 不生效)。注册时烘焙进组件的值(如耐久)天然不可热重载,属已知限制。需要在重载时重建的东西(如炼药锅配方)监听 `callback.SFConfigReload`。
- **i18n**:玩家可见文本一律 `Component.translatable`,键加进 `assets/sfcraft/lang/zh_cn.json` **和** `en_us.json`(⚠️ 越界,需许可)。**禁止**硬编码中文/英文 literal。不要引入 server-translations 依赖——玩家装服务器材质包,lang 由客户端解析。物品显示名 = `ITEM_NAME` 组件挂 translatable。
- **纯服务端**:假设代码总在服务端运行;玩家移动是客户端权威,服务端改玩家速度矢量要置 `entity.hurtMarked = true`。
- **纯服务端**:假设代码总在服务端运行;玩家移动是客户端权威,服务端改玩家速度矢量要置 `entity.syncVelocity = true`(26.2 及以前叫 `hurtMarked`)。
## 踩过的坑
**26.3 变更**(相对 26.2,本次移植踩到的;写代码前先 javap 确认):
- **方块 codec 体系整个删了**:`Block` 不再有 `MapCodec CODEC` / `codec()`,`simpleCodec()` 也没了 —— 自定义方块直接删掉这两段即可(原版 `LayeredCauldronBlock` 同样已无 codec)
- `Entity.hurtMarked` 字段 → **`Entity.syncVelocity`**(仍是 public 字段,纯改名);`markHurt()` 照旧 protected
- `LivingEntity.drop(ItemStack, boolean)` → **`drop(ItemStack, boolean, Prediction)`**(`net.minecraft.util.Prediction`,枚举 `PREDICTED`/`SERVER_ONLY`)。服务端逻辑自己发起的掉落用 `SERVER_ONLY`,客户端没预测过
- `Entity.canSimulateMovement()` 变 **final**,改写点移到 **`getMoveSimulationType()`**(返回 `MoveSimulationType`,默认 `AUTHORITATIVE_SIDE`)。想让服务端始终模拟(纯服务端坐骑)返回 `AUTHORITATIVE_SIDE_AND_SERVER`
- 战利品数字提供器拆成 `providers.number.ints` / `.floats` 两个子包,且全部 `Holder` 化:`UniformGenerator.between(1.0F, 3.0F)` → **`ContextIntProviders.between(1, 3)`**(返回 `Holder<ContextIntProvider>`,`SetItemCountFunction.setCount` 现在收的就是它)
- `BonemealableBlock` 三个方法都多了一个 **`BonemealSource`** 参数(`INTERACTION`/`MOB`)
- `MinecraftServer.clockManager()` 的 `getTotalTicks(clock)` 没了 → **`clockManager().getInstance(clock).totalTicks()`**(`ClockInstance` 接口:`totalTicks/partialTick/rate/isPaused`);`addTicks`/`setTotalTicks` 等写操作仍在 manager 上
- `EnderMan` → **`Enderman`**(只是大小写,包没动)
**26.2 Mojmap 重命名**(相对 1.21.x,写代码前先 javap 确认):
- `ResourceLocation` → `net.minecraft.resources.Identifier`(`Identifier.fromNamespaceAndPath`)
@@ -76,7 +86,7 @@ javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-me
- `MobEffects.SLOWNESS`(不是 MOVEMENT_SLOWDOWN);`SimpleContainer` 没有 addListener 了(覆写 `setChanged()` 持久化)
- `GameProfile` 是 record(`name()`/`id()`);`sendOverlayMessage`/`sendSystemMessage` 取代 `displayClientMessage`;命令权限 `Commands.LEVEL_GAMEMASTERS.check(source.permissions())`
**Polymer(0.17.3+26.2)**:
**Polymer(0.18.2+26.3)**:
- `PacketContext` 在 `net.fabricmc.fabric.api.networking.v1.context.PacketContext`
- BE 类型必须 `PolymerBlockUtils.registerBlockEntity`,实体类型必须 `PolymerEntityUtils.registerType`(`RegistryHelper` 已封装)
+1 -1
View File
@@ -1,6 +1,6 @@
# SFCraft 架构
Fabric **纯服务端** mod(`fabric.mod.json` 中 `environment: "server"`),Minecraft **26.2**,Java 25,**官方 Mojang 映射**。所有自定义内容通过 [Polymer](https://polymer.pb4.eu) 伪装成原版内容;玩家使用原版客户端 + 服务器发布的材质包即可游玩。
Fabric **纯服务端** mod(`fabric.mod.json` 中 `environment: "server"`),Minecraft **26.3**,Java 25,**官方 Mojang 映射**。所有自定义内容通过 [Polymer](https://polymer.pb4.eu) 伪装成原版内容;玩家使用原版客户端 + 服务器发布的材质包即可游玩。
## 分层
+12 -6
View File
@@ -8,20 +8,26 @@ RUN curl -fsSLo /packwiz-installer.jar https://github.com/packwiz/packwiz-instal
COPY --from=hengyunabc/arthas:4.1.1-no-jdk /opt/arthas /opt/arthas
FROM base AS sfcraft-builder
RUN git clone https://github.com/saltedfishclub/sfcraft /mod
COPY . /mod
RUN cd /mod && \
./gradlew shadowJar && \
cp /mod/build/libs/sfcraft-*-all.jar /sfcraft.jar
FROM base AS justbackup-builder
RUN git clone https://github.com/saltedfishclub/justbackup /mod
# 跟随 MC 版本的分支(默认分支仍是 26.2,fabric.mod.json 要求 minecraft ~26.2)。
# 分支不存在时这里会直接失败——刻意如此,比把 26.2 的 mod 打进 26.3 镜像、到加载时才报错好。
ARG JUSTBACKUP_REF=26.3
RUN git clone -b "$JUSTBACKUP_REF" https://github.com/saltedfishclub/justbackup /mod
RUN cd /mod && \
./gradlew build && \
rm -f /mod/build/libs/*sources.jar /mod/build/libs/*dev.jar && \
cp /mod/build/libs/justbackup-1.0.0.jar /justbackup.jar
cp /mod/build/libs/justbackup-*.jar /justbackup.jar
FROM base AS carpet-builder
RUN git clone https://github.com/saltedfishclub/fabric-carpet /mod
# 同上。carpet 的 depends 写的是 minecraft >26.2,旧分支能装上但 mixin 是按旧版编译的,
# 跑在 26.3 上会在 mixin apply 阶段炸,所以同样必须切到 26.3 分支(= 我们的补丁 + 合入上游 v26.3)。
ARG CARPET_REF=26.3
RUN git clone -b "$CARPET_REF" https://github.com/saltedfishclub/fabric-carpet /mod
RUN cd /mod && \
./gradlew build && \
rm -f /mod/build/libs/*sources.jar /mod/build/libs/*dev.jar && \
@@ -29,7 +35,7 @@ RUN cd /mod && \
FROM base
WORKDIR /deployment
COPY ./pack /deployment/pack
COPY server-deploy/pack /deployment/pack
RUN MC_VERSION="$(grep '^minecraft' /deployment/pack/pack.toml | head -n1 | cut -d'"' -f2)" && \
LOADER_VERSION="$(grep '^fabric' /deployment/pack/pack.toml | head -n1 | cut -d'"' -f2)" && \
if [ -z "$MC_VERSION" ] || [ -z "$LOADER_VERSION" ]; then \
@@ -38,7 +44,7 @@ RUN MC_VERSION="$(grep '^minecraft' /deployment/pack/pack.toml | head -n1 | cut
echo "Installing Fabric server (MC=$MC_VERSION, loader=$LOADER_VERSION)" && \
java -jar /fabric-installer.jar server -mcversion "$MC_VERSION" -loader "$LOADER_VERSION" -downloadMinecraft
RUN java -jar /packwiz-installer.jar -g -s server /deployment/pack/pack.toml
COPY ./overrides /deployment
COPY server-deploy/overrides /deployment
COPY --from=sfcraft-builder /sfcraft.jar /deployment/mods/sfcraft.jar
COPY --from=justbackup-builder /justbackup.jar /deployment/mods/justbackup.jar
COPY --from=carpet-builder /fabric-carpet.jar /deployment/mods/fabric-carpet.jar
+10 -14
View File
@@ -1,23 +1,19 @@
# SFCraft
# Welcome to SFCraft
SFCraft is a fabric mod tailored for the Minecraft server of the same name, including many fixes, features and other functionality.
Salted Fish (Club) Craft is a whitelisted, vanilla flavour Minecraft Server.
**It is not recommended to use this mod on your server without proper configuration!!**
[Wiki](https://sfclub.cc/~sfcraft/vanilla/) | [How-to Join](https://sfclub.cc/~sfcraft/guide/join/) | [Web Map](https://mc.sfclub.cc/map/)
This project is open source to allow players to contribute. [Contribution GUIDE](./CONTRIBUTING.md)
This repository contains the server mod implementing most of our features and server publish pipeline.
You may also need our source if you're implementing some of the features listed below.
Contributions are welcomed. A roughly written contributing guide can be found at [CONTRIBUTING.md](./CONTRIBUTING.md). Not only for agents, AGENTS.md and ARCHITECTURE.md are helpful to new-comers human too.
# Features
# About the server
1. Hybrid offline & online authentication: Check [ServerLoginNetworkHandlerMixin](src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java)
2. Worldwide regions, which divide the server into multiple "rooms" with data strictly isolated: See [this blog](https://blog.0w0.ing/2024/07/17/multiserver-based-on-one-utilizing-transfer/)
The implementation is mainly in [subserver](src/main/java/io/ib67/sfcraft/mixin/server/subserver/)
3. Server-side entities, the custom entities, but tied to the vanilla module: Check and look for references to [SFEntityType] (src/main/java/io/ib67/sfcraft/SFEntityType.java)
4. .. and more.
Custom features are often generously opened to every players without complex anti-abuse mechanism. We only accept new players that wouldn't destroy, steal other players' assets or exploit severe bugs in our server.
Besides the above, there are also many tiny features in the [module](src/main/java/io/ib67/sfcraft/module/) package.
If you want to apply to join our server, write a introduction of yourself to [iceBear67](mailto:icebear67@sfclub.cc) and we'll reply soon.
# About our server
# License
SFCraft is a whitelisted Minecraft server. To join us, contact [@ib67_pm_bot](https://t.me/ib67_pm_bot) in telegram (We chat in Chinese.)
Our original art assets are all right reserved, which are located at src/main/resources/assets. Beside those are licensed under MIT.
+2 -2
View File
@@ -2,7 +2,7 @@ plugins {
id 'maven-publish'
id("io.freefair.lombok") version "9.5.0"
id("com.gradleup.shadow") version "9.3.1"
id 'net.fabricmc.fabric-loom' version '1.17-SNAPSHOT'
id 'net.fabricmc.fabric-loom' version '1.18.2'
}
version = project.mod_version
@@ -37,7 +37,7 @@ dependencies {
shadow("org.apache.commons:commons-compress:1.28.0")
shadow("io.ib67.kiwi:lang:1.2.3-SNAPSHOT")
shadow('com.github.houbb:pinyin:0.4.0')
def polymerVersion = "0.17.3+26.2"
def polymerVersion = "0.18.2+26.3"
implementation include("eu.pb4:polymer-core:$polymerVersion")
implementation include("eu.pb4:polymer-virtual-entity:$polymerVersion")
implementation include("eu.pb4:polymer-resource-pack:$polymerVersion")
+4 -4
View File
@@ -3,15 +3,15 @@ org.gradle.jvmargs=-Xmx2G
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=26.2
loader_version=0.19.3
minecraft_version=26.3
loader_version=0.19.5
# Mod Properties
mod_version=2.1
mod_version=2.2
maven_group=io.ib67
archives_base_name=sfcraft
# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.155.0+26.2
fabric_version=0.161.0+26.3
+1 -1
View File
@@ -1,6 +1,6 @@
#Wed Jul 22 12:25:27 GMT+08:00 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
#Minecraft server properties
#Tue Jul 21 19:16:01 UTC 2026
accepts-transfers=true
allow-flight=false
allow-flight=true
allow-nether=true
broadcast-console-to-ops=true
broadcast-rcon-to-ops=true
@@ -0,0 +1,9 @@
§d§lSFCraft 已更新到 2.2。
§a主要改动:
- 创造游乐园新增时间控制指令 /playgrd time,支持 set/add/query/day/noon/night/midnight,且与主世界时间相互独立
- 投影上传现在支持最新版 Litematica 导出的 .litematic 文件
- 修复了部分投影转换后在末尾多出空气方块的问题
- 修复了「代偿图腾」物品名颜色显示异常的问题
具体信息请查阅我们的更新日志:
§bhttps://sfclub.cc/~sfcraft/changelog/2026-08-08/
@@ -0,0 +1,8 @@
§d§lSFCraft 已更新到 2.3。
§a主要改动:
- 修复了非被斩首怪物掉落头颅的问题
- 修复了 4K 图片无法下载到服务器中的问题。现在高分辨率图片会被降采样
- 修复了鞘翅撞进地狱门需要有空隙的问题
具体信息请查阅我们的更新日志:
§bhttps://sfclub.cc/~sfcraft/changelog/2026-08-24/
+1 -1
View File
@@ -1,2 +1,2 @@
§b§lSaltedFish Club Server §r§8| §f26.2 §c§lv2.1
§b§lSaltedFish Club Server §r§8| §f26.3 §c§lv2.2
§8§lINVITED PLAYERS ONLY. 服务器启用白名单.
+10 -10
View File
@@ -2,7 +2,7 @@ hash-format = "sha256"
[[files]]
file = "mods/fabric-api.pw.toml"
hash = "dc5490cc1a9231b0d32ec3e72f9d34279fc1d1ff6fb5a7d288b1f36faec0a6a0"
hash = "6b342a83e9736f0832e750c8d631273cf600305d73b32c8bd6a977811f6972ee"
metafile = true
[[files]]
@@ -12,12 +12,12 @@ metafile = true
[[files]]
file = "mods/farmers-delight-polymer.pw.toml"
hash = "ce231450983fd9afc2fef902a368394e9eea69b3c2772e5cdcabf777e8a2662c"
hash = "c4a9d832bf4d80391d4914ff520602a00fb55710cf6ec2e02fc4a21a19dea3ad"
metafile = true
[[files]]
file = "mods/farmers-delight-refabricated.pw.toml"
hash = "1500f8fc3792a46226c0559af0925cf4da5f11b7224ec2dcaad8b66bc0c5ca4d"
hash = "f4040aeb20676a8eafdeef154891acb2e236ab4a97632b76fac4750824df9f1e"
metafile = true
[[files]]
@@ -27,7 +27,7 @@ metafile = true
[[files]]
file = "mods/jei.pw.toml"
hash = "6ed45647ca366b832d80de4f02d16dd5899ba165459b1870729be04a260515d1"
hash = "cbe64d3867ec3f88c771fafaaa9f9a03febdee27d02b58096371357e9f643f58"
metafile = true
[[files]]
@@ -37,7 +37,7 @@ metafile = true
[[files]]
file = "mods/lithium.pw.toml"
hash = "3b4ab92dd5c39fbe0f5af3d73f0d7f3df3e2450842055353b0a5dc8fb9dae534"
hash = "3a00db6d12bebcafc8a38bde195dcd05a96509a9d563b53c9e0c098b299f2550"
metafile = true
[[files]]
@@ -47,25 +47,25 @@ metafile = true
[[files]]
file = "mods/polymer.pw.toml"
hash = "2a5626a6d9b61f968f294466e9756fbafa1c1208b4fb53c53cfc8d2afb42a857"
hash = "4559bae692a7d7b5c675f0ea199c4dd3506b9f14889748dc029abbfb605e5f7a"
metafile = true
[[files]]
file = "mods/simple-voice-chat.pw.toml"
hash = "be7369da963f5326a771bbfa360365ac6ed939b01c2c268f4dd4a922718cd4a3"
hash = "e55e25489644c7297a7f4cda6618437891fd0aba88e7f670a0439c3eadc14fc0"
metafile = true
[[files]]
file = "mods/squaremap.pw.toml"
hash = "798b2e1aeee88e52cc70cca4c51134639cebd214a3f52e0a0913e51b0b1adbe3"
hash = "014c8f8c8defcda9f7a6f1bd4a970d57828f9cd17ab131d5296f49a18dbb85c1"
metafile = true
[[files]]
file = "mods/syncmatica.pw.toml"
hash = "a8cfd56f9a2a99a9b93e05e41decad8c4b245bba7ed0b27a3032e5d6d1a09e23"
hash = "1063819c4a7de56b72384a52615bc2dc7818c44b5791072b5b315ca372308fca"
metafile = true
[[files]]
file = "mods/worldedit.pw.toml"
hash = "3754793742766dd72246e1a57abfa5c2d57e8a9d7dfecd1e9adc26251fac0b36"
hash = "bf86a568e9d9c647113d3be42de02b4490aa62fd5c61b7e24a86f3ddb57e9e62"
metafile = true
+4 -4
View File
@@ -1,13 +1,13 @@
name = "Fabric API"
filename = "fabric-api-0.155.2+26.2.jar"
filename = "fabric-api-0.161.0+26.3.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/lVXlbH4w/fabric-api-0.155.2%2B26.2.jar"
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/bNnaTiuM/fabric-api-0.161.0%2B26.3.jar"
hash-format = "sha512"
hash = "cc56984378a27c5bcd56374d6ffbb27a45c6bf3355add2ac6be9817ccac5854362249bf9d0147eb271a70fda2716129204e240d53c9aa876a2a7861f4c7f880f"
hash = "ed6b2586d6fde11fde8472f5a527c51e99b67026e46f94d4bfd85e7e28ce5ee299173ee16ad576ceb51f39f98d30a811086a6deb1a86a524859cc16e12da109d"
[update]
[update.modrinth]
mod-id = "P7dR8mSH"
version = "lVXlbH4w"
version = "bNnaTiuM"
@@ -1,13 +1,13 @@
name = "Polymer Patch for Farmer's Delight"
filename = "farmers-delight-polymer-patch-3.6.7.0+26.2.jar"
filename = "farmers-delight-polymer-patch-3.6.21.0+26.3.jar"
side = "server"
[download]
url = "https://cdn.modrinth.com/data/TE3NCFiY/versions/11AvsXE3/farmers-delight-polymer-patch-3.6.7.0%2B26.2.jar"
url = "https://cdn.modrinth.com/data/TE3NCFiY/versions/ZqkEEPvU/farmers-delight-polymer-patch-3.6.21.0%2B26.3.jar"
hash-format = "sha512"
hash = "9e04f5c68e4209bd0e1de932858ebf4273c369b9943e38cbce8665311d7ff7d3fdfe20493e78769d9b92047678dff995e2d5d998f9978ab73896321ed125e09d"
hash = "b65bf9a9271eee5a231a3c221dc7297835a524edf44507f96ddb1808b4a547361d5a87edec226a054769d78394e5feec73281bbc2999a38d3280484bcd2eb522"
[update]
[update.modrinth]
mod-id = "TE3NCFiY"
version = "11AvsXE3"
version = "ZqkEEPvU"
@@ -1,13 +1,13 @@
name = "Farmer's Delight Refabricated"
filename = "FarmersDelight-26.2-3.6.8+refabricated.jar"
filename = "FarmersDelight-26.3-3.6.27+refabricated.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/7vxePowz/versions/PR6Lz8x8/FarmersDelight-26.2-3.6.8%2Brefabricated.jar"
url = "https://cdn.modrinth.com/data/7vxePowz/versions/hTNvMewX/FarmersDelight-26.3-3.6.27%2Brefabricated.jar"
hash-format = "sha512"
hash = "dbf6b0333d91c662c5657043b1f43865f1458c467921403b416df297e4335d3b7387b1bc52abeec76fa7414836a3a1a3bfb68aa8acc45694dfaf522c5d316c84"
hash = "d7c66b883e3c900a5539b2fec91c8a81d73ad438437151019843b958584f4c45bcb29581846056ff83787efea53fe0cd244106b4856a9979d7088eebba54da44"
[update]
[update.modrinth]
mod-id = "7vxePowz"
version = "PR6Lz8x8"
version = "hTNvMewX"
+4 -4
View File
@@ -1,13 +1,13 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.13.0.79.jar"
filename = "jei-26.3-fabric-31.7.0.40.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/RId9q9Qv/jei-26.2-fabric-30.13.0.79.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/1QosOWso/jei-26.3-fabric-31.7.0.40.jar"
hash-format = "sha512"
hash = "91460805a9b161272fbcd590ffefd9e0ec01dd00ec665311d7c1b5420d648435dfd624f62453aee3642cf7b8092a6d4b8447189271b7b5c6e437e2fb17166b5d"
hash = "1e64160d88effd1c7d33fc43aeac01f375ce45613e4f7486d7d98465c947656233a931dcf5633918ed13aba106382d57b9b6d02be16e030662110153f0772951"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "RId9q9Qv"
version = "1QosOWso"
+7 -7
View File
@@ -1,13 +1,13 @@
name = "Lithium (Fabric/NeoForge)"
filename = "lithium-fabric-0.25.2+mc26.2.jar"
filename = "lithium-fabric-0.26.1+mc26.3.jar"
side = "both"
[download]
hash-format = "sha1"
hash = "3bcc94183d7068395de08915fd5fce8d199aaa50"
mode = "metadata:curseforge"
url = "https://cdn.modrinth.com/data/gvQqBUqZ/versions/WXHRsMRl/lithium-fabric-0.26.1%2Bmc26.3.jar"
hash-format = "sha512"
hash = "acbb9b037a203f005e03a20bf1d9866019384abb5ad27664808a12b919639a2501ecb52f8f0d77d27e1409935b0dbdbb70e01ac466480c0ae421ee403f649c59"
[update]
[update.curseforge]
file-id = 8409186
project-id = 360438
[update.modrinth]
mod-id = "gvQqBUqZ"
version = "WXHRsMRl"
+4 -4
View File
@@ -1,13 +1,13 @@
name = "Polymer"
filename = "polymer-bundled-0.17.3+26.2.jar"
filename = "polymer-bundled-0.18.2+26.3.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/xGdtZczs/versions/w0N4I45x/polymer-bundled-0.17.3%2B26.2.jar"
url = "https://cdn.modrinth.com/data/xGdtZczs/versions/REBDssAz/polymer-bundled-0.18.2%2B26.3.jar"
hash-format = "sha512"
hash = "1459edf99834bbbb2eff5f7df2e7159688ad88e20e8704bc473752e81a755963bb96c511641d53c9f7b4e5867436f7b49ef839e3b857c48ca54af93627c2a110"
hash = "a83bec0ef7ee4c905429f102a9a1b2ff9db60fb23f3e6b5a26ff4442b0427c3edd821f311643a3c377541cfd5a4753519001cbfebbeb78014a77041d91449f51"
[update]
[update.modrinth]
mod-id = "xGdtZczs"
version = "w0N4I45x"
version = "REBDssAz"
@@ -1,13 +1,13 @@
name = "Simple Voice Chat"
filename = "voicechat-fabric-2.6.21+26.2.jar"
filename = "voicechat-fabric-2.6.23+26.3.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/9eGKb6K1/versions/3SOh5iiX/voicechat-fabric-2.6.21%2B26.2.jar"
url = "https://cdn.modrinth.com/data/9eGKb6K1/versions/3fRw6NfP/voicechat-fabric-2.6.23%2B26.3.jar"
hash-format = "sha512"
hash = "0308b2e18a05913faaf461530a475d2f5debbfb4d0a46107b512ce51166ceb6cfdbea468f7e21444a3e005655e51729c3764266f81c59b6228629b4866d16b28"
hash = "fd62ca0b4048ab855e7f2436ecdf6c7301e546946afe2d7a3dfa3fb7c180636cbd971c0e9c5e62be4a86d67bbed54fa62ab4d4680c47ea0b366e49b9202ff42a"
[update]
[update.modrinth]
mod-id = "9eGKb6K1"
version = "3SOh5iiX"
version = "3fRw6NfP"
+4 -4
View File
@@ -1,13 +1,13 @@
name = "squaremap"
filename = "squaremap-fabric-mc26.2-1.3.14.jar"
filename = "squaremap-fabric-mc26.3-1.4.0.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/PFb7ZqK6/versions/VbqnAdLU/squaremap-fabric-mc26.2-1.3.14.jar"
url = "https://cdn.modrinth.com/data/PFb7ZqK6/versions/FJzk5i6o/squaremap-fabric-mc26.3-1.4.0.jar"
hash-format = "sha512"
hash = "db0345134c109ccd2bc7fe67ae35dc561655ff25a14a42525d655012696bd10eaa999431b50a84335cc5960ec12ff5d34dddb75ee11bcf36252ac89cff86e71f"
hash = "c7ccc0a8f663def6c655832f92d0cfea0fc26c5e7cbfcb88ce1fe787c38296b50f5a9665eaa17fa8e4b634f2151819d55b6aca1ee2eb92f59c8378020cecba09"
[update]
[update.modrinth]
mod-id = "PFb7ZqK6"
version = "VbqnAdLU"
version = "FJzk5i6o"
+4 -4
View File
@@ -1,13 +1,13 @@
name = "Syncmatica"
filename = "syncmatica-fabric-26.2-0.3.19.jar"
filename = "syncmatica-fabric-26.3-0.3.20.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/bfneejKo/versions/f74T22XS/syncmatica-fabric-26.2-0.3.19.jar"
url = "https://cdn.modrinth.com/data/bfneejKo/versions/uT7TJ2Wp/syncmatica-fabric-26.3-0.3.20.jar"
hash-format = "sha512"
hash = "e44660878dfdf80b2091e6206715cac0658dcc38b2b80ab36bd3e28e3a34a17c727f7f2206dc01d9d84f73096ee49656e51e1c47669d7e33f67f096e3a44e12a"
hash = "7eb067b457123a221be27f1a41ea2480b8380ac07d3aba4d963343405b83ccfba26f8c292c680e48b8d52b97ddb37fca4ebd8e57e86c5ab89af58f3d229fafaa"
[update]
[update.modrinth]
mod-id = "bfneejKo"
version = "f74T22XS"
version = "uT7TJ2Wp"
+4 -4
View File
@@ -1,13 +1,13 @@
name = "WorldEdit"
filename = "worldedit-mod-7.4.4.jar"
filename = "worldedit-mod-7.4.6-beta-02.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/1u6JkXh5/versions/j2w3GmPv/worldedit-mod-7.4.4.jar"
url = "https://cdn.modrinth.com/data/1u6JkXh5/versions/E7sbeRFx/worldedit-mod-7.4.6-beta-02.jar"
hash-format = "sha512"
hash = "48e408fbf3e24521f2a66155e9a85a71f5bf89657a9888aac9525e250c5dd9541d8326afbbc24eae4241dba83506a2438ebff60f0e763e37ecef8e070fa86e57"
hash = "f7de9f7658319f38a9d0211a2592c91d412e98599d1ff378968509ccf0aa8f1ce7ab7e669f5c866552676d2a7a23d3cfdd16b54ee1a50d22c14fce8cfa30bba7"
[update]
[update.modrinth]
mod-id = "1u6JkXh5"
version = "j2w3GmPv"
version = "E7sbeRFx"
+4 -4
View File
@@ -6,11 +6,11 @@ pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "d7c215cba51d8e9324316d5c236c6f03727012917340b2100c868614170ba69e"
hash = "45ebe64136ab754ef8b6b423de6242c174d6bc78230b48767904b2f67dcaaa71"
[versions]
fabric = "0.19.3"
minecraft = "26.2"
fabric = "0.19.5"
minecraft = "26.3"
[options]
acceptable-game-versions = ["26.1", "26.1.2"]
acceptable-game-versions = ["26.2"]
@@ -26,14 +26,16 @@ public class GameConfig {
}
public static class MapArt {
// 单张图片的下载体积上限(字节)
public int maxDownloadBytes = 8388608;
// 单张图片的下载体积上限(字节),边收边计、超限立刻断开连接并提示玩家。
// 16 MiB:4K JPEG 通常 1~5 MB,照片类 4K PNG 约 12 MB;只有不可压缩的极端 PNG(~24 MB)会被拒
public int maxDownloadBytes = 16777216;
// 全服同时进行中的图片下载+解码数上限,超出时新请求直接失败提示稍后重试(不排队)
public int maxConcurrentRenders = 2;
// HTTP 超时(秒)
public int httpTimeoutSeconds = 15;
// 输入图片单边像素上限(解码像素前按头部尺寸拦截,防爆内存);成品按宽高比在 4x4 格内自动选幅画
public int maxImageDimension = 2048;
// 输入图片单边像素上限(解码像素前按头部尺寸拦截,防解压炸弹);4096 覆盖 4K 素材。
// 成品按宽高比在 4x4 格内自动选幅画;解码时按目标画布降采样,故调高此值不会等比推高解码内存
public int maxImageDimension = 4096;
// 远端下载/解码失败的 URL 冷却秒数,期间同一地址不再发起请求
public int failureCooldownSeconds = 60;
// 铁砧生成地图画的经验等级花费
@@ -0,0 +1,26 @@
package io.ib67.sfcraft.mixin.common;
import io.ib67.sfcraft.network.HandshakeAddress;
import net.minecraft.network.Connection;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Connection 从握手一路存活到 play 阶段,拿来记玩家自报的进服地址。
*/
@Mixin(Connection.class)
public class ConnectionMixin implements HandshakeAddress.Holder {
@Unique
private HandshakeAddress sfcraft$handshakeAddress;
@Override
public @Nullable HandshakeAddress sfcraft$handshakeAddress() {
return sfcraft$handshakeAddress;
}
@Override
public void sfcraft$setHandshakeAddress(@Nullable HandshakeAddress address) {
this.sfcraft$handshakeAddress = address;
}
}
@@ -0,0 +1,12 @@
package io.ib67.sfcraft.mixin.common.bridge;
import net.minecraft.network.Connection;
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ServerCommonPacketListenerImpl.class)
public interface ServerCommonNetworkHandlerBridge {
@Accessor("connection")
Connection sfcraft$getConnection();
}
@@ -1,13 +1,13 @@
package io.ib67.sfcraft.mixin.common.fixes;
import io.ib67.sfcraft.mixin.common.MobEntityMixin;
import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.monster.Enderman;
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(EnderMan.class)
@Mixin(Enderman.class)
public abstract class EndermanEntityMixin extends MobEntityMixin {
@Inject(at = @At("HEAD"), method = "requiresCustomPersistence", cancellable = true)
private void sf$cannotDespawn(CallbackInfoReturnable<Boolean> cir) {
@@ -0,0 +1,72 @@
package io.ib67.sfcraft.mixin.gameplay;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.module.game.portal.ElytraPortalModule;
import io.ib67.sfcraft.module.game.portal.ElytraRushHolder;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.Vec3;
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.CallbackInfo;
/**
* 鞘翅冲门在「传送门后面有遮挡」这一情况下的两处补偿:
* <ol>
* <li>{@code travelFallFlying} 头部——此处还在 {@code move()} 之前,deltaMovement 尚未被墙裁剪,
* 把速度存进 {@link ElytraRushHolder} 快照,供下一 tick 的传送门判定使用;</li>
* <li>{@code handleFallFlyingCollisions} 头部——原版在 {@code move()} 之后按碰撞前后的速度差扣动能伤害,
* 玩家高速冲进传送门方块又撞上门后墙体的这一下不该算他的,直接取消。</li>
* </ol>
* 判定与配置读取都在 {@link ElytraPortalModule}。
*/
@Mixin(LivingEntity.class)
public abstract class ElytraRushMixin implements ElytraRushHolder {
@Unique
private Vec3 sfcraft$rushDelta;
@Unique
private int sfcraft$rushTick;
@Inject(method = "travelFallFlying", at = @At("HEAD"))
private void sfcraft$recordRushSpeed(Vec3 input, CallbackInfo ci) {
if ($this() instanceof ServerPlayer player) {
sfcraft$module().mixin$recordRushSpeed(player);
}
}
@Inject(method = "handleFallFlyingCollisions", at = @At("HEAD"), cancellable = true)
private void sfcraft$skipPortalWallImpact(double moveHorLength, double newMoveHorLength, CallbackInfo ci) {
if ($this() instanceof ServerPlayer player && sfcraft$module().shouldIgnoreWallImpact(player, moveHorLength)) {
ci.cancel();
}
}
@Override
public void sfcraft$recordElytraRush(Vec3 delta, int tick) {
sfcraft$rushDelta = delta;
sfcraft$rushTick = tick;
}
@Override
public Vec3 sfcraft$elytraRushDelta() {
return sfcraft$rushDelta;
}
@Override
public int sfcraft$elytraRushTick() {
return sfcraft$rushTick;
}
@Unique
private ElytraPortalModule sfcraft$module() {
return SFCraft.getInjector().getInstance(ElytraPortalModule.class);
}
@Unique
private LivingEntity $this() {
return (LivingEntity) (Object) this;
}
}
@@ -14,8 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* 鞘翅冲出下界传送门时跳过等待:本方法每 tick 被 {@code PortalProcessor} 调用来决定
* 「还要在门里待多久」,命中条件时覆写为 1 tick(与原版创造模式一致),下一 tick 即跨维度;
* 否则不动 cir。判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。
* 「还要在门里待多久」,命中条件时覆写成 0,本次 {@code handlePortal} 就跨维度;否则不动 cir。
* 判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。
*/
@Mixin(NetherPortalBlock.class)
public abstract class NetherPortalBlockMixin {
@@ -27,6 +27,8 @@ public abstract class NetherPortalBlockMixin {
}
var module = SFCraft.getInjector().getInstance(ElytraPortalModule.class);
if (module.shouldTeleportNow(entity)) {
// 门后有遮挡时玩家早被墙撞停了,趁传送前把速度补回去,出门那侧才能继续滑翔
module.restoreRushVelocity(entity);
cir.setReturnValue(module.getInstantTransitionTicks());
}
}
@@ -12,6 +12,7 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.entity.MoveSimulationType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -78,10 +79,12 @@ public abstract class HoglinMountMixin extends Animal implements MountAccess {
}
@Override
public boolean canSimulateMovement() {
// 骑手是 Player 时会被判为客户端权威,但纯服务端坐骑客户端不会模拟 → 强制服务端模拟
if (!level().isClientSide() && getControllingPassenger() != null) return true;
return super.canSimulateMovement();
public MoveSimulationType getMoveSimulationType() {
// 骑手是 Player 时会被判为客户端权威,但纯服务端坐骑客户端不会模拟 → 强制服务端模拟。
// 26.3 起 canSimulateMovement() 变 final,改写点移到这里:AUTHORITATIVE_SIDE_AND_SERVER
// 的语义就是"服务端永远模拟 + 权威端模拟",等价于原来的 !isClientSide 分支。
if (getControllingPassenger() != null) return MoveSimulationType.AUTHORITATIVE_SIDE_AND_SERVER;
return super.getMoveSimulationType();
}
@Override
@@ -12,6 +12,7 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.entity.MoveSimulationType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -82,10 +83,12 @@ public abstract class RavagerMountMixin extends Raider implements MountAccess {
}
@Override
public boolean canSimulateMovement() {
// 骑手是 Player 时会被判为客户端权威,但纯服务端坐骑客户端不会模拟 → 强制服务端模拟
if (!level().isClientSide() && getControllingPassenger() != null) return true;
return super.canSimulateMovement();
public MoveSimulationType getMoveSimulationType() {
// 骑手是 Player 时会被判为客户端权威,但纯服务端坐骑客户端不会模拟 → 强制服务端模拟。
// 26.3 起 canSimulateMovement() 变 final,改写点移到这里:AUTHORITATIVE_SIDE_AND_SERVER
// 的语义就是"服务端永远模拟 + 权威端模拟",等价于原来的 !isClientSide 分支。
if (getControllingPassenger() != null) return MoveSimulationType.AUTHORITATIVE_SIDE_AND_SERVER;
return super.getMoveSimulationType();
}
@Override
@@ -2,7 +2,9 @@ package io.ib67.sfcraft.mixin.server;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.callback.SFCallbacks;
import io.ib67.sfcraft.network.HandshakeAddress;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.handshake.ClientIntentionPacket;
import net.minecraft.network.protocol.status.ServerStatus;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerHandshakePacketListenerImpl;
@@ -11,7 +13,9 @@ 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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerHandshakePacketListenerImpl.class)
public abstract class ServerHandshakeNetworkHandlerMixin {
@@ -19,6 +23,15 @@ public abstract class ServerHandshakeNetworkHandlerMixin {
@Final
private Connection connection;
/**
* 记下玩家实际用来进服的地址,房间 transfer 时把他送回同一个入口。
*/
@Inject(method = "handleIntention", at = @At("HEAD"))
private void sf$captureAddress(ClientIntentionPacket packet, CallbackInfo ci) {
((HandshakeAddress.Holder) connection)
.sfcraft$setHandshakeAddress(HandshakeAddress.sanitize(packet.hostName(), packet.port()));
}
@Redirect(method = "handleIntention", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getStatus()Lnet/minecraft/network/protocol/status/ServerStatus;"))
public ServerStatus getMetadata(MinecraftServer instance) {
var result = SFCallbacks.MOTD.invoker().onMotd(instance,connection);
@@ -16,6 +16,7 @@ import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.block.BonemealableBlock;
import net.minecraft.world.level.block.BonemealSource;
public class FartFertilizerModule extends ServerModule {
private final Object2LongMap<Player> lastSneaked = new Object2LongOpenHashMap<>();
@@ -63,7 +64,7 @@ public class FartFertilizerModule extends ServerModule {
}
if (player.getRandom().nextInt(10) < 3) {
((BonemealableBlock) block).performBonemeal(wld,
wld.getRandom(), blockPos, state);
wld.getRandom(), blockPos, state, BonemealSource.INTERACTION);
}
}
}
@@ -10,6 +10,7 @@ import io.ib67.kiwi.tuple.Tuple2;
import io.ib67.sfcraft.ServerModule;
import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.network.HandshakeAddress;
import io.ib67.sfcraft.registry.RoomRegistry;
import lombok.extern.log4j.Log4j2;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
@@ -20,7 +21,6 @@ import net.minecraft.core.BlockPos;
import net.minecraft.network.PacketSendListener;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.common.ClientboundStoreCookiePacket;
import net.minecraft.network.protocol.common.ClientboundTransferPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.TicketType;
@@ -83,8 +83,9 @@ public class RoomModule extends ServerModule {
return 0;
}
var networkHandler = player.connection;
var transfer = HandshakeAddress.transferPacketFor(player, config);
networkHandler.send(new ClientboundStoreCookiePacket(ROOM_COOKIE, EMPTY), PacketSendListener.thenRun(() -> {
networkHandler.send(new ClientboundTransferPacket(config.domain, config.port));
networkHandler.send(transfer);
}));
return 1;
}
@@ -28,7 +28,7 @@ import java.util.Map;
import java.util.Optional;
/**
* 斩首附魔:持有该附魔的武器击杀敌怪(含玩家)时有概率掉落对应头颅。
* 斩首附魔:用带该附魔的武器打出致命一击击杀敌怪(含玩家)时有概率掉落对应头颅。
* 掉头逻辑纯服务端,附魔本身为数据驱动内容(data/sfcraft/enchantment/beheading.json),
* 无需代码注册,仅作等级标记。
*/
@@ -58,15 +58,20 @@ public class BeheadingModule extends ServerModule {
private void onDeath(LivingEntity victim, DamageSource source) {
if (!(victim.level() instanceof ServerLevel serverLevel)) return;
var killer = source.getEntity() instanceof LivingEntity le ? le : victim.getKillCredit();
if (killer == null) return;
// 只认「致命一击本身」用的武器:getWeaponItem() 取自伤害的直接来源实体
// (近战=攻击者主手,投射物=发射它的弓弩)。摔落/火焰/岩浆/爆炸/溺水等没有直接来源
// 或来源不持武器,一律返回 null/空,不会掉头。
// 旧写法退化到 getKillCredit() + 主手物品,导致玩家只要手持斩首武器,
// 生物死于摔落、着火、被别的怪打死等情况也会掉头。
var weapon = source.getWeaponItem();
if (weapon == null || weapon.isEmpty()) return;
var head = headFor(victim);
if (head == null) return;
var holder = getBeheading(serverLevel.registryAccess());
if (holder.isEmpty()) return;
int level = EnchantmentHelper.getItemEnchantmentLevel(holder.get(), killer.getMainHandItem());
int level = EnchantmentHelper.getItemEnchantmentLevel(holder.get(), weapon);
if (level <= 0) return;
var beheading = config.get().beheading;
@@ -20,7 +20,7 @@ import net.minecraft.world.level.storage.loot.LootPool;
import net.minecraft.world.level.storage.loot.entries.LootItem;
import net.minecraft.world.level.storage.loot.functions.SetItemCountFunction;
import net.minecraft.world.level.storage.loot.predicates.LootItemRandomChanceCondition;
import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator;
import net.minecraft.world.level.storage.loot.providers.number.ints.ContextIntProviders;
/**
* 三种投掷炸弹(普通/黑曜石/烈焰)与它们的战利品注入(地牢箱、苦力怕掉落)。
@@ -76,11 +76,11 @@ public class BombModule extends ServerModule {
tableBuilder.withPool(LootPool.lootPool()
.when(LootItemRandomChanceCondition.randomChance(loot.dungeonBombChance))
.add(LootItem.lootTableItem(bomb)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(1.0F, 3.0F)))));
.apply(SetItemCountFunction.setCount(ContextIntProviders.between(1, 3)))));
tableBuilder.withPool(LootPool.lootPool()
.when(LootItemRandomChanceCondition.randomChance(loot.dungeonObsidianBombChance))
.add(LootItem.lootTableItem(obsidianBomb)
.apply(SetItemCountFunction.setCount(UniformGenerator.between(1.0F, 2.0F)))));
.apply(SetItemCountFunction.setCount(ContextIntProviders.between(1, 2)))));
}
EntityTypes.CREEPER.getDefaultLootTable().ifPresent(creeperTable -> {
if (creeperTable.equals(key)) {
@@ -1,6 +1,5 @@
package io.ib67.sfcraft.module.game.cauldron;
import com.mojang.serialization.MapCodec;
import eu.pb4.polymer.core.api.block.PolymerBlock;
import eu.pb4.polymer.virtualentity.api.BlockWithElementHolder;
import eu.pb4.polymer.virtualentity.api.ElementHolder;
@@ -37,7 +36,6 @@ import java.util.function.BiFunction;
public class AmethystCauldronBlock extends BaseEntityBlock implements PolymerBlock, BlockWithElementHolder {
public static final BooleanProperty HAS_WATER = BooleanProperty.create("has_water");
public static final MapCodec<AmethystCauldronBlock> CODEC = simpleCodec(AmethystCauldronBlock::new);
// Polymer 会把该偏移加到方块中心(Vec3.atCenterOf), 因此这里是相对中心的偏移:
// 结果约为 (x+0.5, y+0.55, z+0.5), 即锅内水中、锅沿以下、水平居中。
private static final Vec3 ELEMENT_HOLDER_OFFSET = new Vec3(0.0, 0.05, 0.0);
@@ -57,11 +55,6 @@ public class AmethystCauldronBlock extends BaseEntityBlock implements PolymerBlo
this.blockEntityFactory = factory;
}
@Override
protected @NonNull MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(HAS_WATER);
@@ -25,6 +25,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.AABB;
import net.minecraft.util.Prediction;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
@@ -124,7 +125,7 @@ public class AmethystCauldronBlockEntity extends BlockEntity {
if (contents.isEmpty()) return InteractionResult.PASS;
var stack = contents.removeLast();
if (!player.getInventory().add(stack)) {
player.drop(stack, false);
player.drop(stack, false, Prediction.SERVER_ONLY);
}
level.playSound(null, getBlockPos(), SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 0.6F, 1.0F);
setChanged();
@@ -1,6 +1,5 @@
package io.ib67.sfcraft.module.game.crystal;
import com.mojang.serialization.MapCodec;
import eu.pb4.polymer.core.api.block.PolymerBlock;
import net.fabricmc.fabric.api.networking.v1.context.PacketContext;
import net.minecraft.core.BlockPos;
@@ -29,7 +28,6 @@ import java.util.function.BiFunction;
public class GravityCrystalBlock extends BaseEntityBlock implements PolymerBlock {
public static final IntegerProperty CHARGE = RespawnAnchorBlock.CHARGE;
public static final MapCodec<GravityCrystalBlock> CODEC = simpleCodec(GravityCrystalBlock::new);
// GravityCrystalModule 注册方块实体类型后回填(方块与其方块实体类型互相引用,无法在构造器闭合)
private BlockEntityType<GravityCrystalBlockEntity> blockEntityType;
@@ -46,11 +44,6 @@ public class GravityCrystalBlock extends BaseEntityBlock implements PolymerBlock
this.blockEntityFactory = factory;
}
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(CHARGE);
@@ -56,7 +56,7 @@ public class GravityCrystalBlockEntity extends BlockEntity {
boosted = boosted.normalize().scale(maxSpeed);
}
entity.setDeltaMovement(boosted);
entity.hurtMarked = true;
entity.syncVelocity = true;
}
}
@@ -34,10 +34,12 @@ import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import net.minecraft.util.Prediction;
import org.jspecify.annotations.Nullable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
@@ -45,8 +47,11 @@ import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -54,8 +59,10 @@ import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
/**
@@ -70,7 +77,8 @@ import java.util.concurrent.TimeUnit;
* </ul>
* 玩家拿到的是单张"种子"地图(128x128 整图预览);上墙后每格替换为 128x128 的局部清晰切片。
* 输入单边像素上限见
* {@code GameConfig.MapArt#maxImageDimension},在解码像素前先读头部尺寸拦截,防止解压炸弹。
* {@code GameConfig.MapArt#maxImageDimension}(默认 4096,覆盖 4K 素材),在解码像素前先读头部尺寸
* 拦截,防止解压炸弹;放行后按目标画布降采样解码({@link #readSubsampled}),峰值内存与该上限脱钩。
* 生成物是普通 {@code FILLED_MAP}+{@code MAP_ID}/{@code CUSTOM_DATA} 组件,纯原版物品,无需 Polymer。
* 名称走 {@code ITEM_NAME}(玩家不再看到每格"地图画"名牌,也不能再被铁砧重命名);
* 上墙的切片用 {@code TOOLTIP_DISPLAY} 隐藏 {@code MAP_ID},工具提示只剩名称一行。
@@ -521,7 +529,7 @@ public class MapArtModule extends ServerModule {
private static void give(ServerPlayer player, ItemStack stack) {
if (!player.getInventory().add(stack)) {
player.drop(stack, false);
player.drop(stack, false, Prediction.SERVER_ONLY);
}
}
@@ -575,19 +583,22 @@ public class MapArtModule extends ServerModule {
.build();
int maxBytes = Math.max(1024, settings.maxDownloadBytes);
try {
return httpClient.sendAsync(request,
HttpResponse.BodyHandlers.buffering(HttpResponse.BodyHandlers.ofByteArray(), maxBytes))
return httpClient.sendAsync(request, info -> new BoundedBodySubscriber(maxBytes))
.thenApply(response -> {
if (response.statusCode() / 100 != 2) {
markFailed(url);
return RenderOutcome.fail(RenderFailure.DOWNLOAD_FAILED, "HTTP " + response.statusCode());
}
if (response.body().overflow()) {
markFailed(url);
return RenderOutcome.fail(RenderFailure.BODY_TOO_LARGE, formatBytes(maxBytes));
}
var contentType = response.headers().firstValue("content-type").orElse("");
if (!contentType.isBlank() && !contentType.regionMatches(true, 0, "image/", 0, 6)) {
markFailed(url);
return RenderOutcome.fail(RenderFailure.NOT_IMAGE, null);
}
return decode(url, response.body());
return decode(url, response.body().bytes());
})
.whenComplete((outcome, error) -> renderSlots.release());
} catch (Exception e) {
@@ -599,7 +610,8 @@ public class MapArtModule extends ServerModule {
}
/**
* 先读图片头部尺寸(不解码像素)拦截超大图,通过后再栅格化到整幅墙画画布。
* 先读图片头部尺寸(不解码像素)拦截超大图,通过后按目标画布降采样解码
* (见 {@link #readSubsampled}),再栅格化到整幅墙画画布。
*/
private RenderOutcome decode(String url, byte[] body) {
int maxDimension = configService.get().mapArt.maxImageDimension;
@@ -626,10 +638,10 @@ public class MapArtModule extends ServerModule {
markFailed(url);
return RenderOutcome.fail(RenderFailure.TOO_LARGE, width + "x" + height);
}
var image = reader.read(0);
var grid = gridFor(width, height);
int canvasWidth = grid[0] * MapArtRenderer.MAP_SIZE;
int canvasHeight = grid[1] * MapArtRenderer.MAP_SIZE;
var image = readSubsampled(reader, width, height, canvasWidth, canvasHeight);
var canvas = grid[0] == 1 && grid[1] == 1
? MapArtRenderer.render(image)
: MapArtRenderer.render(image, canvasWidth, canvasHeight);
@@ -647,6 +659,33 @@ public class MapArtModule extends ServerModule {
}
}
/**
* 按目标画布尺寸降采样解码。目标画布最大也只有 512x512,而 4K 输入全分辨率解码要 64 MiB 的
* ARGB 缓冲——解码期每 step 个像素取一个就够,峰值内存与输入边长上限脱钩。
* 刻意留 2 倍余量交给 {@link MapArtRenderer#render} 的双线性缩放:恰好 2 倍下采样的双线性
* 等价于 2x2 盒式平均,比一路点采样到目标尺寸的锯齿少得多。
*/
private static BufferedImage readSubsampled(ImageReader reader, int width, int height,
int canvasWidth, int canvasHeight) throws IOException {
int step = Math.max(1, Math.min(width / (canvasWidth * 2), height / (canvasHeight * 2)));
if (step == 1) {
return reader.read(0); // 本就接近画布尺寸,不必绕 ImageReadParam
}
var param = reader.getDefaultReadParam();
param.setSourceSubsampling(step, step, 0, 0);
return reader.read(0, param);
}
/**
* 人类可读的体积上限,拼进玩家提示。单位符号留在代码里(不是需要翻译的散文),
* lang 那边只留 "%s" 的位置。
*/
private static String formatBytes(int bytes) {
return bytes >= 1024 * 1024
? (bytes / (1024 * 1024)) + " MiB"
: Math.max(1, bytes / 1024) + " KiB";
}
/**
* 远端失败(网络/非图片/超大)后进入短时黑名单,避免玩家反复触发下载。
*/
@@ -665,6 +704,8 @@ public class MapArtModule extends ServerModule {
case COOLDOWN -> Component.translatable("message.sfcraft.mapart.error.cooldown", outcome.detail());
case TOO_LARGE -> Component.translatable("message.sfcraft.mapart.error.too_large",
outcome.detail(), configService.get().mapArt.maxImageDimension);
case BODY_TOO_LARGE ->
Component.translatable("message.sfcraft.mapart.error.body_too_large", outcome.detail());
case NOT_IMAGE -> Component.translatable("message.sfcraft.mapart.error.not_image");
case DOWNLOAD_FAILED -> Component.translatable("message.sfcraft.mapart.error.download_failed",
outcome.detail() == null ? "?" : outcome.detail());
@@ -732,6 +773,87 @@ public class MapArtModule extends ServerModule {
private record Rendered(int cols, int rows, byte[] canvas) {
}
/**
* 收完的响应体。{@code overflow} 为真表示超过了 {@code GameConfig.MapArt#maxDownloadBytes},
* 上游已取消、字节已丢弃,{@code bytes} 为空。
*/
private record Downloaded(byte[] bytes, boolean overflow) {
private static final Downloaded OVERFLOW = new Downloaded(new byte[0], true);
}
/**
* 真正封顶的响应体收集器。
* <p>
* 注意 {@code BodyHandlers.buffering(downstream, size)} 的 size <b>不是</b>体积上限,只是"攒够多少
* 字节再往下游递"的缓冲块大小;下游 {@code ofByteArray} 依旧会把整个响应体收进内存。铁砧那条路径
* 不需要 OP 权限(任何玩家把地图改名成 URL 就能触发下载),所以这里必须自己数字节:一超上限就
* {@code cancel} 上游、丢掉已收分片,让下游拿到 {@link Downloaded#OVERFLOW}。
* <p>
* 全程非阻塞(不在 HttpClient 的 executor 上做阻塞读),因此不需要额外线程池。
*/
private static final class BoundedBodySubscriber implements HttpResponse.BodySubscriber<Downloaded> {
private final int maxBytes;
private final CompletableFuture<Downloaded> body = new CompletableFuture<>();
private final List<ByteBuffer> chunks = new ArrayList<>();
private Flow.Subscription subscription;
/** long:配置若被填成接近 Integer.MAX_VALUE,int 累加会绕回负数而永远测不出超限。 */
private long received;
private BoundedBodySubscriber(int maxBytes) {
this.maxBytes = maxBytes;
}
@Override
public CompletionStage<Downloaded> getBody() {
return body;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(List<ByteBuffer> buffers) {
if (body.isDone()) {
return; // cancel 之后仍可能收到在途分片
}
for (var buffer : buffers) {
received += buffer.remaining();
if (received > maxBytes) {
chunks.clear();
subscription.cancel();
body.complete(Downloaded.OVERFLOW);
return;
}
chunks.add(buffer);
}
}
@Override
public void onError(Throwable throwable) {
chunks.clear();
body.completeExceptionally(throwable); // 已 complete 时是空操作,保持首个结果
}
@Override
public void onComplete() {
if (body.isDone()) {
return;
}
var bytes = new byte[(int) received]; // 走到这里必有 received <= maxBytes
int offset = 0;
for (var chunk : chunks) {
int length = chunk.remaining();
chunk.get(bytes, offset, length);
offset += length;
}
chunks.clear();
body.complete(new Downloaded(bytes, false));
}
}
private record RenderOutcome(@Nullable Rendered rendered, @Nullable RenderFailure failure,
@Nullable String detail) {
private static RenderOutcome ok(Rendered rendered) {
@@ -744,7 +866,8 @@ public class MapArtModule extends ServerModule {
}
private enum RenderFailure {
INVALID_URL, USERINFO, UNSUPPORTED_EXTENSION, COOLDOWN, DOWNLOAD_FAILED, NOT_IMAGE, TOO_LARGE, BUSY
INVALID_URL, USERINFO, UNSUPPORTED_EXTENSION, COOLDOWN, DOWNLOAD_FAILED, NOT_IMAGE,
TOO_LARGE, BODY_TOO_LARGE, BUSY
}
private static final class AnvilSession {
@@ -3,21 +3,35 @@ package io.ib67.sfcraft.module.game.portal;
import com.google.inject.Inject;
import io.ib67.sfcraft.ServerModule;
import io.ib67.sfcraft.config.GameConfigService;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
/**
* 鞘翅冲门:玩家使用鞘翅滑翔、以不低于配置阈值(米/秒)的速度冲进下界传送门时,
* 跳过原本的传送门等待(生存默认 80 tick),下一 tick 直接跨维度。
* 跳过原本的传送门等待(生存默认 80 tick),当场跨维度。
* <p>
* 本模块不注册任何注册表内容;行为由 {@code NetherPortalBlockMixin} 在
* {@code NetherPortalBlock#getPortalTransitionTime} 头部委托 {@link #shouldTeleportNow}
* 实现:命中条件时把等待时间覆写为 1 tick,否则放行原版(游戏规则)逻辑。
* 本模块不注册任何注册表内容;行为由两个 mixin 委托驱动:
* <ul>
* <li>{@code NetherPortalBlockMixin} 在 {@code NetherPortalBlock#getPortalTransitionTime} 头部委托
* {@link #shouldTeleportNow},命中条件时把等待时间覆写掉,否则放行原版(游戏规则)逻辑;</li>
* <li>{@code ElytraRushMixin} 负责记录撞墙前的速度({@link #mixin$recordRushSpeed})、免掉门后撞墙的
* 动能伤害({@link #shouldIgnoreWallImpact})。</li>
* </ul>
* 配置经 {@link GameConfigService} 每次现读,/sfcraft reload 即时生效。
*/
public class ElytraPortalModule extends ServerModule {
/** 冲门判定时覆写的等待 tick 数:与原版的创造模式进一致。 */
private static final int INSTANT_TRANSITION_TICKS = 1;
/** 冲门判定时覆写的等待 tick 数:0 = 本次 handlePortal 就传送,不再多贴一 tick 在墙上。 */
private static final int INSTANT_TRANSITION_TICKS = 0;
/**
* 撞墙前速度快照的有效期(tick)。进门发生在第 N tick 的 aiStep 末尾(applyEffectsFromBlocks),
* 传送门判定在第 N+1 tick 开头的 handlePortal,理论上只差 1 tick,留点余量应付被墙磨蹭几 tick 的情况。
*/
private static final int RUSH_MEMORY_TICKS = 3;
@Inject
private GameConfigService config;
@@ -27,21 +41,109 @@ public class ElytraPortalModule extends ServerModule {
// 无注册表内容、无事件订阅:特性完全由 mixin 委托驱动
}
/**
* {@code LivingEntity#travelFallFlying} 头部调用:此刻还在 {@code move()} 之前、deltaMovement 尚未被
* 墙裁剪,速度够快就记下来。只记够快的——这样玩家撞墙后那些近零速的 tick 不会把快照冲掉。
*/
public void mixin$recordRushSpeed(ServerPlayer player) {
double threshold = threshold();
if (threshold <= 0) {
return;
}
Vec3 delta = player.getDeltaMovement();
if (metersPerSecond(delta) < threshold) {
return;
}
((ElytraRushHolder) player).sfcraft$recordElytraRush(delta, player.tickCount);
}
/** mixin 在下界传送门询问等待时间时调用;命中即把本次等待覆写为 {@link #INSTANT_TRANSITION_TICKS}。 */
public boolean shouldTeleportNow(Entity entity) {
if (!(entity instanceof ServerPlayer player) || !player.isFallFlying()) {
if (!(entity instanceof ServerPlayer player)) {
return false;
}
double threshold = config.get().elytraPortal.minSpeedBlocksPerSecond;
double threshold = threshold();
// 速度阈值 <= 0 视为关闭本特性
if (threshold <= 0) {
return false;
}
// deltaMovement 单位为 方块/tick,乘 20 换算成米/秒与配置同单位
return player.getDeltaMovement().length() * 20.0 >= threshold;
// 门后没遮挡的常规情况:当前速度就够快
if (player.isFallFlying() && metersPerSecond(player.getDeltaMovement()) >= threshold) {
return true;
}
// 门后有遮挡:进门那一 tick 已经被墙撞停(连 isFallFlying 都可能因随后落地而取消),只能看快照
return recentRushDelta(player) != null;
}
/**
* {@code LivingEntity#handleFallFlyingCollisions} 头部调用:玩家高速冲进传送门方块、又撞上门后墙体时,
* 这一下动能伤害不该算在他头上——他本来就是要穿过去的。
*
* @param preCollisionSpeedPerTick 原版传进来的碰撞前水平速度,单位 方块/tick
*/
public boolean shouldIgnoreWallImpact(ServerPlayer player, double preCollisionSpeedPerTick) {
double threshold = threshold();
return threshold > 0
&& metersPerSecond(preCollisionSpeedPerTick) >= threshold
&& isTouchingNetherPortal(player);
}
/**
* 传送前把被墙撞掉的速度补回来。原版的 {@code TeleportTransition} 带 {@code Relative.DELTA}
* (含 {@code ROTATE_DELTA}),会保留并按出口朝向旋转玩家速度,所以这里只要把快照写回去,
* 出门那一侧就能继续原速滑翔,而不是零速掉下去。
*/
public void restoreRushVelocity(Entity entity) {
if (!(entity instanceof ServerPlayer player)) {
return;
}
double threshold = threshold();
// 只在速度确实掉到阈值以下(被墙吃了)时才补;门后没遮挡的正常冲门不去动它
if (threshold <= 0 || metersPerSecond(player.getDeltaMovement()) >= threshold) {
return;
}
Vec3 recorded = recentRushDelta(player);
if (recorded == null) {
return;
}
player.setDeltaMovement(recorded);
// 玩家移动是客户端权威的,而客户端同样预测到了这次撞墙。不补一个速度包的话,
// 服务端这边写回去的速度会立刻被客户端的位置汇报盖掉。
player.syncVelocity = true;
}
public int getInstantTransitionTicks() {
return INSTANT_TRANSITION_TICKS;
}
/** 仍在有效期内的撞墙前速度快照;过期或从未记录过返回 null。 */
private @Nullable Vec3 recentRushDelta(ServerPlayer player) {
var holder = (ElytraRushHolder) player;
Vec3 delta = holder.sfcraft$elytraRushDelta();
if (delta == null || player.tickCount - holder.sfcraft$elytraRushTick() > RUSH_MEMORY_TICKS) {
return null;
}
return delta;
}
/** 玩家碰撞箱是否正压在传送门方块上——撞墙那一 tick 传送门还没被登记,只能直接看方块。 */
private boolean isTouchingNetherPortal(ServerPlayer player) {
var level = player.level();
// 稍微收一点碰撞箱,避免只是紧贴着门的隔壁方块也被算进来
return BlockPos.betweenClosedStream(player.getBoundingBox().deflate(1.0E-6))
.anyMatch(pos -> level.getBlockState(pos).is(Blocks.NETHER_PORTAL));
}
private double threshold() {
return config.get().elytraPortal.minSpeedBlocksPerSecond;
}
/** deltaMovement 单位为 方块/tick,乘 20 换算成米/秒与配置同单位。 */
private static double metersPerSecond(double blocksPerTick) {
return blocksPerTick * 20.0;
}
private static double metersPerSecond(Vec3 delta) {
return metersPerSecond(delta.length());
}
}
@@ -0,0 +1,23 @@
package io.ib67.sfcraft.module.game.portal;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
/**
* 由 {@code ElytraRushMixin} 混入 LivingEntity:记住玩家本 tick 移动「被碰撞裁剪之前」的滑翔速度。
* <p>
* 传送门后面有遮挡时,玩家进门那一 tick 就被墙撞停(deltaMovement 归零),而传送门是在下一 tick
* 开头的 {@code Entity#handlePortal} 里才问「还要等多久」,那时候速度早就没了、判不出冲门。
* 这份快照就是给那一刻补判用的,见 {@link ElytraPortalModule#shouldTeleportNow}。
*/
public interface ElytraRushHolder {
/** 记下够快的滑翔速度及其发生的 tickCount;低速 tick 不会来覆盖,快照只靠 tick 窗口过期。 */
void sfcraft$recordElytraRush(Vec3 delta, int tick);
/** 最近一次记录的速度;从未记录过则为 null。 */
@Nullable
Vec3 sfcraft$elytraRushDelta();
/** 上一次记录发生在哪个 {@code Entity#tickCount}。 */
int sfcraft$elytraRushTick();
}
@@ -22,6 +22,7 @@ import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import net.minecraft.util.Prediction;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -93,7 +94,7 @@ public class PearlTokenItem extends Item implements PolymerItem {
} else {
stack.shrink(1);
if (!player.getInventory().add(bound)) {
player.drop(bound, false);
player.drop(bound, false, Prediction.SERVER_ONLY);
}
}
player.sendOverlayMessage(Component.translatable("message.sfcraft.token.bound"));
@@ -211,7 +211,7 @@ public class CreativeRoomModule extends ServerModule {
private int onQueryTime(CommandContext<CommandSourceStack> ctx) {
var player = requirePlaygroundPlayer(ctx);
if (player == null) return Command.SINGLE_SUCCESS;
long ticks = ctx.getSource().getServer().clockManager().getTotalTicks(playgroundClock(ctx.getSource()));
long ticks = ctx.getSource().getServer().clockManager().getInstance(playgroundClock(ctx.getSource())).totalTicks();
player.sendSystemMessage(Component.translatable("message.sfcraft.playground.time_query", wrapTime(ticks)));
return Command.SINGLE_SUCCESS;
}
@@ -0,0 +1,61 @@
package io.ib67.sfcraft.network;
import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.mixin.common.bridge.ServerCommonNetworkHandlerBridge;
import net.minecraft.network.protocol.common.ClientboundTransferPacket;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.Nullable;
/**
* 玩家在握手包里自报的服务器地址,也就是他实际用来进服的那个地址。
* 由 {@code ServerHandshakeNetworkHandlerMixin} 记到 {@link net.minecraft.network.Connection} 上,
* transfer 时原样回显,让玩家重连回原本的入口而不是配置里写死的那个。
*/
public record HandshakeAddress(String host, int port) {
/**
* 代理(BungeeCord/Velocity legacy forwarding、FML)会把转发数据拼在 hostName 后面,以 '\0' 分隔。
*/
private static final char FORWARDING_SEPARATOR = '\0';
/**
* 挂在 {@link net.minecraft.network.Connection} 上的握手地址,由 ConnectionMixin 实现。
*/
public interface Holder {
@Nullable
HandshakeAddress sfcraft$handshakeAddress();
void sfcraft$setHandshakeAddress(@Nullable HandshakeAddress address);
}
/**
* 清洗握手包里的 hostName / port,不可用时返回 null。
*/
public static @Nullable HandshakeAddress sanitize(String hostName, int port) {
if (hostName == null || port <= 0 || port > 65535) return null;
var host = hostName;
var separator = host.indexOf(FORWARDING_SEPARATOR);
if (separator >= 0) host = host.substring(0, separator);
// SRV 解析出来的 FQDN 可能带尾点
while (host.endsWith(".")) host = host.substring(0, host.length() - 1);
host = host.trim();
return host.isEmpty() ? null : new HandshakeAddress(host, port);
}
/**
* 读玩家当前连接上记的握手地址,拿不到返回 null。
*/
public static @Nullable HandshakeAddress of(ServerPlayer player) {
var connection = ((ServerCommonNetworkHandlerBridge) player.connection).sfcraft$getConnection();
return connection instanceof Holder holder ? holder.sfcraft$handshakeAddress() : null;
}
/**
* 优先把玩家送回他自己用的地址,拿不到才回退配置里的 domain / port。
*/
public static ClientboundTransferPacket transferPacketFor(ServerPlayer player, SFConfig fallback) {
var address = of(player);
return address == null
? new ClientboundTransferPacket(fallback.domain, fallback.port)
: new ClientboundTransferPacket(address.host(), address.port());
}
}
@@ -6,13 +6,13 @@ import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.module.SignatureService;
import io.ib67.sfcraft.network.HandshakeAddress;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.subserver.Room;
import io.netty.buffer.Unpooled;
import net.minecraft.network.PacketSendListener;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.common.ClientboundStoreCookiePacket;
import net.minecraft.network.protocol.common.ClientboundTransferPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.CommonColors;
@@ -44,6 +44,8 @@ public class RoomTeleporter {
throw new IllegalStateException("world isn't exist");
}
var networkHandler = player.connection;
// 趁连接还在 play 阶段先取好目标地址
var transfer = HandshakeAddress.transferPacketFor(player, config);
networkHandler.switchToConfig();
var request = new RequestedRoom(room.getServerIdentifier(), name, finalUuid);
var requestBuf = Unpooled.buffer();
@@ -58,7 +60,7 @@ public class RoomTeleporter {
)
);
networkHandler.send(new ClientboundStoreCookiePacket(ROOM_COOKIE, cookie), PacketSendListener.thenRun(() -> {
networkHandler.send(new ClientboundTransferPacket(config.domain, config.port));
networkHandler.send(transfer);
}));
}
}
@@ -99,6 +99,7 @@
"message.sfcraft.mapart.error.unsupported_extension": "The URL must end with a supported image extension (png/jpg/jpeg/gif/bmp)",
"message.sfcraft.mapart.error.cooldown": "This URL failed recently; retry in %s seconds",
"message.sfcraft.mapart.error.too_large": "Image too large (%s; max %s pixels per side)",
"message.sfcraft.mapart.error.body_too_large": "Image file too large; the limit is %s",
"message.sfcraft.mapart.error.not_image": "The downloaded content is not a recognizable image",
"message.sfcraft.mapart.error.download_failed": "Download failed: %s",
"message.sfcraft.mapart.error.busy": "The server is already rendering %s map art(s) concurrently; please try again later",
@@ -99,6 +99,7 @@
"message.sfcraft.mapart.error.unsupported_extension": "URL 需以受支持的图片扩展名结尾(png/jpg/jpeg/gif/bmp)",
"message.sfcraft.mapart.error.cooldown": "该地址刚请求失败过,%s 秒后可重试",
"message.sfcraft.mapart.error.too_large": "图片尺寸过大(%s,单边上限 %s 像素)",
"message.sfcraft.mapart.error.body_too_large": "图片体积过大,上限 %s",
"message.sfcraft.mapart.error.not_image": "下载的内容不是可识别的图片",
"message.sfcraft.mapart.error.download_failed": "下载失败: %s",
"message.sfcraft.mapart.error.busy": "当前全服并发渲染已达上限(%s),请稍后再试",
@@ -51,5 +51,5 @@
"max_inclusive": 7,
"min_inclusive": 0
},
"timelines": "#minecraft:in_overworld"
"timelines": "#sfcraft:in_playground"
}
@@ -0,0 +1,5 @@
{
"values": [
"sfcraft:playground_day"
]
}
@@ -0,0 +1,489 @@
{
"clock": "sfcraft:playground",
"period_ticks": 24000,
"time_markers": {
"minecraft:day": {
"show_in_commands": true,
"ticks": 1000
},
"minecraft:midnight": {
"show_in_commands": true,
"ticks": 18000
},
"minecraft:night": {
"show_in_commands": true,
"ticks": 13000
},
"minecraft:noon": {
"show_in_commands": true,
"ticks": 6000
},
"minecraft:roll_village_siege": 18000,
"minecraft:wake_up_from_sleep": 0
},
"tracks": {
"minecraft:audio/firefly_bush_sounds": {
"keyframes": [
{
"ticks": 12600,
"value": true
},
{
"ticks": 23401,
"value": false
}
],
"modifier": "or"
},
"minecraft:gameplay/bees_stay_in_hive": {
"keyframes": [
{
"ticks": 12542,
"value": true
},
{
"ticks": 23460,
"value": false
}
],
"modifier": "or"
},
"minecraft:gameplay/cat_waking_up_gift_chance": {
"ease": "constant",
"keyframes": [
{
"ticks": 362,
"value": 0.0
},
{
"ticks": 23667,
"value": 0.7
}
],
"modifier": "maximum"
},
"minecraft:gameplay/creaking_active": {
"keyframes": [
{
"ticks": 12600,
"value": true
},
{
"ticks": 23401,
"value": false
}
],
"modifier": "or"
},
"minecraft:gameplay/eyeblossom_open": {
"keyframes": [
{
"ticks": 12600,
"value": true
},
{
"ticks": 23401,
"value": false
}
]
},
"minecraft:gameplay/monsters_burn": {
"keyframes": [
{
"ticks": 12542,
"value": false
},
{
"ticks": 23460,
"value": true
}
],
"modifier": "or"
},
"minecraft:gameplay/sky_light_level": {
"keyframes": [
{
"ticks": 133,
"value": 1.0
},
{
"ticks": 11867,
"value": 1.0
},
{
"ticks": 13670,
"value": 0.26666668
},
{
"ticks": 22330,
"value": 0.26666668
}
],
"modifier": "multiply"
},
"minecraft:gameplay/turtle_egg_hatch_chance": {
"ease": "constant",
"keyframes": [
{
"ticks": 21062,
"value": 1.0
},
{
"ticks": 21905,
"value": 0.002
}
],
"modifier": "maximum"
},
"minecraft:visual/cloud_color": {
"keyframes": [
{
"ticks": 133,
"value": -1
},
{
"ticks": 11867,
"value": -1
},
{
"ticks": 13670,
"value": -15132378
},
{
"ticks": 22330,
"value": -15132378
}
],
"modifier": "multiply"
},
"minecraft:visual/fog_color": {
"keyframes": [
{
"ticks": 133,
"value": "#ffffff"
},
{
"ticks": 11867,
"value": "#ffffff"
},
{
"ticks": 13670,
"value": "#0c0c16"
},
{
"ticks": 22330,
"value": "#161616"
}
],
"modifier": "multiply"
},
"minecraft:visual/moon_angle": {
"ease": {
"cubic_bezier": [
0.362,
0.241,
0.638,
0.759
]
},
"keyframes": [
{
"ticks": 6000,
"value": 540.0
},
{
"ticks": 6000,
"value": 180.0
}
]
},
"minecraft:visual/sky_color": {
"keyframes": [
{
"ticks": 133,
"value": "#ffffff"
},
{
"ticks": 11867,
"value": "#ffffff"
},
{
"ticks": 13670,
"value": "#000000"
},
{
"ticks": 22330,
"value": "#000000"
}
],
"modifier": "multiply"
},
"minecraft:visual/sky_light_color": {
"keyframes": [
{
"ticks": 730,
"value": "#ffffff"
},
{
"ticks": 11270,
"value": "#ffffff"
},
{
"ticks": 13140,
"value": "#7a7aff"
},
{
"ticks": 22860,
"value": "#7a7aff"
}
],
"modifier": "multiply"
},
"minecraft:visual/sky_light_factor": {
"keyframes": [
{
"ticks": 730,
"value": 1.0
},
{
"ticks": 11270,
"value": 1.0
},
{
"ticks": 13140,
"value": 0.24
},
{
"ticks": 22860,
"value": 0.24
}
],
"modifier": "multiply"
},
"minecraft:visual/star_angle": {
"ease": {
"cubic_bezier": [
0.362,
0.241,
0.638,
0.759
]
},
"keyframes": [
{
"ticks": 6000,
"value": 360.0
},
{
"ticks": 6000,
"value": 0.0
}
]
},
"minecraft:visual/star_brightness": {
"keyframes": [
{
"ticks": 92,
"value": 0.037
},
{
"ticks": 627,
"value": 0.0
},
{
"ticks": 11373,
"value": 0.0
},
{
"ticks": 11732,
"value": 0.016
},
{
"ticks": 11959,
"value": 0.044
},
{
"ticks": 12399,
"value": 0.143
},
{
"ticks": 12729,
"value": 0.258
},
{
"ticks": 13228,
"value": 0.5
},
{
"ticks": 22772,
"value": 0.5
},
{
"ticks": 23032,
"value": 0.364
},
{
"ticks": 23356,
"value": 0.225
},
{
"ticks": 23758,
"value": 0.101
}
],
"modifier": "maximum"
},
"minecraft:visual/sun_angle": {
"ease": {
"cubic_bezier": [
0.362,
0.241,
0.638,
0.759
]
},
"keyframes": [
{
"ticks": 6000,
"value": 360.0
},
{
"ticks": 6000,
"value": 0.0
}
]
},
"minecraft:visual/sunrise_sunset_color": {
"keyframes": [
{
"ticks": 71,
"value": "#5fefa333"
},
{
"ticks": 310,
"value": "#29f5ba33"
},
{
"ticks": 565,
"value": "#06fbd433"
},
{
"ticks": 730,
"value": "#00ffe533"
},
{
"ticks": 11270,
"value": "#00ffe533"
},
{
"ticks": 11397,
"value": "#04fcd833"
},
{
"ticks": 11522,
"value": "#0ff9cb33"
},
{
"ticks": 11690,
"value": "#29f5ba33"
},
{
"ticks": 11929,
"value": "#5fefa333"
},
{
"ticks": 12243,
"value": "#b1e78733"
},
{
"ticks": 12358,
"value": "#cce47e33"
},
{
"ticks": 12512,
"value": "#e9e07233"
},
{
"ticks": 12613,
"value": "#f6dd6b33"
},
{
"ticks": 12732,
"value": "#feda6333"
},
{
"ticks": 12841,
"value": "#fed75c33"
},
{
"ticks": 13035,
"value": "#ecd25133"
},
{
"ticks": 13252,
"value": "#c1cc4733"
},
{
"ticks": 13775,
"value": "#36be3733"
},
{
"ticks": 13888,
"value": "#1fbb3533"
},
{
"ticks": 14039,
"value": "#09b73333"
},
{
"ticks": 14192,
"value": "#00b33333"
},
{
"ticks": 21807,
"value": "#00b23333"
},
{
"ticks": 21961,
"value": "#09b73333"
},
{
"ticks": 22112,
"value": "#1fbb3533"
},
{
"ticks": 22225,
"value": "#36be3733"
},
{
"ticks": 22748,
"value": "#c1cc4733"
},
{
"ticks": 22965,
"value": "#ecd25133"
},
{
"ticks": 23159,
"value": "#fed75c33"
},
{
"ticks": 23272,
"value": "#feda6333"
},
{
"ticks": 23488,
"value": "#e9e07233"
},
{
"ticks": 23642,
"value": "#cce47e33"
},
{
"ticks": 23757,
"value": "#b1e78733"
}
]
}
}
}
@@ -0,0 +1,15 @@
{
"has_precipitation": true,
"temperature": 0.8,
"downfall": 0.4,
"attributes": {
"minecraft:visual/sky_color": "#78a7ff"
},
"effects": {
"water_color": "#3f76e4"
},
"carvers": [],
"features": [],
"spawners": {},
"spawn_costs": {}
}
+3
View File
@@ -9,16 +9,19 @@
"client": [
],
"server": [
"common.ConnectionMixin",
"common.EntityMixin",
"common.MobEntityMixin",
"common.PlayerEntityMixin",
"common.bridge.EntityBridge",
"common.bridge.ServerChunkManagerBridge",
"common.bridge.ServerCommonNetworkHandlerBridge",
"common.bridge.ServerPlayerBridge",
"common.bridge.ServerWorldBridge",
"common.fixes.EndermanEntityMixin",
"common.fixes.LivingEntityMixin",
"gameplay.AnvilMenuMixin",
"gameplay.ElytraRushMixin",
"gameplay.ItemFrameMixin",
"gameplay.MobCommanderMixin",
"gameplay.NetherPortalBlockMixin",