Skip to content

Use reflection-based tick lookup across Paperweight FAWE adapters#3

Merged
Chwitst187 merged 1 commit intomainfrom
codex/integrate-minecraftreflection-for-folia-compatibility
Feb 25, 2026
Merged

Use reflection-based tick lookup across Paperweight FAWE adapters#3
Chwitst187 merged 1 commit intomainfrom
codex/integrate-minecraftreflection-for-folia-compatibility

Conversation

@Chwitst187
Copy link
Copy Markdown
Owner

Motivation

  • Make FAWE Paperweight adapters robust to server variants (Folia/obfuscated builds) by avoiding a hard-coded reference to MinecraftServer.currentTick.
  • Provide a single, reusable reflective mechanism to obtain the current server tick with per-class caching and safe fallback behavior.

Description

  • Add a new MinecraftReflection utility under com.sk89q.worldedit.bukkit.adapter.impl.fawe in each Paperweight adapter module (1_20_2, 1_20_4, 1_20_5, 1_21, 1_21_4, 1_21_5, 1_21_6, 1_21_9, 1_21_11) that searches for tick fields/methods, caches the discovered Field/Method, and returns 0 on failure.
  • Update all PaperweightFaweWorldNativeAccess implementations to import and use MinecraftReflection.getCurrentTick(MinecraftServer.class) for initial lastTick and per-call currentTick checks instead of directly accessing MinecraftServer.currentTick.
  • Ensure the reflective lookup supports both static fields and no-arg methods and converts numeric results to int with clamping.

Testing

  • Verified there are no remaining direct references with rg "MinecraftServer\.currentTick" worldedit-bukkit/adapters -n which returned no matches.
  • Attempted to compile target adapter modules with ./gradlew :worldedit-bukkit:adapters:adapter-1_21_6:compileJava :worldedit-bukkit:adapters:adapter-1_20_2:compileJava --no-daemon but the build run failed in this environment due to an unrelated multi-project configuration error (Task with name 'build' not found in project ':worldedit-libs:cli').
  • Created and adjusted source files across the listed adapter modules and validated imports/usage changes via local rg and file diffs; source-level changes are consistent and compile-ready when the overall project configuration is healthy.

Codex Task

Copilot AI review requested due to automatic review settings February 25, 2026 20:02
@Chwitst187 Chwitst187 merged commit 2824e5e into main Feb 25, 2026
1 check passed
@Chwitst187 Chwitst187 deleted the codex/integrate-minecraftreflection-for-folia-compatibility branch February 25, 2026 20:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces direct MinecraftServer.currentTick access in FAWE Paperweight adapters with a reflective tick lookup intended to be more robust across server variants/obfuscation.

Changes:

  • Add MinecraftReflection utility that reflectively discovers a tick field/method and caches it.
  • Update each PaperweightFaweWorldNativeAccess to use MinecraftReflection.getCurrentTick(MinecraftServer.class) for tick comparisons and initialization.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility
worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweWorldNativeAccess.java Switch tick reads from direct field access to reflective lookup
worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java New reflective tick lookup + caching utility

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +55
Object cached = CACHE.get(clazz);
if (cached instanceof Field) {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no compatible tick member is found (or reflection is blocked), this method will re-scan all candidates on every call. Since getCurrentTick() is called in hot paths (e.g., per setBlockState), consider caching a negative/sentinel value per class after the first failed lookup to avoid repeated reflective lookups and logging overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +67
for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When getCurrentTick() is called with a Class (as in the adapters: getCurrentTick(MinecraftServer.class)), this loop can discover a non-static candidate field (e.g. tickCount) and cache it before verifying it can be read without an instance. readField() will then return 0, and because the unusable Field is cached, future calls will keep returning 0 even if a later static candidate (field or method) exists. Only consider/cache static members when the input is a Class (or only cache after a successful read), and continue searching if the member isn’t usable with the provided target.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
package com.sk89q.worldedit.bukkit.adapter.impl.fawe;

import java.lang.reflect.Field;
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility class is added with the same fully-qualified name (com.sk89q.worldedit.bukkit.adapter.impl.fawe.MinecraftReflection) in every adapter subproject. Since the Bukkit build shades all adapter subprojects into the final jar, this creates duplicate class entries where whichever adapter is packaged last “wins”, which is brittle if the implementations ever diverge. Consider moving this class to :worldedit-bukkit (adapters already depend on it) or to a shared adapters-common module so it exists exactly once.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants