diff --git a/APPLY_PATCHES_GIT_128.md b/APPLY_PATCHES_GIT_128.md
new file mode 100644
index 0000000000..3d217fbe29
--- /dev/null
+++ b/APPLY_PATCHES_GIT_128.md
@@ -0,0 +1,90 @@
+# applyAllPatches 报错:git config exit 128
+
+## 现象
+
+执行 `.\gradlew.bat applyAllPatches` 时出现:
+
+```
+Execution failed for task ':applyPaperApiFilePatches'.
+> io.papermc.paperweight.PaperweightException: Command finished with 128 exit code:
+ git -c commit.gpgsign=false -c core.safecrlf=false config commit.gpgSign false
+```
+
+或后续任务(如 `applyPaperMinecraftResourcePatches`)报同样的 **git exit 128**。
+
+## 原因
+
+paperweight 在**临时/工作目录**里执行 `git config commit.gpgSign false`。
+在部分 Windows 环境下(例如该目录是 worktree、路径或权限异常),这条命令会以 **exit code 128** 失败,导致 apply 流程中断。
+
+**若项目所在磁盘是 exFAT:**
+exFAT 不支持 NTFS 的权限与部分文件语义,Git 在 exFAT 上写 `.git/config` 或做锁/重命名时更容易出问题,常表现为 exit 128。建议把项目放到 **NTFS** 分区再试。
+
+## 已尝试的缓解方式
+
+1. **全局关闭 GPG 签名**(建议仍做一次)
+ ```bat
+ git config --global commit.gpgsign false
+ ```
+ 不能完全避免,因为插件会在**子目录**里再次执行 `config`。
+
+2. **用 PATH 包装 git**
+ 项目里提供了 `git.bat` 和 `applyPatches-with-git-fix.bat`,用“假”的 `git` 跳过有问题的 `config` 命令。
+ 若 Gradle/paperweight 通过**完整路径**调用 `git.exe`,则不会用到该包装,仍会 128。
+
+3. **跳过会失败的任务**
+ 可先跳过 paper-api 的 apply,让后续步骤尽量跑下去(用于排查或临时用):
+ ```bat
+ .\gradlew.bat applyAllPatches -x applyPaperApiFilePatches -x applyPaperApiFeaturePatches
+ ```
+ 若再在 `applyPaperMinecraftResourcePatches` 等处报 128,说明所有“在子目录里跑 git config”的步骤都会受影响。
+
+## 建议做法
+
+1. **确认 Git 版本与安装路径**
+ - 在 PowerShell 里执行:`git --version`
+ - 确认 Git 来自官方安装(如 `C:\Program Files\Git\cmd\git.exe`),且无多版本冲突。
+
+2. **在“干净”环境下重试**
+ - 关闭杀毒/实时扫描对项目与 `.gradle` 目录的监控后再跑一次 `applyAllPatches`。
+ - 或把项目放在路径较短、无空格、无特殊字符的目录(如 `I:\folia`)再试。
+ - **若当前是 exFAT 盘:把整个项目复制到 NTFS 分区(如 `C:\` 或另一块 NTFS 盘)再执行 apply,往往能消除 128。**
+
+3. **向 PaperMC/paperweight 反馈**
+ - 在 [PaperMC/paperweight](https://github.com/PaperMC/paperweight/issues) 开 issue。
+ - 注明:Windows 版本、Git 版本、完整错误信息,以及是在 `applyPaperApiFilePatches` 还是 `applyPaperMinecraftResourcePatches`(或其它任务)上失败。
+
+4. **在 WSL / Linux 下执行 apply**
+ - 在 WSL 或 Linux 上 clone 同一项目,在那边执行 `./gradlew applyAllPatches`。
+ - 若在 Linux 下成功,可把生成的 `paper-api`、`paper-server` 等目录拷回 Windows 再继续用 Gradle 构建(若你的工作流允许这样做)。
+
+## 与本项目构建的关系
+
+- **folia-api** 已能单独构建:`.\gradlew.bat :folia-api:build`。
+- **folia-server** 依赖 `applyAllPatches`(及后续 Minecraft 相关任务)生成并打补丁的 **paper-server** 等源码;若 apply 因 git 128 未完成,`.\gradlew.bat build` 会缺这些源码而编译失败。
+- 解决或绕过上述 git 128 后,再成功跑完 `applyAllPatches`,即可进行完整构建。
+
+---
+
+## 推荐:一键修复脚本(git 128 + Photographer 补丁)
+
+项目提供 **`fix-and-apply-patches.ps1`**,可自动完成:
+
+1. 将项目目录加入 PATH,使 `git.bat` 包装生效,避免 **git exit 128**。
+2. 临时禁用三个 Photographer 相关补丁(0005 api、0008 server paper、0009 server minecraft),让 `applyAllPatches` 先完整跑通一次。
+3. 从生成的 `paper-api` 和 `paper-server` 中读取正确 blob 哈希,写回补丁中的 `index` 行(解决 **could not build fake ancestor**)。
+4. 重新启用补丁并再次执行 `applyAllPatches`。
+
+**用法(在项目根目录执行):**
+
+```powershell
+.\fix-and-apply-patches.ps1
+```
+
+若仅需绕过 git 128、不修 Photographer 补丁,可只使用:
+
+```bat
+.\applyPatches-with-git-fix.bat
+```
+
+(内部会加 `--no-configuration-cache`,避免复用错误环境。)
diff --git a/README.md b/README.md
index 9a38ec812e..1127f60abb 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,31 @@
Fork of Paper which adds regionised multithreading to the dedicated server.
+## Additions in this fork (Foliaphotographer)
+
+This fork adds a **Photographer API** on top of upstream Folia, so the server can record gameplay as **.mcpr** replays (playable with ReplayMod). A Folia-adapted plugin that uses this API: **[ISeeYou (Folia)](https://github.com/xiaofanforfabric/FoliaISEEYOU)**.
+
+### What was added / changed
+
+| Area | Description |
+|------|-------------|
+| **Photographer API** | New interfaces in folia-api: `PhotographerManager`, `Photographer`, `BukkitRecorderOption`. Plugins use `Server#getPhotographerManager()` to create and manage “photographer” entities that record .mcpr. Conceptually aligned with Leaves’ photographer API. |
+| **Server implementation** | In folia-server: `ServerPhotographer`, `Recorder`, `ReplayFile`, `CraftPhotographerManager`. A photographer is a fake player that follows a real player, captures network packets, writes them to a temp dir, and packs them into a .mcpr file when recording stops. |
+| **PlayerList extensions** | `PlayerList` gains `realPlayers`, `placeNewPhotographer()`, and `removePhotographer()`. Photographers do not consume real player slots; online count and list logic are separated from real players. |
+| **CraftServer / events** | CraftServer exposes `getPhotographerManager()`. CraftEntity maps `ServerPhotographer` to `CraftPhotographer`. Player events skip photographers so they do not trigger player-related logic. |
+| **Folia threading** | `ServerPhotographer#tick()` no longer calls `MinecraftServer.getTickCount()` (which throws on Folia). A local tick counter is used for throttling instead, avoiding `UnsupportedOperationException` and broken/corrupt recordings. |
+| **Patches & build** | Changes are applied via folia-api/folia-server paper-patches and minecraft-patches (e.g. 0005, 0008, 0009). Use `createMojmapPaperclipJar` to build a runnable server JAR with Photographer support. |
+
+### Usage and compatibility
+
+- **Recording .mcpr**: Plugins call `Bukkit.getServer().getPhotographerManager()` to create photographers, set the output path, and start/stop recording. Save path and “player join/leave” behaviour are plugin-defined.
+- **ISeeYou (Folia)**: [**FoliaISEEYOU**](https://github.com/xiaofanforfabric/FoliaISEEYOU) — Folia-only build of ISeeYou for this fork. It detects `getPhotographerManager()` and `dev.folia.replay.BukkitRecorderOption` via reflection. Without CommandAPI only commands are disabled; the plugin still enables.
+- **Runnable JAR**: Start the server with the JAR produced by `createMojmapPaperclipJar` (e.g. `folia-paperclip-*-mojmap.jar`). The plain `jar` task output is not runnable (missing dependencies).
+
+The sections below are the upstream Folia overview and documentation, unchanged.
+
+---
+
## Overview
Folia groups nearby loaded chunks to form an "independent region."
diff --git a/apply-debug.txt b/apply-debug.txt
new file mode 100644
index 0000000000..1177d3f554
--- /dev/null
+++ b/apply-debug.txt
@@ -0,0 +1,634 @@
+2026-02-20T14:36:50.732+0800 [INFO] [org.gradle.internal.nativeintegration.services.NativeServices] Initialized native services in: G:\Android\Gradle\native
+2026-02-20T14:36:50.806+0800 [INFO] [org.gradle.internal.nativeintegration.services.NativeServices] Initialized jansi services in: G:\Android\Gradle\native
+2026-02-20T14:36:50.824+0800 [LIFECYCLE] [org.gradle.launcher.cli.DebugLoggerWarningAction]
+#############################################################################
+ WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
+
+ Debug level logging will leak security sensitive information!
+
+ For more details, please refer to https://docs.gradle.org/8.12/userguide/logging.html#sec:debug_security in the Gradle documentation.
+#############################################################################
+
+2026-02-20T14:36:50.909+0800 [DEBUG] [org.gradle.internal.nativeintegration.services.NativeServices] Native-platform posix files integration is not available. Continuing with fallback.
+2026-02-20T14:36:51.068+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for metadata, path G:\Android\Gradle\caches\8.12\jvms\metadata.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@6676f6a0
+2026-02-20T14:36:51.322+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClient] Executing build d1203780-9c53-485f-bf28-a1823093121c in daemon client {pid=5704}
+2026-02-20T14:36:51.365+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VirtualBox Host-Only Ethernet Adapter-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.365+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.365+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VirtualBox Host-Only Ethernet Adapter-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.365+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.365+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VirtualBox Host-Only Ethernet Adapter-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP)-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.366+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Switch Extension Adapter-Hyper-V Virtual Switch Extension Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP)-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6)-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6)-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.367+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor)-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.368+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.369+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Kernel Debug Network Adapter
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (Network Monitor)
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VirtualBox Host-Only Ethernet Adapter
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:9165:4ad4:d7a0:8eff%ethernet_32770
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /169.254.175.115
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IP)
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IPv6)
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Bluetooth Device (Personal Area Network)
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek PCIe GbE Family Controller
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:8a65:401e:9cef:ce41%ethernet_32774
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:35b:b146:af00:6e21:11d9:4ecc:cb86
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:35b:b146:af00:25d4:6fa9:20a3:2b82
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /240e:35b:b146:af00:0:0:0:1
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /192.168.1.254
+2026-02-20T14:36:51.370+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Realtek USB FE Family Controller
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:6871:f0d5:73cc:7c11%ethernet_32775
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Android Composite USB Ethernet/RNDIS
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Android Composite USB Ethernet/RNDIS #2
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VMware Virtual Ethernet Adapter for VMnet1
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:7bee:356e:aaa8:ee61%ethernet_32778
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /169.254.255.61
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VMware Virtual Ethernet Adapter for VMnet8
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:d5cb:29bd:13db:cd4f%ethernet_32779
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /169.254.203.255
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface VMware Virtual Ethernet Adapter for VMnet0
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:ae07:1e1f:bbd4:4589%ethernet_32780
+2026-02-20T14:36:51.371+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /169.254.237.19
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Qualcomm Wireless HS-USB Ethernet Adapter 90DB
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Switch Extension Adapter
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Hyper-V Virtual Ethernet Adapter
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:e4a1:7cd9:f2a7:5906%ethernet_32783
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /172.20.64.1
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Bluetooth Device (Personal Area Network) #2
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.372+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface OrayBoxVpnEnt Virtual Ethernet Adapter
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Bluetooth Device (Personal Area Network) #3
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:cffb:b53f:e729:ea0b%ethernet_32786
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (PPPOE)
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface RAS Async Adapter
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Software Loopback Interface 1
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? true
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding loopback address /0:0:0:0:0:0:0:1
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding loopback address /127.0.0.1
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.373+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9
+2026-02-20T14:36:51.374+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface TAP-Windows Adapter V9 #2
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:fe1e:60aa:c268:68e0%iftype53_32769
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Tailscale Tunnel
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:d8ff:5060:f230:9168%iftype53_32770
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fd7a:115c:a1e0:0:0:0:ae01:1e37
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /100.89.30.53
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface OrayBoxVpnEnt Tunnel
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:e3f5:626f:ae5a:d4b4%iftype53_32771
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /172.16.2.116
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-Virtual WiFi Filter Driver-0000
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-Native WiFi Filter Driver-0000
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.375+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-Native WiFi Filter Driver-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-WFP Native MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-Native WiFi Filter Driver-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.376+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-VirtualBox NDIS Light-Weight Filter-0000
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-QoS Packet Scheduler-0000
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4-WFP 802.3 MAC Layer LightWeight Filter-0000
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface AIC88DC USB WiFi
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:963d:711b:6710:1c8f%wireless_32768
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #2
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #3
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:99a6:1de4:c312:5ef8%wireless_32771
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Wi-Fi Direct Virtual Adapter #4
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding remote address /fe80:0:0:0:c41f:de5a:bf39:852e%wireless_32772
+2026-02-20T14:36:51.377+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft Teredo Tunneling Adapter
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft IP-HTTPS Platform Adapter
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Microsoft 6to4 Adapter
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (IKEv2)
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (L2TP)
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (PPTP)
+2026-02-20T14:36:51.378+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface WAN Miniport (SSTP)
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Qualcomm HS-USB WWAN Adapter 9091
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Adding IP addresses for network interface Qualcomm HS-USB WWAN Adapter 90E5
+2026-02-20T14:36:51.379+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.InetAddresses] Is this a loopback interface? false
+2026-02-20T14:36:51.396+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
+2026-02-20T14:36:51.399+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
+2026-02-20T14:36:51.407+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
+2026-02-20T14:36:51.419+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.TcpOutgoingConnector] Attempting to connect to [fd2a2044-a2bc-4ec3-a071-50a227a3f35e port:59391, addresses:[/127.0.0.1]].
+2026-02-20T14:36:51.419+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.TcpOutgoingConnector] Trying to connect to address /127.0.0.1.
+2026-02-20T14:36:51.430+0800 [DEBUG] [org.gradle.internal.remote.internal.inet.TcpOutgoingConnector] Connected to address /127.0.0.1:59391.
+2026-02-20T14:36:51.536+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClient] Connected to daemon DaemonInfo{pid=12028, address=[fd2a2044-a2bc-4ec3-a071-50a227a3f35e port:59391, addresses:[/127.0.0.1]], state=Idle, lastBusy=1771569389538, context=DefaultDaemonContext[uid=c5e01d45-d3fa-45d5-8f31-fc23bb130328,javaHome=E:\MCreator20252\jdk,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=G:\Android\Gradle\daemon,pid=12028,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]}. Dispatching request Build{id=d1203780-9c53-485f-bf28-a1823093121c, currentDir=I:\ISEEYOU\Foliaphotographer}.
+2026-02-20T14:36:51.537+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClientConnection] thread 1: dispatching class org.gradle.launcher.daemon.protocol.Build
+2026-02-20T14:36:51.553+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClient] Received result org.gradle.launcher.daemon.protocol.BuildStarted@4e858e0a from daemon DaemonInfo{pid=12028, address=[fd2a2044-a2bc-4ec3-a071-50a227a3f35e port:59391, addresses:[/127.0.0.1]], state=Idle, lastBusy=1771569389538, context=DefaultDaemonContext[uid=c5e01d45-d3fa-45d5-8f31-fc23bb130328,javaHome=E:\MCreator20252\jdk,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=G:\Android\Gradle\daemon,pid=12028,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]} (build should be starting).
+2026-02-20T14:36:51.553+0800 [INFO] [org.gradle.launcher.daemon.server.exec.LogToClient] The client will now receive all logging from the daemon (pid: 12028). The daemon log file: G:\Android\Gradle\daemon\8.12\daemon-12028.out.log
+2026-02-20T14:36:51.554+0800 [INFO] [org.gradle.launcher.daemon.server.exec.LogAndCheckHealth] Starting 5th build in daemon [uptime: 2 mins 13.896 secs, performance: 100%, GC rate: 0.00/s, heap usage: 0% of 512 MiB, non-heap usage: 19% of 384 MiB]
+2026-02-20T14:36:51.554+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] The daemon has started executing the build.
+2026-02-20T14:36:51.555+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] Executing build with daemon context: DefaultDaemonContext[uid=c5e01d45-d3fa-45d5-8f31-fc23bb130328,javaHome=E:\MCreator20252\jdk,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=G:\Android\Gradle\daemon,pid=12028,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]
+2026-02-20T14:36:51.558+0800 [INFO] [org.gradle.internal.work.DefaultWorkerLeaseService] Using 8 worker leases.
+2026-02-20T14:36:51.558+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: acquired lock on worker lease
+2026-02-20T14:36:51.558+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run build' started
+2026-02-20T14:36:51.565+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for fileHashes, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes\fileHashes.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@442c581a
+2026-02-20T14:36:51.566+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Acquiring file lock for file hash cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes)
+2026-02-20T14:36:51.566+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on file hash cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes).
+2026-02-20T14:36:51.567+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on file hash cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes).
+2026-02-20T14:36:51.568+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for resourceHashesCache, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes\resourceHashesCache.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@442c581a
+2026-02-20T14:36:51.568+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for md5-checksums, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums\md5-checksums.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@796c8895
+2026-02-20T14:36:51.569+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Acquiring file lock for checksums cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums)
+2026-02-20T14:36:51.569+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on checksums cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums).
+2026-02-20T14:36:51.570+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on checksums cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums).
+2026-02-20T14:36:51.570+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for sha1-checksums, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums\sha1-checksums.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@796c8895
+2026-02-20T14:36:51.570+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for sha256-checksums, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums\sha256-checksums.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@796c8895
+2026-02-20T14:36:51.570+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for sha512-checksums, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums\sha512-checksums.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@796c8895
+2026-02-20T14:36:51.572+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for outputFiles, path I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup\outputFiles.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@5825b85e
+2026-02-20T14:36:51.572+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Acquiring file lock for Build Output Cleanup Cache (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup)
+2026-02-20T14:36:51.573+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on Build Output Cleanup Cache (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup).
+2026-02-20T14:36:51.573+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on Build Output Cleanup Cache (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup).
+2026-02-20T14:36:51.574+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Check configuration cache fingerprint' started
+2026-02-20T14:36:51.575+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on Gradle Configuration Cache keystore (G:\Android\Gradle\caches\8.12\cc-keystore).
+2026-02-20T14:36:51.575+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on Gradle Configuration Cache keystore (G:\Android\Gradle\caches\8.12\cc-keystore).
+2026-02-20T14:36:51.575+0800 [DEBUG] [org.gradle.internal.encryption.EncryptionService] Loading keystore from G:\Android\Gradle\caches\8.12\cc-keystore\gradle.keystore
+2026-02-20T14:36:51.605+0800 [DEBUG] [org.gradle.internal.encryption.EncryptionService] Retrieved key
+2026-02-20T14:36:51.605+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on Gradle Configuration Cache keystore (G:\Android\Gradle\caches\8.12\cc-keystore).
+2026-02-20T14:36:51.606+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Acquiring file lock for Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache)
+2026-02-20T14:36:51.606+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache).
+2026-02-20T14:36:51.607+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache).
+2026-02-20T14:36:51.609+0800 [DEBUG] [org.gradle.initialization.properties.DefaultProjectPropertiesLoader] Found env project properties: []
+2026-02-20T14:36:51.610+0800 [DEBUG] [org.gradle.initialization.properties.DefaultProjectPropertiesLoader] Found system project properties: []
+2026-02-20T14:36:51.615+0800 [INFO] [org.gradle.process.internal.DefaultExecHandle] Starting process 'command 'git''. Working directory: I:\ISEEYOU\Foliaphotographer Command: git --version
+2026-02-20T14:36:51.615+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTING
+2026-02-20T14:36:51.616+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Waiting until process started: command 'git'.
+2026-02-20T14:36:51.620+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTED
+2026-02-20T14:36:51.620+0800 [DEBUG] [org.gradle.process.internal.ExecHandleRunner] waiting until streams are handled...
+2026-02-20T14:36:51.620+0800 [INFO] [org.gradle.process.internal.DefaultExecHandle] Successfully started process 'command 'git''
+2026-02-20T14:36:51.675+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: SUCCEEDED
+2026-02-20T14:36:51.675+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Process 'command 'git'' finished with exit value 0 (state: SUCCEEDED)
+2026-02-20T14:36:51.677+0800 [INFO] [org.gradle.process.internal.DefaultExecHandle] Starting process 'command 'git''. Working directory: I:\ISEEYOU\Foliaphotographer Command: git --version
+2026-02-20T14:36:51.677+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTING
+2026-02-20T14:36:51.678+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Waiting until process started: command 'git'.
+2026-02-20T14:36:51.680+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTED
+2026-02-20T14:36:51.681+0800 [DEBUG] [org.gradle.process.internal.ExecHandleRunner] waiting until streams are handled...
+2026-02-20T14:36:51.681+0800 [INFO] [org.gradle.process.internal.DefaultExecHandle] Successfully started process 'command 'git''
+2026-02-20T14:36:51.724+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Changing state to: SUCCEEDED
+2026-02-20T14:36:51.724+0800 [DEBUG] [org.gradle.process.internal.DefaultExecHandle] Process 'command 'git'' finished with exit value 0 (state: SUCCEEDED)
+2026-02-20T14:36:51.727+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Check configuration cache fingerprint'
+2026-02-20T14:36:51.727+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Check configuration cache fingerprint' completed
+2026-02-20T14:36:51.727+0800 [LIFECYCLE] [org.gradle.configurationcache] Reusing configuration cache.
+2026-02-20T14:36:51.728+0800 [INFO] [org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner] Watching the file system is configured to be disabled
+2026-02-20T14:36:51.728+0800 [DEBUG] [org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner] Watching the file system computed to be disabled
+2026-02-20T14:36:51.729+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Build started for file system watching' started
+2026-02-20T14:36:51.729+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Build started for file system watching'
+2026-02-20T14:36:51.729+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Build started for file system watching' completed
+2026-02-20T14:36:51.729+0800 [INFO] [org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner] File system watching is inactive
+2026-02-20T14:36:51.729+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Load configuration cache state' started
+2026-02-20T14:36:51.732+0800 [DEBUG] [org.gradle.caching.configuration.internal.DefaultBuildCacheConfiguration] Found class org.gradle.caching.local.internal.DirectoryBuildCacheServiceFactory registered for class org.gradle.caching.local.DirectoryBuildCache
+2026-02-20T14:36:51.733+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Finalize build cache configuration' started
+2026-02-20T14:36:51.733+0800 [DEBUG] [org.gradle.caching.configuration.internal.DefaultBuildCacheConfiguration] Found class org.gradle.caching.local.internal.DirectoryBuildCacheServiceFactory registered for class org.gradle.caching.local.DirectoryBuildCache
+2026-02-20T14:36:51.733+0800 [INFO] [org.gradle.caching.internal.services.AbstractBuildCacheControllerFactory] Using local directory build cache for the root build (location = G:\Android\Gradle\caches\build-cache-1, remove unused entries = after 7 days).
+2026-02-20T14:36:51.734+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Finalize build cache configuration'
+2026-02-20T14:36:51.734+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Finalize build cache configuration' completed
+2026-02-20T14:36:51.738+0800 [DEBUG] [org.gradle.configurationcache] reading task graph in parallel
+2026-02-20T14:36:51.739+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Loading configuration for :' started
+2026-02-20T14:36:51.740+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :checkoutPaperRepo' started
+2026-02-20T14:36:51.741+0800 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Project : - Transitioning model element '' from state Registered to Created
+2026-02-20T14:36:51.741+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: acquired lock on state of project :
+2026-02-20T14:36:51.741+0800 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Project : - Transitioning model element '' to state Discovered.
+2026-02-20T14:36:51.741+0800 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Project : - Transitioning model element '' to state Created.
+2026-02-20T14:36:51.742+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: released lock on state of project :
+2026-02-20T14:36:51.742+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Realize task :checkoutPaperRepo'
+2026-02-20T14:36:51.742+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :checkoutPaperRepo' completed
+2026-02-20T14:36:51.744+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :filterPaperApiFromPaper' started
+2026-02-20T14:36:51.745+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Realize task :filterPaperApiFromPaper'
+2026-02-20T14:36:51.745+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :filterPaperApiFromPaper' completed
+2026-02-20T14:36:51.746+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :applyPaperApiFilePatches' started
+2026-02-20T14:36:51.746+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Realize task :applyPaperApiFilePatches'
+2026-02-20T14:36:51.746+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Realize task :applyPaperApiFilePatches' completed
+2026-02-20T14:36:51.747+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Loading configuration for :'
+2026-02-20T14:36:51.747+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Loading configuration for :' completed
+2026-02-20T14:36:51.749+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Calculate build tree task graph' started
+2026-02-20T14:36:51.749+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Calculate task graph' started
+2026-02-20T14:36:51.749+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Calculate task graph'
+2026-02-20T14:36:51.749+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Calculate task graph' completed
+2026-02-20T14:36:51.750+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: acquired lock on All projects of :
+2026-02-20T14:36:51.750+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Notify task graph whenReady listeners' started
+2026-02-20T14:36:51.750+0800 [INFO] [org.gradle.internal.buildevents.BuildLogger] Tasks to be executed: [task ':checkoutPaperRepo', task ':filterPaperApiFromPaper', task ':applyPaperApiFilePatches']
+2026-02-20T14:36:51.750+0800 [INFO] [org.gradle.internal.buildevents.BuildLogger] Tasks that were excluded: []
+2026-02-20T14:36:51.750+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Notify task graph whenReady listeners'
+2026-02-20T14:36:51.750+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Notify task graph whenReady listeners' completed
+2026-02-20T14:36:51.751+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: released lock on All projects of :
+2026-02-20T14:36:51.751+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Calculate build tree task graph'
+2026-02-20T14:36:51.751+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Calculate build tree task graph' completed
+2026-02-20T14:36:51.752+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Load configuration cache state'
+2026-02-20T14:36:51.752+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Load configuration cache state' completed
+2026-02-20T14:36:51.752+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run main tasks' started
+2026-02-20T14:36:51.752+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: released lock on worker lease
+2026-02-20T14:36:51.753+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: acquired lock on worker lease
+2026-02-20T14:36:51.753+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run tasks' started
+2026-02-20T14:36:51.753+0800 [DEBUG] [org.gradle.execution.plan.DefaultPlanExecutor] Using 8 parallel executor threads
+2026-02-20T14:36:51.753+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: acquired lock on worker lease
+2026-02-20T14:36:51.754+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] Resolve mutations for :checkoutPaperRepo (Thread[#204,Execution worker,5,main]) started.
+2026-02-20T14:36:51.754+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:51.754+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :checkoutPaperRepo' started
+2026-02-20T14:36:51.754+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:51.754+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:51.754+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Resolve mutations for task :checkoutPaperRepo'
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :checkoutPaperRepo' completed
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:51.755+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :checkoutPaperRepo completed, executed: true
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :checkoutPaperRepo finished executing
+2026-02-20T14:36:51.756+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] :checkoutPaperRepo (Thread[#204,Execution worker,5,main]) started.
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: released lock on worker lease
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Creating new cache for executionHistory, path I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory\executionHistory.bin, access org.gradle.cache.internal.DefaultCacheCoordinator@572dcc11
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:51.756+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Acquiring file lock for execution history cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory)
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on execution history cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory).
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:51.757+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on execution history cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory).
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :checkoutPaperRepo' started
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' started
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Identifying work'
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :checkoutPaperRepo' started
+2026-02-20T14:36:51.760+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Snapshot task inputs for :checkoutPaperRepo'
+2026-02-20T14:36:51.761+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Executing task ':checkoutPaperRepo'' started
+2026-02-20T14:36:51.761+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Execute run for :checkoutPaperRepo' started
+2026-02-20T14:36:51.759+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger]
+2026-02-20T14:36:51.759+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :checkoutPaperRepo
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Putting task artifact state for task ':checkoutPaperRepo' into context took 0.0 secs.
+2026-02-20T14:36:51.759+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' completed
+2026-02-20T14:36:51.760+0800 [INFO] [org.gradle.internal.execution.steps.AbstractResolveCachingStateStep] Caching disabled for task ':checkoutPaperRepo' because:
+ Task is untracked because: Git tracks the state
+2026-02-20T14:36:51.760+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :checkoutPaperRepo' completed
+2026-02-20T14:36:51.760+0800 [DEBUG] [org.gradle.internal.execution.steps.SkipUpToDateStep] Determining if task ':checkoutPaperRepo' is up-to-date
+2026-02-20T14:36:51.761+0800 [INFO] [org.gradle.internal.execution.steps.SkipUpToDateStep] Task ':checkoutPaperRepo' is not up-to-date because:
+ Task is untracked because: Git tracks the state
+2026-02-20T14:36:51.761+0800 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [I:\ISEEYOU\Foliaphotographer\.gradle\caches\paperweight\upstreams\paper]
+2026-02-20T14:36:51.761+0800 [DEBUG] [org.gradle.internal.execution.steps.PreCreateOutputParentsStep] Ensuring directory exists for property outputDir at I:\ISEEYOU\Foliaphotographer\.gradle\caches\paperweight\upstreams\paper
+2026-02-20T14:36:51.761+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.TaskExecution] Executing actions for task ':checkoutPaperRepo'.
+2026-02-20T14:36:54.167+0800 [LIFECYCLE] [org.gradle.internal.operations.DefaultBuildOperationRunner]
+2026-02-20T14:36:54.167+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Execute run for :checkoutPaperRepo'
+2026-02-20T14:36:54.167+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Executing task ':checkoutPaperRepo''
+2026-02-20T14:36:53.833+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger]
+2026-02-20T14:36:53.833+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :checkoutPaperRepo
+2026-02-20T14:36:54.167+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Execute run for :checkoutPaperRepo' completed
+2026-02-20T14:36:54.167+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Executing task ':checkoutPaperRepo'' completed
+2026-02-20T14:36:54.168+0800 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [I:\ISEEYOU\Foliaphotographer\.gradle\caches\paperweight\upstreams\paper]
+2026-02-20T14:36:54.168+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Removed task artifact state for task ':checkoutPaperRepo' from context.
+2026-02-20T14:36:54.168+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Task :checkoutPaperRepo'
+2026-02-20T14:36:54.168+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :checkoutPaperRepo' completed
+2026-02-20T14:36:54.169+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :checkoutPaperRepo completed, executed: true
+2026-02-20T14:36:54.169+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :checkoutPaperRepo finished executing
+2026-02-20T14:36:54.169+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: acquired lock on worker lease
+2026-02-20T14:36:54.169+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] Resolve mutations for :filterPaperApiFromPaper (Thread[#203,included builds,5,main]) started.
+2026-02-20T14:36:54.169+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: released lock on worker lease
+2026-02-20T14:36:54.169+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :filterPaperApiFromPaper' started
+2026-02-20T14:36:54.170+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.170+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.170+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.170+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.170+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Resolve mutations for task :filterPaperApiFromPaper'
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :filterPaperApiFromPaper' completed
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.171+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :filterPaperApiFromPaper completed, executed: true
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :filterPaperApiFromPaper finished executing
+2026-02-20T14:36:54.172+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: acquired lock on worker lease
+2026-02-20T14:36:54.173+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] :filterPaperApiFromPaper (Thread[#204,Execution worker,5,main]) started.
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: released lock on worker lease
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :filterPaperApiFromPaper' started
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' started
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Identifying work'
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :filterPaperApiFromPaper' started
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot inputs and outputs before executing task ':filterPaperApiFromPaper'' started
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.175+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.176+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.230+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Opening cache fileHashes.bin (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes\fileHashes.bin)
+2026-02-20T14:36:54.291+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Snapshot inputs and outputs before executing task ':filterPaperApiFromPaper''
+2026-02-20T14:36:54.293+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Snapshot task inputs for :filterPaperApiFromPaper'
+2026-02-20T14:36:54.294+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Opening cache outputFiles.bin (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup\outputFiles.bin)
+2026-02-20T14:36:54.173+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger]
+2026-02-20T14:36:54.173+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :filterPaperApiFromPaper UP-TO-DATE
+2026-02-20T14:36:54.173+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Putting task artifact state for task ':filterPaperApiFromPaper' into context took 0.0 secs.
+2026-02-20T14:36:54.174+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' completed
+2026-02-20T14:36:54.227+0800 [DEBUG] [org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep] Implementation for task ':filterPaperApiFromPaper': io.papermc.paperweight.core.tasks.FilterRepo_Decorated@ca19b813d2471bedecc2e55a89618dc4
+2026-02-20T14:36:54.227+0800 [DEBUG] [org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep] Additional implementations for task ':filterPaperApiFromPaper': [io.papermc.paperweight.core.tasks.FilterRepo_Decorated@ca19b813d2471bedecc2e55a89618dc4]
+2026-02-20T14:36:54.291+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot inputs and outputs before executing task ':filterPaperApiFromPaper'' completed
+2026-02-20T14:36:54.293+0800 [INFO] [org.gradle.internal.execution.steps.AbstractResolveCachingStateStep] Caching disabled for task ':filterPaperApiFromPaper' because:
+ Caching has not been enabled for the task
+2026-02-20T14:36:54.293+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :filterPaperApiFromPaper' completed
+2026-02-20T14:36:54.294+0800 [DEBUG] [org.gradle.internal.execution.steps.SkipUpToDateStep] Determining if task ':filterPaperApiFromPaper' is up-to-date
+2026-02-20T14:36:54.294+0800 [INFO] [org.gradle.internal.execution.steps.SkipUpToDateStep] Skipping task ':filterPaperApiFromPaper' as it is up-to-date.
+2026-02-20T14:36:54.294+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Removed task artifact state for task ':filterPaperApiFromPaper' from context.
+2026-02-20T14:36:54.294+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Task :filterPaperApiFromPaper'
+2026-02-20T14:36:54.295+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :filterPaperApiFromPaper' completed
+2026-02-20T14:36:54.295+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :filterPaperApiFromPaper completed, executed: true
+2026-02-20T14:36:54.295+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :filterPaperApiFromPaper finished executing
+2026-02-20T14:36:54.295+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: acquired lock on worker lease
+2026-02-20T14:36:54.295+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] Resolve mutations for :applyPaperApiFilePatches (Thread[#203,included builds,5,main]) started.
+2026-02-20T14:36:54.295+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: released lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :applyPaperApiFilePatches' started
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.296+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Resolve mutations for task :applyPaperApiFilePatches'
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Resolve mutations for task :applyPaperApiFilePatches' completed
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.297+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.298+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.298+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :applyPaperApiFilePatches completed, executed: true
+2026-02-20T14:36:54.298+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node Resolve mutations for :applyPaperApiFilePatches finished executing
+2026-02-20T14:36:54.298+0800 [INFO] [org.gradle.execution.plan.DefaultPlanExecutor] :applyPaperApiFilePatches (Thread[#203,included builds,5,main]) started.
+2026-02-20T14:36:54.299+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :applyPaperApiFilePatches' started
+2026-02-20T14:36:54.299+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' started
+2026-02-20T14:36:54.299+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Identifying work'
+2026-02-20T14:36:54.300+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :applyPaperApiFilePatches' started
+2026-02-20T14:36:54.302+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: acquired lock on worker lease
+2026-02-20T14:36:54.302+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: released lock on worker lease
+2026-02-20T14:36:54.302+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Snapshot task inputs for :applyPaperApiFilePatches'
+2026-02-20T14:36:54.302+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.303+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Executing task ':applyPaperApiFilePatches'' started
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.305+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.305+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.305+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Execute run for :applyPaperApiFilePatches' started
+2026-02-20T14:36:54.305+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.306+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.306+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.306+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.909+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Execute run for :applyPaperApiFilePatches'
+2026-02-20T14:36:54.910+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Executing task ':applyPaperApiFilePatches''
+2026-02-20T14:36:54.299+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger]
+2026-02-20T14:36:54.299+0800 [LIFECYCLE] [class org.gradle.internal.buildevents.TaskExecutionLogger] > Task :applyPaperApiFilePatches FAILED
+2026-02-20T14:36:54.299+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Putting task artifact state for task ':applyPaperApiFilePatches' into context took 0.0 secs.
+2026-02-20T14:36:54.300+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Identifying work' completed
+2026-02-20T14:36:54.302+0800 [INFO] [org.gradle.internal.execution.steps.AbstractResolveCachingStateStep] Caching disabled for task ':applyPaperApiFilePatches' because:
+ Task is untracked because: Always run when requested
+2026-02-20T14:36:54.302+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :applyPaperApiFilePatches' completed
+2026-02-20T14:36:54.303+0800 [DEBUG] [org.gradle.internal.execution.steps.SkipUpToDateStep] Determining if task ':applyPaperApiFilePatches' is up-to-date
+2026-02-20T14:36:54.303+0800 [INFO] [org.gradle.internal.execution.steps.SkipUpToDateStep] Task ':applyPaperApiFilePatches' is not up-to-date because:
+ Task is untracked because: Always run when requested
+2026-02-20T14:36:54.303+0800 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [I:\ISEEYOU\Foliaphotographer\paper-api]
+2026-02-20T14:36:54.304+0800 [DEBUG] [org.gradle.internal.execution.steps.PreCreateOutputParentsStep] Ensuring directory exists for property output at I:\ISEEYOU\Foliaphotographer\paper-api
+2026-02-20T14:36:54.305+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.TaskExecution] Executing actions for task ':applyPaperApiFilePatches'.
+2026-02-20T14:36:54.910+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Execute run for :applyPaperApiFilePatches' completed
+2026-02-20T14:36:54.910+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Executing task ':applyPaperApiFilePatches'' completed
+2026-02-20T14:36:54.910+0800 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [I:\ISEEYOU\Foliaphotographer\paper-api]
+2026-02-20T14:36:54.911+0800 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Removed task artifact state for task ':applyPaperApiFilePatches' from context.
+2026-02-20T14:36:54.911+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Task :applyPaperApiFilePatches'
+2026-02-20T14:36:54.911+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Task :applyPaperApiFilePatches' completed
+2026-02-20T14:36:54.911+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :applyPaperApiFilePatches completed, executed: true
+2026-02-20T14:36:54.911+0800 [DEBUG] [org.gradle.execution.plan.DefaultFinalizedExecutionPlan] Node :applyPaperApiFilePatches failed
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: acquired lock on worker lease
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: released lock on worker lease
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.912+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.913+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Run tasks'
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run tasks' completed
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] included builds: released lock on worker lease
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: acquired lock on worker lease
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker: released lock on worker lease
+2026-02-20T14:36:54.914+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: acquired lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: acquired lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Run main tasks'
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 5: released lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run main tasks' completed
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: acquired lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 3: released lock on worker lease
+2026-02-20T14:36:54.915+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: acquired lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 6: released lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: acquired lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 7: released lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: acquired lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 2: released lock on worker lease
+2026-02-20T14:36:54.916+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Finish root build tree' started
+2026-02-20T14:36:54.919+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Finish root build tree'
+2026-02-20T14:36:54.919+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Finish root build tree' completed
+.\gradlew.bat : 2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]
+所在位置 G:\Temp\ps-script-a543f8f3-568b-4412-a643-0e6a36ca70d9.ps1:91 字符: 44
++ ... otographer; .\gradlew.bat applyPaperApiFilePatches --debug 2>&1 | Out ...
++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ + CategoryInfo : NotSpecified: (2026-02-20T14:3...ptionReporter] :String) [], RemoteException
+ + FullyQualifiedErrorId : NativeCommandError
+
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] FAILURE: Build failed
+with an exception.
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * What went wrong:
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Execution failed for
+task ':applyPaperApiFilePatches'.
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] >
+io.papermc.paperweight.PaperweightException: Command finished with 128 exit code: git -c commit.gpgsign=false -c
+core.safecrlf=false config commit.gpgSign false
+2026-02-20T14:36:54.920+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * Try:
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Run with --stacktrace
+option to get the stack trace.
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Run with --scan to get
+full insights.
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Get more help at
+https://help.gradle.org.
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildResultLogger]
+2026-02-20T14:36:54.921+0800 [ERROR] [org.gradle.internal.buildevents.BuildResultLogger] BUILD FAILED in 5s
+2026-02-20T14:36:54.921+0800 [LIFECYCLE] [org.gradle.internal.buildevents.TaskExecutionStatisticsReporter] 3 actionable tasks: 2 executed, 1 up-to-date
+2026-02-20T14:36:54.921+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Build finished for file system watching' started
+2026-02-20T14:36:54.921+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Build finished for file system watching'
+2026-02-20T14:36:54.921+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Build finished for file system watching' completed
+2026-02-20T14:36:54.922+0800 [WARN] [org.gradle.internal.cc.impl.problems.ConfigurationCacheProblems] Configuration cache entry reused.
+2026-02-20T14:36:54.923+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for Build Output Cleanup Cache (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup)
+2026-02-20T14:36:54.923+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache outputFiles.bin (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup\outputFiles.bin)
+2026-02-20T14:36:54.924+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on Build Output Cleanup Cache (I:\ISEEYOU\Foliaphotographer\.gradle\buildOutputCleanup).
+2026-02-20T14:36:54.925+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for execution history cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory)
+2026-02-20T14:36:54.925+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on execution history cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\executionHistory).
+2026-02-20T14:36:54.927+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCleanupExecutor] Build cache (G:\Android\Gradle\caches\build-cache-1) has last been fully cleaned up 2 hours ago
+2026-02-20T14:36:54.927+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCleanupExecutor] Skipping cleanup for Build cache (G:\Android\Gradle\caches\build-cache-1) as it is not yet due
+2026-02-20T14:36:54.927+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache Build cache (G:\Android\Gradle\caches\build-cache-1) was closed 0 times.
+2026-02-20T14:36:54.929+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Run build'
+2026-02-20T14:36:54.929+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Run build' completed
+2026-02-20T14:36:54.930+0800 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Daemon worker: released lock on worker lease
+2026-02-20T14:36:54.936+0800 [DEBUG] [org.gradle.deployment.internal.DefaultDeploymentRegistry] Stopping 0 deployment handles
+2026-02-20T14:36:54.937+0800 [DEBUG] [org.gradle.deployment.internal.DefaultDeploymentRegistry] Stopped deployment handles
+2026-02-20T14:36:54.937+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache)
+2026-02-20T14:36:54.937+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache).
+2026-02-20T14:36:54.937+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCleanupExecutor] Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache) has last been fully cleaned up 2 hours ago
+2026-02-20T14:36:54.938+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCleanupExecutor] Skipping cleanup for Configuration Cache (I:\ISEEYOU\Foliaphotographer\.gradle\configuration-cache) as it is not yet due
+2026-02-20T14:36:54.938+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for file hash cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes)
+2026-02-20T14:36:54.939+0800 [DEBUG] [org.gradle.cache.internal.btree.BTreePersistentIndexedCache] Closing cache fileHashes.bin (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes\fileHashes.bin)
+2026-02-20T14:36:54.939+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on file hash cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\fileHashes).
+2026-02-20T14:36:54.940+0800 [DEBUG] [org.gradle.cache.internal.LockOnDemandCrossProcessCacheAccess] Releasing file lock for checksums cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums)
+2026-02-20T14:36:54.940+0800 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on checksums cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\checksums).
+2026-02-20T14:36:54.941+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache Compressed Files Expansion Cache (I:\ISEEYOU\Foliaphotographer\.gradle\8.12\expanded) was closed 0 times.
+2026-02-20T14:36:54.941+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused version-specific caches in I:\ISEEYOU\Foliaphotographer\.gradle' started
+2026-02-20T14:36:54.941+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Deleting unused version-specific caches in I:\ISEEYOU\Foliaphotographer\.gradle'
+2026-02-20T14:36:54.942+0800 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Deleting unused version-specific caches in I:\ISEEYOU\Foliaphotographer\.gradle' completed
+2026-02-20T14:36:54.946+0800 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] The daemon has finished executing the build.
+2026-02-20T14:36:55.100+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClientConnection] thread 1: dispatching class org.gradle.launcher.daemon.protocol.CloseInput
+2026-02-20T14:36:55.111+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClient] Received result Success[value=org.gradle.launcher.exec.BuildActionResult@34997338] from daemon DaemonInfo{pid=12028, address=[fd2a2044-a2bc-4ec3-a071-50a227a3f35e port:59391, addresses:[/127.0.0.1]], state=Idle, lastBusy=1771569389538, context=DefaultDaemonContext[uid=c5e01d45-d3fa-45d5-8f31-fc23bb130328,javaHome=E:\MCreator20252\jdk,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=G:\Android\Gradle\daemon,pid=12028,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=384m,-XX:+HeapDumpOnOutOfMemoryError,-Xms256m,-Xmx512m,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]} (build should be done).
+2026-02-20T14:36:55.111+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClientConnection] thread 1: dispatching class org.gradle.launcher.daemon.protocol.Finished
+2026-02-20T14:36:55.111+0800 [DEBUG] [org.gradle.launcher.daemon.client.DaemonClientConnection] thread 1: connection stop
+2026-02-20T14:36:55.116+0800 [DEBUG] [org.gradle.cache.internal.DefaultCacheCoordinator] Cache cache directory jvms (G:\Android\Gradle\caches\8.12\jvms) was closed 0 times.
+2026-02-20T14:36:55.116+0800 [LIFECYCLE] [org.gradle.launcher.cli.DebugLoggerWarningAction]
+#############################################################################
+ WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
+
+ Debug level logging will leak security sensitive information!
+
+ For more details, please refer to https://docs.gradle.org/8.12/userguide/logging.html#sec:debug_security in the Gradle documentation.
+#############################################################################
+
diff --git a/applyPatches-with-git-fix.bat b/applyPatches-with-git-fix.bat
new file mode 100644
index 0000000000..1cff624b29
--- /dev/null
+++ b/applyPatches-with-git-fix.bat
@@ -0,0 +1,8 @@
+@echo off
+REM Workaround: paperweight runs "git config commit.gpgSign false" in temp dirs and gets exit 128 on some Windows setups.
+REM Use a wrapper that skips that command (global is already false) so applyAllPatches can continue.
+REM --no-configuration-cache ensures a fresh run so the wrapper git is used.
+set "SCRIPTDIR=%~dp0"
+set "PATH=%SCRIPTDIR%;%PATH%"
+call "%SCRIPTDIR%gradlew.bat" applyAllPatches --no-configuration-cache %*
+exit /b %errorlevel%
diff --git a/fix-and-apply-patches.bat b/fix-and-apply-patches.bat
new file mode 100644
index 0000000000..8b2f6ad4c1
--- /dev/null
+++ b/fix-and-apply-patches.bat
@@ -0,0 +1,5 @@
+@echo off
+REM 绕过 PowerShell 执行策略,运行修复并 apply 补丁的脚本
+set "SCRIPTDIR=%~dp0"
+powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPTDIR%fix-and-apply-patches.ps1" %*
+exit /b %errorlevel%
diff --git a/fix-and-apply-patches.ps1 b/fix-and-apply-patches.ps1
new file mode 100644
index 0000000000..ec81a1d5a4
--- /dev/null
+++ b/fix-and-apply-patches.ps1
@@ -0,0 +1,150 @@
+# Fix git 128 + Photographer patches (0005, 0008, 0009).
+# Step 1: PATH so git.bat wrapper is used (avoids exit 128).
+# Step 2: Disable 0005, 0008, 0009 so apply completes.
+# Step 3: Get blob hashes, update patch files, re-enable patches.
+# Step 4: Apply 0005/0008/0009 directly with "git apply" to paper-api and paper-server
+# (avoids second applyAllPatches which often fails with "corrupt patch").
+
+$ErrorActionPreference = "Stop"
+$root = $PSScriptRoot
+$apiPatchDir = Join-Path $root "folia-api\paper-patches\features"
+$serverPaperPatchDir = Join-Path $root "folia-server\paper-patches\features"
+$serverMcPatchDir = Join-Path $root "folia-server\minecraft-patches\features"
+$patch5 = Join-Path $apiPatchDir "0005-Photographer-API.patch"
+$bak5 = Join-Path $apiPatchDir "0005-Photographer-API.patch.bak"
+$patch8 = Join-Path $serverPaperPatchDir "0008-Photographer-API.patch"
+$bak8 = Join-Path $serverPaperPatchDir "0008-Photographer-API.patch.bak"
+$patch9 = Join-Path $serverMcPatchDir "0009-Photographer-API.patch"
+$bak9 = Join-Path $serverMcPatchDir "0009-Photographer-API.patch.bak"
+$paperApi = Join-Path $root "paper-api"
+$paperServer = Join-Path $root "paper-server"
+$newBlob = "0000000000000000000000000000000000000001"
+
+# Use wrapper git so "git config commit.gpgSign false" is skipped
+$env:PATH = "$root;$env:PATH"
+
+# Disable all three Photographer patches
+if (Test-Path $patch5) { Copy-Item $patch5 $bak5 -Force; Remove-Item $patch5 -Force; Write-Host "[1a] 0005 disabled" }
+if (Test-Path $patch8) { Copy-Item $patch8 $bak8 -Force; Remove-Item $patch8 -Force; Write-Host "[1b] 0008 disabled" }
+if (Test-Path $patch9) { Copy-Item $patch9 $bak9 -Force; Remove-Item $patch9 -Force; Write-Host "[1c] 0009 disabled" }
+
+# Run apply
+Write-Host "[2] Running applyAllPatches..."
+& (Join-Path $root "gradlew.bat") applyAllPatches --no-configuration-cache --no-daemon
+if ($LASTEXITCODE -ne 0) {
+ if (Test-Path $bak5) { Move-Item $bak5 $patch5 -Force }
+ if (Test-Path $bak8) { Move-Item $bak8 $patch8 -Force }
+ if (Test-Path $bak9) { Move-Item $bak9 $patch9 -Force }
+ throw "applyAllPatches failed. Restored 0005, 0008, 0009."
+}
+
+# --- Fix 0005 (paper-api) ---
+if (-not (Test-Path $paperApi)) { throw "paper-api not found" }
+Push-Location $paperApi
+$bukkitBlob = (git hash-object "src/main/java/org/bukkit/Bukkit.java").Trim()
+$serverBlob = (git hash-object "src/main/java/org/bukkit/Server.java").Trim()
+Pop-Location
+Write-Host "[3] API blobs: Bukkit=$bukkitBlob Server=$serverBlob"
+
+$content5 = Get-Content $bak5 -Raw -Encoding UTF8
+$content5 = $content5 -replace "(diff --git a/src/main/java/org/bukkit/Bukkit\.java b/src/main/java/org/bukkit/Bukkit\.java)\r?\n(--- a/)", "`$1`nindex ${bukkitBlob}..${newBlob} 100644`n`$2"
+$content5 = $content5 -replace "(diff --git a/src/main/java/org/bukkit/Server\.java b/src/main/java/org/bukkit/Server\.java)\r?\n(--- a/)", "`$1`nindex ${serverBlob}..${newBlob} 100644`n`$2"
+$content5 = $content5 -replace "`r`n", "`n"
+[System.IO.File]::WriteAllText($patch5, $content5, [System.Text.UTF8Encoding]::new($false))
+Remove-Item $bak5 -Force
+Write-Host "[4a] 0005 updated and re-enabled"
+
+# --- Fix 0008 (paper-server / craftbukkit) ---
+$serverPaperFiles = @(
+ "src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java",
+ "src/main/java/org/bukkit/craftbukkit/CraftServer.java",
+ "src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java",
+ "src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java"
+)
+Push-Location $paperServer
+$hashes8 = @{}
+foreach ($f in $serverPaperFiles) {
+ if (Test-Path $f) { $hashes8[$f] = (git hash-object $f).Trim() }
+}
+Pop-Location
+
+$content8 = Get-Content $bak8 -Raw -Encoding UTF8
+foreach ($f in $serverPaperFiles) {
+ $hash = $hashes8[$f]
+ if (-not $hash) { continue }
+ $escaped = [regex]::Escape($f) -replace "/", "\/"
+ $pattern = "(diff --git a/${escaped} b/${escaped})\r?\nindex [0-9a-f]+\.\.[0-9a-f]+ 100644\r?\n(--- a/)"
+ $content8 = $content8 -replace $pattern, "`$1`nindex ${hash}..${newBlob} 100644`n`$2"
+}
+$content8 = $content8 -replace "`r`n", "`n"
+[System.IO.File]::WriteAllText($patch8, $content8, [System.Text.UTF8Encoding]::new($false))
+Remove-Item $bak8 -Force
+Write-Host "[4b] 0008 updated and re-enabled"
+
+# --- Fix 0009 (folia-server src/minecraft - minecraft source lives here) ---
+# Patch uses path "net/minecraft/..."; minecraft source is in folia-server/src/minecraft/java/net/minecraft/...
+$mcPatchPaths = @(
+ "net/minecraft/commands/CommandSourceStack.java",
+ "net/minecraft/commands/arguments/selector/EntitySelector.java",
+ "net/minecraft/server/MinecraftServer.java",
+ "net/minecraft/server/PlayerAdvancements.java",
+ "net/minecraft/server/commands/OpCommand.java",
+ "net/minecraft/server/level/ServerLevel.java",
+ "net/minecraft/server/players/PlayerList.java"
+)
+$mcDir = Join-Path $root "folia-server\src\minecraft\java"
+if (-not (Test-Path $mcDir)) { throw "folia-server src/minecraft/java not found" }
+Push-Location $mcDir
+$hashes = @{}
+foreach ($p in $mcPatchPaths) {
+ if (Test-Path $p) { $hashes[$p] = (git hash-object $p).Trim() }
+}
+Pop-Location
+
+$content9 = Get-Content $bak9 -Raw -Encoding UTF8
+foreach ($p in $mcPatchPaths) {
+ $hash = $hashes[$p]
+ if (-not $hash) { continue }
+ $escaped = [regex]::Escape($p) -replace "/", "\/"
+ $pattern = "(diff --git a/${escaped} b/${escaped})\r?\nindex [0-9a-f]+\.\.[0-9a-f]+ 100644\r?\n(--- a/)"
+ $replacement = "`$1`nindex ${hash}..${newBlob} 100644`n`$2"
+ $content9 = $content9 -replace $pattern, $replacement
+}
+$content9 = $content9 -replace "`r`n", "`n"
+[System.IO.File]::WriteAllText($patch9, $content9, [System.Text.UTF8Encoding]::new($false))
+Remove-Item $bak9 -Force
+Write-Host "[4c] 0009 updated and re-enabled"
+
+# Step 5: Apply the three patches with "git apply" (no second applyAllPatches)
+# Mailbox format: strip header before first "diff --git", then apply as unified diff
+function Apply-MailboxPatch {
+ param([string]$PatchPath, [string]$WorkDir, [int]$Strip = 1)
+ $full = [System.IO.File]::ReadAllText($PatchPath)
+ $idx = $full.IndexOf("diff --git")
+ if ($idx -lt 0) { throw "No diff in patch: $PatchPath" }
+ $diffOnly = $full.Substring($idx)
+ $diffOnly = $diffOnly -replace "`r`n", "`n"
+ # Remove "index ..." lines so git apply uses context only
+ $diffOnly = $diffOnly -replace "(?m)^index [0-9a-f]+\.\.[0-9a-f]+ 100644\r?\n", ""
+ $tmp = [System.IO.Path]::GetTempFileName()
+ [System.IO.File]::WriteAllText($tmp, $diffOnly, [System.Text.UTF8Encoding]::new($false))
+ try {
+ Push-Location $WorkDir
+ & git apply -p $Strip --ignore-whitespace $tmp
+ if ($LASTEXITCODE -ne 0) { throw "git apply failed in $WorkDir for $PatchPath" }
+ } finally {
+ Pop-Location
+ Remove-Item $tmp -Force -ErrorAction SilentlyContinue
+ }
+}
+
+Write-Host "[5] Applying 0005 to paper-api (git apply)..."
+Apply-MailboxPatch -PatchPath $patch5 -WorkDir $paperApi -Strip 1
+Write-Host "[6] Applying 0008 to paper-server (git apply)..."
+Apply-MailboxPatch -PatchPath $patch8 -WorkDir $paperServer -Strip 1
+Write-Host "[7] Applying 0009 to folia-server src/minecraft/java (git apply)..."
+$mcDir = Join-Path $root "folia-server\src\minecraft\java"
+if (-not (Test-Path $mcDir)) { throw "folia-server src/minecraft/java not found (run applyAllPatches first)" }
+Apply-MailboxPatch -PatchPath $patch9 -WorkDir $mcDir -Strip 1
+
+Write-Host "Done. Photographer patches applied. You can run: .\gradlew.bat build"
diff --git a/folia-api/bin/main/dev/folia/entity/Photographer.class b/folia-api/bin/main/dev/folia/entity/Photographer.class
new file mode 100644
index 0000000000..3d7df6d5cc
Binary files /dev/null and b/folia-api/bin/main/dev/folia/entity/Photographer.class differ
diff --git a/folia-api/bin/main/dev/folia/entity/PhotographerManager.class b/folia-api/bin/main/dev/folia/entity/PhotographerManager.class
new file mode 100644
index 0000000000..d5871c1618
Binary files /dev/null and b/folia-api/bin/main/dev/folia/entity/PhotographerManager.class differ
diff --git a/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class b/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class
new file mode 100644
index 0000000000..32e214062b
Binary files /dev/null and b/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class differ
diff --git a/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption.class b/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption.class
new file mode 100644
index 0000000000..75edca8648
Binary files /dev/null and b/folia-api/bin/main/dev/folia/replay/BukkitRecorderOption.class differ
diff --git a/folia-api/bin/main/io/papermc/paper/threadedregions/RegionizedServerInitEvent.class b/folia-api/bin/main/io/papermc/paper/threadedregions/RegionizedServerInitEvent.class
new file mode 100644
index 0000000000..a2f10b9d9a
Binary files /dev/null and b/folia-api/bin/main/io/papermc/paper/threadedregions/RegionizedServerInitEvent.class differ
diff --git a/folia-api/build.gradle.kts.patch b/folia-api/build.gradle.kts.patch
index 0313e402c7..c311c1e8c8 100644
--- a/folia-api/build.gradle.kts.patch
+++ b/folia-api/build.gradle.kts.patch
@@ -1,5 +1,14 @@
--- a/paper-api/build.gradle.kts
+++ b/paper-api/build.gradle.kts
+@@ -39,6 +_,8 @@
+ }
+
+ dependencies {
++ // Paper API (Bukkit/Spigot/Paper) for compilation when paper-api source tree is not present
++ compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
+
+ // api dependencies are listed transitively to API consumers
+ api("com.google.guava:guava:33.3.1-jre")
@@ -93,7 +_,7 @@
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
@@ -28,26 +37,43 @@
}
}
}
-@@ -169,7 +_,7 @@
+@@ -167,9 +_,12 @@
+ }
+ val services = objects.newInstance()
++val paperApiJavadocDir = rootProject.layout.projectDirectory.dir("paper-api/src/main/javadoc").asFile
tasks.withType {
val options = options as StandardJavadocDocletOptions
- options.overview = "src/main/javadoc/overview.html"
-+ options.overview = "../paper-api/src/main/javadoc/overview.html"
++ if (paperApiJavadocDir.exists()) {
++ options.overview = paperApiJavadocDir.resolve("overview.html").absolutePath
++ }
options.use()
options.isDocFilesSubDirs = true
options.links(
-@@ -202,11 +_,11 @@
+@@ -201,15 +_,17 @@
+ )
}
- // workaround for https://github.com/gradle/gradle/issues/4046
+- // workaround for https://github.com/gradle/gradle/issues/4046
- inputs.dir("src/main/javadoc").withPropertyName("javadoc-sourceset")
-+ inputs.dir("../paper-api/src/main/javadoc").withPropertyName("javadoc-sourceset")
- val fsOps = services.fileSystemOperations
- doLast {
- fsOps.copy {
+- val fsOps = services.fileSystemOperations
+- doLast {
+- fsOps.copy {
- from("src/main/javadoc") {
-+ from("../paper-api/src/main/javadoc") {
- include("**/doc-files/**")
+- include("**/doc-files/**")
++ // workaround for https://github.com/gradle/gradle/issues/4046 (only when paper-api is present)
++ if (paperApiJavadocDir.exists()) {
++ inputs.dir(paperApiJavadocDir).withPropertyName("javadoc-sourceset")
++ val fsOps = services.fileSystemOperations
++ doLast {
++ fsOps.copy {
++ from(paperApiJavadocDir) {
++ include("**/doc-files/**")
++ }
++ into("build/docs/javadoc")
}
- into("build/docs/javadoc")
+- into("build/docs/javadoc")
+ }
+ }
+ }
diff --git a/folia-api/paper-patches/features/0005-Photographer-API.patch b/folia-api/paper-patches/features/0005-Photographer-API.patch
new file mode 100644
index 0000000000..9d84ad2d7e
--- /dev/null
+++ b/folia-api/paper-patches/features/0005-Photographer-API.patch
@@ -0,0 +1,45 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Folia Photographer
+Date: Fri, 20 Feb 2025 00:00:00 +0000
+Subject: [PATCH] Photographer API (Replay Mod)
+
+This patch adds the Photographer API for recording gameplay in .mcpr format.
+Powered by ReplayMod (https://github.com/ReplayMod)
+
+diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java
+index 64694483664c5a7380b1ce7846f2eef59004762f..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/org/bukkit/Bukkit.java
++++ b/src/main/java/org/bukkit/Bukkit.java
+@@ -2980,6 +2980,16 @@ public final class Bukkit {
+ return server.getRegionTPS(world, chunkX, chunkZ);
+ }
+ // Folia end - region TPS API
++
++ // Folia start - Photographer API
++ public static @NotNull dev.folia.entity.PhotographerManager getPhotographerManager() {
++ return server.getPhotographerManager();
++ }
++ // Folia end - Photographer API
+
+ /**
+ * @deprecated All methods on this class have been deprecated, see the individual methods for replacements.
+diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java
+index a0922805fbee8e064a74115fbf93e9e8a98772cb..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/org/bukkit/Server.java
++++ b/src/main/java/org/bukkit/Server.java
+@@ -67,6 +67,7 @@ import org.jetbrains.annotations.Contract;
+ import org.jetbrains.annotations.NotNull;
+ import org.jetbrains.annotations.Nullable;
+
++import dev.folia.entity.PhotographerManager;
+ /**
+ * Represents a server implementation.
+ */
+@@ -2730,4 +2731,10 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
+ double @Nullable [] getRegionTPS(@NotNull World world, int chunkX, int chunkZ);
+ // Folia end - region TPS API
++
++ // Folia start - Photographer API
++ @NotNull PhotographerManager getPhotographerManager();
++ // Folia end - Photographer API
+ }
diff --git a/folia-api/src/main/java/dev/folia/entity/Photographer.java b/folia-api/src/main/java/dev/folia/entity/Photographer.java
new file mode 100644
index 0000000000..8b6dea253d
--- /dev/null
+++ b/folia-api/src/main/java/dev/folia/entity/Photographer.java
@@ -0,0 +1,27 @@
+package dev.folia.entity;
+
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+public interface Photographer extends Player {
+
+ @NotNull
+ String getId();
+
+ void setRecordFile(@NotNull File file);
+
+ void stopRecording();
+
+ void stopRecording(boolean async);
+
+ void stopRecording(boolean async, boolean save);
+
+ void pauseRecording();
+
+ void resumeRecording();
+
+ void setFollowPlayer(@Nullable Player player);
+}
diff --git a/folia-api/src/main/java/dev/folia/entity/PhotographerManager.java b/folia-api/src/main/java/dev/folia/entity/PhotographerManager.java
new file mode 100644
index 0000000000..b1777da1f6
--- /dev/null
+++ b/folia-api/src/main/java/dev/folia/entity/PhotographerManager.java
@@ -0,0 +1,37 @@
+package dev.folia.entity;
+
+import org.bukkit.Location;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import dev.folia.replay.BukkitRecorderOption;
+
+import java.util.Collection;
+import java.util.UUID;
+
+public interface PhotographerManager {
+ @Nullable
+ Photographer getPhotographer(@NotNull UUID uuid);
+
+ @Nullable
+ Photographer getPhotographer(@NotNull String id);
+
+ @Nullable
+ Photographer createPhotographer(@NotNull String id, @NotNull Location location);
+
+ @Nullable
+ Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption);
+
+ void removePhotographer(@NotNull String id);
+
+ void removePhotographer(@NotNull UUID uuid);
+
+ void removeAllPhotographers();
+
+ /**
+ * Returns all photographers currently on the server.
+ *
+ * @return unmodifiable collection of photographers, never null
+ */
+ @NotNull
+ Collection getPhotographers();
+}
diff --git a/folia-api/src/main/java/dev/folia/replay/BukkitRecorderOption.java b/folia-api/src/main/java/dev/folia/replay/BukkitRecorderOption.java
new file mode 100644
index 0000000000..4536d8ad61
--- /dev/null
+++ b/folia-api/src/main/java/dev/folia/replay/BukkitRecorderOption.java
@@ -0,0 +1,16 @@
+package dev.folia.replay;
+
+public class BukkitRecorderOption {
+
+ public String serverName = "Folia";
+ public BukkitRecordWeather forceWeather = BukkitRecordWeather.NULL;
+ public int forceDayTime = -1;
+ public boolean ignoreChat = false;
+
+ public enum BukkitRecordWeather {
+ CLEAR,
+ RAIN,
+ THUNDER,
+ NULL
+ }
+}
diff --git a/folia-server/bin/main/dev/folia/entity/CraftPhotographer.class b/folia-server/bin/main/dev/folia/entity/CraftPhotographer.class
new file mode 100644
index 0000000000..c6076d85f2
Binary files /dev/null and b/folia-server/bin/main/dev/folia/entity/CraftPhotographer.class differ
diff --git a/folia-server/bin/main/dev/folia/entity/CraftPhotographerManager.class b/folia-server/bin/main/dev/folia/entity/CraftPhotographerManager.class
new file mode 100644
index 0000000000..75f3ca7a15
Binary files /dev/null and b/folia-server/bin/main/dev/folia/entity/CraftPhotographerManager.class differ
diff --git a/folia-server/bin/main/dev/folia/entity/Photographer.class b/folia-server/bin/main/dev/folia/entity/Photographer.class
new file mode 100644
index 0000000000..1fd01b7318
Binary files /dev/null and b/folia-server/bin/main/dev/folia/entity/Photographer.class differ
diff --git a/folia-server/bin/main/dev/folia/entity/PhotographerManager.class b/folia-server/bin/main/dev/folia/entity/PhotographerManager.class
new file mode 100644
index 0000000000..0d245212a6
Binary files /dev/null and b/folia-server/bin/main/dev/folia/entity/PhotographerManager.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class b/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class
new file mode 100644
index 0000000000..32e214062b
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption$BukkitRecordWeather.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption.class b/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption.class
new file mode 100644
index 0000000000..75edca8648
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/BukkitRecorderOption.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/DigestOutputStream.class b/folia-server/bin/main/dev/folia/replay/DigestOutputStream.class
new file mode 100644
index 0000000000..b3b26775c0
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/DigestOutputStream.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/PhotographerStatsCounter.class b/folia-server/bin/main/dev/folia/replay/PhotographerStatsCounter.class
new file mode 100644
index 0000000000..a1af70ddf6
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/PhotographerStatsCounter.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/RecordMetaData.class b/folia-server/bin/main/dev/folia/replay/RecordMetaData.class
new file mode 100644
index 0000000000..e69210b1d7
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/RecordMetaData.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/Recorder.class b/folia-server/bin/main/dev/folia/replay/Recorder.class
new file mode 100644
index 0000000000..cef9777dc4
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/Recorder.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/RecorderOption$RecordWeather.class b/folia-server/bin/main/dev/folia/replay/RecorderOption$RecordWeather.class
new file mode 100644
index 0000000000..8ab7324206
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/RecorderOption$RecordWeather.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/RecorderOption.class b/folia-server/bin/main/dev/folia/replay/RecorderOption.class
new file mode 100644
index 0000000000..60c96a31ae
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/RecorderOption.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ReplayFile.class b/folia-server/bin/main/dev/folia/replay/ReplayFile.class
new file mode 100644
index 0000000000..10c5b09481
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ReplayFile.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ReplayMarker$Serializer.class b/folia-server/bin/main/dev/folia/replay/ReplayMarker$Serializer.class
new file mode 100644
index 0000000000..c9d688a4ab
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ReplayMarker$Serializer.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ReplayMarker.class b/folia-server/bin/main/dev/folia/replay/ReplayMarker.class
new file mode 100644
index 0000000000..e867e7e7ae
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ReplayMarker.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ServerPhotographer$PhotographerCreateState.class b/folia-server/bin/main/dev/folia/replay/ServerPhotographer$PhotographerCreateState.class
new file mode 100644
index 0000000000..ed39af2ceb
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ServerPhotographer$PhotographerCreateState.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ServerPhotographer.class b/folia-server/bin/main/dev/folia/replay/ServerPhotographer.class
new file mode 100644
index 0000000000..0325192e9a
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ServerPhotographer.class differ
diff --git a/folia-server/bin/main/dev/folia/replay/ServerPhotographerGameMode.class b/folia-server/bin/main/dev/folia/replay/ServerPhotographerGameMode.class
new file mode 100644
index 0000000000..93539a9052
Binary files /dev/null and b/folia-server/bin/main/dev/folia/replay/ServerPhotographerGameMode.class differ
diff --git a/folia-server/bin/main/dev/folia/util/UUIDSerializer.class b/folia-server/bin/main/dev/folia/util/UUIDSerializer.class
new file mode 100644
index 0000000000..1d40c53797
Binary files /dev/null and b/folia-server/bin/main/dev/folia/util/UUIDSerializer.class differ
diff --git a/folia-server/build.gradle.kts.patch b/folia-server/build.gradle.kts.patch
index 8a268b88ce..a856b09033 100644
--- a/folia-server/build.gradle.kts.patch
+++ b/folia-server/build.gradle.kts.patch
@@ -18,14 +18,18 @@
spigot {
buildDataRef = "3edaf46ec1eed4115ce1b18d2846cded42577e42"
-@@ -101,7 +_,20 @@
+@@ -101,7 +_,24 @@
}
}
-val log4jPlugins = sourceSets.create("log4jPlugins")
++// Compile with patched paper-server + Minecraft sources (paper-server is populated by applyAllPatches / server patches)
+sourceSets {
+ main {
-+ java { srcDir("../paper-server/src/main/java") }
++ java {
++ srcDir("../paper-server/src/main/java")
++ srcDir("src/main/java")
++ }
+ resources { srcDir("../paper-server/src/main/resources") }
+ }
+ test {
diff --git a/folia-server/build.gradle.kts.patch.rej b/folia-server/build.gradle.kts.patch.rej
new file mode 100644
index 0000000000..24f01be4a2
--- /dev/null
+++ b/folia-server/build.gradle.kts.patch.rej
@@ -0,0 +1,60 @@
+++++ REJECTED HUNK: 2
+@@ -100,7 +111,20 @@
+ }
+
+ val log4jPlugins = sourceSets.create("log4jPlugins")
++sourceSets {
++ main {
++ java {
++ srcDir("../paper-server/src/main/java")
++ srcDir("src/main/java")
++ }
++ resources { srcDir("../paper-server/src/main/resources") }
++ }
++ test {
++ java { srcDir("../paper-server/src/test/java") }
++ resources { srcDir("../paper-server/src/test/resources") }
++ }
++}
++
++val log4jPlugins = sourceSets.create("log4jPlugins") {
++ java { srcDir("../paper-server/src/log4jPlugins/java") }
++}
+ configurations.named(log4jPlugins.compileClasspathConfigurationName) {
+ extendsFrom(configurations.compileClasspath.get())
+ }
+++++ END HUNK
+
+++++ REJECTED HUNK: 3
+@@ -118,7 +142,7 @@
+ }
+
+ dependencies {
+- implementation(project(":paper-api"))
++ implementation(project(":folia-api"))
+ implementation("ca.spottedleaf:concurrentutil:0.0.3")
+ implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
+ implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21
+++++ END HUNK
+
+++++ REJECTED HUNK: 4
+@@ -188,14 +212,14 @@
+ val gitBranch = git.exec(providers, "rev-parse", "--abbrev-ref", "HEAD").get().trim()
+ attributes(
+ "Main-Class" to "org.bukkit.craftbukkit.Main",
+- "Implementation-Title" to "Paper",
++ "Implementation-Title" to "Folia",
+ "Implementation-Version" to implementationVersion,
+ "Implementation-Vendor" to date,
+- "Specification-Title" to "Paper",
++ "Specification-Title" to "Folia",
+ "Specification-Version" to project.version,
+ "Specification-Vendor" to "Paper Team",
+- "Brand-Id" to "papermc:paper",
+- "Brand-Name" to "Paper",
++ "Brand-Id" to "papermc:folia",
++ "Brand-Name" to "Folia",
+ "Build-Number" to (build ?: ""),
+ "Build-Time" to buildTime.toString(),
+ "Git-Branch" to gitBranch,
+++++ END HUNK
diff --git a/folia-server/minecraft-patches/features/0009-Photographer-API.patch b/folia-server/minecraft-patches/features/0009-Photographer-API.patch
new file mode 100644
index 0000000000..fc3140d84f
--- /dev/null
+++ b/folia-server/minecraft-patches/features/0009-Photographer-API.patch
@@ -0,0 +1,353 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Folia Photographer
+Date: Fri, 20 Feb 2025 00:00:00 +0000
+Subject: [PATCH] Photographer API (Replay Mod)
+
+This patch adds the Photographer API for recording gameplay in .mcpr format.
+Powered by ReplayMod (https://github.com/ReplayMod)
+
+diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java
+index d5eefed0912c728ded360ddac4d9bcd1813730b2..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/commands/CommandSourceStack.java
++++ b/net/minecraft/commands/CommandSourceStack.java
+@@ -589,7 +589,7 @@ public class CommandSourceStack implements ExecutionCommandSource getOnlinePlayerNames() {
+- return this.entity instanceof ServerPlayer sourcePlayer && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().getName()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players
++ return this.entity instanceof ServerPlayer sourcePlayer && !(sourcePlayer instanceof dev.folia.replay.ServerPhotographer) && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().getName()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players // Folia - exclude photographer
+ }
+
+ @Override
+diff --git a/net/minecraft/commands/arguments/selector/EntitySelector.java b/net/minecraft/commands/arguments/selector/EntitySelector.java
+index 514f8fbdeb776087608665c35de95294aadf5cf0..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/commands/arguments/selector/EntitySelector.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelector.java
+@@ -128,11 +128,12 @@ public class EntitySelector {
+ return this.findPlayers(source);
+ } else if (this.playerName != null) {
+ ServerPlayer playerByName = source.getServer().getPlayerList().getPlayerByName(this.playerName);
++ playerByName = playerByName instanceof dev.folia.replay.ServerPhotographer ? null : playerByName; // Folia - skip photographer
+ return playerByName == null ? List.of() : List.of(playerByName);
+ } else if (this.entityUUID != null) {
+ for (ServerLevel serverLevel : source.getServer().getAllLevels()) {
+ Entity entity = serverLevel.getEntity(this.entityUUID);
+- if (entity != null) {
++ if (entity != null && !(entity instanceof dev.folia.replay.ServerPhotographer)) {
+ if (entity.getType().isEnabled(source.enabledFeatures())) {
+ return List.of(entity);
+ }
+@@ -146,7 +147,7 @@ public class EntitySelector {
+ AABB absoluteAabb = this.getAbsoluteAabb(vec3);
+ if (this.currentEntity) {
+ Predicate predicate = this.getPredicate(vec3, absoluteAabb, null);
+- return source.getEntity() != null && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of();
++ return source.getEntity() != null && !(source.getEntity() instanceof dev.folia.replay.ServerPhotographer) && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of(); // Folia - skip photographer
+ } else {
+ Predicate predicate = this.getPredicate(vec3, absoluteAabb, source.enabledFeatures());
+ List list = new ObjectArrayList<>();
+@@ -157,6 +158,7 @@ public class EntitySelector {
+ this.addEntities(list, serverLevel1, absoluteAabb, predicate);
+ }
+ }
++ list.removeIf(entity -> entity instanceof dev.folia.replay.ServerPhotographer); // Folia - skip photographer
+
+ return this.sortAndLimit(vec3, list);
+ }
+@@ -192,9 +194,11 @@ public class EntitySelector {
+ this.checkPermissions(source);
+ if (this.playerName != null) {
+ ServerPlayer playerByName = source.getServer().getPlayerList().getPlayerByName(this.playerName);
++ playerByName = playerByName instanceof dev.folia.replay.ServerPhotographer ? null : playerByName; // Folia - skip photographer
+ return playerByName == null ? List.of() : List.of(playerByName);
+ } else if (this.entityUUID != null) {
+ ServerPlayer playerByName = source.getServer().getPlayerList().getPlayer(this.entityUUID);
++ playerByName = playerByName instanceof dev.folia.replay.ServerPhotographer ? null : playerByName; // Folia - skip photographer
+ return playerByName == null ? List.of() : List.of(playerByName);
+ } else {
+ Vec3 vec3 = this.position.apply(source.getPosition());
+@@ -206,12 +210,12 @@ public class EntitySelector {
+ int resultLimit = this.getResultLimit();
+ List players;
+ if (this.isWorldLimited()) {
+- players = source.getLevel().getPlayers(predicate, resultLimit);
++ players = source.getLevel().getPlayers((player -> !(player instanceof dev.folia.replay.ServerPhotographer) && predicate.test(player)), resultLimit); // Folia - skip photographer
+ } else {
+ players = new ObjectArrayList<>();
+
+ for (ServerPlayer serverPlayer1 : source.getServer().getPlayerList().getPlayers()) {
+- if (predicate.test(serverPlayer1)) {
++ if (predicate.test(serverPlayer1) && !(serverPlayer1 instanceof dev.folia.replay.ServerPhotographer)) { // Folia - skip photographer
+ players.add(serverPlayer1);
+ if (players.size() >= resultLimit) {
+ return players;
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index faf72dd6dff74296c73cb058aaabd1f9f475a072..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -1738,7 +1738,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading
++ List players = new java.util.ArrayList<>(this.playerList.realPlayers); // Folia - Photographer API
+ int maxPlayers = this.getMaxPlayers();
+ if (this.hidesOnlinePlayers()) {
+ return new ServerStatus.Players(maxPlayers, players.size(), List.of());
+diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
+index 2a60512bcf922e1356d7de0e4fce337c46d1315b..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/server/PlayerAdvancements.java
++++ b/net/minecraft/server/PlayerAdvancements.java
+@@ -171,6 +171,11 @@ public class PlayerAdvancements {
+ }
+
+ public boolean award(AdvancementHolder advancement, String criterionKey) {
++ // Folia start - photographer can't get advancement
++ if (player instanceof dev.folia.replay.ServerPhotographer) {
++ return false;
++ }
++ // Folia end - photographer can't get advancement
+ boolean flag = false;
+ AdvancementProgress orStartProgress = this.getOrStartProgress(advancement);
+ boolean isDone = orStartProgress.isDone();
+diff --git a/net/minecraft/server/commands/OpCommand.java b/net/minecraft/server/commands/OpCommand.java
+index 5c0a04db38821dbb0cba2bb6f0787f113d167efd..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/server/commands/OpCommand.java
++++ b/net/minecraft/server/commands/OpCommand.java
+@@ -25,7 +25,7 @@ public class OpCommand {
+ (context, builder) -> {
+ PlayerList playerList = context.getSource().getServer().getPlayerList();
+ return SharedSuggestionProvider.suggest(
+- playerList.getPlayers()
++ playerList.realPlayers // Folia - Photographer API
+ .stream()
+ .filter(player -> !playerList.isOp(player.getGameProfile()))
+ .map(player -> player.getGameProfile().getName()),
+diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
+index f884c707d4c1f36801018e15efb9adf91f5d6b33..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/server/level/ServerLevel.java
++++ b/net/minecraft/server/level/ServerLevel.java
+@@ -179,6 +179,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ private static final int EMPTY_TIME_NO_TICK = 300;
+ private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536;
+ final List players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading
++ final List realPlayers = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - Photographer API
+ public final ServerChunkCache chunkSource;
+ private final MinecraftServer server;
+ public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
+@@ -2622,6 +2623,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ if (entity instanceof ServerPlayer serverPlayer) {
+ ServerLevel.this.players.add(serverPlayer);
++ if (!(serverPlayer instanceof dev.folia.replay.ServerPhotographer)) {
++ ServerLevel.this.realPlayers.add(serverPlayer);
++ }
+ ServerLevel.this.updateSleepStatus();
+ if (ServerLevel.this.isDebug()) {
+ ServerLevel.this.logger.info("{} joined the level", serverPlayer.getName().getString());
+@@ -2695,6 +2697,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ if (entity instanceof ServerPlayer serverPlayer) {
+ ServerLevel.this.players.remove(serverPlayer);
++ if (!(serverPlayer instanceof dev.folia.replay.ServerPhotographer)) {
++ ServerLevel.this.realPlayers.remove(serverPlayer);
++ }
+ ServerLevel.this.updateSleepStatus();
+ ServerLevel.this.logger.debug("{} left the level", serverPlayer.getName().getString());
+diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
+index 751b011701d6ae373099226ea63ffbafcd24ce6a..0000000000000000000000000000000000000001 100644
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -131,6 +131,7 @@ public abstract class PlayerList {
+ private boolean allowCommandsForAllPlayers;
+ private static final boolean ALLOW_LOGOUTIVATOR = false;
+ private int sendAllPlayerInfoIn;
++ public final List realPlayers = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - Photographer API
+
+ // CraftBukkit start
+ private org.bukkit.craftbukkit.CraftServer cserver;
+@@ -139,6 +140,119 @@ public abstract class PlayerList {
+
+ abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor
+
++ // Folia start - Photographer API
++ public void placeNewPhotographer(net.minecraft.network.Connection connection, dev.folia.replay.ServerPhotographer player, ServerLevel worldserver) {
++ player.isRealPlayer = true; // Paper
++ player.loginTime = System.currentTimeMillis(); // Paper
++
++ ServerLevel worldserver1 = worldserver;
++
++ player.setServerLevel(worldserver1);
++ player.spawnIn(worldserver1);
++ player.gameMode.setLevel((ServerLevel) player.level());
++
++ LevelData worlddata = worldserver1.getLevelData();
++
++ player.loadGameTypes(null);
++ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, CommonListenerCookie.createInitial(player.gameProfile, false));
++ GameRules gamerules = worldserver1.getGameRules();
++ boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN);
++ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
++ boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING);
++
++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management
++ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
++ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
++ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities()));
++ playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected));
++ RecipeManager craftingmanager = this.server.getRecipeManager();
++ playerconnection.send(new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes()));
++
++ this.sendPlayerPermissionLevel(player);
++ player.getStats().markAllDirty();
++ player.getRecipeBook().sendInitialRecipeBook(player);
++ this.updateEntireScoreboard(worldserver1.getScoreboard(), player);
++ this.server.invalidateStatus();
++
++ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
++ ServerStatus serverping = this.server.getStatus();
++
++ if (serverping != null) {
++ player.sendServerStatus(serverping);
++ }
++
++ this.players.add(player);
++ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
++ this.playersByUUID.put(player.getUUID(), player);
++
++ player.supressTrackerForLogin = true;
++ worldserver1.addNewPlayer(player);
++ this.server.getCustomBossEvents().onPlayerConnect(player);
++ org.bukkit.craftbukkit.entity.CraftPlayer bukkitPlayer = player.getBukkitEntity();
++
++ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer);
++ if (!player.connection.isAcceptingMessages()) {
++ return;
++ }
++
++ final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1);
++ for (int i = 0; i < this.players.size(); ++i) {
++ ServerPlayer entityplayer1 = this.players.get(i);
++
++ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) {
++ continue;
++ }
++
++ onlinePlayers.add(entityplayer1);
++ }
++ if (!onlinePlayers.isEmpty()) {
++ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player));
++ }
++
++ player.sentListPacket = true;
++ player.supressTrackerForLogin = false;
++ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player);
++
++ this.sendLevelInfo(player, worldserver1);
++
++ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) {
++ worldserver1.addNewPlayer(player);
++ this.server.getCustomBossEvents().onPlayerConnect(player);
++ }
++
++ worldserver1 = player.serverLevel();
++ java.util.Iterator iterator = player.getActiveEffects().iterator();
++ while (iterator.hasNext()) {
++ MobEffectInstance mobeffect = iterator.next();
++ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect, false));
++ }
++
++ if (player.isDeadOrDying()) {
++ net.minecraft.core.Holder plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME)
++ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS);
++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket(
++ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains),
++ worldserver1.getLightEngine(), null, null, false)
++ );
++ }
++ }
++ // Folia end - Photographer API
++
+ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
+ player.isRealPlayer = true; // Paper
+ player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
+@@ -304,6 +418,7 @@ public abstract class PlayerList {
+
+ // player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below
+ this.players.add(player);
++ this.realPlayers.add(player); // Folia - Photographer API
+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot
+ this.playersByUUID.put(player.getUUID(), player);
+ // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); // CraftBukkit - replaced with loop below
+@@ -373,6 +488,12 @@ public abstract class PlayerList {
+ continue;
+ }
+
++ // Folia start - skip photographer
++ if (entityplayer1 instanceof dev.folia.replay.ServerPhotographer) {
++ continue;
++ }
++ // Folia end - skip photographer
++
+ onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join
+ }
+ // Paper start - Use single player info update packet on join
+@@ -507,6 +628,43 @@ public abstract class PlayerList {
+ }
+ }
+
++ // Folia start - Photographer API
++ public void removePhotographer(dev.folia.replay.ServerPhotographer entityplayer) {
++ ServerLevel worldserver = entityplayer.serverLevel();
++
++ entityplayer.awardStat(Stats.LEAVE_GAME);
++
++ if (entityplayer.containerMenu != entityplayer.inventoryMenu) {
++ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT);
++ }
++
++ if (server.isSameThread()) entityplayer.doTick();
++
++ if (this.collideRuleTeamName != null) {
++ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
++ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
++ if (entityplayer.getTeam() == team && team != null) {
++ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team);
++ }
++ }
++
++ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
++ entityplayer.retireScheduler();
++ entityplayer.getAdvancements().stopListening();
++ this.players.remove(entityplayer);
++ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT));
++ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer);
++ UUID uuid = entityplayer.getUUID();
++ ServerPlayer entityplayer1 = this.playersByUUID.get(uuid);
++
++ if (entityplayer1 == entityplayer) {
++ this.playersByUUID.remove(uuid);
++ }
++
++ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity());
++ }
++ // Folia end - Photographer API
++
+ public net.kyori.adventure.text.Component remove(ServerPlayer player) { // CraftBukkit - return string // Paper - return Component
+ // Paper start - Fix kick event leave message not be sent
+ 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())));
+@@ -582,6 +740,7 @@ public abstract class PlayerList {
+ player.retireScheduler(); // Paper - Folia schedulers
+ player.getAdvancements().stopListening();
+ this.players.remove(player);
++ this.realPlayers.remove(player); // Folia - Photographer API
+ this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
+ this.server.getCustomBossEvents().onPlayerDisconnect(player);
+ UUID uuid = player.getUUID();
+@@ -678,7 +837,7 @@ public abstract class PlayerList {
+ // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)
+ // ? Component.translatable("multiplayer.disconnect.server_full")
+ // : null;
+- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) {
++ if (this.realPlayers.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { // Folia - Photographer API
+ event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure
+ }
+ }
diff --git a/folia-server/paper-patches/features/0008-Photographer-API.patch b/folia-server/paper-patches/features/0008-Photographer-API.patch
new file mode 100644
index 0000000000..ac4fdd0f47
--- /dev/null
+++ b/folia-server/paper-patches/features/0008-Photographer-API.patch
@@ -0,0 +1,82 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Folia Photographer
+Date: Fri, 20 Feb 2025 00:00:00 +0000
+Subject: [PATCH] Photographer API (Replay Mod)
+
+This patch adds the Photographer API for recording gameplay in .mcpr format.
+Powered by ReplayMod (https://github.com/ReplayMod)
+
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+index 7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+@@ -45,6 +45,12 @@ class PaperEventManager {
+ }
+
+ HandlerList handlers = event.getHandlers();
+ RegisteredListener[] listeners = handlers.getRegisteredListeners();
++
++ // Folia start - skip photographer
++ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof dev.folia.entity.Photographer) {
++ return;
++ }
++ // Folia end - skip photographer
+
+ for (RegisteredListener registration : listeners) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index a60e6ed437764ebeaab64e9ae7b87a177119e436..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -318,6 +318,7 @@ public final class CraftServer implements Server {
+ private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes
+ public final io.papermc.paper.SparksFly spark; // Paper - spark
+
++ private final dev.folia.entity.CraftPhotographerManager photographerManager = new dev.folia.entity.CraftPhotographerManager(); // Folia - Photographer API
++
+ // Paper start - Folia region threading API
+ private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading
+@@ -395,7 +396,7 @@ public final class CraftServer implements Server {
+ public CraftServer(DedicatedServer console, PlayerList playerList) {
+ this.console = console;
+ this.playerList = (DedicatedPlayerList) playerList;
+- this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function() {
++ this.playerView = Collections.unmodifiableList(Lists.transform(playerList.realPlayers, new Function() { // Folia - Photographer API
+ @Override
+ public CraftPlayer apply(ServerPlayer player) {
+ return player.getBukkitEntity();
+@@ -3415,4 +3416,13 @@ public final class CraftServer implements Server {
+ }
+ }
+ // Folia end - region TPS API
++
++ // Folia start - Photographer API
++ @Override
++ public dev.folia.entity.CraftPhotographerManager getPhotographerManager() {
++ return photographerManager;
++ }
++ // Folia end - Photographer API
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index 895695015d21b4ae0ab87ef68d6b3da30f4616c1..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -98,6 +98,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+ return new CraftHumanEntity(server, (net.minecraft.world.entity.player.Player) entity);
+ }
+
++ if (entity instanceof dev.folia.replay.ServerPhotographer photographer) { return new dev.folia.entity.CraftPhotographer(server, photographer); }
++
+ // Special case complex part, since there is no extra entity type for them
+ if (entity instanceof EnderDragonPart complexPart) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index f7ceb89d9b908f02fc9e90e426e8e14e330ac041..0000000000000000000000000000000000000001 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2251,7 +2251,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+
+ @Override
+ public boolean canSee(Player player) {
+- return this.canSee((org.bukkit.entity.Entity) player);
++ return !(player instanceof dev.folia.entity.Photographer) && this.canSee((org.bukkit.entity.Entity) player); // Folia - skip photographer
+ }
diff --git a/folia-server/src/main/java/dev/folia/entity/CraftPhotographer.java b/folia-server/src/main/java/dev/folia/entity/CraftPhotographer.java
new file mode 100644
index 0000000000..e19d2adf3b
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/entity/CraftPhotographer.java
@@ -0,0 +1,73 @@
+package dev.folia.entity;
+
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import dev.folia.replay.ServerPhotographer;
+
+import java.io.File;
+
+public class CraftPhotographer extends CraftPlayer implements Photographer {
+
+ public CraftPhotographer(CraftServer server, ServerPhotographer entity) {
+ super(server, entity);
+ }
+
+ @Override
+ public void stopRecording() {
+ this.stopRecording(true);
+ }
+
+ @Override
+ public void stopRecording(boolean async) {
+ this.stopRecording(async, true);
+ }
+
+ @Override
+ public void stopRecording(boolean async, boolean save) {
+ this.getHandle().remove(async, save);
+ }
+
+ @Override
+ public void pauseRecording() {
+ this.getHandle().pauseRecording();
+ }
+
+ @Override
+ public void resumeRecording() {
+ this.getHandle().resumeRecording();
+ }
+
+ @Override
+ public void setRecordFile(@NotNull File file) {
+ this.getHandle().setSaveFile(file);
+ }
+
+ @Override
+ public void setFollowPlayer(@Nullable Player player) {
+ ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null;
+ this.getHandle().setFollowPlayer(serverPlayer);
+ }
+
+ @Override
+ public @NotNull String getId() {
+ return this.getHandle().createState.id;
+ }
+
+ @Override
+ public ServerPhotographer getHandle() {
+ return (ServerPhotographer) entity;
+ }
+
+ public void setHandle(final ServerPhotographer entity) {
+ super.setHandle(entity);
+ }
+
+ @Override
+ public String toString() {
+ return "CraftPhotographer{" + "name=" + getName() + '}';
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/entity/CraftPhotographerManager.java b/folia-server/src/main/java/dev/folia/entity/CraftPhotographerManager.java
new file mode 100644
index 0000000000..278480f3bd
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/entity/CraftPhotographerManager.java
@@ -0,0 +1,82 @@
+package dev.folia.entity;
+
+import com.google.common.collect.Lists;
+import org.bukkit.Location;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import dev.folia.replay.BukkitRecorderOption;
+import dev.folia.replay.RecorderOption;
+import dev.folia.replay.ServerPhotographer;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
+
+public class CraftPhotographerManager implements PhotographerManager {
+
+ private final Collection photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer));
+
+ @Override
+ public @Nullable Photographer getPhotographer(@NotNull UUID uuid) {
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
+ if (photographer != null) {
+ return photographer.getBukkitPlayer();
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable Photographer getPhotographer(@NotNull String id) {
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
+ if (photographer != null) {
+ return photographer.getBukkitPlayer();
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) {
+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync();
+ if (photographer != null) {
+ return photographer.getBukkitPlayer();
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) {
+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync();
+ if (photographer != null) {
+ return photographer.getBukkitPlayer();
+ }
+ return null;
+ }
+
+ @Override
+ public void removePhotographer(@NotNull String id) {
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
+ if (photographer != null) {
+ photographer.remove(true);
+ }
+ }
+
+ @Override
+ public void removePhotographer(@NotNull UUID uuid) {
+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
+ if (photographer != null) {
+ photographer.remove(true);
+ }
+ }
+
+ @Override
+ public void removeAllPhotographers() {
+ for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) {
+ photographer.remove(true);
+ }
+ }
+
+ @Override
+ public Collection getPhotographers() {
+ return photographerViews;
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/entity/Photographer.java b/folia-server/src/main/java/dev/folia/entity/Photographer.java
new file mode 100644
index 0000000000..8b6dea253d
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/entity/Photographer.java
@@ -0,0 +1,27 @@
+package dev.folia.entity;
+
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+public interface Photographer extends Player {
+
+ @NotNull
+ String getId();
+
+ void setRecordFile(@NotNull File file);
+
+ void stopRecording();
+
+ void stopRecording(boolean async);
+
+ void stopRecording(boolean async, boolean save);
+
+ void pauseRecording();
+
+ void resumeRecording();
+
+ void setFollowPlayer(@Nullable Player player);
+}
diff --git a/folia-server/src/main/java/dev/folia/entity/PhotographerManager.java b/folia-server/src/main/java/dev/folia/entity/PhotographerManager.java
new file mode 100644
index 0000000000..99350844b2
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/entity/PhotographerManager.java
@@ -0,0 +1,31 @@
+package dev.folia.entity;
+
+import org.bukkit.Location;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import dev.folia.replay.BukkitRecorderOption;
+
+import java.util.Collection;
+import java.util.UUID;
+
+public interface PhotographerManager {
+ @Nullable
+ Photographer getPhotographer(@NotNull UUID uuid);
+
+ @Nullable
+ Photographer getPhotographer(@NotNull String id);
+
+ @Nullable
+ Photographer createPhotographer(@NotNull String id, @NotNull Location location);
+
+ @Nullable
+ Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption);
+
+ void removePhotographer(@NotNull String id);
+
+ void removePhotographer(@NotNull UUID uuid);
+
+ void removeAllPhotographers();
+
+ Collection getPhotographers();
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/BukkitRecorderOption.java b/folia-server/src/main/java/dev/folia/replay/BukkitRecorderOption.java
new file mode 100644
index 0000000000..4536d8ad61
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/BukkitRecorderOption.java
@@ -0,0 +1,16 @@
+package dev.folia.replay;
+
+public class BukkitRecorderOption {
+
+ public String serverName = "Folia";
+ public BukkitRecordWeather forceWeather = BukkitRecordWeather.NULL;
+ public int forceDayTime = -1;
+ public boolean ignoreChat = false;
+
+ public enum BukkitRecordWeather {
+ CLEAR,
+ RAIN,
+ THUNDER,
+ NULL
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/DigestOutputStream.java b/folia-server/src/main/java/dev/folia/replay/DigestOutputStream.java
new file mode 100644
index 0000000000..3e183743f1
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/DigestOutputStream.java
@@ -0,0 +1,46 @@
+package dev.folia.replay;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.Checksum;
+
+public class DigestOutputStream extends OutputStream {
+
+ private final Checksum sum;
+ private final OutputStream out;
+
+ public DigestOutputStream(OutputStream out, Checksum sum) {
+ this.out = out;
+ this.sum = sum;
+ }
+
+ @Override
+ public void close() throws IOException {
+ out.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ sum.update(b);
+ out.write(b);
+ }
+
+ @Override
+ public void write(byte @NotNull [] b) throws IOException {
+ sum.update(b);
+ out.write(b);
+ }
+
+ @Override
+ public void write(byte @NotNull [] b, int off, int len) throws IOException {
+ sum.update(b, off, len);
+ out.write(b, off, len);
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/PhotographerStatsCounter.java b/folia-server/src/main/java/dev/folia/replay/PhotographerStatsCounter.java
new file mode 100644
index 0000000000..b71340cd9a
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/PhotographerStatsCounter.java
@@ -0,0 +1,36 @@
+package dev.folia.replay;
+
+import com.mojang.datafixers.DataFixer;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.stats.ServerStatsCounter;
+import net.minecraft.stats.Stat;
+import net.minecraft.world.entity.player.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+public class PhotographerStatsCounter extends ServerStatsCounter {
+
+ private static final File UNKNOWN_FILE = new File("PHOTOGRAPHER_STATS_REMOVE_THIS");
+
+ public PhotographerStatsCounter(MinecraftServer server) {
+ super(server, UNKNOWN_FILE);
+ }
+
+ @Override
+ public void save() {
+ }
+
+ @Override
+ public void setValue(@NotNull Player player, @NotNull Stat> stat, int value) {
+ }
+
+ @Override
+ public void parseLocal(@NotNull DataFixer dataFixer, @NotNull String json) {
+ }
+
+ @Override
+ public int getValue(@NotNull Stat> stat) {
+ return 0;
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/RecordMetaData.java b/folia-server/src/main/java/dev/folia/replay/RecordMetaData.java
new file mode 100644
index 0000000000..ff1bf4224f
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/RecordMetaData.java
@@ -0,0 +1,23 @@
+package dev.folia.replay;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public class RecordMetaData {
+
+ public static final int CURRENT_FILE_FORMAT_VERSION = 14;
+
+ public boolean singleplayer = false;
+ public String serverName = "Folia";
+ public int duration = 0;
+ public long date;
+ public String mcversion;
+ public String fileFormat = "MCPR";
+ public int fileFormatVersion;
+ public int protocol;
+ public String generator;
+ public int selfId = -1;
+
+ public Set players = new HashSet<>();
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/Recorder.java b/folia-server/src/main/java/dev/folia/replay/Recorder.java
new file mode 100644
index 0000000000..ad34246da7
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/Recorder.java
@@ -0,0 +1,283 @@
+package dev.folia.replay;
+
+import com.mojang.serialization.DynamicOps;
+import io.netty.channel.local.LocalChannel;
+import net.minecraft.SharedConstants;
+import net.minecraft.core.LayeredRegistryAccess;
+import net.minecraft.core.RegistrySynchronization;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
+import net.minecraft.network.Connection;
+import net.minecraft.network.ConnectionProtocol;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.protocol.BundlePacket;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.PacketFlow;
+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
+import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket;
+import net.minecraft.network.protocol.common.ClientboundServerLinksPacket;
+import net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket;
+import net.minecraft.network.protocol.common.custom.BrandPayload;
+import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket;
+import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket;
+import net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks;
+import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
+import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket;
+import net.minecraft.network.protocol.game.ClientboundSetTimePacket;
+import net.minecraft.network.protocol.game.ClientboundSystemChatPacket;
+import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.RegistryLayer;
+import net.minecraft.server.packs.repository.KnownPack;
+import net.minecraft.tags.TagNetworkSerialization;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.flag.FeatureFlags;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class Recorder extends Connection {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger("Folia-Photographer");
+
+ private final ReplayFile replayFile;
+ private final ServerPhotographer photographer;
+ private final RecorderOption recorderOption;
+ private final RecordMetaData metaData;
+
+ private final ExecutorService saveService = Executors.newSingleThreadExecutor();
+
+ private boolean stopped = false;
+ private boolean paused = false;
+ private boolean resumeOnNextPacket = true;
+
+ private long startTime;
+ private long lastPacket;
+ private long timeShift = 0;
+
+ private boolean isSaved;
+ private boolean isSaving;
+ private ConnectionProtocol state = ConnectionProtocol.LOGIN;
+
+ public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException {
+ super(PacketFlow.CLIENTBOUND);
+
+ this.photographer = photographer;
+ this.recorderOption = recorderOption;
+ this.metaData = new RecordMetaData();
+ this.replayFile = new ReplayFile(replayFile);
+ this.channel = new LocalChannel();
+ }
+
+ public void start() {
+ startTime = System.currentTimeMillis();
+
+ metaData.singleplayer = false;
+ metaData.serverName = recorderOption.serverName;
+ metaData.date = startTime;
+ metaData.mcversion = SharedConstants.getCurrentVersion().getName();
+
+ this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN);
+ this.startConfiguration();
+
+ if (recorderOption.forceWeather != null) {
+ setWeather(recorderOption.forceWeather);
+ }
+ }
+
+ public void startConfiguration() {
+ this.state = ConnectionProtocol.CONFIGURATION;
+ MinecraftServer server = MinecraftServer.getServer();
+
+ this.savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION);
+ this.savePacket(new ClientboundServerLinksPacket(server.serverLinks().untrust()), ConnectionProtocol.CONFIGURATION);
+ this.savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION);
+
+ List knownPackslist = server.getResourceManager().listPacks().flatMap((iresourcepack) -> iresourcepack.location().knownPackInfo().stream()).toList();
+ this.savePacket(new ClientboundSelectKnownPacks(knownPackslist), ConnectionProtocol.CONFIGURATION);
+
+ server.getServerResourcePack().ifPresent((info) -> this.savePacket(new ClientboundResourcePackPushPacket(
+ info.id(), info.url(), info.hash(), info.isRequired(), Optional.ofNullable(info.prompt())
+ )));
+
+ LayeredRegistryAccess layeredregistryaccess = server.registries();
+ DynamicOps dynamicOps = layeredregistryaccess.compositeAccess().createSerializationContext(NbtOps.INSTANCE);
+ RegistrySynchronization.packRegistries(dynamicOps, layeredregistryaccess.getAccessFrom(RegistryLayer.WORLDGEN), Set.copyOf(knownPackslist),
+ (key, entries) ->
+ this.savePacket(new ClientboundRegistryDataPacket(key, entries), ConnectionProtocol.CONFIGURATION)
+ );
+ this.savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION);
+
+ this.savePacket(ClientboundFinishConfigurationPacket.INSTANCE, ConnectionProtocol.CONFIGURATION);
+ state = ConnectionProtocol.PLAY;
+ }
+
+ @Override
+ public void flushChannel() {
+ }
+
+ public void stop() {
+ stopped = true;
+ }
+
+ public void pauseRecording() {
+ resumeOnNextPacket = false;
+ paused = true;
+ }
+
+ public void resumeRecording() {
+ resumeOnNextPacket = true;
+ }
+
+ public void setWeather(RecorderOption.RecordWeather weather) {
+ weather.getPackets().forEach(this::savePacket);
+ }
+
+ public long getRecordedTime() {
+ final long base = System.currentTimeMillis() - startTime;
+ return base - timeShift;
+ }
+
+ private synchronized long getCurrentTimeAndUpdate() {
+ long now = getRecordedTime();
+ if (paused) {
+ if (resumeOnNextPacket) {
+ paused = false;
+ }
+ timeShift += now - lastPacket;
+ return lastPacket;
+ }
+ return lastPacket = now;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return true;
+ }
+
+ @Override
+ public void send(@NotNull Packet> packet, @Nullable PacketSendListener callbacks, boolean flush) {
+ if (!stopped) {
+ if (packet instanceof BundlePacket> packet1) {
+ packet1.subPackets().forEach(subPacket -> send(subPacket, null));
+ return;
+ }
+
+ if (packet instanceof ClientboundAddEntityPacket packet1) {
+ if (packet1.getType() == EntityType.PLAYER) {
+ metaData.players.add(packet1.getUUID());
+ saveMetadata();
+ }
+ }
+
+ if (packet instanceof ClientboundDisconnectPacket) {
+ return;
+ }
+
+ if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) {
+ packet = new ClientboundSetTimePacket(packet1.dayTime(), recorderOption.forceDayTime, false);
+ }
+
+ if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) {
+ ClientboundGameEventPacket.Type type = packet1.getEvent();
+ if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) {
+ return;
+ }
+ }
+
+ if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) {
+ return;
+ }
+
+ savePacket(packet);
+ }
+ }
+
+ private void saveMetadata() {
+ saveService.submit(() -> {
+ try {
+ replayFile.saveMetaData(metaData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private void savePacket(Packet> packet) {
+ this.savePacket(packet, state);
+ }
+
+ private void savePacket(Packet> packet, final ConnectionProtocol protocol) {
+ try {
+ final long timestamp = getCurrentTimeAndUpdate();
+ saveService.submit(() -> {
+ try {
+ replayFile.savePacket(timestamp, packet, protocol);
+ } catch (Exception e) {
+ LOGGER.error("Error saving packet", e);
+ }
+ });
+ } catch (Exception e) {
+ LOGGER.error("Error saving packet", e);
+ }
+ }
+
+ public boolean isSaved() {
+ return isSaved;
+ }
+
+ public CompletableFuture saveRecording(File dest, boolean save) {
+ isSaved = true;
+ if (!isSaving) {
+ isSaving = true;
+ metaData.duration = (int) lastPacket;
+ return CompletableFuture.runAsync(() -> {
+ saveMetadata();
+ saveService.shutdown();
+ boolean interrupted = false;
+ try {
+ saveService.awaitTermination(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ try {
+ if (save) {
+ replayFile.closeAndSave(dest);
+ } else {
+ replayFile.closeNotSave();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new CompletionException(e);
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }, runnable -> {
+ final Thread thread = new Thread(runnable, "Recording file save thread");
+ thread.start();
+ });
+ } else {
+ LOGGER.warn("saveRecording() called twice");
+ return CompletableFuture.supplyAsync(() -> {
+ throw new IllegalStateException("saveRecording() called twice");
+ });
+ }
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/RecorderOption.java b/folia-server/src/main/java/dev/folia/replay/RecorderOption.java
new file mode 100644
index 0000000000..d943936dd9
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/RecorderOption.java
@@ -0,0 +1,54 @@
+package dev.folia.replay;
+
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class RecorderOption {
+
+ public int recordDistance = -1;
+ public String serverName = "Folia";
+ public RecordWeather forceWeather = null;
+ public int forceDayTime = -1;
+ public boolean ignoreChat = false;
+
+ @NotNull
+ @Contract(" -> new")
+ public static RecorderOption createDefaultOption() {
+ return new RecorderOption();
+ }
+
+ @NotNull
+ public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) {
+ RecorderOption recorderOption = new RecorderOption();
+ recorderOption.serverName = bukkitRecorderOption.serverName;
+ recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat;
+ recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime;
+ recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) {
+ case RAIN -> RecordWeather.RAIN;
+ case CLEAR -> RecordWeather.CLEAR;
+ case THUNDER -> RecordWeather.THUNDER;
+ case NULL -> null;
+ };
+ return recorderOption;
+ }
+
+ public enum RecordWeather {
+ CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
+ RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)),
+ THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1));
+
+ private final List> packets;
+
+ RecordWeather(Packet>... packets) {
+ this.packets = List.of(packets);
+ }
+
+ public List> getPackets() {
+ return packets;
+ }
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/ReplayFile.java b/folia-server/src/main/java/dev/folia/replay/ReplayFile.java
new file mode 100644
index 0000000000..5025a81722
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/ReplayFile.java
@@ -0,0 +1,197 @@
+package dev.folia.replay;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import net.minecraft.SharedConstants;
+import net.minecraft.network.ConnectionProtocol;
+import net.minecraft.network.ProtocolInfo;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.configuration.ConfigurationProtocols;
+import net.minecraft.network.protocol.game.GameProtocols;
+import net.minecraft.network.protocol.login.LoginProtocols;
+import net.minecraft.network.protocol.status.StatusProtocols;
+import net.minecraft.server.MinecraftServer;
+import org.jetbrains.annotations.NotNull;
+import dev.folia.util.UUIDSerializer;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class ReplayFile {
+
+ private static final String RECORDING_FILE = "recording.tmcpr";
+ private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32";
+ private static final String MARKER_FILE = "markers.json";
+ private static final String META_FILE = "metaData.json";
+
+ private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create();
+ private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create();
+
+ private final File tmpDir;
+ private final DataOutputStream packetStream;
+ private final CRC32 crc32 = new CRC32();
+
+ private final File markerFile;
+ private final File metaFile;
+
+ private final Map> protocols;
+
+ public ReplayFile(@NotNull File name) throws IOException {
+ this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp");
+ if (tmpDir.exists()) {
+ if (!ReplayFile.deleteDir(tmpDir)) {
+ throw new IOException("Recording file " + name + " already exists!");
+ }
+ }
+
+ if (!tmpDir.mkdirs()) {
+ throw new IOException("Failed to create temp directory for recording " + tmpDir);
+ }
+
+ File packetFile = new File(tmpDir, RECORDING_FILE);
+ this.metaFile = new File(tmpDir, META_FILE);
+ this.markerFile = new File(tmpDir, MARKER_FILE);
+
+ this.packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32));
+
+ this.protocols = Map.of(
+ ConnectionProtocol.STATUS, StatusProtocols.CLIENTBOUND,
+ ConnectionProtocol.LOGIN, LoginProtocols.CLIENTBOUND,
+ ConnectionProtocol.CONFIGURATION, ConfigurationProtocols.CLIENTBOUND,
+ ConnectionProtocol.PLAY, GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess()))
+ );
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) {
+ ProtocolInfo> protocol = this.protocols.get(state);
+ if (protocol == null) {
+ throw new IllegalArgumentException("Unknown protocol state " + state);
+ }
+
+ ByteBuf buf = Unpooled.buffer();
+ protocol.codec().encode(buf, packet);
+
+ buf.readerIndex(0);
+ byte[] ret = new byte[buf.readableBytes()];
+ buf.readBytes(ret);
+ buf.release();
+ return ret;
+ }
+
+ public void saveMarkers(List markers) throws IOException {
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) {
+ writer.write(MARKER_GSON.toJson(markers));
+ }
+ }
+
+ public void saveMetaData(@NotNull RecordMetaData data) throws IOException {
+ data.fileFormat = "MCPR";
+ data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION;
+ data.protocol = SharedConstants.getCurrentVersion().getProtocolVersion();
+ data.generator = "replay-folia-" + SharedConstants.getCurrentVersion().getName();
+
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) {
+ writer.write(META_GSON.toJson(data));
+ }
+ }
+
+ public void savePacket(long timestamp, Packet> packet, ConnectionProtocol protocol) throws Exception {
+ byte[] data = getPacketBytes(packet, protocol);
+ packetStream.writeInt((int) timestamp);
+ packetStream.writeInt(data.length);
+ packetStream.write(data);
+ }
+
+ public synchronized void closeAndSave(File file) throws IOException {
+ packetStream.close();
+
+ String[] files = tmpDir.list();
+ if (files == null) {
+ return;
+ }
+
+ try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
+ for (String fileName : files) {
+ os.putNextEntry(new ZipEntry(fileName));
+ File f = new File(tmpDir, fileName);
+ copy(new FileInputStream(f), os);
+ }
+
+ os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32));
+ Writer writer = new OutputStreamWriter(os);
+ writer.write(Long.toString(crc32.getValue()));
+ writer.flush();
+ }
+
+ for (String fileName : files) {
+ File f = new File(tmpDir, fileName);
+ Files.delete(f.toPath());
+ }
+ Files.delete(tmpDir.toPath());
+ }
+
+ public synchronized void closeNotSave() throws IOException {
+ packetStream.close();
+
+ String[] files = tmpDir.list();
+ if (files == null) {
+ return;
+ }
+
+ for (String fileName : files) {
+ File f = new File(tmpDir, fileName);
+ Files.delete(f.toPath());
+ }
+ Files.delete(tmpDir.toPath());
+ }
+
+ private void copy(@NotNull InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[8192];
+ int len;
+ while ((len = in.read(buffer)) > -1) {
+ out.write(buffer, 0, len);
+ }
+ in.close();
+ }
+
+ private static boolean deleteDir(File dir) {
+ if (dir == null || !dir.exists()) {
+ return false;
+ }
+
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteDir(file);
+ } else {
+ if (!file.delete()) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return dir.delete();
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/ReplayMarker.java b/folia-server/src/main/java/dev/folia/replay/ReplayMarker.java
new file mode 100644
index 0000000000..a62c92a463
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/ReplayMarker.java
@@ -0,0 +1,43 @@
+package dev.folia.replay;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import java.lang.reflect.Type;
+
+public class ReplayMarker {
+
+ public int time;
+ public String name;
+ public double x = 0;
+ public double y = 0;
+ public double z = 0;
+ public float phi = 0;
+ public float theta = 0;
+ public float varphi = 0;
+
+ public static class Serializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject ret = new JsonObject();
+ JsonObject value = new JsonObject();
+ JsonObject position = new JsonObject();
+ ret.add("realTimestamp", new JsonPrimitive(src.time));
+ ret.add("value", value);
+
+ value.add("name", new JsonPrimitive(src.name));
+ value.add("position", position);
+
+ position.add("x", new JsonPrimitive(src.x));
+ position.add("y", new JsonPrimitive(src.y));
+ position.add("z", new JsonPrimitive(src.z));
+ position.add("yaw", new JsonPrimitive(src.phi));
+ position.add("pitch", new JsonPrimitive(src.theta));
+ position.add("roll", new JsonPrimitive(src.varphi));
+ return ret;
+ }
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/ServerPhotographer.java b/folia-server/src/main/java/dev/folia/replay/ServerPhotographer.java
new file mode 100644
index 0000000000..d200c5934b
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/ServerPhotographer.java
@@ -0,0 +1,230 @@
+package dev.folia.replay;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.stats.ServerStatsCounter;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import dev.folia.entity.CraftPhotographer;
+import dev.folia.entity.Photographer;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class ServerPhotographer extends ServerPlayer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger("Folia-Photographer");
+ private static final List photographers = new CopyOnWriteArrayList<>();
+
+ public PhotographerCreateState createState;
+ private ServerPlayer followPlayer;
+ private Recorder recorder;
+ private File saveFile;
+ private Vec3 lastPos;
+ /** 本地 tick 计数,用于节流 resetPosition/move(Folia 无全局 getTickCount) */
+ private int tickCounter;
+
+ private final ServerStatsCounter stats;
+
+ private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) {
+ super(server, world, profile, ClientInformation.createDefault());
+ this.followPlayer = null;
+ this.stats = new PhotographerStatsCounter(server);
+ this.lastPos = this.position();
+ try {
+ Field gameModeField = ServerPlayer.class.getDeclaredField("gameMode");
+ gameModeField.setAccessible(true);
+ gameModeField.set(this, new ServerPhotographerGameMode(this));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to set photographer game mode", e);
+ }
+ }
+
+ public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException {
+ if (!isCreateLegal(state.id)) {
+ throw new IllegalArgumentException(state.id + " is a invalid photographer id");
+ }
+
+ MinecraftServer server = MinecraftServer.getServer();
+
+ ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle();
+ GameProfile profile = new GameProfile(UUID.randomUUID(), state.id);
+
+ ServerPhotographer photographer = new ServerPhotographer(server, world, profile);
+ photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id));
+ photographer.saveFile = new File("replay", state.id + ".mcpr");
+ photographer.createState = state;
+
+ photographer.recorder.start();
+ MinecraftServer.getServer().getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world);
+ photographer.serverLevel().chunkSource.move(photographer);
+ photographer.setInvisible(true);
+ photographers.add(photographer);
+
+ LOGGER.info("Photographer " + state.id + " created");
+
+ return photographer;
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+ super.doTick();
+
+ tickCounter++;
+ if (tickCounter % 10 == 0) {
+ connection.resetPosition();
+ this.serverLevel().chunkSource.move(this);
+ }
+
+ if (this.followPlayer != null) {
+ if (this.getCamera() == this || this.getCamera().level() != this.level()) {
+ this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation());
+ this.setCamera(followPlayer);
+ } else if (lastPos.distanceToSqr(this.position()) > 1024D) {
+ this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation());
+ }
+ }
+
+ lastPos = this.position();
+ }
+
+ @Override
+ public void die(@NotNull DamageSource damageSource) {
+ super.die(damageSource);
+ remove(true);
+ }
+
+ @Override
+ public boolean isInvulnerableTo(@NotNull ServerLevel world, @NotNull DamageSource damageSource) {
+ return true;
+ }
+
+ @Override
+ public boolean hurtServer(@NotNull ServerLevel world, @NotNull DamageSource source, float amount) {
+ return false;
+ }
+
+ @Override
+ public void setHealth(float health) {
+ }
+
+ @NotNull
+ @Override
+ public ServerStatsCounter getStats() {
+ return stats;
+ }
+
+ public void remove(boolean async) {
+ this.remove(async, true);
+ }
+
+ public void remove(boolean async, boolean save) {
+ super.remove(RemovalReason.KILLED);
+ photographers.remove(this);
+ this.recorder.stop();
+ this.server.getPlayerList().removePhotographer(this);
+
+ LOGGER.info("Photographer " + createState.id + " removed");
+
+ if (!recorder.isSaved()) {
+ CompletableFuture future = recorder.saveRecording(saveFile, save);
+ if (!async) {
+ future.join();
+ }
+ }
+ }
+
+ public void setFollowPlayer(ServerPlayer followPlayer) {
+ this.setCamera(followPlayer);
+ this.followPlayer = followPlayer;
+ }
+
+ public void setSaveFile(File saveFile) {
+ this.saveFile = saveFile;
+ }
+
+ public void pauseRecording() {
+ this.recorder.pauseRecording();
+ }
+
+ public void resumeRecording() {
+ this.recorder.resumeRecording();
+ }
+
+ public static ServerPhotographer getPhotographer(String id) {
+ for (ServerPhotographer photographer : photographers) {
+ if (photographer.createState.id.equals(id)) {
+ return photographer;
+ }
+ }
+ return null;
+ }
+
+ public static ServerPhotographer getPhotographer(UUID uuid) {
+ for (ServerPhotographer photographer : photographers) {
+ if (photographer.getUUID().equals(uuid)) {
+ return photographer;
+ }
+ }
+ return null;
+ }
+
+ public static List getPhotographers() {
+ return photographers;
+ }
+
+ public Photographer getBukkitPlayer() {
+ return getBukkitEntity();
+ }
+
+ @Override
+ @NotNull
+ public CraftPhotographer getBukkitEntity() {
+ return (CraftPhotographer) super.getBukkitEntity();
+ }
+
+ public static boolean isCreateLegal(@NotNull String name) {
+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) {
+ return false;
+ }
+
+ return Bukkit.getPlayerExact(name) == null && ServerPhotographer.getPhotographer(name) == null;
+ }
+
+ public static class PhotographerCreateState {
+
+ public RecorderOption option;
+ public Location loc;
+ public final String id;
+
+ public PhotographerCreateState(Location loc, String id, RecorderOption option) {
+ this.loc = loc;
+ this.id = id;
+ this.option = option;
+ }
+
+ public ServerPhotographer createSync() {
+ try {
+ return createPhotographer(this);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/replay/ServerPhotographerGameMode.java b/folia-server/src/main/java/dev/folia/replay/ServerPhotographerGameMode.java
new file mode 100644
index 0000000000..3d7c6a8ec0
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/replay/ServerPhotographerGameMode.java
@@ -0,0 +1,35 @@
+package dev.folia.replay;
+
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.level.GameType;
+import org.bukkit.event.player.PlayerGameModeChangeEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class ServerPhotographerGameMode extends ServerPlayerGameMode {
+
+ public ServerPhotographerGameMode(ServerPhotographer photographer) {
+ super(photographer);
+ super.setGameModeForPlayer(GameType.SPECTATOR, null);
+ }
+
+ @Override
+ public boolean changeGameModeForPlayer(@NotNull GameType gameMode) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) {
+ return null;
+ }
+
+ @Override
+ protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) {
+ }
+
+ @Override
+ public void tick() {
+ }
+}
diff --git a/folia-server/src/main/java/dev/folia/util/UUIDSerializer.java b/folia-server/src/main/java/dev/folia/util/UUIDSerializer.java
new file mode 100644
index 0000000000..e205437be9
--- /dev/null
+++ b/folia-server/src/main/java/dev/folia/util/UUIDSerializer.java
@@ -0,0 +1,17 @@
+package dev.folia.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Type;
+import java.util.UUID;
+
+public class UUIDSerializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(src.toString());
+ }
+}
diff --git a/git-wrapper.bat b/git-wrapper.bat
new file mode 100644
index 0000000000..701d479b4b
--- /dev/null
+++ b/git-wrapper.bat
@@ -0,0 +1,10 @@
+@echo off
+REM Workaround for paperweight: git config commit.gpgSign false fails with exit 128 in some work dirs.
+REM If this is that command, skip it (global gpgsign is already false) and exit 0.
+set "ARGS=%*"
+echo %ARGS% | findstr /C:"config" | findstr /C:"gpgSign" | findstr /C:"false" >nul
+if %errorlevel% equ 0 exit /b 0
+REM Call real git (avoid recursion: remove this script's dir from PATH)
+set "PATH=%PATH:%~dp0=%;%PATH:%~dp0;=%;"
+"%ProgramFiles%\Git\cmd\git.exe" %*
+exit /b %errorlevel%
diff --git a/git.bat b/git.bat
new file mode 100644
index 0000000000..88662547fd
--- /dev/null
+++ b/git.bat
@@ -0,0 +1,14 @@
+@echo off
+REM Workaround for paperweight: "git config commit.gpgSign false" fails with exit 128 in some work dirs.
+REM Skip that command (global gpgsign is already false); otherwise call real git.
+set "ARGS=%*"
+echo %ARGS% | findstr /i "gpgSign" >nul 2>&1
+if %errorlevel% equ 0 (
+ echo %ARGS% | findstr /i "config" >nul 2>&1
+ if %errorlevel% equ 0 exit /b 0
+)
+set "GITEXE=%ProgramFiles%\Git\cmd\git.exe"
+if not exist "%GITEXE%" set "GITEXE=%ProgramFiles(x86)%\Git\cmd\git.exe"
+if not exist "%GITEXE%" set "GITEXE=git.exe"
+"%GITEXE%" %*
+exit /b %errorlevel%