Compare commits

...

37 Commits

Author SHA1 Message Date
Helvetica Volubi 1c4dea41c8 fix: fix servux's MobCapData build & send
rebuild in folia environment
2025-08-30 03:50:31 +08:00
Helvetica Volubi 4740744adb Update upstream(Luminol) 2025-08-30 01:23:09 +08:00
Bacteriawa 434694f6a1 [ci skip]update readme 2025-08-29 21:13:17 +08:00
Bacteriawa e584866cf5 [ci skip]update readme 2025-08-29 21:11:05 +08:00
Helvetica Volubi 1cff183f26 [ci skip]fix up 2025-08-27 23:45:26 +08:00
Helvetica Volubi a3af47db18 Update upstream to fix a config bug 2025-08-27 22:36:54 +08:00
Helvetica Volubi b830c66be7 Update upstream(Luminol) & rename config names 2025-08-27 00:22:52 +08:00
Helvetica Volubi fc50c2ac44 Update gradle.properties 2025-08-26 14:08:25 +08:00
Helvetica Volubi f81cee2454 feat: add a config to revert MC-159283 2025-08-26 03:04:39 +08:00
Helvetica Volubi 049716a64c Update upstream(Luminol) 2025-08-25 22:31:49 +08:00
dependabot[bot] c7c37c5a95 build(deps): bump actions/setup-java from 4 to 5 (#52)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 21:32:02 +08:00
Helvetica Volubi 4dea005c0d Update gradle.properties 2025-08-24 22:07:20 +08:00
Helvetica Volubi 074c5c9553 Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-24 03:55:44 +08:00
Helvetica Volubi f79cc86332 Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-22 20:55:59 +08:00
Helvetica Volubi e880fdecfa Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-22 02:18:26 +08:00
xiaoxijun dda45b510a update maven repo 2025-08-20 03:51:09 +08:00
Bacteriawa 6fec3a73ac update maven repo 2025-08-20 02:59:17 +08:00
Helvetica Volubi d22d5fdf5a Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-19 07:11:56 +08:00
Helvetica Volubi fcb90d468e Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-18 23:18:53 +08:00
dependabot[bot] 1094716d9d [ci skip]build(deps): bump actions/checkout from 4 to 5 (#49)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 19:22:24 +08:00
Helvetica Volubi 3d57a67913 Update upstream 2025-08-17 22:25:14 +08:00
Helvetica Volubi ecc514b340 Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-16 02:51:48 +08:00
Helvetica Volubi 75baf3dfaf Update some patches from Leaves 2025-08-15 03:21:36 +08:00
Helvetica Volubi 4ed8600511 Update upstream(Luminol-lithium-sleeping-block-entity) 2025-08-13 22:27:33 +08:00
Helvetica Volubi 0160aac4c6 Update upstream (Luminol) 2025-08-11 13:35:13 +08:00
Helvetica Volubi cbed24020a Update upstream (Luminol) 2025-08-09 02:37:40 +08:00
Helvetica Volubi 682029a451 fix: fix some bugs
fix crash in contain view (#44)

fix tps and mspt send in servux
2025-08-08 00:59:22 +08:00
Helvetica Volubi f3334ae6ec Update Upstream(Luminol) 2025-08-04 20:00:05 +08:00
Helvetica Volubi df0e0eb82b [ci skip]add missing license 2025-08-04 18:01:47 +08:00
Helvetica Volubi d09469e84b servux protocol & unapply old replaceable by mushrooms [(#38)(#40)](#43)
* feat: add a config to allow to disable some check for operator

* feat: leaves servux protocol

* fix: upload missing files

* Update Upstream(Luminol)

* fix: upload missing files - 2

* fix: reuse tickCount

* fix: fix

* fix: adjust folia

* add leaves license

* add infinite support
2025-08-04 15:19:09 +08:00
Helvetica Volubi a5e56ff2ca feat: add a config to allow to disable some check for operator 2025-08-03 16:55:38 +08:00
Helvetica Volubi e38108c99b fix: temporarily fix endermite spawn position 2025-08-02 12:44:30 +08:00
Helvetica Volubi ab31ebc674 Update Upstream(Luminol) 2025-08-01 13:40:00 +08:00
Helvetica Volubi 87ba8b9746 Update Upstream(Luminol) 2025-07-30 21:55:01 +08:00
Helvetica Volubi e71b36e331 [ci skip]docs: refactor format 2025-07-30 16:18:31 +08:00
Helvetica Volubi cfb1ad88f4 Update Upstream(Luminol) 2025-07-28 23:03:43 +08:00
Helvetica Volubi b40276f037 feat: use new limit of 524288 instead of 16384 2025-07-28 17:25:16 +08:00
122 changed files with 11659 additions and 11275 deletions
+2
View File
@@ -9,6 +9,8 @@ body:
- type: markdown
attributes:
value: |
在提问之前,请您先阅读 [提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) ,我们不会为任何无意义的问题作回复。
Please read [smart questions](http://www.catb.org/~esr/faqs/smart-questions.html) before asking questions, we won't respond to any meaningless questions.
请详细描述您所遇到的Bug。确保您**真的**遇到了**服务端**Bug,而非原版特性或客户端Bug。
Please describe the bug you encountered in detail. Make sure you're **actually** hitting a **server-side** bug, not a vanilla feature or client-side bug.<br>
如果您确定的话,就按照下面的模板填充信息,并提交反馈。
@@ -11,6 +11,8 @@ body:
value: |
感谢您为 Lophine 提交新的需求!请尽可能详细地描述,以便我们可以更轻松地考虑和审查请求。
Thank you for filling out a feature request for Lophine! Please be as detailed as possible so that we may consider and review the request easier.<br>
在提问之前,请您先阅读 [提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) ,我们不会为任何无意义的问题作回复。
Please read [smart questions](http://www.catb.org/~esr/faqs/smart-questions.html) before asking questions, we won't respond to any meaningless questions.
请搜索所有问题以避免重复的功能请求。如果您期望的功能已存在,请回复如果您有什么要补充的。
We ask that you search all the issues to avoid a duplicate feature request. If one exists, please reply if you have anything to add.<br>
在请求新功能之前,请确保您使用的是最新版本并且您请求的功能不在 Lophine 中。
+11 -11
View File
@@ -16,10 +16,10 @@ jobs:
environment: default
steps:
- name: Checkout Git Repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 21
@@ -39,14 +39,6 @@ jobs:
- name: CreateMojmapPaperclipJar
run: ./gradlew --refresh-dependencies createMojmapPaperclipJar
- name: Publish to repo
if: github.event_name != 'pull_request' && false # do not push to any repo
continue-on-error: true
run: ./gradlew --refresh-dependencies generateDevelopmentBundle publish -PpublishDevBundle=true
env:
PRIVATE_MAVEN_REPO_PASSWORD: ${{ secrets.PRIVATE_MAVEN_REPO_PASSWORD }}
PRIVATE_MAVEN_REPO_USERNAME: ${{ secrets.PRIVATE_MAVEN_REPO_USERNAME }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
@@ -57,8 +49,16 @@ jobs:
if: github.event_name != 'pull_request'
run: sh scripts/SetENV.sh
- name: Publish to repo
if: github.event_name != 'pull_request' && env.flag_release == 'true' && false # do not push to any repo
continue-on-error: true
run: ./gradlew --refresh-dependencies generateDevelopmentBundle publish -PpublishDevBundle=true
env:
PRIVATE_MAVEN_REPO_PASSWORD: ${{ secrets.PRIVATE_MAVEN_REPO_PASSWORD }}
PRIVATE_MAVEN_REPO_USERNAME: ${{ secrets.PRIVATE_MAVEN_REPO_USERNAME }}
- name: Create Release
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && env.flag_release == 'true'
uses: ncipollo/release-action@v1
with:
tag: ${{ env.tag }}
+1 -1
View File
@@ -2,7 +2,7 @@ Lophine inherits its licensing from the included upstream projects.
Lophine从上游继承了许可证。
As such, Lophine is licensed under the
As such, Lophine is licensed under the
[GNU General Public License version 3](licenses/GPL.md); as it inherits it from Luminol,
who in turn inherits it from the original Folia, Paper, Spigot, Bukkit and CraftBukkit projects.
+101 -37
View File
@@ -1,54 +1,78 @@
<img src="public/image/lophine/lophine3.png" alt="Logo" align="right" width="250">
<div align="center">
<img src="./public/image/lophine/lophine3.png" alt="Lophine Logo" width="300">
# Lophine
*Lophine 是一个基于Luminol的分支,具有许多有用的优化和可配置的原版特性,目标是在Folia上实现更多生电的内容(请注意,完整生电请使用Fabric)*
![Created At](https://img.shields.io/github/created-at/LuminolMC/Lophine?style=for-the-badge&color=blue)
[![License](https://img.shields.io/github/license/LuminolMC/Lophine?style=for-the-badge&color=green)](LICENSE.md)
[![Issues](https://img.shields.io/github/issues/LuminolMC/Lophine?style=for-the-badge&color=orange)](https://github.com/LuminolMC/Lophine/issues)
![Commit Activity](https://img.shields.io/github/commit-activity/w/LuminolMC/Lophine?style=for-the-badge&color=purple)
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/LuminolMC/Lophine?style=for-the-badge&color=yellow)
![GitHub all releases](https://img.shields.io/github/downloads/LuminolMC/Lophine/total?style=for-the-badge&color=red)
![Repo contributors](https://img.shields.io/github/contributors/LuminolMC/Lophine?style=for-the-badge&color=brightgreen)
[English](./README_EN.md) | **中文**
</div>
# Lophine
---
<h4>Lophine 是一个基于Luminol的分支,具有许多有用的优化和可配置的原版特性,目标是在Folia上实现更多生电的实现</h4>
## ✨ 核心特性
[![License](https://img.shields.io/github/license/LuminolMC/Lophine?style=flat-square)](LICENSE.md)
[![Issues](https://img.shields.io/github/issues/LuminolMC/Lophine?style=flat-square)](https://github.com/LuminolMC/Lophine/issues)
![Commit Activity](https://img.shields.io/github/commit-activity/w/LuminolMC/Lophine?style=flat-square)
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/LuminolMC/Lophine?style=flat-square)
![GitHub all releases](https://img.shields.io/github/downloads/LuminolMC/Lophine/total?style=flat-square)
- 🔧 **可配置的原版特性** - 灵活调整游戏机制以适应不同服务器需求
- 📊 **Tpsbar 支持** - 实时显示服务器 TPS 状态
- 🐛 **Folia Bug 修复** - 针对 Folia 已知问题的专项修复
- 💾 **多存档格式支持** - 支持 linear 和 b_linearlinear 重新实现)存档格式
- 🔬 **生电功能增强** - 在 Folia 上实现更多生电内容(完整生电请使用 Fabric)
- 🛠️ **更多实用功能** - 持续添加有用的服务器功能
[English](./README_EN.md) | **中文**
## 📥 下载
## 特性
- 支持部分Bukkit插件
- 可配置的原版特性
- Tpsbar 支持
- 对单线程区域性能的优化
- 更多有用的功能
### 稳定版本
所有发布版本都可以在 [Releases](https://github.com/LuminolMC/Lophine/releases) 页面找到。
## 下载
任何版本都可以在 [Release](https://github.com/LuminolMC/Lophine/releases), 中找到,你也可以通过[以下步骤](./README.md#构建)自己构建。
### 开发版本
如果您想体验最新功能,可以通过以下步骤自行构建。
## 构建
要构建一个paperclip jar,你需要运行以下命令。你可以在lophine-server/build/libs中找到jar(注意:需要`JDK21`
```shell
### 构建步骤
```bash
# 克隆项目
git clone https://github.com/LuminolMC/Lophine.git
cd Lophine
# 应用补丁并构建 Paperclip JAR
./gradlew applyAllPatches && ./gradlew createMojmapPaperclipJar
```
## 使用API
使用 Gradle:
构建完成后,您可以在 `lophine-server/build/libs` 目录中找到生成的 JAR 文件。
## 🔌 API 使用
### Gradle 配置
```kotlin
repositories {
maven {
url = "https://repo.menthamc.com/repository/maven-public/"
url = "https://repo.menthamc.org/repository/maven-public/"
}
}
dependencies {
compileOnly("me.earthme.luminol:luminol-api:$VERSION")
}
```
```
使用 Maven
### Maven 配置
```xml
<repositories>
<repository>
<id>menthamc</id>
<url>https://repo.menthamc.com/repository/maven-public/</url>
<url>https://repo.menthamc.org/repository/maven-public/</url>
</repository>
</repositories>
@@ -61,29 +85,65 @@ dependencies {
</dependencies>
```
## 联系方式
## 💬 社区与支持
> 如果您对这个项目感兴趣或有任何问题,请随时向我们提问。
**QQ群: [1015048616](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=QML5kIVsniPi1PlZvnjHQT_02EHsZ5Jc&authKey=%2FTCJsZC7JFQ9sxAroPCKuYnlV57Z5fyqp36ewXZk3Sn4iJ9p4MB1JKdc%2FFcX3HOM&noverify=0&group_code=1015048616)** | QQ频道: [点击加入](https://pd.qq.com/s/eq9krf9j) | Telegram: [点击加入](https://t.me/LuminolMinecraft) | Discord: [点击加入](https://discord.gg/Qd7m3V6eDx)
### 加入我们的社区
- **QQ群**: [1015048616](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=QML5kIVsniPi1PlZvnjHQT_02EHsZ5Jc&authKey=%2FTCJsZC7JFQ9sxAroPCKuYnlV57Z5fyqp36ewXZk3Sn4iJ9p4MB1JKdc%2FFcX3HOM&noverify=0&group_code=1015048616)
- **QQ频道**: [点击加入](https://pd.qq.com/s/eq9krf9j)
- **Telegram**: [点击加入](https://t.me/LuminolMinecraft)
- **Discord**: [点击加入](https://discord.gg/Qd7m3V6eDx)
## 关于 Issue
当您遇到任何问题时,请向我们提问,我们将尽力解决,但请记得清楚地描述您的问题并提供足够的日志等信息。
### 获取帮助
## 贡献代码
可查看 [Contributing](./docs/CONTRIBUTING.md)
- 📋 [提交 Issue](https://github.com/LuminolMC/Lophine/issues)
- 💬 [GitHub Discussions](https://github.com/LuminolMC/Lophine/discussions)
- 📖 [项目文档](./docs/)
## 🐛 问题反馈
当您遇到任何问题时,请向我们提问,我们将尽力解决。请记得:
- 📝 **清楚描述问题** - 详细说明问题的具体表现
- 📋 **提供完整日志** - 包含错误日志和相关配置信息
- 🔍 **环境信息** - 说明服务器版本、插件列表等环境详情
- 🔄 **复现步骤** - 如果可能,请提供问题复现的具体步骤
## 🤝 贡献代码
我们欢迎社区贡献!详细的贡献指南请查看:
- 📖 [贡献指南 (中文)](./docs/CONTRIBUTING.md)
- 📖 [Contributing Guide (English)](./docs/CONTRIBUTING_EN.md)
## 📊 项目统计
### BStats 数据
## BStats
![bStats](https://bstats.org/signatures/server-implementation/Lophine.svg "bStats")
## 特别感谢
<b>感谢[LegacyLands](https://github.com/LegacyLands)对本项目的赞助,如果你想开发一些跨folia/非folia平台的插件,[legacy-lands-library](https://github.com/LegacyLands/legacy-lands-library/)将会是个不错的lib</b>
## 🎉 特别感谢
![legacy-lands-logo](public/image/legacy-lands-logo.png)
### 项目赞助商
<div align="center">
<b>感谢 <a href="https://github.com/LegacyLands">LegacyLands</a> 对本项目的赞助</b>
<br>
<i>如果你想开发跨 Folia/非 Folia 平台的插件,<a href="https://github.com/LegacyLands/legacy-lands-library/">legacy-lands-library</a> 将会是个不错的选择</i>
<br><br>
<img src="public/image/legacy-lands-logo.png" alt="LegacyLands Logo" width="200">
</div>
---
## ⭐ 请给我们一个 Star
## 请给我们一个 ⭐Star
> 你的每一个免费的 ⭐Star 就是我们每一个前进的动力。
### Star 历史
<a href="https://star-history.com/#LuminolMC/Luminol&LuminolMC/LightingLuminol&LuminolMC/Lophine&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=LuminolMC/Luminol%2CLuminolMC/LightingLuminol%2CLuminolMC/Lophine&type=Date&theme=dark" />
@@ -91,3 +151,7 @@ dependencies {
<img alt="Star历史表" src="https://api.star-history.com/svg?repos=LuminolMC/Luminol%2CLuminolMC/LightingLuminol%2CLuminolMC/Lophine&type=Date" />
</picture>
</a>
<div align="center">
<b>如果这个项目对您有帮助,请不要忘记给我们一个 ⭐Star!</b>
</div>
+112 -44
View File
@@ -1,86 +1,150 @@
<img src="public/image/lophine/lophine3.png" alt="Logo" align="right" width="250">
<div align="center">
<img src="./public/image/lophine/lophine3.png" alt="Lophine Logo" width="300">
# Lophine
*Lophine is a Luminol fork with many useful optimizations and configurable vanilla features, aims to provide more function for survival-usable circuit on folia (Please note that Fabric should be used for complete survival-usable)*
![Created At](https://img.shields.io/github/created-at/LuminolMC/Lophine?style=for-the-badge&color=blue)
[![License](https://img.shields.io/github/license/LuminolMC/Lophine?style=for-the-badge&color=green)](LICENSE.md)
[![Issues](https://img.shields.io/github/issues/LuminolMC/Lophine?style=for-the-badge&color=orange)](https://github.com/LuminolMC/Lophine/issues)
![Commit Activity](https://img.shields.io/github/commit-activity/w/LuminolMC/Lophine?style=for-the-badge&color=purple)
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/LuminolMC/Lophine?style=for-the-badge&color=yellow)
![GitHub all releases](https://img.shields.io/github/downloads/LuminolMC/Lophine/total?style=for-the-badge&color=red)
![Repo contributors](https://img.shields.io/github/contributors/LuminolMC/Lophine?style=for-the-badge&color=brightgreen)
**English** | [中文](./README.md)
</div>
# Lophine
---
<h4>LightingLuminol is a Luminol fork with many useful optimizations and configurable vanilla features, aims to provide more function for survival-usable circuit on folia</h4>
## ✨ Core Features
[![License](https://img.shields.io/github/license/LuminolMC/Lophine?style=flat-square)](LICENSE.md)
[![Issues](https://img.shields.io/github/issues/LuminolMC/Lophine?style=flat-square)](https://github.com/LuminolMC/Lophine/issues)
![Commit Activity](https://img.shields.io/github/commit-activity/w/LuminolMC/Lophine?style=flat-square)
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/LuminolMC/Lophine?style=flat-square)
![GitHub all releases](https://img.shields.io/github/downloads/LuminolMC/Lophine/total?style=flat-square)
- 🔧 **Configurable Vanilla Features** - Flexibly adjust game mechanics to suit different server needs
- 📊 **Tpsbar Support** - Real-time TPS status display
- 🐛 **Folia Bug Fixes** - Targeted fixes for known Folia issues
- 💾 **Multiple World Format Support** - Support for linear and b_linear (linear reimplementation) world formats
- 🔬 **Redstone Enhancement** - More redstone functionality on Folia (use Fabric for complete redstone features)
- 🛠️ **More Useful Functions** - Continuously adding useful server features
**English** | [中文](./README.md)
## 📥 Download
## Features
- Supported some Bukkit plugin
- Configurable vanilla features
- Tpsbar support
- Useful optimizations to improve the performance of single threaded region
- More functions
### Stable Releases
All release versions can be found on the [Releases](https://github.com/LuminolMC/Lophine/releases) page.
## Download
Any versions are available in the [release](https://github.com/LuminolMC/Lophine/releases), also you can build it by yourself through [the following steps](./README_EN.md#Build).
### Development Builds
If you want to experience the latest features, you can build it yourself following the steps below.
## Build
To build a paperclip jar, you need to run the following command. You can find the jar in lophine-server/build/libs(Note: `JDK21` is needed)
```shell
./gradlew applyAllPatches && ./gradlew createMojmapPaperclipJar
### Build Steps
```bash
# Clone the project
git clone https://github.com/LuminolMC/Lophine.git
cd Lophine
# Apply patches and build Paperclip JAR
./gradlew applyAllPatches && ./gradlew createMojmapPaperclipJar
```
## Using API
For gradle:
After building, you can find the generated JAR file in the `lophine-server/build/libs` directory.
## 🔌 API Usage
### Gradle Configuration
```kotlin
repositories {
maven {
url = "https://repo.menthamc.com/repository/maven-public/"
url = "https://repo.menthamc.org/repository/maven-public/"
}
}
dependencies {
compileOnly("me.earthme.luminol:luminol-api:$VERSION")
}
```
```
For maven
### Maven Configuration
```xml
<repositories>
<repository>
<id>menthamc</id>
<url>https://repo.menthamc.com/repository/maven-public/</url>
<url>https://repo.menthamc.org/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>me.earthme.luminol</groupId>
<artifactId>luminol-api</artifactId>
<version>$VERSION</version>
</dependency>
<dependency>
<groupId>me.earthme.luminol</groupId>
<artifactId>luminol-api</artifactId>
<version>$VERSION</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
## Contact
> If you are interested in this project or have any issue, feel free to ask us.
## 💬 Community & Support
**QQ Group: [1015048616](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=hTPlI5j6XB8pgk4sdx6RkjhBPGG1r4IR&authKey=pnu6uCKQP7Sja2CJWC15Qi3BeI%2FAsh8tU4m5muufMBjbB3zz%2BwHBZCTRRdSNKhld&noverify=0&group_code=1015048616)** | QQ Channel: [Click To Join](https://pd.qq.com/s/eq9krf9j) | Telegram: [Click To Join](https://t.me/LuminolMinecraft) | Discord: [Click To Join](https://discord.gg/Qd7m3V6eDx)
> If you're interested in this project or have any questions, feel free to ask us.
## About Issue
When you meet any problems, just ask us, we will do our best to solve it, but remember to state your problem clear and provide enough logs etc.</br>
### Join Our Community
## Pull Requests
See [Contributing](./docs/CONTRIBUTING_EN.md)
- **QQ Group**: [1015048616](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=QML5kIVsniPi1PlZvnjHQT_02EHsZ5Jc&authKey=%2FTCJsZC7JFQ9sxAroPCKuYnlV57Z5fyqp36ewXZk3Sn4iJ9p4MB1JKdc%2FFcX3HOM&noverify=0&group_code=1015048616)
- **QQ Channel**: [Join Here](https://pd.qq.com/s/eq9krf9j)
- **Telegram**: [Join Here](https://t.me/LuminolMinecraft)
- **Discord**: [Join Here](https://discord.gg/Qd7m3V6eDx)
### Get Help
- 📋 [Submit Issues](https://github.com/LuminolMC/Lophine/issues)
- 💬 [GitHub Discussions](https://github.com/LuminolMC/Lophine/discussions)
- 📖 [Project Documentation](./docs/)
## 🐛 Bug Reports
When you encounter any issues, please ask us and we'll do our best to resolve them. Please remember to:
- 📝 **Describe the problem clearly** - Provide detailed information about the specific issue
- 📋 **Provide complete logs** - Include error logs and relevant configuration information
- 🔍 **Environment details** - Specify server version, plugin list, and other environment details
- 🔄 **Reproduction steps** - If possible, provide specific steps to reproduce the issue
## 🤝 Contributing
We welcome community contributions! For detailed contribution guidelines, please see:
- 📖 [Contributing Guide (English)](./docs/CONTRIBUTING_EN.md)
- 📖 [贡献指南 (中文)](./docs/CONTRIBUTING.md)
## 📊 Project Statistics
### BStats Data
## BStats
![bStats](https://bstats.org/signatures/server-implementation/Lophine.svg "bStats")
## Special Thanks
Thanks [LegacyLands](https://github.com/LegacyLands) for sponsoring this project.If you want to develop some crossing folia/non-folia platform plugins, [legacy-lands-library](https://github.com/LegacyLands/legacy-lands-library/) will be a nice choice for you
## 🎉 Special Thanks
![legacy-lands-logo](public/image/legacy-lands-logo.png)
### Project Sponsors
<div align="center">
<b>Thanks to <a href="https://github.com/LegacyLands">LegacyLands</a> for sponsoring this project</b>
<br>
<i>If you want to develop cross-Folia/non-Folia platform plugins, <a href="https://github.com/LegacyLands/legacy-lands-library/">legacy-lands-library</a> would be a great choice</i>
<br><br>
<img src="public/image/legacy-lands-logo.png" alt="LegacyLands Logo" width="200">
</div>
---
## ⭐ Give Us a Star!
> Every free ⭐Star you give is the motivation for our every step forward.
### Star History
## Please ⭐star us!
<a href="https://star-history.com/#LuminolMC/Luminol&LuminolMC/LightingLuminol&LuminolMC/Lophine&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=LuminolMC/Luminol%2CLuminolMC/LightingLuminol%2CLuminolMC/Lophine&type=Date&theme=dark" />
@@ -88,3 +152,7 @@ Thanks [LegacyLands](https://github.com/LegacyLands) for sponsoring this project
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=LuminolMC/Luminol%2CLuminolMC/LightingLuminol%2CLuminolMC/Lophine&type=Date" />
</picture>
</a>
<div align="center">
<b>If this project helps you, please don't forget to give us a ⭐Star!</b>
</div>
+2 -2
View File
@@ -41,7 +41,7 @@ paperweight {
}
val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/"
val menthaMavenPublicUrl = "https://repo.menthamc.com/repository/maven-public/";
val menthaMavenPublicUrl = "https://repo.menthamc.org/repository/maven-public/";
subprojects {
apply(plugin = "java-library")
@@ -88,7 +88,7 @@ subprojects {
extensions.configure<PublishingExtension> {
repositories {
maven("https://repo.menthamc.com/repository/maven-snapshots/") {
maven("https://repo.menthamc.org/repository/maven-snapshots/") {
name = "MenthaMC"
credentials(PasswordCredentials::class) {
username = System.getenv("PRIVATE_MAVEN_REPO_USERNAME")
+13 -10
View File
@@ -1,5 +1,5 @@
为Lophine贡献代码
===========
===============
[English](./CONTRIBUTING_EN.md) | **中文**
@@ -45,17 +45,20 @@ Lophine 使用和 Folia 一样的补丁系统,并为了针对不同部分的
4. 进入 仓库根目录下的 `*-api``*-server` 文件夹进行修改。
以下为对上述各个文件夹的简单描述,详细描述可以参考[这里](https://github.com/Toffikk/paperweight-examples/blob/18241979c88068d5b061d95ad69c98ecb201c246/README.md)
1. API部分
- `lophine-api` :对新增API的修改
- `luminol-api` :对luminol-API的修改应当在此文件夹下进行
- `folia-api` :对folia-API的修改应当在此文件夹下进行
- `paper-api` :对paper-API/spigot-API/bukkit-API的修改应在此文件夹下进行
- `lophine-api` :对新增API的修改
- `luminol-api` :对luminol-API的修改应当在此文件夹下进行
- `folia-api` :对folia-API的修改应在此文件夹下进行
- `paper-api` :对paper-API/spigot-API/bukkit-API的修改应该在此文件夹下进行
2. Server部分
- `lophine-server` :对Minecraft原版服务器的修改和新增文件应当在此文件夹下进行
- `luminol-server` :对luminol-server的修改应当在此文件夹下进行
- `folia-server` :对folia-Server的修改应当在此文件夹下进行
- `paper-server` :对于paper对服务器逻辑的修改应当在此文件夹下进行
- `lophine-server` :对Minecraft原版服务器的修改和新增文件应当在此文件夹下进行
- `luminol-server` :对luminol-server的修改应当在此文件夹下进行
- `folia-server` :对folia-Server的修改应当在此文件夹下进行
- `paper-server` :对于paper对服务器逻辑的修改应当在此文件夹下进行
顺便一提,仓库根目录下的 `*-api``*-server` 并不是正常的 git 仓库:
@@ -82,7 +85,7 @@ Lophine 使用和 Folia 一样的补丁系统,并为了针对不同部分的
1. 在 HEAD 上直接进行修改;
2. 使用 `git commit -a --fixup <hash>` 来进行一个更正提交;(不要提交对在lophine新建文件的修改)
- 如果你想要更改提交信息,你也可以用 `--squash` 来代替 `--fixup`
- 如果你想要更改提交信息,你也可以用 `--squash` 来代替 `--fixup`
3. 使用 `git rebase -i --autosquash base` 来进行自动变基,你只需要输入 `:q` 来关闭确认页面即可;
4. 运行 Gradle 任务 `fixupPaperApiFilePatches` 来修改已被修改的在lophine新建文件的补丁(注意不要提交);
5. 运行 Gradle 任务 `rebuildAllServerPatches` 来修改已被修改的补丁;
+11 -9
View File
@@ -1,5 +1,5 @@
Contributing to Lophine
===========
=======================
**English** | [中文](./CONTRIBUTING.md)
@@ -50,16 +50,18 @@ If you have forked the main repository, then you should follow the steps below:
The following is the simple description of the aforementioned folders, detailed description can be referred to [here](https://github.com/Toffikk/paperweight-examples/blob/18241979c88068d5b061d95ad69c98ecb201c246/README.md):
1. API part
- `lophine-api` : Modifications to the new API
- `luminol-api` : Modifications to Luminol API should be carried out in this folder
- `folia-api` : Modifications to Folia API should be carried out in this folder
- `paper-api` : Modifications to Paper API/Spigot API/Bukkit API should be carried out in this folder
- `lophine-api` : Modifications to the new API
- `luminol-api` : Modifications to Luminol API should be carried out in this folder
- `folia-api` : Modifications to Folia API should be carried out in this folder
- `paper-api` : Modifications to Paper API/Spigot API/Bukkit API should be carried out in this folder
2. Server part
- `lophine-server` : Changes and new files to the Minecraft vanilla server should be made in this folder
- `luminol-server` : Changes to luminol-server should be made in this folder
- `folia-server` : Changes to folia-server should be made in this folder
- `paper-server` : Modifications to the server logic for paper should be made in this folder
- `lophine-server` : Changes and new files to the Minecraft vanilla server should be made in this folder
- `luminol-server` : Changes to luminol-server should be made in this folder
- `folia-server` : Changes to folia-server should be made in this folder
- `paper-server` : Modifications to the server logic for paper should be made in this folder
BTW, `*-api` and `*-server` and are not normal git repositories.
+1 -1
View File
@@ -4,7 +4,7 @@ mcVersion=1.21.8
release=2
# 0 for skip release, 1 for pre-release, 2 for release
luminolRef=059a38cc66981740d5055dca76e156dd5f1d8162
luminolRef=09014c29e45c4d8c1e2ba08bc8d6d58c5f3cd004
org.gradle.configuration-cache=true
org.gradle.caching=true
+71 -70
View File
@@ -1,4 +1,5 @@
### GNU GENERAL PUBLIC LICENSE
==============================
Version 3, 29 June 2007
@@ -217,23 +218,23 @@ produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
@@ -252,42 +253,42 @@ sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
@@ -363,23 +364,23 @@ Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
+2 -2
View File
@@ -1,5 +1,5 @@
The MIT License (MIT)
=====================
### The MIT License (MIT)
=========================
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
+2 -2
View File
@@ -51,7 +51,7 @@
// Luminol start - Dependenices insert
implementation("com.electronwill.night-config:toml:3.8.2") // Night config
implementation("com.github.luben:zstd-jni:1.5.4-1")
@@ -277,14 +_,14 @@
@@ -276,14 +_,14 @@
val gitBranch = git.exec(providers, "rev-parse", "--abbrev-ref", "HEAD").get().trim()
attributes(
"Main-Class" to "org.bukkit.craftbukkit.Main",
@@ -70,7 +70,7 @@
"Build-Number" to (build ?: ""),
"Build-Time" to buildTime.toString(),
"Git-Branch" to gitBranch,
@@ -448,7 +_,7 @@
@@ -447,7 +_,7 @@
}
fill {
@@ -5,35 +5,23 @@ Subject: [PATCH] Rebrand to Lophine
diff --git a/src/main/java/me/earthme/luminol/config/ConfigManager.java b/src/main/java/me/earthme/luminol/config/ConfigManager.java
index 26a812cd526acfd1edf31a5c9799fa6deeae927a..9b01cf6f066b2998a2b178321782be1c96f73b0c 100644
index 4be82092886728b7862b6e8951cf1c8ab917ca55..1b822e9576573c360b997e5cb4fa02278c88b74f 100644
--- a/src/main/java/me/earthme/luminol/config/ConfigManager.java
+++ b/src/main/java/me/earthme/luminol/config/ConfigManager.java
@@ -19,6 +19,7 @@ public class ConfigManager {
@@ -21,6 +21,7 @@ public class ConfigManager {
public static void initConfigs() throws IOException {
configfiles.put("luminol", new ConfigsInstance(new File("luminol_config"), "luminol", "me.earthme.luminol.config.modules"));
+ configfiles.put("lophine", new ConfigsInstance(new File("lophine_config"), "lophine", "fun.bm.lophine.config.modules")); // add lophine config to global config
public static void initConfigs() {
configfiles.put("luminol", ConfigsInstance.of(new File("luminol_config"), "luminol", "me.earthme.luminol.config.modules"));
+ configfiles.put("lophine", ConfigsInstance.of(new File("lophine_config"), "lophine", "fun.bm.lophine.config.modules")); // add lophine config to global config
preLoad();
}
diff --git a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandBlockExecutionConfig.java b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandBlockExecutionConfig.java
index 531214d7ab28ece4bf84e82b39d1c9f983f0fb0c..b68fa0427bb975e91e76f73ab4cc089a424937a3 100644
--- a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandBlockExecutionConfig.java
+++ b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandBlockExecutionConfig.java
@@ -5,6 +5,7 @@ import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class CommandBlockExecutionConfig implements IConfigModule {
+ @me.earthme.luminol.config.flags.TransformedConfig(name = "command_block_enabled", category = {"experiment", "command"}, originInstance = "lophine")
@ConfigInfo(baseName = "enabled")
public static boolean enabled = false;
diff --git a/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java b/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java
index ded1f0667327a70a923ebda55c41b1c9094d3e37..57ddc0832b919b115b85c81ac30f136e32f1e77e 100644
index f164e41975adee8b68dbcb400007681c0b66a4f8..4e76fb3b1274f2fde734089b20458f6561c39700 100644
--- a/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java
+++ b/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java
@@ -6,7 +6,7 @@ import me.earthme.luminol.config.flags.ConfigInfo;
@@ -8,7 +8,7 @@ import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.MISC, mainName = "server_mod_name")
public class ServerModNameConfig implements IConfigModule {
@ConfigInfo(baseName = "name")
- public static String serverModName = "Luminol";
@@ -0,0 +1,30 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Tue, 26 Aug 2025 22:08:54 +0800
Subject: [PATCH] Transformed Configs
diff --git a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java
index 4e906e8e0ebd57b9f7000e4194280e3e3636e52c..14eefd162c9facd54a53718a7a0b1b0266e1ada8 100644
--- a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java
+++ b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java
@@ -11,6 +11,7 @@ public class CommandConfig implements IConfigModule {
@TransformedConfig(name = "enable", category = {"experiment", "force_the_data_command_to_be_enabled"})
@ConfigInfo(baseName = "enable_data_command")
public static boolean data = false;
+ @TransformedConfig(name = "command_block_enabled", category = {"experiment", "command"}, originInstance = "lophine")
@TransformedConfig(name = "enabled", category = {"experiment", "force_enable_command_block_command_execution"})
@ConfigInfo(baseName = "enable_command_block")
public static boolean commandBlock = false;
diff --git a/src/main/java/me/earthme/luminol/config/modules/optimizations/PaperPacketLimiterConfig.java b/src/main/java/me/earthme/luminol/config/modules/optimizations/PaperPacketLimiterConfig.java
index 192a67360db810934d02a15c955af3579e6f6b20..fbafc5a8aa2581301e62e12adb22169ab7fd8e19 100644
--- a/src/main/java/me/earthme/luminol/config/modules/optimizations/PaperPacketLimiterConfig.java
+++ b/src/main/java/me/earthme/luminol/config/modules/optimizations/PaperPacketLimiterConfig.java
@@ -8,6 +8,7 @@ import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.OPTIMIZATIONS, mainName = "force_disable_packet_limiter_of_paper")
public class PaperPacketLimiterConfig implements IConfigModule {
+ @TransformedConfig(name = "unlimit-packet", category = {"misc", "network"}, originInstance = "lophine")
@TransformedConfig(name = "force_disable", category = {"misc", "force_disable_packet_limiter_of_paper"})
@ConfigInfo(baseName = "force_disable")
public static boolean forceDisable = false;
@@ -0,0 +1,188 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Wed, 13 Aug 2025 22:23:46 +0800
Subject: [PATCH] Leaves: Item overstack util
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/d93e9766d3797d130f66179e70a1b374c546fef7/leaves-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java b/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java
index 102de78be0eb82d6898b4104fad1bf5e8366f993..0f8c9d4932c8bbd619f21d4aa28b890e14439b46 100644
--- a/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java
+++ b/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java
@@ -17,12 +17,173 @@
package org.leavesmc.leaves.util;
+import fun.bm.lophine.config.modules.function.ContainerExpansionConfig;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.CustomData;
+import net.minecraft.world.item.component.ItemContainerContents;
+import net.minecraft.world.level.block.ShulkerBoxBlock;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
// Only the used part is retained
public class ItemOverstackUtils {
+ private static final List<ItemUtil> overstackUtils = List.of(
+ new ShulkerBox()
+ );
+
+ public static int getItemStackMaxCount(ItemStack stack) {
+ int size;
+ for (ItemUtil util : overstackUtils) {
+ if ((size = util.getMaxServerStackCount(stack)) != -1) {
+ return size;
+ }
+ }
+ return stack.getMaxStackSize();
+ }
+
+ public static int getNetworkMaxCount(ItemStack stack) {
+ int size;
+ for (ItemUtil util : overstackUtils) {
+ if ((size = util.getMaxClientStackCount(stack)) != -1) {
+ return size;
+ }
+ }
+ return stack.getMaxStackSize();
+ }
+
+ public static boolean tryStackItems(ItemEntity self, ItemEntity other) {
+ for (ItemUtil util : overstackUtils) {
+ if (util.tryStackItems(self, other)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean hasOverstackingItem() {
+ return overstackUtils.stream().anyMatch(ItemUtil::isEnabled);
+ }
+
+ public static int getItemStackMaxCountReal(ItemStack stack) {
+ CompoundTag nbt = Optional.ofNullable(stack.get(DataComponents.CUSTOM_DATA)).orElse(CustomData.EMPTY).copyTag();
+ return nbt.getInt("Leaves.RealStackSize").orElse(stack.getMaxStackSize());
+ }
+
+ public static ItemStack encodeMaxStackSize(ItemStack itemStack) {
+ int realMaxStackSize = getItemStackMaxCountReal(itemStack);
+ int modifiedMaxStackSize = getNetworkMaxCount(itemStack);
+ if (itemStack.getMaxStackSize() != modifiedMaxStackSize) {
+ itemStack.set(DataComponents.MAX_STACK_SIZE, modifiedMaxStackSize);
+ CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag();
+ nbt.putInt("Leaves.RealStackSize", realMaxStackSize);
+ itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt));
+ }
+ return itemStack;
+ }
+
+ public static ItemStack decodeMaxStackSize(ItemStack itemStack) {
+ int realMaxStackSize = getItemStackMaxCountReal(itemStack);
+ if (itemStack.getMaxStackSize() != realMaxStackSize) {
+ itemStack.set(DataComponents.MAX_STACK_SIZE, realMaxStackSize);
+ CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag();
+ nbt.remove("Leaves.RealStackSize");
+ if (nbt.isEmpty()) {
+ itemStack.remove(DataComponents.CUSTOM_DATA);
+ } else {
+ itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt));
+ }
+ }
+ return itemStack;
+ }
+
public static float getItemStackSignalStrength(int maxStackSize, ItemStack itemStack) {
float result = (float) itemStack.getCount() / Math.min(maxStackSize, itemStack.getMaxStackSize());
return Math.clamp(result, 0f, 1f);
}
+
+ public static boolean isStackable(ItemStack itemStack) {
+ return getItemStackMaxCount(itemStack) > 1 && (!itemStack.isDamageableItem() || !itemStack.isDamaged());
+ }
+
+
+ private interface ItemUtil {
+ boolean isEnabled();
+
+ boolean tryStackItems(ItemEntity self, ItemEntity other);
+
+ // number -> modified count, -1 -> I don't care
+ int getMaxServerStackCount(ItemStack stack);
+
+ // number -> modified count, -1 -> I don't care
+ default int getMaxClientStackCount(ItemStack stack) {
+ return getMaxServerStackCount(stack);
+ }
+ }
+
+ private static class ShulkerBox implements ItemUtil {
+ public static boolean shulkerBoxCheck(@NotNull ItemStack stack1, @NotNull ItemStack stack2) {
+ if (ContainerExpansionConfig.nbtShulkerStackable) {
+ return Objects.equals(stack1.getComponents(), stack2.getComponents());
+ }
+ return shulkerBoxNoItem(stack1) && shulkerBoxNoItem(stack2) && Objects.equals(stack1.getComponents(), stack2.getComponents());
+ }
+
+ public static boolean shulkerBoxNoItem(@NotNull ItemStack stack) {
+ return stack.getComponents().getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).stream().findAny().isEmpty();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return ContainerExpansionConfig.shulkerCount > 1;
+ }
+
+ @Override
+ public boolean tryStackItems(ItemEntity self, ItemEntity other) {
+ ItemStack selfStack = self.getItem();
+ if (!isEnabled() ||
+ !(selfStack.getItem() instanceof BlockItem blockItem) ||
+ !(blockItem.getBlock() instanceof ShulkerBoxBlock)
+ ) {
+ return false;
+ }
+
+ ItemStack otherStack = other.getItem();
+ if (selfStack.getItem() == otherStack.getItem()
+ && shulkerBoxCheck(selfStack, otherStack)
+ && selfStack.getCount() != ContainerExpansionConfig.shulkerCount) {
+ int amount = Math.min(otherStack.getCount(), ContainerExpansionConfig.shulkerCount - selfStack.getCount());
+
+ selfStack.grow(amount);
+ self.setItem(selfStack);
+
+ self.pickupDelay = Math.max(other.pickupDelay, self.pickupDelay);
+ self.age = Math.min(other.getAge(), self.age);
+
+ otherStack.shrink(amount);
+ if (otherStack.isEmpty()) {
+ other.discard();
+ } else {
+ other.setItem(otherStack);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int getMaxServerStackCount(ItemStack stack) {
+ if (isEnabled() && stack.getItem() instanceof BlockItem bi &&
+ bi.getBlock() instanceof ShulkerBoxBlock && (ContainerExpansionConfig.nbtShulkerStackable || shulkerBoxNoItem(stack))) {
+ return ContainerExpansionConfig.shulkerCount;
+ }
+ return -1;
+ }
+ }
}
\ No newline at end of file
@@ -5,7 +5,7 @@ Subject: [PATCH] Add config to allow infinite trade
diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java
index 79242037e868a140513f032e74e8594c0c6ff41c..adf4b57aeedf80128dfa03c790616f54062b33e7 100644
index 79242037e868a140513f032e74e8594c0c6ff41c..050e89c3e95c7c3ef7ad98f8ef03ab206241a7e2 100644
--- a/net/minecraft/world/entity/npc/Villager.java
+++ b/net/minecraft/world/entity/npc/Villager.java
@@ -490,8 +490,15 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
@@ -16,9 +16,9 @@ index 79242037e868a140513f032e74e8594c0c6ff41c..adf4b57aeedf80128dfa03c790616f54
+ // Lophine start - infinite trade
+ MerchantOffers offers = this.getOffers().copy();
Player tradingPlayer = this.getTradingPlayer();
+ if (fun.bm.lophine.config.modules.misc.VillagerConfig.villagerInfiniteTrade) {
+ if (fun.bm.lophine.config.modules.function.VillagerConfig.villagerInfiniteTrade) {
+ for (MerchantOffer merchantOffer : offers) {
+ merchantOffer.maxUses = 16384;
+ merchantOffer.maxUses = 524288;
+ }
+ }
+ // Lophine end - infinite trade
@@ -26,7 +26,7 @@ index 79242037e868a140513f032e74e8594c0c6ff41c..adf4b57aeedf80128dfa03c790616f54
tradingPlayer.sendMerchantOffers(
tradingPlayer.containerMenu.containerId,
diff --git a/net/minecraft/world/item/trading/Merchant.java b/net/minecraft/world/item/trading/Merchant.java
index 6e2fa442ddcd4ef3c4fe789b9ebac9dd409c9de7..a876335e26daec9a9a3a2f00ee5cb23cfd913aa6 100644
index 6e2fa442ddcd4ef3c4fe789b9ebac9dd409c9de7..0c7318a0ed214204f5493576c8a7b02fbe51c00b 100644
--- a/net/minecraft/world/item/trading/Merchant.java
+++ b/net/minecraft/world/item/trading/Merchant.java
@@ -42,7 +42,14 @@ public interface Merchant {
@@ -36,9 +36,9 @@ index 6e2fa442ddcd4ef3c4fe789b9ebac9dd409c9de7..a876335e26daec9a9a3a2f00ee5cb23c
- MerchantOffers offers = this.getOffers();
+ // Lophine start - infinite trade
+ MerchantOffers offers = this.getOffers().copy();
+ if (fun.bm.lophine.config.modules.misc.VillagerConfig.villagerInfiniteTrade) {
+ if (fun.bm.lophine.config.modules.function.VillagerConfig.villagerInfiniteTrade) {
+ for (MerchantOffer merchantOffer : offers) {
+ merchantOffer.maxUses = 16384;
+ merchantOffer.maxUses = 5242884;
+ }
+ }
+ // Lophine end - infinite trade
@@ -46,7 +46,7 @@ index 6e2fa442ddcd4ef3c4fe789b9ebac9dd409c9de7..a876335e26daec9a9a3a2f00ee5cb23c
player.sendMerchantOffers(optionalInt.getAsInt(), offers, level, this.getVillagerXp(), this.showProgressBar(), this.canRestock());
}
diff --git a/net/minecraft/world/item/trading/MerchantOffer.java b/net/minecraft/world/item/trading/MerchantOffer.java
index 64c99df8ff305fa28c75dc03fc5ef8c61634ad84..21ec77d5167cfdf05043ea474165148fb2b0c2d9 100644
index 64c99df8ff305fa28c75dc03fc5ef8c61634ad84..770f37d7158c7b4cec62783025f623a68c1f2457 100644
--- a/net/minecraft/world/item/trading/MerchantOffer.java
+++ b/net/minecraft/world/item/trading/MerchantOffer.java
@@ -196,7 +196,7 @@ public class MerchantOffer {
@@ -54,7 +54,7 @@ index 64c99df8ff305fa28c75dc03fc5ef8c61634ad84..21ec77d5167cfdf05043ea474165148f
public boolean isOutOfStock() {
- return this.uses >= this.maxUses;
+ return (this.uses >= this.maxUses && !fun.bm.lophine.config.modules.misc.VillagerConfig.villagerInfiniteTrade); // Lophine - infinite trade
+ return (this.uses >= this.maxUses && !fun.bm.lophine.config.modules.function.VillagerConfig.villagerInfiniteTrade); // Lophine - infinite trade
}
public void setToOutOfStock() {
@@ -1,27 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Wed, 21 May 2025 13:04:00 +0800
Subject: [PATCH] Add config to disable end crystal check
diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java
index 4fa8371c212dcc02b8cf5fd267b736e1cf3f50c1..4fb9244172b74c3c5fed984d5d3b968f76dd12be 100644
--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java
+++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java
@@ -547,6 +547,8 @@ public class EndDragonFight {
blockPos = this.portalLocation;
}
+ // Luminol start - Disable end crystal check
+ if (!fun.bm.lophine.config.modules.misc.DisableEndCrystalCheckConfig.disableCheck) {
// Paper start - Perf: Do crystal-portal proximity check before entity lookup
if (placedEndCrystalPos != null) {
// The end crystal must be 0 or 1 higher than the portal origin
@@ -562,6 +564,7 @@ public class EndDragonFight {
}
}
// Paper end - Perf: Do crystal-portal proximity check before entity lookup
+ } // Luminol end - Disable end crystal check
List<EndCrystal> list = Lists.newArrayList();
@@ -0,0 +1,19 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Tue, 26 Aug 2025 02:55:23 +0800
Subject: [PATCH] Add config to allow generation of end void rings
diff --git a/net/minecraft/world/level/levelgen/DensityFunctions.java b/net/minecraft/world/level/levelgen/DensityFunctions.java
index 04527a5c65ad630f794fed9071d485aedd02257a..57e4c6c60f7d012a9de411b07d000f3504c27e6b 100644
--- a/net/minecraft/world/level/levelgen/DensityFunctions.java
+++ b/net/minecraft/world/level/levelgen/DensityFunctions.java
@@ -528,7 +528,7 @@ public final class DensityFunctions {
int i1 = z / 2;
int i2 = x % 2;
int i3 = z % 2;
- float f = 100.0F - Mth.sqrt((long)x * (long)x + (long)z * (long)z) * 8.0F; // Paper - cast ints to long to avoid integer overflow
+ float f = fun.bm.lophine.config.modules.fixes.EndVoidRingsConfig.enabled ? 100.0F - Mth.sqrt(x * x + z * z) * 8.0F : 100.0F - Mth.sqrt((long)x * (long)x + (long)z * (long)z) * 8.0F; // Paper - cast ints to long to avoid integer overflow // Lophine - customize end void rings generation
f = Mth.clamp(f, -100.0F, 80.0F);
NoiseCache cache = noiseCache.get().computeIfAbsent(noise, noiseKey -> new NoiseCache()); // Paper - Perf: Optimize end generation
@@ -1,14 +1,14 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Tue, 29 Apr 2025 23:03:56 +0800
Subject: [PATCH] Add config to enable Cross Region Damage trace
Subject: [PATCH] Add config to enable cross region damage trace
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index 06e41bca6b964ca71ea7bca547b03cdb3610ba31..b7bc75648634aefc549a69631ab6e401eb902e7d 100644
index 11e6197dec541b28733715f0d7eaa4c7b834d632..6b7c92fdf562dfa46eee8e8acbc82b02fc3226c5 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -1341,6 +1341,13 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -1345,6 +1345,13 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
this.awardStat(Stats.ENTITY_KILLED_BY.get(killCredit.getType()));
killCredit.awardKillScore(this, cause);
this.createWitherRose(killCredit);
@@ -22,7 +22,7 @@ index 06e41bca6b964ca71ea7bca547b03cdb3610ba31..b7bc75648634aefc549a69631ab6e401
}
this.level().broadcastEntityEvent(this, (byte)3);
@@ -1364,6 +1371,24 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -1368,6 +1375,24 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
}
// Leaves end - exp fix
@@ -48,40 +48,10 @@ index 06e41bca6b964ca71ea7bca547b03cdb3610ba31..b7bc75648634aefc549a69631ab6e401
AABB aabb = new AABB(this.blockPosition()).inflate(32.0, 10.0, 32.0);
this.level()
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index d55a6989fd68f0bcf7bd05e9420fda1115afa6c0..5369a6597e48ed9f0d8f7f0b9685ce4950df4fe5 100644
index 567d63ae589705e13403814b36e35d00c241a69e..3f4b1c54f9207ba1854a8c8da965cbaca5ea517d 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -1216,6 +1216,29 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
}
}
+ // Luminol Start - raid revert adapt Cross Region Damage trace
+ public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent, boolean async) {
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) {
+ return addEffect(effectInstance, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true);
+ } else if (fun.bm.lophine.config.modules.experiment.EntityDamageSourceTraceConfig.enabled) {
+ postToEntityThreadAddEffect(effectInstance, entity, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true);
+ return true;
+ }
+ return false;
+ }
+
+ private void postToEntityThreadAddEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) {
+ if (entity != null)
+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> {
+ try {
+ addEffect(effectInstance, nmsEntity, cause, fireEvent);
+ } catch (Throwable ex) {
+ LOGGER.error(ex.getMessage(), ex);
+ }
+ }, null, 1L );
+ }
+ // Luminol End - raid revert adapt Cross Region Damage trace
+
public boolean canBeAffected(MobEffectInstance effectInstance) {
if (this.getType().is(EntityTypeTags.IMMUNE_TO_INFESTED)) {
return !effectInstance.is(MobEffects.INFESTED);
@@ -1948,6 +1971,13 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -1948,6 +1948,13 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
final LivingEntity killer = this.getKillCredit();
if (killer != null) {
killer.awardKillScore(this, damageSource);
@@ -95,7 +65,7 @@ index d55a6989fd68f0bcf7bd05e9420fda1115afa6c0..5369a6597e48ed9f0d8f7f0b9685ce49
}
}); // Paper end
this.postDeathDropItems(deathEvent); // Paper
@@ -1958,6 +1988,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -1958,6 +1965,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
return deathEvent; // Paper
}
@@ -114,7 +84,7 @@ index d55a6989fd68f0bcf7bd05e9420fda1115afa6c0..5369a6597e48ed9f0d8f7f0b9685ce49
protected void dropEquipment(ServerLevel level) {
}
protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {} // Paper - method for post death logic that cannot be ran before the event is potentially cancelled
@@ -2556,6 +2598,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
@@ -2556,6 +2575,18 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
}
}
@@ -133,22 +103,3 @@ index d55a6989fd68f0bcf7bd05e9420fda1115afa6c0..5369a6597e48ed9f0d8f7f0b9685ce49
public final float getMaxHealth() {
return (float)this.getAttributeValue(Attributes.MAX_HEALTH);
}
diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java
index 510514e363f4816a6e286fb9b5488153e03c9e55..4638af10539a316acae64fb26881c8e699c496e0 100644
--- a/net/minecraft/world/entity/raid/Raider.java
+++ b/net/minecraft/world/entity/raid/Raider.java
@@ -157,7 +157,13 @@ public abstract class Raider extends PatrollingMonster {
net.minecraft.world.effect.MobEffectInstance mobeffect1 = new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.BAD_OMEN, fun.bm.lophine.config.modules.misc.RaidChangesConfig.infinite ? net.minecraft.world.effect.MobEffectInstance.INFINITE_DURATION : 120000, i, false, false, true);
if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_DISABLE_RAIDS)) {
- entityhuman.addEffect(mobeffect1, entityhuman, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true); // CraftBukkit
+ // Luminol start - Raid changes adapt DamageSource trace
+ if (fun.bm.lophine.config.modules.experiment.EntityDamageSourceTraceConfig.enabled) {
+ entityhuman.addEffect(mobeffect1, entityhuman, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true, true);
+ } else {
+ entityhuman.addEffect(mobeffect1, entityhuman, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true); // CraftBukkit
+ }
+ // Luminol end - Raid changes adapt DamageSource trace
}
this.setPatrolLeader(false);
}
@@ -5,11 +5,11 @@ Subject: [PATCH] Add config to enable datapack command
diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java
index 0d5ed9602492c8393088954105e9a19032067b8b..e2fc15d84fafde6847ecead23c55169449275e1b 100644
index e05f75b6bbd48d6fb7379926d68d5f8ed6014be9..af895859057ca303177ec3c8a07cf7534e62cca6 100644
--- a/net/minecraft/commands/Commands.java
+++ b/net/minecraft/commands/Commands.java
@@ -192,7 +192,9 @@ public class Commands {
if(me.earthme.luminol.config.modules.experiment.CommandDataConfig.enabled) {
if(me.earthme.luminol.config.modules.experiment.CommandConfig.data) {
DataCommands.register(this.dispatcher); // Folia - region threading - TODO
}
- //DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO
@@ -20,10 +20,10 @@ index 0d5ed9602492c8393088954105e9a19032067b8b..e2fc15d84fafde6847ecead23c551694
DefaultGameModeCommands.register(this.dispatcher);
//DialogCommand.register(this.dispatcher, context); // Folia - region threading - TODO
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 6675b83c9f15d0341dcf169ec3ac20f401c5d10b..e8c9e739de35313c0212bab97a202fa362fd0887 100644
index 100e8fdc52c201d386e3c779d19f6b5abde39820..d08a5b553f4e5932d960fbcdeb6a43419730e32a 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -2349,6 +2349,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -2352,6 +2352,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return this.reloadResources(selectedIds, io.papermc.paper.event.server.ServerResourcesReloadedEvent.Cause.PLUGIN);
}
@@ -1,207 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Fri, 11 Apr 2025 16:53:57 +0800
Subject: [PATCH] Add config to revert raid changes
Co-authored by: huanli233 <392352840@qq.com>
Some of changes is a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/b5793e809b6346e16b7ea218496806a312562e27/leaves-server/minecraft-patches/features/0106-Revert-raid-changes.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/effect/BadOmenMobEffect.java b/net/minecraft/world/effect/BadOmenMobEffect.java
index 80f17f33f670018240c854df589cf90cdeab6e70..985f27d249810bf133a520d937b7006e57f553ec 100644
--- a/net/minecraft/world/effect/BadOmenMobEffect.java
+++ b/net/minecraft/world/effect/BadOmenMobEffect.java
@@ -22,6 +22,11 @@ class BadOmenMobEffect extends MobEffect {
&& !serverPlayer.isSpectator()
&& level.getDifficulty() != Difficulty.PEACEFUL
&& level.isVillage(serverPlayer.blockPosition())) {
+ // Leaves start - Revert raid changes
+ if (fun.bm.lophine.config.modules.misc.RaidChangesConfig.trigger) {
+ return level.getRaids().createOrExtendRaid(serverPlayer, serverPlayer.blockPosition()) == null;
+ }
+ // Leaves end - Revert raid changes
Raid raidAt = level.getRaidAt(serverPlayer.blockPosition());
if (raidAt == null || raidAt.getRaidOmenLevel() < raidAt.getMaxRaidOmenLevel()) {
serverPlayer.addEffect(new MobEffectInstance(MobEffects.RAID_OMEN, 600, amplifier));
diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java
index 1910bff3f08147bfd63703097e30b9f88c84e337..26f4d7359f638a11ea2d768d18091e261d6b1791 100644
--- a/net/minecraft/world/entity/raid/Raid.java
+++ b/net/minecraft/world/entity/raid/Raid.java
@@ -340,7 +340,20 @@ public class Raid {
}
if (flag1) {
- this.waveSpawnPos = this.getValidSpawnPos(level);
+ // Luminol Start - Raid revert
+ if (!fun.bm.lophine.config.modules.misc.RaidChangesConfig.posRevert) {
+ this.waveSpawnPos = this.getValidSpawnPos(level);
+ } else {
+ int n4 = 0;
+ if (this.raidCooldownTicks < 100) {
+ n4 = 1;
+ }
+ if (this.raidCooldownTicks < 40) {
+ n4 = 2;
+ }
+ this.waveSpawnPos = this.getValidSpawnPos(level, n4);
+ }
+ // Luminol End - Raid revert
}
if (this.raidCooldownTicks == 300 || this.raidCooldownTicks % 20 == 0) {
@@ -375,7 +388,14 @@ public class Raid {
int i = 0;
while (this.shouldSpawnGroup()) {
- BlockPos blockPos = this.waveSpawnPos.orElseGet(() -> this.findRandomSpawnPos(level, 20));
+ // Luminol Start - Raid revert
+ BlockPos blockPos;
+ if (!fun.bm.lophine.config.modules.misc.RaidChangesConfig.posRevert) {
+ blockPos = this.waveSpawnPos.orElseGet(() -> this.findRandomSpawnPos(level, 20));
+ } else {
+ blockPos = this.waveSpawnPos.isPresent() ? this.waveSpawnPos.get() : this.findRandomSpawnPos(level, i, 20);
+ }
+ // Luminol End - Raid revert
if (blockPos != null) {
this.started = true;
this.spawnGroup(level, blockPos);
@@ -387,7 +407,7 @@ public class Raid {
i++;
}
- if (i > 5) {
+ if (i > (fun.bm.lophine.config.modules.misc.RaidChangesConfig.posRevert ? 3 : 5)) { // Luminol - Raid revert
org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(level, this, org.bukkit.event.raid.RaidStopEvent.Reason.UNSPAWNABLE); // CraftBukkit
this.stop();
break;
@@ -458,6 +478,17 @@ public class Raid {
return blockPos != null ? Optional.of(blockPos) : Optional.empty();
}
+ // Luminol Start - Raid revert
+ private Optional<BlockPos> getValidSpawnPos(ServerLevel level, int n) {
+ for (int i = 0; i < 3; ++i) {
+ BlockPos blockPos = this.findRandomSpawnPos(level, n, 1);
+ if (blockPos == null) continue;
+ return Optional.of(blockPos);
+ }
+ return Optional.empty();
+ }
+ // Luminol End - Raid revert
+
private boolean hasMoreWaves() {
return this.hasBonusWave() ? !this.hasSpawnedBonusWave() : !this.isFinalWave();
}
@@ -683,7 +714,7 @@ public class Raid {
int i2 = this.center.getX() + Mth.floor(Mth.cos(f2) * 32.0F * f) + level.random.nextInt(3) * Mth.floor(f);
int i3 = this.center.getZ() + Mth.floor(Mth.sin(f2) * 32.0F * f) + level.random.nextInt(3) * Mth.floor(f);
int height = level.getHeight(Heightmap.Types.WORLD_SURFACE, i2, i3);
- if (Mth.abs(height - this.center.getY()) <= 96) {
+ if (fun.bm.lophine.config.modules.misc.RaidChangesConfig.heightCheck || Mth.abs(height - this.center.getY()) <= 96) { // Leaves - Disable height check
mutableBlockPos.set(i2, height, i3);
if (!level.isVillage(mutableBlockPos) || i <= 7) {
int i4 = 10;
@@ -702,6 +733,26 @@ public class Raid {
return null;
}
+ // Luminol Start - Raid revert
+ @Nullable
+ private BlockPos findRandomSpawnPos(ServerLevel level, int n, int n2) {
+ int n3 = 2 - n;
+ BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+ SpawnPlacementType spawnPlacementType = SpawnPlacements.getPlacementType(EntityType.RAVAGER);
+ for (int i = 0; i < n2; ++i) {
+ float f = level.random.nextFloat() * ((float)Math.PI * 2);
+ int n4 = this.center.getX() + Mth.floor(Mth.cos(f) * 32.0f * (float)n3) + level.random.nextInt(5);
+ int n5 = this.center.getZ() + Mth.floor(Mth.sin(f) * 32.0f * (float)n3) + level.random.nextInt(5);
+ int n6 = level.getHeight(Heightmap.Types.WORLD_SURFACE, n4, n5);
+ mutableBlockPos.set(n4, n6, n5);
+ if (level.isVillage(mutableBlockPos) && n < 2) continue;
+ if (!level.hasChunksAt(mutableBlockPos.getX() - 10, mutableBlockPos.getZ() - 10, mutableBlockPos.getX() + 10, mutableBlockPos.getZ() + 10) || !level.isPositionEntityTicking(mutableBlockPos) || !spawnPlacementType.isSpawnPositionOk(level, mutableBlockPos, EntityType.RAVAGER) && (!level.getBlockState(mutableBlockPos.below()).is(Blocks.SNOW) || !level.getBlockState(mutableBlockPos).isAir())) continue;
+ return mutableBlockPos;
+ }
+ return null;
+ }
+ // Luminol End - Raid revert
+
private boolean addWaveMob(ServerLevel level, int wave, Raider raider) {
// Folia start - make raids thread-safe
if (!this.ownsRaid(level)) {
diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java
index 3e30989c99cd11c8c94d6fd194d2969e3a7efda6..510514e363f4816a6e286fb9b5488153e03c9e55 100644
--- a/net/minecraft/world/entity/raid/Raider.java
+++ b/net/minecraft/world/entity/raid/Raider.java
@@ -128,6 +128,41 @@ public abstract class Raider extends PatrollingMonster {
currentRaid.removeFromRaid(serverLevel, this, false);
}
+
+ // Leaves start - Revert raid changes
+ if (fun.bm.lophine.config.modules.misc.RaidChangesConfig.effect && !this.hasRaid()) {
+ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD);
+ net.minecraft.world.entity.player.Player entityhuman = null;
+ if (entity instanceof net.minecraft.world.entity.player.Player player) {
+ entityhuman = player;
+ } else if (entity instanceof net.minecraft.world.entity.animal.wolf.Wolf wolf) {
+ LivingEntity entityliving = wolf.getOwner();
+ if (wolf.isTame() && entityliving instanceof net.minecraft.world.entity.player.Player player) {
+ entityhuman = player;
+ }
+ }
+
+ if (entityhuman != null && !itemstack.isEmpty() && this.isCaptain()) {
+ net.minecraft.world.effect.MobEffectInstance mobeffect = entityhuman.getEffect(net.minecraft.world.effect.MobEffects.BAD_OMEN);
+ int i = 1;
+
+ if (mobeffect != null) {
+ i += mobeffect.getAmplifier();
+ entityhuman.removeEffectNoUpdate(net.minecraft.world.effect.MobEffects.BAD_OMEN);
+ } else {
+ --i;
+ }
+
+ i = net.minecraft.util.Mth.clamp(i, 0, 4);
+ net.minecraft.world.effect.MobEffectInstance mobeffect1 = new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.BAD_OMEN, fun.bm.lophine.config.modules.misc.RaidChangesConfig.infinite ? net.minecraft.world.effect.MobEffectInstance.INFINITE_DURATION : 120000, i, false, false, true);
+
+ if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_DISABLE_RAIDS)) {
+ entityhuman.addEffect(mobeffect1, entityhuman, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true); // CraftBukkit
+ }
+ this.setPatrolLeader(false);
+ }
+ }
+ // Leaves end - Revert raid changes
}
super.die(cause);
@@ -156,7 +191,7 @@ public abstract class Raider extends PatrollingMonster {
}
public boolean hasRaid() {
- return this.level() instanceof ServerLevel serverLevel && (this.getCurrentRaid() != null || serverLevel.getRaidAt(this.blockPosition()) != null);
+ return !fun.bm.lophine.config.modules.misc.RaidChangesConfig.selfCheck && (this.level() instanceof ServerLevel serverLevel && (this.getCurrentRaid() != null || serverLevel.getRaidAt(this.blockPosition()) != null)); // Leaves - Disable raid self check
}
public boolean hasActiveRaid() {
diff --git a/net/minecraft/world/item/component/OminousBottleAmplifier.java b/net/minecraft/world/item/component/OminousBottleAmplifier.java
index 33907bb190ffa22ccf9ea424b1e536297878711a..2fca459f2c888079322f6b957bbee3ac0e33c4bc 100644
--- a/net/minecraft/world/item/component/OminousBottleAmplifier.java
+++ b/net/minecraft/world/item/component/OminousBottleAmplifier.java
@@ -29,7 +29,7 @@ public record OminousBottleAmplifier(int value) implements ConsumableListener, T
@Override
public void onConsume(Level level, LivingEntity entity, ItemStack stack, Consumable consumable) {
- entity.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true)); // Paper - properly resend entities - diff on change for below
+ entity.addEffect(new MobEffectInstance(MobEffects.BAD_OMEN, fun.bm.lophine.config.modules.misc.RaidChangesConfig.infinite ? net.minecraft.world.effect.MobEffectInstance.INFINITE_DURATION : 120000, this.value, false, false, true)); // Paper - properly resend entities - diff on change for below // Luminol - Raid effect infinite
}
// Paper start - properly resend entities - collect packets for bundle
@@ -41,7 +41,7 @@ public record OminousBottleAmplifier(int value) implements ConsumableListener, T
@Override
public void addToTooltip(Item.TooltipContext context, Consumer<Component> tooltipAdder, TooltipFlag flag, DataComponentGetter componentGetter) {
- List<MobEffectInstance> list = List.of(new MobEffectInstance(MobEffects.BAD_OMEN, 120000, this.value, false, false, true));
+ List<MobEffectInstance> list = List.of(new MobEffectInstance(MobEffects.BAD_OMEN, fun.bm.lophine.config.modules.misc.RaidChangesConfig.infinite ? net.minecraft.world.effect.MobEffectInstance.INFINITE_DURATION : 120000, this.value, false, false, true)); // Luminol - Raid effect infinite
PotionContents.addPotionTooltip(list, tooltipAdder, 1.0F, context.tickRate());
}
}
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: MrHua269 <wangxyper@163.com>
Date: Wed, 5 Feb 2025 15:22:19 +0800
Subject: [PATCH] Add config to enable Raytracing tracker
Subject: [PATCH] Add config to enable raytracing tracker
diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
@@ -18,7 +18,7 @@ index 6868b915bf3deb85783a638d4441a15fea6da2dc..70740381c6501c1a518c52b24381edd1
double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1);
if (rangeY != -1) {
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index 2f733a3f75f9fe637864158a31d7507eac121d45..f7fcb4782b49374df8fe8f4401a4259f037221f7 100644
index 8ec753fe9c84ffe1188f7e7ebb6895cf4bba9e39..ee4a459700d75676eae3422d4a0e10be73c45871 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -147,7 +147,7 @@ import net.minecraft.world.waypoints.WaypointTransmitter;
@@ -30,7 +30,7 @@ index 2f733a3f75f9fe637864158a31d7507eac121d45..f7fcb4782b49374df8fe8f4401a4259f
// CraftBukkit start
private static final int CURRENT_LEVEL = 2;
public boolean preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; keep initial motion on first snapTo
@@ -6439,4 +6439,46 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@@ -6472,4 +6472,46 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
// Paper end - Expose entity id counter
public boolean shouldTickHot() { return this.tickCount > 20 * 10 && this.isAlive(); } // KioCG
@@ -44,7 +44,7 @@ index e1842ab8f20271a8b20ab8462b09a4745a612a9a..8405157ec35a443e238fa6866772c839
world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
}
diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java
index 0357792de0ed8ec9058d1847c8b45c33ff365af6..4945c11f573a53df2f5be4beb7af6a3f4d32f7d4 100644
index c485d5b3ba95f2a969bdf0dab9654ac284976f8d..b957585cf7cf7f845e20f59c7b355722880f3b4e 100644
--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java
+++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java
@@ -31,8 +31,8 @@ public final class TickRegionScheduler {
@@ -97,10 +97,10 @@ index 0357792de0ed8ec9058d1847c8b45c33ff365af6..4945c11f573a53df2f5be4beb7af6a3f
this.scheduler.regionFailed(this, false, thr);
// regionFailed will schedule a shutdown, so we should avoid letting this region tick further
diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java
index 8a90aa5c145f53afbb3f2371f97cb09974f30bbb..0d5ed9602492c8393088954105e9a19032067b8b 100644
index af895859057ca303177ec3c8a07cf7534e62cca6..94a3cea5d42b1f98bbde22ef514f8f7dbd64ba58 100644
--- a/net/minecraft/commands/Commands.java
+++ b/net/minecraft/commands/Commands.java
@@ -243,7 +243,11 @@ public class Commands {
@@ -245,7 +245,11 @@ public class Commands {
TeleportCommand.register(this.dispatcher);
TellRawCommand.register(this.dispatcher, context);
//TestCommand.register(this.dispatcher, context); // Folia - region threading
@@ -114,7 +114,7 @@ index 8a90aa5c145f53afbb3f2371f97cb09974f30bbb..0d5ed9602492c8393088954105e9a190
TitleCommand.register(this.dispatcher, context);
//TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 2ed2f4cca3a81117c0beaf7a11bbbe8156f6a9ce..6675b83c9f15d0341dcf169ec3ac20f401c5d10b 100644
index d08a5b553f4e5932d960fbcdeb6a43419730e32a..a0f1db50adf5163a671d3d9e9df7adb7de4250c6 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -267,7 +267,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -5,7 +5,7 @@ Subject: [PATCH] Add config to enable unsafe waypoint bar
diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java
index e2fc15d84fafde6847ecead23c55169449275e1b..b0a06cdfaa49237f372ecd131d47289160476b07 100644
index 94a3cea5d42b1f98bbde22ef514f8f7dbd64ba58..e311977fca4d0ba6e3efd4a41b2732cf5e7b4b9c 100644
--- a/net/minecraft/commands/Commands.java
+++ b/net/minecraft/commands/Commands.java
@@ -253,7 +253,11 @@ public class Commands {
@@ -0,0 +1,198 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 3 Aug 2025 15:24:01 +0800
Subject: [PATCH] Leaves: Base Protocol Core
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/minecraft-patches/features/0004-Leaves-Protocol-Core.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java
index 8405157ec35a443e238fa6866772c839621d73d7..4edb6e6e6287f232f7eef45c747554264ec05319 100644
--- a/io/papermc/paper/threadedregions/RegionizedServer.java
+++ b/io/papermc/paper/threadedregions/RegionizedServer.java
@@ -334,6 +334,8 @@ public final class RegionizedServer {
}
// Luminol end - Add a config to enable tick command
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleTick(tickCount); // Leaves - protocol
+
// tick connections
this.tickConnections();
@@ -443,6 +445,8 @@ public final class RegionizedServer {
world.updateTickData();
+ MinecraftServer.getServer().handleTickCount(tickCount); // Lophine - reuse tick count
+
world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates
}
diff --git a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java
index fb263fa1f30a7dfcb7ec2656abfb38e5fe88eac9..56fd1ed7ccaf96e7eedea60fbdbf7f934939d563 100644
--- a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java
+++ b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java
@@ -40,13 +40,22 @@ public interface CustomPacketPayload {
@Override
public void encode(B buffer, CustomPacketPayload value) {
+ // Leaves start - protocol core
+ if (value instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload payload) {
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.encode(buffer, payload);
+ return;
+ }
+ // Leaves end - protocol core
this.writeCap(buffer, value.type(), value);
}
@Override
public CustomPacketPayload decode(B buffer) {
ResourceLocation resourceLocation = buffer.readResourceLocation();
- return (CustomPacketPayload)this.findCodec(resourceLocation).decode(buffer);
+ // Leaves start - protocol core
+ var payload = org.leavesmc.leaves.protocol.core.LeavesProtocolManager.decode(resourceLocation, buffer);
+ return java.util.Objects.requireNonNullElseGet(payload, () -> this.findCodec(resourceLocation).decode(buffer));
+ // Leaves end - protocol core
}
};
}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index a0f1db50adf5163a671d3d9e9df7adb7de4250c6..af13707933af14171447d5ab6faacb64ddf194a7 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -305,6 +305,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Folia start - regionised ticking
public final io.papermc.paper.threadedregions.RegionizedServer regionizedServer = new io.papermc.paper.threadedregions.RegionizedServer();
+ private int tickCount;
@Override
public <V> CompletableFuture<V> submit(java.util.function.Supplier<V> task) {
@@ -2226,9 +2227,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return false;
}
+ // Lophine start - reuse tick count
public int getTickCount() {
- throw new UnsupportedOperationException(); // Folia - region threading
+ if (false) throw new UnsupportedOperationException(); // Folia - region threading
+ return this.tickCount;
+ }
+
+ public void handleTickCount(int tickCount1) {
+ tickCount = tickCount1;
}
+ // Lophine end - reuse tick count
public int getSpawnProtectionRadius() {
return 16;
diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
index 8150b16c196edcb226be9268ea6e0012e44517fa..c54bf0429c0ca3a35a730658c7b1b3ddc776bf9e 100644
--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
@@ -57,6 +57,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
public @Nullable String playerBrand;
public final java.util.Set<String> pluginMessagerChannels;
// Paper end - retain certain values
+ public final GameProfile profile; // Leaves - protocol core
public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
this.server = server;
@@ -70,6 +71,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
this.pluginMessagerChannels = cookie.channels();
this.keepAlive = cookie.keepAlive();
// Paper end
+ this.profile = cookie.gameProfile(); // Leaves - protocol core
}
// Paper start - configuration phase API
@@ -160,6 +162,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
@Override
public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+ // Leaves start - protocol
+ if (packet.payload() instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload leavesPayload) {
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePayload(org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this), leavesPayload);
+ return;
+ }
+ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.DiscardedPayload(net.minecraft.resources.ResourceLocation id, byte[] data)) {
+ if (org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleBytebuf(org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this), id, io.netty.buffer.Unpooled.wrappedBuffer(data))) {
+ return;
+ }
+ }
+ // Leaves end - protocol
+
// Paper start
if (!(packet.payload() instanceof final net.minecraft.network.protocol.common.custom.DiscardedPayload discardedPayload)) {
return;
@@ -219,10 +233,11 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
final String channel = new String(data, from, length, java.nio.charset.StandardCharsets.US_ASCII);
if (register) {
bridge.addChannel(channel);
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleMinecraftRegister(channel, org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this)); // Leaves - protocol
} else {
bridge.removeChannel(channel);
}
- // Paper end
+ // Paper end
}
@Override
@@ -385,9 +400,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
net.minecraft.server.level.ServerPlayer player = serverGamePacketListener.player;
org.bukkit.event.player.PlayerKickEvent.Cause cause = disconnectionDetails.disconnectionReason().orElseThrow().game().orElse(org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
org.bukkit.event.player.PlayerKickEvent event = new org.bukkit.event.player.PlayerKickEvent(
- player.getBukkitEntity(),
- io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()),
- rawLeaveMessage, cause
+ player.getBukkitEntity(),
+ io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()),
+ rawLeaveMessage, cause
);
@@ -420,10 +435,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
private void disconnect0(DisconnectionDetails disconnectionDetails) {
this.connection
- .send(
- new ClientboundDisconnectPacket(disconnectionDetails.reason()),
- PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
- );
+ .send(
+ new ClientboundDisconnectPacket(disconnectionDetails.reason()),
+ PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
+ );
this.onDisconnect(disconnectionDetails);
this.connection.setReadOnly();
// CraftBukkit - Don't wait
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 4ef2816210f06082f2209c51d04aad6bb188232b..593dbcda4b497291eb1ecdc64b947a8d279f8be6 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -423,6 +423,8 @@ public abstract class PlayerList {
//return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect
}
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player);
+
final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure
@@ -605,6 +607,7 @@ public abstract class PlayerList {
return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName())));
}
public @Nullable net.kyori.adventure.text.Component remove(ServerPlayer player, net.kyori.adventure.text.Component leaveMessage) {
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerLeave(player); // Leaves - protocol
// Paper end - Fix kick event leave message not being sent
ServerLevel serverLevel = player.level();
player.awardStat(Stats.LEAVE_GAME);
@@ -1520,6 +1523,7 @@ public abstract class PlayerList {
serverPlayer.connection.send(clientboundUpdateRecipesPacket);
serverPlayer.getRecipeBook().sendInitialRecipeBook(serverPlayer);
}
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleDataPackReload(); // Leaves - protocol core
}
public boolean isAllowCommandsForAllPlayers() {
@@ -1,62 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 27 Jul 2025 13:48:10 +0800
Subject: [PATCH] Leaves: Disable packet limit
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/6a2b9ee6b330cd18b0a8eb2a824d561fa76012bb/leaves-server/minecraft-patches/features/0043-Disable-packet-limit.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
index 6dc5f3e88a3886b03b99d59b317085fdc3e2fa9a..e556bcaf2a5863073a047947f5962179d054e8d9 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -280,8 +280,8 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
if (this.stopReadingPackets) {
return;
}
- if (this.allPacketCounts != null ||
- io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) {
+ if (!fun.bm.lophine.config.modules.optimizations.NetworkConfig.disablePacketLimit && (this.allPacketCounts != null || // Leaves - can disable
+ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass()))) { // Leaves - can disable
long time = System.nanoTime();
synchronized (PACKET_LIMIT_LOCK) {
if (this.allPacketCounts != null) {
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 68f0023c4234585a8385a830ed410883c8bd1549..206b4a532eba02b2a1f6f3b832970bc4c94fc45b 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -856,7 +856,7 @@ public class ServerGamePacketListenerImpl
public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
// PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); // Paper - AsyncTabCompleteEvent; run this async
// CraftBukkit start
- if (!this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) { // Paper - configurable tab spam limits
+ if (!fun.bm.lophine.config.modules.optimizations.NetworkConfig.disablePacketLimit && !this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) { // Paper - configurable tab spam limits // Leaves - can disable
this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause // Paper - add proper async disconnect
return;
}
@@ -2047,6 +2047,7 @@ public class ServerGamePacketListenerImpl
private static int getSpamThreshold() { return io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold; } // Paper - Configurable threshold
private boolean checkLimit(long timestamp) {
+ if (fun.bm.lophine.config.modules.optimizations.NetworkConfig.disablePacketLimit) return true; // Leaves - disable
if (this.lastLimitedPacket != -1 && timestamp - this.lastLimitedPacket < getSpamThreshold() && this.limitedPackets++ >= 8) { // Paper - Configurable threshold; raise packet limit to 8
return false;
}
@@ -2586,6 +2587,7 @@ public class ServerGamePacketListenerImpl
// Spigot start - spam exclusions
private void detectRateSpam(String message) {
+ if (fun.bm.lophine.config.modules.optimizations.NetworkConfig.disablePacketLimit) return; // Leaves - disable
// CraftBukkit start - replaced with thread safe throttle
if (org.spigotmc.SpigotConfig.enableSpamExclusions) {
for (String exclude : org.spigotmc.SpigotConfig.spamExclusions) {
@@ -3333,7 +3335,7 @@ public class ServerGamePacketListenerImpl
@Override
public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) {
// Paper start - auto recipe limit
- if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ if (!fun.bm.lophine.config.modules.optimizations.NetworkConfig.disablePacketLimit && !org.bukkit.Bukkit.isPrimaryThread()) { // Leaves - can disable
if (!this.recipeSpamPackets.isIncrementAndUnderThreshold()) {
this.disconnectAsync(net.minecraft.network.chat.Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - add proper async disconnect
return;
@@ -28,7 +28,7 @@ index 643797124fe5a4489d0b7419b7e600c04f283ef2..04b4100da96aad50f08e8c59200eec93
public String serialize(HolderLookup.Provider levelRegistry) {
diff --git a/net/minecraft/server/commands/GiveCommand.java b/net/minecraft/server/commands/GiveCommand.java
index 5a1c21b05545a03fbb00cf734605049870d3eecb..a0459bc6b2241e7fca74bfbfce058882a8ad7f49 100644
index 7ef109cb9921c25987905149616eddbd0b2f9b9c..70fc9d6c5b226211ff5964464c37e11695d5f745 100644
--- a/net/minecraft/server/commands/GiveCommand.java
+++ b/net/minecraft/server/commands/GiveCommand.java
@@ -55,7 +55,7 @@ public class GiveCommand {
@@ -41,10 +41,10 @@ index 5a1c21b05545a03fbb00cf734605049870d3eecb..a0459bc6b2241e7fca74bfbfce058882
if (count > i) {
source.sendFailure(Component.translatable("commands.give.failed.toomanyitems", i, itemStack.getDisplayName()));
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b511bf13241 100644
index 74ec003486f9362d2bafbad8392b9bbdf0c15882..f06b9d8ad9512d35f3f460eb77c869690321c67f 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -3078,7 +3078,7 @@ public class ServerGamePacketListenerImpl
@@ -3079,7 +3079,7 @@ public class ServerGamePacketListenerImpl
} else if (slot.mayPlace(cursor)) {
if (ItemStack.isSameItemSameComponents(clickedItem, cursor)) {
int toPlace = packet.buttonNum() == 0 ? cursor.getCount() : 1;
@@ -53,7 +53,7 @@ index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b51
toPlace = Math.min(toPlace, slot.container.getMaxStackSize() - clickedItem.getCount());
if (toPlace == 1) {
action = InventoryAction.PLACE_ONE;
@@ -3114,7 +3114,7 @@ public class ServerGamePacketListenerImpl
@@ -3115,7 +3115,7 @@ public class ServerGamePacketListenerImpl
}
} else if (ItemStack.isSameItemSameComponents(cursor, clickedItem)) {
if (clickedItem.getCount() >= 0) {
@@ -62,15 +62,15 @@ index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b51
// As of 1.5, this is result slots only
action = InventoryAction.PICKUP_ALL;
}
@@ -3325,6 +3325,7 @@ public class ServerGamePacketListenerImpl
@@ -3326,6 +3326,7 @@ public class ServerGamePacketListenerImpl
this.player.containerMenu.broadcastFullState();
} else {
this.player.containerMenu.broadcastChanges();
+ if (org.leavesmc.leaves.util.ItemOverstackUtils.hasOverstackingItem()) this.player.containerMenu.broadcastCarriedItem(); // Leaves - item over-stack util - force send carried item
}
if (packet.buttonNum() == Inventory.SLOT_OFFHAND && this.player.containerMenu != this.player.inventoryMenu) this.player.containerSynchronizer.sendOffHandSlotChange(); // Paper - update offhand data when the player is clicking in an inventory not their own as the sychronizer does not include offhand slots
if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
}
@@ -3435,7 +3436,7 @@ public class ServerGamePacketListenerImpl
@@ -3437,7 +3438,7 @@ public class ServerGamePacketListenerImpl
}
boolean flag1 = packet.slotNum() >= 1 && packet.slotNum() <= 45;
@@ -79,7 +79,7 @@ index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b51
if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot
// CraftBukkit start - Call click event
org.bukkit.inventory.InventoryView inventory = this.player.inventoryMenu.getBukkitView();
@@ -3477,6 +3478,7 @@ public class ServerGamePacketListenerImpl
@@ -3479,6 +3480,7 @@ public class ServerGamePacketListenerImpl
this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemStack);
this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemStack);
this.player.inventoryMenu.broadcastChanges();
@@ -88,10 +88,10 @@ index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b51
} else if (flag && flag2) {
if (this.dropSpamThrottler.isUnderThreshold()) {
diff --git a/net/minecraft/world/Container.java b/net/minecraft/world/Container.java
index b382665cc125b8b5c0938e5e55984e4bf91d37ff..86cac164a2bf0e76528396e6aabbfd64cfc29559 100644
index 2afaf6a3f18af7889f4f3b35cee9bbb5e4ec01f2..da99b4bc7fe8460945070915073be141f9bd6778 100644
--- a/net/minecraft/world/Container.java
+++ b/net/minecraft/world/Container.java
@@ -32,6 +32,12 @@ public interface Container extends Clearable, Iterable<ItemStack> {
@@ -32,6 +32,12 @@ public interface Container extends Clearable, Iterable<ItemStack>, org.leavesmc.
return Math.min(this.getMaxStackSize(), stack.getMaxStackSize());
}
@@ -127,10 +127,10 @@ index d907e24d563e27acab2f2bf9711b1755ea9afd19..75170c8d3be477a6ea2a1d62018a6ab6
if (min > 0) {
other.grow(min);
diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java
index b745eb5d9c68547335247910ff2ae8d5fb36349c..0c2bf0f8edb34a9011092ea94e54f7bbcee0bbbc 100644
index fd4f1e63f980b6959fc80084651991f8f8bc6c34..e5e8c752129a6ebb0dd1315c9b8ec459be11ef07 100644
--- a/net/minecraft/world/entity/item/ItemEntity.java
+++ b/net/minecraft/world/entity/item/ItemEntity.java
@@ -279,10 +279,15 @@ public class ItemEntity extends Entity implements TraceableEntity {
@@ -283,10 +283,15 @@ public class ItemEntity extends Entity implements TraceableEntity, ChangePublish
private boolean isMergable() {
ItemStack item = this.getItem();
@@ -229,7 +229,7 @@ index 02d2efef2dc0f0e12eac0c71fa290af706f7694d..99f109e2653eff10c011f380694bd77a
default SlotAccess getChestVehicleSlot(final int index) {
diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java
index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35590f2474 100644
index 2d6acceffb2cb7e55dd504018a70b05afccd9c11..bdc0ca1f625dc264bef6e5722ec66121ba48625a 100644
--- a/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -295,6 +295,13 @@ public abstract class AbstractContainerMenu {
@@ -273,7 +273,7 @@ index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35
}
} else if (clickType == ClickType.THROW && this.getCarried().isEmpty() && slotId >= 0) {
Slot slot2 = this.slots.get(slotId);
@@ -656,15 +663,15 @@ public abstract class AbstractContainerMenu {
@@ -660,15 +667,15 @@ public abstract class AbstractContainerMenu {
int maxStackSize = button == 0 ? 1 : -1;
for (int i3 = 0; i3 < 2; i3++) {
@@ -292,7 +292,7 @@ index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35
itemStack.grow(itemStack1.getCount());
}
}
@@ -766,7 +773,7 @@ public abstract class AbstractContainerMenu {
@@ -770,7 +777,7 @@ public abstract class AbstractContainerMenu {
i = endIndex - 1;
}
@@ -301,7 +301,7 @@ index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35
while (!stack.isEmpty() && (reverseDirection ? i >= startIndex : i < endIndex)) {
Slot slot = this.slots.get(i);
ItemStack item = slot.getItem();
@@ -867,7 +874,7 @@ public abstract class AbstractContainerMenu {
@@ -871,7 +878,7 @@ public abstract class AbstractContainerMenu {
public static boolean canItemQuickReplace(@Nullable Slot slot, ItemStack stack, boolean stackSizeMatters) {
boolean flag = slot == null || !slot.hasItem();
return !flag && ItemStack.isSameItemSameComponents(stack, slot.getItem())
@@ -310,7 +310,7 @@ index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35
: flag;
}
@@ -875,7 +882,7 @@ public abstract class AbstractContainerMenu {
@@ -879,7 +886,7 @@ public abstract class AbstractContainerMenu {
return switch (type) {
case 0 -> Mth.floor((float)stack.getCount() / slots.size());
case 1 -> 1;
@@ -319,7 +319,7 @@ index 9b6cd35e4dfa894069238a1365db16050aa5542b..7e9bfb433a252bb4211e490365265d35
default -> stack.getCount();
};
}
@@ -897,7 +904,7 @@ public abstract class AbstractContainerMenu {
@@ -902,7 +909,7 @@ public abstract class AbstractContainerMenu {
for (int i = 0; i < container.getContainerSize(); i++) {
ItemStack item = container.getItem(i);
if (!item.isEmpty()) {
@@ -355,10 +355,10 @@ index 5ceb8964476b40db4511bec91ff13c4f522a1357..f416d7976e41a06b5a8be93485eaa233
@Nullable
diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java
index 6154da1158756cff072d7e8cae10e20eafed1eec..93bbe7b473b8150954f06602a9f311867fe9a550 100644
index 33c3a5e39e8e1aa023baab76ef94f8aed481480e..a27e1d53abc2d8b8912226add33510067e10ed52 100644
--- a/net/minecraft/world/item/ItemStack.java
+++ b/net/minecraft/world/item/ItemStack.java
@@ -162,7 +162,7 @@ public final class ItemStack implements DataComponentHolder {
@@ -166,7 +166,7 @@ public final class ItemStack implements DataComponentHolder, ChangePublisher<net
@Deprecated
@Nullable
private Item item;
@@ -367,7 +367,7 @@ index 6154da1158756cff072d7e8cae10e20eafed1eec..93bbe7b473b8150954f06602a9f31186
@Nullable
private Entity entityRepresentation;
@@ -189,7 +189,8 @@ public final class ItemStack implements DataComponentHolder {
@@ -193,7 +193,8 @@ public final class ItemStack implements DataComponentHolder, ChangePublisher<net
} else {
Holder<Item> holder = Item.STREAM_CODEC.decode(buffer);
DataComponentPatch dataComponentPatch = codec.decode(buffer);
@@ -377,7 +377,7 @@ index 6154da1158756cff072d7e8cae10e20eafed1eec..93bbe7b473b8150954f06602a9f31186
}
}
@@ -198,13 +199,15 @@ public final class ItemStack implements DataComponentHolder {
@@ -202,13 +203,15 @@ public final class ItemStack implements DataComponentHolder, ChangePublisher<net
if (value.isEmpty() || value.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem()
buffer.writeVarInt(0);
} else {
@@ -397,7 +397,7 @@ index 6154da1158756cff072d7e8cae10e20eafed1eec..93bbe7b473b8150954f06602a9f31186
} finally {
net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev);
}
@@ -299,7 +302,7 @@ public final class ItemStack implements DataComponentHolder {
@@ -303,7 +306,7 @@ public final class ItemStack implements DataComponentHolder, ChangePublisher<net
for (ItemStack itemStack : itemContainerContents.nonEmptyItems()) {
int count = itemStack.getCount();
@@ -420,10 +420,10 @@ index 38b03c7b02bdfc579e5e126c12de3d878e26d188..caf31218a1aff84367da126ee1dc46b8
org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
index 36a72a11d28f99bfe85868461925b778cc01478e..2a9f204a514d697178fb3ffac25aece0024f0347 100644
index d7a211220d5eba7e34b9083f06f39e9527962599..cef2d6d9a42ae36d6e2a0b31685a7738d2a400cf 100644
--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java
@@ -409,7 +409,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit
@@ -411,7 +411,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit
ItemStack itemStack = this.items.get(index);
boolean flag = !stack.isEmpty() && ItemStack.isSameItemSameComponents(itemStack, stack);
this.items.set(index, stack);
@@ -433,10 +433,10 @@ index 36a72a11d28f99bfe85868461925b778cc01478e..2a9f204a514d697178fb3ffac25aece0
this.cookingTotalTime = getTotalCookTime(serverLevel, this, this.recipeType, this.cookSpeedMultiplier); // Paper - cook speed multiplier API
this.cookingTimer = 0;
diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
index 5a094257a31f0500278a706a418e1697f8810ffb..63d6a43dab067aa4c8fb67095c455130196eef9f 100644
index 10b2cc20bbf3b70e4e09dbbe14b90506e6366158..1535bb8cec8692e8d26eaff090288f1b8f75c253 100644
--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
@@ -145,7 +145,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co
@@ -155,7 +155,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co
@Override
public void setItem(int slot, ItemStack stack) {
this.getItems().set(slot, stack);
@@ -446,10 +446,10 @@ index 5a094257a31f0500278a706a418e1697f8810ffb..63d6a43dab067aa4c8fb67095c455130
}
diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
index 5de769e7bd56bb3355c3c711d46ce5e103ea4409..8ba900fda7cce45679cde2b8b3272654129135ca 100644
index accbf3256db42b1ce15ea022fcfe1c61e8cb1c7a..8b7d885ae0ec48039dc1573a5227c322454e46ab 100644
--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -123,7 +123,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
@@ -145,7 +145,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
public void setItem(int index, ItemStack stack) {
this.unpackLootTable(null);
this.getItems().set(index, stack);
@@ -458,7 +458,7 @@ index 5de769e7bd56bb3355c3c711d46ce5e103ea4409..8ba900fda7cce45679cde2b8b3272654
}
@Override
@@ -690,9 +690,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
@@ -759,9 +759,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
if (item.isEmpty()) {
// Spigot start - SPIGOT-6693, SimpleContainer#setItem
ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size
@@ -469,4 +469,4 @@ index 5de769e7bd56bb3355c3c711d46ce5e103ea4409..8ba900fda7cce45679cde2b8b3272654
+ stack = stack.split(Math.min(destination.getMaxStackSize(), stack.getMaxStackSize())); // Leaves - item over-stack util
}
// Spigot end
IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading
//IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading // Luminol - Use vanilla hopper logic
@@ -1,21 +1,21 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Mon, 7 Jul 2025 15:33:57 +0800
Subject: [PATCH] Old Explosion Damage Calculator
Subject: [PATCH] Leaves: Old Explosion Damage Calculator
Co-authored by: MC_XiaoHei <xor7xiaohei@gmail.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/3e96b237749a960f297f211d439ffc9ea7fd2381/leaves-server/minecraft-patches/features/0134-Old-wet-tnt-explode-behavior.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java
index b3e897a4afee742b5055d652ad7047a660d8ea04..cf8869fac003c73024634078b2f7b36cddda640e 100644
index b3e897a4afee742b5055d652ad7047a660d8ea04..33471f3d0e2cbd54eaf095c19555f8fdaeb55878 100644
--- a/net/minecraft/world/level/ServerExplosion.java
+++ b/net/minecraft/world/level/ServerExplosion.java
@@ -729,6 +729,7 @@ public class ServerExplosion implements Explosion {
public boolean shouldAffectBlocklikeEntities() {
boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
boolean flag = this.source == null || this.source.getType() != EntityType.BREEZE_WIND_CHARGE && this.source.getType() != EntityType.WIND_CHARGE;
+ if (fun.bm.lophine.config.modules.misc.OldFeatureConfig.oldExplosionDamageCalculator) flag = flag && (this.source == null || !this.source.isInWater()); // Leaves - Old MC TNT wet explosion no item damage
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldExplosionDamageCalculator) flag = flag && (this.source == null || !this.source.isInWater()); // Leaves - Old MC TNT wet explosion no item damage
return _boolean ? flag : this.blockInteraction.shouldAffectBlocklikeEntities() && flag;
}
@@ -0,0 +1,216 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Fri, 11 Apr 2025 16:53:57 +0800
Subject: [PATCH] Leaves: Old raid behavior
Co-authored by: huanli233 <392352840@qq.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/79d9ef74c2684eb49060f07e1ab31c827326c9a5/leaves-server/minecraft-patches/features/0106-Old-raid-behavior.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/effect/BadOmenMobEffect.java b/net/minecraft/world/effect/BadOmenMobEffect.java
index 80f17f33f670018240c854df589cf90cdeab6e70..c9af615ff9120c07787f436dbb6024e54e3a498b 100644
--- a/net/minecraft/world/effect/BadOmenMobEffect.java
+++ b/net/minecraft/world/effect/BadOmenMobEffect.java
@@ -22,6 +22,11 @@ class BadOmenMobEffect extends MobEffect {
&& !serverPlayer.isSpectator()
&& level.getDifficulty() != Difficulty.PEACEFUL
&& level.isVillage(serverPlayer.blockPosition())) {
+ // Leaves start - Revert raid changes
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior) {
+ return level.getRaids().createOrExtendRaid(serverPlayer, serverPlayer.blockPosition()) == null;
+ }
+ // Leaves end - Revert raid changes
Raid raidAt = level.getRaidAt(serverPlayer.blockPosition());
if (raidAt == null || raidAt.getRaidOmenLevel() < raidAt.getMaxRaidOmenLevel()) {
serverPlayer.addEffect(new MobEffectInstance(MobEffects.RAID_OMEN, 600, amplifier));
diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java
index 1910bff3f08147bfd63703097e30b9f88c84e337..f45aa44a7a17946aeb406a447f18de34320a4fa2 100644
--- a/net/minecraft/world/entity/raid/Raid.java
+++ b/net/minecraft/world/entity/raid/Raid.java
@@ -263,7 +263,7 @@ public class Raid {
}
public boolean absorbRaidOmen(ServerPlayer player) {
- MobEffectInstance effect = player.getEffect(MobEffects.RAID_OMEN);
+ MobEffectInstance effect = player.getEffect(fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior ? MobEffects.BAD_OMEN : MobEffects.RAID_OMEN); // Leaves - old Raid Behavior
if (effect == null) {
return false;
} else {
@@ -340,7 +340,13 @@ public class Raid {
}
if (flag1) {
- this.waveSpawnPos = this.getValidSpawnPos(level);
+ // Leaves Start - old FindSpawnPosition
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior) {
+ this.waveSpawnPos = this.preCalcRavagerSpawnLocation(level, this.raidCooldownTicks < 100 ? 1 : 0);
+ } else {
+ this.waveSpawnPos = this.getValidSpawnPos(level);
+ }
+ // Leaves End - old FindSpawnPosition
}
if (this.raidCooldownTicks == 300 || this.raidCooldownTicks % 20 == 0) {
@@ -375,7 +381,14 @@ public class Raid {
int i = 0;
while (this.shouldSpawnGroup()) {
- BlockPos blockPos = this.waveSpawnPos.orElseGet(() -> this.findRandomSpawnPos(level, 20));
+ // Leaves Start - old FindSpawnPosition
+ BlockPos blockPos;
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior) {
+ blockPos = this.getRavagerSpawnLocation(level, i, 20);
+ } else {
+ blockPos = this.waveSpawnPos.orElseGet(() -> this.findRandomSpawnPos(level, 20));
+ }
+ // Leaves End - old FindSpawnPosition
if (blockPos != null) {
this.started = true;
this.spawnGroup(level, blockPos);
@@ -387,7 +400,7 @@ public class Raid {
i++;
}
- if (i > 5) {
+ if (i > (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior ? 3 : 5)) { // Leaves - old FindSpawnPosition
org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(level, this, org.bukkit.event.raid.RaidStopEvent.Reason.UNSPAWNABLE); // CraftBukkit
this.stop();
break;
@@ -683,7 +696,7 @@ public class Raid {
int i2 = this.center.getX() + Mth.floor(Mth.cos(f2) * 32.0F * f) + level.random.nextInt(3) * Mth.floor(f);
int i3 = this.center.getZ() + Mth.floor(Mth.sin(f2) * 32.0F * f) + level.random.nextInt(3) * Mth.floor(f);
int height = level.getHeight(Heightmap.Types.WORLD_SURFACE, i2, i3);
- if (Mth.abs(height - this.center.getY()) <= 96) {
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior || Mth.abs(height - this.center.getY()) <= 96) { // Leaves - skippable
mutableBlockPos.set(i2, height, i3);
if (!level.isVillage(mutableBlockPos) || i <= 7) {
int i4 = 10;
@@ -702,6 +715,36 @@ public class Raid {
return null;
}
+ // Leaves Start - old FindSpawnPosition
+ @Nullable
+ private BlockPos findRandomSpawnPos(ServerLevel level, int n, int n2) {
+ int n3 = 2 - n;
+ BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
+ SpawnPlacementType spawnPlacementType = SpawnPlacements.getPlacementType(EntityType.RAVAGER);
+ for (int i = 0; i < n2; ++i) {
+ float f = level.random.nextFloat() * ((float)Math.PI * 2);
+ int n4 = this.center.getX() + Mth.floor(Mth.cos(f) * 32.0f * (float)n3) + level.random.nextInt(5);
+ int n5 = this.center.getZ() + Mth.floor(Mth.sin(f) * 32.0f * (float)n3) + level.random.nextInt(5);
+ int n6 = level.getHeight(Heightmap.Types.WORLD_SURFACE, n4, n5);
+ mutableBlockPos.set(n4, n6, n5);
+ if (level.isVillage(mutableBlockPos) && n < 2) continue;
+ if (!level.hasChunksAt(mutableBlockPos.getX() - 10, mutableBlockPos.getZ() - 10, mutableBlockPos.getX() + 10, mutableBlockPos.getZ() + 10) || !level.isPositionEntityTicking(mutableBlockPos) || !spawnPlacementType.isSpawnPositionOk(level, mutableBlockPos, EntityType.RAVAGER) && (!level.getBlockState((BlockPos)mutableBlockPos.below()).is(Blocks.SNOW) || !level.getBlockState(mutableBlockPos).isAir())) continue;
+ return mutableBlockPos;
+ }
+ return null;
+ }
+
+ private Optional<BlockPos> getValidSpawnPos(ServerLevel level, int n) {
+ for (int i = 0; i < 3; ++i) {
+ BlockPos blockPos = this.findRandomSpawnPos(level, n, 1);
+ if (blockPos == null) continue;
+ return Optional.of(blockPos);
+ }
+ return Optional.empty();
+ }
+ // Leaves End - old FindSpawnPosition
+
+
private boolean addWaveMob(ServerLevel level, int wave, Raider raider) {
// Folia start - make raids thread-safe
if (!this.ownsRaid(level)) {
@@ -864,4 +907,43 @@ public class Raid {
this.spawnsPerWaveBeforeBonus = spawnsPerWaveBeforeBonus;
}
}
+
+ // Leaves start - old FindSpawnPosition
+ private BlockPos getRavagerSpawnLocation(ServerLevel serverWorld, int proximity, int tries) {
+ int i = proximity == 0 ? 2 : 2 - proximity;
+ BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
+ SpawnPlacementType spawnLocation = SpawnPlacements.getPlacementType(EntityType.RAVAGER);
+
+ for (int j = 0; j < tries; j++) {
+ float f = serverWorld.random.nextFloat() * (float) (Math.PI * 2);
+ int k = this.center.getX() + Mth.floor(Mth.cos(f) * 32.0F * (float) i + serverWorld.random.nextInt(5));
+ int l = this.center.getZ() + Mth.floor(Mth.sin(f) * 32.0F * (float) i + serverWorld.random.nextInt(5));
+ int m = serverWorld.getHeight(Heightmap.Types.WORLD_SURFACE, k, l);
+ mutable.set(k, m, l);
+
+ if (serverWorld.isVillage(mutable) && proximity < 2) {
+ continue;
+ }
+ if (serverWorld.hasChunksAt(mutable.getX() - 10, mutable.getZ() - 10, mutable.getX() + 10, mutable.getZ() + 10)
+ && serverWorld.isPositionEntityTicking(mutable)
+ && (spawnLocation.isSpawnPositionOk(serverWorld, mutable, EntityType.RAVAGER)
+ || serverWorld.getBlockState(mutable.below()).is(Blocks.SNOW)
+ && serverWorld.getBlockState(mutable).isAir())
+ ) {
+ return mutable;
+ }
+ }
+ return null;
+ }
+
+ private Optional<BlockPos> preCalcRavagerSpawnLocation(ServerLevel serverWorld, int proximity) {
+ for (int i = 0; i < 3; i++) {
+ BlockPos blockPos = this.getRavagerSpawnLocation(serverWorld, proximity, 1);
+ if (blockPos != null) {
+ return Optional.of(blockPos);
+ }
+ }
+ return Optional.empty();
+ }
+ // Leaves end - old FindSpawnPosition
}
diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java
index 3e30989c99cd11c8c94d6fd194d2969e3a7efda6..2d163f3d5eb1af4d539d24c0256855bbec960de4 100644
--- a/net/minecraft/world/entity/raid/Raider.java
+++ b/net/minecraft/world/entity/raid/Raider.java
@@ -128,6 +128,43 @@ public abstract class Raider extends PatrollingMonster {
currentRaid.removeFromRaid(serverLevel, this, false);
}
+
+ // Leaves start - Revert raid changes
+ if (this.level() instanceof ServerLevel) {
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.oldRaidBehavior && !this.hasRaid()) {
+ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD);
+ net.minecraft.world.entity.player.Player entityhuman = null;
+ if (entity instanceof net.minecraft.world.entity.player.Player player) {
+ entityhuman = player;
+ } else if (entity instanceof net.minecraft.world.entity.animal.wolf.Wolf wolf) {
+ LivingEntity entityliving = wolf.getOwner();
+ if (wolf.isTame() && entityliving instanceof net.minecraft.world.entity.player.Player player) {
+ entityhuman = player;
+ }
+ }
+
+ if (entityhuman != null && !itemstack.isEmpty() && this.isCaptain()) {
+ net.minecraft.world.effect.MobEffectInstance mobeffect = entityhuman.getEffect(net.minecraft.world.effect.MobEffects.BAD_OMEN);
+ int i = 1;
+
+ if (mobeffect != null) {
+ i += mobeffect.getAmplifier();
+ entityhuman.removeEffectNoUpdate(net.minecraft.world.effect.MobEffects.BAD_OMEN);
+ } else {
+ --i;
+ }
+
+ i = net.minecraft.util.Mth.clamp(i, 0, 4);
+ net.minecraft.world.effect.MobEffectInstance mobeffect1 = new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.BAD_OMEN, 120000, i, false, false, true);
+
+ if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_DISABLE_RAIDS)) {
+ entityhuman.addEffect(mobeffect1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN); // CraftBukkit
+ }
+ this.setPatrolLeader(false);
+ }
+ }
+ }
+ // Leaves end - Revert raid changes
}
super.die(cause);
@@ -1,153 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Wed, 23 Jul 2025 20:28:40 +0800
Subject: [PATCH] Leaves: Redstone Shears Wrench
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/bfde470a867b74d177fe1b9a2a3ed5c49cd126c7/leaves-server/minecraft-patches/features/0009-Redstone-Shears-Wrench.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/item/ShearsItem.java b/net/minecraft/world/item/ShearsItem.java
index 8cf3e51e12f9cf98836657e722edb23943f9e866..155693a18bb641a930a000bc9b1ccfc853e1bae5 100644
--- a/net/minecraft/world/item/ShearsItem.java
+++ b/net/minecraft/world/item/ShearsItem.java
@@ -24,6 +24,30 @@ import net.minecraft.world.level.block.GrowingPlantHeadBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gameevent.GameEvent;
+// Leaves start - shears wrench
+import net.minecraft.Util;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.level.block.ComparatorBlock;
+import net.minecraft.world.level.block.DispenserBlock;
+import net.minecraft.world.level.block.HopperBlock;
+import net.minecraft.world.level.block.ObserverBlock;
+import net.minecraft.world.level.block.RepeaterBlock;
+import net.minecraft.world.level.block.CrafterBlock;
+import net.minecraft.world.level.block.LeverBlock;
+import net.minecraft.world.level.block.CocoaBlock;
+import net.minecraft.world.level.block.TrapDoorBlock;
+import net.minecraft.world.level.block.FenceGateBlock;
+import net.minecraft.world.level.block.LightningRodBlock;
+import net.minecraft.world.level.block.RailBlock;
+import net.minecraft.world.level.block.BaseRailBlock;
+import net.minecraft.world.level.block.PoweredRailBlock;
+import net.minecraft.world.level.block.CalibratedSculkSensorBlock;
+import net.minecraft.world.level.block.piston.PistonBaseBlock;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.world.level.block.state.properties.RailShape;
+import net.minecraft.world.level.block.state.properties.Property;
+// Leaves end - shears wrench
+
public class ShearsItem extends Item {
public ShearsItem(Item.Properties properties) {
super(properties);
@@ -80,7 +104,108 @@ public class ShearsItem extends Item {
return InteractionResult.SUCCESS;
} else {
+ // Leaves start - shears wrench
+ Block block = blockState.getBlock();
+ if (fun.bm.lophine.config.modules.misc.RedStoneConfig.shears && (
+ block instanceof ObserverBlock ||
+ block instanceof DispenserBlock ||
+ block instanceof PistonBaseBlock ||
+ block instanceof HopperBlock ||
+ block instanceof RepeaterBlock ||
+ block instanceof ComparatorBlock ||
+ block instanceof CrafterBlock ||
+ block instanceof LeverBlock ||
+ block instanceof CocoaBlock ||
+ block instanceof TrapDoorBlock ||
+ block instanceof FenceGateBlock ||
+ block instanceof LightningRodBlock ||
+ block instanceof CalibratedSculkSensorBlock ||
+ block instanceof BaseRailBlock
+ )) {
+ StateDefinition<Block, BlockState> blockstatelist = block.getStateDefinition();
+ Property<?> iblockstate;
+ if (block instanceof CrafterBlock) iblockstate = blockstatelist.getProperty("orientation");
+ else if (block instanceof BaseRailBlock) iblockstate = blockstatelist.getProperty("shape");
+ else iblockstate = blockstatelist.getProperty("facing");
+ Player player = context.getPlayer();
+
+ if (iblockstate == null || player == null) {
+ return InteractionResult.FAIL;
+ }
+
+ if (block instanceof BaseRailBlock) {
+ if (block instanceof RailBlock) {
+ if (blockState.getValue(RailBlock.SHAPE).isSlope()) {
+ return InteractionResult.FAIL;
+ }
+ } else {
+ if (getNameHelper(blockState, PoweredRailBlock.POWERED).equals("true")) {
+ return InteractionResult.FAIL;
+ }
+ if (blockState.getValue(PoweredRailBlock.SHAPE).isSlope()) {
+ return InteractionResult.FAIL;
+ }
+ }
+ }
+
+ if (block instanceof PistonBaseBlock) {
+ if (getNameHelper(blockState, PistonBaseBlock.EXTENDED).equals("true")) {
+ return InteractionResult.FAIL;
+ }
+ }
+
+ if (block instanceof RepeaterBlock || block instanceof ComparatorBlock) {
+ if (getNameHelper(blockState, ComparatorBlock.POWERED).equals("true")) {
+ return InteractionResult.FAIL;
+ }
+ if (block instanceof RepeaterBlock) {
+ if (getNameHelper(blockState, RepeaterBlock.LOCKED).equals("true")) {
+ return InteractionResult.FAIL;
+ }
+ }
+ }
+
+ if (block instanceof CrafterBlock) {
+ if (getNameHelper(blockState, CrafterBlock.CRAFTING).equals("true")) {
+ return InteractionResult.FAIL;
+ }
+ }
+
+ BlockState iblockdata1 = ShearsItem.cycleState(blockState, iblockstate, player.isSecondaryUseActive());
+ level.setBlock(clickedPos, iblockdata1, 18);
+ ShearsItem.message(player, Component.translatable("item.minecraft.debug_stick.update", iblockstate.getName(), ShearsItem.getNameHelper(iblockdata1, iblockstate)));
+ return InteractionResult.CONSUME;
+ }
+ // Leaves end - shears wrench
+
return super.useOn(context);
}
}
+
+ // Leaves start - shears wrench
+ private static <T extends Comparable<T>> BlockState cycleState(BlockState state, Property<T> property, boolean inverse) {
+ List<T> possibleValues = property.getPossibleValues();
+ if (possibleValues.getFirst() instanceof RailShape) {
+ boolean isRailBlock = state.getBlock() instanceof RailBlock;
+ possibleValues = possibleValues.stream().filter(possibleValue -> {
+ RailShape shape = (RailShape) possibleValue;
+ if (isRailBlock) return !shape.isSlope();
+ return !shape.isSlope() && shape != RailShape.NORTH_EAST && shape != RailShape.NORTH_WEST && shape != RailShape.SOUTH_EAST && shape != RailShape.SOUTH_WEST;
+ }).toList();
+ }
+ return state.setValue(property, ShearsItem.getRelative(possibleValues, state.getValue(property), inverse)); // CraftBukkit - decompile error
+ }
+
+ private static <T> T getRelative(Iterable<T> elements, T current, boolean inverse) {
+ return inverse ? Util.findPreviousInIterable(elements, current) : Util.findNextInIterable(elements, current);
+ }
+
+ private static void message(Player player, Component message) {
+ ((ServerPlayer) player).sendSystemMessage(message, true);
+ }
+
+ private static <T extends Comparable<T>> String getNameHelper(BlockState state, Property<T> property) {
+ return property.getName(state.getValue(property));
+ }
+ // Leaves end - shears wrench
}
@@ -0,0 +1,44 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Wed, 23 Jul 2025 20:28:40 +0800
Subject: [PATCH] Leaves: Redstone Shears Wrench
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/79d9ef74c2684eb49060f07e1ab31c827326c9a5/leaves-server/minecraft-patches/features/0009-Redstone-Shears-Wrench.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java
index 6db566adf2d0df1d26221eda04aa01738df6d3d2..24ebfdf98e343a7892af621d032c35819156deec 100644
--- a/net/minecraft/world/item/BlockItem.java
+++ b/net/minecraft/world/item/BlockItem.java
@@ -56,6 +56,14 @@ public class BlockItem extends Item {
if (blockPlaceContext == null) {
return InteractionResult.FAIL;
} else {
+ // Leaves start - shears wrench
+ if (org.leavesmc.leaves.util.ShearsWrenchUtil.shouldSkipPlace(context)) {
+ if (context.getPlayer() != null) {
+ context.getPlayer().containerMenu.forceHeldSlot(blockPlaceContext.getHand());
+ }
+ return InteractionResult.FAIL;
+ }
+ // Leaves end - shears wrench
BlockState placementState = this.getPlacementState(blockPlaceContext);
// CraftBukkit start - special case for handling block placement with water lilies and snow buckets
org.bukkit.block.BlockState bukkitState = null;
diff --git a/net/minecraft/world/item/ShearsItem.java b/net/minecraft/world/item/ShearsItem.java
index 8cf3e51e12f9cf98836657e722edb23943f9e866..5fd5e9fd4e1a79dd1a9d62a5cf0c308805979420 100644
--- a/net/minecraft/world/item/ShearsItem.java
+++ b/net/minecraft/world/item/ShearsItem.java
@@ -80,7 +80,10 @@ public class ShearsItem extends Item {
return InteractionResult.SUCCESS;
} else {
- return super.useOn(context);
+ // Leaves start - shears wrench
+ InteractionResult result = org.leavesmc.leaves.util.ShearsWrenchUtil.tryApplyRotate(context, blockState);
+ return result == null ? super.useOn(context) : result;
+ // Leaves end - shears wrench
}
}
}
@@ -1,86 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Mon, 28 Jul 2025 01:06:40 +0800
Subject: [PATCH] Leaves: Vanilla hopper
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/2c9f83c7b99178851edf5608b37b8c2adddd93cd/leaves-server/minecraft-patches/features/0095-Vanilla-hopper.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
index 8ba900fda7cce45679cde2b8b3272654129135ca..9d7ec9f4f18ed263540ccddf16c6c1a92d3477a4 100644
--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -280,36 +280,49 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
ItemStack movedItem = origItemStack;
final int originalItemCount = origItemStack.getCount();
final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount);
- container.setChanged(); // original logic always marks source inv as changed even if no move happens.
- movedItem.setCount(movedItemCount);
- if (!worldData.skipPullModeEventFire) { // Folia - region threading
- movedItem = callPullMoveEvent(hopper, container, movedItem);
- if (movedItem == null) { // cancelled
+ // Leaves start - fix vanilla
+ if (fun.bm.lophine.config.modules.misc.RedStoneConfig.vanillaHopper && movedItem.getCount() <= movedItemCount) {
+ movedItem = origItemStack.copy();
+ final ItemStack remainingItem = addItem(container, hopper, container.removeItem(i, movedItemCount), null);
+ if (remainingItem.isEmpty()) {
+ container.setChanged();
+ return true;
+ }
+ container.setItem(i, movedItem);
+ } else {
+ container.setChanged(); // original logic always marks source inv as changed even if no move happens.
+ movedItem.setCount(movedItemCount);
+
+ if (!worldData.skipPullModeEventFire) {
+ movedItem = callPullMoveEvent(hopper, container, movedItem);
+ if (movedItem == null) { // cancelled
+ origItemStack.setCount(originalItemCount);
+ // Drastically improve performance by returning true.
+ // No plugin could have relied on the behavior of false as the other call
+ // site for IMIE did not exhibit the same behavior
+ return true;
+ }
+ }
+ final ItemStack remainingItem = addItem(container, hopper, movedItem, null);
+ final int remainingItemCount = remainingItem.getCount();
+ if (remainingItemCount != movedItemCount) {
+ origItemStack = origItemStack.copy(true);
origItemStack.setCount(originalItemCount);
- // Drastically improve performance by returning true.
- // No plugin could have relied on the behavior of false as the other call
- // site for IMIE did not exhibit the same behavior
+ if (!origItemStack.isEmpty()) {
+ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
+ }
+
+ IGNORE_TILE_UPDATES.set(true);
+ container.setItem(i, origItemStack);
+ IGNORE_TILE_UPDATES.set(false);
+ container.setChanged();
return true;
}
- }
-
- final ItemStack remainingItem = addItem(container, hopper, movedItem, null);
- final int remainingItemCount = remainingItem.getCount();
- if (remainingItemCount != movedItemCount) {
- origItemStack = origItemStack.copy(true);
origItemStack.setCount(originalItemCount);
- if (!origItemStack.isEmpty()) {
- origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
- }
- IGNORE_TILE_UPDATES.set(true); // Folia - region threading
- container.setItem(i, origItemStack);
- IGNORE_TILE_UPDATES.set(false); // Folia - region threading
- container.setChanged();
- return true;
}
- origItemStack.setCount(originalItemCount);
+ // Leaves end - fix vanilla
if (level.paperConfig().hopper.cooldownWhenFull) {
applyCooldown(hopper);
@@ -0,0 +1,36 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 3 Aug 2025 15:19:23 +0800
Subject: [PATCH] Leaves: Servux Protocol
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/net/minecraft/server/ServerTickRateManager.java b/net/minecraft/server/ServerTickRateManager.java
index d0d90a25a10bbecfffceee1992af88c60d14fd87..187e48e78e69c80b25710f193813335ec643957f 100644
--- a/net/minecraft/server/ServerTickRateManager.java
+++ b/net/minecraft/server/ServerTickRateManager.java
@@ -134,4 +134,10 @@ public class ServerTickRateManager extends TickRateManager {
player.connection.send(ClientboundTickingStatePacket.from(this));
player.connection.send(ClientboundTickingStepPacket.from(this));
}
+
+ // Leaves start - servux
+ public long getRemainingSprintTicks() {
+ return remainingSprintTicks;
+ }
+ // Leaves end - servux
}
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index eb3aef1f5198592eda5dafe84b91271e861b4135..576dde41a4b48aac9079cca902f4371a31470af3 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -2368,6 +2368,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
this.lastSpawnChunkRadius = i;
+ org.leavesmc.leaves.protocol.servux.ServuxHudDataProtocol.refreshSpawnMetadata = true; // Leaves - servux
}
public LongSet getForceLoadedChunks() {
@@ -1,25 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sat, 26 Jul 2025 13:40:28 +0800
Subject: [PATCH] Paper: Fix quick craft
Co-authored by: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
As a part of : Paper (https://github.com/PaperMC/Paper/pull/11831/files#diff-3991edc9191addaab9f9701589f8fb4a05001c6f3c9ce1f51d22001f8de805ca)
Licensed under: MIT (https://github.com/PaperMC/Paper/blob/main/licenses/MIT.md)
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 5369a6597e48ed9f0d8f7f0b9685ce4950df4fe5..919628d93add71d9b30cae29587a0049cce6973d 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -4012,11 +4012,6 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
return null;
} else {
double d = this.getEyeY() - 0.3F;
- // Paper start
- final ItemStack tmp = stack.copy();
- stack.setCount(0);
- stack = tmp;
- // Paper end
ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), d, this.getZ(), stack);
itemEntity.setPickUpDelay(40);
if (includeThrower) {
@@ -8,16 +8,16 @@ As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18
Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE)
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 5c21fc0c749973d17df690fe7e2eab597b198131..f01e3147cb49ee37d8e33d4146aa1a72cf38014f 100644
index 593dbcda4b497291eb1ecdc64b947a8d279f8be6..cb863a36fc67e993956ec3be655e6e4b9764c051 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -1086,6 +1086,10 @@ public abstract class PlayerList {
@@ -1087,6 +1087,10 @@ public abstract class PlayerList {
player.getBukkitEntity().recalculatePermissions(); // CraftBukkit
this.server.getCommands().sendCommands(player);
} // Paper - Add sendOpLevel API
+
+ // Purpur start - Barrels and enderchests 6 rows
+ player.enderChestSlotCount = fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows < 7 && fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows > 0 ? 9 * fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows : 27;
+ player.enderChestSlotCount = fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows < 7 && fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows > 0 ? 9 * fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows : 27;
+ // Purpur end - Barrels and enderchests 6 rows
}
@@ -70,15 +70,15 @@ index 0fffa384f928ab84451331380968fb4650eafe26..d84a10396395939149da88bcb01be59e
return new ChestMenu(MenuType.GENERIC_9x6, containerId, playerInventory, container, 6);
}
diff --git a/net/minecraft/world/inventory/PlayerEnderChestContainer.java b/net/minecraft/world/inventory/PlayerEnderChestContainer.java
index beb74bc8398de8b48f41af7daef14d624826310e..5dd6e89931d397b8afdb84024ed46b0d06dcf347 100644
index 51148b7b95c93a9e25da70ac9275c63f1458d84c..dba8acfcacfe9a7b7e0fb9943fba49468214616e 100644
--- a/net/minecraft/world/inventory/PlayerEnderChestContainer.java
+++ b/net/minecraft/world/inventory/PlayerEnderChestContainer.java
@@ -25,11 +25,18 @@ public class PlayerEnderChestContainer extends SimpleContainer {
@@ -26,11 +26,18 @@ public class PlayerEnderChestContainer extends SimpleContainer {
}
public PlayerEnderChestContainer(Player owner) {
- super(27);
+ super(fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows < 7 && fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows > 0 ? 9 * fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows : 27);
+ super(fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows < 7 && fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows > 0 ? 9 * fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows : 27);
this.owner = owner;
// CraftBukkit end
}
@@ -94,7 +94,7 @@ index beb74bc8398de8b48f41af7daef14d624826310e..5dd6e89931d397b8afdb84024ed46b0d
this.activeChest = enderChestBlockEntity;
}
diff --git a/net/minecraft/world/level/block/EnderChestBlock.java b/net/minecraft/world/level/block/EnderChestBlock.java
index 5077a9ff7b78801bdc53536a37aee07b8d86ee4d..fe374f08b7829b846b2a1c2ac3ca500ca3e48684 100644
index 5077a9ff7b78801bdc53536a37aee07b8d86ee4d..cef29289b7bc7fdf9613c0cd52f2bf4f715f288d 100644
--- a/net/minecraft/world/level/block/EnderChestBlock.java
+++ b/net/minecraft/world/level/block/EnderChestBlock.java
@@ -85,8 +85,14 @@ public class EnderChestBlock extends AbstractChestBlock<EnderChestBlockEntity> i
@@ -103,7 +103,7 @@ index 5077a9ff7b78801bdc53536a37aee07b8d86ee4d..fe374f08b7829b846b2a1c2ac3ca500c
new SimpleMenuProvider(
- (containerId, playerInventory, player1) -> ChestMenu.threeRows(containerId, playerInventory, enderChestInventory), CONTAINER_TITLE
- )
+ (containerId, playerInventory, player1) -> switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows) {
+ (containerId, playerInventory, player1) -> switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows) {
+ case 6 -> ChestMenu.sixRows(containerId, playerInventory, enderChestInventory);
+ case 5 -> ChestMenu.fiveRows(containerId, playerInventory, enderChestInventory);
+ case 4 -> ChestMenu.fourRows(containerId, playerInventory, enderChestInventory);
@@ -115,16 +115,16 @@ index 5077a9ff7b78801bdc53536a37aee07b8d86ee4d..fe374f08b7829b846b2a1c2ac3ca500c
// Paper end - Fix InventoryOpenEvent cancellation - moved up;
player.awardStat(Stats.OPEN_ENDERCHEST);
diff --git a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
index d679ab599dfd0bdbdc3ab5530d7fcd1c38baf7fa..6b4e2956222a0ca7f72f73032dc98878fbd257ee 100644
index de99652629c7301a4bc0e9ec48bed9a26a338f0d..b43445d8313c330a0376aaa4eb872495ebd87b5f 100644
--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
@@ -56,7 +56,17 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
@@ -56,7 +56,17 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity implemen
}
// CraftBukkit end
- private NonNullList<ItemStack> items = NonNullList.withSize(27, ItemStack.EMPTY);
+ // Purpur start - Barrels and enderchests 6 rows
+ private NonNullList<ItemStack> items = NonNullList.withSize(switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.barrelRows) {
+ private NonNullList<ItemStack> items = NonNullList.withSize(switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.barrelRows) {
+ case 6 -> 54;
+ case 5 -> 45;
+ case 4 -> 36;
@@ -137,13 +137,13 @@ index d679ab599dfd0bdbdc3ab5530d7fcd1c38baf7fa..6b4e2956222a0ca7f72f73032dc98878
public final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() {
@Override
protected void onOpen(Level level, BlockPos pos, BlockState state) {
@@ -108,7 +118,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
@@ -108,7 +118,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity implemen
@Override
public int getContainerSize() {
- return 27;
+ // Purpur start - Barrels and enderchests 6 rows
+ return switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.barrelRows) {
+ return switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.barrelRows) {
+ case 6 -> 54;
+ case 5 -> 45;
+ case 4 -> 36;
@@ -155,13 +155,13 @@ index d679ab599dfd0bdbdc3ab5530d7fcd1c38baf7fa..6b4e2956222a0ca7f72f73032dc98878
}
@Override
@@ -128,7 +147,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
@@ -129,7 +148,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity implemen
@Override
protected AbstractContainerMenu createMenu(int id, Inventory player) {
- return ChestMenu.threeRows(id, player, this);
+ // Purpur start - Barrels and enderchests 6 rows
+ return switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.barrelRows) {
+ return switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.barrelRows) {
+ case 6 -> ChestMenu.sixRows(id, player, this);
+ case 5 -> ChestMenu.fiveRows(id, player, this);
+ case 4 -> ChestMenu.fourRows(id, player, this);
@@ -0,0 +1,54 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Fri, 15 Aug 2025 02:29:30 +0800
Subject: [PATCH] Compatibility fix for Raid Revert and Cross Region Damage
Trace
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 3f4b1c54f9207ba1854a8c8da965cbaca5ea517d..a8348a073682eec31990e9ab963b11ad32cf8bf8 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -1216,6 +1216,29 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
}
}
+ // Lophine Start - raid revert adapt Cross Region Damage trace
+ public boolean addEffect(MobEffectInstance effectInstance, EntityPotionEffectEvent.Cause cause, boolean fireEvent, boolean async) {
+ if (!async || ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) {
+ return addEffect(effectInstance, this, cause, fireEvent);
+ } else if (fun.bm.lophine.config.modules.experiment.EntityDamageSourceTraceConfig.enabled) {
+ postToAddEffect(effectInstance, this, cause, fireEvent);
+ return true;
+ }
+ return false;
+ }
+
+ private void postToAddEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) {
+ if (entity != null)
+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> {
+ try {
+ addEffect(effectInstance, nmsEntity, cause, fireEvent);
+ } catch (Throwable ex) {
+ LOGGER.error(ex.getMessage(), ex);
+ }
+ }, null, 1L );
+ }
+ // Lophine End - raid revert adapt Cross Region Damage trace
+
public boolean canBeAffected(MobEffectInstance effectInstance) {
if (this.getType().is(EntityTypeTags.IMMUNE_TO_INFESTED)) {
return !effectInstance.is(MobEffects.INFESTED);
diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java
index 2d163f3d5eb1af4d539d24c0256855bbec960de4..2cc0a7b5f088c29d9343b369b6e71c8a3ebdef75 100644
--- a/net/minecraft/world/entity/raid/Raider.java
+++ b/net/minecraft/world/entity/raid/Raider.java
@@ -158,7 +158,7 @@ public abstract class Raider extends PatrollingMonster {
net.minecraft.world.effect.MobEffectInstance mobeffect1 = new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.BAD_OMEN, 120000, i, false, false, true);
if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_DISABLE_RAIDS)) {
- entityhuman.addEffect(mobeffect1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN); // CraftBukkit
+ entityhuman.addEffect(mobeffect1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN, true, fun.bm.lophine.config.modules.experiment.EntityDamageSourceTraceConfig.enabled); // Lophine - raid revert adapt Cross Region Damage trace
}
this.setPatrolLeader(false);
}
@@ -0,0 +1,55 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 3 Aug 2025 14:26:37 +0800
Subject: [PATCH] Disable some check for operators
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index f06b9d8ad9512d35f3f460eb77c869690321c67f..e17f9629369f8653b484b4cc71de4874b5faa4d6 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -367,7 +367,7 @@ public class ServerGamePacketListenerImpl
this.tickCount++;
this.knownMovePacketCount = this.receivedMovePacketCount;
if (this.clientIsFloating && !this.player.isSleeping() && !this.player.isPassenger() && !this.player.isDeadOrDying()) {
- if (++this.aboveGroundTickCount > this.getMaximumFlyingTicks(this.player)) {
+ if (++this.aboveGroundTickCount > this.getMaximumFlyingTicks(this.player) && !(fun.bm.lophine.config.modules.misc.DisableCheckConfig.disableOpFlyCheck && this.player.hasPermissions(2))) { // Lophine - disable fly check for operator
// LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString()); // Paper - Logging moved to net.minecraft.server.network.ServerCommonPacketListenerImpl.disconnect()
this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingPlayer, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_PLAYER); // Paper - use configurable kick message & kick event cause
return;
@@ -386,7 +386,7 @@ public class ServerGamePacketListenerImpl
this.vehicleLastGoodY = this.lastVehicle.getY();
this.vehicleLastGoodZ = this.lastVehicle.getZ();
if (this.clientVehicleIsFloating && this.lastVehicle.getControllingPassenger() == this.player) {
- if (++this.aboveGroundVehicleTickCount > this.getMaximumFlyingTicks(this.lastVehicle)) {
+ if (++this.aboveGroundVehicleTickCount > this.getMaximumFlyingTicks(this.lastVehicle) && !(fun.bm.lophine.config.modules.misc.DisableCheckConfig.disableOpFlyCheck && this.player.hasPermissions(2))) { // Lophine - disable fly check for operator
// LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString()); // Paper - Logging moved to net.minecraft.server.network.ServerCommonPacketListenerImpl.disconnect()
this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingVehicle, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_VEHICLE); // Paper - use configurable kick message & kick event cause
return;
@@ -644,7 +644,7 @@ public class ServerGamePacketListenerImpl
d5 = d2 - rootVehicle.getZ();
d7 = d3 * d3 + d4 * d4 + d5 * d5;
boolean flag1 = false;
- if (!me.earthme.luminol.config.modules.misc.DisableMovedWronglyThreshold.enabled && d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
+ if (!me.earthme.luminol.config.modules.misc.DisableMovedWronglyThreshold.enabled && d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !(fun.bm.lophine.config.modules.misc.DisableCheckConfig.disableOpMoveCheck && this.player.hasPermissions(2))) { // Spigot // Lophine - disable move check for operator
flag1 = true; // Paper - diff on change, this should be moved wrongly
LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", rootVehicle.getName().getString(), this.player.getName().getString(), Math.sqrt(d7));
}
@@ -1540,7 +1540,7 @@ public class ServerGamePacketListenerImpl
// Paper start - Add fail move event
io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY,
toX, toY, toZ, toYaw, toPitch, true);
- if (!event.isAllowed()) {
+ if (!event.isAllowed() && !(fun.bm.lophine.config.modules.misc.DisableCheckConfig.disableOpMoveCheck && this.player.hasPermissions(2))) { // Lophine - disable move check for operator
if (event.getLogWarning()) {
LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), d3, d4, d5);
}
@@ -1618,7 +1618,7 @@ public class ServerGamePacketListenerImpl
// Paper start - Add fail move event
io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_WRONGLY,
toX, toY, toZ, toYaw, toPitch, true);
- if (!event.isAllowed()) {
+ if (!event.isAllowed() && !(fun.bm.lophine.config.modules.misc.DisableCheckConfig.disableOpMoveCheck && this.player.hasPermissions(2))) { // Lophine - disable move check for operator
movedWrongly = true;
if (event.getLogWarning())
// Paper end
@@ -0,0 +1,44 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Thu, 12 Jun 2025 04:44:24 +0800
Subject: [PATCH] I18n support
diff --git a/net/minecraft/locale/Language.java b/net/minecraft/locale/Language.java
index 7b9e2a1a208b46a69c16e6afd8b502259893574f..db46a31ea8a36562e548eb07f0c4cf9b9e8c3360 100644
--- a/net/minecraft/locale/Language.java
+++ b/net/minecraft/locale/Language.java
@@ -65,7 +65,7 @@ public abstract class Language {
};
}
- private static void parseTranslations(BiConsumer<String, String> output, String languagePath) {
+ public static void parseTranslations(BiConsumer<String, String> output, String languagePath) { // Lophine - private -> public
try (InputStream resourceAsStream = Language.class.getResourceAsStream(languagePath)) {
loadFromJson(resourceAsStream, output);
} catch (JsonParseException | IOException var7) {
diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
index cf72cb8b418239b56c755486b03a321672e16bc3..b69c06607ca900beeebf4892a978442dad2db19b 100644
--- a/net/minecraft/server/Main.java
+++ b/net/minecraft/server/Main.java
@@ -155,6 +155,8 @@ public class Main {
return;
}
+ fun.bm.lophine.utils.ServerI18nUtil.preInit(); // Lophine - I18n support
+
// Paper start - Detect headless JRE
String awtException = io.papermc.paper.util.ServerEnvironment.awtDependencyCheck();
if (awtException != null) {
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index c93feb64b6088a4878fcdb6e7c733a5f37e9385b..79c911003c7b1c3d94e7e9d3974eeadcc16bde3a 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -172,6 +172,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
// Paper end - initialize global and world-defaults configuration
me.earthme.luminol.config.ConfigManager.loadConfigFiles(); // Luminol - load config file
+ fun.bm.lophine.utils.ServerI18nUtil.init(); // Lophine - I18n support
if (false) this.server.spark.enableEarlyIfRequested(); // Paper - spark // Luminol - Force disable builtin spark
// Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save
if (this.convertOldUsers()) {
@@ -1,51 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Wed, 18 Jun 2025 14:24:54 +0800
Subject: [PATCH] Old replaceable by mushrooms
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 93d722d437121e605cee0e168ac17ee9bef0610e..cb2a2fbd17157726b285665fdca0ab9663e0d1de 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -1111,7 +1111,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
if (worldData.captureTreeGeneration) { // Folia - region threading
// Paper start - Protect Bedrock and End Portal/Frames from being destroyed
BlockState type = getBlockState(pos);
- if (!type.isDestroyable()) return false;
+ if (!type.isDestroyable() && !(fun.bm.lophine.config.modules.misc.OldFeatureConfig.oldReplaceableByMushrooms && flags == 3)) return false;
// Paper end - Protect Bedrock and End Portal/Frames from being destroyed
CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading
if (blockstate == null) {
diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java
index 834e27ef2f7b342b074ff9e1e390e02f3ca1c399..f5765e3bbf9e735d6f959e1bf6ee1846ee11b4e9 100644
--- a/net/minecraft/world/level/block/state/BlockBehaviour.java
+++ b/net/minecraft/world/level/block/state/BlockBehaviour.java
@@ -770,6 +770,14 @@ public abstract class BlockBehaviour implements FeatureElement {
return this.solidRender;
}
+ public boolean mushroomCheck(BlockState state) {
+ BlockState blockState = state.asState();
+ if (blockState.canOcclude()) {
+ return !Block.isShapeFullBlock(blockState.getOcclusionShape());
+ }
+ return true;
+ }
+
public final boolean canOcclude() { // Paper - Perf: Final for inlining
return this.canOcclude;
}
diff --git a/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java b/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java
index 3a37d66ab3c27c9abd60f35ef3bf3a93f8d7c3cd..2aad65d76cdd0800ac593a5bb3ce84ad1925eab0 100644
--- a/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java
+++ b/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java
@@ -26,7 +26,7 @@ public abstract class AbstractHugeMushroomFeature extends Feature<HugeMushroomFe
protected void placeMushroomBlock(LevelAccessor level, BlockPos.MutableBlockPos mutablePos, BlockState state) {
BlockState blockState = level.getBlockState(mutablePos);
- if (blockState.isAir() || blockState.is(BlockTags.REPLACEABLE_BY_MUSHROOMS)) {
+ if (blockState.isAir() || blockState.is(BlockTags.REPLACEABLE_BY_MUSHROOMS) || (fun.bm.lophine.config.modules.misc.OldFeatureConfig.oldReplaceableByMushrooms && state.mushroomCheck(blockState))) { // Lophine - old replaceable by mushrooms
this.setBlock(level, mutablePos, state);
}
}
@@ -5,7 +5,7 @@ Subject: [PATCH] Old zombie reinforcement
diff --git a/net/minecraft/world/entity/monster/Zombie.java b/net/minecraft/world/entity/monster/Zombie.java
index 14d0758aca54d9a25e369dfa8c89c930f27946c9..6f1f26b7f759e56f843e674167470da943785728 100644
index 14d0758aca54d9a25e369dfa8c89c930f27946c9..1324f2c8c0d6571bac36855f08354a84e0f08b70 100644
--- a/net/minecraft/world/entity/monster/Zombie.java
+++ b/net/minecraft/world/entity/monster/Zombie.java
@@ -341,7 +341,7 @@ public class Zombie extends Monster {
@@ -13,7 +13,7 @@ index 14d0758aca54d9a25e369dfa8c89c930f27946c9..6f1f26b7f759e56f843e674167470da9
int floor1 = Mth.floor(this.getY());
int floor2 = Mth.floor(this.getZ());
- EntityType<? extends Zombie> type = this.getType();
+ EntityType<? extends Zombie> type = fun.bm.lophine.config.modules.misc.OldFeatureConfig.oldZombieReinforcement ? EntityType.ZOMBIE : this.getType(); // Lophine - old zombie reinforcement
+ EntityType<? extends Zombie> type = fun.bm.lophine.config.modules.function.OldFeatureConfig.oldZombieReinforcement ? EntityType.ZOMBIE : this.getType(); // Lophine - old zombie reinforcement
Zombie zombie = type.create(level, EntitySpawnReason.REINFORCEMENT);
if (zombie == null) {
return true;
@@ -1,18 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Thu, 12 Jun 2025 04:44:24 +0800
Subject: [PATCH] I18n support
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index c6c6432c2efeb7b95c2894b188cd966321ae3186..1cd9af71ee66df5f277748c0a5a59a30bd57accc 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -172,6 +172,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
// Paper end - initialize global and world-defaults configuration
me.earthme.luminol.config.ConfigManager.loadConfigFiles(); // Luminol - load config file
+ fun.bm.lophine.utils.ServerI18nUtil.init(); // Lophine - I18n support
if (false) this.server.spark.enableEarlyIfRequested(); // Paper - spark // Luminol - Force disable builtin spark
// Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save
if (this.convertOldUsers()) {
@@ -5,7 +5,7 @@ Subject: [PATCH] Spawn invulnerable time
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index b7bc75648634aefc549a69631ab6e401eb902e7d..0e2d575200f3ff3ec1600a0663152e3c37ea67b0 100644
index 6b7c92fdf562dfa46eee8e8acbc82b02fc3226c5..c29e9c6cb2513fe4b01a619e16433893efb44e6f 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -232,6 +232,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -24,12 +24,12 @@ index b7bc75648634aefc549a69631ab6e401eb902e7d..0e2d575200f3ff3ec1600a0663152e3c
if (this.invulnerableTime > 0) {
this.invulnerableTime--;
}
@@ -1431,6 +1433,13 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -1435,6 +1437,13 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
if (this.isInvulnerableTo(level, damageSource)) {
return false;
} else {
+ // Lophine start - spawn invulnerable time
+ if (fun.bm.lophine.config.modules.misc.OldFeatureConfig.spawnInvulnerableTime) {
+ if (fun.bm.lophine.config.modules.function.OldFeatureConfig.spawnInvulnerableTime) {
+ if (this.spawnInvulnerableTime > 0 && !damageSource.is(net.minecraft.tags.DamageTypeTags.BYPASSES_INVULNERABILITY)) {
+ return false;
+ }
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,14 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Suisuroru <qwertyuiop14077@qq.com>
Date: Thu, 20 Feb 2025 01:00:29 +0800
Subject: [PATCH] Purpur-Barrels-and-enderchests-6-rows
Subject: [PATCH] Purpur: Barrels and enderchests 6 rows
Co-authored by: William Blake Galbreath <Blake.Galbreath@GMail.com>
As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/purpur-server/paper-patches/features/0003-Barrels-and-enderchests-6-rows.patch)
Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE)
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
index 2f41a92465b9da28e026297cc3528898bb1c8412..fb5300ec1bd19b3dfd6a2b4106440b9f73c48205 100644
index 2f41a92465b9da28e026297cc3528898bb1c8412..168301a2f1c3081ae15d3695c2dcc72b4538cdfb 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
@@ -148,9 +148,26 @@ public class CraftContainer extends AbstractContainerMenu {
@@ -15,7 +18,7 @@ index 2f41a92465b9da28e026297cc3528898bb1c8412..fb5300ec1bd19b3dfd6a2b4106440b9f
- case BARREL:
- this.delegate = new ChestMenu(net.minecraft.world.inventory.MenuType.GENERIC_9x3, windowId, bottom, top, top.getContainerSize() / 9);
+ // Purpur start - Barrels and enderchests 6 rows
+ this.delegate = new ChestMenu(switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.enderchestRows) {
+ this.delegate = new ChestMenu(switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.enderchestRows) {
+ case 6 -> net.minecraft.world.inventory.MenuType.GENERIC_9x6;
+ case 5 -> net.minecraft.world.inventory.MenuType.GENERIC_9x5;
+ case 4 -> net.minecraft.world.inventory.MenuType.GENERIC_9x4;
@@ -25,7 +28,7 @@ index 2f41a92465b9da28e026297cc3528898bb1c8412..fb5300ec1bd19b3dfd6a2b4106440b9f
+ }, windowId, bottom, top, top.getContainerSize() / 9);
break;
+ case BARREL:
+ this.delegate = new ChestMenu(switch (fun.bm.lophine.config.modules.misc.ContainerExpansionConfig.barrelRows) {
+ this.delegate = new ChestMenu(switch (fun.bm.lophine.config.modules.function.ContainerExpansionConfig.barrelRows) {
+ case 6 -> net.minecraft.world.inventory.MenuType.GENERIC_9x6;
+ case 5 -> net.minecraft.world.inventory.MenuType.GENERIC_9x5;
+ case 4 -> net.minecraft.world.inventory.MenuType.GENERIC_9x4;
@@ -0,0 +1,29 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Helvetica Volubi <suisuroru@blue-millennium.fun>
Date: Sun, 3 Aug 2025 15:57:08 +0800
Subject: [PATCH] Leaves: Leaves Protocol Core
Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com>
As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch)
Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html)
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 4f7077b592f84334483f8edf3656fccd5b90a9ca..26d5a96edd283a8f8fd50222e2671cef2c4b257b 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -497,6 +497,7 @@ public final class CraftServer implements Server {
this.potionBrewer = new io.papermc.paper.potion.PaperPotionBrewer(console); // Paper - custom potion mixes
datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper
if (false) this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark // Luminol - Force disable builtin spark
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol
}
public boolean getCommandBlockOverride(String command) {
@@ -1115,6 +1116,7 @@ public final class CraftServer implements Server {
if (false) this.spark.registerCommandBeforePlugins(this); // Paper - spark // Luminol - Force disable builtin spark
this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*");
this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions");
+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleServerReload(); // Leaves - protocol
int pollCount = 0;
@@ -0,0 +1,34 @@
package com.logisticscraft.occlusionculling;
import com.logisticscraft.occlusionculling.util.Vec3d;
public interface DataProvider {
/**
* Prepares the requested chunk. Returns true if the chunk is ready, false when
* not loaded. Should not reload the chunk when the x and y are the same as the
* last request!
*
* @param chunkX
* @param chunkZ
* @return
*/
boolean prepareChunk(int chunkX, int chunkZ);
/**
* Location is inside the chunk.
*
* @param x
* @param y
* @param z
* @return
*/
boolean isOpaqueFullCube(int x, int y, int z);
default void cleanup() {
}
default void checkingPosition(Vec3d[] targetPoints, int size, Vec3d viewerPosition) {
}
}
@@ -0,0 +1,515 @@
package com.logisticscraft.occlusionculling;
import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache;
import com.logisticscraft.occlusionculling.cache.OcclusionCache;
import com.logisticscraft.occlusionculling.util.MathUtilities;
import com.logisticscraft.occlusionculling.util.Vec3d;
import java.util.Arrays;
import java.util.BitSet;
public class OcclusionCullingInstance {
private static final int ON_MIN_X = 0x01;
private static final int ON_MAX_X = 0x02;
private static final int ON_MIN_Y = 0x04;
private static final int ON_MAX_Y = 0x08;
private static final int ON_MIN_Z = 0x10;
private static final int ON_MAX_Z = 0x20;
private final int reach;
private final double aabbExpansion;
private final DataProvider provider;
private final OcclusionCache cache;
// Reused allocated data structures
private final BitSet skipList = new BitSet(); // Grows bigger in case some mod introduces giant hitboxes
private final Vec3d[] targetPoints = new Vec3d[15];
private final Vec3d targetPos = new Vec3d(0, 0, 0);
private final int[] cameraPos = new int[3];
private final boolean[] dotselectors = new boolean[14];
private boolean allowRayChecks = false;
private final int[] lastHitBlock = new int[3];
private boolean allowWallClipping = false;
public OcclusionCullingInstance(int maxDistance, DataProvider provider) {
this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5);
}
public OcclusionCullingInstance(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) {
this.reach = maxDistance;
this.provider = provider;
this.cache = cache;
this.aabbExpansion = aabbExpansion;
for (int i = 0; i < targetPoints.length; i++) {
targetPoints[i] = new Vec3d(0, 0, 0);
}
}
public boolean isAABBVisible(Vec3d aabbMin, Vec3d aabbMax, Vec3d viewerPosition) {
try {
int maxX = MathUtilities.floor(aabbMax.x
+ aabbExpansion);
int maxY = MathUtilities.floor(aabbMax.y
+ aabbExpansion);
int maxZ = MathUtilities.floor(aabbMax.z
+ aabbExpansion);
int minX = MathUtilities.floor(aabbMin.x
- aabbExpansion);
int minY = MathUtilities.floor(aabbMin.y
- aabbExpansion);
int minZ = MathUtilities.floor(aabbMin.z
- aabbExpansion);
cameraPos[0] = MathUtilities.floor(viewerPosition.x);
cameraPos[1] = MathUtilities.floor(viewerPosition.y);
cameraPos[2] = MathUtilities.floor(viewerPosition.z);
Relative relX = Relative.from(minX, maxX, cameraPos[0]);
Relative relY = Relative.from(minY, maxY, cameraPos[1]);
Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]);
if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
return true; // We are inside of the AABB, don't cull
}
skipList.clear();
// Just check the cache first
int id = 0;
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
int cachedValue = getCacheValue(x, y, z);
if (cachedValue == 1) {
// non-occluding
return true;
}
if (cachedValue != 0) {
// was checked and it wasn't visible
skipList.set(id);
}
id++;
}
}
}
// only after the first hit wall the cache becomes valid.
allowRayChecks = false;
// since the cache wasn't helpfull
id = 0;
for (int x = minX; x <= maxX; x++) {
byte visibleOnFaceX = 0;
byte faceEdgeDataX = 0;
faceEdgeDataX |= (x == minX) ? ON_MIN_X : 0;
faceEdgeDataX |= (x == maxX) ? ON_MAX_X : 0;
visibleOnFaceX |= (x == minX && relX == Relative.POSITIVE) ? ON_MIN_X : 0;
visibleOnFaceX |= (x == maxX && relX == Relative.NEGATIVE) ? ON_MAX_X : 0;
for (int y = minY; y <= maxY; y++) {
byte faceEdgeDataY = faceEdgeDataX;
byte visibleOnFaceY = visibleOnFaceX;
faceEdgeDataY |= (y == minY) ? ON_MIN_Y : 0;
faceEdgeDataY |= (y == maxY) ? ON_MAX_Y : 0;
visibleOnFaceY |= (y == minY && relY == Relative.POSITIVE) ? ON_MIN_Y : 0;
visibleOnFaceY |= (y == maxY && relY == Relative.NEGATIVE) ? ON_MAX_Y : 0;
for (int z = minZ; z <= maxZ; z++) {
byte faceEdgeData = faceEdgeDataY;
byte visibleOnFace = visibleOnFaceY;
faceEdgeData |= (z == minZ) ? ON_MIN_Z : 0;
faceEdgeData |= (z == maxZ) ? ON_MAX_Z : 0;
visibleOnFace |= (z == minZ && relZ == Relative.POSITIVE) ? ON_MIN_Z : 0;
visibleOnFace |= (z == maxZ && relZ == Relative.NEGATIVE) ? ON_MAX_Z : 0;
if (skipList.get(id)) { // was checked and it wasn't visible
id++;
continue;
}
if (visibleOnFace != 0) {
targetPos.set(x, y, z);
if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) {
return true;
}
}
id++;
}
}
}
return false;
} catch (Throwable t) {
// Failsafe
t.printStackTrace();
}
return true;
}
/**
* @param viewerPosition
* @param position
* @param faceData contains rather this Block is on the outside for a given face
* @param visibleOnFace contains rather a face should be concidered
* @return
*/
private boolean isVoxelVisible(Vec3d viewerPosition, Vec3d position, byte faceData, byte visibleOnFace) {
int targetSize = 0;
Arrays.fill(dotselectors, false);
if ((visibleOnFace & ON_MIN_X) == ON_MIN_X) {
dotselectors[0] = true;
if ((faceData & ~ON_MIN_X) != 0) {
dotselectors[1] = true;
dotselectors[4] = true;
dotselectors[5] = true;
}
dotselectors[8] = true;
}
if ((visibleOnFace & ON_MIN_Y) == ON_MIN_Y) {
dotselectors[0] = true;
if ((faceData & ~ON_MIN_Y) != 0) {
dotselectors[3] = true;
dotselectors[4] = true;
dotselectors[7] = true;
}
dotselectors[9] = true;
}
if ((visibleOnFace & ON_MIN_Z) == ON_MIN_Z) {
dotselectors[0] = true;
if ((faceData & ~ON_MIN_Z) != 0) {
dotselectors[1] = true;
dotselectors[4] = true;
dotselectors[5] = true;
}
dotselectors[10] = true;
}
if ((visibleOnFace & ON_MAX_X) == ON_MAX_X) {
dotselectors[4] = true;
if ((faceData & ~ON_MAX_X) != 0) {
dotselectors[5] = true;
dotselectors[6] = true;
dotselectors[7] = true;
}
dotselectors[11] = true;
}
if ((visibleOnFace & ON_MAX_Y) == ON_MAX_Y) {
dotselectors[1] = true;
if ((faceData & ~ON_MAX_Y) != 0) {
dotselectors[2] = true;
dotselectors[5] = true;
dotselectors[6] = true;
}
dotselectors[12] = true;
}
if ((visibleOnFace & ON_MAX_Z) == ON_MAX_Z) {
dotselectors[2] = true;
if ((faceData & ~ON_MAX_Z) != 0) {
dotselectors[3] = true;
dotselectors[6] = true;
dotselectors[7] = true;
}
dotselectors[13] = true;
}
if (dotselectors[0]) targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.05);
if (dotselectors[1]) targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.05);
if (dotselectors[2]) targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.95);
if (dotselectors[3]) targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.95);
if (dotselectors[4]) targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.05);
if (dotselectors[5]) targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.05);
if (dotselectors[6]) targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.95);
if (dotselectors[7]) targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.95);
// middle points
if (dotselectors[8]) targetPoints[targetSize++].setAdd(position, 0.05, 0.5, 0.5);
if (dotselectors[9]) targetPoints[targetSize++].setAdd(position, 0.5, 0.05, 0.5);
if (dotselectors[10]) targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.05);
if (dotselectors[11]) targetPoints[targetSize++].setAdd(position, 0.95, 0.5, 0.5);
if (dotselectors[12]) targetPoints[targetSize++].setAdd(position, 0.5, 0.95, 0.5);
if (dotselectors[13]) targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.95);
return isVisible(viewerPosition, targetPoints, targetSize);
}
private boolean rayIntersection(int[] b, Vec3d rayOrigin, Vec3d rayDir) {
Vec3d rInv = new Vec3d(1, 1, 1).div(rayDir);
double t1 = (b[0] - rayOrigin.x) * rInv.x;
double t2 = (b[0] + 1 - rayOrigin.x) * rInv.x;
double t3 = (b[1] - rayOrigin.y) * rInv.y;
double t4 = (b[1] + 1 - rayOrigin.y) * rInv.y;
double t5 = (b[2] - rayOrigin.z) * rInv.z;
double t6 = (b[2] + 1 - rayOrigin.z) * rInv.z;
double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
// if tmax > 0, ray (line) is intersecting AABB, but the whole AABB is behind us
if (tmax > 0) {
return false;
}
// if tmin > tmax, ray doesn't intersect AABB
if (tmin > tmax) {
return false;
}
return true;
}
/**
* returns the grid cells that intersect with this Vec3d<br>
* <a href=
* "http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html">http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html</a>
* <p>
* Caching assumes that all Vec3d's are inside the same block
*/
private boolean isVisible(Vec3d start, Vec3d[] targets, int size) {
// start cell coordinate
int x = cameraPos[0];
int y = cameraPos[1];
int z = cameraPos[2];
for (int v = 0; v < size; v++) {
// ray-casting target
Vec3d target = targets[v];
double relativeX = start.x - target.getX();
double relativeY = start.y - target.getY();
double relativeZ = start.z - target.getZ();
if (allowRayChecks && rayIntersection(lastHitBlock, start, new Vec3d(relativeX, relativeY, relativeZ).normalize())) {
continue;
}
// horizontal and vertical cell amount spanned
double dimensionX = Math.abs(relativeX);
double dimensionY = Math.abs(relativeY);
double dimensionZ = Math.abs(relativeZ);
// distance between horizontal intersection points with cell border as a
// fraction of the total Vec3d length
double dimFracX = 1f / dimensionX;
// distance between vertical intersection points with cell border as a fraction
// of the total Vec3d length
double dimFracY = 1f / dimensionY;
double dimFracZ = 1f / dimensionZ;
// total amount of intersected cells
int intersectCount = 1;
// 1, 0 or -1
// determines the direction of the next cell (horizontally / vertically)
int x_inc, y_inc, z_inc;
// the distance to the next horizontal / vertical intersection point with a cell
// border as a fraction of the total Vec3d length
double t_next_y, t_next_x, t_next_z;
if (dimensionX == 0f) {
x_inc = 0;
t_next_x = dimFracX; // don't increment horizontally because the Vec3d is perfectly vertical
} else if (target.x > start.x) {
x_inc = 1; // target point is horizontally greater than starting point so increment every
// step by 1
intersectCount += MathUtilities.floor(target.x) - x; // increment total amount of intersecting cells
t_next_x = (float) ((x + 1 - start.x) * dimFracX); // calculate the next horizontal
// intersection
// point based on the position inside
// the first cell
} else {
x_inc = -1; // target point is horizontally smaller than starting point so reduce every step
// by 1
intersectCount += x - MathUtilities.floor(target.x); // increment total amount of intersecting cells
t_next_x = (float) ((start.x - x)
* dimFracX); // calculate the next horizontal
// intersection point
// based on the position inside
// the first cell
}
if (dimensionY == 0f) {
y_inc = 0;
t_next_y = dimFracY; // don't increment vertically because the Vec3d is perfectly horizontal
} else if (target.y > start.y) {
y_inc = 1; // target point is vertically greater than starting point so increment every
// step by 1
intersectCount += MathUtilities.floor(target.y) - y; // increment total amount of intersecting cells
t_next_y = (float) ((y + 1 - start.y)
* dimFracY); // calculate the next vertical
// intersection
// point based on the position inside
// the first cell
} else {
y_inc = -1; // target point is vertically smaller than starting point so reduce every step
// by 1
intersectCount += y - MathUtilities.floor(target.y); // increment total amount of intersecting cells
t_next_y = (float) ((start.y - y)
* dimFracY); // calculate the next vertical intersection
// point
// based on the position inside
// the first cell
}
if (dimensionZ == 0f) {
z_inc = 0;
t_next_z = dimFracZ; // don't increment vertically because the Vec3d is perfectly horizontal
} else if (target.z > start.z) {
z_inc = 1; // target point is vertically greater than starting point so increment every
// step by 1
intersectCount += MathUtilities.floor(target.z) - z; // increment total amount of intersecting cells
t_next_z = (float) ((z + 1 - start.z)
* dimFracZ); // calculate the next vertical
// intersection
// point based on the position inside
// the first cell
} else {
z_inc = -1; // target point is vertically smaller than starting point so reduce every step
// by 1
intersectCount += z - MathUtilities.floor(target.z); // increment total amount of intersecting cells
t_next_z = (float) ((start.z - z)
* dimFracZ); // calculate the next vertical intersection
// point
// based on the position inside
// the first cell
}
boolean finished = stepRay(start, x, y, z,
dimFracX, dimFracY, dimFracZ, intersectCount, x_inc, y_inc,
z_inc, t_next_y, t_next_x, t_next_z);
provider.cleanup();
if (finished) {
cacheResult(targets[0], true);
return true;
} else {
allowRayChecks = true;
}
}
cacheResult(targets[0], false);
return false;
}
private boolean stepRay(Vec3d start, int currentX, int currentY,
int currentZ, double distInX, double distInY,
double distInZ, int n, int x_inc, int y_inc,
int z_inc, double t_next_y, double t_next_x,
double t_next_z) {
allowWallClipping = true; // initially allow rays to go through walls till they are on the outside
// iterate through all intersecting cells (n times)
for (; n > 1; n--) { // n-1 times because we don't want to check the last block
// towards - where from
// get cached value, 0 means uncached (default)
int cVal = getCacheValue(currentX, currentY, currentZ);
if (cVal == 2 && !allowWallClipping) {
// block cached as occluding, stop ray
lastHitBlock[0] = currentX;
lastHitBlock[1] = currentY;
lastHitBlock[2] = currentZ;
return false;
}
if (cVal == 0) {
// save current cell
int chunkX = currentX >> 4;
int chunkZ = currentZ >> 4;
if (!provider.prepareChunk(chunkX, chunkZ)) { // Chunk not ready
return false;
}
if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) {
if (!allowWallClipping) {
cache.setLastHidden();
lastHitBlock[0] = currentX;
lastHitBlock[1] = currentY;
lastHitBlock[2] = currentZ;
return false;
}
} else {
// outside of wall, now clipping is not allowed
allowWallClipping = false;
cache.setLastVisible();
}
}
if (cVal == 1) {
// outside of wall, now clipping is not allowed
allowWallClipping = false;
}
if (t_next_y < t_next_x && t_next_y < t_next_z) { // next cell is upwards/downwards because the distance to
// the next vertical
// intersection point is smaller than to the next horizontal intersection point
currentY += y_inc; // move up/down
t_next_y += distInY; // update next vertical intersection point
} else if (t_next_x < t_next_y && t_next_x < t_next_z) { // next cell is right/left
currentX += x_inc; // move right/left
t_next_x += distInX; // update next horizontal intersection point
} else {
currentZ += z_inc; // move right/left
t_next_z += distInZ; // update next horizontal intersection point
}
}
return true;
}
// -1 = invalid location, 0 = not checked yet, 1 = visible, 2 = occluding
private int getCacheValue(int x, int y, int z) {
x -= cameraPos[0];
y -= cameraPos[1];
z -= cameraPos[2];
if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2
|| Math.abs(z) > reach - 2) {
return -1;
}
// check if target is already known
return cache.getState(x + reach, y + reach, z + reach);
}
private void cacheResult(int x, int y, int z, boolean result) {
int cx = x - cameraPos[0] + reach;
int cy = y - cameraPos[1] + reach;
int cz = z - cameraPos[2] + reach;
if (result) {
cache.setVisible(cx, cy, cz);
} else {
cache.setHidden(cx, cy, cz);
}
}
private void cacheResult(Vec3d vector, boolean result) {
int cx = MathUtilities.floor(vector.x) - cameraPos[0] + reach;
int cy = MathUtilities.floor(vector.y) - cameraPos[1] + reach;
int cz = MathUtilities.floor(vector.z) - cameraPos[2] + reach;
if (result) {
cache.setVisible(cx, cy, cz);
} else {
cache.setHidden(cx, cy, cz);
}
}
public void resetCache() {
this.cache.resetCache();
}
private enum Relative {
INSIDE, POSITIVE, NEGATIVE;
public static Relative from(int min, int max, int pos) {
if (max > pos && min > pos) {
return POSITIVE;
} else if (min < pos && max < pos) {
return NEGATIVE;
}
return INSIDE;
}
}
}
@@ -0,0 +1,57 @@
package com.logisticscraft.occlusionculling.cache;
import java.util.Arrays;
public class ArrayOcclusionCache implements OcclusionCache {
private final int reachX2;
private final byte[] cache;
private int positionKey;
private int entry;
private int offset;
public ArrayOcclusionCache(int reach) {
this.reachX2 = reach * 2;
this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4];
}
@Override
public void resetCache() {
Arrays.fill(cache, (byte) 0);
}
@Override
public void setVisible(int x, int y, int z) {
positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
cache[entry] |= 1 << offset;
}
@Override
public void setHidden(int x, int y, int z) {
positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
cache[entry] |= 1 << offset + 1;
}
@Override
public int getState(int x, int y, int z) {
positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
return cache[entry] >> offset & 3;
}
@Override
public void setLastVisible() {
cache[entry] |= 1 << offset;
}
@Override
public void setLastHidden() {
cache[entry] |= 1 << offset + 1;
}
}
@@ -0,0 +1,17 @@
package com.logisticscraft.occlusionculling.cache;
public interface OcclusionCache {
void resetCache();
void setVisible(int x, int y, int z);
void setHidden(int x, int y, int z);
int getState(int x, int y, int z);
void setLastHidden();
void setLastVisible();
}
@@ -0,0 +1,25 @@
package com.logisticscraft.occlusionculling.util;
/**
* Contains MathHelper methods
*/
public final class MathUtilities {
private MathUtilities() {
}
public static int floor(double d) {
int i = (int) d;
return d < (double) i ? i - 1 : i;
}
public static int fastFloor(double d) {
return (int) (d + 1024.0) - 1024;
}
public static int ceil(double d) {
int i = (int) d;
return d > (double) i ? i + 1 : i;
}
}
@@ -0,0 +1,87 @@
package com.logisticscraft.occlusionculling.util;
public class Vec3d {
public double x;
public double y;
public double z;
public Vec3d(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
public void set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public void setAdd(Vec3d vec, double x, double y, double z) {
this.x = vec.x + x;
this.y = vec.y + y;
this.z = vec.z + z;
}
public Vec3d div(Vec3d rayDir) {
this.x /= rayDir.x;
this.z /= rayDir.z;
this.y /= rayDir.y;
return this;
}
public Vec3d normalize() {
double mag = Math.sqrt(x * x + y * y + z * z);
this.x /= mag;
this.y /= mag;
this.z /= mag;
return this;
}
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Vec3d)) {
return false;
}
Vec3d vec3d = (Vec3d) other;
if (Double.compare(vec3d.x, x) != 0) {
return false;
}
if (Double.compare(vec3d.y, y) != 0) {
return false;
}
return Double.compare(vec3d.z, z) == 0;
}
@Override
public int hashCode() {
long l = Double.doubleToLongBits(x);
int i = (int) (l ^ l >>> 32);
l = Double.doubleToLongBits(y);
i = 31 * i + (int) (l ^ l >>> 32);
l = Double.doubleToLongBits(z);
i = 31 * i + (int) (l ^ l >>> 32);
return i;
}
@Override
public String toString() {
return "(" + x + ", " + y + ", " + z + ")";
}
}
@@ -1,9 +1,11 @@
package fun.bm.lophine.config.modules.experiment;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.EXPERIMENT, mainName = "command")
public class CommandConfig implements IConfigModule {
@ConfigInfo(baseName = "tick_command_enabled", comments =
"""
@@ -15,14 +17,4 @@ public class CommandConfig implements IConfigModule {
Allow to use datapack command
--- datapack hot-update unsupported, please restart server""")
public static boolean datapack = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.EXPERIMENT;
}
@Override
public String getBaseName() {
return "command";
}
}
@@ -1,22 +1,14 @@
package fun.bm.lophine.config.modules.experiment;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.EXPERIMENT, mainName = "entity_damage_source_trace")
public class EntityDamageSourceTraceConfig implements IConfigModule {
@ConfigInfo(baseName = "enabled", comments =
"""
Allow trace damage source cross different Region Scheduler.""")
public static boolean enabled = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.EXPERIMENT;
}
@Override
public String getBaseName() {
return "entity_damage_source_trace";
}
}
@@ -1,9 +1,11 @@
package fun.bm.lophine.config.modules.experiment;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.EXPERIMENT, mainName = "ray_tracking_entity_tracker")
public class RayTrackingEntityTrackerConfig implements IConfigModule {
@ConfigInfo(baseName = "enabled")
public static boolean enabled = false;
@@ -15,14 +17,4 @@ public class RayTrackingEntityTrackerConfig implements IConfigModule {
public static int tracingDistance = 48;
@ConfigInfo(baseName = "hitbox_limit")
public static int hitboxLimit = 50;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.EXPERIMENT;
}
@Override
public String getBaseName() {
return "ray_tracking_entity_tracker";
}
}
@@ -1,24 +1,16 @@
package fun.bm.lophine.config.modules.experiment;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.EXPERIMENT, mainName = "waypoint_bar")
public class WaypointConfig implements IConfigModule {
@TransformedConfig(name = "enable-waypoint", category = {"experiment", "waypoint bar"})
@ConfigInfo(baseName = "enabled", comments =
"""
Enable waypoint bar unsafe""")
public static boolean forceEnableWaypointUnsafe = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.EXPERIMENT;
}
@Override
public String getBaseName() {
return "waypoint_bar";
}
}
@@ -0,0 +1,15 @@
package fun.bm.lophine.config.modules.fixes;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FIXES, mainName = "end-void-ring")
public class EndVoidRingsConfig implements IConfigModule {
@TransformedConfig(name = "enabled", category = {"gameplay", "end_void_rings"})
@ConfigInfo(baseName = "enabled", comments = """
If enabled, it will generate end void rings, like MC-159283.""")
public static boolean enabled = false;
}
@@ -0,0 +1,32 @@
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FUNCTION, mainName = "container_expansion")
public class ContainerExpansionConfig implements IConfigModule {
@TransformedConfig(name = "barrel_rows", category = {"misc", "container_expansion"})
@ConfigInfo(baseName = "barrel_rows", comments =
"""
range: 1~6""")
public static int barrelRows = 3;
@TransformedConfig(name = "enderchest_rows", category = {"misc", "container_expansion"})
@ConfigInfo(baseName = "enderchest_rows", comments =
"""
range: 1~6""")
public static int enderchestRows = 3;
@TransformedConfig(name = "shulker_stackable_count", category = {"misc", "container_expansion"})
@ConfigInfo(baseName = "shulker_stackable_count", comments =
"""
range: 1~64""")
public static int shulkerCount = 1;
@TransformedConfig(name = "same_nbt_shulker_stackable", category = {"misc", "container_expansion"})
@ConfigInfo(baseName = "same_nbt_shulker_stackable")
public static boolean nbtShulkerStackable = false;
}
@@ -0,0 +1,16 @@
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FUNCTION, mainName = "language")
public class LanguageConfig implements IConfigModule {
@TransformedConfig(name = "lang", category = {"optimizations", "language"})
@ConfigInfo(baseName = "lang", comments = """
Please use the key from https://minecraft.wiki/w/Language
Sample of format: en_us zh_cn zh_hk zh_tw""")
public static String lang = "en_us";
}
@@ -0,0 +1,27 @@
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FUNCTION, mainName = "old-feature")
public class OldFeatureConfig implements IConfigModule {
@TransformedConfig(name = "spawn_invulnerable_time", category = {"misc", "old-feature"})
@ConfigInfo(baseName = "spawn_invulnerable_time")
public static boolean spawnInvulnerableTime = false;
@TransformedConfig(name = "old_zombie_reinforcement", category = {"misc", "old-feature"})
@ConfigInfo(baseName = "old_zombie_reinforcement")
public static boolean oldZombieReinforcement = false;
@TransformedConfig(name = "old_explosion_damage_calculator", category = {"misc", "old-feature"})
@ConfigInfo(baseName = "old_explosion_damage_calculator")
public static boolean oldExplosionDamageCalculator = false;
@TransformedConfig(name = "old_raid_behavior", category = {"misc", "old-feature"})
@TransformedConfig(name = "give_bad_omen_when_kill_raid_captain", category = {"misc", "revert_raid_changes"}, transformComments = false)
@ConfigInfo(baseName = "old_raid_behavior")
public static boolean oldRaidBehavior = false;
}
@@ -1,27 +1,17 @@
package fun.bm.lophine.config.modules.misc;
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FUNCTION, mainName = "redstone")
public class RedStoneConfig implements IConfigModule {
@TransformedConfig(name = "shears_rotate", category = {"misc", "redstone"})
@TransformedConfig(name = "allow_skip_cooldown", category = {"misc", "redstone"})
@ConfigInfo(baseName = "shears_rotate", comments =
"""
Allows you to use the Shears to right-click to rotate the block.""")
public static boolean shears = false;
@ConfigInfo(baseName = "vanilla_hopper")
public static boolean vanillaHopper = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "redstone";
}
}
@@ -0,0 +1,48 @@
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
import java.util.List;
@ConfigClassInfo(configAttribution = EnumConfigCategory.FUNCTION, mainName = "survux-protocol")
public class SurvuxProtocolConfig implements IConfigModule {
@TransformedConfig(name = "entity-protocol", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "entity-protocol")
public static boolean entityProtocol = false;
@TransformedConfig(name = "hud-logger-protocol", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "hud-logger-protocol")
public static boolean hudLoggerProtocol = false;
@TransformedConfig(name = "hud-metadata-protocol", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "hud-metadata-protocol")
public static boolean hudMetadataProtocol = false;
@TransformedConfig(name = "hud-metadata-share-seed", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "hud-metadata-share-seed")
public static boolean hudMetadataShareSeed = false;
@TransformedConfig(name ="structure-protocol", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "structure-protocol")
public static boolean structureProtocol = false;
@TransformedConfig(name = "hud-enabled-loggers", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "hud-enabled-loggers")
public static List<String> hudEnabledLoggers = List.of("tps", "mob_caps");
@TransformedConfig(name = "hud-update-interval", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "hud-update-interval")
public static int hudUpdateInterval = 1;
@TransformedConfig(name = "litematics-enabled", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "litematics-enabled")
public static boolean litematicsEnabled = false;
@TransformedConfig(name = "litematics-max-nbt-size", category = {"misc", "survux-protocol"})
@ConfigInfo(baseName = "litematics-max-nbt-size")
public static int litematicsMaxNbtSize = 2097152;
}
@@ -0,0 +1,18 @@
package fun.bm.lophine.config.modules.function;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.MISC, mainName = "villager")
public class VillagerConfig implements IConfigModule {
@TransformedConfig(name = "villager-infinite-trade", category = {"misc", "villager"})
@TransformedConfig(name = "villager-infinite-trade", category = {"misc", "villager-config"})
@ConfigInfo(baseName = "villager-infinite-trade", comments =
"""
Allow villager infinite trade (limit of 524288 times)
---- we won't edit saved data, only edit in send data to client.""")
public static boolean villagerInfiniteTrade = false;
}
@@ -1,37 +0,0 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class ContainerExpansionConfig implements IConfigModule {
@ConfigInfo(baseName = "barrel_rows", comments =
"""
range: 1~6""")
public static int barrelRows = 3;
@ConfigInfo(baseName = "enderchest_rows", comments =
"""
range: 1~6""")
public static int enderchestRows = 3;
@ConfigInfo(baseName = "shulker_stackable_count", comments =
"""
range: 1~64""")
public static int shulkerCount = 1;
@ConfigInfo(baseName = "same_nbt_shulker_stackable", comments =
"""
range: 1~64""")
public static boolean nbtShulkerStackable = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "container_expansion";
}
}
@@ -0,0 +1,17 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.MISC, mainName = "disable-check")
public class DisableCheckConfig implements IConfigModule {
@ConfigInfo(baseName = "disable-op-move-check", comments = """
Disable the check for the operator's move check""")
public static boolean disableOpMoveCheck = false;
@ConfigInfo(baseName = "disable-op-fly-check", comments = """
Disable the check for the operator's fly check""")
public static boolean disableOpFlyCheck = false;
}
@@ -1,23 +0,0 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class DisableEndCrystalCheckConfig implements IConfigModule {
@ConfigInfo(baseName = "disable_end_crystal_check", comments =
"""
Disable paper's End Crystal position check.
It reverts to vanilla respawn dragon logic.""")
public static boolean disableCheck = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "end_crystal";
}
}
@@ -1,29 +0,0 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class OldFeatureConfig implements IConfigModule {
@ConfigInfo(baseName = "spawn_invulnerable_time")
public static boolean spawnInvulnerableTime = false;
@ConfigInfo(baseName = "old_zombie_reinforcement")
public static boolean oldZombieReinforcement = false;
@ConfigInfo(baseName = "old_replaceable_by_mushrooms")
public static boolean oldReplaceableByMushrooms = false;
@ConfigInfo(baseName = "old_explosion_damage_calculator")
public static boolean oldExplosionDamageCalculator = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "old-feature";
}
}
@@ -1,54 +0,0 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class RaidChangesConfig implements IConfigModule {
@ConfigInfo(baseName = "allow_skip_cooldown", comments =
"""
Allow players with bad omen to skip a
30-second cooldown and trigger attacks directly""")
public static boolean trigger = false;
@ConfigInfo(baseName = "give_bad_omen_when_kill_raid_captain", comments =
"""
Enable players to obtain a bad omen
effect when killing the raid captain""")
public static boolean effect = false;
@ConfigInfo(baseName = "bad_omen_infinite", comments =
"""
Enable bad omen effect infinite time
--- this config is not old version's function""")
public static boolean infinite = false;
@ConfigInfo(baseName = "skip_height_check", comments =
"""
Disable y <= 96 check.
If you enabled use_old_position_find, this config
will useless and always behavior of enabled""")
public static boolean heightCheck = false;
@ConfigInfo(baseName = "skip_self_raid_check", comments =
"""
Disable raid self check
--- this config is not old version's function""")
public static boolean selfCheck = false;
@ConfigInfo(baseName = "use_old_position_find", comments =
"""
Revert Old raid's find spawn position logic
--- This revert MC-274911""")
public static boolean posRevert = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "revert_raid_changes";
}
}
@@ -1,23 +0,0 @@
package fun.bm.lophine.config.modules.misc;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class VillagerConfig implements IConfigModule {
@ConfigInfo(baseName = "villager-infinite-trade", comments =
"""
Allow villager infinite trade (limit of 16384 times)
---- we won't edit saved data, only edit in send data to client.""")
public static boolean villagerInfiniteTrade = false;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.MISC;
}
@Override
public String getBaseName() {
return "villager-config";
}
}
@@ -1,22 +0,0 @@
package fun.bm.lophine.config.modules.optimizations;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class LanguageConfig implements IConfigModule {
@ConfigInfo(baseName = "lang", comments = """
Please use the key from https://minecraft.wiki/w/Language
Sample of format: en_us zh_cn zh_hk zh_tw""")
public static String lang = "en_us";
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.OPTIMIZATIONS;
}
@Override
public String getBaseName() {
return "language";
}
}
@@ -1,21 +0,0 @@
package fun.bm.lophine.config.modules.optimizations;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigInfo;
public class NetworkConfig implements IConfigModule {
@ConfigInfo(baseName = "unlimit-packet", comments = """
Disable packet limit""")
public static boolean disablePacketLimit = true;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.OPTIMIZATIONS;
}
@Override
public String getBaseName() {
return "network";
}
}
@@ -1,25 +1,26 @@
package fun.bm.lophine.config.modules.removed;
import me.earthme.luminol.config.EnumConfigCategory;
import me.earthme.luminol.config.IConfigModule;
import me.earthme.luminol.config.flags.ConfigClassInfo;
import me.earthme.luminol.config.flags.ConfigInfo;
import me.earthme.luminol.config.flags.TransformedConfig;
import me.earthme.luminol.enums.EnumConfigCategory;
@ConfigClassInfo(configAttribution = EnumConfigCategory.REMOVED, mainName = "removed_config")
public class RemovedConfig implements IConfigModule {
@TransformedConfig(name = "disable_end_crystal_check", category = {"fixes", "end_crystal"}, transform = false)
@TransformedConfig(name = "disable_end_crystal_check", category = {"misc", "end_crystal"}, transform = false)
@TransformedConfig(name = "allow_skip_cooldown", category = {"misc", "revert_raid_changes"}, transform = false)
@TransformedConfig(name = "bad_omen_infinite", category = {"misc", "revert_raid_changes"}, transform = false)
@TransformedConfig(name = "skip_height_check", category = {"misc", "revert_raid_changes"}, transform = false)
@TransformedConfig(name = "skip_self_raid_check", category = {"misc", "revert_raid_changes"}, transform = false)
@TransformedConfig(name = "use_old_position_find", category = {"misc", "revert_raid_changes"}, transform = false)
@TransformedConfig(name = "vanilla_hopper", category = {"misc", "redstone"}, transform = false)
@TransformedConfig(name = "old_replaceable_by_mushrooms", category = {"misc", "old-feature"}, transform = false)
@TransformedConfig(name = "old_nether_portal_collision", category = {"misc", "old-feature"}, transform = false)
@TransformedConfig(name = "better_shulker_box", category = {"misc", "container_expansion"}, transform = false)
@ConfigInfo(baseName = "removed", comments =
"""
RemovedConfig redirect to here, no any function.""")
public static boolean enabled = true;
@Override
public EnumConfigCategory getCategory() {
return EnumConfigCategory.REMOVED;
}
@Override
public String getBaseName() {
return "removed_config";
}
}
@@ -4,7 +4,10 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import fun.bm.lophine.config.modules.optimizations.LanguageConfig;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.config.modules.function.LanguageConfig;
import me.earthme.luminol.config.ConfigManager;
import me.earthme.luminol.config.ConfigsInstance;
import net.minecraft.DetectedVersion;
import net.minecraft.locale.DeprecatedTranslationsInfo;
import net.minecraft.locale.Language;
@@ -12,8 +15,8 @@ import net.minecraft.network.chat.FormattedText;
import net.minecraft.network.chat.Style;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.util.StringDecomposer;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.InputStream;
@@ -23,13 +26,13 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.logging.Logger;
/*
* authored by: Helvetica Volubi <suisuroru@blue-millennium.fun>
@@ -37,30 +40,62 @@ import java.util.logging.Logger;
* Some of diff form Leaves
*/
public class ServerI18nUtil {
private static final Logger logger = Logger.getLogger("LangLoader");
private static final Logger logger = LogUtils.getClassLogger();
private static final String VERSION = DetectedVersion.BUILT_IN.name();
private static final String BASE_PATH = "cache/lophine/" + VERSION + "/";
private static final String defaultLophineLangPath = "/assets/lophine/lang/en_us.json";
private static final String manifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
private static final String resourceBaseUrl = "https://resources.download.minecraft.net/";
// pre-load
private static CompletableFuture<Void> preloadTask;
// paths
private static String langPath;
private static String assetsPath;
private static String versionPath;
private static String manifestPath;
private static String langJsonPath;
private static String lophineLangPath;
public static void init() {
if (Objects.equals(LanguageConfig.lang, "en_us")) {
return;
}
langPath = BASE_PATH + "lang/" + LanguageConfig.lang + ".json";
langJsonPath = "minecraft/lang/" + LanguageConfig.lang + ".json";
lophineLangPath = "/assets/Lophine/lang/" + LanguageConfig.lang + ".json";
logger.info("Starting load language: {}", LanguageConfig.lang);
preloadTask.thenAcceptAsync(v -> loadI18n(LanguageConfig.lang, 2));
}
public static void preInit() {
assetsPath = BASE_PATH + "assets.json";
versionPath = BASE_PATH + VERSION + ".json";
manifestPath = BASE_PATH + "manifest.json";
langJsonPath = "minecraft/lang/" + LanguageConfig.lang + ".json";
logger.info("Starting load language: " + LanguageConfig.lang);
CompletableFuture.runAsync(() -> loadI18n(LanguageConfig.lang, 2));
preloadTask = CompletableFuture.runAsync(() -> preLoadI18n(2));
}
private static void preLoadI18n(int retryTime) {
try {
if (!Files.exists(Path.of(assetsPath))) {
downloadAssets(true);
}
} catch (UnsupportedLanguageException e) {
logger.warn("Unsupported language: {}", LanguageConfig.lang);
// Fallback to English
final ConfigsInstance configsInstance = ConfigManager.configfiles.get("lophine");
configsInstance.setConfig(new String[]{"optimizations", "lang"}, "en_us");
configsInstance.reloadAsync();
} catch (Exception e) {
if (e instanceof MalformedJsonException malformedJson) {
malformedJson.clean();
}
logger.warn("Failed to download language list file: ", e);
if (retryTime > 0) {
preLoadI18n(retryTime - 1);
} else {
logger.error("Failed to download language list file for many times, skip pre-load language.");
}
}
}
private static void loadI18n(String lang, int retryTime) {
@@ -69,15 +104,16 @@ public class ServerI18nUtil {
downloadLang(true);
}
Language.inject(createLangInstance());
logger.info("Successfully loaded language: " + lang);
logger.info("Successfully loaded language: {}", lang);
} catch (Exception e) {
logger.warning("Failed to load language file for " + lang + "\n" + e);
if (e instanceof MalformedJsonException malformedJson) {
malformedJson.clean();
}
logger.warn("Failed to load language file for {}", lang, e);
if (retryTime > 0) {
cleanCache();
loadI18n(lang, retryTime - 1);
} else {
logger.severe("Failed to load for many times, use default lang \"en_us\" instead");
cleanCache();
logger.error("Failed to load for many times, use default lang \"en_us\" instead");
}
}
}
@@ -94,6 +130,10 @@ public class ServerI18nUtil {
JsonObject langEntry = json.getAsJsonObject("objects").getAsJsonObject(langJsonPath);
if (langEntry == null) {
throw new UnsupportedLanguageException();
}
String hash = langEntry.get("hash").getAsString();
if (hash == null || hash.length() < 2) {
throw new IllegalArgumentException("Invalid hash value");
@@ -145,37 +185,31 @@ public class ServerI18nUtil {
fetchAndSave(versionUrl, versionPath);
}
private static String createHttpResponse(String path) throws IOException, InterruptedException {
private static byte[] fetch(String urlString) throws IOException, InterruptedException {
try {
HttpResponse<String> response;
try (HttpClient httpClient = HttpClient.newHttpClient()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(path))
.uri(URI.create(urlString))
.timeout(Duration.ofSeconds(10))
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
int responseCode = response.statusCode();
if (responseCode != 200) {
logger.info("Unexpected response code: " + responseCode);
logger.info("Response body: " + response.body());
logger.info("Unexpected response code: {}", responseCode);
logger.info("Response body: {}", response.body());
throw new UnsupportedEncodingException("Unexpected response code");
} else {
return response.body();
return response.body().getBytes();
}
} catch (Exception e) {
logger.warning("Error in getting info: " + e.getMessage());
logger.warn("Error in getting info!");
throw e;
}
}
private static byte[] fetch(String urlString) throws IOException, InterruptedException {
String ret = createHttpResponse(urlString);
return ret.getBytes();
}
private static void fetchAndSave(String url, String savePath) throws IOException, InterruptedException {
byte[] data = fetch(url);
Path outputPath = Path.of(savePath);
@@ -183,23 +217,15 @@ public class ServerI18nUtil {
Files.write(outputPath, data, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
private static void cleanCache() {
try {
FileUtils.deleteDirectory(Path.of(BASE_PATH).toFile());
} catch (IOException e) {
logger.severe("Cache cleanup failed: " + e);
}
}
private static JsonObject loadJson(String path) {
try {
byte[] data = Files.readAllBytes(Paths.get(path));
return JsonParser.parseString(new String(data)).getAsJsonObject();
} catch (JsonSyntaxException e) {
logger.warning("Corrupt json file: " + e);
throw e;
logger.warn("Corrupt json file!");
throw new MalformedJsonException(e, path);
} catch (Exception e) {
logger.warning("Failed to load local JSON: " + e);
logger.warn("Failed to load local JSON!");
return null;
}
}
@@ -208,6 +234,7 @@ public class ServerI18nUtil {
DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource();
Map<String, String> map = new HashMap<>();
parseTranslations(map::put);
loadLophineI18n(map::put);
deprecatedTranslationsInfo.applyToMap(map);
final Map<String, String> map1 = Map.copyOf(map);
return new Language() {
@@ -237,16 +264,52 @@ public class ServerI18nUtil {
};
}
private static void loadLophineI18n(BiConsumer<String, String> bi) {
if (Language.class.getResource(lophineLangPath) != null) {
Language.parseTranslations(bi, lophineLangPath);
} else {
loadLophineI18nDefault(bi);
}
}
public static void loadLophineI18nDefault(BiConsumer<String, String> bi) {
if (Language.class.getResource(defaultLophineLangPath) != null) {
Language.parseTranslations(bi, defaultLophineLangPath);
}
}
private static void parseTranslations(BiConsumer<String, String> output) throws IOException {
Path filePath = Path.of(langPath);
try (InputStream fileStream = Files.newInputStream(filePath)) {
Language.loadFromJson(fileStream, output);
} catch (NoSuchFileException noSuchFileException) {
logger.warning("Couldn't find language file: " + langPath);
throw noSuchFileException;
} catch (NoSuchFileException e) {
logger.warn("Couldn't find language file: {}", langPath);
throw e;
} catch (JsonSyntaxException e) {
throw new MalformedJsonException(e, langPath);
} catch (Exception e) {
logger.warning("Failed to load language from filesystem " + filePath + "\n" + e);
logger.warn("Failed to load language from filesystem {}", filePath);
throw e;
}
}
private static class UnsupportedLanguageException extends Exception {
}
private static class MalformedJsonException extends RuntimeException {
private final Path path;
protected MalformedJsonException(Throwable root, String path) {
super(root);
this.path = Path.of(path);
}
public void clean() {
try {
Files.delete(path);
} catch (IOException e) {
logger.info("Failed to delete malformed JSON file: {}", path);
}
}
}
}
@@ -0,0 +1,26 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import com.mojang.authlib.GameProfile;
import net.minecraft.network.Connection;
import org.jetbrains.annotations.NotNull;
public record Context(@NotNull GameProfile profile, Connection connection) {
}
@@ -0,0 +1,28 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.Nullable;
public record IdentifierSelector(@Nullable Context context, @Nullable ServerPlayer player) {
public Object select(ProtocolHandler.Stage stage) {
return stage == ProtocolHandler.Stage.CONFIGURATION ? context : player;
}
}
@@ -0,0 +1,48 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public interface LeavesCustomPayload extends CustomPacketPayload {
Type<? extends CustomPacketPayload> LEAVES_TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath("leaves", "custom_payload"));
@Override
default @NotNull Type<? extends CustomPacketPayload> type() {
return LEAVES_TYPE;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ID {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Codec {
}
}
@@ -0,0 +1,39 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public interface LeavesProtocol {
boolean isActive();
default int tickerInterval(String tickerID) {
return 1;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Register {
String namespace();
}
}
@@ -0,0 +1,401 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import com.mojang.logging.LogUtils;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.leavesmc.leaves.protocol.core.invoker.*;
import org.slf4j.Logger;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class LeavesProtocolManager {
private static final Logger LOGGER = LogUtils.getClassLogger();
private static final Map<Class<? extends LeavesCustomPayload>, PayloadReceiverInvokerHolder> PAYLOAD_RECEIVERS = new HashMap<>();
private static final Map<Class<? extends LeavesCustomPayload>, ResourceLocation> IDS = new HashMap<>();
private static final Map<Class<? extends LeavesCustomPayload>, StreamCodec<? super RegistryFriendlyByteBuf, LeavesCustomPayload>> CODECS = new HashMap<>();
private static final Map<ResourceLocation, StreamCodec<? super RegistryFriendlyByteBuf, LeavesCustomPayload>> ID2CODEC = new HashMap<>();
private static final Map<String, BytebufReceiverInvokerHolder> STRICT_BYTEBUF_RECEIVERS = new HashMap<>();
private static final Map<String, BytebufReceiverInvokerHolder> NAMESPACED_BYTEBUF_RECEIVERS = new HashMap<>();
private static final List<BytebufReceiverInvokerHolder> GENERIC_BYTEBUF_RECEIVERS = new ArrayList<>();
private static final Map<String, MinecraftRegisterInvokerHolder> STRICT_MINECRAFT_REGISTER = new HashMap<>();
private static final Map<String, MinecraftRegisterInvokerHolder> NAMESPACED_MINECRAFT_REGISTER = new HashMap<>();
private static final List<MinecraftRegisterInvokerHolder> WILD_MINECRAFT_REGISTER = new ArrayList<>();
private static final List<EmptyInvokerHolder<ProtocolHandler.Ticker>> TICKERS = new ArrayList<>();
private static final List<PlayerInvokerHolder<ProtocolHandler.PlayerJoin>> PLAYER_JOIN = new ArrayList<>();
private static final List<PlayerInvokerHolder<ProtocolHandler.PlayerLeave>> PLAYER_LEAVE = new ArrayList<>();
private static final List<EmptyInvokerHolder<ProtocolHandler.ReloadServer>> RELOAD_SERVER = new ArrayList<>();
private static final List<EmptyInvokerHolder<ProtocolHandler.ReloadDataPack>> RELOAD_DATAPACK = new ArrayList<>();
@SuppressWarnings("unchecked")
public static void init() {
for (Class<?> clazz : getClasses("org.leavesmc.leaves.protocol")) {
if (LeavesCustomPayload.class.isAssignableFrom(clazz) && !clazz.equals(LeavesCustomPayload.class)) {
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (!Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
final LeavesCustomPayload.ID id = field.getAnnotation(LeavesCustomPayload.ID.class);
if (id != null && field.getType().equals(ResourceLocation.class)) {
IDS.put((Class<? extends LeavesCustomPayload>) clazz, (ResourceLocation) field.get(null));
}
final LeavesCustomPayload.Codec codec = field.getAnnotation(LeavesCustomPayload.Codec.class);
if (codec != null && field.getType().equals(StreamCodec.class)) {
CODECS.put((Class<? extends LeavesCustomPayload>) clazz, (StreamCodec<? super RegistryFriendlyByteBuf, LeavesCustomPayload>) field.get(null));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
continue;
}
final LeavesProtocol.Register register = clazz.getAnnotation(LeavesProtocol.Register.class);
if (register == null) {
continue;
}
LeavesProtocol protocol;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
protocol = (LeavesProtocol) constructor.newInstance();
} catch (Throwable throwable) {
LOGGER.error("Failed to load class {}. {}", clazz.getName(), throwable);
return;
}
for (final Method method : clazz.getDeclaredMethods()) {
if (method.isBridge() || method.isSynthetic()) {
continue;
}
method.setAccessible(true);
final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class);
if (init != null) {
InitInvokerHolder holder = new InitInvokerHolder(protocol, method, init);
try {
holder.invoke();
} catch (RuntimeException exception) {
LOGGER.error("Failed to invoke init method {} in {}, {}: {}", method.getName(), clazz.getName(), exception.getCause(), exception.getMessage());
}
continue;
}
final ProtocolHandler.PayloadReceiver payloadReceiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class);
if (payloadReceiver != null) {
PAYLOAD_RECEIVERS.put(payloadReceiver.payload(), new PayloadReceiverInvokerHolder(protocol, method, payloadReceiver));
continue;
}
final ProtocolHandler.BytebufReceiver bytebufReceiver = method.getAnnotation(ProtocolHandler.BytebufReceiver.class);
if (bytebufReceiver != null) {
String key = bytebufReceiver.key();
BytebufReceiverInvokerHolder holder = new BytebufReceiverInvokerHolder(protocol, method, bytebufReceiver);
if (bytebufReceiver.onlyNamespace()) {
NAMESPACED_BYTEBUF_RECEIVERS.put(key.isEmpty() ? register.namespace() : key, holder);
} else {
if (key.isEmpty()) {
GENERIC_BYTEBUF_RECEIVERS.add(holder);
} else {
if (key.contains(":")) {
STRICT_BYTEBUF_RECEIVERS.put(key, holder);
} else {
STRICT_BYTEBUF_RECEIVERS.put(register.namespace() + ":" + key, holder);
}
}
}
continue;
}
final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class);
if (ticker != null) {
TICKERS.add(new EmptyInvokerHolder<>(protocol, method, ticker));
continue;
}
final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class);
if (playerJoin != null) {
PLAYER_JOIN.add(new PlayerInvokerHolder<>(protocol, method, playerJoin));
continue;
}
final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class);
if (playerLeave != null) {
PLAYER_LEAVE.add(new PlayerInvokerHolder<>(protocol, method, playerLeave));
continue;
}
final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class);
if (reloadServer != null) {
RELOAD_SERVER.add(new EmptyInvokerHolder<>(protocol, method, reloadServer));
continue;
}
final ProtocolHandler.ReloadDataPack reloadDataPack = method.getAnnotation(ProtocolHandler.ReloadDataPack.class);
if (reloadDataPack != null) {
RELOAD_DATAPACK.add(new EmptyInvokerHolder<>(protocol, method, reloadDataPack));
continue;
}
final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class);
if (minecraftRegister != null) {
String key = minecraftRegister.key();
MinecraftRegisterInvokerHolder holder = new MinecraftRegisterInvokerHolder(protocol, method, minecraftRegister);
if (minecraftRegister.onlyNamespace()) {
NAMESPACED_MINECRAFT_REGISTER.put(key.isEmpty() ? register.namespace() : key, holder);
} else {
if (key.isEmpty()) {
WILD_MINECRAFT_REGISTER.add(holder);
} else {
if (key.contains(":")) {
STRICT_MINECRAFT_REGISTER.put(key, holder);
} else {
STRICT_MINECRAFT_REGISTER.put(register.namespace() + ":" + key, holder);
}
}
}
}
}
}
for (var idInfo : IDS.entrySet()) {
var codec = CODECS.get(idInfo.getKey());
if (codec == null) {
throw new IllegalArgumentException("Payload " + idInfo.getKey() + " is not configured correctly");
}
ID2CODEC.put(idInfo.getValue(), codec);
}
}
public static LeavesCustomPayload decode(ResourceLocation location, FriendlyByteBuf buf) {
var codec = ID2CODEC.get(location);
if (codec == null) {
return null;
}
return codec.decode(ProtocolUtils.decorate(buf));
}
public static void encode(FriendlyByteBuf buf, LeavesCustomPayload payload) {
var location = IDS.get(payload.getClass());
var codec = CODECS.get(payload.getClass());
if (location == null || codec == null) {
throw new IllegalArgumentException("Payload " + payload.getClass() + " is not configured correctly " + location + " " + codec);
}
buf.writeResourceLocation(location);
codec.encode(ProtocolUtils.decorate(buf), payload);
}
public static void handlePayload(IdentifierSelector selector, LeavesCustomPayload payload) {
PayloadReceiverInvokerHolder holder;
if ((holder = PAYLOAD_RECEIVERS.get(payload.getClass())) != null) {
holder.invoke(selector, payload);
}
}
public static boolean handleBytebuf(IdentifierSelector selector, ResourceLocation location, ByteBuf buf) {
RegistryFriendlyByteBuf buf1 = ProtocolUtils.decorate(buf);
BytebufReceiverInvokerHolder holder;
if ((holder = STRICT_BYTEBUF_RECEIVERS.get(location.toString())) != null) {
holder.invoke(selector, buf1);
return true;
}
if ((holder = NAMESPACED_BYTEBUF_RECEIVERS.get(location.getNamespace())) != null) {
if (holder.invoke(selector, buf1)) {
return true;
}
}
for (var holder1 : GENERIC_BYTEBUF_RECEIVERS) {
if (holder1.invoke(selector, buf1)) {
return true;
}
}
return false;
}
public static void handleTick(long tickCount) {
for (var tickerInfo : TICKERS) {
if (tickCount % tickerInfo.owner().tickerInterval(tickerInfo.handler().tickerId()) == 0) {
tickerInfo.invoke();
}
}
}
public static void handlePlayerJoin(ServerPlayer player) {
sendKnownId(player);
for (var join : PLAYER_JOIN) {
join.invoke(player);
}
}
public static void handlePlayerLeave(ServerPlayer player) {
for (var leave : PLAYER_LEAVE) {
leave.invoke(player);
}
}
public static void handleServerReload() {
for (var reload : RELOAD_SERVER) {
reload.invoke();
}
}
public static void handleDataPackReload() {
for (var reload : RELOAD_DATAPACK) {
reload.invoke();
}
}
public static void handleMinecraftRegister(String channelId, IdentifierSelector selector) {
ResourceLocation location = ResourceLocation.tryParse(channelId);
if (location == null) {
return;
}
for (var wildHolder : WILD_MINECRAFT_REGISTER) {
wildHolder.invoke(selector, location);
}
MinecraftRegisterInvokerHolder holder;
if ((holder = STRICT_MINECRAFT_REGISTER.get(location.toString())) != null) {
holder.invoke(selector, location);
}
if ((holder = NAMESPACED_MINECRAFT_REGISTER.get(location.getNamespace())) != null) {
holder.invoke(selector, location);
}
}
private static void sendKnownId(ServerPlayer player) {
Set<String> set = new HashSet<>();
PAYLOAD_RECEIVERS.forEach((clazz, holder) -> {
if (holder.owner().isActive()) {
set.add(IDS.get(clazz).toString());
}
});
STRICT_BYTEBUF_RECEIVERS.forEach((key, holder) -> {
if (holder.owner().isActive()) {
set.add(key);
}
});
ProtocolUtils.sendBytebufPacket(player, ResourceLocation.fromNamespaceAndPath("minecraft", "register"), buf -> {
for (String channel : set) {
buf.writeBytes(channel.getBytes(StandardCharsets.US_ASCII));
buf.writeByte(0);
}
buf.writerIndex(Math.max(buf.writerIndex() - 1, 0));
});
}
public static Set<Class<?>> getClasses(String pack) {
Set<Class<?>> classes = new LinkedHashSet<>();
String packageDirName = pack.replace('.', '/');
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
findClassesInPackageByFile(pack, filePath, classes);
} else if ("jar".equals(protocol)) {
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
findClassesInPackageByJar(pack, entries, packageDirName, classes);
} catch (IOException exception) {
LOGGER.warn("Failed to load jar file, {}: {}", exception.getCause(), exception.getMessage());
}
}
}
} catch (IOException exception) {
LOGGER.warn("Failed to load classes, {}: {}", exception.getCause(), exception.getMessage());
}
return classes;
}
private static void findClassesInPackageByFile(String packageName, String packagePath, Set<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] dirfiles = dir.listFiles((file) -> file.isDirectory() || file.getName().endsWith(".class"));
if (dirfiles != null) {
for (File file : dirfiles) {
if (file.isDirectory()) {
findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), classes);
} else {
String className = file.getName().substring(0, file.getName().length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException exception) {
LOGGER.warn("Failed to load class {}, {}: {}", className, exception.getCause(), exception.getMessage());
}
}
}
}
}
private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, Set<Class<?>> classes) {
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException exception) {
LOGGER.warn("Failed to load class {}, {}: {}", className, exception.getCause(), exception.getMessage());
}
}
}
}
}
}
@@ -0,0 +1,103 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import net.minecraft.server.level.ServerPlayer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class ProtocolHandler {
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Init {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PayloadReceiver {
Class<? extends LeavesCustomPayload> payload();
Stage stage() default Stage.GAME;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BytebufReceiver {
String key() default "";
boolean onlyNamespace() default false;
Stage stage() default Stage.GAME;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Ticker {
String tickerId() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlayerJoin {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlayerLeave {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReloadServer {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MinecraftRegister {
String key() default "";
boolean onlyNamespace() default false;
Stage stage() default Stage.CONFIGURATION;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReloadDataPack {
}
public enum Stage {
CONFIGURATION(Context.class),
GAME(ServerPlayer.class);
private final Class<?> identifier;
Stage(Class<?> identifier) {
this.identifier = identifier;
}
public Class<?> identifier() {
return identifier;
}
}
}
@@ -0,0 +1,89 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.papermc.paper.ServerBuildInfo;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.network.protocol.common.custom.DiscardedPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
import java.util.function.Function;
public class ProtocolUtils {
private static final Function<ByteBuf, RegistryFriendlyByteBuf> bufDecorator = buf -> buf instanceof RegistryFriendlyByteBuf registry ? registry : new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess());
private static final byte[] EMPTY = new byte[0];
public static String buildProtocolVersion(String protocol) {
return protocol + "-lophine-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE);
}
public static void sendEmptyPacket(ServerPlayer player, ResourceLocation id) {
player.connection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, EMPTY)));
}
public static void sendBytebufPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer<? super RegistryFriendlyByteBuf> consumer) {
RegistryFriendlyByteBuf buf = decorate(Unpooled.buffer());
consumer.accept(buf);
player.connection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, ByteBufUtil.getBytes(buf))));
}
public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) {
player.connection.send(new ClientboundCustomPayloadPacket(payload));
}
public static void sendEmptyPacket(Context context, ResourceLocation id) {
context.connection().send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, EMPTY)));
}
public static void sendBytebufPacket(@NotNull Context context, ResourceLocation id, Consumer<? super RegistryFriendlyByteBuf> consumer) {
RegistryFriendlyByteBuf buf = decorate(Unpooled.buffer());
consumer.accept(buf);
context.connection().send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, ByteBufUtil.getBytes(buf))));
}
public static void sendPayloadPacket(Context context, CustomPacketPayload payload) {
context.connection().send(new ClientboundCustomPayloadPacket(payload));
}
public static RegistryFriendlyByteBuf decorate(ByteBuf buf) {
return bufDecorator.apply(buf);
}
public static IdentifierSelector createSelector(ServerCommonPacketListenerImpl common) {
ServerPlayer player = common instanceof ServerGamePacketListenerImpl game ? game.getPlayer() : null;
return new IdentifierSelector(new Context(common.profile, common.connection), player);
}
public static ByteBuf wrapNullable(byte @Nullable [] data) {
return data == null ? Unpooled.wrappedBuffer(EMPTY) : Unpooled.wrappedBuffer(data);
}
}
@@ -0,0 +1,92 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public abstract class AbstractInvokerHolder<T> {
protected final LeavesProtocol owner;
protected final Method invoker;
protected final T handler;
protected final Class<?> returnType;
protected final Class<?>[] parameterTypes;
protected final boolean isStatic;
protected AbstractInvokerHolder(LeavesProtocol owner, Method invoker, T handler, @Nullable Class<?> returnType, @NotNull Class<?>... parameterTypes) {
this.owner = owner;
this.invoker = invoker;
this.handler = handler;
this.returnType = returnType;
this.parameterTypes = parameterTypes;
this.isStatic = Modifier.isStatic(invoker.getModifiers());
validateMethodSignature();
}
protected void validateMethodSignature() {
if (returnType != null && !returnType.isAssignableFrom(invoker.getReturnType())) {
throw new IllegalArgumentException("Return type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() +
": expected " + returnType.getName() + " but found " + invoker.getReturnType().getName());
}
Class<?>[] methodParamTypes = invoker.getParameterTypes();
if (methodParamTypes.length != parameterTypes.length) {
throw new IllegalArgumentException("Parameter count mismatch in " + owner.getClass().getName() + "#" + invoker.getName() +
": expected " + parameterTypes.length + " but found " + methodParamTypes.length);
}
for (int i = 0; i < parameterTypes.length; i++) {
if (!parameterTypes[i].isAssignableFrom(methodParamTypes[i])) {
throw new IllegalArgumentException("Parameter type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() +
" at index " + i + ": expected " + parameterTypes[i].getName() + " but found " + methodParamTypes[i].getName());
}
}
}
public LeavesProtocol owner() {
return owner;
}
public T handler() {
return handler;
}
protected Object invoke0(boolean force, Object... args) {
if (!force && !owner.isActive()) {
return null;
}
try {
if (isStatic) {
return invoker.invoke(null, args);
} else {
return invoker.invoke(owner, args);
}
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,35 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import net.minecraft.network.FriendlyByteBuf;
import org.leavesmc.leaves.protocol.core.IdentifierSelector;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import java.lang.reflect.Method;
public class BytebufReceiverInvokerHolder extends AbstractInvokerHolder<ProtocolHandler.BytebufReceiver> {
public BytebufReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.BytebufReceiver handler) {
super(owner, invoker, handler, null, handler.stage().identifier(), FriendlyByteBuf.class);
}
public boolean invoke(IdentifierSelector selector, FriendlyByteBuf buf) {
return invoke0(false, selector.select(handler.stage()), buf) instanceof Boolean b && b;
}
}
@@ -0,0 +1,32 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import java.lang.reflect.Method;
public class EmptyInvokerHolder<T> extends AbstractInvokerHolder<T> {
public EmptyInvokerHolder(LeavesProtocol owner, Method invoker, T handler) {
super(owner, invoker, handler, null);
}
public void invoke() {
invoke0(false);
}
}
@@ -0,0 +1,33 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import java.lang.reflect.Method;
public class InitInvokerHolder extends AbstractInvokerHolder<ProtocolHandler.Init> {
public InitInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.Init handler) {
super(owner, invoker, handler, null);
}
public void invoke() {
invoke0(true);
}
}
@@ -0,0 +1,35 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import net.minecraft.resources.ResourceLocation;
import org.leavesmc.leaves.protocol.core.IdentifierSelector;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import java.lang.reflect.Method;
public class MinecraftRegisterInvokerHolder extends AbstractInvokerHolder<ProtocolHandler.MinecraftRegister> {
public MinecraftRegisterInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.MinecraftRegister handler) {
super(owner, invoker, handler, null, handler.stage().identifier(), ResourceLocation.class);
}
public void invoke(IdentifierSelector selector, ResourceLocation id) {
invoke0(false, selector.select(handler.stage()), id);
}
}
@@ -0,0 +1,35 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import org.leavesmc.leaves.protocol.core.IdentifierSelector;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import java.lang.reflect.Method;
public class PayloadReceiverInvokerHolder extends AbstractInvokerHolder<ProtocolHandler.PayloadReceiver> {
public PayloadReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.PayloadReceiver handler) {
super(owner, invoker, handler, null, handler.stage().identifier(), handler.payload());
}
public void invoke(IdentifierSelector selector, LeavesCustomPayload payload) {
invoke0(false, selector.select(handler.stage()), payload);
}
}
@@ -0,0 +1,33 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.core.invoker;
import net.minecraft.server.level.ServerPlayer;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import java.lang.reflect.Method;
public class PlayerInvokerHolder<T> extends AbstractInvokerHolder<T> {
public PlayerInvokerHolder(LeavesProtocol owner, Method invoker, T handler) {
super(owner, invoker, handler, null, ServerPlayer.class);
}
public void invoke(ServerPlayer player) {
invoke0(false, player);
}
}
@@ -0,0 +1,134 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
// Powered by Servux(https://github.com/sakura-ryoko/servux)
/**
* Network packet splitter code from QuickCarpet by skyrising
*
* @author skyrising
* <p>
* Updated by Sakura to work with newer versions by changing the Reading Session keys,
* and using the HANDLER interface to send packets via the Payload system
* <p>
* Move to Leaves by violetc
*/
public class PacketSplitter {
public static final int MAX_TOTAL_PER_PACKET_S2C = 1048576;
public static final int MAX_PAYLOAD_PER_PACKET_S2C = MAX_TOTAL_PER_PACKET_S2C - 5;
public static final int MAX_TOTAL_PER_PACKET_C2S = 32767;
public static final int MAX_PAYLOAD_PER_PACKET_C2S = MAX_TOTAL_PER_PACKET_C2S - 5;
public static final int DEFAULT_MAX_RECEIVE_SIZE_C2S = 1048576;
public static final int DEFAULT_MAX_RECEIVE_SIZE_S2C = 67108864;
private static final Map<Long, ReadingSession> READING_SESSIONS = new HashMap<>();
public static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player) {
return send(handler, packet, player, MAX_PAYLOAD_PER_PACKET_S2C);
}
private static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player, int payloadLimit) {
int len = packet.writerIndex();
packet.resetReaderIndex();
for (int offset = 0; offset < len; offset += payloadLimit) {
int thisLen = Math.min(len - offset, payloadLimit);
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(thisLen));
buf.resetWriterIndex();
if (offset == 0) {
buf.writeVarInt(len);
}
buf.writeBytes(packet, thisLen);
handler.encode(player, buf);
}
packet.release();
return true;
}
public static FriendlyByteBuf receive(long key, FriendlyByteBuf buf) {
return receive(key, buf, DEFAULT_MAX_RECEIVE_SIZE_S2C);
}
@Nullable
private static FriendlyByteBuf receive(long key, FriendlyByteBuf buf, int maxLength) {
return READING_SESSIONS.computeIfAbsent(key, ReadingSession::new).receive(buf, maxLength);
}
public interface IPacketSplitterHandler {
void encode(ServerPlayer player, FriendlyByteBuf buf);
}
/**
* I had to fix the `Pair.of` key mappings, because they were removed from MC;
* So I made it into a pre-shared random session 'key' between client and server.
* Generated using 'long key = Random.create(Util.getMeasuringTimeMs()).nextLong();'
* -
* It can be shared to the receiving end via a separate packet; or it can just be
* generated randomly on the receiving end per an expected Reading Session.
* It needs to be stored and changed for every unique session.
*/
private static class ReadingSession {
private final long key;
private int expectedSize = -1;
private FriendlyByteBuf received;
private ReadingSession(long key) {
this.key = key;
}
@Nullable
private FriendlyByteBuf receive(FriendlyByteBuf data, int maxLength) {
data.readerIndex(0);
// data = PacketUtils.slice(data);
if (this.expectedSize < 0) {
this.expectedSize = data.readVarInt();
if (this.expectedSize > maxLength) {
throw new IllegalArgumentException("Payload too large");
}
this.received = new FriendlyByteBuf(Unpooled.buffer(this.expectedSize));
}
this.received.writeBytes(data.readBytes(data.readableBytes()));
if (this.received.writerIndex() >= this.expectedSize) {
READING_SESSIONS.remove(this.key);
return this.received;
}
return null;
}
}
}
@@ -0,0 +1,287 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.config.modules.function.SurvuxProtocolConfig;
import io.netty.buffer.Unpooled;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.leavesmc.leaves.util.TagUtil;
import org.slf4j.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
// Powered by Servux(https://github.com/sakura-ryoko/servux)
@LeavesProtocol.Register(namespace = "servux")
public class ServuxEntityDataProtocol implements LeavesProtocol {
private static final Logger LOGGER = LogUtils.getClassLogger();
public static final int PROTOCOL_VERSION = 1;
private static final Map<UUID, Long> readingSessionKeys = new HashMap<>();
@ProtocolHandler.PlayerJoin
public static void onPlayerJoin(ServerPlayer player) {
sendMetadata(player);
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLeave(ServerPlayer player) {
readingSessionKeys.remove(player.getUUID());
}
@ProtocolHandler.PayloadReceiver(payload = EntityDataPayload.class)
public static void onPacketReceive(ServerPlayer player, EntityDataPayload payload) {
switch (payload.packetType) {
case PACKET_C2S_METADATA_REQUEST -> sendMetadata(player);
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.pos);
case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.entityId);
case PACKET_C2S_NBT_RESPONSE_DATA -> {
UUID uuid = player.getUUID();
long readingSessionKey;
if (!readingSessionKeys.containsKey(uuid)) {
readingSessionKey = RandomSource.create(Util.getMillis()).nextLong();
readingSessionKeys.put(uuid, readingSessionKey);
} else {
readingSessionKey = readingSessionKeys.get(uuid);
}
FriendlyByteBuf fullPacket = PacketSplitter.receive(readingSessionKey, payload.buffer);
if (fullPacket != null) {
readingSessionKeys.remove(uuid);
LOGGER.warn("ServuxEntityDataProtocol,PACKET_C2S_NBT_RESPONSE_DATA NOT Implemented!");
}
}
}
}
public static void sendMetadata(ServerPlayer player) {
CompoundTag metadata = new CompoundTag();
metadata.putString("name", "entity_data");
metadata.putString("id", EntityDataPayload.CHANNEL.toString());
metadata.putInt("version", PROTOCOL_VERSION);
metadata.putString("servux", ServuxProtocol.SERVUX_STRING);
EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_METADATA);
payload.nbt.merge(metadata);
sendPacket(player, payload);
}
public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) {
io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
player.level(),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()),
() -> {
BlockEntity be = player.level().getBlockEntity(pos);
CompoundTag nbt = be != null ? be.saveWithFullMetadata(player.registryAccess()) : new CompoundTag();
EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE);
payload.pos = pos.immutable();
payload.nbt.merge(nbt);
sendPacket(player, payload);
}
);
}
public static void onEntityRequest(ServerPlayer player, int entityId) {
io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
player.level(),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()),
() -> {
Entity entity = player.level().getEntity(entityId);
CompoundTag nbt = TagUtil.saveEntityWithoutId(entity);
EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE);
payload.entityId = entityId;
payload.nbt.merge(nbt);
sendPacket(player, payload);
}
);
}
public static void sendPacket(ServerPlayer player, EntityDataPayload payload) {
if (payload.packetType == EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) {
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
buffer.writeNbt(payload.nbt);
PacketSplitter.send(ServuxEntityDataProtocol::sendWithSplitter, buffer, player);
} else {
ProtocolUtils.sendPayloadPacket(player, payload);
}
}
private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) {
EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA);
payload.buffer = buf;
payload.nbt = new CompoundTag();
sendPacket(player, payload);
}
@Override
public boolean isActive() {
return SurvuxProtocolConfig.entityProtocol;
}
public enum EntityDataPayloadType {
PACKET_S2C_METADATA(1),
PACKET_C2S_METADATA_REQUEST(2),
PACKET_C2S_BLOCK_ENTITY_REQUEST(3),
PACKET_C2S_ENTITY_REQUEST(4),
PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5),
PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6),
// For Packet Splitter (Oversize Packets, S2C)
PACKET_S2C_NBT_RESPONSE_START(10),
PACKET_S2C_NBT_RESPONSE_DATA(11),
// For Packet Splitter (Oversize Packets, C2S)
PACKET_C2S_NBT_RESPONSE_START(12),
PACKET_C2S_NBT_RESPONSE_DATA(13);
public final int type;
EntityDataPayloadType(int type) {
this.type = type;
Helper.ID_TO_TYPE.put(type, this);
}
public static EntityDataPayloadType fromId(int id) {
return Helper.ID_TO_TYPE.get(id);
}
private static final class Helper {
static Map<Integer, EntityDataPayloadType> ID_TO_TYPE = new HashMap<>();
}
}
public static class EntityDataPayload implements LeavesCustomPayload {
@ID
public static final ResourceLocation CHANNEL = ServuxProtocol.id("entity_data");
@Codec
public static final StreamCodec<FriendlyByteBuf, EntityDataPayload> CODEC = StreamCodec.of(
(buf, payload) -> {
buf.writeVarInt(payload.packetType.type);
switch (payload.packetType) {
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> {
buf.writeVarInt(payload.transactionId);
buf.writeBlockPos(payload.pos);
}
case PACKET_C2S_ENTITY_REQUEST -> {
buf.writeVarInt(payload.transactionId);
buf.writeVarInt(payload.entityId);
}
case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> {
buf.writeBlockPos(payload.pos);
buf.writeNbt(payload.nbt);
}
case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> {
buf.writeVarInt(payload.entityId);
buf.writeNbt(payload.nbt);
}
case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA ->
buf.writeBytes(payload.buffer.copy());
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> buf.writeNbt(payload.nbt);
}
},
buf -> {
EntityDataPayloadType type = EntityDataPayloadType.fromId(buf.readVarInt());
if (type == null) {
throw new IllegalStateException("invalid packet type received");
}
EntityDataPayload payload = new EntityDataPayload(type);
switch (type) {
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> {
buf.readVarInt();
payload.pos = buf.readBlockPos().immutable();
}
case PACKET_C2S_ENTITY_REQUEST -> {
buf.readVarInt();
payload.entityId = buf.readVarInt();
}
case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> {
payload.pos = buf.readBlockPos().immutable();
CompoundTag nbt = buf.readNbt();
if (nbt != null) {
payload.nbt.merge(nbt);
}
}
case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> {
payload.entityId = buf.readVarInt();
CompoundTag nbt = buf.readNbt();
if (nbt != null) {
payload.nbt.merge(nbt);
}
}
case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> {
payload.buffer = new FriendlyByteBuf(buf.readBytes(buf.readableBytes()));
payload.nbt = new CompoundTag();
}
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> {
CompoundTag nbt = buf.readNbt();
if (nbt != null) {
payload.nbt.merge(nbt);
}
}
}
return payload;
}
);
private final EntityDataPayloadType packetType;
private final int transactionId;
private int entityId;
private BlockPos pos;
private CompoundTag nbt;
private FriendlyByteBuf buffer;
private EntityDataPayload(EntityDataPayloadType type) {
this.packetType = type;
this.transactionId = -1;
this.entityId = -1;
this.pos = BlockPos.ZERO;
this.nbt = new CompoundTag();
this.clearPacket();
}
private void clearPacket() {
if (this.buffer != null) {
this.buffer.clear();
this.buffer = new FriendlyByteBuf(Unpooled.buffer());
}
}
}
}
@@ -0,0 +1,376 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.mojang.serialization.DataResult;
import fun.bm.lophine.config.modules.function.SurvuxProtocolConfig;
import io.netty.buffer.Unpooled;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.level.GameRules;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.leavesmc.leaves.protocol.servux.logger.DataLogger;
import javax.annotation.Nonnull;
import java.util.*;
@LeavesProtocol.Register(namespace = "servux")
public class ServuxHudDataProtocol implements LeavesProtocol {
public static final int PROTOCOL_VERSION = 2;
private static final List<ServerPlayer> players = new ArrayList<>();
private static final int updateInterval = 80;
private static final HashMap<ServerPlayer, List<DataLogger.Type>> loggerPlayers = new HashMap<>();
private static final HashMap<DataLogger.Type, DataLogger<?>> LOGGERS = new HashMap<>();
private static final Table<DataLogger.Type, ServerPlayer, Tag> DATA = HashBasedTable.create();
public static boolean refreshSpawnMetadata = false;
@ProtocolHandler.Init
private static void initializeLoggers() {
if (!LOGGERS.isEmpty()) {
return;
}
for (DataLogger.Type type : DataLogger.Type.VALUES) {
DataLogger<?> entry = type.init();
if (entry != null) {
LOGGERS.put(type, entry);
}
}
}
@ProtocolHandler.PlayerJoin
private static void onPlayerJoin(ServerPlayer player) {
sendHudMetadata(player);
}
@ProtocolHandler.PlayerLeave
private static void onPlayerLeave(ServerPlayer player) {
players.remove(player);
loggerPlayers.remove(player);
}
@ProtocolHandler.PayloadReceiver(payload = HudDataPayload.class)
public static void onPacketReceive(ServerPlayer player, HudDataPayload payload) {
switch (payload.packetType) {
case PACKET_C2S_METADATA_REQUEST -> {
players.add(player);
sendHudMetadata(player);
}
case PACKET_C2S_SPAWN_DATA_REQUEST -> refreshSpawnMetadata(player);
case PACKET_C2S_RECIPE_MANAGER_REQUEST -> refreshRecipeManager(player);
case PACKET_C2S_DATA_LOGGER_REQUEST -> refreshLoggers(player, payload.nbt);
}
}
public static void sendHudMetadata(ServerPlayer player) {
CompoundTag metadata = new CompoundTag();
metadata.putString("name", "hud_metadata");
metadata.putString("id", HudDataPayload.CHANNEL.toString());
metadata.putInt("version", PROTOCOL_VERSION);
metadata.putString("servux", ServuxProtocol.SERVUX_STRING);
if (SurvuxProtocolConfig.hudLoggerProtocol) {
CompoundTag nbt = new CompoundTag();
for (DataLogger.Type type : DataLogger.Type.VALUES) {
nbt.putBoolean(type.getSerializedName(), isLoggerTypeEnabled(type));
}
metadata.put("Loggers", nbt);
}
putWorldData(metadata);
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_METADATA, metadata));
}
public static void refreshSpawnMetadata(ServerPlayer player) {
CompoundTag metadata = new CompoundTag();
metadata.putString("id", HudDataPayload.CHANNEL.toString());
metadata.putString("servux", ServuxProtocol.SERVUX_STRING);
putWorldData(metadata);
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_SPAWN_DATA, metadata));
}
public static void refreshRecipeManager(ServerPlayer player) {
Collection<RecipeHolder<?>> recipes = MinecraftServer.getServer().getRecipeManager().getRecipes();
CompoundTag nbt = new CompoundTag();
ListTag list = new ListTag();
recipes.forEach((recipeEntry -> {
DataResult<Tag> dr = Recipe.CODEC.encodeStart(NbtOps.INSTANCE, recipeEntry.value());
if (dr.result().isPresent()) {
CompoundTag entry = new CompoundTag();
entry.putString("id_reg", recipeEntry.id().registry().toString());
entry.putString("id_value", recipeEntry.id().location().toString());
entry.put("recipe", dr.result().get());
list.add(entry);
}
}));
nbt.put("RecipeManager", list);
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START, nbt));
}
public static void refreshWeatherData(ServerPlayer player) {
ServerLevel level = MinecraftServer.getServer().overworld();
if (!level.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) {
return;
}
CompoundTag nbt = new CompoundTag();
nbt.putString("id", HudDataPayload.CHANNEL.toString());
nbt.putString("servux", ServuxProtocol.SERVUX_STRING);
if (level.serverLevelData.isRaining() && level.serverLevelData.getRainTime() > -1) {
nbt.putInt("SetRaining", level.serverLevelData.getRainTime());
nbt.putBoolean("isRaining", true);
} else {
nbt.putBoolean("isRaining", false);
}
if (level.serverLevelData.isThundering() && level.serverLevelData.getThunderTime() > -1) {
nbt.putInt("SetThundering", level.serverLevelData.getThunderTime());
nbt.putBoolean("isThundering", true);
} else {
nbt.putBoolean("isThundering", false);
}
if (level.serverLevelData.getClearWeatherTime() > -1) {
nbt.putInt("SetClear", level.serverLevelData.getClearWeatherTime());
}
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_WEATHER_TICK, nbt));
}
private static void putWorldData(@NotNull CompoundTag metadata) {
ServerLevel level = MinecraftServer.getServer().overworld();
BlockPos spawnPos = level.levelData.getSpawnPos();
metadata.putInt("spawnPosX", spawnPos.getX());
metadata.putInt("spawnPosY", spawnPos.getY());
metadata.putInt("spawnPosZ", spawnPos.getZ());
metadata.putInt("spawnChunkRadius", level.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS));
if (SurvuxProtocolConfig.hudMetadataShareSeed) {
metadata.putLong("worldSeed", level.getSeed());
}
}
public static void refreshLoggers(ServerPlayer player, @Nonnull CompoundTag nbt) {
if (!player.getBukkitEntity().hasPermission("servux.provider.hud_data.logger")) {
player.sendSystemMessage(Component.translatable("servux.hud_data.error.insufficient_for_loggers", "any"));
return;
}
if (nbt.isEmpty()) {
return;
}
List<DataLogger.Type> list = new ArrayList<>();
for (String key : nbt.keySet()) {
DataLogger.Type type = DataLogger.Type.fromStringStatic(key);
if (type != null && nbt.getBooleanOr(key, false)) {
list.add(type);
}
}
if (!list.isEmpty()) {
loggerPlayers.put(player, list);
} else {
loggerPlayers.remove(player);
}
}
private static boolean isLoggerTypeEnabled(DataLogger.Type type) {
return SurvuxProtocolConfig.hudEnabledLoggers.contains(type.getSerializedName());
}
@ProtocolHandler.Ticker
public void protocolTick() {
for (ServerPlayer player : players) {
if (refreshSpawnMetadata) {
refreshSpawnMetadata(player);
}
refreshWeatherData(player);
}
refreshSpawnMetadata = false;
}
@ProtocolHandler.Ticker(tickerId = "logger")
public void loggerTick() {
if (!SurvuxProtocolConfig.hudLoggerProtocol) {
return;
}
MinecraftServer server = MinecraftServer.getServer();
if (server.getTickCount() % SurvuxProtocolConfig.hudUpdateInterval == 0) {
LOGGERS.forEach((type, logger) -> {
if (!isLoggerTypeEnabled(type)) {
return;
}
for (ServerPlayer player : players) {
Tag ret = logger.getResult(server, this, player);
if (ret != null) {
DATA.put(type, player, ret);
}
}
});
}
for (ServerPlayer player : loggerPlayers.keySet()) {
List<DataLogger.Type> list = loggerPlayers.get(player);
if (list.isEmpty()) {
return;
}
CompoundTag nbt = new CompoundTag();
for (DataLogger.Type type : list) {
Tag data = DATA.get(type, player);
if (data != null) {
nbt.put(type.getSerializedName(), data);
DATA.remove(type, player);
}
}
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_DATA_LOGGER_TICK, nbt));
}
}
public void applyData(DataLogger.Type type, ServerPlayer player, Tag tag) {
DATA.put(type, player, tag);
}
@Override
public boolean isActive() {
return SurvuxProtocolConfig.hudMetadataProtocol;
}
@Override
public int tickerInterval(String tickerID) {
return tickerID.equals("logger") ? 1 : updateInterval;
}
public static void sendPacket(ServerPlayer player, HudDataPayload payload) {
if (payload.packetType == HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) {
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
buffer.writeNbt(payload.nbt);
PacketSplitter.send(ServuxHudDataProtocol::sendWithSplitter, buffer, player);
} else {
ProtocolUtils.sendPayloadPacket(player, payload);
}
}
private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) {
sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA, buf));
}
public enum HudDataPayloadType {
PACKET_S2C_METADATA(1),
PACKET_C2S_METADATA_REQUEST(2),
PACKET_S2C_SPAWN_DATA(3),
PACKET_C2S_SPAWN_DATA_REQUEST(4),
PACKET_S2C_WEATHER_TICK(5),
PACKET_C2S_RECIPE_MANAGER_REQUEST(6),
PACKET_S2C_DATA_LOGGER_TICK(7),
PACKET_C2S_DATA_LOGGER_REQUEST(8),
// For Packet Splitter (Oversize Packets, S2C)
PACKET_S2C_NBT_RESPONSE_START(10),
PACKET_S2C_NBT_RESPONSE_DATA(11);
public final int type;
HudDataPayloadType(int type) {
this.type = type;
Helper.ID_TO_TYPE.put(type, this);
}
public static HudDataPayloadType fromId(int id) {
return Helper.ID_TO_TYPE.get(id);
}
private static final class Helper {
static Map<Integer, HudDataPayloadType> ID_TO_TYPE = new HashMap<>();
}
}
public record HudDataPayload(HudDataPayloadType packetType, CompoundTag nbt,
FriendlyByteBuf buffer) implements LeavesCustomPayload {
@ID
public static final ResourceLocation CHANNEL = ServuxProtocol.id("hud_metadata");
@Codec
public static final StreamCodec<FriendlyByteBuf, HudDataPayload> CODEC = StreamCodec.of(
(buf, payload) -> {
buf.writeVarInt(payload.packetType().type);
switch (payload.packetType()) {
case PACKET_S2C_NBT_RESPONSE_DATA -> buf.writeBytes(payload.buffer().copy());
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST,
PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK, PACKET_C2S_RECIPE_MANAGER_REQUEST,
PACKET_C2S_DATA_LOGGER_REQUEST, PACKET_S2C_DATA_LOGGER_TICK -> buf.writeNbt(payload.nbt());
}
},
buf -> {
HudDataPayloadType type = HudDataPayloadType.fromId(buf.readVarInt());
if (type == null) {
throw new IllegalStateException("invalid packet type received");
}
switch (type) {
case PACKET_S2C_NBT_RESPONSE_DATA -> {
return new HudDataPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes())));
}
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST,
PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK,
PACKET_C2S_RECIPE_MANAGER_REQUEST, PACKET_C2S_DATA_LOGGER_REQUEST,
PACKET_S2C_DATA_LOGGER_TICK -> {
return new HudDataPayload(type, buf.readNbt());
}
}
throw new IllegalStateException("invalid packet type received");
}
);
public HudDataPayload(HudDataPayloadType type, CompoundTag nbt) {
this(type, nbt, null);
}
public HudDataPayload(HudDataPayloadType type, FriendlyByteBuf buffer) {
this(type, new CompoundTag(), buffer);
}
}
}
@@ -0,0 +1,36 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Contract;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ServuxProtocol {
public static final String PROTOCOL_ID = "servux";
public static final Logger LOGGER = LoggerFactory.getLogger(PROTOCOL_ID.toUpperCase());
public static final String SERVUX_STRING = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID);
@Contract("_ -> new")
public static ResourceLocation id(String path) {
return ResourceLocation.tryBuild(PROTOCOL_ID, path);
}
}
@@ -0,0 +1,420 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux;
import com.mojang.logging.LogUtils;
import fun.bm.lophine.config.modules.function.SurvuxProtocolConfig;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.slf4j.Logger;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// Powered by Servux(https://github.com/sakura-ryoko/servux)
@LeavesProtocol.Register(namespace = "servux")
public class ServuxStructuresProtocol implements LeavesProtocol {
private static final Logger LOGGER = LogUtils.getClassLogger();
public static final int PROTOCOL_VERSION = 2;
private static final int updateInterval = 40;
private static final int timeout = 30 * 20;
private static final Map<Integer, ServerPlayer> players = new ConcurrentHashMap<>();
private static final Map<UUID, Map<ChunkPos, Timeout>> timeouts = new HashMap<>();
private static int retainDistance;
@ProtocolHandler.PlayerJoin
public static void onPlayerJoin(ServerPlayer player) {
sendMetaData(player);
}
@ProtocolHandler.PayloadReceiver(payload = StructuresPayload.class)
public static void onPacketReceive(ServerPlayer player, StructuresPayload payload) {
switch (payload.packetType()) {
case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player);
case PACKET_C2S_REQUEST_SPAWN_METADATA -> ServuxHudDataProtocol.refreshSpawnMetadata(player); // move to
case PACKET_C2S_STRUCTURES_UNREGISTER -> onPlayerLoggedOut(player);
}
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLoggedOut(@NotNull ServerPlayer player) {
players.remove(player.getId());
timeouts.remove(player.getUUID());
}
@ProtocolHandler.Ticker
public static void tick() {
MinecraftServer server = MinecraftServer.getServer();
int tickCounter = server.getTickCount();
retainDistance = server.getPlayerList().getViewDistance() + 2;
for (ServerPlayer player : players.values()) {
// TODO DimensionChange
refreshTrackedChunks(player, tickCounter);
}
}
public static void onStartedWatchingChunk(ServerPlayer player, LevelChunk chunk) {
MinecraftServer server = player.getServer();
if (players.containsKey(player.getId()) && server != null) {
addChunkTimeoutIfHasReferences(player.getUUID(), chunk, server.getTickCount());
}
}
private static void addChunkTimeoutIfHasReferences(final UUID uuid, LevelChunk chunk, final int tickCounter) {
final ChunkPos pos = chunk.getPos();
if (chunkHasStructureReferences(pos.x, pos.z, chunk.getLevel())) {
final Map<ChunkPos, Timeout> map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>());
map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter - timeout));
}
}
private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, @NotNull Level world) {
if (!world.hasChunk(chunkX, chunkZ)) {
return false;
}
ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false);
if (chunk != null) {
for (Map.Entry<Structure, LongSet> entry : chunk.getAllReferences().entrySet()) {
if (!entry.getValue().isEmpty()) {
return true;
}
}
}
return false;
}
public static void onPlayerSubscribed(@NotNull ServerPlayer player) {
if (!players.containsKey(player.getId())) {
players.put(player.getId(), player);
} else {
LOGGER.warn("{} re-register servux:structures", player.getScoreboardName());
}
MinecraftServer server = MinecraftServer.getServer();
sendMetaData(player);
initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount());
}
private static void sendMetaData(ServerPlayer player) {
CompoundTag tag = new CompoundTag();
tag.putString("name", "structure_bounding_boxes");
tag.putString("id", StructuresPayload.CHANNEL.toString());
tag.putInt("version", PROTOCOL_VERSION);
tag.putString("servux", ServuxProtocol.SERVUX_STRING);
tag.putInt("timeout", timeout);
sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_METADATA, tag));
}
public static void initialSyncStructures(ServerPlayer player, int chunkRadius, int tickCounter) {
UUID uuid = player.getUUID();
ChunkPos center = player.getLastSectionPos().chunk();
Map<Structure, LongSet> references = getStructureReferences(player.level(), center, chunkRadius);
timeouts.remove(uuid);
sendStructures(player, references, tickCounter);
}
public static Map<Structure, LongSet> getStructureReferences(ServerLevel world, ChunkPos center, int chunkRadius) {
Map<Structure, LongSet> references = new HashMap<>();
for (int cx = center.x - chunkRadius; cx <= center.x + chunkRadius; ++cx) {
for (int cz = center.z - chunkRadius; cz <= center.z + chunkRadius; ++cz) {
getReferencesFromChunk(cx, cz, world, references);
}
}
return references;
}
public static void getReferencesFromChunk(int chunkX, int chunkZ, Level world, Map<Structure, LongSet> references) {
if (!world.hasChunk(chunkX, chunkZ)) {
return;
}
ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false);
if (chunk != null) {
for (Map.Entry<Structure, LongSet> entry : chunk.getAllReferences().entrySet()) {
Structure feature = entry.getKey();
LongSet startChunks = entry.getValue();
// TODO add an option && feature != StructureFeature.MINESHAFT (?)
if (!startChunks.isEmpty()) {
references.merge(feature, startChunks, (oldSet, entrySet) -> {
LongOpenHashSet newSet = new LongOpenHashSet(oldSet);
newSet.addAll(entrySet);
return newSet;
});
}
}
}
}
public static void sendStructures(ServerPlayer player, Map<Structure, LongSet> references, int tickCounter) {
ServerLevel world = player.level();
Map<ChunkPos, StructureStart> starts = getStructureStarts(world, references);
if (!starts.isEmpty()) {
addOrRefreshTimeouts(player.getUUID(), references, tickCounter);
ListTag structureList = getStructureList(starts, world);
if (players.containsKey(player.getId())) {
CompoundTag test = new CompoundTag();
test.put("Structures", structureList.copy());
sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START, test));
}
}
}
public static ListTag getStructureList(Map<ChunkPos, StructureStart> structures, ServerLevel world) {
ListTag list = new ListTag();
StructurePieceSerializationContext ctx = StructurePieceSerializationContext.fromLevel(world);
for (Map.Entry<ChunkPos, StructureStart> entry : structures.entrySet()) {
ChunkPos pos = entry.getKey();
list.add(entry.getValue().createTag(ctx, pos));
}
return list;
}
public static Map<ChunkPos, StructureStart> getStructureStarts(ServerLevel world, Map<Structure, LongSet> references) {
Map<ChunkPos, StructureStart> starts = new HashMap<>();
for (Map.Entry<Structure, LongSet> entry : references.entrySet()) {
Structure structure = entry.getKey();
LongSet startChunks = entry.getValue();
LongIterator iter = startChunks.iterator();
while (iter.hasNext()) {
ChunkPos pos = new ChunkPos(iter.nextLong());
if (!world.hasChunk(pos.x, pos.z)) {
continue;
}
ChunkAccess chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false);
StructureStart start = null;
if (chunk != null) {
start = chunk.getStartForStructure(structure);
}
if (start != null) {
starts.put(pos, start);
}
}
}
return starts;
}
public static void refreshTrackedChunks(ServerPlayer player, int tickCounter) {
UUID uuid = player.getUUID();
Map<ChunkPos, Timeout> map = timeouts.get(uuid);
if (map != null) {
sendAndRefreshExpiredStructures(player, map, tickCounter);
}
}
public static void sendAndRefreshExpiredStructures(ServerPlayer player, Map<ChunkPos, Timeout> map, int tickCounter) {
Set<ChunkPos> positionsToUpdate = new HashSet<>();
for (Map.Entry<ChunkPos, Timeout> entry : map.entrySet()) {
Timeout out = entry.getValue();
if (out.needsUpdate(tickCounter, timeout)) {
positionsToUpdate.add(entry.getKey());
}
}
if (!positionsToUpdate.isEmpty()) {
ServerLevel world = player.level();
ChunkPos center = player.getLastSectionPos().chunk();
Map<Structure, LongSet> references = new HashMap<>();
for (ChunkPos pos : positionsToUpdate) {
if (isOutOfRange(pos, center)) {
map.remove(pos);
} else {
getReferencesFromChunk(pos.x, pos.z, world, references);
Timeout timeout = map.get(pos);
if (timeout != null) {
timeout.setLastSync(tickCounter);
}
}
}
if (!references.isEmpty()) {
sendStructures(player, references, tickCounter);
}
}
}
protected static boolean isOutOfRange(ChunkPos pos, ChunkPos center) {
return Math.abs(pos.x - center.x) > retainDistance || Math.abs(pos.z - center.z) > retainDistance;
}
public static void addOrRefreshTimeouts(final UUID uuid, final Map<Structure, LongSet> references, final int tickCounter) {
Map<ChunkPos, Timeout> map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>());
for (LongSet chunks : references.values()) {
for (Long chunkPosLong : chunks) {
final ChunkPos pos = new ChunkPos(chunkPosLong);
map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter)).setLastSync(tickCounter);
}
}
}
public static void sendPacket(ServerPlayer player, StructuresPayload payload) {
if (payload.packetType() == StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START) {
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
buffer.writeNbt(payload.nbt());
PacketSplitter.send(ServuxStructuresProtocol::sendWithSplitter, buffer, player);
} else {
ProtocolUtils.sendPayloadPacket(player, payload);
}
}
private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) {
sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA, buf));
}
@Override
public int tickerInterval(String tickerID) {
return updateInterval;
}
@Override
public boolean isActive() {
return SurvuxProtocolConfig.structureProtocol;
}
public enum StructuresPayloadType {
PACKET_S2C_METADATA(1),
PACKET_S2C_STRUCTURE_DATA(2),
PACKET_C2S_STRUCTURES_REGISTER(3),
PACKET_C2S_STRUCTURES_UNREGISTER(4),
PACKET_S2C_STRUCTURE_DATA_START(5),
PACKET_S2C_SPAWN_METADATA(10),
PACKET_C2S_REQUEST_SPAWN_METADATA(11);
public final int type;
StructuresPayloadType(int type) {
this.type = type;
Helper.ID_TO_TYPE.put(type, this);
}
public static StructuresPayloadType fromId(int id) {
return Helper.ID_TO_TYPE.get(id);
}
private static final class Helper {
static Map<Integer, StructuresPayloadType> ID_TO_TYPE = new HashMap<>();
}
}
public record StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt,
FriendlyByteBuf buffer) implements LeavesCustomPayload {
@ID
public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures");
@Codec
public static final StreamCodec<FriendlyByteBuf, StructuresPayload> CODEC = StreamCodec.of(
(buf, payload) -> {
buf.writeVarInt(payload.packetType().type);
if (payload.packetType().equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) {
buf.writeBytes(payload.buffer().readBytes(payload.buffer().readableBytes()));
} else {
buf.writeNbt(payload.nbt());
}
},
buf -> {
int i = buf.readVarInt();
StructuresPayloadType type = StructuresPayloadType.fromId(i);
if (type == null) {
throw new IllegalStateException("invalid packet type received");
} else if (type.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) {
return new StructuresPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes())));
} else {
return new StructuresPayload(type, buf.readNbt());
}
}
);
public StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt) {
this(packetType, nbt, null);
}
public StructuresPayload(StructuresPayloadType packetType, FriendlyByteBuf buffer) {
this(packetType, new CompoundTag(), buffer);
}
}
public static class Timeout {
private int lastSync;
public Timeout(int currentTick) {
this.lastSync = currentTick;
}
public boolean needsUpdate(int currentTick, int timeout) {
return currentTick - this.lastSync >= timeout;
}
public void setLastSync(int tickCounter) {
this.lastSync = tickCounter;
}
}
}
@@ -0,0 +1,281 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics;
import com.google.common.collect.ImmutableMap;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.LongArrayTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.ScheduledTick;
import net.minecraft.world.ticks.TickPriority;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import org.leavesmc.leaves.protocol.servux.ServuxProtocol;
import org.leavesmc.leaves.protocol.servux.litematics.container.LitematicaBlockStateContainer;
import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType;
import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils;
import org.leavesmc.leaves.protocol.servux.litematics.utils.PositionUtils;
import java.util.*;
public record LitematicaSchematic(Map<String, SubRegion> subRegions, SchematicMetadata metadata) {
public static final int MINECRAFT_DATA_VERSION = SharedConstants.getProtocolVersion();
public static final int SCHEMATIC_VERSION = 7;
@NotNull
@Contract("_ -> new")
public static LitematicaSchematic readFromNBT(@NotNull CompoundTag nbt) {
if (nbt.contains("Version")) {
final int version = nbt.getIntOr("Version", -1);
final int minecraftDataVersion = nbt.contains("MinecraftDataVersion") ? nbt.getInt("MinecraftDataVersion").orElseThrow() : SharedConstants.getProtocolVersion();
if (version >= 1 && version <= SCHEMATIC_VERSION) {
SchematicMetadata metadata = SchematicMetadata.readFromNBT(nbt.getCompoundOrEmpty("Metadata"), version, minecraftDataVersion, FileType.LITEMATICA_SCHEMATIC);
Map<String, SubRegion> subRegions = readSubRegionsFromNBT(nbt.getCompoundOrEmpty("Regions"), version, minecraftDataVersion);
return new LitematicaSchematic(subRegions, metadata);
} else {
throw new RuntimeException("Unsupported or future schematic version");
}
} else {
throw new RuntimeException("The schematic doesn't have version information, and can't be safely loaded!");
}
}
@NotNull
private static Map<String, SubRegion> readSubRegionsFromNBT(@NotNull CompoundTag tag, int version, int minecraftDataVersion) {
Map<String, SubRegion> subRegions = new HashMap<>();
for (String regionName : tag.keySet()) {
Tag region = tag.get(regionName);
if (region == null || region.getId() != Tag.TAG_COMPOUND) {
throw new RuntimeException("Unknown region: " + regionName);
}
subRegions.put(regionName, SubRegion.readFromNBT(tag.getCompoundOrEmpty(regionName), version, minecraftDataVersion));
}
return subRegions;
}
@NotNull
@Unmodifiable
public Map<String, BlockPos> getAreaSizes() {
ImmutableMap.Builder<String, BlockPos> builder = ImmutableMap.builderWithExpectedSize(subRegions.size());
for (Map.Entry<String, SubRegion> entry : subRegions.entrySet()) {
builder.put(entry.getKey(), entry.getValue().size());
}
return builder.build();
}
@NotNull
public SubRegion getSubRegion(String name) {
return Objects.requireNonNull(subRegions.get(name));
}
public record SubRegion(
LitematicaBlockStateContainer blockContainers,
Map<BlockPos, CompoundTag> tileEntities,
Map<BlockPos, ScheduledTick<Block>> pendingBlockTicks,
Map<BlockPos, ScheduledTick<Fluid>> pendingFluidTicks,
List<EntityInfo> entities,
BlockPos position,
BlockPos size
) {
@NotNull
@Contract("_, _, _ -> new")
public static SubRegion readFromNBT(@NotNull CompoundTag regionTag, int version, int minecraftDataVersion) {
BlockPos position = NbtUtils.readBlockPos(regionTag.getCompoundOrEmpty("Position"));
BlockPos size = NbtUtils.readBlockPos(regionTag.getCompoundOrEmpty("Size"));
if (position == null || size == null) {
throw new IllegalArgumentException("Invalid region");
}
Map<BlockPos, CompoundTag> tileEntities;
List<EntityInfo> entities;
if (version >= 2) {
tileEntities = readTileEntitiesFromNBT(regionTag.getListOrEmpty("TileEntities"));
entities = readEntitiesFromNBT(regionTag.getListOrEmpty("Entities"));
} else {
tileEntities = readTileEntitiesFromNBT_v1(regionTag.getListOrEmpty("TileEntities"));
entities = readEntitiesFromNBT_v1(regionTag.getListOrEmpty("Entities"));
}
Map<BlockPos, ScheduledTick<Block>> pendingBlockTicks = null;
if (version >= 3) {
pendingBlockTicks = readPendingTicksFromNBT(regionTag.getListOrEmpty("PendingBlockTicks"), BuiltInRegistries.BLOCK, "Block", Blocks.AIR);
}
Map<BlockPos, ScheduledTick<Fluid>> pendingFluidTicks = null;
if (version >= 5) {
pendingFluidTicks = readPendingTicksFromNBT(regionTag.getListOrEmpty("PendingFluidTicks"), BuiltInRegistries.FLUID, "Fluid", Fluids.EMPTY);
}
LitematicaBlockStateContainer blockContainers = null;
Tag blockState = regionTag.get("BlockStates");
if (blockState != null && blockState.getId() == Tag.TAG_LONG_ARRAY) {
ListTag palette = regionTag.getListOrEmpty("BlockStatePalette");
long[] blockStateArr = ((LongArrayTag) blockState).getAsLongArray();
BlockPos posEndRel = PositionUtils.getRelativeEndPositionFromAreaSize(size).offset(position);
BlockPos posMin = PositionUtils.getMinCorner(position, posEndRel);
BlockPos posMax = PositionUtils.getMaxCorner(position, posEndRel);
BlockPos containerSize = posMax.subtract(posMin).offset(1, 1, 1);
blockContainers = LitematicaBlockStateContainer.createFrom(palette, blockStateArr, containerSize);
if (minecraftDataVersion < MINECRAFT_DATA_VERSION) {
ServuxProtocol.LOGGER.warn("Cannot process minecraft data version: {}", minecraftDataVersion);
}
}
return new SubRegion(blockContainers, tileEntities, pendingBlockTicks, pendingFluidTicks, entities, position, size);
}
private static List<EntityInfo> readEntitiesFromNBT(ListTag tagList) {
List<EntityInfo> entityList = new ArrayList<>();
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag entityData = tagList.getCompoundOrEmpty(i);
Vec3 posVec = NbtUtils.readEntityPositionFromTag(entityData);
if (posVec != null && !entityData.isEmpty()) {
entityList.add(new EntityInfo(posVec, entityData));
}
}
return entityList;
}
private static Map<BlockPos, CompoundTag> readTileEntitiesFromNBT(ListTag tagList) {
Map<BlockPos, CompoundTag> tileMap = new HashMap<>();
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag tag = tagList.getCompoundOrEmpty(i);
BlockPos pos = NbtUtils.readBlockPos(tag);
if (pos != null && !tag.isEmpty()) {
tileMap.put(pos, tag);
}
}
return tileMap;
}
private static <T> Map<BlockPos, ScheduledTick<T>> readPendingTicksFromNBT(ListTag tagList, Registry<T> registry, String tagName, T emptyValue) {
Map<BlockPos, ScheduledTick<T>> tickMap = new HashMap<>();
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag tag = tagList.getCompoundOrEmpty(i);
// XXX these were accidentally saved as longs in version 3
if (!tag.contains("Time")) {
continue;
}
T target;
ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getStringOr(tagName, ""));
if (resourceLocation == null) {
continue;
}
Optional<Holder.Reference<T>> tReference = registry.get(resourceLocation);
if (tReference.isEmpty()) {
continue;
}
target = tReference.get().value();
if (target == emptyValue) {
continue;
}
BlockPos pos = new BlockPos(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0));
TickPriority priority = TickPriority.byValue(tag.getIntOr("Priority", 0));
int scheduledTime = tag.getIntOr("Time", 0);
long subTick = tag.getLongOr("SubTick", 0L);
tickMap.put(pos, new ScheduledTick<>(target, pos, scheduledTime, priority, subTick));
}
return tickMap;
}
private static List<EntityInfo> readEntitiesFromNBT_v1(ListTag tagList) {
List<EntityInfo> entityList = new ArrayList<>();
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag tag = tagList.getCompoundOrEmpty(i);
Vec3 posVec = NbtUtils.readVec3(tag);
CompoundTag entityData = tag.getCompoundOrEmpty("EntityData");
if (posVec != null && !entityData.isEmpty()) {
// Update the correct position to the TileEntity NBT, where it is stored in version 2
NbtUtils.writeEntityPositionToTag(posVec, entityData);
entityList.add(new EntityInfo(posVec, entityData));
}
}
return entityList;
}
private static Map<BlockPos, CompoundTag> readTileEntitiesFromNBT_v1(ListTag tagList) {
Map<BlockPos, CompoundTag> tileMap = new HashMap<>();
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag tag = tagList.getCompoundOrEmpty(i);
CompoundTag tileNbt = tag.getCompoundOrEmpty("TileNBT");
// Note: This within-schematic relative position is not inside the tile tag!
BlockPos pos = NbtUtils.readBlockPos(tag);
if (pos != null && !tileNbt.isEmpty()) {
// Update the correct position to the entity NBT, where it is stored in version 2
NbtUtils.writeBlockPosToTag(pos, tileNbt);
tileMap.put(pos, tileNbt);
}
}
return tileMap;
}
}
public record EntityInfo(Vec3 posVec, CompoundTag nbt) {
public EntityInfo(Vec3 posVec, CompoundTag nbt) {
this.posVec = posVec;
if (nbt.contains("SleepingX")) {
nbt.putInt("SleepingX", Mth.floor(posVec.x));
}
if (nbt.contains("SleepingY")) {
nbt.putInt("SleepingY", Mth.floor(posVec.y));
}
if (nbt.contains("SleepingZ")) {
nbt.putInt("SleepingZ", Mth.floor(posVec.z));
}
this.nbt = nbt;
}
}
}
@@ -0,0 +1,62 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType;
import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils;
import org.leavesmc.leaves.protocol.servux.litematics.utils.Schema;
import javax.annotation.Nullable;
import java.util.Objects;
public record SchematicMetadata(
String name,
String author,
String description,
Vec3i enclosingSize,
long timeCreated,
long timeModified,
int minecraftDataVersion,
int schematicVersion,
Schema schema,
FileType type,
int regionCount,
long totalVolume,
long totalBlocks,
@Nullable int[] thumbnailPixelData
) {
@NotNull
@Contract("_, _, _, _ -> new")
public static SchematicMetadata readFromNBT(@NotNull CompoundTag nbt, int version, int minecraftDataVersion, FileType fileType) {
String name = nbt.getStringOr("Name", "?");
String author = nbt.getStringOr("Author", "?");
String description = nbt.getStringOr("Description", "");
int regionCount = nbt.getIntOr("RegionCount", -1);
long timeCreated = nbt.getLongOr("TimeCreated", -1L);
long timeModified = nbt.getLongOr("TimeModified", -1L);
long totalVolume = nbt.getIntOr("TotalVolume", -1);
long totalBlocks = nbt.getIntOr("TotalBlocks", -1);
Vec3i enclosingSize = Objects.requireNonNullElse(NbtUtils.readVec3iFromTag(nbt.getCompoundOrEmpty("EnclosingSize")), Vec3i.ZERO);
int[] thumbnailPixelData = nbt.getIntArray("PreviewImageData").orElse(null);
return new SchematicMetadata(name, author, description, enclosingSize, timeCreated, timeModified, minecraftDataVersion, version, Schema.getSchemaByDataVersion(minecraftDataVersion), fileType, regionCount, totalVolume, totalBlocks, thumbnailPixelData);
}
}
@@ -0,0 +1,457 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics;
import fun.bm.lophine.config.modules.function.SurvuxProtocolConfig;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.leavesmc.leaves.protocol.servux.PacketSplitter;
import org.leavesmc.leaves.protocol.servux.ServuxProtocol;
import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement;
import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils;
import org.leavesmc.leaves.protocol.servux.litematics.utils.ReplaceBehavior;
import org.leavesmc.leaves.util.TagUtil;
import java.util.*;
@LeavesProtocol.Register(namespace = "servux")
public class ServuxLitematicsProtocol implements LeavesProtocol {
public static final int PROTOCOL_VERSION = 1;
private static final CompoundTag metadata = new CompoundTag();
private static final Map<UUID, Long> playerSession = new HashMap<>();
@ProtocolHandler.Init
public static void init() {
metadata.putString("name", "litematic_data");
metadata.putString("id", ServuxLitematicaPayload.CHANNEL.toString());
metadata.putInt("version", PROTOCOL_VERSION);
metadata.putString("servux", ServuxProtocol.SERVUX_STRING);
}
public static boolean hasPermission(ServerPlayer player) {
return player.getBukkitEntity().hasPermission("leaves.protocol.litematics");
}
public static void sendMetaData(ServerPlayer player) {
ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_METADATA);
send.nbt.merge(metadata);
encodeServerData(player, send);
}
public static void encodeServerData(ServerPlayer player, @NotNull ServuxLitematicaPayload packet) {
if (packet.packetType.equals(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START)) {
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
buffer.writeVarInt(packet.getTransactionId());
buffer.writeNbt(packet.getCompound());
PacketSplitter.send(ServuxLitematicsProtocol::sendWithSplitter, buffer, player);
} else {
ProtocolUtils.sendPayloadPacket(player, packet);
}
}
private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) {
ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_DATA);
payload.buffer = buf;
payload.nbt = new CompoundTag();
encodeServerData(player, payload);
}
@ProtocolHandler.PlayerJoin
public static void onPlayerJoin(ServerPlayer player) {
sendMetaData(player);
}
@ProtocolHandler.PayloadReceiver(payload = ServuxLitematicaPayload.class)
public static void onPacketReceive(ServerPlayer player, ServuxLitematicaPayload payload) {
if (!hasPermission(player)) {
return;
}
switch (payload.packetType) {
case PACKET_C2S_METADATA_REQUEST -> sendMetaData(player);
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.getPos());
case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.getEntityId());
case PACKET_C2S_BULK_ENTITY_NBT_REQUEST ->
onBulkEntityRequest(player, payload.getChunkPos(), payload.getCompound());
case PACKET_C2S_NBT_RESPONSE_DATA -> {
ServuxProtocol.LOGGER.debug("nbt response data");
UUID uuid = player.getUUID();
Long session = playerSession.getOrDefault(uuid, new Random().nextLong());
playerSession.put(uuid, session);
FriendlyByteBuf fullPacket = PacketSplitter.receive(session, payload.getBuffer());
if (fullPacket == null) {
ServuxProtocol.LOGGER.debug("packet is none");
return;
}
playerSession.remove(uuid);
fullPacket.readVarInt();
Tag tag = FriendlyByteBuf.readNbt(fullPacket, new NbtAccounter(SurvuxProtocolConfig.litematicsMaxNbtSize == -1 ? Long.MAX_VALUE : SurvuxProtocolConfig.litematicsMaxNbtSize, 512));
if (!(tag instanceof CompoundTag)) {
ServuxProtocol.LOGGER.error("cannot read nbt tag from packet");
return;
}
handleClientPasteRequest(player, (CompoundTag) tag);
}
}
}
public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) {
if (!hasPermission(player)) {
return;
}
io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
player.level(),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()),
() -> {
BlockEntity be = player.level().getBlockEntity(pos);
CompoundTag tag = be != null ? be.saveWithFullMetadata(MinecraftServer.getServer().registryAccess()) : new CompoundTag();
ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE);
payload.pos = pos;
payload.nbt = tag;
encodeServerData(player, payload);
}
);
}
public static void onEntityRequest(ServerPlayer player, int entityId) {
if (!hasPermission(player)) {
return;
}
io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
player.level(),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()),
() -> {
Entity entity = player.level().getEntity(entityId);
if (entity == null) {
return;
}
CompoundTag tag = new CompoundTag();
ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE);
payload.entityId = entityId;
if (entity instanceof net.minecraft.world.entity.player.Player) {
ResourceLocation loc = EntityType.getKey(entity.getType());
tag = TagUtil.saveEntity(entity);
tag.putString("id", loc.toString());
payload.nbt = tag;
encodeServerData(player, payload);
} else if (TagUtil.saveEntityAsPassenger(entity, tag)) {
payload.nbt = tag;
encodeServerData(player, payload);
}
}
);
}
public static void onBulkEntityRequest(ServerPlayer player, ChunkPos chunkPos, CompoundTag req) {
if (req == null || req.isEmpty()) {
return;
}
ServerLevel world = player.level();
ChunkAccess chunk = world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.FULL, false);
if (chunk == null) {
return;
}
if (!req.contains("Task") || req.getStringOr("Task", "").equals("BulkEntityRequest")) {
ServuxProtocol.LOGGER.debug("litematic_data: Sending Bulk NBT Data for ChunkPos [{}] to player {}", chunkPos, player.getName().getString());
long timeStart = System.currentTimeMillis();
ListTag tileList = new ListTag();
ListTag entityList = new ListTag();
int minY = req.getIntOr("minY", -64);
int maxY = req.getIntOr("maxY", 319);
BlockPos pos1 = new BlockPos(chunkPos.getMinBlockX(), minY, chunkPos.getMinBlockZ());
BlockPos pos2 = new BlockPos(chunkPos.getMaxBlockX(), maxY, chunkPos.getMaxBlockZ());
AABB bb = AABB.encapsulatingFullBlocks(pos1, pos2);
Set<BlockPos> teSet = chunk.getBlockEntitiesPos();
List<Entity> entities = world.getEntitiesOfClass(Entity.class, bb, e -> !(e instanceof Player));
for (BlockPos tePos : teSet) {
if ((tePos.getX() < chunkPos.getMinBlockX() || tePos.getX() > chunkPos.getMaxBlockX()) ||
(tePos.getZ() < chunkPos.getMinBlockZ() || tePos.getZ() > chunkPos.getMaxBlockZ()) ||
(tePos.getY() < minY || tePos.getY() > maxY)) {
continue;
}
BlockEntity be = world.getBlockEntity(tePos);
CompoundTag beTag = TagUtil.saveTileWithId(be);
tileList.add(beTag);
}
for (Entity entity : entities) {
CompoundTag entTag = new CompoundTag();
if (TagUtil.saveEntity(entity, entTag)) {
Vec3 posVec = new Vec3(entity.getX() - pos1.getX(), entity.getY() - pos1.getY(), entity.getZ() - pos1.getZ());
NbtUtils.writeEntityPositionToTag(posVec, entTag);
entTag.putInt("entityId", entity.getId());
entityList.add(entTag);
}
}
CompoundTag output = new CompoundTag();
output.putString("Task", "BulkEntityReply");
output.put("TileEntities", tileList);
output.put("Entities", entityList);
output.putInt("chunkX", chunkPos.x);
output.putInt("chunkZ", chunkPos.z);
ServuxProtocol.LOGGER.debug("process bulk entity used: {}ms", System.currentTimeMillis() - timeStart);
ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START);
send.nbt.merge(output);
encodeServerData(player, send);
}
}
public static void handleClientPasteRequest(ServerPlayer player, @NotNull CompoundTag tags) {
if (!hasPermission(player)) {
player.getBukkitEntity().sendActionBar(Component.text("Insufficient Permissions for the Litematic paste operation", NamedTextColor.RED));
return;
}
if (!player.isCreative()) {
player.getBukkitEntity().sendActionBar(Component.text("Creative Mode is required for the Litematic paste operation", NamedTextColor.RED));
return;
}
if (tags.getStringOr("Task", "").equals("LitematicaPaste")) {
ServuxProtocol.LOGGER.debug("litematic_data: Servux Paste request from player {}", player.getName().getString());
ServerLevel serverLevel = player.level();
long timeStart = System.currentTimeMillis();
SchematicPlacement placement = SchematicPlacement.createFromNbt(tags);
ReplaceBehavior replaceMode = ReplaceBehavior.fromStringStatic(tags.getStringOr("ReplaceMode", ReplaceBehavior.NONE.name()));
io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
player.level(),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()),
ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()),
() -> {
placement.pasteTo(serverLevel, replaceMode);
long timeElapsed = System.currentTimeMillis() - timeStart;
player.getBukkitEntity().sendActionBar(
Component.text("Pasted ")
.append(Component.text(placement.getName(), NamedTextColor.AQUA))
.append(Component.text(" to world "))
.append(Component.text(serverLevel.serverLevelData.getLevelName(), NamedTextColor.LIGHT_PURPLE))
.append(Component.text(" in "))
.append(Component.text(timeElapsed, NamedTextColor.GREEN))
.append(Component.text("ms"))
);
});
}
}
@Override
public boolean isActive() {
return SurvuxProtocolConfig.litematicsEnabled;
}
public enum ServuxLitematicaPayloadType {
PACKET_S2C_METADATA(1),
PACKET_C2S_METADATA_REQUEST(2),
PACKET_C2S_BLOCK_ENTITY_REQUEST(3),
PACKET_C2S_ENTITY_REQUEST(4),
PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5),
PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6),
PACKET_C2S_BULK_ENTITY_NBT_REQUEST(7),
// For Packet Splitter (Oversize Packets, S2C)
PACKET_S2C_NBT_RESPONSE_START(10),
PACKET_S2C_NBT_RESPONSE_DATA(11),
// For Packet Splitter (Oversize Packets, C2S)
PACKET_C2S_NBT_RESPONSE_START(12),
PACKET_C2S_NBT_RESPONSE_DATA(13);
public final int type;
ServuxLitematicaPayloadType(int type) {
this.type = type;
Helper.ID_TO_TYPE.put(type, this);
}
public static ServuxLitematicaPayloadType fromId(int id) {
return Helper.ID_TO_TYPE.get(id);
}
private static final class Helper {
static Map<Integer, ServuxLitematicaPayloadType> ID_TO_TYPE = new HashMap<>();
}
}
public static class ServuxLitematicaPayload implements LeavesCustomPayload {
@ID
public static final ResourceLocation CHANNEL = ServuxProtocol.id("litematics");
@Codec
public static final StreamCodec<FriendlyByteBuf, ServuxLitematicaPayload> CODEC = StreamCodec.of(
(buf, payload) -> {
buf.writeVarInt(payload.packetType.type);
switch (payload.packetType) {
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> {
buf.writeVarInt(payload.transactionId);
buf.writeBlockPos(payload.pos);
}
case PACKET_C2S_ENTITY_REQUEST -> {
buf.writeVarInt(payload.transactionId);
buf.writeVarInt(payload.entityId);
}
case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> {
buf.writeBlockPos(payload.pos);
buf.writeNbt(payload.nbt);
}
case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> {
buf.writeVarInt(payload.entityId);
buf.writeNbt(payload.nbt);
}
case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> {
buf.writeChunkPos(payload.chunkPos);
buf.writeNbt(payload.nbt);
}
case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA ->
buf.writeBytes(payload.buffer.readBytes(payload.buffer.readableBytes()));
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> buf.writeNbt(payload.nbt);
default -> ServuxProtocol.LOGGER.error("ServuxLitematicaPacket#toPacket: Unknown packet type!");
}
},
buf -> {
ServuxLitematicaPayloadType type = ServuxLitematicaPayloadType.fromId(buf.readVarInt());
if (type == null) {
throw new IllegalStateException("invalid packet type received");
}
ServuxLitematicaPayload payload = new ServuxLitematicaPayload(type);
switch (type) {
case PACKET_C2S_BLOCK_ENTITY_REQUEST -> {
buf.readVarInt();
payload.pos = buf.readBlockPos().immutable();
}
case PACKET_C2S_ENTITY_REQUEST -> {
buf.readVarInt();
payload.entityId = buf.readVarInt();
}
case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> {
payload.pos = buf.readBlockPos().immutable();
payload.nbt = buf.readNbt();
}
case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> {
payload.entityId = buf.readVarInt();
payload.nbt = buf.readNbt();
}
case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> {
payload.chunkPos = buf.readChunkPos();
payload.nbt = buf.readNbt();
}
case PACKET_C2S_NBT_RESPONSE_DATA, PACKET_S2C_NBT_RESPONSE_DATA ->
payload.buffer = new FriendlyByteBuf(buf.readBytes(buf.readableBytes()));
case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> payload.nbt = buf.readNbt();
}
return payload;
}
);
public static final int PROTOCOL_VERSION = 1;
private final ServuxLitematicaPayloadType packetType;
private final int transactionId;
private int entityId;
private BlockPos pos;
private CompoundTag nbt;
private ChunkPos chunkPos;
private FriendlyByteBuf buffer;
private ServuxLitematicaPayload(ServuxLitematicaPayloadType type) {
this.packetType = type;
this.transactionId = -1;
this.entityId = -1;
this.pos = BlockPos.ZERO;
this.chunkPos = ChunkPos.ZERO;
this.nbt = new CompoundTag();
}
public int getVersion() {
return PROTOCOL_VERSION;
}
public int getTransactionId() {
return this.transactionId;
}
public int getEntityId() {
return this.entityId;
}
public BlockPos getPos() {
return this.pos;
}
public CompoundTag getCompound() {
return this.nbt;
}
public ChunkPos getChunkPos() {
return this.chunkPos;
}
public FriendlyByteBuf getBuffer() {
return this.buffer;
}
public boolean hasBuffer() {
return this.buffer != null && this.buffer.isReadable();
}
public boolean hasNbt() {
return this.nbt != null && !this.nbt.isEmpty();
}
public boolean isEmpty() {
return !this.hasBuffer() && !this.hasNbt();
}
}
}
@@ -0,0 +1,104 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics.container;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nullable;
import java.util.Objects;
public class LitematicaBitArray {
/**
* The long array that is used to store the data for this BitArray.
*/
private final long[] longArray;
/**
* Number of bits a single entry takes up
*/
private final int bitsPerEntry;
/**
* The maximum value for a single entry. This also works as a bitmask for a single entry.
* For instance, if bitsPerEntry were 5, this value would be 31 (ie, {@code 0b00011111}).
*/
private final long maxEntryValue;
/**
* Number of entries in this array (<b>not</b> the length of the long array that internally backs this array)
*/
private final long arraySize;
public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn) {
this(bitsPerEntryIn, arraySizeIn, null);
}
public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn, @Nullable long[] longArrayIn) {
Validate.inclusiveBetween(1L, 32L, bitsPerEntryIn);
this.arraySize = arraySizeIn;
this.bitsPerEntry = bitsPerEntryIn;
this.maxEntryValue = (1L << bitsPerEntryIn) - 1L;
this.longArray = Objects.requireNonNullElseGet(longArrayIn, () -> new long[(int) (roundUp(arraySizeIn * bitsPerEntryIn, 64L) / 64L)]);
}
public static long roundUp(long value, long interval) {
if (interval == 0L) {
return 0L;
} else if (value == 0L) {
return interval;
} else {
if (value < 0L) {
interval *= -1L;
}
long remainder = value % interval;
return remainder == 0L ? value : value + interval - remainder;
}
}
public void setAt(long index, int value) {
long startOffset = index * (long) this.bitsPerEntry;
int startArrIndex = (int) (startOffset >> 6); // startOffset / 64
int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6);
int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64
this.longArray[startArrIndex] = this.longArray[startArrIndex] & ~(this.maxEntryValue << startBitOffset) | ((long) value & this.maxEntryValue) << startBitOffset;
if (startArrIndex != endArrIndex) {
int endOffset = 64 - startBitOffset;
int j1 = this.bitsPerEntry - endOffset;
this.longArray[endArrIndex] = this.longArray[endArrIndex] >>> j1 << j1 | ((long) value & this.maxEntryValue) >> endOffset;
}
}
public int getAt(long index) {
long startOffset = index * (long) this.bitsPerEntry;
int startArrIndex = (int) (startOffset >> 6); // startOffset / 64
int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6);
int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64
if (startArrIndex == endArrIndex) {
return (int) (this.longArray[startArrIndex] >>> startBitOffset & this.maxEntryValue);
} else {
int endOffset = 64 - startBitOffset;
return (int) ((this.longArray[startArrIndex] >>> startBitOffset | this.longArray[endArrIndex] << endOffset) & this.maxEntryValue);
}
}
public long size() {
return this.arraySize;
}
}
@@ -0,0 +1,118 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics.container;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.ListTag;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
public class LitematicaBlockStateContainer {
public static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
protected final Vec3i size;
protected final int sizeX;
protected final int sizeY;
protected final int sizeZ;
protected final int sizeLayer;
protected final long totalVolume;
protected LitematicaBitArray storage;
protected LitematicaBlockStatePalette palette;
protected int bits;
public LitematicaBlockStateContainer(int sizeX, int sizeY, int sizeZ, int bits, @Nullable long[] backingLongArray) {
this.sizeX = sizeX;
this.sizeY = sizeY;
this.sizeZ = sizeZ;
this.sizeLayer = sizeX * sizeZ;
this.totalVolume = (long) this.sizeX * (long) this.sizeY * (long) this.sizeZ;
this.size = new Vec3i(this.sizeX, this.sizeY, this.sizeZ);
this.setBits(bits, backingLongArray);
}
public static LitematicaBlockStateContainer createFrom(ListTag palette, long[] blockStates, BlockPos size) {
int bits = Math.max(2, Integer.SIZE - Integer.numberOfLeadingZeros(palette.size() - 1));
LitematicaBlockStateContainer container = new LitematicaBlockStateContainer(size.getX(), size.getY(), size.getZ(), bits, blockStates);
container.palette.readFromNBT(palette);
return container;
}
public Vec3i getSize() {
return this.size;
}
public LitematicaBitArray getArray() {
return this.storage;
}
public BlockState get(int x, int y, int z) {
BlockState state = this.palette.getBlockState(this.storage.getAt(this.getIndex(x, y, z)));
return state == null ? AIR_BLOCK_STATE : state;
}
protected int getIndex(int x, int y, int z) {
return (y * this.sizeLayer) + z * this.sizeX + x;
}
protected void setBits(int bitsIn, @Nullable long[] backingLongArray) {
if (bitsIn != this.bits) {
this.bits = bitsIn;
if (this.bits <= 4) {
this.bits = Math.max(2, this.bits);
this.palette = new LitematicaBlockStatePaletteLinear(this.bits, this);
} else {
this.palette = new LitematicaBlockStatePaletteHashMap(this.bits, this);
}
this.palette.idFor(AIR_BLOCK_STATE);
if (backingLongArray != null) {
this.storage = new LitematicaBitArray(this.bits, this.totalVolume, backingLongArray);
} else {
this.storage = new LitematicaBitArray(this.bits, this.totalVolume);
}
}
}
public int onResize(int bits, BlockState state) {
LitematicaBitArray oldStorage = this.storage;
LitematicaBlockStatePalette oldPalette = this.palette;
final long storageLength = oldStorage.size();
this.setBits(bits, null);
LitematicaBitArray newStorage = this.storage;
for (long index = 0; index < storageLength; ++index) {
newStorage.setAt(index, oldStorage.getAt(index));
}
this.palette.readFromNBT(oldPalette.writeToNBT());
return this.palette.idFor(state);
}
public LitematicaBlockStatePalette getPalette() {
return this.palette;
}
}
@@ -0,0 +1,62 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics.container;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
public interface LitematicaBlockStatePalette {
/**
* Gets the palette id for the given block state and adds
* the state to the palette if it doesn't exist there yet.
*/
int idFor(BlockState state);
/**
* Gets the block state by the palette id.
*/
@Nullable
BlockState getBlockState(int indexKey);
void requestNewId(BlockState state);
default void readFromNBT(ListTag tagList) {
Registry<Block> lookup = MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BLOCK);
final int size = tagList.size();
for (int i = 0; i < size; ++i) {
CompoundTag tag = tagList.getCompound(i).orElseThrow();
BlockState state = NbtUtils.readBlockState(lookup, tag);
if (i > 0 || state != LitematicaBlockStateContainer.AIR_BLOCK_STATE) {
this.requestNewId(state);
}
}
}
ListTag writeToNBT();
}
@@ -0,0 +1,90 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics.container;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.state.BlockState;
import org.leavesmc.leaves.protocol.servux.litematics.utils.Int2ObjectBiMap;
import javax.annotation.Nullable;
public class LitematicaBlockStatePaletteHashMap implements LitematicaBlockStatePalette {
private final Int2ObjectBiMap<BlockState> statePaletteMap;
private final LitematicaBlockStateContainer blockStateContainer;
private final int bits;
public LitematicaBlockStatePaletteHashMap(int bitsIn, LitematicaBlockStateContainer blockStateContainer) {
this.bits = bitsIn;
this.blockStateContainer = blockStateContainer;
this.statePaletteMap = Int2ObjectBiMap.create(1 << bitsIn);
}
@Override
public int idFor(BlockState state) {
int i = this.statePaletteMap.getRawId(state);
if (i == -1) {
i = this.statePaletteMap.add(state);
if (i >= (1 << this.bits)) {
i = this.blockStateContainer.onResize(this.bits + 1, state);
}
}
return i;
}
@Override
@Nullable
public BlockState getBlockState(int indexKey) {
return this.statePaletteMap.get(indexKey);
}
public void requestNewId(BlockState state) {
final int origId = this.statePaletteMap.add(state);
if (origId >= (1 << this.bits)) {
int newId = this.blockStateContainer.onResize(this.bits + 1, LitematicaBlockStateContainer.AIR_BLOCK_STATE);
if (newId <= origId) {
this.statePaletteMap.add(state);
}
}
}
@Override
public ListTag writeToNBT() {
ListTag tagList = new ListTag();
for (int id = 0; id < this.statePaletteMap.size(); ++id) {
BlockState state = this.statePaletteMap.get(id);
if (state == null) {
state = LitematicaBlockStateContainer.AIR_BLOCK_STATE;
}
CompoundTag tag = NbtUtils.writeBlockState(state);
tagList.add(tag);
}
return tagList;
}
}
@@ -0,0 +1,91 @@
/*
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
*
* Leaves is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Leaves is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
*/
package org.leavesmc.leaves.protocol.servux.litematics.container;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
public class LitematicaBlockStatePaletteLinear implements LitematicaBlockStatePalette {
private final BlockState[] states;
private final LitematicaBlockStateContainer blockStateContainer;
private final int bits;
private int currentSize;
public LitematicaBlockStatePaletteLinear(int bitsIn, LitematicaBlockStateContainer blockStateContainer) {
this.states = new BlockState[1 << bitsIn];
this.bits = bitsIn;
this.blockStateContainer = blockStateContainer;
}
@Override
public int idFor(BlockState state) {
for (int i = 0; i < this.currentSize; ++i) {
if (this.states[i] == state) {
return i;
}
}
final int size = this.currentSize;
if (size < this.states.length) {
this.states[size] = state;
++this.currentSize;
return size;
} else {
return this.blockStateContainer.onResize(this.bits + 1, state);
}
}
@Override
@Nullable
public BlockState getBlockState(int indexKey) {
return indexKey >= 0 && indexKey < this.currentSize ? this.states[indexKey] : null;
}
public void requestNewId(BlockState state) {
final int size = this.currentSize;
if (size < this.states.length) {
this.states[size] = state;
++this.currentSize;
}
}
@Override
public ListTag writeToNBT() {
ListTag tagList = new ListTag();
for (int id = 0; id < this.currentSize; ++id) {
BlockState state = this.states[id];
if (state == null) {
state = LitematicaBlockStateContainer.AIR_BLOCK_STATE;
}
CompoundTag tag = NbtUtils.writeBlockState(state);
tagList.add(tag);
}
return tagList;
}
}

Some files were not shown because too many files have changed in this diff Show More