Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
00b6d47
Mark Bukkit plugin as Folia-supported
Chwitst187 Feb 25, 2026
d116c46
Merge pull request #1 from Chwitst187/codex/fix-fastasyncworldedit-pl…
Chwitst187 Feb 25, 2026
5bb84cf
Handle Folia scheduler UnsupportedOperationException
Chwitst187 Feb 25, 2026
0d88dad
Merge pull request #2 from Chwitst187/codex/fix-fastasyncworldedit-en…
Chwitst187 Feb 25, 2026
b3b8b14
Use reflection-based tick lookup in paperweight adapters
Chwitst187 Feb 25, 2026
2824e5e
Merge pull request #3 from Chwitst187/codex/integrate-minecraftreflec…
Chwitst187 Feb 25, 2026
4a6998c
Fix Folia task scheduling for zero-delay and async fallback
Chwitst187 Feb 25, 2026
16cd4ed
Merge pull request #4 from Chwitst187/codex/fix-fastasyncworldedit-en…
Chwitst187 Feb 25, 2026
83c692e
Avoid Folia deadlock when giving wand item
Chwitst187 Feb 25, 2026
a71a8a5
Merge pull request #5 from Chwitst187/codex/investigate-folia-watchdo…
Chwitst187 Feb 25, 2026
9960dd5
Fix main-thread detection for Folia global scheduler
Chwitst187 Feb 25, 2026
7b6a69e
Merge pull request #6 from Chwitst187/codex/fix-thread-error-in-fasta…
Chwitst187 Feb 25, 2026
960c994
Fix Folia chunk ticket scheduling in 1.21.11 adapter
Chwitst187 Feb 25, 2026
a09dd3f
Merge pull request #7 from Chwitst187/codex/fix-unsupportedoperatione…
Chwitst187 Feb 25, 2026
50cce78
Fix Folia unsupported execute call when sending chunk packets
Chwitst187 Feb 25, 2026
205ff45
Merge pull request #8 from Chwitst187/codex/fix-unsupportedoperatione…
Chwitst187 Feb 25, 2026
41f653d
Fix Folia NPE when reading tile entities during copy
Chwitst187 Feb 25, 2026
b5c9bf4
Merge pull request #9 from Chwitst187/codex/fix-nullpointerexception-…
Chwitst187 Feb 25, 2026
1a222b6
Update default prefix to purple FastAsyncWorldEdit
Chwitst187 Feb 26, 2026
c0af25b
Merge pull request #10 from Chwitst187/codex/rename-plugin-prefix-to-…
Chwitst187 Feb 26, 2026
fee5ef6
Fix Folia-unsafe sync bridges for entity and command block access
Chwitst187 Feb 26, 2026
5660449
Merge pull request #11 from Chwitst187/codex/scan-repository-for-foli…
Chwitst187 Feb 26, 2026
0bdb250
Improve Folia thread safety for Bukkit entity operations
Chwitst187 Feb 26, 2026
fdfb2a1
Merge pull request #12 from Chwitst187/codex/scan-plugin-for-folia-is…
Chwitst187 Feb 26, 2026
523c2e5
Handle Folia world data null when applying tile entities
Chwitst187 Feb 26, 2026
58f70c9
Merge pull request #13 from Chwitst187/codex/fix-nullpointerexception…
Chwitst187 Feb 26, 2026
67da7df
Prefix Change
Chwitst187 Feb 26, 2026
a306385
Fix large clipboard file mapping overflow
Chwitst187 Feb 28, 2026
8eb0afb
Merge pull request #14 from Chwitst187/codex/fix-clipboard-loading-si…
Chwitst187 Feb 28, 2026
d97640d
Merge branch 'IntellectualSites:main' into main
Chwitst187 Mar 1, 2026
687fa8b
Guard regen pollTask on Folia global scheduler
Chwitst187 Mar 8, 2026
47b4ef0
Merge pull request #15 from Chwitst187/codex/fix-nullpointerexception…
Chwitst187 Mar 8, 2026
4cd2495
build: pin paperweight to stable beta release
Chwitst187 Mar 8, 2026
e27a58d
Merge pull request #16 from Chwitst187/codex/fix-gradle-build-configu…
Chwitst187 Mar 8, 2026
e86d1a4
Fix reflective Folia task cancellation access
Chwitst187 Mar 8, 2026
572b814
Merge pull request #17 from Chwitst187/codex/fix-folia-task-cancellat…
Chwitst187 Mar 8, 2026
a338e43
Use teleportAsync for BukkitPlayer folia-safe teleports
Chwitst187 Mar 8, 2026
373c5b6
Merge pull request #18 from Chwitst187/codex/fix-unsupportedoperation…
Chwitst187 Mar 8, 2026
2bc053a
Fix Folia player teleportAsync fallback
Chwitst187 Mar 11, 2026
70e3105
Merge pull request #19 from Chwitst187/codex/fix-teleport-command-err…
Chwitst187 Mar 11, 2026
548afd6
Merge branch 'main' into main
Chwitst187 Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ junit = "6.0.3"
pluginyml = "0.6.0"
mod-publish-plugin = "1.1.0"
grgit = "5.3.3"
shadow = "9.4.1"
paperweight = "2.0.0-SNAPSHOT"
shadow = "9.3.2"
paperweight = "2.0.0-beta.19"
codecov = "0.2.0"


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.sk89q.worldedit.bukkit.adapter.impl.fawe;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Utility class that attempts to determine the current server tick via reflection.
* Returns {@code 0} on failures and caches discovered fields/methods by class.
*/
public final class MinecraftReflection {

private static final Logger LOGGER = Logger.getLogger("FAWE-MinecraftReflection");
private static final ConcurrentMap<Class<?>, Object> CACHE = new ConcurrentHashMap<>();

private static final List<String> FIELD_CANDIDATES = Arrays.asList(
"currentTick",
"tickCount",
"fullTick",
"fullTickCount",
"ticks",
"currentTicks",
"au",
"ac"
);

private static final List<String> METHOD_CANDIDATES = Arrays.asList(
"getCurrentTick",
"getTickCount",
"getFullTick",
"getTicks",
"getFullTickCount",
"getTick"
);

private MinecraftReflection() {
}

public static int getCurrentTick(Object serverOrClass) {
try {
Class<?> clazz = resolveClass(serverOrClass);
if (clazz == null) {
LOGGER.fine("MinecraftReflection: could not resolve MinecraftServer class");
return 0;
}

Object cached = CACHE.get(clazz);
if (cached instanceof Field) {
return readField((Field) cached, serverOrClass);
}
if (cached instanceof Method) {
return invokeMethod((Method) cached, serverOrClass);
}

for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
} catch (NoSuchFieldException ignored) {
} catch (Throwable t) {
LOGGER.log(Level.FINE, "Error while reading field '" + name + "'", t);
}
}

for (String methodName : METHOD_CANDIDATES) {
try {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
CACHE.put(clazz, method);
return invokeMethod(method, serverOrClass);
} catch (NoSuchMethodException ignored) {
} catch (Throwable t) {
LOGGER.log(Level.FINE, "Error while invoking method '" + methodName + "'", t);
}
}
} catch (Throwable t) {
LOGGER.log(Level.FINE, "MinecraftReflection unexpected error", t);
}

LOGGER.fine("MinecraftReflection: returning fallback tick 0");
return 0;
}

private static Class<?> resolveClass(Object serverOrClass) {
if (serverOrClass instanceof Class) {
return (Class<?>) serverOrClass;
}
if (serverOrClass != null) {
return serverOrClass.getClass();
}
try {
return Class.forName("net.minecraft.server.MinecraftServer");
} catch (ClassNotFoundException e) {
return null;
}
}

private static int readField(Field field, Object serverOrClass) {
try {
Object value;
if (Modifier.isStatic(field.getModifiers())) {
value = field.get(null);
} else {
if (serverOrClass instanceof Class || serverOrClass == null) {
return 0;
}
value = field.get(serverOrClass);
}

if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Long) {
long longValue = (Long) value;
return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, longValue));
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
} catch (IllegalAccessException | IllegalArgumentException e) {
LOGGER.log(Level.FINE, "Error while reading tick field", e);
}
return 0;
}

private static int invokeMethod(Method method, Object serverOrClass) {
try {
Object target = Modifier.isStatic(method.getModifiers()) ? null : serverOrClass;
if (target instanceof Class || target == null && !Modifier.isStatic(method.getModifiers())) {
return 0;
}

Object result = method.invoke(target);
if (result instanceof Integer) {
return (Integer) result;
}
if (result instanceof Long) {
long longValue = (Long) result;
return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, longValue));
}
if (result instanceof Number) {
return ((Number) result).intValue();
}
} catch (IllegalAccessException | InvocationTargetException e) {
LOGGER.log(Level.FINE, "Error while invoking tick method", e);
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fastasyncworldedit.core.util.TaskManager;
import com.fastasyncworldedit.core.util.task.RunnableVal;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.bukkit.adapter.impl.fawe.MinecraftReflection;
import com.sk89q.worldedit.internal.block.BlockStateIdAccess;
import com.sk89q.worldedit.internal.wna.WorldNativeAccess;
import com.sk89q.worldedit.util.SideEffect;
Expand Down Expand Up @@ -58,7 +59,7 @@ public PaperweightFaweWorldNativeAccess(PaperweightFaweAdapter paperweightFaweAd
this.level = level;
// Use the actual tick as minecraft-defined so we don't try to force blocks into the world when the server's already lagging.
// - With the caveat that we don't want to have too many cached changed (1024) so we'd flush those at 1024 anyway.
this.lastTick = new AtomicInteger(MinecraftServer.currentTick);
this.lastTick = new AtomicInteger(MinecraftReflection.getCurrentTick(MinecraftServer.class));
}

private Level getLevel() {
Expand Down Expand Up @@ -94,7 +95,7 @@ public synchronized net.minecraft.world.level.block.state.BlockState setBlockSta
LevelChunk levelChunk, BlockPos blockPos,
net.minecraft.world.level.block.state.BlockState blockState
) {
int currentTick = MinecraftServer.currentTick;
int currentTick = MinecraftReflection.getCurrentTick(MinecraftServer.class);
if (Fawe.isMainThread()) {
return levelChunk.setBlockState(blockPos, blockState,
this.sideEffectSet != null && this.sideEffectSet.shouldApply(SideEffect.UPDATE)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.sk89q.worldedit.bukkit.adapter.impl.fawe;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Utility class that attempts to determine the current server tick via reflection.
* Returns {@code 0} on failures and caches discovered fields/methods by class.
*/
public final class MinecraftReflection {

private static final Logger LOGGER = Logger.getLogger("FAWE-MinecraftReflection");
private static final ConcurrentMap<Class<?>, Object> CACHE = new ConcurrentHashMap<>();

private static final List<String> FIELD_CANDIDATES = Arrays.asList(
"currentTick",
"tickCount",
"fullTick",
"fullTickCount",
"ticks",
"currentTicks",
"au",
"ac"
);

private static final List<String> METHOD_CANDIDATES = Arrays.asList(
"getCurrentTick",
"getTickCount",
"getFullTick",
"getTicks",
"getFullTickCount",
"getTick"
);

private MinecraftReflection() {
}

public static int getCurrentTick(Object serverOrClass) {
try {
Class<?> clazz = resolveClass(serverOrClass);
if (clazz == null) {
LOGGER.fine("MinecraftReflection: could not resolve MinecraftServer class");
return 0;
}

Object cached = CACHE.get(clazz);
if (cached instanceof Field) {
return readField((Field) cached, serverOrClass);
}
if (cached instanceof Method) {
return invokeMethod((Method) cached, serverOrClass);
}

for (String name : FIELD_CANDIDATES) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
CACHE.put(clazz, field);
return readField(field, serverOrClass);
} catch (NoSuchFieldException ignored) {
} catch (Throwable t) {
LOGGER.log(Level.FINE, "Error while reading field '" + name + "'", t);
}
}

for (String methodName : METHOD_CANDIDATES) {
try {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
CACHE.put(clazz, method);
return invokeMethod(method, serverOrClass);
} catch (NoSuchMethodException ignored) {
} catch (Throwable t) {
LOGGER.log(Level.FINE, "Error while invoking method '" + methodName + "'", t);
}
}
} catch (Throwable t) {
LOGGER.log(Level.FINE, "MinecraftReflection unexpected error", t);
}

LOGGER.fine("MinecraftReflection: returning fallback tick 0");
return 0;
}

private static Class<?> resolveClass(Object serverOrClass) {
if (serverOrClass instanceof Class) {
return (Class<?>) serverOrClass;
}
if (serverOrClass != null) {
return serverOrClass.getClass();
}
try {
return Class.forName("net.minecraft.server.MinecraftServer");
} catch (ClassNotFoundException e) {
return null;
}
}

private static int readField(Field field, Object serverOrClass) {
try {
Object value;
if (Modifier.isStatic(field.getModifiers())) {
value = field.get(null);
} else {
if (serverOrClass instanceof Class || serverOrClass == null) {
return 0;
}
value = field.get(serverOrClass);
}

if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Long) {
long longValue = (Long) value;
return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, longValue));
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
} catch (IllegalAccessException | IllegalArgumentException e) {
LOGGER.log(Level.FINE, "Error while reading tick field", e);
}
return 0;
}

private static int invokeMethod(Method method, Object serverOrClass) {
try {
Object target = Modifier.isStatic(method.getModifiers()) ? null : serverOrClass;
if (target instanceof Class || target == null && !Modifier.isStatic(method.getModifiers())) {
return 0;
}

Object result = method.invoke(target);
if (result instanceof Integer) {
return (Integer) result;
}
if (result instanceof Long) {
long longValue = (Long) result;
return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, longValue));
}
if (result instanceof Number) {
return ((Number) result).intValue();
}
} catch (IllegalAccessException | InvocationTargetException e) {
LOGGER.log(Level.FINE, "Error while invoking tick method", e);
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fastasyncworldedit.core.util.TaskManager;
import com.fastasyncworldedit.core.util.task.RunnableVal;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.bukkit.adapter.impl.fawe.MinecraftReflection;
import com.sk89q.worldedit.internal.block.BlockStateIdAccess;
import com.sk89q.worldedit.internal.wna.WorldNativeAccess;
import com.sk89q.worldedit.util.SideEffect;
Expand Down Expand Up @@ -58,7 +59,7 @@ public PaperweightFaweWorldNativeAccess(PaperweightFaweAdapter paperweightFaweAd
this.level = level;
// Use the actual tick as minecraft-defined so we don't try to force blocks into the world when the server's already lagging.
// - With the caveat that we don't want to have too many cached changed (1024) so we'd flush those at 1024 anyway.
this.lastTick = new AtomicInteger(MinecraftServer.currentTick);
this.lastTick = new AtomicInteger(MinecraftReflection.getCurrentTick(MinecraftServer.class));
}

private Level getLevel() {
Expand Down Expand Up @@ -94,7 +95,7 @@ public synchronized net.minecraft.world.level.block.state.BlockState setBlockSta
LevelChunk levelChunk, BlockPos blockPos,
net.minecraft.world.level.block.state.BlockState blockState
) {
int currentTick = MinecraftServer.currentTick;
int currentTick = MinecraftReflection.getCurrentTick(MinecraftServer.class);
if (Fawe.isMainThread()) {
return levelChunk.setBlockState(blockPos, blockState,
this.sideEffectSet != null && this.sideEffectSet.shouldApply(SideEffect.UPDATE)
Expand Down
Loading
Loading