Compare commits
10
Commits
21db083fed
...
b8d4725e7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d4725e7b | ||
|
|
2b70971b80 | ||
|
|
08be6bc3ae | ||
|
|
bf38f8b2b2 | ||
|
|
63250d48ae | ||
|
|
f026c65915 | ||
|
|
b1c86c63aa | ||
|
|
f7f417174d | ||
|
|
ffa6d9826c | ||
|
|
f7980342c3 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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) 伪装成原版内容;玩家使用原版客户端 + 服务器发布的材质包即可游玩。
|
||||
|
||||
## 分层
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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,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. 服务器启用白名单.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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": {}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user